《SpringBoot2.0 实战》系列-整合shiro、Jwt实现鉴权验证,及Jwt续期实现

简介

Apache Shiro 是 Java 的一个安全框架,相对于SpringSecurity更简单、轻量。需要整合SpringSecurity的可移步《springBoot整合springsecurity、jwt-token实现权限验证》。本文主要介绍shiro的使用。
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。

分析

首先我们需要了解shiro的三大主体。

1、Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject;所有 Subject 都绑定到SecurityManager。
2、SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有Subject;是 Shiro 的核心,它负责与后边介绍的其他组件进行交互;
3、Realm:域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;

其次需要了解Jwt相关知识:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

了解这些后,我们就直接开始整合。

初始准备

项目中增加如下shiro和JWT的jar依赖。

<!-- shiro spring. -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>${
    
    shiro.version}</version>
</dependency>
<!-- JWT -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>${
    
    jwt.version}</version>
</dependency>

增加鉴权配置

# 鉴权
auth:
  # JWT
  jwt:
    # jwt自定义请求头key
    header: jwt-token
    # 秘钥
    encryptJWTKey: U0JBUElKV1RkV2FuZzkyNjQ1NA==
    # JWT过期时间(单位s)
    accessTokenExpireTime: 300
    # JWT续期时间(单位s),即token过期后,此时间内操作会自动续期。
    refreshTokenExpireTime: 1800
  # 不需要鉴权的路径
  ignores:
    - "/"
    - "/imgs/**"
    - "*.css"
    - "*.js"
    - "*.gif"
    - "*.jpg"
    - "*.png"
    - "*.ico"
    - "/favicon.ico"
    - "/actuator/**"
    - "/swagger-ui.html"
    - "/doc.html"
    - "/swagger-resources/**"
    - "/service-worker.js"
    - "/v2/**"
    - "/webjars/**"

配置属性类,接收配置属性。

@Data
@Component
@ConfigurationProperties(prefix = "auth")
public class AuthProperties {
    
    
    /**
     * 忽略校验的路径
     */
    private String[] ignores;
    /**
     * jwt相关配置
     */
    private JwtConfig jwt;

    @Data
    public static class JwtConfig {
    
    
        /**
         * jwt自定义请求头key
         */
        private String header;
        /**
         * 密钥
         */
        private String encryptJWTKey;
        /**
         * JWT过期时间(单位s)
         */
        private Long accessTokenExpireTime ;
        /**
         * JWT续期时间(单位s),即token过期后,此时间内操作会自动续期。
         */
        private Long refreshTokenExpireTime;
    }
}

核心配置

JWT核心类,可根据实际情况调整。

@Data
public class JwtClaim {
    
    
    /**
     * 此处为用户Id
     */
    private String subject;
    /**
     * 承租人id
     */
    private Long tenantId;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 账号
     */
    private String account;
    /**
     * 角色
     */
    private String[] roles;
    /**
     * 权限
     */
    private String[] permissions;
}
@Data
@NoArgsConstructor
@ApiModel(value = "用户Token", description = "用户Token")
public class JwtToken implements AuthenticationToken{
    
    

    @ApiModelProperty("密钥")
    private String accessToken;

    @ApiModelProperty("承租人id")
    private Long tenantId;

    @ApiModelProperty("用户id")
    private Long userId;

    @ApiModelProperty("用户名")
    private String userName;

    @Override
    @JsonIgnore
    public Object getPrincipal() {
    
    
        return accessToken;
    }

    @Override
    @JsonIgnore
    public Object getCredentials() {
    
    
        return accessToken;
    }

    public  JwtToken(String accessToken,Long userId,String userName){
    
    
        this.accessToken = accessToken;
        this.userId = userId;
        this.userName = userName;
    }
    public  JwtToken(String accessToken,Long tenantId,Long userId,String userName){
    
    
        this.accessToken = accessToken;
        this.tenantId=tenantId;
        this.userId = userId;
        this.userName = userName;
    }
    public JwtToken(String accessToken){
    
    
        this.accessToken = accessToken;
    }
}

核心工具类

@Slf4j
public class JwtUtil {
    
    

    private AuthProperties authProperties;

    public JwtUtil(AuthProperties authProperties){
    
    
        this.authProperties = authProperties;
    }


