Springboot + spring security front and back end separation + Jwt permission authentication (transformation record)

preface

Generally speaking, we use spring security. By default, the front and back ends are integrated, such as thymeleaf or Freemarker. Spring security also comes with a login page and allows you to configure the login page and error page.
But now the separation of front and back ends is the right way. If the front and back ends are separated, the returned page needs to be changed into Json format and handed over to the front end for processing

By default, spring security uses Session to determine whether the requesting user logs in, but it is inconvenient for distributed expansion. Although spring security also supports using spring Session to manage user state under distributed, distributed Jwt is still the mainstream. So let's talk about how to separate the front and back ends of spring security and use Jwt for authentication

1, Five handler s, one filter and two users

Five handler s are

  • Implement the AuthenticationEntryPoint interface to intercept and handle anonymous requests that require a login interface
  • Implement the AuthenticationSuccessHandler interface. When the login is successful, the method of this processing class is called
  • Implement the AuthenticationFailureHandler interface. When the login fails, the method of this processing class is called
  • Implement the AccessDeniedHandler interface. When the access interface does not have permission after login, the method of this processing class will be called
  • Implement the LogoutSuccessHandler interface, which is called when logging off

1.1 AuthenticationEntryPoint

Anonymous access without login is called when login authentication is required

/**
 * The calling class of the resource that needs to be logged in when anonymous is not logged in
 * @author zzzgd
 */
@Component
public class CustomerAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
	    //Set the response status code and return error information
	    ...
        ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));
    }
}

1.2 AuthenticationSuccessHandler

This is the method we called after we entered the user name and password.
Simply put, it is to obtain user information, generate a token using JWT, and then return the token

/**
 * Successful login processing class. After successful login, the method inside will be called
 * @author Exrickx
 */
@Slf4j
@Component
public class CustomerAuthenticationSuccessHandler implements AuthenticationSuccessHandler {


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    	//Simply put, get the current user, get the user name or userId, create a token, and return
        log.info("Login successful...");
        CustomerUserDetails principal = (CustomerUserDetails) authentication.getPrincipal();
        //Issue token
        Map<String,Object> emptyMap = new HashMap<>(4);
        emptyMap.put(UserConstants.USER_ID,principal.getId());
        String token = JwtTokenUtil.generateToken(principal.getUsername(), emptyMap);
        ResponseUtil.out(ResultUtil.success(token));
    }
}

1.3 AuthenticationFailureHandler

Login success means login failure
This method is called when login fails. You can limit login errors or other operations. Here, I directly set the status code of the response header to 401 and return

/**
 * The processing class that will be called when the login account and password are wrong
 * @author Exrickx
 */
@Slf4j
@Component
public class CustomerAuthenticationFailHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
    //Set the response status code and return error information
    	....
        ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.LOGIN_UNMATCH_ERROR));
    }
}


1.4 LogoutSuccessHandler

It is called when logging out. Jwt has a disadvantage that it cannot actively control the failure. Jwt+session can be used, such as deleting the token stored in Redis

It should be noted that if the session of spring security is configured as stateless or the session is not saved, the authentication here is null, Note the null pointer problem( See configuring WebSecurityConfigurerAdapter below for details

/**
 * Logout successful calling class
 * @author zzzgd
 */
@Component
public class CustomerLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        ResponseUtil.out(ResultUtil.success("Logout Success!"));
    }
}

1.5 AccessDeniedHandler

After logging in, access to resources with missing permissions will be called.

/**
 * The calling class when access is denied without permission
 * @author Exrickx
 */
@Component
@Slf4j
public class CustomerRestAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
        ResponseUtil.out(403, ResultUtil.failure(ErrorCodeConstants.PERMISSION_DENY));
    }
}

1.6 one filter OncePerRequestFilter

This is a small point.
After logging in successfully, we returned a token. How can we use this token?

When the front end initiates a request, it puts the token in the request header and parses the request header in the filter.

  1. If there is a request header of accessToken (you can define your own name), take out the token and parse it. If the parsing is successful, it indicates that the token is correct. Put the parsed user information into the context of spring security
  2. If there is a request header for accessToken, the token parsing fails (invalid token or expired token), and the user information cannot be obtained. Release
  3. No request header for accessToken, release

Some people may wonder why the token should be released when it fails?
This is because spring security will do the login authentication and permission verification by itself, relying on the SecurityContextHolder.getContext().setAuthentication(authentication);, which we put in the spring security context;, If you don't get the authentication and release it, spring security will still go to authentication and verification. At this time, you will find that you don't have login or permission.

