springboot3+jdk17+shiro+jwt+redis

springboot3+jdk17整合jwt+shiro+redis实现登录认证

注意,jdk17的规范是Jakarta EE,虽然最新版本shiro适配springboot3,但是部分包要单独适配

  • 首先讲一下整体流程
  1. 用户首次登录时,会发送一个包含用户名和密码的请求到服务器。这个请求通常是一个POST请求,发送到一个特定的登录URL,例如 /user/login。
  2. 服务器接收到请求后,会验证用户名和密码。如果验证成功,服务器会生成一个JWT,并将其发送回用户。这个JWT包含了用户的一些信息,例如用户ID,以及一个签名。签名是用服务器的私钥生成的,可以用来验证JWT的真实性和完整性。
  3. 用户收到JWT后,会将其存储在本地,例如在浏览器的localStorage中。然后,每次发送请求到服务器时,都会在请求的Header中携带这个JWT,通常是在 Authorization 字段中。
  4. 服务器接收到请求后,会首先检查请求的Header中是否包含JWT。如果没有包含,那么服务器就会拒绝这个请求,因为它无法验证请求的来源。如果包含了JWT,那么服务器就会验证这个JWT的签名。如果签名验证失败,那么服务器也会拒绝这个请求,因为这意味着JWT可能被篡改。如果签名验证成功,那么服务器就会从JWT中提取出用户信息,然后处理这个请求。
  5. 在处理请求时,服务器可能还需要进行授权检查,例如检查用户是否有权限访问某个资源。这个检查通常是通过查询数据库来完成的。
  6. 如果用户长时间没有活动,那么服务器会认为用户已经退出登录,此时的JWT就会过期。用户再次发送请求时,服务器就会发现JWT已经过期,然后拒绝这个请求。用户需要重新登录,以获取新的JWT。
  7. 在每次用户请求时,服务器会检查他们的JWT令牌是否即将过期。如果令牌即将过期,并且用户在Redis中被标记为活跃,那么服务器会自动为他们续签令牌。这个过程在JwtFilter类的onAccessDenied方法中实现。
  8. 如果用户在过去2小时内有任何活动,那么他们会被标记为活跃用户。这是通过在每次用户请求时调用markUserActive方法来实现的。这个方法会在Redis中设置一个键,键的格式是 "active_users:" 加上当前的日期和时间,并将这个键对应的位(由用户ID指定)设置为 true。这个键的过期时间为2小时。
  9. 如果用户在过去2小时内没有任何活动,那么他们的活跃状态就会被移除,即Redis中对应的键会过期并被删除。如果这时用户发送的请求中的JWT令牌即将过期,那么服务器不会为他们续签令牌,而是要求他们重新登录。
  • 整个认证流程登录和登录之后每一次的认证是不一样的,

  • 登录后由工具类jwtUtil生成一个token,返回给前端用于之后每次请求

引入依赖

  • pom.xml,注意jwt相关包版本不能太低

    <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                <version>3.5.5</version>
            </dependency>
    <!-- shiro -->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <classifier>jakarta</classifier>
                <version>1.12.0</version>
                <!-- 排除仍使用了javax.servlet的依赖 -->
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.shiro</groupId>
                        <artifactId>shiro-core</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>org.apache.shiro</groupId>
                        <artifactId>shiro-web</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
            <!-- 引入适配jakarta的依赖包 -->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-core</artifactId>
                <classifier>jakarta</classifier>
                <version>1.12.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-web</artifactId>
                <classifier>jakarta</classifier>
                <version>1.12.0</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.apache.shiro</groupId>
                        <artifactId>shiro-core</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    <!--        jwt创建、解析、验证 JWT 的功能,并且支持对 JWT 进行签名和加密 -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.7.0</version>
            </dependency>
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.4.0</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!--       jedis 是 Redis 官方推荐的 Java 客户端,提供了比较全面的 Redis 命令的支持。-->
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
            </dependency>
    	<dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.14.2</version>
            </dependency>
    