    /**
     * 过期时间改为从配置文件获取
     */
    private static Long accessTokenExpireTime;
    /**
     * RefreshToken过期时间-30分钟-30*60(秒为单位)
     */
    private static Long refreshTokenExpireTime;

    /**
     * JWT认证加密私钥(Base64加密)
     */
    private static String encryptJWTKey;

    @PostConstruct
    public void init(){
    
    
        encryptJWTKey = authProperties.getJwt().getEncryptJWTKey();
        accessTokenExpireTime = authProperties.getJwt().getAccessTokenExpireTime();
        refreshTokenExpireTime = authProperties.getJwt().getRefreshTokenExpireTime();
    }

    /**
     * 获取当前用户信息
     *
     * @return
     */
    public static JwtToken getCurrentUser(){
    
    
        Object principal = SecurityUtils.getSubject().getPrincipal();
        if(principal == null ){
    
    
            return null;
        }
        String accessToken = principal.toString();
        Map<String, Claim> claims = getClaims(accessToken);
        Long userId = Long.valueOf(getSubject(accessToken));
        String username = claims.get(JwtConstant.JWT_USER_NAME).asString();
        Long tenantId = claims.get(JwtConstant.JWT_TENANT_ID).asLong();
        return new JwtToken(accessToken,tenantId,userId,username);
    }

    /**
     * 校验token是否正确
     * @param token Token
     * @return boolean 是否正确
     * @author Wang926454
     * @date 2018/8/31 9:05
     */
    public static boolean verify(String token) {
    
    
        try {
    
    
            // 帐号加JWT私钥解密
            String secret = getSubject(token) + Base64ConvertUtil.decode(encryptJWTKey);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(token);
            return true;
        } catch (UnsupportedEncodingException e) {
    
    
            log.error("JWTToken认证解密出现UnsupportedEncodingException异常:{}", e.getMessage());
            throw ResponseEnum.INTERNAL_SERVER_ERROR.newException("JWTToken认证解密异常");
        }catch (TokenExpiredException e){
    
    
            throw new TokenExpiredException("JWTToken过期");
        }
    }

    /**
     * 获得Token中的信息无需secret解密也能获得
     * @param token
     * @param claim
     * @return java.lang.String
     */
    public static Object getClaimString(String token, String claim) {
    
    
        try {
    
    
            DecodedJWT jwt = JWT.decode(token);
            // 只能输出String类型,如果是其他类型返回null
            return jwt.getClaim(claim).asString();
        } catch (JWTDecodeException e) {
    
    
            log.error("解密Token中的公共信息出现JWTDecodeException异常:{}", e.getMessage());
            throw ResponseEnum.INTERNAL_SERVER_ERROR.newException("解密Token中的公共信息异常");
        }
    }
    /**
     * 获得Token中的信息无需secret解密也能获得
     * @param token
     * @param claim
     * @return java.lang.String
     */
    public static Object getClaimLong(String token, String claim) {
    
    
        try {
    
    
            DecodedJWT jwt = JWT.decode(token);
            // 只能输出String类型,如果是其他类型返回null
            return jwt.getClaim(claim).asLong();
        } catch (JWTDecodeException e) {
    
    
            log.error("解密Token中的公共信息出现JWTDecodeException异常:{}", e.getMessage());
            throw ResponseEnum.INTERNAL_SERVER_ERROR.newException("解密Token中的公共信息异常");
        }
    }
    /**
     * 获得Token中的信息
     * @param token
     * @return java.lang.String
     */
    public static Map<String, Claim> getClaims(String token) {
    
    
        try {
    
    
            DecodedJWT jwt = JWT.decode(token);
            // 只能输出String类型,如果是其他类型返回null
            return jwt.getClaims();
        } catch (JWTDecodeException e) {
    
    
            log.error("解密Token中的公共信息出现JWTDecodeException异常:{}", e.getMessage());
            throw ResponseEnum.INTERNAL_SERVER_ERROR.newException("解密Token中的公共信息异常");
        }
    }


