Shiro, JWT, redis integration

Time:2021-12-22

Shiro, JWT, redis integration

1、 Steps

1. Import coordinates

org.crazycake
    shiro-redis-spring-boot-starter
    3.2.1

 
 
     io.jsonwebtoken
     jjwt
     0.9.1

2. Write shiroconfig

Purpose:Realize session sharing

Session sharing

The HTTP protocol (1.1) is statelessTherefore, when the server needs to identify user access, it should make corresponding records to track user operations. This implementation mechanism is session. When a user accesses the server for the first time, the server will create a session for the user. Each session has a unique sessionid (application level) to identify the user.

Therefore, use Shiro to inherit redis

Using redis to manage sessions can facilitate the implementation of session clusters, and sessions will not be lost when the service is restarted (or the server is restarted).

package com.gyb.config;

import com.gyb.shiro.AccountRealm;
import com.gyb.shiro.JwtFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Autowired
    JwtFilter jwtFilter;

    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }

    @Bean
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                   SessionManager sessionManager,
                                                   RedisCacheManager redisCacheManager) {

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);

        securityManager.setSessionManager(sessionManager);

        securityManager.setCacheManager(redisCacheManager);
        return securityManager;
    }

    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        Map filterMap = new LinkedHashMap<>();

        //Leave all the filtering to the custom JWT
        filterMap.put("/**", "jwt");
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }

    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        Map filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);

        Map filterMap = shiroFilterChainDefinition.getFilterChainMap();

        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

}

3. Solve cross domain problems

(it will be solved later in the filter: because the filter is executed before the interceptor, it may be directly filtered out)

Step analysis
During spring initialization, the configuration information of corsmapping is loaded and registered in the IOC container. The specific instance and information encapsulation are: requestmappinghandlermapping – > corsconfigurationsource – > corsconfigurations

During interface access, in the dodispatch method in the dispatchservlet, get the handlerexecutionchain through gethandler. The CORS configuration information is encapsulated in the handlerexecutionchain instance, and this in the gethandler method You can see the previously registered requestmappinghandlermapping in the handlermappings collection

package com.gyb.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 *Solve cross domain problems
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                //Allow sending cookies
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

4. Create Shiro package

Create in package

AccountProfile:The first parameter returned when overriding the dogetauthenticationinfo authentication method

AccountRealm:User defined domain class (mainly implementing authentication and authorization methods)

*** JwtFifter:** 1. Store the token in the request (using jwttoken class)

​ 2. Judge whether the token has expired. If it has not, execute executelogin, and then go step by step to the two methods implemented in the domain class written by yourself

​ 3. Rewrite the login failure method and return the error message: result = result fail(throwable.getMessage());

​ 4. Solve cross domain problems

JwtToken: encapsulate the value of authorization in the request field.

4.1 create accountprofile class

package com.gyb.shiro;

import lombok.Data;

import java.io.Serializable;

@Data
public class AccountProfile implements Serializable {

    private Long id;

    private String username;

    private String avatar;

    private String email;

}

4.2 create accountrealm class

package com.gyb.shiro;

import cn.hutool.core.bean.BeanUtil;
import com.gyb.entity.User;
import com.gyb.service.UserService;
import com.gyb.util.JwtUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AccountRealm extends AuthorizingRealm {

    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    UserService userService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

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

    /**
     *Authentication
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        JwtToken jwtToken = (JwtToken) token;

        String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();

        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");
        }

        AccountProfile profile = new AccountProfile();
        BeanUtil.copyProperties(user, profile);

        return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
    }
}

4.3 create jwtfifth class

package com.gyb.shiro;

import cn.hutool.json.JSONUtil;
import com.gyb.common.lang.Result;
import com.gyb.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtFilter extends AuthenticatingFilter {

    @Autowired
    JwtUtils jwtUtils;

    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(!StringUtils.hasLength(jwt) ) {
            return null;
        }

        return new JwtToken(jwt);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(!StringUtils.hasLength(jwt)) {
            return true;
        } else {

            //Check JWT
            Claims claim = jwtUtils.getClaimByToken(jwt);
            if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                Throw new expiredcredentialsexception ("token has expired, please log in again");
            }

            //Execute login
            return executeLogin(servletRequest, servletResponse);
        }
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        Throwable throwable = e.getCause() == null ? e : e.getCause();
        Result result = Result.fail(throwable.getMessage());
        String json = JSONUtil.toJsonStr(result);

        try {
            httpServletResponse.getWriter().print(json);
        } catch (IOException ioException) {

        }
        return false;
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        //When cross domain, we will first send an options request. Here, we directly return to the normal state for the options request
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

4.4 create jwttoken class

package com.gyb.shiro;

import org.apache.shiro.authc.AuthenticationToken;

public class JwtToken implements AuthenticationToken {

    private String token;

    public JwtToken(String jwt) {
        this.token = jwt;
    }

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

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

5. Create util package

JwtUtil:1. Generate JWT when the user calls the login interface

​ 2. The method of obtaining the claims object of token. Claims can be understood as JWT plaintext structure, which can be used to obtain the current request during verification User payload information.

​ 3. Determine whether the token has expired

ShiroUtil: there is only one line of code. I want to simplify =, (you can also not write this kind of code)

​ Used to get the principal of the current user

5.1 JwtUtil

package com.gyb.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 *JWT tool class
 */
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "gyb.jwt")
public class JwtUtils {

    private String secret;
    private long expire;
    private String header;

    /**
     *Generate JWT token
     */
    public String generateToken(long userId) {
        Date nowDate = new Date();
        //Expiration time
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(userId+"")
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            log.debug("validate is token error ", e);
            return null;
        }
    }

    /**
     *Whether the token expires
     *@ return true: expired
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
}

5.2 ShiroUtil

package com.gyb.util;import com.gyb.shiro.AccountProfile;import org.apache.shiro.SecurityUtils;public class ShiroUtil {    public static AccountProfile getProfile() {        return (AccountProfile) SecurityUtils.getSubject().getPrincipal();    }}