Old version, latest at the bottom

package com.zgd.shop.web.config.auth.filter;

import com.zgd.shop.common.constants.SecurityConstants;
import com.zgd.shop.common.util.jwt.JwtTokenUtil;
import com.zgd.shop.web.config.auth.user.CustomerUserDetailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * When the request comes, the filter parses the token in the request header, parses the token to get the user information, and then saves it to the SecurityContextHolder
 * @author zzzgd
 */
@Component
@Slf4j
public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    CustomerUserDetailService customerUserDetailService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        
    	//The request header is accessToken
    	//The request body is Bearer token

    	String authHeader = request.getHeader(SecurityConstants.HEADER);

        if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {

            final String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());
            String username = JwtTokenUtil.parseTokenGetUsername(authToken);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);
                if (userDetails != null) {
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

1.7 implement UserDetails extension field

For the User information represented by this interface, spring security implements a User by default, but there are few fields, only username and password, and the UserDetail is also obtained when obtaining the User information later.

So we take the User of our database as an extension and implement this interface ourselves. It inherits the User corresponding to the database, not the User of spring security

package com.zgd.shop.web.config.auth.user;

import com.zgd.shop.common.constants.UserConstants;
import com.zgd.shop.dao.entity.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * CustomerUserDetails
 *
 * @author zgd
 * @date 2019/7/17 15:29
 */
public class CustomerUserDetails extends User implements UserDetails {

  private Collection<? extends GrantedAuthority> authorities;

  public CustomerUserDetails(User user){
    this.setId(user.getId());
    this.setUsername(user.getUsername());
    this.setPassword(user.getPassword());
    this.setRoles(user.getRoles());
    this.setStatus(user.getStatus());
  }

  public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
    this.authorities = authorities;
  }

  /**
   * Add permissions and roles owned by users
   * @return
   */
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return this.authorities;
  }

  /**
   * Whether the account has expired
   * @return
   */
  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  /**
   * Disable
   * @return
   */
  @Override
  public boolean isAccountNonLocked() {
    return  true;
  }

  /**
   * Does the password expire
   * @return
   */
  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  /**
   * Enable
   * @return
   */
  @Override
  public boolean isEnabled() {
    return UserConstants.USER_STATUS_NORMAL.equals(this.getStatus());
  }
}

1.8 implement UserDetailsService

When spring security logs in, go back to the database (or other sources), get the correct user information according to the username, and get the user information and permissions according to the service class. We realize it ourselves

package com.zgd.shop.web.config.auth.user;

import com.alibaba.fastjson.JSON;
import com.zgd.shop.dao.entity.model.User;
import com.zgd.shop.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @author zgd
 * @date 2019/1/16 16:27
 * @description Implement UserDetailService and obtain user information with spring security
 */
@Service
@Slf4j
public class CustomerUserDetailService implements UserDetailsService {

  @Autowired
  private IUserService userService;

  /**
   * Get the user information and give it to spring to verify the permissions
   * @param username
   * @return
   * @throws UsernameNotFoundException
   */
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //Get user information
    User user = userService.getUserRoleByUserName(username);
    if(user == null){
      throw new UsernameNotFoundException("user name does not exist");
    }
    CustomerUserDetails customerUserDetails = new CustomerUserDetails(user);

    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    //Permissions for adding users. Just add user permissions to authorities and everything will be fine.
    if (CollectionUtils.isNotEmpty(user.getRoles())){
      user.getRoles().forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_"+r.getRoleName())));
    }
    customerUserDetails.setAuthorities(authorities);
    log.info("authorities:{}", JSON.toJSONString(authorities));
    
    //The UserDetail defined by ourselves is returned here
    return customerUserDetails;
  }
}

2, Configure WebSecurityConfigurerAdapter

We need to register the handler and filter defined above with spring security. At the same time, configure some release URLs

Here is a point to note: if the following SessionCreationPolicy.STATELESS is configured, spring security will not save the session and will not get the user entity object when / logout logs out.

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

If the logout does not depend on spring security and the session is managed by redis's token, you can follow the above configuration.

package com.zgd.shop.web.config;

