refactor(auth): 重构认证过滤器并整合 Spring Security
-将 JwtInterceptor 重命名为 JwtAuthenticationFilter,并改为继承 OncePerRequestFilter - 新增白名单路径配置,对特定请求进行放行- 优化 token 验证逻辑,改进错误响应处理 -重构 JwtUtil 类,简化方法并提高代码可读性 - 更新 SecurityConfig,整合自定义认证过滤器
This commit is contained in:
parent
c007ae5c8a
commit
ff6c20fe00
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.example.admin_server.config;
|
||||||
|
|
||||||
|
import com.example.admin_server.filter.JwtAuthenticationFilter;
|
||||||
|
import com.example.admin_server.utils.JwtUtil;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
|
public SecurityConfig(JwtUtil jwtUtil) {
|
||||||
|
this.jwtUtil = jwtUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtAuthenticationFilter jwtAuthenticationFilter() {
|
||||||
|
return new JwtAuthenticationFilter(jwtUtil);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf().disable()
|
||||||
|
.sessionManagement()
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
|
.and()
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(
|
||||||
|
"/swagger-ui.html",
|
||||||
|
"/swagger-ui/**",
|
||||||
|
"/v3/api-docs/**",
|
||||||
|
"/api/public/**",
|
||||||
|
"/api/admin/login"
|
||||||
|
).permitAll()
|
||||||
|
.antMatchers("/api/admin/**").authenticated()
|
||||||
|
.antMatchers("/api/client/**").authenticated()
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
.and()
|
||||||
|
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
package com.example.admin_server.config;
|
|
||||||
|
|
||||||
import com.example.admin_server.interceptor.JwtInterceptor;
|
|
||||||
import com.example.admin_server.utils.JwtUtil;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSecurity
|
|
||||||
public class WebSecurityConfig implements WebMvcConfigurer {
|
|
||||||
|
|
||||||
private final JwtUtil jwtUtil;
|
|
||||||
|
|
||||||
public WebSecurityConfig(JwtUtil jwtUtil) {
|
|
||||||
this.jwtUtil = jwtUtil;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册自定义的 JWT 拦截器
|
|
||||||
@Override
|
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
|
||||||
registry.addInterceptor(new JwtInterceptor(jwtUtil))
|
|
||||||
.addPathPatterns("/api/admin/**", "/api/client/**"); // 只拦截需要鉴权的路径
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spring Security的过滤器链配置
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
||||||
http
|
|
||||||
// 关闭csrf,因为JWT不需要csrf保护
|
|
||||||
.csrf().disable()
|
|
||||||
// 使用无状态会话,不创建session
|
|
||||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
||||||
.and()
|
|
||||||
// 允许匿名访问的接口(登录、注册等)
|
|
||||||
.authorizeRequests()
|
|
||||||
.antMatchers("/api/public/**", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", "/doc.html").permitAll()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
.and()
|
|
||||||
// 禁用默认登录页
|
|
||||||
.formLogin().disable()
|
|
||||||
// 禁用默认注销功能
|
|
||||||
.logout().disable();
|
|
||||||
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package com.example.admin_server.filter;
|
||||||
|
|
||||||
|
import com.example.admin_server.constant.AuthConst;
|
||||||
|
import com.example.admin_server.utils.JwtUtil;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
|
// 白名单路径,登录接口一般放这里
|
||||||
|
private static final Set<String> WHITELIST = new HashSet<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
WHITELIST.add("/api/admin/login");
|
||||||
|
WHITELIST.add("/api/client/login");
|
||||||
|
WHITELIST.add("/api/employee/login");
|
||||||
|
// 也可以放其它公开接口
|
||||||
|
}
|
||||||
|
|
||||||
|
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
|
||||||
|
this.jwtUtil = jwtUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
@NonNull HttpServletResponse response,
|
||||||
|
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
// 如果路径在白名单,放行
|
||||||
|
if (WHITELIST.contains(requestURI)) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String token = null;
|
||||||
|
if (requestURI.startsWith("/api/admin/")) {
|
||||||
|
token = request.getHeader(AuthConst.ADMIN_AUTHORIZATION_HEADER);
|
||||||
|
} else if (requestURI.startsWith("/api/client/")) {
|
||||||
|
token = request.getHeader(AuthConst.FRONT_AUTHORIZATION_HEADER);
|
||||||
|
} else {
|
||||||
|
// 非需鉴权路径,放行
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StringUtils.hasText(token)) {
|
||||||
|
writeResponse(response, 401, "{\"code\":400,\"msg\":\"未登录:缺少token\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<Claims> claimsOpt = jwtUtil.parseToken(token);
|
||||||
|
if (!claimsOpt.isPresent() || jwtUtil.isTokenExpired(token)) {
|
||||||
|
writeResponse(response, 401, "{\"code\":401,\"msg\":\"未授权:token无效或过期\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以把用户信息放到SecurityContextHolder或request里
|
||||||
|
// SecurityContextHolder.getContext().setAuthentication(...)
|
||||||
|
// request.setAttribute("claims", claimsOpt.get());
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeResponse(HttpServletResponse response, int status, String json) throws IOException {
|
||||||
|
response.setStatus(status);
|
||||||
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
response.getWriter().write(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
package com.example.admin_server.interceptor;
|
|
||||||
|
|
||||||
import com.example.admin_server.common.Result;
|
|
||||||
import com.example.admin_server.constant.AuthConst;
|
|
||||||
import com.example.admin_server.enums.ResultCode;
|
|
||||||
import com.example.admin_server.utils.JwtUtil;
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import lombok.NonNull;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class JwtInterceptor implements HandlerInterceptor {
|
|
||||||
|
|
||||||
private final JwtUtil jwtUtil;
|
|
||||||
|
|
||||||
public JwtInterceptor(JwtUtil jwtUtil) {
|
|
||||||
this.jwtUtil = jwtUtil;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,@NonNull Object handler) throws Exception {
|
|
||||||
String requestURI = request.getRequestURI();
|
|
||||||
|
|
||||||
String token = null;
|
|
||||||
String clientType = null;
|
|
||||||
|
|
||||||
if (requestURI.startsWith("/api/admin/")) {
|
|
||||||
// 管理端请求
|
|
||||||
token = request.getHeader(AuthConst.ADMIN_AUTHORIZATION_HEADER);
|
|
||||||
clientType = "admin";
|
|
||||||
} else if (requestURI.startsWith("/api/client/")) {
|
|
||||||
// 客户端请求
|
|
||||||
token = request.getHeader(AuthConst.FRONT_AUTHORIZATION_HEADER);
|
|
||||||
clientType = "front";
|
|
||||||
} else {
|
|
||||||
// 非需鉴权路径,可以放行
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(token)) {
|
|
||||||
log.warn("请求[{}]缺少{}令牌", requestURI, clientType);
|
|
||||||
writeJsonResponse(response, Result.of(ResultCode.NOT_LOGIN));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<Claims> claims = jwtUtil.parseToken(token);
|
|
||||||
if (!claims.isPresent() || jwtUtil.isTokenExpired(token)) {
|
|
||||||
log.warn("请求[{}]令牌无效或已过期", requestURI);
|
|
||||||
writeJsonResponse(response, Result.of(ResultCode.UNAUTHORIZED, "token无效或过期"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过校验,将Claims和客户端类型存入请求属性,方便后续使用
|
|
||||||
request.setAttribute("claims", claims.get());
|
|
||||||
request.setAttribute("clientType", clientType);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeJsonResponse(HttpServletResponse response, Object resultObj) throws Exception {
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
|
||||||
String json = com.fasterxml.jackson.databind.json.JsonMapper.builder().build().writeValueAsString(resultObj);
|
|
||||||
response.getOutputStream().write(json.getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,6 @@ import io.jsonwebtoken.JwtException;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
|
@ -15,28 +14,22 @@ import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
@Component
|
||||||
public class JwtUtil {
|
public class JwtUtil {
|
||||||
|
|
||||||
@Value("${app.jwt.secret:mwsK9Ol9Ni2IyTvcdgFDVBxatw8QWx2o}")
|
@Value("${app.jwt.secret:mwsK9Ol9Ni2IyTvcdgFDVBxatw8QWx2o}")
|
||||||
private String secret;
|
private String secret;
|
||||||
|
|
||||||
@Value("${app.jwt.expiration:86400000}") // 默认过期时间:1天(毫秒)
|
@Value("${app.jwt.expiration:86400000}") // 1天过期
|
||||||
private long expire;
|
private long expire;
|
||||||
|
|
||||||
private Key key;
|
private Key key;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
this.key = Keys.hmacShaKeyFor(secret.getBytes());
|
key = Keys.hmacShaKeyFor(secret.getBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成 JWT token
|
|
||||||
* @param claims 自定义载荷,比如 userId、roles 等
|
|
||||||
* @return token 字符串
|
|
||||||
*/
|
|
||||||
public String generateToken(Map<String, Object> claims) {
|
public String generateToken(Map<String, Object> claims) {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
Date expiryDate = new Date(now.getTime() + expire);
|
Date expiryDate = new Date(now.getTime() + expire);
|
||||||
|
|
@ -49,11 +42,6 @@ public class JwtUtil {
|
||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 JWT,返回 Optional<Claims>
|
|
||||||
* @param token JWT字符串
|
|
||||||
* @return Claims 或空 Optional
|
|
||||||
*/
|
|
||||||
public Optional<Claims> parseToken(String token) {
|
public Optional<Claims> parseToken(String token) {
|
||||||
try {
|
try {
|
||||||
Claims claims = Jwts.parserBuilder()
|
Claims claims = Jwts.parserBuilder()
|
||||||
|
|
@ -62,39 +50,14 @@ public class JwtUtil {
|
||||||
.parseClaimsJws(token)
|
.parseClaimsJws(token)
|
||||||
.getBody();
|
.getBody();
|
||||||
return Optional.of(claims);
|
return Optional.of(claims);
|
||||||
} catch (JwtException | IllegalArgumentException e) {
|
} catch (JwtException e) {
|
||||||
log.warn("解析JWT失败: {}", e.getMessage());
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断 Token 是否过期
|
|
||||||
* @param token JWT字符串
|
|
||||||
* @return true 如果过期或者解析失败
|
|
||||||
*/
|
|
||||||
public boolean isTokenExpired(String token) {
|
public boolean isTokenExpired(String token) {
|
||||||
return parseToken(token)
|
return parseToken(token)
|
||||||
.map(claims -> claims.getExpiration().before(new Date()))
|
.map(c -> c.getExpiration().before(new Date()))
|
||||||
.orElse(true);
|
.orElse(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断 Token 是否有效(非空且未过期)
|
|
||||||
* @param token JWT字符串
|
|
||||||
* @return true 有效
|
|
||||||
*/
|
|
||||||
public boolean isTokenValid(String token) {
|
|
||||||
return parseToken(token).isPresent() && !isTokenExpired(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 Token 中获取 userId (假设保存在 claims 的 "userId" 字段)
|
|
||||||
* @param token JWT字符串
|
|
||||||
* @return Optional<String> 用户ID
|
|
||||||
*/
|
|
||||||
public Optional<String> getUserIdFromToken(String token) {
|
|
||||||
return parseToken(token)
|
|
||||||
.map(claims -> claims.get("userId", String.class));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue