多版本共用redis的token有效期校验(过期重新登录)

目录标题

      • 一、前置类和配置等
      • [二、JwtAuthenticationTokenFilter token过滤器(之前判断逻辑有问题,要先判断用户是否存在或者版本是否一致,再判断SecurityContext 中没有认证信息)](#二、JwtAuthenticationTokenFilter token过滤器(之前判断逻辑有问题,要先判断用户是否存在或者版本是否一致,再判断SecurityContext 中没有认证信息))
      • 三、TokenService
      • [四、spring security配置](#四、spring security配置)

参考之前blog: 多版本共用redis导致数据没及时更新报错

里面有bug,就是用户token即使失效或者不存在也是重新获取用户信息重新写入redis

导致一直用户可以访问且是登录状态

前置知识:

springSecurity 的config 配置里面 anonymous 和 permitAll 区别

permitAll() 是"无条件敞开大门",而 anonymous() 是"为未登录者发一张临时门卡"

特性 .permitAll() .anonymous()
官方定义 允许所有人访问,无论是否登录 允许匿名用户访问,即未认证的用户(认证后的就不能访问)
身份标识 不关心用户身份 会为未认证用户创建一个 AnonymousAuthenticationToken 对象作为身份标识
后续权限判断 直接放行,不再进行任何权限检查 放行后,如果其他配置(如 hasRole("ANONYMOUS"))需要,可以基于匿名身份进行判断
对已登录用户 允许访问。 禁止访问(因为已登录用户不再是匿名身份)

一、前置类和配置等

yml

因为 JwtAuthenticationTokenFilter 和 SecurityConfig 都用到白名单配置,所以抽取到yml中

yaml 复制代码
# 安全配置 - 白名单路径
security:
    paths:
        ANONYMOUS_PATHS:
            - /login
            - /register
            - /captchaImage
            - /v2/kms/init/**
            - /swagger-ui.html
            - /swagger-resources/**
            - /swagger-ui/**
            - /*/api-docs
            - /isNeedKey/**
            - /randomString16
            - /encryptionKey/info/**
            - /base/symmetricKey/getKeyText/**
        PERMIT_ALL_PATHS:
            - /
            - /*.html
            - /**/*.html
            - /**/*.css
            - /**/*.js
            - /*.ico
            - /favicon.ico
            - /profile/**
            - /**/*.png
            - /**/*.woff
            - /**/*.ttf
            - /**/*.jpg
            - /**/*.svg
            - /static/**
            - /webjars/**
            - /druid/**
            - /api/**
            - /data/upgrade
            - /system/config/configKey/*

对应配置类SecurityProperties

java 复制代码
/**
 * 安全配置属性
 *
 * @author skms
 */
@Data
@Component
@Slf4j
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
    /**
     * 白名单路径配置
     * 配置位置:application.yml 中 security.paths
     */
    private Map<String, List<String>> paths = new HashMap<>();

    /**
     * 获取需要 anonymous 的路径数组(登录接口等)
     */
    public String[] getAnonymousArray() {
        List<String> paths = this.paths.get("ANONYMOUS_PATHS");
        return paths != null ? paths.toArray(new String[0]) : new String[0];
    }

    /**
     * 获取需要 permitAll 的路径数组(静态资源等)
     */
    public String[] getPermitAllArray() {
        List<String> paths = this.paths.get("PERMIT_ALL_PATHS");
        return paths != null ? paths.toArray(new String[0]) : new String[0];
    }

    /**
     * 获取所有白名单路径数组(用于 JwtAuthenticationTokenFilter)
     */
    public String[] getAllWhiteListArray() {
        List<String> allPaths = new ArrayList<>();
        if (this.paths != null) {
            for (List<String> pathList : this.paths.values()) {
                if (pathList != null) {
                    allPaths.addAll(pathList);
                }
            }
        }
        return allPaths.toArray(new String[0]);
    }

    /**
     * 检查路径是否在白名单中(支持通配符)
     */
    public boolean isWhiteListPath(String requestPath) {
        if (paths == null || paths.isEmpty()) {
            return false;
        }
        for (List<String> pathList : paths.values()) {
            if (pathList == null) continue;
            for (String path : pathList) {
                // 1. 匹配 /** 结尾(如 /swagger-ui/**)
                if (path.endsWith("/**")) {
                    String prefix = path.substring(0, path.length() - 3);
                    if (requestPath.startsWith(prefix)) {
//                        log.info("DEBUG: whitelist match: {} matches {} via /**", requestPath, path);
                        return true;
                    }
                }
                // 2. 匹配 */xxx 模式(如 */api-docs)
                else if (path.indexOf("/*") >= 0) {
                    // 找出 /* 的位置
                    int starSlashIndex = path.indexOf("/*");
                    String before = path.substring(0, starSlashIndex);  // /* 前面的部分
                    String after = path.substring(starSlashIndex + 2);  // /* 后面的部分(不包括 /*)

                    // 检查 requestPath 是否满足:以 before 开头,以 after 结尾,中间至少有一个字符
                    if (requestPath.startsWith(before) && requestPath.endsWith(after)) {
                        int beforeEnd = before.length();
                        int afterStart = requestPath.length() - after.length();
                        if (beforeEnd < afterStart) {
                            String middle = requestPath.substring(beforeEnd, afterStart);
                            // 中间部分不能为空,且如果 before 以 / 结尾,middle 应该以 / 开头
                            if (!middle.isEmpty()) {
//                                log.info("DEBUG: whitelist match: {} matches {} via */*", requestPath, path);
                                return true;
                            }
                        }
                    }
                }
                // 3. 匹配 /*.xxx 结尾(如 /*.ico)
                else if (path.length() >= 3 && path.substring(0, 3).equals("/*.")) {
                    String ext = path.substring(1);  // 提取 .ico 部分
                    if (requestPath.endsWith(ext)) {
//                        log.info("DEBUG: whitelist match: {} matches {} via /*.", requestPath, path);
                        return true;
                    }
                }
                // 4. 精确匹配
                else if (requestPath.equals(path)) {
//                    log.info("DEBUG: whitelist match: {} equals {}", requestPath, path);
                    return true;
                }
            }
        }
//        log.info("DEBUG: NO whitelist match for: {}", requestPath);
        return false;
    }

    /**
     * 获取指定路径的访问类型
     */
    public AccessType getAccessType(String path) {
        if (paths == null || paths.isEmpty()) {
            return AccessType.PERMIT_ALL;
        }

        // 先检查 ANONYMOUS_PATHS
        List<String> anonymousPaths = paths.get("ANONYMOUS_PATHS");
        if (anonymousPaths != null) {
            for (String p : anonymousPaths) {
                if (p.endsWith("/**")) {
                    if (path.startsWith(p.substring(0, p.length() - 3))) return AccessType.ANONYMOUS;
                } else if (p.endsWith("/*")) {
                    if (path.startsWith(p.substring(0, p.length() - 2))) return AccessType.ANONYMOUS;
                } else if (path.equals(p)) {
                    return AccessType.ANONYMOUS;
                }
            }
        }

        // 再检查 PERMIT_ALL_PATHS
        List<String> permitAllPaths = paths.get("PERMIT_ALL_PATHS");
        if (permitAllPaths != null) {
            for (String p : permitAllPaths) {
                if (p.endsWith("/**")) {
                    if (path.startsWith(p.substring(0, p.length() - 3))) return AccessType.PERMIT_ALL;
                } else if (p.endsWith("/*")) {
                    if (path.startsWith(p.substring(0, p.length() - 2))) return AccessType.PERMIT_ALL;
                } else if (path.equals(p)) {
                    return AccessType.PERMIT_ALL;
                }
            }
        }

        return AccessType.PERMIT_ALL;
    }

    /**
     * 访问类型枚举
     */
    public enum AccessType {
        PERMIT_ALL,  // 允许任意访问(不创建认证主体)
        ANONYMOUS    // 允许匿名访问(创建 AnonymousAuthenticationToken)
    }
}

涉及的实体类

java 复制代码
/**
 * 获取用户登录结果
 *
 * @author skms
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserResult {
    /**
     * 用户信息
     */
    private LoginUser loginUser;

    /**
     * 用户状态
     */
    private LoginUserStatus status;

    /**
     * 是否有效
     */
    public boolean isValid() {
        return status == LoginUserStatus.VALID;
    }

    /**
     * 是否需要重新登录
     */
    public boolean needReLogin() {
        return status == LoginUserStatus.CACHE_NOT_FOUND
                || status == LoginUserStatus.INACTIVE_TIMEOUT
                || status == LoginUserStatus.TOKEN_PARSE_ERROR;
    }
}
java 复制代码
/**
 * 登录用户身份权限
 *
 * @author skms
 */