    /**
     * 获得Token中的subject,即userId
     * @param token
     * @return java.lang.String
     */
    public static String getSubject(String token) {
    
    
        try {
    
    
            DecodedJWT jwt = JWT.decode(token);
            // 只能输出String类型,如果是其他类型返回null
            return jwt.getSubject();
        } catch (JWTDecodeException e) {
    
    
            log.error("解密Token中的公共信息出现JWTDecodeException异常:{}", e.getMessage());
            throw ResponseEnum.INTERNAL_SERVER_ERROR.newException("解密Token中的公共信息异常");
        }
    }

    /**
     * 生成Token
     *
     * @param jwtClaim
     * @return 返回加密的Token
     */
    public static String generateToken(JwtClaim jwtClaim) {
    
    
        try {
    
    
            // 帐号加JWT私钥加密
            String secret = jwtClaim.getSubject() + Base64ConvertUtil.decode(encryptJWTKey);
            long currentTimeMillis = System.currentTimeMillis();
            // 此处过期时间是以毫秒为单位,所以乘以1000
            Date date = new Date( currentTimeMillis + accessTokenExpireTime * 1000);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            String token = JWT.create()
                    .withClaim(JwtConstant.JWT_USER_NAME, jwtClaim.getUserName())
                    .withClaim(JwtConstant.JWT_USER_ACCOUNT, jwtClaim.getAccount())
                    .withArrayClaim(JwtConstant.JWT_ROLES_KEY, jwtClaim.getRoles())
                    .withArrayClaim(JwtConstant.JWT_PERMISSIONS_KEY, jwtClaim.getPermissions())
                    .withClaim(JwtConstant.JWT_CURRENT_TIME_MILLIS, currentTimeMillis)
                    .withClaim(JwtConstant.JWT_TENANT_ID, jwtClaim.getTenantId())
                    .withSubject(jwtClaim.getSubject())
                    .withExpiresAt(date)
                    .sign(algorithm);
            // 设置到redis缓存,key和value均为jwt-token,过期时间设置为 过期时间 + 续期时间
            RedisUtil.setStrExpire(token, token,accessTokenExpireTime + refreshTokenExpireTime);
            return token;
        } catch (UnsupportedEncodingException e) {
    
    
            log.error("JWTToken加密出现UnsupportedEncodingException异常:{}", e.getMessage());
            throw ResponseEnum.INTERNAL_SERVER_ERROR.newException("JWTToken加密异常");
        }
    }

    /**
     * 刷新token
     * @param token
     * @return java.lang.String 返回加密的Token
     */
    public static String refreshToken(String token) {
    
    
        String subject = getSubject(token);
        // 获取token的属性
        Map<String, Claim> claims = getClaims(token);
        JwtClaim jwtClaim = new JwtClaim();
        jwtClaim.setSubject(subject);
        jwtClaim.setUserName(claims.get(JwtConstant.JWT_USER_NAME).asString());
        jwtClaim.setAccount(claims.get(JwtConstant.JWT_USER_ACCOUNT).asString());
        jwtClaim.setRoles(claims.get(JwtConstant.JWT_ROLES_KEY).asArray(String.class));
        jwtClaim.setPermissions(claims.get(JwtConstant.JWT_PERMISSIONS_KEY).asArray(String.class));
        jwtClaim.setTenantId(claims.get(JwtConstant.JWT_TENANT_ID).asLong());
        // 重新生成token
        return generateToken(jwtClaim);
    }
    /**
     * 续期token
     * @param oldToken
     * @return java.lang.String 返回加密的Token
     */
    public static Boolean reNewToken(String oldToken) {
    
    
        // 重新生成token
        String refreshToken = refreshToken(oldToken);
        // 续期原token的过期时间,并更新value为新token
      RedisUtil.setStrExpire(oldToken,refreshToken,accessTokenExpireTime + refreshTokenExpireTime);
        return Boolean.TRUE;
    }
}

Shiro核心配置类

