The fourth in the Spring Security series uses JWT for authentication

chapter

Spring Security is a series of simple introduction and practical

The second part of the Spring Security series analyzes the authentication process

Spring Security series 3: Custom SMS Login Authentication

The fourth in the Spring Security series uses JWT for authentication

Spring Security series # 5: User authorization for backend decoupage projects

Spring Security Series 6 authorization Process Analysis

After the above custom SMS login authentication, we must be more familiar with the implementation of custom login.

We want to implement a custom login component in five steps:

  1. Custom filter, which takes the parameters from the request and assembles them into a token
  2. A user-defined provider parses the parameters in the sent tokens to verify the identity
  3. Create successful and failed logon callbacks
  4. Create a configuration class that combines the above classes
  5. Add the configuration class to the Spring Security configuration

With the above five steps in mind, there is no problem implementing any custom logins, so let’s implement JWT logins in the same order as above.

Login authentication

Custom login filter

Json method is generally used to pass parameters of the front and back separated items, instead of using the default form login, so we need to define a custom filter to obtain the username and password from the request:

public class JwtAuthenticationLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JwtAuthenticationLoginFilter(a) {
        super(new AntPathRequestMatcher("/jwtLogin"."POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        // Get username and password from JSON
        String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
        String username = "";
        String password = "";
        if(StringUtils.hasText(body)) {
            JsonObject jsonObject = new JsonParser().parse(body).getAsJsonObject();
            username = jsonObject.get("username").getAsString().trim();
            password = jsonObject.get("password").getAsString().trim();
        }
        // Encapsulate the commit into a token
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
        returngetAuthenticationManager().authenticate(authRequest); }}Copy the code

A custom provider

Actually think carefully, we use or user name password login here, didn’t say want to use other parameters calibration, so why not just use the system to provide DaoAuthenticationProvider, so don’t need to custom implementation again ~ ~ ~ ~ not because lazy, just skip ~

Login result callback

Whether the login succeeds or fails, the data needs to be passed to the front-end in JSON format, rather than being redirected by the back-end itself as before.

If the user is authenticated, then we need to generate a token to put into the header, so for the successful login callback:

@Slf4j
@Component
public class JwtAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private UserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        User user = (User) authentication.getPrincipal();
        String generateToken = userService.generateToken(user);
        response.setHeader("jwt-token",generateToken);
        log.info("Generate token and set to header: {}",generateToken);
        // The user information is displayed after successful login
        PrintWriter out = response.getWriter();
        user.setPassword("");
        String userJson = newGson().toJson(user, User.class); out.write(userJson); out.flush(); out.close(); }}Copy the code

Login failure also returns the failure cause:

@Slf4j
@Component
public class JwtAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {

    @Data
    @AllArgsConstructor
    private static class ExceptionResult{
        private int code;
        private String message;

    }
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
        resp.setContentType("application/json; charset=utf-8");
        log.error("User login failed, {}",e.getMessage());
        PrintWriter out = resp.getWriter();
        ExceptionResult respBean = new ExceptionResult(HttpStatus.UNAUTHORIZED.value(), e.getMessage());
        resp.setStatus(HttpStatus.UNAUTHORIZED.value());
        if (e instanceof LockedException) {
            respBean.setMessage("Account locked, please contact administrator!");
        } else if (e instanceof CredentialsExpiredException) {
            respBean.setMessage("Password expired, please contact administrator!");
        } else if (e instanceof AccountExpiredException) {
            respBean.setMessage("Account expired, please contact administrator!");
        } else if (e instanceof DisabledException) {
            respBean.setMessage("Account disabled, please contact administrator!");
        } else if (e instanceof BadCredentialsException) {
            respBean.setMessage("Wrong user name or password, please re-enter!");
        }
        String json = newGson().toJson(respBean,ExceptionResult.class); out.write(json); out.flush(); out.close(); }}Copy the code

Login Configuration Class

Use the system to provide DaoAuthenticationProvider, needs to be UserDetailsService and PasswordEncoder manually set, don’t ask why, ask is to start system complains:

@Component
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
    @Autowired
    private JwtAuthSuccessHandler jwtAuthSuccessHandler;
    @Autowired
    private JwtAuthFailHandler jwtAuthFailHandler;
    @Autowired
    private UserService userService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        JwtAuthenticationLoginFilter filter = new JwtAuthenticationLoginFilter();
        filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
        filter.setAuthenticationSuccessHandler(jwtAuthSuccessHandler);
        filter.setAuthenticationFailureHandler(jwtAuthFailHandler);
        DaoAuthenticationProvider provider = newDaoAuthenticationProvider(); provider.setUserDetailsService(userService); provider.setPasswordEncoder(passwordEncoder); builder.authenticationProvider(provider); builder.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