public class LoginUser implements UserDetails {
    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 用户唯一标识
     */
    private String token;

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 过期时间
     */
    private Long expireTime;

    /**
     * 登录IP地址
     */
    private String ipaddr;

    /**
     * 登录地点
     */
    private String loginLocation;

    /**
     * 浏览器类型
     */
    private String browser;

    /**
     * 操作系统
     */
    private String os;

    /**
     * 权限列表
     */
    private Set<String> permissions;

    /**
     * 用户信息
     */
    private SysUser user;

    /**
     * 应用版本标识
     */
    private String appVersion;

    public LoginUser() {
    }

    public LoginUser(SysUser user, Set<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    public LoginUser(Long userId, SysUser user, Set<String> permissions) {
        this.userId = userId;
        this.user = user;
        this.permissions = permissions;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    @JSONField(serialize = false)
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     *
     * @return 结果
     */
    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     *
     * @return 结果
     */
    @JSONField(serialize = false)
    @Override
    public boolean isEnabled() {
        return true;
    }

    public Long getLoginTime() {
        return loginTime;
    }

    public void setLoginTime(Long loginTime) {
        this.loginTime = loginTime;
    }

    public String getIpaddr() {
        return ipaddr;
    }

    public void setIpaddr(String ipaddr) {
        this.ipaddr = ipaddr;
    }

    public String getLoginLocation() {
        return loginLocation;
    }

    public void setLoginLocation(String loginLocation) {
        this.loginLocation = loginLocation;
    }

    public String getBrowser() {
        return browser;
    }

    public void setBrowser(String browser) {
        this.browser = browser;
    }

    public String getOs() {
        return os;
    }

    public void setOs(String os) {
        this.os = os;
    }

    public Long getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(Long expireTime) {
        this.expireTime = expireTime;
    }

    public Set<String> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<String> permissions) {
        this.permissions = permissions;
    }

    public SysUser getUser() {
        return user;
    }

    public void setUser(SysUser user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    public String getAppVersion() {
        return appVersion;
    }

    public void setAppVersion(String appVersion) {
        this.appVersion = appVersion;
    }
}
java 复制代码
/**
 * 用户登录状态枚举
 *
 * @author skms
 */
public enum LoginUserStatus {
    /**
     * 有效
     */
    VALID,

    /**
     * 缓存不存在
     */
    CACHE_NOT_FOUND,

    /**
     * 版本不匹配
     */
    VERSION_MISMATCH,

    /**
     * 令牌过期
     */
    TOKEN_TIMEOUT,

    /**
     * 无操作超时
     */
    INACTIVE_TIMEOUT,

    /**
     * Token 解析失败
     */
    TOKEN_PARSE_ERROR
}
java 复制代码
/**
 * 通用常量信息
 *
 * @author skms
 */
public class Constants {
    /**
     * 验证码有效期(分钟)
     */
    public static final Integer CAPTCHA_EXPIRATION = 2;

    /**
     * 令牌
     */
    public static final String TOKEN = "token";

    /**
     * 令牌前缀
     */
    public static final String TOKEN_PREFIX = "Bearer ";

    /**
     * 令牌前缀
     */
    public static final String LOGIN_USER_KEY = "login_user_key";

    /**
     * 用户ID
     */
    public static final String JWT_USERID = "userid";
        /**
     * 应用版本标识-v1
     */
    public static final String APP_VERSION_V1 = "v1";

    /**
     * 应用版本标识-v2
     */
    public static final String APP_VERSION_V2 = "v2";
}

二、JwtAuthenticationTokenFilter token过滤器(之前判断逻辑有问题,要先判断用户是否存在或者版本是否一致,再判断SecurityContext 中没有认证信息)

重点刷新或者重新从用户中心获取用户信息等都在 tokenService 中,里面判断缓存是否过期及快过期续期等操作

java 复制代码
/**
 * token 过滤器 验证 token 有效性
 *
 * @author skms
 */
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Resource
    private TokenService tokenService;

    @Resource
    private SecurityProperties securityProperties;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String requestPath = request.getRequestURI();

        // 检查是否在白名单中 (因为这个 filter 在 SecurityConfig 前面执行,所以这里也要共用同一份白名单)
        if (isWhiteListPath(requestPath)) {
            chain.doFilter(request, response);
            return;
        }

        // 获取用户信息及状态
        LoginUserResult userResult = tokenService.getLoginUser(request);

//        1.先判断用户是否有效(有效:更新缓存过期时间和登录时间,无效直接返回 throw),是否版本不匹配(通过用户中心重新获取权限并刷新缓存),然后再判断SecurityContext 中没有认证信息
//        否则 缓存不存在、无操作超时、缓存过期或 token 解析失败 就不用在判断 SecurityContext 中有没有认证信息
        if(userResult.getStatus() == LoginUserStatus.VALID){
            // 用户有效但 token 即将过期,刷新缓存并重置登录时间
            if (tokenService.isTokenExpired(userResult.getLoginUser())) {
                // token 已过期,删除缓存并强制重新登录
                log.info("Token 已过期,userId: {}", userResult.getLoginUser().getUserId());
                tokenService.delLoginUser(userResult.getLoginUser().getToken());
                throw new AuthenticationServiceException("用户会话已过期,请重新登录");
            } else if (!tokenService.isTokenValid(userResult.getLoginUser())) {
                tokenService.verifyToken(userResult.getLoginUser());
            }
            if(StringUtils.isNull(SecurityUtils.getAuthentication())){
                UsernamePasswordAuthenticationToken authenticationToken = buildAuthentication(userResult.getLoginUser());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        } else if(userResult.getStatus() == LoginUserStatus.VERSION_MISMATCH){
            // 版本不匹配,通过用户中心重新获取权限并刷新缓存
            LoginUserResult refreshResult = tokenService.refreshUserFromUserCenter(request);
            if (refreshResult != null && refreshResult.isValid()) {
                if(StringUtils.isNull(SecurityUtils.getAuthentication())){
                    UsernamePasswordAuthenticationToken authenticationToken = buildAuthentication(refreshResult.getLoginUser());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            } else {
                throw new AuthenticationServiceException("用户会话已超时,请重新登录");
            }
        } else {
            // 缓存不存在、无操作超时、缓存过期或 token 解析失败,抛出异常触发认证失败处理
            throw new AuthenticationServiceException("用户会话已超时,请重新登录");
        }

        chain.doFilter(request, response);
    }

    /**
     * 构建 Authentication 对象
     */
    private UsernamePasswordAuthenticationToken buildAuthentication(LoginUser loginUser) {
        return new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
    }

    /**
     * 检查请求路径是否在白名单中
     */
    private boolean isWhiteListPath(String requestPath) {
        return securityProperties.isWhiteListPath(requestPath);
    }
}

三、TokenService

java 复制代码
/**
 * token 验证处理
 *
 * @author skms
 */
@Slf4j
@Component
public class TokenService {
    protected static final long MILLIS_SECOND = 1000;
    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
    /**
     * 令牌自定义标识
     */
    @Value("${token.header}")
    private String header;
    /**
     * 令牌秘钥
     */
    @Value("${token.secret}")
    private String secret;
    /**
     * 令牌有效期(默认 30 分钟)
     */
    @Value("${token.expireTime}")
    private int expireTime;
    /**
     * 连续无操作超时时间 (默认 24 小时=1440 分钟)
     */
    @Value("${token.inactiveTime}")
    private int inactiveTime;
    /**
     * 应用版本标识
     */
    @Value("${token.appVersion:v2}")
    private String appVersion;

    @Resource
    private RedisCache redisCache;
    @Resource
    private ISysUserService userService;
    @Resource
    private UserClientApi userClientApi;
    @Resource
    private SysPermissionService permissionService;
    @Resource
    private ISysUserDeviceService sysUserDeviceService;
    @Resource
    private ICertService certService;
    @Resource
    @Qualifier("UserDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    /**
     * 获取用户身份信息
     *
     * @return 用户信息及状态
     */
    public LoginUserResult getLoginUser(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token)) {
            try {
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
                String userKey = getTokenKey(uuid);
                LoginUser loginUser = redisCache.getCacheObject(userKey);

                // 缓存不存在
                if (loginUser == null) {
                    log.debug("用户缓存不存在,token: {}", token);
                    return LoginUserResult.builder()
                            .loginUser(null)
                            .status(LoginUserStatus.CACHE_NOT_FOUND)
                            .build();
                }

                // 验证令牌是否已过期(先验证令牌是否过期,拿一个过期令牌过来,也不用判断是不是版本不一致了)
                if(isTokenExpired(loginUser)){
                    log.info("用户token令牌过期,userId: {}", loginUser.getUserId());
                    return LoginUserResult.builder()
                            .loginUser(loginUser)
                            .status(LoginUserStatus.TOKEN_TIMEOUT)
                            .build();
                }

                // 验证版本是否匹配
                if (!isCurrentVersion(loginUser)) {
                    log.info("用户版本不匹配,userId: {}", loginUser.getUserId());
                    return LoginUserResult.builder()
                            .loginUser(loginUser)
                            .status(LoginUserStatus.VERSION_MISMATCH)
                            .build();
                }

                return LoginUserResult.builder()
                        .loginUser(loginUser)
                        .status(LoginUserStatus.VALID)
                        .build();
            } catch (Exception e) {
                log.error("获取用户身份信息失败", e);
                return LoginUserResult.builder()
                        .loginUser(null)
                        .status(LoginUserStatus.TOKEN_PARSE_ERROR)
                        .build();
            }
        }
        return LoginUserResult.builder()
                .loginUser(null)
                .status(LoginUserStatus.CACHE_NOT_FOUND)
                .build();
    }

    /**
     * 设置用户身份信息
     */
    public void setLoginUser(LoginUser loginUser) {
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) {
            refreshToken(loginUser);
        }
    }