ShiroConfig

  • config目录下创建一个Shiro配置类

    java 复制代码
    package com.example.englishhub.config;
    
    /**
     * @Author: hahaha
     * @Date: 2024/4/11 16:29
     */
    
    import com.example.englishhub.security.JwtFilter;
    import com.example.englishhub.security.JwtRealm;
    import jakarta.servlet.Filter;
    import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
    import org.apache.shiro.mgt.DefaultSubjectDAO;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    
    /**
     * shiro的三个重要配置
     * 1、Realm
     * 2、DefaultWebSecurityManager
     * 3、ShiroFilterFactoryBean
     */
    
    /**
     1.用户请求,不携带token,就在JwtFilter处进行错误处理返回,让它去登陆
     2.用户请求,携带token,就到JwtFilter中获取jwt,使用JwtRealm进行认证,若token过期则返回401状态码
     3.在JwtRealm中进行认证判断这个token是否有效,
     */
    
    @Configuration
    public class ShiroConfig {
    
        @Bean("securityManager")
        public DefaultWebSecurityManager securityManager(@Qualifier("jwtRealm") JwtRealm jwtRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 设置自定义Realm
            securityManager.setRealm(jwtRealm);
            // 关闭shiroDao功能,关闭session
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            // 不需要将ShiroSession中的东西存到任何地方包括Http Session中)
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
            // securityManager.setSubjectFactory(subjectFactory());
            return securityManager;
        }
    
        @Bean("shiroFilterFactoryBean")
        public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
    
            /**
             *  注册jwt过滤器,除/login,/register外都先经过jwtFilter
             *
             *   先经过过滤器,如果检测到请求头存在 token,则用 token 去 login,接着走 Realm 去验证
             */
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
            HashMap<String, Filter> filterMap = new HashMap<>();
            filterMap.put("jwt", new JwtFilter());
            shiroFilter.setFilters(filterMap);
            LinkedHashMap<String, String> map = new LinkedHashMap<>();
    
            map.put("/user/login", "anon");
            map.put("/user/register", "anon");
            // 所有请求通过我们自己的JWT Filter
            map.put("/**", "jwt");
            shiroFilter.setFilterChainDefinitionMap(map);
            return shiroFilter;
        }
    
        /**
         * 解决@RequiresAuthentication注解不生效的配置
         */
        @Bean("lifecycleBeanPostProcessor")
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        @Bean
        @DependsOn({"lifecycleBeanPostProcessor"})
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        /**
         * 为Spring-Bean开启对Shiro注解的支持
         */
        @Bean("authorizationAttributeSourceAdvisor")
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    }
  • 在启动后,首先加载 securityManager,关闭了原有的session(因为使用了jwtToken),将自定义的Realm注入(在其中完成对jwtToken的验证)

  • 之后添加我们自定义的jwt过滤器,起到拦截请求的作用(就相当于拦截器),排除特殊接口如登录注册以及接口文档

JwtToken

java 复制代码
package com.example.englishhub.security;

/**
 * @Author: hahaha
 * @Date: 2024/4/11 17:26
 */

import org.apache.shiro.authc.AuthenticationToken;

/**
 * 继承AuthenticationToken,跟JwtRealmh中的doGetAuthenticationInfo的参数类型保持一致
 */
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;
    }
}

