refactor: 优化 JWT 解析和验证代码和修复用户名密码错误的异常提示
This commit is contained in:
@@ -2,8 +2,10 @@ package com.youlai.system;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
public class SystemApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SystemApplication.class, args);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.system.security.constant;
|
||||
package com.youlai.system.common.constant;
|
||||
|
||||
/**
|
||||
* JWT Claims声明常量
|
||||
@@ -24,4 +24,16 @@ public interface SecurityConstants {
|
||||
String BLACKLIST_TOKEN_PREFIX = "token:blacklist:";
|
||||
|
||||
|
||||
/**
|
||||
* 登录路径
|
||||
*/
|
||||
String LOGIN_PATH = "/api/v1/auth/login";
|
||||
|
||||
|
||||
/**
|
||||
* JWT Token 前缀
|
||||
*/
|
||||
String JWT_TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
package com.youlai.system.common.util;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.youlai.system.common.result.IResultCode;
|
||||
import com.youlai.system.common.result.Result;
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import static com.youlai.system.common.result.ResultCode.*;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 响应工具类
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class ResponseUtils {
|
||||
|
||||
/**
|
||||
* 异常消息返回(适用过滤器中处理异常响应)
|
||||
*
|
||||
* @param response
|
||||
* @param resultCode
|
||||
* @param response HttpServletResponse
|
||||
* @param resultCode 响应结果码
|
||||
*/
|
||||
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) throws IOException {
|
||||
switch (resultCode) {
|
||||
case ACCESS_UNAUTHORIZED:
|
||||
case TOKEN_INVALID:
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
break;
|
||||
case TOKEN_ACCESS_FORBIDDEN:
|
||||
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
break;
|
||||
default:
|
||||
response.setStatus(HttpStatus.BAD_REQUEST.value());
|
||||
break;
|
||||
}
|
||||
public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
|
||||
// 根据不同的结果码设置HTTP状态
|
||||
int status = switch (resultCode) {
|
||||
case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
|
||||
case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value();
|
||||
default -> HttpStatus.BAD_REQUEST.value();
|
||||
};
|
||||
|
||||
response.setStatus(status);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.getWriter().print(JSONUtil.toJsonStr(Result.failed(resultCode)));
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
|
||||
try (PrintWriter writer = response.getWriter()) {
|
||||
String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));
|
||||
writer.print(jsonResponse);
|
||||
writer.flush(); // 确保将响应内容写入到输出流
|
||||
} catch (IOException e) {
|
||||
log.error("响应异常处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.youlai.system.config;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import com.youlai.system.security.constant.SecurityConstants;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.config.property.SecurityProperties;
|
||||
import com.youlai.system.security.exception.MyAccessDeniedHandler;
|
||||
import com.youlai.system.security.exception.MyAuthenticationEntryPoint;
|
||||
import com.youlai.system.filter.JwtValidationFilter;
|
||||
@@ -17,6 +19,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
@@ -26,7 +29,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
|
||||
/**
|
||||
* Spring Security 权限配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray Hao
|
||||
* @since 2023/2/17
|
||||
*/
|
||||
@Configuration
|
||||
@@ -39,29 +42,33 @@ public class SecurityConfig {
|
||||
private final MyAccessDeniedHandler accessDeniedHandler;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CodeGenerator codeGenerator;
|
||||
private final SecurityProperties securityProperties;
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
|
||||
.authorizeHttpRequests(requestMatcherRegistry ->
|
||||
requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
|
||||
httpSecurityExceptionHandlingConfigurer
|
||||
.authenticationEntryPoint(authenticationEntryPoint)
|
||||
.accessDeniedHandler(accessDeniedHandler)
|
||||
)
|
||||
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||
|
||||
;
|
||||
|
||||
// 验证码校验过滤器
|
||||
http.addFilterBefore(new CaptchaValidationFilter(redisTemplate,codeGenerator), UsernamePasswordAuthenticationFilter.class);
|
||||
http.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class);
|
||||
// JWT 校验过滤器
|
||||
http.addFilterBefore(new JwtValidationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class);
|
||||
http.addFilterBefore(new JwtValidationFilter(redisTemplate,securityProperties.getJwt().getKey()), UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
@@ -71,18 +78,11 @@ public class SecurityConfig {
|
||||
*/
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring()
|
||||
.requestMatchers(
|
||||
"/api/v1/auth/captcha",
|
||||
"/webjars/**",
|
||||
"/doc.html",
|
||||
"/swagger-resources/**",
|
||||
"/v3/api-docs/**",
|
||||
"/swagger-ui/**",
|
||||
"/swagger-ui.html",
|
||||
"/ws/**",
|
||||
"/ws-app/**"
|
||||
);
|
||||
return (web) -> {
|
||||
if (CollectionUtil.isNotEmpty(securityProperties.getIgnoreUrls())) {
|
||||
web.ignoring().requestMatchers(securityProperties.getIgnoreUrls().toArray(new String[0]));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.system.config;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.jwt.JWTPayload;
|
||||
import com.youlai.system.security.util.JwtUtils;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -83,7 +83,7 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
// 这里不应该用"name"
|
||||
// String username = JwtUtils.parseToken(bearerToken).get("name").toString();
|
||||
String username = JwtUtils.parseToken(bearerToken).get(JWTPayload.SUBJECT).toString();
|
||||
String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT);
|
||||
|
||||
if (StrUtil.isNotBlank(username)) {
|
||||
accessor.setUser(() -> username);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.youlai.system.config.property;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author haoxr
|
||||
* @since 2024/4/18
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "security")
|
||||
public class SecurityProperties {
|
||||
|
||||
/**
|
||||
* 白名单 URL 集合
|
||||
*/
|
||||
private List<String> ignoreUrls;
|
||||
|
||||
/**
|
||||
* JWT 配置
|
||||
*/
|
||||
private JwtProperty jwt;
|
||||
|
||||
|
||||
/**
|
||||
* JWT 配置
|
||||
*/
|
||||
@Data
|
||||
public static class JwtProperty {
|
||||
|
||||
/**
|
||||
* JWT 秘钥
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* JWT 过期时间
|
||||
*/
|
||||
private Long ttl;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import java.io.IOException;
|
||||
*/
|
||||
public class CaptchaValidationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(com.youlai.system.security.constant.SecurityConstants.LOGIN_PATH, "POST");
|
||||
private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");
|
||||
|
||||
public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode";
|
||||
public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey";
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package com.youlai.system.filter;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.jwt.JWT;
|
||||
import cn.hutool.jwt.JWTPayload;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
import com.youlai.system.security.util.JwtUtils;
|
||||
import com.youlai.system.common.util.ResponseUtils;
|
||||
import com.youlai.system.common.exception.BusinessException;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -19,22 +20,25 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JWT token 校验过滤器
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray Hao
|
||||
* @since 2023/9/13
|
||||
*/
|
||||
public class JwtValidationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public JwtValidationFilter(RedisTemplate<String, Object> redisTemplate) {
|
||||
private final byte[] secretKey;
|
||||
|
||||
public JwtValidationFilter(RedisTemplate<String, Object> redisTemplate, String secretKey) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
this.secretKey = secretKey.getBytes();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从请求中获取 JWT Token,校验 JWT Token 是否合法
|
||||
* <p>
|
||||
@@ -44,27 +48,38 @@ public class JwtValidationFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
try {
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
Map<String, Object> payload = JwtUtils.parseToken(token);
|
||||
|
||||
String jti = Convert.toStr(payload.get(JWTPayload.JWT_ID));
|
||||
Boolean isTokenBlacklisted = redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti);
|
||||
if (Boolean.TRUE.equals(isTokenBlacklisted)) {
|
||||
try {
|
||||
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
|
||||
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); // 去除 Bearer 前缀
|
||||
// 校验 Token 是否有效
|
||||
if (JWTUtil.verify(token, secretKey)) {
|
||||
// 解析 Token 获取有效载荷
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
JSONObject payloads = jwt.getPayloads();
|
||||
|
||||
// 检查 Token 是否已被加入黑名单
|
||||
String jti = payloads.getStr(JWTPayload.JWT_ID);
|
||||
boolean isTokenBlacklisted = Boolean.TRUE.equals(redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti));
|
||||
if (isTokenBlacklisted) {
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
// Token 有效将其解析为 Authentication 对象,并设置到 Spring Security 上下文中
|
||||
Authentication authentication = JwtUtils.getAuthentication(payloads);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
} else {
|
||||
// Token 无效,直接返回响应
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
Authentication authentication = JwtUtils.getAuthentication(payload);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
} catch (BusinessException ex) {
|
||||
//this is very important, since it guarantees the user is not authenticated at all
|
||||
} catch (Exception e) {
|
||||
SecurityContextHolder.clearContext();
|
||||
ResponseUtils.writeErrMsg(response, (ResultCode) ex.getResultCode());
|
||||
ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
// Token有效或无Token时继续执行过滤链
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.youlai.system.plugin.dupsubmit.aspect;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.system.plugin.dupsubmit.annotation.PreventDuplicateSubmit;
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import cn.hutool.jwt.RegisteredPayload;
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.common.exception.BusinessException;
|
||||
import com.youlai.system.security.util.JwtUtils;
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
import com.youlai.system.plugin.dupsubmit.annotation.PreventDuplicateSubmit;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -69,8 +70,10 @@ public class DuplicateSubmitAspect {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
|
||||
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
String jti = Convert.toStr(JwtUtils.parseToken(token).get("jti"), null);
|
||||
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
|
||||
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
|
||||
// 从 JWT Token 中获取 jti
|
||||
String jti = (String) JWTUtil.parseToken(token).getPayload(RegisteredPayload.JWT_ID);
|
||||
resubmitLockKey = RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI();
|
||||
}
|
||||
return resubmitLockKey;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.youlai.system.security.constant;
|
||||
|
||||
/**
|
||||
* Security 常量
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface SecurityConstants {
|
||||
|
||||
/**
|
||||
* 登录接口路径
|
||||
*/
|
||||
String LOGIN_PATH = "/api/v1/auth/login";
|
||||
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.youlai.system.model.dto.UserAuthInfo;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
@@ -24,6 +25,7 @@ import java.util.stream.Collectors;
|
||||
@NoArgsConstructor
|
||||
public class SysUserDetails implements UserDetails {
|
||||
|
||||
@Getter
|
||||
private Long userId;
|
||||
|
||||
private String username;
|
||||
@@ -60,10 +62,6 @@ public class SysUserDetails implements UserDetails {
|
||||
this.dataScope = user.getDataScope();
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
|
||||
@@ -3,12 +3,10 @@ package com.youlai.system.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.JSONArray;
|
||||
import cn.hutool.jwt.JWT;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.jwt.JWTPayload;
|
||||
import cn.hutool.jwt.JWTUtil;
|
||||
import com.youlai.system.security.constant.JwtClaimConstants;
|
||||
import com.youlai.system.common.constant.JwtClaimConstants;
|
||||
import com.youlai.system.security.model.SysUserDetails;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
@@ -21,9 +19,9 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* JWT 工具类
|
||||
* JWT Token 工具类
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray Hao
|
||||
* @since 2.6.0
|
||||
*/
|
||||
@Component
|
||||
@@ -41,12 +39,12 @@ public class JwtUtils {
|
||||
private static int ttl;
|
||||
|
||||
|
||||
@Value("${jwt.key}")
|
||||
@Value("${security.jwt.key}")
|
||||
public void setKey(String key) {
|
||||
JwtUtils.key = key.getBytes();
|
||||
}
|
||||
|
||||
@Value("${jwt.ttl}")
|
||||
@Value("${security.jwt.ttl}")
|
||||
public void setTtl(Integer ttl) {
|
||||
JwtUtils.ttl = ttl;
|
||||
}
|
||||
@@ -57,7 +55,7 @@ public class JwtUtils {
|
||||
* @param authentication 用户认证信息
|
||||
* @return Token 字符串
|
||||
*/
|
||||
public static String generateToken(Authentication authentication) {
|
||||
public static String createToken(Authentication authentication) {
|
||||
|
||||
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
|
||||
|
||||
@@ -80,25 +78,25 @@ public class JwtUtils {
|
||||
payload.put(JWTPayload.SUBJECT, authentication.getName());
|
||||
payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());
|
||||
|
||||
return JWTUtil.createToken(payload, JwtUtils.key);
|
||||
return JWTUtil.createToken(payload, key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从 JWT Token 中解析 Authentication 用户认证信息
|
||||
*
|
||||
* @param payload JWT 载体
|
||||
* @param payloads JWT 载体
|
||||
* @return 用户认证信息
|
||||
*/
|
||||
public static UsernamePasswordAuthenticationToken getAuthentication(Map<String, Object> payload) {
|
||||
public static UsernamePasswordAuthenticationToken getAuthentication(JSONObject payloads) {
|
||||
SysUserDetails userDetails = new SysUserDetails();
|
||||
userDetails.setUserId(Convert.toLong(payload.get(JwtClaimConstants.USER_ID))); // 用户ID
|
||||
userDetails.setDeptId(Convert.toLong(payload.get(JwtClaimConstants.DEPT_ID))); // 部门ID
|
||||
userDetails.setDataScope(Convert.toInt(payload.get(JwtClaimConstants.DATA_SCOPE))); // 数据权限范围
|
||||
userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
|
||||
userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID
|
||||
userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围
|
||||
|
||||
userDetails.setUsername(Convert.toStr(payload.get(JWTPayload.SUBJECT))); // 用户名
|
||||
userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
|
||||
// 角色集合
|
||||
Set<SimpleGrantedAuthority> authorities = ((JSONArray) payload.get(JwtClaimConstants.AUTHORITIES))
|
||||
Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES)
|
||||
.stream()
|
||||
.map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
|
||||
.collect(Collectors.toSet());
|
||||
@@ -107,30 +105,4 @@ public class JwtUtils {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析 JWT Token 获取载体信息
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 载体信息
|
||||
*/
|
||||
public static Map<String, Object> parseToken(String token) {
|
||||
try {
|
||||
if (StrUtil.isBlank(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token.startsWith("Bearer ")) {
|
||||
token = token.substring(7);
|
||||
}
|
||||
|
||||
JWT jwt = JWTUtil.parseToken(token);
|
||||
if (jwt.setKey(JwtUtils.key).validate(0)) {
|
||||
return jwt.getPayloads();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package com.youlai.system.service.impl;
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
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.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.common.enums.CaptchaTypeEnum;
|
||||
import com.youlai.system.model.dto.CaptchaResult;
|
||||
@@ -28,7 +29,6 @@ import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ import java.util.concurrent.TimeUnit;
|
||||
public class AuthServiceImpl implements AuthService {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RedisTemplate<String,Object> redisTemplate;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CodeGenerator codeGenerator;
|
||||
private final Font captchaFont;
|
||||
private final CaptchaProperties captchaProperties;
|
||||
@@ -57,10 +57,13 @@ public class AuthServiceImpl implements AuthService {
|
||||
*/
|
||||
@Override
|
||||
public LoginResult login(String username, String password) {
|
||||
// 认证用户信息
|
||||
UsernamePasswordAuthenticationToken authenticationToken =
|
||||
new UsernamePasswordAuthenticationToken(username.toLowerCase().trim(), password);
|
||||
// 认证
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
String accessToken = JwtUtils.generateToken(authentication);
|
||||
// 认证成功,生成Token
|
||||
String accessToken = JwtUtils.createToken(authentication);
|
||||
return LoginResult.builder()
|
||||
.tokenType("Bearer")
|
||||
.accessToken(accessToken)
|
||||
@@ -74,18 +77,24 @@ public class AuthServiceImpl implements AuthService {
|
||||
public void logout() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(token)) {
|
||||
|
||||
Map<String, Object> claims = JwtUtils.parseToken(token);
|
||||
String jti = Convert.toStr(claims.get(JWTPayload.JWT_ID));
|
||||
Long expiration = Convert.toLong(claims.get(JWTPayload.EXPIRES_AT));
|
||||
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) {
|
||||
// 将Token的jti加入黑名单,并设置剩余有效时间,使其在过期后自动从黑名单移除
|
||||
long ttl = expiration - System.currentTimeMillis() / 1000;
|
||||
redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS);
|
||||
} else {
|
||||
// 如果exp不存在,说明Token永不过期,则永久加入黑名单
|
||||
redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null);
|
||||
}
|
||||
}
|
||||
// 清空Spring Security上下文
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user