shiro-redis-jwt integration

1. Integrated process logic

20201031132733189.png

2. Integration steps

1. Import the starter package of shiro-redis: There is also the jwt tool package toolkit, and to simplify development, I introduced the hutool toolkit.

<!--shiro-redis integration-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
             <version>3.2.1</version>
        </dependency>
        <!--hutool tool class-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

2. Write configuration

  • Introduce RedisSessionDAO and RedisCacheManager to save shiro permission data and session information to redis to achieve session sharing.

  • Rewrite the SessionManager and DefaultWebSecurityManager in shiro, and at the same time close the session that comes with shiro in the rewritten DefaultWebSecurityManager. You need to set the bit to false, so that users will not be able to log in to shiro through session mode. Later, use jwt credentials to log in.

  • Override shiro’s ShiroFilterChainDefinition to register your own filter. We will no longer intercept access paths through coding. Instead, all paths will pass through the JwtFilter filter registered by ourselves, and then determine whether there is a jwt certificate. If there is a jwt certificate, log in, if not, skip it. After skipping, there will be Shiro’s permission annotation for interception. , eg:@RequiredAuthentication, this controls permission access.

    @Configuration
    public class ShiroConfig {
     @Autowired
     JwtFilter jwtFilter;
     /**
      * session domain management
      * @param redisSessionDAO
      * @return
      */
     @Bean
     public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
         DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    
         // inject redisSessionDAO
         sessionManager.setSessionDAO(redisSessionDAO);
         return sessionManager;
     }
    
    
/**
 * Rewrite Shiro's security management container,
 * @param accountRealm
 * @param sessionManager
 * @param redisCacheManager
 * @return
 */
@Bean
public SessionsSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);

    //inject sessionManager
    securityManager.setSessionManager(sessionManager);

    // inject redisCacheManager
    securityManager.setCacheManager(redisCacheManager);
    return securityManager;
}

/**
 * Define filters
 * @return
 */
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
    //Apply for a default filter chain
    DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    Map<String,String> filterMap = new LinkedHashMap<>();

    //Add a jwt filter to the filter chain
    filterMap.put("/**","jwt");
    chainDefinition.addPathDefinitions(filterMap);
    return chainDefinition;
}

/**
 * Filter factory business
 * @param securityManager Security management in shiro
 * @param shiroFilterChainDefinition
 * @return
 */
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                             ShiroFilterChainDefinition shiroFilterChainDefinition){
    /*shiro filter bean object*/
    ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
    shiroFilter.setSecurityManager(securityManager);

    //Filter rules that need to be added
    Map<String,Filter> filters = new HashMap<>();
    filters.put("jwt",jwtFilter);
    shiroFilter.setFilters(filters);


    Map<String,String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
    shiroFilter.setFilterChainDefinitionMap(filterMap);
    return shiroFilter;
}

}

### 3. Write realm
AccountRealm shiiro's logic for login or permission verification.

Three methods need to be rewritten.

