temp
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package org.xyzh.common.auth.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* @description HttpLogin.java文件描述 HTTP登录注解
|
||||
* @filename HttpLogin.java
|
||||
* @author yslg
|
||||
* @copyright yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface HttpLogin {
|
||||
|
||||
/**
|
||||
* @description 是否必需,默认为true
|
||||
* @return boolean
|
||||
* @author yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
boolean required() default true;
|
||||
|
||||
/**
|
||||
* @description 当token无效时的错误消息
|
||||
* @return String
|
||||
* @author yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
String message() default "用户未登录或登录已过期";
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package org.xyzh.common.auth.annotation.resovler;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.xyzh.common.auth.annotation.HttpLogin;
|
||||
import org.xyzh.common.auth.token.TokenParser;
|
||||
import org.xyzh.common.core.domain.LoginDomain;
|
||||
import org.xyzh.common.utils.NonUtils;
|
||||
import org.xyzh.redis.service.RedisService;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* @description HttpLoginArgumentResolver.java文件描述 HTTP登录参数解析器
|
||||
* @filename HttpLoginArgumentResolver.java
|
||||
* @author yslg
|
||||
* @copyright yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
@Component
|
||||
public class HttpLoginArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final TokenParser tokenParser;
|
||||
private final RedisService redisService;
|
||||
|
||||
/**
|
||||
* 使用构造器注入,依赖TokenParser接口而非具体实现
|
||||
* 这样避免了与auth模块的直接依赖,解决循环依赖问题
|
||||
*/
|
||||
public HttpLoginArgumentResolver(TokenParser tokenParser,
|
||||
RedisService redisService) {
|
||||
this.tokenParser = tokenParser;
|
||||
this.redisService = redisService;
|
||||
}
|
||||
|
||||
private static final String TOKEN_PREFIX = "Bearer ";
|
||||
private static final String REDIS_LOGIN_PREFIX = "login:token:";
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(HttpLogin.class)
|
||||
&& LoginDomain.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||
|
||||
HttpLogin httpLogin = parameter.getParameterAnnotation(HttpLogin.class);
|
||||
if (httpLogin == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从请求头中获取token
|
||||
String token = extractTokenFromRequest(webRequest);
|
||||
|
||||
if (NonUtils.isEmpty(token)) {
|
||||
if (httpLogin.required()) {
|
||||
throw new IllegalArgumentException(httpLogin.message());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证token格式和有效性
|
||||
if (!tokenParser.validateToken(token, tokenParser.getUserIdFromToken(token))) {
|
||||
if (httpLogin.required()) {
|
||||
throw new IllegalArgumentException(httpLogin.message());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从Redis中获取LoginDomain
|
||||
String userId = tokenParser.getUserIdFromToken(token);
|
||||
String redisKey = REDIS_LOGIN_PREFIX + userId;
|
||||
LoginDomain loginDomain = (LoginDomain) redisService.get(redisKey);
|
||||
|
||||
if (loginDomain == null) {
|
||||
if (httpLogin.required()) {
|
||||
throw new IllegalArgumentException(httpLogin.message());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更新token信息
|
||||
loginDomain.setToken(token);
|
||||
return loginDomain;
|
||||
|
||||
} catch (Exception e) {
|
||||
if (httpLogin.required()) {
|
||||
throw new IllegalArgumentException(httpLogin.message());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从请求中提取token
|
||||
* @param webRequest 请求对象
|
||||
* @return String token
|
||||
* @author yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
private String extractTokenFromRequest(NativeWebRequest webRequest) {
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 优先从Authorization头获取
|
||||
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (NonUtils.isNotEmpty(authHeader) && authHeader.startsWith(TOKEN_PREFIX)) {
|
||||
return authHeader.substring(TOKEN_PREFIX.length());
|
||||
}
|
||||
|
||||
// 从请求参数中获取token
|
||||
String token = request.getParameter("token");
|
||||
if (NonUtils.isNotEmpty(token)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// 从请求头中获取token
|
||||
token = request.getHeader("token");
|
||||
if (NonUtils.isNotEmpty(token)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package org.xyzh.common.auth.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 认证配置属性类
|
||||
* 用于配置认证相关的属性,包括白名单路径
|
||||
*
|
||||
* @author yslg
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "urban-lifeline.auth")
|
||||
public class AuthProperties {
|
||||
|
||||
/**
|
||||
* 是否启用认证过滤器
|
||||
* 默认启用
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 登录接口路径
|
||||
* 支持不同服务自定义登录地址
|
||||
*/
|
||||
private String loginPath = "/urban-lifeline/auth/login";
|
||||
|
||||
/**
|
||||
* 登出接口路径
|
||||
*/
|
||||
private String logoutPath = "/urban-lifeline/auth/logout";
|
||||
|
||||
/**
|
||||
* 验证码获取接口路径
|
||||
*/
|
||||
private String captchaPath = "/urban-lifeline/auth/captcha";
|
||||
|
||||
/**
|
||||
* 刷新 Token 接口路径
|
||||
*/
|
||||
private String refreshPath = "/urban-lifeline/auth/refresh";
|
||||
|
||||
/**
|
||||
* 通用白名单路径列表(非认证接口)
|
||||
* 例如:Swagger、静态资源、健康检查等
|
||||
*/
|
||||
private List<String> whitelist = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Token 请求头名称
|
||||
* 默认: Authorization
|
||||
*/
|
||||
private String tokenHeader = "Authorization";
|
||||
|
||||
/**
|
||||
* Token 前缀
|
||||
* 默认: Bearer
|
||||
*/
|
||||
private String tokenPrefix = "Bearer ";
|
||||
|
||||
public AuthProperties() {
|
||||
// 默认通用白名单:Swagger 及静态资源等
|
||||
whitelist.add("/swagger-ui/**");
|
||||
whitelist.add("/swagger-ui.html");
|
||||
whitelist.add("/v3/api-docs/**");
|
||||
whitelist.add("/webjars/**");
|
||||
whitelist.add("/favicon.ico");
|
||||
whitelist.add("/error");
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getLoginPath() {
|
||||
return loginPath;
|
||||
}
|
||||
|
||||
public void setLoginPath(String loginPath) {
|
||||
this.loginPath = loginPath;
|
||||
}
|
||||
|
||||
public String getLogoutPath() {
|
||||
return logoutPath;
|
||||
}
|
||||
|
||||
public void setLogoutPath(String logoutPath) {
|
||||
this.logoutPath = logoutPath;
|
||||
}
|
||||
|
||||
public String getCaptchaPath() {
|
||||
return captchaPath;
|
||||
}
|
||||
|
||||
public void setCaptchaPath(String captchaPath) {
|
||||
this.captchaPath = captchaPath;
|
||||
}
|
||||
|
||||
public String getRefreshPath() {
|
||||
return refreshPath;
|
||||
}
|
||||
|
||||
public void setRefreshPath(String refreshPath) {
|
||||
this.refreshPath = refreshPath;
|
||||
}
|
||||
|
||||
public List<String> getWhitelist() {
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
public void setWhitelist(List<String> whitelist) {
|
||||
this.whitelist = whitelist;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证相关接口路径集合(login / logout / captcha / refresh)
|
||||
* 供 SecurityConfig 和 JwtAuthenticationFilter 统一放行
|
||||
*/
|
||||
public List<String> getAuthPaths() {
|
||||
List<String> authPaths = new ArrayList<>();
|
||||
if (StringUtils.hasText(loginPath)) {
|
||||
authPaths.add(loginPath);
|
||||
}
|
||||
if (StringUtils.hasText(logoutPath)) {
|
||||
authPaths.add(logoutPath);
|
||||
}
|
||||
if (StringUtils.hasText(captchaPath)) {
|
||||
authPaths.add(captchaPath);
|
||||
}
|
||||
if (StringUtils.hasText(refreshPath)) {
|
||||
authPaths.add(refreshPath);
|
||||
}
|
||||
return authPaths;
|
||||
}
|
||||
|
||||
public String getTokenHeader() {
|
||||
return tokenHeader;
|
||||
}
|
||||
|
||||
public void setTokenHeader(String tokenHeader) {
|
||||
this.tokenHeader = tokenHeader;
|
||||
}
|
||||
|
||||
public String getTokenPrefix() {
|
||||
return tokenPrefix;
|
||||
}
|
||||
|
||||
public void setTokenPrefix(String tokenPrefix) {
|
||||
this.tokenPrefix = tokenPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.xyzh.common.auth.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.xyzh.common.auth.filter.JwtAuthenticationFilter;
|
||||
import org.xyzh.common.auth.token.TokenParser;
|
||||
import org.xyzh.redis.service.RedisService;
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationFilter jwtAuthenticationFilter(TokenParser tokenParser,
|
||||
AuthProperties authProperties,
|
||||
RedisService redisService) {
|
||||
return new JwtAuthenticationFilter(tokenParser, authProperties, redisService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http,
|
||||
AuthProperties authProperties,
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.formLogin(form -> form.disable())
|
||||
.httpBasic(basic -> basic.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(authz -> {
|
||||
// 认证接口放行(login / logout / captcha / refresh)
|
||||
if (authProperties.getAuthPaths() != null) {
|
||||
authProperties.getAuthPaths().forEach(path -> authz.requestMatchers(path).permitAll());
|
||||
}
|
||||
|
||||
// 通用白名单放行(Swagger、静态资源等)
|
||||
if (authProperties.getWhitelist() != null) {
|
||||
authProperties.getWhitelist().forEach(path -> authz.requestMatchers(path).permitAll());
|
||||
}
|
||||
|
||||
authz
|
||||
.requestMatchers("/error", "/favicon.ico").permitAll()
|
||||
.anyRequest().authenticated();
|
||||
})
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.xyzh.common.auth.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.xyzh.common.auth.annotation.resovler.HttpLoginArgumentResolver;
|
||||
|
||||
/**
|
||||
* @description WebMvcConfig.java文件描述 WebMVC配置类
|
||||
* @filename WebMvcConfig.java
|
||||
* @author yslg
|
||||
* @copyright yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final HandlerMethodArgumentResolver httpLoginArgumentResolver;
|
||||
|
||||
/**
|
||||
* 使用构造器注入
|
||||
* 通过接口抽象解决了循环依赖问题,不再需要@Lazy注解
|
||||
*/
|
||||
public WebMvcConfig(HttpLoginArgumentResolver httpLoginArgumentResolver) {
|
||||
this.httpLoginArgumentResolver = httpLoginArgumentResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||
resolvers.add(httpLoginArgumentResolver);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.xyzh.common.auth.contants;
|
||||
|
||||
/**
|
||||
* @description 认证相关常量类
|
||||
* @filename AuthContants.java
|
||||
* @author yslg
|
||||
* @copyright yslg
|
||||
* @since 2025-11-09
|
||||
*/
|
||||
public class AuthContants {
|
||||
|
||||
/**
|
||||
* 用户ID请求属性键
|
||||
*/
|
||||
public static final String USER_ID_ATTRIBUTE = "userId";
|
||||
|
||||
/**
|
||||
* 用户名请求属性键
|
||||
*/
|
||||
public static final String USERNAME_ATTRIBUTE = "username";
|
||||
|
||||
/**
|
||||
* Token请求属性键
|
||||
*/
|
||||
public static final String TOKEN_ATTRIBUTE = "token";
|
||||
|
||||
/**
|
||||
* JWT Claims 中的用户名键
|
||||
*/
|
||||
public static final String CLAIMS_USERNAME_KEY = "username";
|
||||
|
||||
/**
|
||||
* JWT Claims 中的用户ID键
|
||||
*/
|
||||
public static final String CLAIMS_USER_ID_KEY = "userId";
|
||||
|
||||
/**
|
||||
* 私有构造函数,防止实例化
|
||||
*/
|
||||
private AuthContants() {
|
||||
throw new UnsupportedOperationException("常量类不允许实例化");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package org.xyzh.common.auth.filter;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.xyzh.common.auth.config.AuthProperties;
|
||||
import org.xyzh.common.auth.contants.AuthContants;
|
||||
import org.xyzh.common.auth.token.TokenParser;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.core.domain.LoginDomain;
|
||||
import org.xyzh.common.dto.sys.TbSysPermissionDTO;
|
||||
import org.xyzh.redis.service.RedisService;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* @description JWT认证过滤器,用于检测用户请求是否登录,支持白名单配置
|
||||
* @filename JwtAuthenticationFilter.java
|
||||
* @author yslg
|
||||
* @copyright yslg
|
||||
* @since 2025-11-09
|
||||
*/
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
||||
|
||||
private final TokenParser tokenParser;
|
||||
private final AuthProperties authProperties;
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
private final RedisService redisService;
|
||||
private static final String REDIS_LOGIN_PREFIX = "login:token:";
|
||||
|
||||
public JwtAuthenticationFilter(TokenParser tokenParser, AuthProperties authProperties) {
|
||||
this.tokenParser = tokenParser;
|
||||
this.authProperties = authProperties;
|
||||
this.redisService = null; // 占位,使用另一个构造函数注入
|
||||
}
|
||||
|
||||
public JwtAuthenticationFilter(TokenParser tokenParser, AuthProperties authProperties, RedisService redisService) {
|
||||
this.tokenParser = tokenParser;
|
||||
this.authProperties = authProperties;
|
||||
this.redisService = redisService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// 如果认证过滤器未启用,直接放行
|
||||
if (!authProperties.isEnabled()) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String requestPath = request.getRequestURI();
|
||||
if (requestPath == null) {
|
||||
requestPath = "";
|
||||
}
|
||||
|
||||
// 去掉 context-path,仅保留业务路径用于白名单匹配
|
||||
String contextPath = request.getContextPath();
|
||||
if (contextPath != null && !contextPath.isEmpty() && requestPath.startsWith(contextPath)) {
|
||||
requestPath = requestPath.substring(contextPath.length());
|
||||
}
|
||||
|
||||
// 检查是否在白名单中
|
||||
if (isWhitelisted(requestPath)) {
|
||||
log.debug("请求路径在白名单中,跳过认证: {}", requestPath);
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 从请求头获取Token
|
||||
String token = extractToken(request);
|
||||
|
||||
if (!StringUtils.hasText(token)) {
|
||||
log.warn("请求缺少Token: {}", requestPath);
|
||||
handleUnauthorized(response, "未提供认证令牌,请先登录");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证Token
|
||||
if (tokenParser.isTokenExpired(token)) {
|
||||
log.warn("Token已过期: {}", requestPath);
|
||||
handleUnauthorized(response, "认证令牌已过期,请重新登录");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
String userId = tokenParser.getUserIdFromToken(token);
|
||||
if (!StringUtils.hasText(userId)) {
|
||||
log.warn("Token中未找到用户ID: {}", requestPath);
|
||||
handleUnauthorized(response, "认证令牌无效");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证Token有效性
|
||||
if (!tokenParser.validateToken(token, userId)) {
|
||||
log.warn("Token验证失败: userId={}, path={}", userId, requestPath);
|
||||
handleUnauthorized(response, "认证令牌验证失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 将用户信息放入请求属性中,供后续使用
|
||||
Claims claims = tokenParser.getAllClaimsFromToken(token);
|
||||
request.setAttribute(AuthContants.USER_ID_ATTRIBUTE, userId);
|
||||
request.setAttribute(AuthContants.USERNAME_ATTRIBUTE, claims.get(AuthContants.CLAIMS_USERNAME_KEY, String.class));
|
||||
request.setAttribute(AuthContants.TOKEN_ATTRIBUTE, token);
|
||||
|
||||
// 从Redis加载 LoginDomain,并将权限装配到 Spring Security 上下文
|
||||
if (redisService != null) {
|
||||
Object obj = redisService.get(REDIS_LOGIN_PREFIX + userId);
|
||||
if (obj instanceof LoginDomain loginDomain) {
|
||||
// 组装权限码 authorities(已存在)
|
||||
List<SimpleGrantedAuthority> permAuthorities = null;
|
||||
if (loginDomain.getUserPermissions() != null) {
|
||||
permAuthorities = loginDomain.getUserPermissions().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(TbSysPermissionDTO::getCode)
|
||||
.filter(StringUtils::hasText)
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 组装角色 authorities(关键:ROLE_ 前缀)
|
||||
List<SimpleGrantedAuthority> roleAuthorities = null;
|
||||
if (loginDomain.getUserRoles() != null) {
|
||||
roleAuthorities = loginDomain.getUserRoles().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(r -> r.getRoleId()) // 若有角色code/名称,可替换为对应字段
|
||||
.filter(StringUtils::hasText)
|
||||
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 合并权限与角色
|
||||
List<SimpleGrantedAuthority> authorities = Stream
|
||||
.concat(
|
||||
permAuthorities != null ? permAuthorities.stream() : Stream.empty(),
|
||||
roleAuthorities != null ? roleAuthorities.stream() : Stream.empty()
|
||||
)
|
||||
.toList();
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(loginDomain, null, authorities);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Token验证成功: userId={}, path={}", userId, requestPath);
|
||||
|
||||
// 继续执行过滤器链
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Token解析或验证异常: path={}, error={}", requestPath, e.getMessage(), e);
|
||||
handleUnauthorized(response, "认证令牌解析失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否在白名单中
|
||||
*/
|
||||
private boolean isWhitelisted(@NonNull String path) {
|
||||
// 1. 先检查认证相关接口(login / logout / captcha / refresh)
|
||||
if (authProperties.getAuthPaths() != null) {
|
||||
for (String pattern : authProperties.getAuthPaths()) {
|
||||
if (pattern != null && pathMatcher.match(pattern, path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 再检查通用白名单
|
||||
if (authProperties.getWhitelist() != null) {
|
||||
for (String pattern : authProperties.getWhitelist()) {
|
||||
if (pattern != null && pathMatcher.match(pattern, path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取Token
|
||||
*/
|
||||
private String extractToken(HttpServletRequest request) {
|
||||
String header = request.getHeader(authProperties.getTokenHeader());
|
||||
|
||||
if (!StringUtils.hasText(header)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 支持 Bearer 前缀
|
||||
String prefix = authProperties.getTokenPrefix();
|
||||
if (StringUtils.hasText(prefix) && header.startsWith(prefix)) {
|
||||
return header.substring(prefix.length()).trim();
|
||||
}
|
||||
|
||||
// 也支持直接传递Token(不带前缀)
|
||||
return header.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理未授权响应
|
||||
*/
|
||||
private void handleUnauthorized(HttpServletResponse response, String message) throws IOException {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
|
||||
ResultDomain<Object> result = ResultDomain.failure(HttpStatus.UNAUTHORIZED.value(), message);
|
||||
String json = JSON.toJSONString(result);
|
||||
|
||||
response.getWriter().write(json);
|
||||
response.getWriter().flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.xyzh.common.auth.token;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.xyzh.common.auth.utils.JwtTokenUtil;
|
||||
|
||||
/**
|
||||
* @description JwtTokenParser.java文件描述 JWT令牌解析器实现类
|
||||
* @filename JwtTokenParser.java
|
||||
* @author yslg
|
||||
* @copyright yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
@Component
|
||||
public class JwtTokenParser implements TokenParser {
|
||||
|
||||
private final JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
/**
|
||||
* Spring会自动注入,无需@Autowired注解
|
||||
*/
|
||||
public JwtTokenParser(JwtTokenUtil jwtTokenUtil) {
|
||||
this.jwtTokenUtil = jwtTokenUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserIdFromToken(String token) {
|
||||
return jwtTokenUtil.getUserIdFromToken(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Claims getAllClaimsFromToken(String token) {
|
||||
return jwtTokenUtil.getAllClaimsFromToken(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateToken(String token, String userId) {
|
||||
return jwtTokenUtil.validateToken(token, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTokenExpired(String token) {
|
||||
return jwtTokenUtil.isTokenExpired(token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.xyzh.common.auth.token;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
|
||||
/**
|
||||
* @description TokenParser.java文件描述 令牌解析器接口
|
||||
* @filename TokenParser.java
|
||||
* @author yslg
|
||||
* @copyright yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
public interface TokenParser {
|
||||
|
||||
/**
|
||||
* @description 从令牌中获取用户ID
|
||||
* @param token 令牌
|
||||
* @return String 用户ID
|
||||
* @author yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
String getUserIdFromToken(String token);
|
||||
|
||||
/**
|
||||
* @description 从令牌中获取所有声明信息
|
||||
* @param token 令牌
|
||||
* @return Claims 所有声明信息
|
||||
* @author yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
Claims getAllClaimsFromToken(String token);
|
||||
|
||||
/**
|
||||
* @description 验证令牌
|
||||
* @param token 令牌
|
||||
* @param userId 用户ID
|
||||
* @return boolean 是否有效
|
||||
* @author yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
boolean validateToken(String token, String userId);
|
||||
|
||||
/**
|
||||
* @description 检查令牌是否过期
|
||||
* @param token 令牌
|
||||
* @return boolean 是否过期
|
||||
* @author yslg
|
||||
* @since 2025-11-02
|
||||
*/
|
||||
boolean isTokenExpired(String token);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package org.xyzh.common.auth.utils;
|
||||
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.xyzh.common.core.domain.LoginDomain;
|
||||
import org.xyzh.common.dto.sys.TbSysUserDTO;
|
||||
import org.xyzh.common.utils.IDUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description JwtTokenUtil.java文件描述 JWT工具类
|
||||
* @filename JwtTokenUtil.java
|
||||
* @author yslg
|
||||
* @copyright yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
@Component
|
||||
public class JwtTokenUtil {
|
||||
|
||||
@Value("${urban-lifeline.auth.jwt-secret:schoolNewsDefaultSecretKeyForJWT2025}")
|
||||
private String secret;
|
||||
|
||||
@Value("${urban-lifeline.auth.jwt-expiration:86400}")
|
||||
private Long expiration;
|
||||
|
||||
private SecretKey getSigningKey() {
|
||||
return Keys.hmacShaKeyFor(secret.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生成JWT令牌
|
||||
* @param loginDomain 登录域对象
|
||||
* @return String JWT令牌
|
||||
* @author yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
public String generateToken(LoginDomain loginDomain) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
TbSysUserDTO user = loginDomain.getUser();
|
||||
|
||||
claims.put("userId", user.getUserId());
|
||||
claims.put("username", user.getUsername());
|
||||
claims.put("email", user.getEmail());
|
||||
claims.put("loginType", loginDomain.getLoginType());
|
||||
claims.put("ipAddress", loginDomain.getIpAddress());
|
||||
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(user.getUserId())
|
||||
.setId(IDUtils.generateID()) // 使用IDUtils生成JWT ID
|
||||
.setIssuedAt(new Date())
|
||||
.setExpiration(generateExpirationDate())
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从令牌中获取用户ID
|
||||
* @param token JWT令牌
|
||||
* @return String 用户ID
|
||||
* @author yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
public String getUserIdFromToken(String token) {
|
||||
return getClaimFromToken(token, Claims::getSubject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从令牌中获取过期时间
|
||||
* @param token JWT令牌
|
||||
* @return Date 过期时间
|
||||
* @author yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
return getClaimFromToken(token, Claims::getExpiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从令牌中获取指定信息
|
||||
* @param token JWT令牌
|
||||
* @param claimsResolver 信息解析器
|
||||
* @return T 指定信息
|
||||
* @author yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
public <T> T getClaimFromToken(String token, java.util.function.Function<Claims, T> claimsResolver) {
|
||||
final Claims claims = getAllClaimsFromToken(token);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从令牌中获取所有信息
|
||||
* @param token JWT令牌
|
||||
* @return Claims 所有信息
|
||||
* @author yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
public Claims getAllClaimsFromToken(String token) {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(getSigningKey())
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 验证令牌
|
||||
* @param token JWT令牌
|
||||
* @param userId 用户ID
|
||||
* @return boolean 是否有效
|
||||
* @author yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
public boolean validateToken(String token, String userId) {
|
||||
try {
|
||||
final String tokenUserId = getUserIdFromToken(token);
|
||||
return (userId.equals(tokenUserId) && !isTokenExpired(token));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 检查令牌是否过期
|
||||
* @param token JWT令牌
|
||||
* @return boolean 是否过期
|
||||
* @author yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
final Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration.before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生成过期时间
|
||||
* @return Date 过期时间
|
||||
* @author yslg
|
||||
* @since 2025-11-07
|
||||
*/
|
||||
private Date generateExpirationDate() {
|
||||
return new Date(System.currentTimeMillis() + expiration * 1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# 认证配置示例文件
|
||||
# 将此配置添加到各服务的 application.yml 中
|
||||
|
||||
urban-lifeline:
|
||||
auth:
|
||||
enabled: true
|
||||
|
||||
# 认证接口:可以按服务自定义
|
||||
login-path: /urban-lifeline/auth/login
|
||||
logout-path: /urban-lifeline/auth/logout
|
||||
captcha-path: /urban-lifeline/auth/captcha
|
||||
refresh-path: /urban-lifeline/auth/refresh
|
||||
|
||||
# 通用白名单(非认证接口)
|
||||
whitelist:
|
||||
# Swagger/OpenAPI 文档相关(建议不带 context-path)
|
||||
- /swagger-ui/**
|
||||
- /swagger-ui.html
|
||||
- /v3/api-docs/**
|
||||
- /webjars/**
|
||||
|
||||
# 静态资源
|
||||
- /favicon.ico
|
||||
- /error
|
||||
|
||||
# 健康检查
|
||||
- /actuator/health
|
||||
- /actuator/info
|
||||
|
||||
# 其他需要放行的路径
|
||||
# - /public/**
|
||||
# - /api/public/**
|
||||
|
||||
Reference in New Issue
Block a user