JwtFilter

  • 我在项目目录下新建了一个 security存放安全认证相关的类

    package com.example.englishhub.security;
    
    import com.example.englishhub.exception.JwtValidationException;
    import com.example.englishhub.utils.Result;
    import com.example.englishhub.utils.ResultType;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import jakarta.servlet.ServletOutputStream;
    import jakarta.servlet.ServletRequest;
    import jakarta.servlet.ServletResponse;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.web.filter.AccessControlFilter;
    import org.apache.shiro.web.util.WebUtils;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    
    import javax.security.sasl.AuthenticationException;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    
    /**
     * @Description: JwtFilter
     * shiro的过滤器,用于拦截请求,
     * @Author: hahaha
     * @Date: 2024/4/11 17:26
     */
    
    @Slf4j
    @Component
    public class JwtFilter extends AccessControlFilter {
    
        /**
         * isAccessAllowed()判断是否携带了有效的JwtToken
         * onAccessDenied()是没有携带JwtToken的时候进行账号密码登录
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
            /**
             * 1. 返回true,shiro就直接允许访问url
             * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
             *  这里先让它始终返回false来使用onAccessDenied()方法
             *  如果带有 token,则对 token 进行检查,否则直接通过
             *
             */
            try {
                onAccessDenied(servletRequest, servletResponse);
    //            return true;
            } catch (Exception e) {
                log.error("isAccessAllowed error:", e);
                responseError(servletResponse, ResultType.UNAUTHORIZED.getCode(), "Authentication failed: " + e.getMessage());
    //            return false;
            }
            return true;
        }
    
    
        /**
         * @param servletRequest
         * @param servletResponse
         * @throws Exception
         * @return 返回结果为true表明登录通过
         */
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            /**
             *  跟前端约定将jwtToken放在请求的Header的token中,token:token
             */
            log.info("onAccessDenied方法被调用");
    //        HttpServletResponse response = (HttpServletResponse) servletResponse;
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String token = request.getHeader("token");
            //如果token为空的话,返回true,交给控制层进行判断;也会达到没有权限的作用
            if (token == null) {
                responseError(servletResponse, ResultType.UNAUTHORIZED.getCode(), "No token provided");
                return false;
            }
            JwtToken jwtToken = new JwtToken(token);
            try {
                //进行登录处理,委托realm进行登录认证,调用JwtRealm进行的认证,doGetAuthenticationInfo
                getSubject(servletRequest, servletResponse).login(jwtToken);
                return true;
            } catch (JwtValidationException e) {
                // Handle specific custom exceptions
                responseError(servletResponse, e.getStatusCode(), e.getMessage());
                return false;
            }
    //        catch (AuthenticationException e) {
    //            responseError(servletResponse, ResultType.UNAUTHORIZED.getCode(), "Authentication failed: " + e.getMessage());
    //            return false;
    //        }
        }
    
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            System.out.println("进入拦截器");
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;
    
            // 设置CORS头部
            resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
            resp.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            resp.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,token");
            resp.setHeader("Access-Control-Allow-Credentials", "true");
    
            if ("OPTIONS".equals(req.getMethod())) {
                resp.setStatus(HttpServletResponse.SC_OK);
                return false; // 阻止后续的过滤器链执行
            }
    
            return super.preHandle(request, response);
        }
    
        //失败要执行的方法
        private void responseError(ServletResponse response, String statusCode, String message) throws IOException {
    
            HttpServletResponse resp = WebUtils.toHttp(response);
            resp.setStatus(HttpStatus.UNAUTHORIZED.value());
            resp.setCharacterEncoding("UTF-8");
            resp.setContentType("application/json; charset=utf-8");
            ObjectMapper mapper = new ObjectMapper();
    
            try (ServletOutputStream out = resp.getOutputStream()) {
                Result<String> result = new Result<>();
                result.setStatusCode(statusCode);
                result.setMessage(message);
                String json = mapper.writeValueAsString(result);
                out.write(json.getBytes(StandardCharsets.UTF_8));
            } catch (IOException e) {
                throw new AuthenticationException("Response writing failed with IOException: " + e.getMessage());
            }
        }
    }
    
  • 在登录之后的每次请求都会携带 token,这可以通过前端配置一个请求拦截器实现,在后端从header头中取出该token,通过JwtToken类封装成shrio接受的token,委托 JwtRealm认证,传入JwtToken

JwtRealm

java 复制代码
package com.example.englishhub.security;

import com.example.englishhub.exception.JwtValidationException;
import com.example.englishhub.service.UserService;
import com.example.englishhub.utils.JwtUtil;
import com.example.englishhub.utils.ResultType;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Description: JwtRealm
 * shiro的Realm,用于处理JwtToken的认证和授权
 * @Author: hahaha
 * @Date: 2024/4/11 17:26
 */

@Slf4j
@Component
public class JwtRealm extends AuthorizingRealm {

    @Resource
    private JwtUtil jwtUtil;

    @Autowired
    private UserService userService;