import com.zgd.shop.web.config.auth.encoder.MyAesPasswordEncoder;
import com.zgd.shop.web.config.auth.encoder.MyEmptyPasswordEncoder;
import com.zgd.shop.web.config.auth.handler.*;
import com.zgd.shop.web.config.auth.filter.CustomerJwtAuthenticationTokenFilter;
import com.zgd.shop.web.config.auth.user.CustomerUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @Author: zgd
 * @Date: 2019/1/15 17:42
 * @Description:
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)// Control @ Secured permission annotation
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  /**
   * You need to give spring injection instead of new directly
   */
  @Autowired
  private PasswordEncoder passwordEncoder;
  @Autowired
  private CustomerUserDetailService customerUserDetailService;
  @Autowired
  private CustomerAuthenticationFailHandler customerAuthenticationFailHandler;
  @Autowired
  private CustomerAuthenticationSuccessHandler customerAuthenticationSuccessHandler;
  @Autowired
  private CustomerJwtAuthenticationTokenFilter customerJwtAuthenticationTokenFilter;
  @Autowired
  private CustomerRestAccessDeniedHandler customerRestAccessDeniedHandler;
  @Autowired
  private CustomerLogoutSuccessHandler customerLogoutSuccessHandler;
  @Autowired
  private CustomerAuthenticationEntryPoint customerAuthenticationEntryPoint;


  /**
   * This method defines the source of authentication user information and the rules of password verification
   *
   * @param auth
   * @throws Exception
   */
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //auth.authenticationProvider(myauthenticationProvider) defines the rules for password verification

    //If you need to change the source of authenticated user information, we can implement UserDetailsService
    auth.userDetailsService(customerUserDetailService).passwordEncoder(passwordEncoder);
  }


  @Override
  protected void configure(HttpSecurity http) throws Exception {
    /**
     * antMatchers: ant Wildcard rules for
     * ?	Match any single character
     * *	Matches 0 or any number of characters, excluding '/'
     * **	Match 0 or more directories, including "/"
     */
    http.headers()
        .frameOptions().disable();
	//After logging in, you do not have access to the processing class
    http.exceptionHandling().accessDeniedHandler(customerRestAccessDeniedHandler)
        //Anonymous access, processing class without permission
        .authenticationEntryPoint(customerAuthenticationEntryPoint);

    //Use jwt's Authentication to resolve whether the request has a token
    http.addFilterBefore(customerJwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    http.authorizeRequests()
        //It means that "/ any" and "/ ignore" do not need permission verification
        .antMatchers("/ignore/**", "/login", "/**/register/**").permitAll()
        .anyRequest().authenticated()
        // This means that any request requires verification and authentication (release configured above)

        .and()
        //Configure login, detect the url address to jump when the user is not logged in, and log in and release
        .formLogin()
        //It needs to be consistent with the action address of the front-end form
        .loginProcessingUrl("/login")
        .successHandler(customerAuthenticationSuccessHandler)
        .failureHandler(customerAuthenticationFailHandler)
        .permitAll()

        //Configure and cancel session management, and then use Jwt to obtain the user status. Otherwise, even if the token is invalid, there will be session information, and the user is still judged to be in login status
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

        //Configure logout and logout release
        .and()
        .logout()
        .logoutSuccessHandler(customerLogoutSuccessHandler)
        .permitAll()
        
        .and()
        .csrf().disable();
  }
}

3, Other

That's about it. Start up, localhost:8080/login, use postman, use form data, post submission, and the parameters are username and password. Call and return token.

Put the token in the header and request the interface. ok

3.1 deficiencies

