User login authentication is a very common service in Web applications. The general process is as follows:

  • The client sends the user name and password to the server
  • After the authentication succeeds on the server, relevant data, such as login time and LOGIN IP address, is saved in the current session.
  • The server returns a session_ID to the client, which the client stores in a Cookie.
  • Session_id is passed back to the server when the client makes a request to the server.
  • After obtaining the session_ID, the server authenticates the user.

This mode is fine on a single machine, but it can be painful for Web applications with separate front and back ends. Instead of saving session data on the server, the session data is stored on the client, and the client sends this data to the server for validation each time it initiates a request. JWT (JSON Web Token) is a good example of this solution.

I. About JWT

JWT is one of the most popular cross-domain authentication solutions: the client initiates a user login request, the server receives and authenticates successfully, generates a JSON object (as shown below), and returns it to the client.

{
  "sub": "wanger",
  "created": 1645700436900,
  "exp": 1646305236
}
Copy the code

When the client communicates with the server again, it piggies this JSON object along as a credential of trust between the front and back ends. After receiving the request, the server authenticates the user with a JSON object, eliminating the need to store any session data.

If I now use username wanger and password 123456 to access the programming Meow (Codingmore) login interface, the actual JWT is a string that looks like it’s been encrypted.

I have copied it to the JWT website for a clearer view.

The Encoded part on the left is JWT ciphertext, with “. Split into three parts (Decoded part on the right) :

  • Header, which describes the metadata of the JWT, wherealgAttribute represents the signature algorithm (currently HS512);
  • The Payload is used to store data that needs to be transmittedsubProperty represents the subject (the actual value is the username),createdProperty represents the time when JWT was generated,expProperty represents the expiration time
  • The Signature of the first two parts prevents data tampering. Here we need the server to specify a key (known only to the server) that cannot be disclosed to the client, and then use the signature algorithm specified in the Header to generate the signature as follows:
HMACSHA512(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)
Copy the code

After calculating the Signature, combine the Header, Payload, and Signature into a string. Split, and you can return it to the client.

After the client gets the JWT, it can be placed in localStorage or Cookie.

const TokenKey = '1D596CD8-8A20-4CEC-98DD-CDC12282D65C' // createUuid()

export function getToken () {
  return Cookies.get(TokenKey)
}

export function setToken (token) {
  return Cookies.set(TokenKey, token)
}
Copy the code

When the client communicates with the server in the future, it will carry this JWT, which is generally placed in the Authorization field of HTTP request header information.

Authorization: Bearer <token>
Copy the code

After receiving the request, the server verifies the JWT and returns the corresponding resource if the verification is successful.

Second, actual combat JWT

The first step is to add the JWT dependencies to the POM.xml file.

<dependency> <groupId> IO. Jsonwebtoken </groupId> <artifactId> JJWT </artifactId> <version>0.9.0</version> </dependency>Copy the code

Second, add JWT configuration items to application.yml.

Secret: codingmore-admin-secret #JWT encryption/decrypting key expiration: 604800 #JWT out-of-date time (60*60*24*7) tokenHead: 'Bearer '#JWT load received in the leadCopy the code

Step 3: Create a new jWTToKenUtil.java utility class with three main methods:

  • generateToken(UserDetails userDetails): Generates tokens based on login users
  • getUserNameFromToken(String token): Obtains the login user from the token
  • validateToken(String token, UserDetails userDetails): Checks whether the token is still valid
public class JwtTokenUtil {

    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    /** * Generate token */ based on user information
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /** * Generate JWT token */ based on user name and creation time
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /** * Obtain the login user name from the token */
    public String getUserNameFromToken(String token) {
        String username = null;
        Claims claims = getClaimsFromToken(token);
        if(claims ! =null) {
            username = claims.getSubject();
        }

        return username;
    }

    /** * get the payload in JWT from token */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT format validation failed :{}", token);
        }
        return claims;
    }

    /** * Verify that the token is still valid **@paramToken Indicates the token * passed by the client@paramUserDetails User information queried from the database */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        returnusername.equals(userDetails.getUsername()) && ! isTokenExpired(token); }/** * Check whether the token is invalid */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /** * Get expiration time from token */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        returnclaims.getExpiration(); }}Copy the code

Fourth, add the login login interface to usersController.java, receive the username and password, and return the JWT to the client.

@Controller
@ Api (tags = "user")
@RequestMapping("/users")
public class UsersController {
    @Autowired
    private IUsersService usersService;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @apiOperation (value = "return token after login ")
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
        String token = usersService.login(users.getUserLogin(), users.getUserPass());

        if (token == null) {
            return ResultObject.validateFailed("Wrong username or password");
        }

        // Pass JWT back to the client
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        returnResultObject.success(tokenMap); }}Copy the code

Step 5 Add the login method in UsersServiceImpl. Java to query the user from the database based on the user name. After the password is verified, the JWT is generated.