    /**
     * 多重写一个support
     * 标识这个Realm是专门用来验证JwtToken
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String jwt = (String) token.getCredentials();
        // 获取jwt中关于用户id,解码过程中如果token过期或者被篡改会抛出异常
//        String id = null;
        log.info("处理jwt认证", jwt);
        try {
            // Validate the token
            String id = jwtUtil.validateToken(jwt);
            log.info("jwt认证成功,用户id:", id);
            // 标记用户为活跃状态
            userService.markUserActive(Integer.parseInt(id));
            return new SimpleAuthenticationInfo(jwt, jwt, getName());
        }
        catch (ExpiredJwtException e) {
            throw new JwtValidationException(ResultType.UNAUTHORIZED.getCode(), "Token已过期,请重新登录");
        } catch (JwtException e) {
            throw new JwtValidationException(ResultType.UNAUTHORIZED.getCode(), "无效的Token");
        }
    }

    /**
     * 授权时调用
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return new SimpleAuthorizationInfo();
    }
}
  • 调用工具类 JwtUtil,解析token获取其中的用户id,并对token过期等情况进行处理,抛出异常被捕获

  • 然后在filter层走失败的方法,返回结果给前端

JwtUtil

java 复制代码
package com.example.englishhub.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.context.annotation.Configuration;

import java.util.Base64;
import java.util.Date;
import java.util.HashMap;

/**
 * jwt工具类
 *
 * @author hahaha
 */
@Configuration
public class JwtUtil {
    // 30 秒
    private static long EXPIRATION_TIME = 1000 * 30;
    // 1 hour
//    private static long EXPIRATION_TIME = 3600000 * 1;
    // 一天
//    private static long EXPIRATION_TIME = 3600000 * 1;
//private static long EXPIRATION_TIME = 10000 * 10;
//    private static String SECRET = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjY34DFDSSSd";// 秘钥
    // 使用Base64编码的密钥
    private static String SECRET = Base64.getEncoder().encodeToString("MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjY34DFDSSSd".getBytes());

    private static final String USER_ID = "id";


    /**
     * 生成jwtToken
     *
     * @param id
     * @return
     */
    public static String generateToken(String id) {
        HashMap<String, Object> map = new HashMap<>();
        // you can put any data in the map
        map.put(USER_ID, id);
        // 1. setClaims(map):将map中的数据存储到Claims中
        // 2. setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)):设置过期时间
        // 3. signWith(SignatureAlgorithm.HS512, SECRET):设置加密算法和密钥
        // 4. compact():生成token
        String token = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setClaims(map)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        return token;
    }

    /**
     * 校验jwtToken
     *
     * @param token
     * @return
     */
    // 优化验证逻辑
    public static String validateToken(String token) {
        Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        return claims.get(USER_ID, String.class);
//        // 使用自定义异常
//        if (StringUtils.isBlank(token)) {
//            throw new JwtValidationException(ResultType.UNAUTHORIZED.getCode(), "Token为空");
//        }
//        try {
//            Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
//            return claims.get(USER_ID, String.class);
//        } catch (ExpiredJwtException e) {
//            throw new JwtValidationException(ResultType.AGAIN_LOGIN.getCode(), "Token已过期,请重新登录");
//        } catch (JwtException e) {
//            throw new JwtValidationException(ResultType.UNAUTHORIZED.getCode(), "无效的Token");
//        }
    }
  
    public static void main(String[] args) {
        String id = "hahaha15";

        String token = generateToken(id);
        System.out.println(token);

        //token = "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6IjY4NzZhYjFmYjk0MmZkNGYyN2Zm";
        id = validateToken(token);
        System.out.println(id);
//        HashMap<String, Object> map = new HashMap<>();
//        // you can put any data in the map
//        map.put("name", id);
//        token = Jwts.builder().setClaims(map).setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
//                .signWith(SignatureAlgorithm.HS512, SECRET).compact();
//
//         String result = "";
//        try {
//
//            result = validateToken(token);
//            System.out.println(result);
//        }catch (Exception e){
//            System.out.println(e.toString());
//        }

    }
}

测试

  • 使用postman,前端就可以根据返回结果作相应处理,提示用户重新登录。

token续签

后端

  • 通过在shiro的过滤器中调用userService里的方法判断该用户是否在最近两小时为活跃状态,但是却报错了

Cannot invoke "com.example.englishhub.service.UserService.isUserActive(int)" because "this.userService" is null

userService 没有被正确注入到 JwtFilter 中,导致在执行 isAccessAllowed 方法时产生空指针异常。

@Autowired
private UserService userService;

然后询问gpt说可以使用构造函数注入

private final UserService userService;

    @Autowired
    public JwtFilter(UserService userService) {
        this.userService = userService;
    }
  • 但是还是不行,过滤器可能在Spring上下文完全初始化之前实例化,因此无法访问Service层的bean。

所以改为使用 WebApplicationContextUtils

如果注入失败,可以通过 WebApplicationContextUtils 从Servlet上下文获取Spring的 WebApplicationContext,然后手动获取所需的bean。这种方法虽然有些繁琐,但可以解决过滤器中注入失败的问题。

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

public class JwtFilter extends AccessControlFilter {
    private UserService userService;

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        if (userService == null) {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
            userService = context.getBean(UserService.class);
        }

