JWT verification was adopted in the login verification part of the recent project. And since the adoption of the Spring Boot framework, authentication and permission management parts, it is natural to use Spring Security. Here’s the implementation.

Before the project adopts JWT scheme, it is necessary to understand its characteristics and application scenarios. After all, there is no silver bullet in software engineering. Only the right scene, no essential oil solution.

In short, JWT can carry non-sensitive information and is immutable. You can verify whether it has been tampered with and read the content of the information to complete the network authentication of three questions: “who are you”, “what permissions do you have”, “is not fake”. For security, you need to use the Https protocol, and you must be careful not to leak the key used for encryption.

In JWT authentication mode, the server does not store user status information and cannot be discarded within the validity period. After the validity period expires, you need to create a new one to replace it. Therefore, it is not suitable for long-term state retention, not suitable for scenarios where users need to be kicked off, and not suitable for scenarios where user information needs to be changed frequently. To solve these problems, it is always necessary to query the database or cache, or repeatedly encrypt and decrypt, so it is better to use Session directly. However, as a short time switch between services, or very suitable, such as OAuth and so on.

Target function point

  • Enter the user name and password to log in.
    • After successful authentication, the server generates a JWT authentication token and returns it to the client.
    • An error message is returned after authentication failed.
    • The client carries the JWT with each request to access the interface within the permissions.
  • Verify token validity and permissions on each request and throw 401 unauthorized error if no valid token is available.
  • When it is found that the token is about to expire, it returns a status code to request a new token.

The preparatory work

Introducing Maven dependencies

For this implementation of login authentication, three packages Spring Security, Jackson, and Java-JWT need to be introduced.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.12.1</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.12.1</version>
</dependency>
Copy the code

Configure the DAO data layer

Before authenticating a user, you naturally create a user entity object and get the user’s service class. The difference is that these two classes need to implement Spring Security’s interfaces in order to integrate them into the validation framework.

User

The user entity class implements the “UserDetails” interface, which requires the methods getUsername, getPassword, and getAuthorities to obtain the user name, password, and permissions. IsAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, and isEnabled all return true because they are not related to authentication. Lombok is used here for convenience.

@Data
public class User implements UserDetails {

  private static final long serialVersionUID = 1L;

  private String username;

  private String password;

  privateCollection<? extends GrantedAuthority> authorities; . }Copy the code

UserService

The user service class needs to implement the “UserDetailsService” interface, which is very simple and only needs to implement such a method loadUserByUsername(String Username). MyBatis is used here to connect to the database for user information.

@Service
public class UserService implements UserDetailsService {
  
  @Autowired
  UserMapper userMapper;

  @Override
  @Transactional
  public User loadUserByUsername(String username) {
      returnuserMapper.getByUsername(username); }... }Copy the code

Create the JWT utility class

This utility class is responsible for the generation, validation, and value of tokens.

@Component
public class JwtTokenProvider {

  private static final long JWT_EXPIRATION = 5 * 60 * 1000L; // Expire in 5 minutes

  public static final String TOKEN_PREFIX = "Bearer "; // Token starting string

  private String jwtSecret = "XXX key. Never tell anyone."; . }Copy the code

Generate JWT: Obtain the user information from the authenticated authentication object and generate the token with the specified encryption mode and expiration time. We simply add the user name to the token:

public String generateToken(Authentication authentication) {
    User userPrincipal = (User) authentication.getPrincipal(); // Get the user object
    Date expireDate = new Date(System.currentTimeMillis() + JWT_EXPIRATION); // Set the expiration time
    try {
        Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // Specify the encryption mode
        return JWT.create().withExpiresAt(expireDate).withClaim("username", userPrincipal.getUsername()) 
                .sign(algorithm); / / issue JWT
    } catch (JWTCreationException jwtCreationException) {
        return null; }}Copy the code

Verify JWT: Specifies the same encryption method used to verify that the token is issued by the server, tampered with, or expired.

public boolean validateToken(String authToken) {
    try {
        Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // Consistent with the issue
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(authToken);
        return true;
    } catch (JWTVerificationException jwtVerificationException) {
        return false; }}Copy the code

Obtain load information: Parse the user name information from the load part of the token, which is MD5-encoded and public information.

public String getUsernameFromJWT(String authToken) {
    try {
        DecodedJWT jwt = JWT.decode(authToken);
        return jwt.getClaim("username").asString();
    } catch (JWTDecodeException jwtDecodeException) {
        return null; }}Copy the code

The login

In the login section, you need to create three files: an interceptor that handles the login interface, and a class that handles login success or failure.

LoginFilter

Default comes with Spring Security login form, is responsible for handling this login validation process filter called “UsernamePasswordAuthenticationFilter”, but it only support form value, here use custom classes inherit it, allow it to support JSON values, Responsible for login authentication interface.

