feat: 完善注销功能逻辑,通过黑名单的方式实现注销场景JWT失效控制
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
package com.youlai.system.common.constant;
|
||||
|
||||
/**
|
||||
* Redis 缓存常量
|
||||
*
|
||||
* @author: haoxr
|
||||
* @date: 2023/03/24
|
||||
*/
|
||||
public interface CacheConstants {
|
||||
|
||||
/**
|
||||
* 验证码缓存前缀
|
||||
*/
|
||||
String VERIFY_CODE_CACHE_PREFIX = "AUTH:VERIFY_CODE:";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.youlai.system.common.constant;
|
||||
|
||||
/**
|
||||
* Security 常量
|
||||
*
|
||||
* @author: haoxr
|
||||
* @date: 2023/03/24
|
||||
*/
|
||||
public interface SecurityConstants {
|
||||
|
||||
/**
|
||||
* 登录接口路径
|
||||
*/
|
||||
String LOGIN_PATH = "/api/v1/auth/login";
|
||||
|
||||
|
||||
/**
|
||||
* Token 前缀
|
||||
*/
|
||||
String TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
/**
|
||||
* 请求头Token的Key
|
||||
*/
|
||||
String TOKEN_KEY = "Authorization";
|
||||
|
||||
|
||||
/**
|
||||
* 验证码缓存前缀
|
||||
*/
|
||||
String VERIFY_CODE_CACHE_PREFIX = "AUTH:VERIFY_CODE:";
|
||||
|
||||
/**
|
||||
* 用户权限集合缓存前缀
|
||||
*/
|
||||
String USER_PERMS_CACHE_PREFIX = "AUTH:USER_PERMS:";
|
||||
|
||||
/**
|
||||
* 黑名单Token缓存前缀
|
||||
*/
|
||||
String BLACK_TOKEN_CACHE_PREFIX = "AUTH:BLACK_TOKEN:";
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.youlai.system.common.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 请求工具类
|
||||
*
|
||||
* @author haoxr
|
||||
*/
|
||||
public class RequestUtils {
|
||||
|
||||
/**
|
||||
* 请求头解析获取 Token
|
||||
*/
|
||||
public static String resolveToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader(SecurityConstants.TOKEN_KEY);
|
||||
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith(SecurityConstants.TOKEN_PREFIX)) {
|
||||
return bearerToken.substring(SecurityConstants.TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* response 响应工具类
|
||||
* 响应工具类
|
||||
*
|
||||
* @author haoxr
|
||||
* @date 2022/10/18
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
package com.youlai.system.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.common.result.Result;
|
||||
import com.youlai.system.common.util.RequestUtils;
|
||||
import com.youlai.system.framework.easycaptcha.service.EasyCaptchaService;
|
||||
import com.youlai.system.pojo.dto.CaptchaResult;
|
||||
import com.youlai.system.pojo.dto.LoginResult;
|
||||
import com.youlai.system.framework.security.JwtTokenManager;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
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.web.bind.annotation.*;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Tag(name = "01.认证中心")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@@ -25,6 +34,8 @@ public class AuthController {
|
||||
private final JwtTokenManager jwtTokenManager;
|
||||
private final EasyCaptchaService easyCaptchaService;
|
||||
|
||||
private final RedisTemplate redisTemplate;
|
||||
|
||||
@Operation(summary = "登录")
|
||||
@PostMapping("/login")
|
||||
public Result<LoginResult> login(
|
||||
@@ -48,7 +59,22 @@ public class AuthController {
|
||||
|
||||
@Operation(summary = "注销", security = {@SecurityRequirement(name = "Authorization")})
|
||||
@DeleteMapping("/logout")
|
||||
public Result login() {
|
||||
public Result login(HttpServletRequest request) {
|
||||
String token = RequestUtils.resolveToken(request);
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
Claims claims = jwtTokenManager.getTokenClaims(token);
|
||||
String jti = claims.get("jti", String.class);
|
||||
|
||||
Date expiration = claims.getExpiration();
|
||||
if (expiration != null) {
|
||||
// 有过期时间,在token有效时间内存入黑名单,超出时间移除黑名单节省内存占用
|
||||
long ttl = (expiration.getTime() - System.currentTimeMillis()) ;
|
||||
redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null, ttl, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
// 无过期时间,永久加入黑名单
|
||||
redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null);
|
||||
}
|
||||
}
|
||||
SecurityContextHolder.clearContext();
|
||||
return Result.success("注销成功");
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import lombok.SneakyThrows;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Tag(name = "08.文件接口")
|
||||
@Tag(name = "07.文件接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/files")
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "07.字典接口")
|
||||
@Tag(name = "06.字典接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dict")
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.system.framework.easycaptcha.config;
|
||||
|
||||
import com.youlai.system.framework.easycaptcha.enums.VerifyCodeTypeEnum;
|
||||
import com.youlai.system.framework.easycaptcha.enums.CaptchaTypeEnum;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -21,7 +21,7 @@ public class EasyCaptchaConfig {
|
||||
/**
|
||||
* 验证码类型
|
||||
*/
|
||||
private VerifyCodeTypeEnum verifyCodeType = VerifyCodeTypeEnum.ARITHMETIC;
|
||||
private CaptchaTypeEnum type = CaptchaTypeEnum.ARITHMETIC;
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ package com.youlai.system.framework.easycaptcha.enums;
|
||||
* @author: haoxr
|
||||
* @date: 2023/03/24
|
||||
*/
|
||||
public enum VerifyCodeTypeEnum {
|
||||
public enum CaptchaTypeEnum {
|
||||
|
||||
/**
|
||||
* 算数
|
||||
@@ -27,7 +27,7 @@ public class EasyCaptchaProducer {
|
||||
int length = easyCaptchaConfig.getLength();
|
||||
String fontName = easyCaptchaConfig.getFontName();
|
||||
|
||||
switch (easyCaptchaConfig.getVerifyCodeType()) {
|
||||
switch (easyCaptchaConfig.getType()) {
|
||||
case ARITHMETIC:
|
||||
captcha = new ArithmeticCaptcha(width, height);
|
||||
//固定设置为两位,图片为算数运算表达式
|
||||
@@ -50,7 +50,7 @@ public class EasyCaptchaProducer {
|
||||
captcha.setLen(length);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("验证码配置信息错误!正确配置查看 VerifyCodeTypeEnum ");
|
||||
throw new RuntimeException("验证码配置信息错误!正确配置查看 CaptchaTypeEnum ");
|
||||
}
|
||||
captcha.setFont(new Font(fontName, easyCaptchaConfig.getFontStyle(), easyCaptchaConfig.getFontSize()));
|
||||
return captcha;
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.system.framework.easycaptcha.service;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.wf.captcha.base.Captcha;
|
||||
import com.youlai.system.common.constant.CacheConstants;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.framework.easycaptcha.config.EasyCaptchaConfig;
|
||||
import com.youlai.system.framework.easycaptcha.producer.EasyCaptchaProducer;
|
||||
import com.youlai.system.pojo.dto.CaptchaResult;
|
||||
@@ -39,9 +39,9 @@ public class EasyCaptchaService {
|
||||
String captchaText = captcha.text(); // 验证码文本
|
||||
String captchaBase64 = captcha.toBase64(); // 验证码图片Base64字符串
|
||||
|
||||
// 验证码文本缓存至Redis,用于登录比较
|
||||
// 验证码文本缓存至Redis,用于登录校验
|
||||
String verifyCodeKey = IdUtil.fastSimpleUUID();
|
||||
redisTemplate.opsForValue().set(CacheConstants.VERIFY_CODE_CACHE_PREFIX + verifyCodeKey, captchaText,
|
||||
redisTemplate.opsForValue().set(SecurityConstants.VERIFY_CODE_CACHE_PREFIX + verifyCodeKey, captchaText,
|
||||
easyCaptchaConfig.getTtl(), TimeUnit.SECONDS);
|
||||
|
||||
CaptchaResult captchaResult = CaptchaResult.builder()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.youlai.system.framework.security;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.framework.security.userdetails.SysUserDetails;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwtParser;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.io.Decoders;
|
||||
import io.jsonwebtoken.io.DecodingException;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
@@ -15,8 +14,8 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
@@ -24,7 +23,6 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* JWT token manager
|
||||
*
|
||||
@@ -35,16 +33,16 @@ import java.util.stream.Collectors;
|
||||
public class JwtTokenManager {
|
||||
|
||||
/**
|
||||
* secret key.
|
||||
* token加密密钥
|
||||
*/
|
||||
@Value("${auth.token.secret_key}")
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* Token validity time(seconds).
|
||||
* token有效期(单位:秒)
|
||||
*/
|
||||
@Value("${auth.token.token_validity}")
|
||||
private long tokenValidity;
|
||||
@Value("${auth.token.ttl}")
|
||||
private Long tokenTtl;
|
||||
|
||||
/**
|
||||
* secret key byte array.
|
||||
@@ -63,13 +61,6 @@ public class JwtTokenManager {
|
||||
* @return token
|
||||
*/
|
||||
public String createToken(Authentication authentication) {
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
Date validity;
|
||||
|
||||
validity = new Date(now + tokenValidity * 1000L);
|
||||
|
||||
Claims claims = Jwts.claims().setSubject(authentication.getName());
|
||||
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
||||
claims.put("userId", userDetails.getUserId());
|
||||
@@ -81,29 +72,32 @@ public class JwtTokenManager {
|
||||
Set<String> roles = userDetails.getAuthorities().stream()
|
||||
.map(item -> item.getAuthority()).collect(Collectors.toSet());
|
||||
claims.put("authorities", roles);
|
||||
claims.put("jti",IdUtil.fastSimpleUUID());
|
||||
|
||||
// 权限数据多放入Redis
|
||||
Set<String> perms = userDetails.getPerms();
|
||||
redisTemplate.opsForValue().set("USER_PERMS:" + userDetails.getUserId(), perms);
|
||||
redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_PREFIX + userDetails.getUserId(), perms);
|
||||
|
||||
return Jwts.builder().setClaims(claims).setExpiration(validity)
|
||||
.signWith( Keys.hmacShaKeyFor(this.getSecretKeyBytes()),SignatureAlgorithm.HS256).compact();
|
||||
// 过期时间
|
||||
Date expirationTime = new Date(System.currentTimeMillis() + tokenTtl * 1000L);
|
||||
return Jwts.builder()
|
||||
//.setId(IdUtil.fastSimpleUUID()) TODO 设置jti无效
|
||||
.setClaims(claims)
|
||||
.setExpiration(expirationTime)
|
||||
.signWith(Keys.hmacShaKeyFor(this.getSecretKeyBytes()), SignatureAlgorithm.HS256).compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认证信息
|
||||
*/
|
||||
public Authentication getAuthentication(String token) {
|
||||
if (jwtParser == null) {
|
||||
jwtParser = Jwts.parserBuilder().setSigningKey(this.getSecretKeyBytes()).build();
|
||||
}
|
||||
Claims claims = jwtParser.parseClaimsJws(token).getBody();
|
||||
Claims claims = this.getTokenClaims(token);
|
||||
|
||||
SysUserDetails principal = new SysUserDetails();
|
||||
principal.setUserId(Convert.toLong(claims.get("userId")));
|
||||
principal.setUsername(Convert.toStr(claims.get("username")));
|
||||
principal.setDeptId(Convert.toLong(claims.get("deptId")));
|
||||
principal.setDataScope(Convert.toInt(claims.get("dataScope")));
|
||||
principal.setUserId(Convert.toLong(claims.get("userId"))); // 用户ID
|
||||
principal.setUsername(Convert.toStr(claims.get("username"))); // 用户名
|
||||
principal.setDeptId(Convert.toLong(claims.get("deptId"))); // 部门ID
|
||||
principal.setDataScope(Convert.toInt(claims.get("dataScope"))); // 数据权限
|
||||
|
||||
List<SimpleGrantedAuthority> authorities = ((ArrayList<String>) claims.get("authorities"))
|
||||
.stream()
|
||||
@@ -114,13 +108,21 @@ public class JwtTokenManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证token
|
||||
* 验证 token
|
||||
*/
|
||||
public void validateToken(String token) {
|
||||
if (jwtParser == null) {
|
||||
jwtParser = Jwts.parserBuilder().setSigningKey(this.getSecretKeyBytes()).build();
|
||||
}
|
||||
jwtParser.parseClaimsJws(token);
|
||||
// 验证 JWT,无异常解析成功说明JWT有效
|
||||
Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);
|
||||
// 验证 JWT 是否在黑名单(注销场景会存入黑名单)
|
||||
Claims claims = claimsJws.getBody();
|
||||
Boolean isBlack = redisTemplate.hasKey(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + claims.get("jti"));
|
||||
|
||||
if (isBlack) {
|
||||
throw new RuntimeException("token 已被禁用");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getSecretKeyBytes() {
|
||||
@@ -130,9 +132,19 @@ public class JwtTokenManager {
|
||||
} catch (DecodingException e) {
|
||||
secretKeyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
return secretKeyBytes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* get token claims
|
||||
*/
|
||||
public Claims getTokenClaims(String token) {
|
||||
if (jwtParser == null) {
|
||||
jwtParser = Jwts.parserBuilder().setSigningKey(this.getSecretKeyBytes()).build();
|
||||
}
|
||||
Claims claims = jwtParser.parseClaimsJws(token).getBody();
|
||||
return claims;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.system.framework.security.config;
|
||||
|
||||
import com.youlai.system.framework.security.constant.SecurityConstants;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.framework.security.filter.JwtAuthenticationFilter;
|
||||
import com.youlai.system.framework.security.exception.MyAccessDeniedHandler;
|
||||
import com.youlai.system.framework.security.exception.MyAuthenticationEntryPoint;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.youlai.system.framework.security.constant;
|
||||
|
||||
/**
|
||||
* Security 常量
|
||||
*
|
||||
* @author: haoxr
|
||||
* @date: 2023/03/24
|
||||
*/
|
||||
public interface SecurityConstants {
|
||||
|
||||
/**
|
||||
* 登录接口路径
|
||||
*/
|
||||
String LOGIN_PATH = "/api/v1/auth/login";
|
||||
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.youlai.system.framework.security.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
import com.youlai.system.common.util.RequestUtils;
|
||||
import com.youlai.system.common.util.ResponseUtils;
|
||||
import com.youlai.system.framework.security.JwtTokenManager;
|
||||
import com.youlai.system.framework.security.constant.SecurityConstants;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -23,8 +24,6 @@ import java.io.IOException;
|
||||
*/
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
private final JwtTokenManager tokenManager;
|
||||
|
||||
public JwtAuthenticationFilter(JwtTokenManager tokenManager) {
|
||||
@@ -37,10 +36,10 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
// 登录接口放行
|
||||
chain.doFilter(request, response);
|
||||
}else{
|
||||
String jwt = resolveToken(request);
|
||||
String jwt = RequestUtils.resolveToken(request);
|
||||
if (StrUtil.isNotBlank(jwt) && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
try {
|
||||
// 验证JWT
|
||||
// 验证 JWT
|
||||
this.tokenManager.validateToken(jwt);
|
||||
|
||||
// JWT验证有效获取Authentication存入Security上下文
|
||||
@@ -56,15 +55,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token from header.
|
||||
*/
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
|
||||
return bearerToken.substring(TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ package com.youlai.system.framework.security.filter;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.youlai.system.common.constant.CacheConstants;
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
import com.youlai.system.common.util.ResponseUtils;
|
||||
import com.youlai.system.framework.security.constant.SecurityConstants;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -49,7 +48,7 @@ public class VerifyCodeFilter extends OncePerRequestFilter {
|
||||
}
|
||||
// 缓存中的验证码
|
||||
String verifyCodeKey = request.getParameter(VERIFY_CODE_KEY);
|
||||
Object cacheVerifyCode = redisTemplate.opsForValue().get(CacheConstants.VERIFY_CODE_CACHE_PREFIX + verifyCodeKey);
|
||||
Object cacheVerifyCode = redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_CACHE_PREFIX + verifyCodeKey);
|
||||
if (cacheVerifyCode == null) {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user