        // ... 接下来的代码 ...
    }
}
  • 经测试可以

    // 在JwtUtil类中添加检查令牌是否即将过期的方法
    public boolean isTokenExpiring(String token) {
    Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
    Date expiration = claims.getExpiration();
    long diff = expiration.getTime() - System.currentTimeMillis();
    return diff < 10 * 60 * 1000; // 10分钟
    }

    // 在JwtFilter的onAccessDenied方法中添加续签逻辑
    JwtToken jwtToken = new JwtToken(token);
    try {
    //进行登录处理,委托realm进行登录认证,调用JwtRealm进行的认证,doGetAuthenticationInfo
    getSubject(servletRequest, servletResponse).login(jwtToken);
    String id = jwtUtil.validateToken(token);
    if (jwtUtil.isTokenExpiring(token) && userService.isUserActive(Integer.parseInt(id))) {
    // 续签令牌
    String newToken = jwtUtil.generateToken(id);
    // 将新令牌发送给客户端
    HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
    httpServletResponse.setHeader("token", newToken);
    }
    return true;
    }
    catch (Exception e) {
    log.error("onAccessDenied error:", e);
    responseError(servletResponse, ResultType.UNAUTHORIZED.getCode(), e.getMessage());
    return false;
    }

前端

  • 在接口服务那边通过拦截器判断是否需要新的token续签

    class ApiService {
      final Dio dio = Dio();
    
      void showFeedback(String type, String message, Color backgroundColor) {
        Get.snackbar(type, message,
            snackPosition: SnackPosition.BOTTOM,
            backgroundColor: backgroundColor,
            colorText: Colors.white);
      }
    
      ApiService() {
        dio
            ..options.baseUrl = 'http://localhost:8899/englishhub/'
    	..interceptors.add(InterceptorsWrapper(
            // 请求拦截器
            onRequest: (options, handler) async {
              final storageService = Get.find<StorageService>();
            String? token = storageService.getToken();
            // print('token: $token');
            if (token != null) {
              options.headers["token"] = token;
            }
            return handler.next(options);
            },
            // 响应拦截器
            onResponse: (response, handler) {
              final storageService = Get.find<StorageService>();
    
              // 检查是否有新令牌
              if (response.headers.value("token") != null) {
                String? newToken = response.headers.value("token");
                storageService.saveToken(newToken!); // 保存新令牌
              }
    
              print('请求结果: $response');
              return handler.next(response);
            },
    	onError: (DioError e, handler) {
              print('请求错误: $e');
              // 401错误码表示token过期,清除token,跳转到登录页
              if (e.response?.statusCode == 401) {
                Get.find<StorageService>().clearToken();
                showFeedback('', 'token过期,请重新登录', Colors.red);
                Get.offAllNamed('/login');
              }
            },
          ));
      }
    }
    

();

        // 检查是否有新令牌
        if (response.headers.value("token") != null) {
          String? newToken = response.headers.value("token");
          storageService.saveToken(newToken!); // 保存新令牌
        }

        print('请求结果: $response');
        return handler.next(response);
      },
onError: (DioError e, handler) {
        print('请求错误: $e');
        // 401错误码表示token过期,清除token,跳转到登录页
        if (e.response?.statusCode == 401) {
          Get.find<StorageService>().clearToken();
          showFeedback('', 'token过期,请重新登录', Colors.red);
          Get.offAllNamed('/login');
        }
      },
    ));
}

}

复制代码
相关推荐
BergerLee12 小时前
对不经常变动的数据集合添加Redis缓存
数据库·redis·缓存
huapiaoy12 小时前
Redis中数据类型的使用(hash和list)
redis·算法·哈希算法
【D'accumulation】13 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
Cikiss13 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
一休哥助手14 小时前
Redis 五种数据类型及底层数据结构详解
数据结构·数据库·redis
盒马盒马15 小时前
Redis:zset类型
数据库·redis
Jay_fearless17 小时前
Redis SpringBoot项目学习
spring boot·redis
Wang's Blog17 小时前
Redis: 集群环境搭建,集群状态检查,分析主从日志,查看集群信息
数据库·redis
wclass-zhengge1 天前
Redis篇(最佳实践)(持续更新迭代)
redis·缓存·bootstrap