diff --git a/src/main/java/com/youlai/boot/core/security/util/JwtUtils.java b/src/main/java/com/youlai/boot/core/security/util/JwtUtils.java index 6a7ae6a3..e950ede4 100644 --- a/src/main/java/com/youlai/boot/core/security/util/JwtUtils.java +++ b/src/main/java/com/youlai/boot/core/security/util/JwtUtils.java @@ -3,12 +3,16 @@ package com.youlai.boot.core.security.util; import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWTPayload; import cn.hutool.jwt.JWTUtil; import com.youlai.boot.common.constant.JwtClaimConstants; +import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.core.security.model.SysUserDetails; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -16,6 +20,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Component; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -27,6 +32,14 @@ import java.util.stream.Collectors; @Component public class JwtUtils { + private static StringRedisTemplate redisTemplate; + + @Autowired + public JwtUtils(StringRedisTemplate redisTemplate) { + JwtUtils.redisTemplate = redisTemplate; + } + + /** * JWT 加解密使用的密钥 */ @@ -106,5 +119,31 @@ public class JwtUtils { return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); } + /** + * 将 Token 加入黑名单 + */ + public static void addTokenToBlacklist(String token) { + if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); + JSONObject payloads = JWTUtil.parseToken(token).getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); + + if (expiration != null) { + long currentTimeSeconds = System.currentTimeMillis() / 1000; + if (expiration < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + long ttl = expiration - currentTimeSeconds; + redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null); + } + } + } + } diff --git a/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java b/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java index e78c0e01..47b39ea4 100644 --- a/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java +++ b/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java @@ -4,9 +4,13 @@ import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.core.security.model.SysUserDetails; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import java.util.Collection; import java.util.Collections; @@ -106,4 +110,24 @@ public class SecurityUtils { return roles.contains(SystemConstants.ROOT_ROLE_CODE); } + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + /** + * 将 Token 加入黑名单并清空 Spring Security 上下文 + * + * @param token 要失效的 Token + */ + public static void invalidateToken(String token) { + JwtUtils.addTokenToBlacklist(token); + SecurityContextHolder.clearContext(); + } + } diff --git a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java index 91f5a3f0..d3e71527 100644 --- a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java @@ -5,28 +5,22 @@ import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.jwt.JWTPayload; -import cn.hutool.jwt.JWTUtil; import com.youlai.boot.common.constant.SecurityConstants; +import com.youlai.boot.core.security.util.SecurityUtils; import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum; import com.youlai.boot.shared.auth.service.AuthService; import com.youlai.boot.system.model.dto.CaptchaResult; import com.youlai.boot.system.model.dto.LoginResult; import com.youlai.boot.config.property.CaptchaProperties; import com.youlai.boot.core.security.util.JwtUtils; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; import java.awt.*; import java.util.concurrent.TimeUnit; @@ -78,32 +72,10 @@ public class AuthServiceImpl implements AuthService { */ @Override public void logout() { - HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); - String token = request.getHeader(HttpHeaders.AUTHORIZATION); - if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { - token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); - // 解析Token以获取有效载荷(payload) - JSONObject payloads = JWTUtil.parseToken(token).getPayloads(); - // 解析 Token 获取 jti(JWT ID) 和 exp(过期时间) - String jti = payloads.getStr(JWTPayload.JWT_ID); - Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); // 过期时间(秒) - // 如果exp存在,则计算Token剩余有效时间 - if (expiration != null) { - long currentTimeSeconds = System.currentTimeMillis() / 1000; - if (expiration < currentTimeSeconds) { - // Token已过期,不再加入黑名单 - return; - } - // 将Token的jti加入黑名单,并设置剩余有效时间,使其在过期后自动从黑名单移除 - long ttl = expiration - currentTimeSeconds; - redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS); - } else { - // 如果exp不存在,说明Token永不过期,则永久加入黑名单 - redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null); - } + String token = SecurityUtils.getTokenFromRequest(); + if (StrUtil.isNotBlank(token)) { + SecurityUtils.invalidateToken(token); } - // 清空Spring Security上下文 - SecurityContextHolder.clearContext(); } /**