@Slf4j
@Configuration
@Import(AuthProperties.class)
public class ShiroConfig {
    
    
    public static final String JWT = "jwt";
    public static final String ANON = "anon";
    public static final String ALL_PATH_KEY = "/**";
    @Bean
    public ShiroFilterFactoryBean shiroFilter(AuthProperties authProperties, @Qualifier("securityManager") DefaultSecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>(4);
        filterMap.put(JWT, new JwtAuthFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        // 获取到不需要鉴权的路径
        if(authProperties.getIgnores() != null ){
    
    
            List<String> ignores = Arrays.asList(authProperties.getIgnores());
            if(CollectionUtils.isNotEmpty(ignores)){
    
    
                ignores.forEach(e->filterRuleMap.put(e, ANON));
            }
        }
        // 过滤链定义,从上向下顺序执行,jwt过滤器放在最下边
        filterRuleMap.put(ALL_PATH_KEY, JWT);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilterFactoryBean;
    }
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultSecurityManager(ShiroRealm shiroRealm) {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm);
        // 关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    
    
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    
    
        return new LifecycleBeanPostProcessor();
    }
    /**
     * 开启Shiro注解模式,可以在Controller中的方法上添加注解
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
    
    
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
    @Bean
    public ShiroRealm getShiroRealm() {
    
    
        return new ShiroRealm();
    }
    /**
     * jwt工具类
     * @param authProperties
     * @return
     */
    @Bean
    public JwtUtil getJwtUtil(AuthProperties authProperties) {
    
    
        return new JwtUtil(authProperties);
    }
}

核心校验类

public class ShiroRealm extends AuthorizingRealm {
    
    
    @Override
    public boolean supports(AuthenticationToken token) {
    
    
        // 表示此Realm只支持JWTToken类型
        return token instanceof JwtToken;
    }
    /**
     * 默认使用此方法进行用户正确与否验证,错误抛出异常即可
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws UnauthorizedException {
    
    
        String token = auth.getCredentials().toString();
        // 开始认证,要AccessToken认证通过
        if (StringUtils.isNotBlank(RedisUtil.getStr(token)) && JwtUtil.verify(RedisUtil.getStr(token))) {
    
    
            return new SimpleAuthenticationInfo(token, token, this.getClass().getName());
        }
        throw new AuthenticationException("Token验证失败(Token expired or incorrect.)");
    }
    /**
     * 此方法调用  hasRole,hasPermission的时候才会进行回调.
     *
     * 权限信息.(授权):
     * 1、如果用户正常退出,缓存自动清空;
     * 2、如果用户非正常退出,缓存自动清空;
     * 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。
     * (需要手动编程进行实现;放在service进行调用)
     * 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,
     * 调用clearCached方法;
     * :Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    
    

        String accessToken = principals.toString();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        Map<String, Claim> claims = JwtUtil.getClaims(accessToken);
        if(MapUtils.isEmpty(claims)){
    
    
            throw new UnauthorizedException("Token验证失败(Token expired or incorrect.)");
        }
        // 解析角色和权限
        List<String> roles = claims.get(JwtConstant.JWT_ROLES_KEY).asList(String.class);
        List<String> permissions = claims.get(JwtConstant.JWT_PERMISSIONS_KEY).asList(String.class);
        simpleAuthorizationInfo.addRoles(roles);
        simpleAuthorizationInfo.addStringPermissions(permissions);
        return simpleAuthorizationInfo;
    }
}

核心过滤器
代码的执行流程:preHandle->isAccessAllowed->isLoginAttempt->executeLogin

@Component
@Slf4j
public class  JwtAuthFilter extends BasicHttpAuthenticationFilter {
    
    

    @Autowired
    private AuthProperties authProperties;

    /**
     * 判断用户是否想要登入
     * 检测header里面是否包含token即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
    
    
        if(authProperties == null ){
    
    
            authProperties = SpringContextHolder.getBean("authProperties",AuthProperties.class);
        }
        HttpServletRequest req = (HttpServletRequest) request;
        String jwtToken = req.getHeader(authProperties.getJwt().getHeader());
        return StringUtils.isNotEmpty(jwtToken);
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response){
    
    
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String accessToken = httpServletRequest.getHeader(authProperties.getJwt().getHeader());
        JwtToken jwtToken = new JwtToken(accessToken);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问 例如我们提供一个地址 GET /article 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西 所以我们在这里返回true,Controller中可以通过
     * subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    
    
        // 查看当前Header中是否携带Authorization属性(Token),有的话就进行登录认证授权
        if (this.isLoginAttempt(request, response)) {
    
    
            try {
    
    
                // 进行Shiro的登录Realm
                return this.executeLogin(request, response);
            } catch (Exception e) {
    
    
                // 认证出现异常,传递错误信息msg
                String msg = e.getMessage();
                // 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
                Throwable throwable = e.getCause();
                if (throwable instanceof SignatureVerificationException) {
    
    
                    // 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
                    msg = "Token或者密钥不正确(" + throwable.getMessage() + ")";
                } else if (throwable instanceof TokenExpiredException) {
    
    
                    // 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新
                    HttpServletRequest req = (HttpServletRequest) request;
                    String jwtToken = req.getHeader(authProperties.getJwt().getHeader());
                    if (RedisUtil.existStrAny(jwtToken)) {
    
    
                        log.info("Token自动续期");
                        JwtUtil.reNewToken(jwtToken);
                        // 进行Shiro的登录Realm
                        return this.executeLogin(request, response);
                    }else {
    
    
                        msg = "Token已过期(" + throwable.getMessage() + ")";
                    }
                } else {
    
    
                    // 应用异常不为空
                    if (throwable != null) {
    
    
                        // 获取应用异常msg
                        msg = throwable.getMessage();
                    }
                }
                // Token认证失败直接返回Response信息
                this.response401(response, msg);
                return false;
            }
        } else {
    
    
            // 没有携带Token
            HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
            // 如果是feign调用不行鉴权
            String tokenIgnoreFlag = httpServletRequest.getHeader(HeaderConstant.HEADER_TOKEN_IGNORE);
            if(HeaderConstant.TOKEN_IGNORE_FLAG.equals(tokenIgnoreFlag)){
    
    
                return true;
            }
            // 获取当前请求类型
            String httpMethod = httpServletRequest.getMethod();
            // 获取当前请求URI
            String requestURI = httpServletRequest.getRequestURI();
            log.info("当前请求 {} Authorization属性(Token)为空 请求类型 {}", requestURI, httpMethod);
            this.response401(response, "请先登录");
            return false;
        }
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
    
    
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) 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"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
    
    
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 无需转发,直接返回Response信息
     */
    private void response401(ServletResponse response, String msg) {
    
    
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        try (PrintWriter out = httpServletResponse.getWriter()) {
    
    
            String data = JsonConvertUtil.objectToJson( BaseResponse.fail(ResponseEnum.UNAUTHORIZED, "无权访问(Unauthorized):" + msg));
            out.append(data);
        } catch (IOException e) {
    
    
            log.error("直接返回Response信息出现IOException异常:{}", e.getMessage());
            throw ResponseEnum.INTERNAL_SERVER_ERROR.newException("直接返回Response信息出现IOException异常");
        }
    }
}