- supports: In order for realm to support jwt certificate verification
- doGetAuthorizationInfo: permission verification
- doGetAuthenticationInfo: Login authentication verification
```java
@Slf4j
@Component
public class AccountRealm extends AuthorizingRealm{

    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    UserService userService;

    /**
     * Determine whether it is a jwt token
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * ASD
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * Login authentication
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // Force the incoming AuthenticationToken to JwtToken
        JwtToken jwtToken = (JwtToken) authenticationToken;
        // Get the userId in jwtToken
        String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
        //Query the database based on the userId in jwtToken
        User user = userService.getById(Long.valueOf(userId));
        if(user == null){
            throw new UnknownAccountException("Account does not exist!");
        }
        if(user.getStatus() == -1){
            throw new LockedAccountException("Account has been locked!");
        }
        //Put the information that can be displayed in this carrier. Secret information such as passwords do not need to be placed in this carrier.
        AccountProfile accountProfile = new AccountProfile();
        BeanUtils.copyProperties(user,accountProfile);
        log.info("jwt------------->{}",jwtToken);
        // Return the basic information of the user in the token to Shiro
        return new SimpleAuthenticationInfo(accountProfile,jwtToken.getCredentials(),getName());
    }
}

It mainly configures the doGetAuthenticationInfo login authentication method, obtains user information through jwt credentials, determines the user’s status, and finally throws the corresponding exception information when an exception occurs.

4.Write JwtToken

Shiro’s default supports is UsernamePasswordToken, and we use jwt, so we need to define a JwtToken to rewrite the token.

public class JwtToken implements AuthenticationToken{
    private String token;
    public JwtToken(String token){
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

5. Write a tool class for JwtUtils to generate and verify jwt

Some jwt-related key information is obtained from the project’s configuration file.

@Component
@ConfigurationProperties(prefix = "mt.vuemtblog.jwt")
public class JwtUtils {
    private String secret;
    private long expire;
    private String header;

    /**
     * Generate jwt token
     * @param userId
     * @return
     */
    public static String generateToken(long userId){
        return null;
    }

    /**
     * Get jwt information
     * @param token
     * @return
     */
    public static Claims getClaimByToken(String token){
        return null;
    }

    /**
     * Verify whether the token has expired
     * @param expiration
     * @return true expired
     */
    public static boolean isTokenExpired(Date expiration){
        return expiration.before(new Date());
    }
}

6. Write the carrier AccountProfile that returns user information after successful login

@Data
public class AccountProfile implements Serializable {
    private Long id;
    private String username;
    private String avatar;
}

7. Basic information of global configuration

shiro-redis:
  enabled: true
  redis-manger:
    host:127.0.0.1:6379

mt:
  vuemtblog:
    jwt:
    #Encryption key
    secret:f4e2e52034348f86b67cde581c0f9eb5
    #Token is valid for 7 days in seconds
    expire:604800
    # Set the key value of the token in the header
    header:authorization

8. If the project uses spring-boot-devtools, you need to add a configuration file,

Create a new META-INF in the resources directory, and then create a new spring-devtools.properties, so that hot restart will not report an error.

restart.include.shiro-redis=/shiro-[\w-\.] + jar

9. Write a custom JwtFileter filter

What we inherit here is Shiro’s built-in AuthenticatingFilter, a filter that can have built-in automatic login methods. Some students can also inherit BasicHttpAuthenticationFilter.

We need to override a few methods:

  1. createToken: To implement login, we need to generate our custom supported JwtToken

  2. onAccessDenied: interception verification. When there is no Authorization in the header, we pass it directly and do not need to log in automatically. When it is included, we first verify the validity of the jwt. If there is no problem, we directly execute the executeLogin method to realize automatic login.

  3. onLoginFailure: The method entered when a login exception occurs. We directly encapsulate the exception information and then throw it.

  4. preHandle: The pre-interception of the interceptor. Because we are a front-end and back-end analysis project, in addition to the need for cross-domain global configuration in the project, we also need to provide cross-domain support in the interceptor. In this way, the interceptor will not be restricted before entering the Controller.

    @Component
    public class JwtFilter extends AuthenticatingFilter{
    
     @Autowired
     JwtUtils jwtUtils;
     /**
      * Implement login and generate customized JwtToken
      * @param servletRequest
      * @param servletResponse
      * @return
      * @throwsException
      */
     @Override
     protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
         HttpServletRequest request = (HttpServletRequest)servletRequest;
         String jwt = request.getHeader("Authorization");
         if(StringUtils.isEmpty(jwt)){
             return null;
         }
         return new JwtToken(jwt);
     }
    
     /**
      * Interception verification
      * @description When there is no Authorization in the header, it will be passed directly without automatic login.
      * When using Authorization, you need to verify the timeliness of jwt first. If there is no problem, execute executeLogin directly to achieve automatic login, and entrust the token to shiro.
      * @param servletRequest
      * @param servletResponse
      * @return
      * @throwsException
      */
     @Override
     protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
         HttpServletRequest request = (HttpServletRequest) servletRequest;
         // Get the token in the user request header
         String token = request.getHeader("Authorization");
         if (StringUtils.isEmpty(token)) {// No token
             return true;
         } else {
             //Verify jwt
             Claims claim = jwtUtils.getClaimByToken(token);
             // Token is empty or time has expired
             if (claim == null || jwtUtils.isTokenExpired((claim.getExpiration()))) {
                 throw new ExpiredCredentialsException("Token has expired, please log in again!");
             }
         }
         //Perform automatic login
         return executeLogin(servletRequest, servletResponse);
     }
    
     /**
      * An exception occurred while executing login
      * @param token
      * @param e
      * @param request
      * @param response
      * @return
      */
     @Override
     protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
    
         HttpServletResponse httpServletResponse = (HttpServletResponse)response;
         // 1. Determine whether the login failed due to abnormality
         Throwable throwable = e.getCause() == null ? e : e.getCause();
         // 2. Obtain login exception information and return json data in a customized Resut response format
         Result result = Result.error(throwable.getMessage());
         String json = JSONUtil.toJsonStr(result);//A json tool of hutool
    
         // 3. Print response
         try{
             httpServletResponse.getWriter().print(json);
         }catch (IOException ioException){
    
         }
         return false;
     }
    }

    3. Global exception handling in springboot

    To separate the front-end and back-end, we need to configure the exception handling mechanism and return a friendly and simple format to the front-end.

Processing method:

  1. Unified exception handling through @ControllerAdvice

  2. Specify each type of Exception to be caught through @ExceptionHandler(value=RuntimeException.class). This exception handling is global, and all similar exceptions will be caught.

    /**
    * Global exception handling
    */
    @Slf4j
    @RestControllerAdvice
    public class GlobalExcepitonHandler {
     //Catch Shiro's exception
     @ResponseStatus(HttpStatus.UNAUTHORIZED)
     @ExceptionHandler(ShiroException.class)
     public Result handle401(ShiroException e) {
         return Result.fail(401, e.getMessage(), null);
     }
     /**
      * Handle Assert exceptions
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(value = IllegalArgumentException.class)
     public Result handler(IllegalArgumentException e) throws IOException {
         log.error("Assert exception:-------------->{}",e.getMessage());
         return Result.fail(e.getMessage());
     }
     /**
      * @Validated Verification error exception handling
      */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(value = MethodArgumentNotValidException.class)
     public Result handler(MethodArgumentNotValidException e) throws IOException {
         log.error("Runtime exception:-------------->",e);
         //Intercept all necessary error information; only the error cause will be displayed, and other causes will not be displayed BY....
         BindingResult bindingResult = e.getBindingResult();
         ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
         return Result.fail(objectError.getDefaultMessage());
     }
     /*
     * Runtime exception
     */
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(value = RuntimeException.class)
     public Result handler(RuntimeException e) throws IOException {
         log.error("Runtime exception:-------------->",e);
         return Result.fail(e.getMessage());
     }
    }

    Above we caught several exceptions:

    • ShiroException: Exception thrown by shiro, such as no permission, user login exception

    • IllegalArgumentException: Handling Assert exception

    • MethodArgumentNotValidException: Exception handling entity verification

    • RuntimeException: catch other exceptions

      1. Entity verification in springboot

      Using the springboot framework, Hibernate validator is automatically integrated.

      Step 1: Add validation rules to entity attributes

      @TableName("m_user")
      public class User implements Serializable {
      private static final long serialVersionUID = 1L;
      @TableId(value = "id", type = IdType.AUTO)
      private Long id;
      
      @NotBlank(message = "Nickname cannot be blank")
      private String username;
      private String avatar;
      
      @NotBlank(message = "The mailbox cannot be blank")
      @Email(message = "The email format is incorrect")
      private String email;
      }

      Step 2: Test entity verification

      Using the @Validated annotation, if any entity does not comply with the verification rules, an exception will be thrown and captured in MethodArgumentNotValidException in exception handling.

      @PostMapping("/save")
      public Object save(@Validated @RequestBody User user) {
      
         return user.toString();
      }

      4. Cross-domain processing with front-end and back-end separation

      Perform global cross-domain processing in the background “`java /**

    • Solve cross-domain issues

    • project: vue-mt-blog

    • created by Maotao on 2020/6/30

    • / @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) {

      registry.addMapping("/**").
              allowedOrigins("*").
              allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS").
              allowCredentials(true).
              maxAge(3600).
              allowedHeaders("*");

      } }

Global cross-domain processing
```java
/**
 * Solve cross-domain issues
 * project: vue-mt-blog
 * created by Maotao on 2020/6/30
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").
                allowedOrigins("*").
                allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS").
                allowCredentials(true).
                maxAge(3600).
                allowedHeaders("*");
    }
}

This article is published by OpenWrite, a blog posting platform!