The above is the simplest processing, and there are many optimization places. such as

  1. Control token destruction?
    Using the redis+token combination, you can not only parse the token, but also judge whether redis has this token. Logoff and active invalidation token: delete the key of redis
  2. Control token expiration time? If the user is still operating one second before the token expires, he needs to log in again the next second, which is definitely bad
    1. Consider adding a refreshToken. The expiration time is longer than the token. The front end obtains the expiration time when it gets the token. One minute before the expiration, use the refreshToken to call the refresh interface to re obtain a new token.
    2. Set a shorter expiration time for the returned jwtToken, redis saves the token, and set a longer expiration time. If the requested token expires, query redis. If redis still exists, return a new token( Why is the expiration time of redis greater than that of token? Because the expiration of redis is controllable, it can be deleted manually (subject to the expiration of redis)
  3. Every request will be intercepted by OncePerRequestFilter, and every time it will be requested by the database to obtain user data in UserDetailService. You can consider caching, redis or saving it directly in memory

3.2 solution

Update 2019-07-19

This is for 2.2 above, that is, redis takes a long time. After the jwt expires, if redis does not expire, issue a new jwt.
However, it is recommended that the expiration date of the front end be estimated, and the new refresh will be called by calling the jwt interface before the expiration date.

Why do things turn out like this?
If the expiration time of redis is one week and the jwt is one hour, you can create as many new jwts as you want after one hour with the expired jwt. Of course, when there is no restriction on the expired jwt, if restrictions should be considered, such as adding a field to the value of redis, saving the current jwt, and overwriting it with a new jwt after refresh, the refresh interface determines whether the current expired jwt is the same as redis.

In short, it is also necessary to judge whether the expiration jwt is legal when refreshing the token. You can't refresh the expired token of last year.
If you refresh the token before it expires, at least this will not happen

However, I write my own demo here in the way of 2.2, that is, give a new one after expiration. The idea is as follows:

  1. Issue a token after logging in. The token has a timestamp, and the username assembly is used as the key. Save the timestamp to the cache (redis, cache)
  2. When the request comes, the filter parses the token. If it does not expire, it also needs to compare whether the timestamp in the cache is the same as that of the token. If the timestamp is different, it means that the token cannot be refreshed. ignore
  3. Log off, clear cached data

In this way, I can avoid getting unlimited refresh after the token expires.

However, there is still a problem with details, and the issue of refreshing token s at the same time is not considered. Part of the code is as follows

Old version, latest at the bottom

package com.zgd.shop.web.auth.filter;

import com.zgd.shop.common.constants.SecurityConstants;
import com.zgd.shop.common.util.jwt.JwtTokenUtil;
import com.zgd.shop.web.auth.user.CustomerUserDetailService;
import com.zgd.shop.web.auth.user.CustomerUserDetails;
import com.zgd.shop.web.auth.user.UserSessionService;
import com.zgd.shop.web.auth.user.UserTokenManager;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * When the request comes, the filter parses the token in the request header, parses the token to get the user information, and then saves it to the SecurityContextHolder
 * @author zzzgd
 */
@Component
@Slf4j
public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    CustomerUserDetailService customerUserDetailService;
    @Autowired
    UserSessionService userSessionService;
    @Autowired
    UserTokenManager userTokenManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        
    	//The request header is accessToken
    	//The request body is Bearer token

    	String authHeader = request.getHeader(SecurityConstants.HEADER);

        if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {

            final String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());

            String username;
            Claims claims;
            try {
                claims = JwtTokenUtil.parseToken(authToken);
                username = claims.getSubject();
            } catch (ExpiredJwtException e) {
                //token expiration
                claims = e.getClaims();
                username = claims.getSubject();
                CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);
                if (userDetails != null){
                    //The session has not expired. Check whether the timestamp is consistent. If yes, reissue the token
                    if (isSameTimestampToken(username,e.getClaims())){
                        userTokenManager.awardAccessToken(userDetails,true);
                    }
                }
            }
            //Avoid requesting the database to query user information every time, and query from the cache
            CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//                UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);
                if (userDetails != null) {
                    if(isSameTimestampToken(username,claims)){
                        //The timestamp resolved by the token must be consistent with that saved by the session
                        UsernamePasswordAuthenticationToken authentication =
                                new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
        }
        chain.doFilter(request, response);
    }

    /**
     * Determine whether the same timestamp
     * @param username 
     * @param claims
     * @return
     */
    private boolean isSameTimestampToken(String username, Claims claims){
        Long timestamp = userSessionService.getTokenTimestamp(username);
        Long jwtTimestamp = (Long) claims.get(SecurityConstants.TIME_STAMP);
        return timestamp.equals(jwtTimestamp);
    }
}
package com.zgd.shop.web.auth.user;

import com.google.common.collect.Maps;
import com.zgd.shop.common.constants.SecurityConstants;
import com.zgd.shop.common.constants.UserConstants;
import com.zgd.shop.common.util.ResponseUtil;
import com.zgd.shop.common.util.jwt.JwtTokenUtil;
import com.zgd.shop.core.result.ResultUtil;
import com.zgd.shop.web.config.auth.UserAuthProperties;
import org.apache.commons.collections.MapUtils;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * UserTokenManager
 * token Administration
 *
 * @author zgd
 * @date 2019/7/19 15:25
 */
@Component
public class UserTokenManager {

  @Autowired
  private UserAuthProperties userAuthProperties;
  @Autowired
  private UserSessionService userSessionService;