Token续期

Token续期逻辑,代码中均已实现,此处梳理下逻辑:

1、配置token过期时间、续期时间
2、登录生成token时,设置过期时间为配置的token过期时间,并以生成的 token为key和value设置到redis,同时设置过期时间为配置的过期时间+续期时间。
在这里插入图片描述
3、请求操作校验token时,捕获过期异常TokenExpiredException,校验redis中是否还存在当前token,若存在则续期,生成新的token,并以原token为key,新token为value,过期时间为配置的过期时间+续期时间,重新设置到redis。
在这里插入图片描述

登录Demo

生成JWT时需要获取roles、permissons等信息,如果不设置到JWT,需要每次校验权限的时候去查询数据库,此处直接将数据设置到JWT,使用时直接解析即可。
以下是简单逻辑,具体值需要自己根据业务获取。

public JwtToken login(String account, String password) {
    
    
        // 业务逻辑....
        // 生成token
        JwtClaim jwtClaim = new JwtClaim();
        jwtClaim.setSubject(user.getId().toString());
        jwtClaim.setUserName(user.getName());
        jwtClaim.setAccount(user.getAccount());
        jwtClaim.setRoles(roleCodeArray);
        jwtClaim.setPermissions(permissionArray);
        jwtClaim.setTenantId(user.getTenantId());
        String accessToken = JwtUtil.generateToken(jwtClaim);
        JwtToken jwtUser = new JwtToken(accessToken,user.getId(),user.getName());
        return jwtUser;
    }

测试返回结果如下:
在这里插入图片描述

结语

至此,整合结束。如有错误之处,欢迎指正。

代码摘自本人的开源项目cloud-plus:https://blog.csdn.net/HXNLYW/article/details/104635673

猜你喜欢

转载自blog.csdn.net/HXNLYW/article/details/109119370