This commit is contained in:
2025-12-02 13:21:18 +08:00
parent fab8c13cb3
commit ee6dd64f98
192 changed files with 25783 additions and 0 deletions

View File

@@ -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 "用户未登录或登录已过期";
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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("常量类不允许实例化");
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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/**