The interceptor just takes care of taking the value from the request, and Spring Security takes care of the validation.

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

  @Override
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
      if(! request.getMethod().equals("POST")) {
          throw new AuthenticationServiceException("Login interface method not supported:" + request.getMethod());
      }
      if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
          Map<String, String> loginData = new HashMap<>();
          try {
              loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
          } catch (IOException e) {
          }
          String username = loginData.get(getUsernameParameter());
          String password = loginData.get(getPasswordParameter());
          if (username == null) {
              username = "";
          }
          if (password == null) {
              password = "";
          }
          username = username.trim();
          UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,
                  password);
          setDetails(request, authRequest);
          return this.getAuthenticationManager().authenticate(authRequest);
      } else {
          return super.attemptAuthentication(request, response); }}}Copy the code

LoginSuccessHandler

Responsible for generating JWT to the front end after successful login.

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        ResponseData responseData = new ResponseData();
        String token = jwtTokenProvider.generateToken(authentication);
        responseData.setData(JwtTokenProvider.TOKEN_PREFIX + token);
        response.setContentType("application/json; charset=utf-8");
        ObjectMapper mapper = newObjectMapper(); mapper.writeValue(response.getWriter(), responseData); }}Copy the code

LoginFailureHandler

An error message is returned after the authentication fails.

@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json; charset=utf-8");
        ResponseData respBean = setResponseData(exception);
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(response.getWriter(), respBean);
    }

    private ResponseData setResponseData(AuthenticationException exception) {
        if (exception instanceof LockedException) {
            return ResponseData.build("User has been locked.");
        } else if (exception instanceof CredentialsExpiredException) {
            return ResponseData.build("Password has expired");
        } else if (exception instanceof AccountExpiredException) {
            return ResponseData.build("User name has expired");
        } else if (exception instanceof DisabledException) {
            return ResponseData.build("Account unavailable");
        } else if (exception instanceof BadCredentialsException) {
            return ResponseData.build("Verification failed");
        }
        return ResponseData.build("Login failed. Please contact your administrator."); }}Copy the code

validation

After a successful login, the front end carries a signed JWT with each request, allowing the server to identify the logged user.

Also, if the JWT is not carried, or the token carried is expired, or illegal, an error message is returned with a separate processing class.

JwtAuthenticationFilter

On each request, the JWT in the request header is parsed to retrieve user information and generate validation objects to pass to the next filter.

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider jwtProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);
            UsernamePasswordAuthenticationToken authentication = verifyToken(jwt);
            if(authentication ! =null) {
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            }
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } catch (Exception e) {
            logger.error("Unable to set user authentication object for Security context", e);
        }

        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken == null| |! bearerToken.startsWith(JwtTokenProvider.TOKEN_PREFIX)) { logger.info("Request header does not contain JWT token, call next filter");
            return null;
        }

        return bearerToken.split("") [1].trim();
    }

    // Verify the token and generate the authenticated token
    private UsernamePasswordAuthenticationToken verifyToken(String token) {
        if (token == null) {
            return null;
        }
        // Authentication failed. Null is returned
        if(! jwtProvider.validateToken(token)) {return null;
        }
        // Extract the user name
        String username = jwtProvider.getUsernameFromJWT(token);
        UserDetails userDetails = new User(username);

        // Build an authenticated token
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); }}Copy the code

AuthenticationEntryPoint

This class is simpler, but returns a 401 response with an error message if the validation fails.

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
  
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        logger.error("Verification passed. Message - {}", authException.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); }}Copy the code

Centralized configuration

The functionality of Spring Security is implemented through a chain of filters, and the entire configuration of Spring Security is unified in a single class.

Now let’s create the class to inherit from “WebSecurityConfigurerAdapter”, the above prepared various files, configuration in one by one.

The first step is to set up global Spring Security functionality to be turned on through annotations, and introduce the newly created classes through dependency injection.

@Configuration
@EnableWebSecurity
public class KanpmSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
    UserDetailsService userDetailsService;

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public LoginFilter loginFilter(LoginSuccessHandler loginSuccessHandler, LoginFailureHandler loginFailureHandler)
            throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
        loginFilter.setAuthenticationFailureHandler(loginFailureHandler);
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setFilterProcessesUrl("/auth/login");
        return loginFilter;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean(a) throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder(a) {
        return newBCryptPasswordEncoder(); }... }Copy the code

Next, configure the user acquisition service classes and encryption methods into Spring Security so that it knows how to authenticate logins.

@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
Copy the code

Finally, the JWT filter in the filter chain, use a custom login filter to replace the default “UsernamePasswordAuthenticationFilter”, complete functions.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable().anyRequest().authenticated().and()
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler);

    http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
    .addFilterAt(loginFilter(new LoginSuccessHandler(), new LoginFailureHandler()),
            UsernamePasswordAuthenticationFilter.class);
}
Copy the code

Source reference