  /**
   * Issue token
   * @param principal
   * @author zgd
   * @date 2019/7/19 15:34
   * @return void
   */
  public void awardAccessToken(CustomerUserDetails principal,boolean isRefresh) {
    //The issuing token determines the timestamp and is saved in the session and token
    long mill = System.currentTimeMillis();
    userSessionService.saveSession(principal);
    userSessionService.saveTokenTimestamp(principal.getUsername(),mill);

    Map<String,Object> param = new HashMap<>(4);
    param.put(UserConstants.USER_ID,principal.getId());
    param.put(SecurityConstants.TIME_STAMP,mill);

    String token = JwtTokenUtil.generateToken(principal.getUsername(), param,userAuthProperties.getJwtExpirationTime());
    HashMap<String, String> map = Maps.newHashMapWithExpectedSize(1);
    map.put(SecurityConstants.HEADER,token);
    int code = isRefresh ? 201 : 200;
    ResponseUtil.outWithHeader(code,ResultUtil.success(),map);
  }
}
to update

2019-09-30

The filter for token parsing is optimized:

  1. If the session of redis has not expired, but the token in the request header has expired, issue a new token and return it after judging that the timestamp is consistent
  2. If the session of redis has not expired, but the token in the request header has expired, and the timestamp is inconsistent, it means that the token in the current request cannot be refreshed. Set the response code to 401 and return
  3. If the token in the request header expires, but the redis session fails or cannot be found, it will be released directly and handed over to the subsequent permission verification processing (that is, the login information is not set for the context SecurityContextHolder. Later, if it is judged that the request lacks permission, it will be processed by itself)
package com.zgd.shop.web.auth.filter;

import com.zgd.shop.common.constants.SecurityConstants;
import com.zgd.shop.common.util.ResponseUtil;
import com.zgd.shop.common.util.jwt.JwtTokenUtil;
import com.zgd.shop.core.error.ErrorCodeConstants;
import com.zgd.shop.core.result.ResultUtil;
import com.zgd.shop.web.auth.user.CustomerUserDetailService;
import com.zgd.shop.web.auth.user.CustomerUserDetails;
import com.zgd.shop.web.auth.user.UserSessionService;
import com.zgd.shop.web.auth.user.UserTokenManager;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * When the request comes, the filter parses the token in the request header, parses the token to get the user information, and then saves it to the SecurityContextHolder
 * @author zzzgd
 */
@Component
@Slf4j
public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    CustomerUserDetailService customerUserDetailService;
    @Autowired
    UserSessionService userSessionService;
    @Autowired
    UserTokenManager userTokenManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        
    	//The request header is accessToken
    	//The request body is Bearer token

    	String authHeader = request.getHeader(SecurityConstants.HEADER);

        if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {
            //The request header has a token
            final String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());

            String username;
            Claims claims;
            try {
                claims = JwtTokenUtil.parseToken(authToken);
                username = claims.getSubject();
            } catch (ExpiredJwtException e) {
                //token expiration
                claims = e.getClaims();
                username = claims.getSubject();
                CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);
                if (userDetails != null){
                    //The session has not expired. Check whether the timestamp is consistent. If yes, reissue the token
                    if (isSameTimestampToken(username,e.getClaims())){
                        userTokenManager.awardAccessToken(userDetails,true);
                        //Directly set the response code to 201 and return directly
                        return;
                    }else{
                        //Inconsistent timestamp. Invalid token, unable to refresh token, response code 401, front-end jump to login page
                        ResponseUtil.out(HttpStatus.UNAUTHORIZED.value(),ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));
                        return;
                    }
                }else{
                    //Direct release and hand it over to the following handler for processing. If the current request requires access permission, it will be processed by CustomerRestAccessDeniedHandler
                    chain.doFilter(request, response);
                    return;
                }
            }

            //Avoid requesting the database to query user information every time, and query from the cache
            CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//                UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);
                if (userDetails != null) {
                    if(isSameTimestampToken(username,claims)){
                        //The timestamp resolved by the token must be consistent with that saved by the session
                        UsernamePasswordAuthenticationToken authentication =
                                new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
        }
        chain.doFilter(request, response);
    }

    /**
     * Determine whether the same timestamp
     * @param username
     * @param claims
     * @return
     */
    private boolean isSameTimestampToken(String username, Claims claims){
        Long timestamp = userSessionService.getTokenTimestamp(username);
        Long jwtTimestamp = (Long) claims.get(SecurityConstants.TIME_STAMP);
        return timestamp.equals(jwtTimestamp);
    }
}

Tags: Java Spring Boot Spring Security

Posted on Tue, 07 Sep 2021 14:56:31 -0400 by lewisstevens1