Add login authentication configuration to Security

Now that the above components are ready, let’s configure our security:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;
    @Autowired
    private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .disable()
                .apply(smsAuthenticationSecurityConfig)
                .and()
                .apply(jwtAuthenticationSecurityConfig)
                .and()
                // Set URL authorization
                .authorizeRequests()
                // The login page must be allowed
                .antMatchers("/login"."/verifyCode"."/smsLogin"."/failure"."/jwtLogin")
                .permitAll()
                // anyRequest() All requests authenticated() must be authenticated
                .anyRequest()
                .authenticated()
                .and()
                / / close CSRF
                .csrf().disable();
    }

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

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

test

Now we can start the project and verify with Postman, login interface:

User information is returned. Header is added successfully:

We add a test interface to the Controller:

@GetMapping("/getResource")
@ResponseBody
public ResponseEntity<String> getResource(a){
	return ResponseEntity.ok("Resource obtained successfully");
}
Copy the code

This interface is supposed to be accessible only after the user has logged in, but you will find that after logging in, you do not need to carry the header to access this interface, and you will not be able to access this interface after the server restarts. What’s going on here? This can write a bug out, I really do not fit to do development, go home to farm.

Fortunately, I decided to stick to it after all the friends’ praise and no support. Why does this happen? Spring Security’s login information is stored in the session, which is obviously contrary to JWT. So we need to do two more things:

  1. Disable the session
  2. Intercepts requests other than logins and must carry tokens

So let’s solve these two problems.

Request certification

Disable the session

Methods to call HttpSecurity:

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
Copy the code

This disables the session. Is it very simple?

How do you intercept the request and verify that it carries a token? As with login authentication, request verification is implemented through the above five steps.

User-defined request interception Filter

We need another Filter to intercept these requests. This interceptor mainly extracts tokens from headers:

public class JwtAuthenticationRequestFilter extends OncePerRequestFilter {
    private final RequestHeaderRequestMatcher requestHeaderRequestMatcher = new RequestHeaderRequestMatcher("jwt-token");
    @Autowired
    private UserService userService;
    @Autowired
    private JwtAuthFailHandler failHandler;
    @Autowired
    private JwtAuthRequestSuccessHandler successHandler;

    private AuthenticationManager authenticationManager;

    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // If the request does not support anonymous users and does not carry a token, it is ok to pass this request because the SecurityContext has no authentication information and will be intercepted by the permission control module later
        if(! requestHeaderRequestMatcher.matches(request)){ filterChain.doFilter(request,response);return;
        }
        String token = request.getHeader("jwt-token");
        if (StringUtils.isBlank(token)) {
            AuthenticationException failed = new InsufficientAuthenticationException("token is empty");
            SecurityContextHolder.clearContext();
            failHandler.onAuthenticationFailure(request, response, failed);
            return;
        }else {
            User user = userService.getUserInfoFromToken(token);
            JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(user.getAuthorities(),user,token);
            Authentication authResult = authenticationManager.authenticate(authenticationToken);
            // Successful callback methodSecurityContextHolder.getContext().setAuthentication(authResult); successHandler.onAuthenticationSuccess(request, response, authResult); } filterChain.doFilter(request,response); }}Copy the code

Slightly different from the logged-in filter, this filter has actually completed the verification of JWT token here, so the JwtAuthenticationToken generated here has actually been verified.

public class JwtAuthenticationToken extends AbstractAuthenticationToken {
    private final Object principal;
    private Object credentials;

    public JwtAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        // Initialization is complete, but not yet authenticated
        setAuthenticated(false);
    }

    public JwtAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials(a) {
        return credentials;
    }

    @Override
    public Object getPrincipal(a) {
        returnprincipal; }}Copy the code

User-defined request verification provider

In this provider, you don’t need to do the authentication, just return the authentication:

@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        return authentication;
    }

    @Override
    public boolean supports(Class
        authentication) {
        returnauthentication.isAssignableFrom(JwtAuthenticationToken.class); }}Copy the code

Request validation result callback

For token authentication failure, it returns a 401 status authentication failure message to the user. This is the same as the login handler, so it can be reused directly. For token authentication success, check whether the token needs to be refreshed before continuing with the other filters in filterchain and, unlike logins, return user information, so redefine a handler:

@Slf4j
@Component
@EnableConfigurationProperties(JwtProperties.class)
public class JwtAuthRequestSuccessHandler implements AuthenticationSuccessHandler {
    /** * Refresh interval 5 minutes */
    private final long tokenRefreshInterval = TimeUnit.MINUTES.toSeconds(5);
    @Autowired
    private UserService userService;
    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        User user = (User) authentication.getPrincipal();
        String oldToken = (String) authentication.getCredentials();
        Date expireDate = JwtUtils.getExpireDate(oldToken, jwtProperties.getPublicKey());
        if (shouldTokenRefresh(expireDate)){
            // Generate a token to reset the validity period of the token
            String generateToken = userService.generateToken(user);
            log.debug("Regenerate token: {}",generateToken);
            response.setHeader("jwt-token",generateToken); }}protected boolean shouldTokenRefresh(Date issueAt){
        LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault());
        returnLocalDateTime.now().minusSeconds(tokenRefreshInterval).isAfter(issueTime); }}Copy the code

Add request validation configuration to Security

With the above authentication component in place, we need a configuration class:

@Component
public class JwtRequestSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
    @Autowired
    private JwtAuthenticationProvider provider;
    @Override
    public void configure(HttpSecurity builder) throws Exception {
        JwtAuthenticationRequestFilter filter = new JwtAuthenticationRequestFilter();
        filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
        // Add the filter to the Spring container so that it can be injected into the filterpostProcess(filter); builder.authenticationProvider(provider); builder.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

Because our filter uses objects from the Spring container, we call the postProcess method to add the filter to the Spring container.

Add this configuration to the Spring Security configuration class as you did with the login:

/ / injection first
@Autowired
private JwtRequestSecurityConfig jwtRequestSecurityConfig; 
// Then add it in HTTP
http.apply(jwtRequestSecurityConfig);
Copy the code

A complete JWT implementation of Spring Security login authentication was completed, and the above interface test was not a problem at all. However, the project was still not ready to use because it did not address one of the most fundamental problems in a front-end separation project: cross-domain.

Cross domain

Back-end separation projects must address cross-domain issues. Cross-domain support has been added to Spring Security:

@Bean
public CorsFilter corsFilter(a){
    //1. Add cORS configuration information
    CorsConfiguration config = new CorsConfiguration();
    // 1 allowed fields, do not write *
    List<String> allowedOrigins = new ArrayList<>();
    allowedOrigins.add("http://localhost:8080");
    allowedOrigins.forEach(config::addAllowedOrigin);
    2 Whether to send cookie information
    config.setAllowCredentials(true);
    //.3 Allowed request modes
    List<String> allowedMethods = new ArrayList<>();
    allowedMethods.add("GET");
    allowedMethods.add("POST");
    allowedMethods.add("OPTIONS");
    allowedMethods.add("HEAD");

    allowedMethods.forEach(config::addAllowedMethod);
    //.4 Allowed headers
    config.addAllowedHeader("*");
    config.addExposedHeader("jwt-token");
    5 Add the validity period
    config.setMaxAge(3600L);

    //2. Add a mapping path
    UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
    configurationSource.registerCorsConfiguration("/ * *",config);

    //3. Return a new CorsFilter
    return new CorsFilter(configurationSource);
}
Copy the code

Then add to HTTP:

http.cors().and().addFilterAfter(corsFilter(), CorsFilter.class)
Copy the code

Add a complete configuration:

@Override
protected void configure(HttpSecurity http) throws Exception {
    //super.configure(http);
    http.formLogin()
        .disable()
        // Add header Settings to support cross-domain and Ajax requests
        .cors().and()
        .addFilterAfter(corsFilter(), CorsFilter.class)
        .apply(smsAuthenticationSecurityConfig)
        .and()
        .apply(jwtAuthenticationSecurityConfig)
        .and()
        .apply(jwtRequestSecurityConfig)
        .and()
        // Set URL authorization
        .authorizeRequests()
        // The login page must be allowed
        .antMatchers("/login"."/verifyCode"."/smsLogin"."/failure"."/jwtLogin")
        .permitAll()
        // anyRequest() All requests authenticated() must be authenticated
        .anyRequest()
        .authenticated()
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        / / close CSRF
        .csrf().disable();
}
Copy the code

conclusion

After the above series of custom certifications, you are probably familiar with Spring Security. Custom login implementations should also come in handy.

The generation and verification of JWT tokens is not the focus of this article, so it is not mentioned here. You can go to Gitee to check all the source code: Spring Security Combat, login authentication is not discussed, the next article will start with authorization authentication.