    /**
     * 删除用户身份信息
     */
    public void delLoginUser(String token) {
        if (StringUtils.isNotEmpty(token)) {
            String userKey = getTokenKey(token);
            redisCache.deleteObject(userKey);
        }
    }

    /**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    public String createToken(LoginUser loginUser) {
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        setUserAgent(loginUser);
        refreshToken(loginUser);

        Map<String, Object> claims = new HashMap<>(16);
        claims.put(Constants.LOGIN_USER_KEY, token);
        claims.put(Constants.JWT_USERID, loginUser.getUserId());
        return createToken(claims);
    }

    /**
     * 验证令牌是否已过期
     *
     * @param loginUser 登录用户
     * @return true=已过期,false=未过期
     */
    public boolean isTokenExpired(LoginUser loginUser) {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        return expireTime - currentTime <= 0;
    }

    /**
     * 验证令牌是否有效(未过期且剩余时间充足)
     *
     * @param loginUser 登录用户
     * @return true=有效且充足,false=即将过期
     */
    public boolean isTokenValid(LoginUser loginUser) {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        // Token 未过期且有效期不足 20 分钟时才认为是有效的
        return expireTime - currentTime > MILLIS_MINUTE_TEN;
    }

    /**
     * 验证令牌有效期,相差不足 20 分钟,自动刷新缓存
     * 同时更新登录时间以重置无操作超时计时器
     *
     * @param loginUser 登录用户
     */
    public void verifyToken(LoginUser loginUser) {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
            refreshToken(loginUser);
        }
    }

    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser) {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
//        暂时注释,token有效期临时使用 10 分钟
//        loginUser.setExpireTime(loginUser.getLoginTime() + 10 * MILLIS_MINUTE);
        // 设置应用版本标识
        loginUser.setAppVersion(appVersion);
        // 根据 uuid 将 loginUser 缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
//        redisCache.setCacheObject(userKey, loginUser, 10, TimeUnit.MINUTES);
    }

    /**
     * 验证用户缓存是否为当前应用版本
     */
    private boolean isCurrentVersion(LoginUser loginUser) {
        String currentVersion = appVersion != null ? appVersion : Constants.APP_VERSION_V2;
        return currentVersion.equals(loginUser.getAppVersion());
    }

    /**
     * 设置用户代理信息
     *
     * @param loginUser 登录信息
     */
    public void setUserAgent(LoginUser loginUser) {
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
        loginUser.setIpaddr(ip);
        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        loginUser.setBrowser(userAgent.getBrowser().getName());
        loginUser.setOs(userAgent.getOperatingSystem().getName());
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getSubject();
    }

    /**
     * 获取请求 token
     *
     * @param request 请求
     * @return token
     */
    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(header);
        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

    private String getTokenKey(String uuid) {
        return Constants.LOGIN_TOKEN_KEY + uuid;
    }

    /**
     * 从用户中心重新获取用户信息(适用于版本不匹配场景)
     * 会检查 Token 中的 loginTime,如果连续无操作超时则拒绝登录
     */
    public LoginUserResult refreshUserFromUserCenter(HttpServletRequest request) {
        String token = getToken(request);
        if (StringUtils.isEmpty(token)) {
            return LoginUserResult.builder()
                    .loginUser(null)
                    .status(com.skms.common.core.domain.model.LoginUserStatus.CACHE_NOT_FOUND)
                    .build();
        }

        try {
            Claims claims = parseToken(token);
            Object userIdObj = claims.get(Constants.JWT_USERID);
            Long userId;
            if (userIdObj instanceof Integer) {
                userId = ((Integer) userIdObj).longValue();
            } else if (userIdObj instanceof Long) {
                userId = (Long) userIdObj;
            } else {
                log.error("重新获取用户信息:userId 类型错误,类型为{}", userIdObj == null ? "null" : userIdObj.getClass().getName());
                return null;
            }
            if (userId == null) {
                log.error("重新获取用户信息:userId 不存在.");
                return null;
            }

            // 通过用户中心 API 根据 userId 获取最新用户信息
            FeignSysUser feignSysUser = userClientApi.getInternalInfo(userId);
            if (feignSysUser == null) {
                log.error("重新获取用户信息:userId={} 不存在.", userId);
                return null;
            }

            // 重新加载本地角色和设备信息(使用 v2 的数据)
            List<Long> roleIds = userService.selectRoleIdsByUserId(feignSysUser.getUserId());
            feignSysUser.setRoleIds(roleIds.toArray(new Long[0]));

            List<SysRole> roles = userService.selectRolesByUserId(feignSysUser.getUserId());
            feignSysUser.setRoles(roles);

            List<String> deviceIds = sysUserDeviceService.selectSysDeviceIdsByUserId(feignSysUser.getUserId());
            feignSysUser.setDeviceIds(deviceIds);

            // 构建新的 LoginUser(使用 v2 的权限)
            SysUser user = FeignTransferUser.toFeignSysUser(feignSysUser);
            if (StringUtils.isNull(user)) {
                log.error("重新获取用户信息:userId={} 不存在.", userId);
                throw new UsernameNotFoundException("重新获取用户信息:userId=" + userId + " 不存在");
            } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
                log.error("重新获取用户信息:userId={} 已被删除.", userId);
                throw new BaseException("对不起,您的账号:" + userId + " 已被删除");
            } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
                log.error("重新获取用户信息:userId={} 已被停用.", userId);
                throw new BaseException("对不起,您的账号:" + userId + " 已停用");
            }

            LoginUser loginUser = new LoginUser(feignSysUser.getUserId(), user, permissionService.getMenuPermission(user));
            // 从 claims 中取 UUID 作为 token(与 Redis key 保持一致)
            loginUser.setToken((String) claims.get(Constants.LOGIN_USER_KEY));

            // 刷新 Redis 缓存(带新版本号)
            refreshToken(loginUser);

            return LoginUserResult.builder()
                    .loginUser(loginUser)
                    .status(com.skms.common.core.domain.model.LoginUserStatus.VALID)
                    .build();
        } catch (Exception e) {
            log.error("重新获取用户信息失败", e);
            return LoginUserResult.builder()
                    .loginUser(null)
                    .status(com.skms.common.core.domain.model.LoginUserStatus.TOKEN_PARSE_ERROR)
                    .build();
        }
    }
}

