From ff6c20fe001724fa53df085cb94ab0bbd6bcb35f Mon Sep 17 00:00:00 2001 From: FalingCliff Date: Sat, 24 May 2025 18:26:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor(auth):=20=E9=87=8D=E6=9E=84=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E8=BF=87=E6=BB=A4=E5=99=A8=E5=B9=B6=E6=95=B4=E5=90=88?= =?UTF-8?q?=20Spring=20Security?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -将 JwtInterceptor 重命名为 JwtAuthenticationFilter,并改为继承 OncePerRequestFilter - 新增白名单路径配置,对特定请求进行放行- 优化 token 验证逻辑,改进错误响应处理 -重构 JwtUtil 类,简化方法并提高代码可读性 - 更新 SecurityConfig,整合自定义认证过滤器 --- .../admin_server/config/SecurityConfig.java | 51 +++++++++++ .../config/WebSecurityConfig.java | 52 ------------ .../filter/JwtAuthenticationFilter.java | 85 +++++++++++++++++++ .../admin_server/filter/JwtInterceptor.java | 73 ---------------- .../example/admin_server/utils/JwtUtil.java | 45 +--------- 5 files changed, 140 insertions(+), 166 deletions(-) create mode 100644 src/main/java/com/example/admin_server/config/SecurityConfig.java delete mode 100644 src/main/java/com/example/admin_server/config/WebSecurityConfig.java create mode 100644 src/main/java/com/example/admin_server/filter/JwtAuthenticationFilter.java delete mode 100644 src/main/java/com/example/admin_server/filter/JwtInterceptor.java diff --git a/src/main/java/com/example/admin_server/config/SecurityConfig.java b/src/main/java/com/example/admin_server/config/SecurityConfig.java new file mode 100644 index 0000000..6068a2f --- /dev/null +++ b/src/main/java/com/example/admin_server/config/SecurityConfig.java @@ -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(); + } +} diff --git a/src/main/java/com/example/admin_server/config/WebSecurityConfig.java b/src/main/java/com/example/admin_server/config/WebSecurityConfig.java deleted file mode 100644 index af15728..0000000 --- a/src/main/java/com/example/admin_server/config/WebSecurityConfig.java +++ /dev/null @@ -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(); - } -} diff --git a/src/main/java/com/example/admin_server/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/admin_server/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..95c6a8b --- /dev/null +++ b/src/main/java/com/example/admin_server/filter/JwtAuthenticationFilter.java @@ -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 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 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); + } +} diff --git a/src/main/java/com/example/admin_server/filter/JwtInterceptor.java b/src/main/java/com/example/admin_server/filter/JwtInterceptor.java deleted file mode 100644 index f1ea782..0000000 --- a/src/main/java/com/example/admin_server/filter/JwtInterceptor.java +++ /dev/null @@ -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 = 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)); - } -} diff --git a/src/main/java/com/example/admin_server/utils/JwtUtil.java b/src/main/java/com/example/admin_server/utils/JwtUtil.java index 32428f3..8ef93e6 100644 --- a/src/main/java/com/example/admin_server/utils/JwtUtil.java +++ b/src/main/java/com/example/admin_server/utils/JwtUtil.java @@ -5,7 +5,6 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -15,28 +14,22 @@ import java.util.Date; import java.util.Map; import java.util.Optional; -@Slf4j @Component public class JwtUtil { @Value("${app.jwt.secret:mwsK9Ol9Ni2IyTvcdgFDVBxatw8QWx2o}") private String secret; - @Value("${app.jwt.expiration:86400000}") // 默认过期时间:1天(毫秒) + @Value("${app.jwt.expiration:86400000}") // 1天过期 private long expire; private Key key; @PostConstruct 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 claims) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expire); @@ -49,11 +42,6 @@ public class JwtUtil { .compact(); } - /** - * 解析 JWT,返回 Optional - * @param token JWT字符串 - * @return Claims 或空 Optional - */ public Optional parseToken(String token) { try { Claims claims = Jwts.parserBuilder() @@ -62,39 +50,14 @@ public class JwtUtil { .parseClaimsJws(token) .getBody(); return Optional.of(claims); - } catch (JwtException | IllegalArgumentException e) { - log.warn("解析JWT失败: {}", e.getMessage()); + } catch (JwtException e) { return Optional.empty(); } } - /** - * 判断 Token 是否过期 - * @param token JWT字符串 - * @return true 如果过期或者解析失败 - */ public boolean isTokenExpired(String token) { return parseToken(token) - .map(claims -> claims.getExpiration().before(new Date())) + .map(c -> c.getExpiration().before(new Date())) .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 用户ID - */ - public Optional getUserIdFromToken(String token) { - return parseToken(token) - .map(claims -> claims.get("userId", String.class)); - } }