@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper.Users> implements IUsersService {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    public String login(String username, String password) {
        String token = null;
        // The password needs to be encrypted by the client
        try {
            // Query user + user resources
            UserDetails userDetails = loadUserByUsername(username);

            // Verify password
            if(! passwordEncoder.matches(password, userDetails.getPassword())) { Asserts.fail("Incorrect password");
            }

            / / returns the JWT
            token = jwtTokenUtil.generateToken(userDetails);
        } catch (AuthenticationException e) {
            LOGGER.warn("Login exception :{}", e.getMessage());
        }
        returntoken; }}Copy the code

Step 6, new JwtAuthenticationTokenFilter. Java, each time the client requested to verify the JWT.

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // Get the JWT from the client request
        String authHeader = request.getHeader(this.tokenHeader);
        // The JWT is our specified format, starting with tokenHead
        if(authHeader ! =null && authHeader.startsWith(this.tokenHead)) {
            // The part after "Bearer "
            String authToken = authHeader.substring(this.tokenHead.length());
            // Get the user name from JWT
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("checking username:{}", username);

            // SecurityContextHolder is a utility class for SpringSecurity
            // Save the security context of the current user in the application
            if(username ! =null && SecurityContextHolder.getContext().getAuthentication() == null) {
                // Obtain the login user information based on the user name
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                // Verify that the token expires
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    // Save the logged-in user to the security context
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
                            null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                    LOGGER.info("authenticated user:{}", username); } } } chain.doFilter(request, response); }}Copy the code

JwtAuthenticationTokenFilter inherited OncePerRequestFilter, the filter can ensure a request by a filter only, without the need to repeat. That is, the filter is executed every time the client initiates a request.

This filter is really critical, and I’ve commented almost every line of code, but of course, just to make sure you know exactly what this class does, let me draw a flow diagram just so it’s clear.

SpringSecurity is a security management framework that seamlessly integrates with Spring Boot applications. SecurityContextHolder is a key tool class that holds security context information, including who the current user is and whether the user has been authenticated. Key information, such as user permissions.

SecurityContextHolder by default uses a ThreadLocal policy to store authentication information. A ThreadLocal policy allows the thread to access the data stored in it. This means that different requests will be processed by different threads when they enter the server. For example, Thread A stores the user information of request 1 into ThreadLocal, while Thread B cannot obtain the user information when it processes request 2.

In each request would come so JwtAuthenticationTokenFilter filter verified again JWT, ensure that the client request is safe. SpringSecurity then releases the next request interface. This is the fundamental difference between JWT and Session:

  • JWT requires validation once per request, and as long as the JWT does not expire, authentication remains valid even if the server is restarted.
  • If a Session does not expire, you do not need to re-authenticate user information. After the server restarts, users need to re-log in to obtain a new Session.

In other words, in JWT scheme, the secret saved by the server must not be disclosed, otherwise the client can forge the user’s authentication information according to the signature algorithm.

JWT validation is added to Swagger

For back-end developers, how do you add JWT validation to Swagger (enhanced with Knife4j integration)?

First step, access the login interface, enter the user name and password to login, and obtain the JWT returned by the server.

In the second step, collect the tokenHead and token returned by the server and fill in the Authorize (note that there is a space between the tokenHead and token) to complete login authentication.

Third, Swagger automatically sends Authorization to the server as the request header when requesting another interface.

The fourth step, the server receives the request, through JwtAuthenticationTokenFilter filter to check the JWT.

That’s it. The whole process is through. Perfect!

Four,

In summary, JWT is very slippery for cross-domain authentication in front and back end separation projects. This is mainly due to the versatility of JSON, which can be supported across languages, JavaScript and Java. In addition, the COMPOSITION of JWT is very simple and very easy to transport; JWT also doesn’t need to keep Session information on the server, making it easy to scale.

Of course, to ensure the security of the JWT, do not store sensitive information in the JWT, because the JWT can be easily decrypted on the client if the private key is disclosed. If yes, use HTTPS.

Reference links:

Nguyen one: www.ruanyifeng.com/blog/2018/0… Spring, summer, autumn and winter: segmentfault.com/a/119000001… Jiangnan some rain: cloud.tencent.com/developer/a… Dearmadman:www.jianshu.com/p/576dbf44b… McArozheng:www.macrozheng.com/

Source path:

Github.com/itwanger/co…


This article has been published on GitHub’s 1.6K + Star. It is said that every good Java programmer likes her. It is humorous and easy to understand. The content includes Java foundation, Java concurrent programming, Java virtual machine, Java enterprise development, Java interview and other core knowledge points. Learn Java, look for Java programmers to advance the road 😄.

Github.com/itwanger/to…

Star the repository and you have the potential to become a good Java engineer. Click the link below to jump to the Java Programmer’s Path to Progress website and start your fun learning journey.

tobebetterjavaer.com/

Nothing keeps me here but purpose, and though there are roses by the shore, and trees by the shore, and still harbours, I am not tied.