四、spring security配置

java 复制代码
import com.skms.framework.config.properties.SecurityProperties;
import com.skms.framework.security.filter.JwtAuthenticationTokenFilter;
import com.skms.framework.security.handle.AuthenticationEntryPointImpl;
import com.skms.framework.security.handle.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;

import javax.annotation.Resource;

/**
 * spring security配置
 *
 * @author skms
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定义用户认证逻辑
     */
    @Resource
    @Qualifier("UserDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Resource
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Resource
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Resource
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Resource
    private CorsFilter corsFilter;

    /**
     * 安全配置属性(白名单)
     */
    @Resource
    private SecurityProperties securityProperties;

    /**
     * 解决 无法直接注入 AuthenticationManager
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // CSRF 禁用,因为不使用 session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于 token,所以不需要 session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // OPTIONS 请求全部允许(跨域预检)
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 从配置文件读取 permitAll 路径
                .antMatchers(securityProperties.getPermitAllArray()).permitAll()
                // 从配置文件读取 anonymous 路径
                .antMatchers(securityProperties.getAnonymousArray()).anonymous()
                // 除白名单外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加 JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加 CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}
相关推荐
疯狂打码的少年1 小时前
【软件工程】结构化设计:模块独立性与耦合内聚
java·开发语言·笔记·软件工程
乐观的Terry1 小时前
3、数据库设计与领域实体
java·数据库·spring boot·spring cloud·ai编程
C++、Java和Python的菜鸟2 小时前
第4章 后端Web基础(基础知识)
java·开发语言
芷栀夏2 小时前
Java教育平台实战复盘:课程、考试与学习行为分析系统设计
java·开发语言·学习
Wzx1980122 小时前
Redis&ES——Retriever的抽象实现
数据库·人工智能·redis·elasticsearch
维基框架2 小时前
维基框架(Wiki-Framework) 1.2.0:Mybatis 升级为主从读写分离
java·后端·架构
Memory_荒年2 小时前
Java函数式接口:把代码变成“拼乐高”的快乐,你体验过吗?
java
Memory_荒年2 小时前
WebSocket:让服务器学会“主动搭讪”的黑科技
java
Henrii_历小海3 小时前
WhatsApp Business API 2026 计费架构重构:从“对话计费“到“按消息计费“的技术实现与工程应对
java·重构·架构