This commit is contained in:
parent
31c52d4115
commit
73717d6e6f
|
@ -1,5 +1,6 @@
|
|||
package com.guwan.backend.config;
|
||||
|
||||
import com.guwan.backend.constant.SecurityConstants;
|
||||
import com.guwan.backend.security.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -15,6 +16,10 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* Spring Security 安全配置类
|
||||
* 配置安全相关的全局策略
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
|
@ -22,34 +27,43 @@ public class SecurityConfig {
|
|||
|
||||
private final JwtAuthenticationFilter jwtAuthFilter;
|
||||
|
||||
/**
|
||||
* 配置安全过滤链
|
||||
* 定义了系统的安全策略,包括:
|
||||
* 1. CSRF 和 CORS 配置
|
||||
* 2. Session 管理策略
|
||||
* 3. 请求授权规则
|
||||
* 4. JWT 过滤器配置
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.csrf(AbstractHttpConfigurer::disable) // 禁用CSRF保护
|
||||
.cors(AbstractHttpConfigurer::disable) // 禁用CORS保护
|
||||
.sessionManagement(session ->
|
||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 使用无状态会话
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/demo/**",
|
||||
"/api/user/register",
|
||||
"/api/user/login",
|
||||
"/api/user/register/email",
|
||||
"/api/user/register/phone",
|
||||
"/api/user/email/code",
|
||||
"/api/user/phone/code"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.requestMatchers(SecurityConstants.WHITE_LIST.toArray(new String[0])).permitAll() // 配置API白名单
|
||||
.requestMatchers(SecurityConstants.STATIC_RESOURCES.toArray(new String[0])).permitAll() // 配置静态资源白名单
|
||||
.anyRequest().authenticated() // 其他所有请求都需要认证
|
||||
)
|
||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // 添加JWT过滤器
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
* 使用 BCrypt 加密算法
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package com.guwan.backend.constant;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 安全相关常量配置
|
||||
*/
|
||||
public class SecurityConstants {
|
||||
|
||||
/**
|
||||
* API接口白名单
|
||||
* 这些路径可以直接访问,不需要认证
|
||||
*/
|
||||
public static final List<String> WHITE_LIST = List.of(
|
||||
"/demo/**", // 测试接口
|
||||
"/api/user/register", // 用户注册
|
||||
"/api/user/login", // 用户登录
|
||||
"/api/user/register/email", // 邮箱注册
|
||||
"/api/user/register/phone", // 手机号注册
|
||||
"/api/user/email/code", // 获取邮箱验证码
|
||||
"/api/user/phone/code" // 获取手机验证码
|
||||
);
|
||||
|
||||
/**
|
||||
* 静态资源白名单
|
||||
* 这些路径用于访问静态资源,不需要认证
|
||||
*/
|
||||
public static final List<String> STATIC_RESOURCES = List.of(
|
||||
"/static/**", // 静态资源目录
|
||||
"/public/**", // 公共资源目录
|
||||
"/error" // 错误页面
|
||||
);
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.guwan.backend.security;
|
|||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.guwan.backend.common.Result;
|
||||
import com.guwan.backend.constant.SecurityConstants;
|
||||
import com.guwan.backend.util.JwtUtil;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
|
@ -18,9 +19,11 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JWT认证过滤器
|
||||
* 负责处理JWT token的验证和用户认证
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
|
@ -30,29 +33,26 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||
private final UserDetailsServiceImpl userDetailsService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// 不需要验证的路径
|
||||
private static final List<String> PERMIT_PATHS = Arrays.asList(
|
||||
"/demo/**",
|
||||
"/api/user/register",
|
||||
"/api/user/login",
|
||||
"/api/user/register/email",
|
||||
"/api/user/register/phone",
|
||||
"/api/user/email/code",
|
||||
"/api/user/phone/code"
|
||||
);
|
||||
|
||||
/**
|
||||
* 过滤器主要逻辑
|
||||
* 1. 检查是否是白名单路径
|
||||
* 2. 验证JWT token
|
||||
* 3. 设置认证信息
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// 检查是否是允许的路径
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain chain) throws ServletException, IOException {
|
||||
String path = request.getServletPath();
|
||||
|
||||
// 如果是白名单路径,直接放行
|
||||
if (isPermitPath(path)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 验证token
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
handleAuthenticationError(response, "未登录");
|
||||
|
@ -65,16 +65,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||
return;
|
||||
}
|
||||
|
||||
Long userId = jwtUtil.getUserIdFromToken(jwt);
|
||||
if (SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = userDetailsService.loadUserById(userId);
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
// 设置认证信息
|
||||
setAuthentication(request, jwt);
|
||||
|
||||
chain.doFilter(request, response);
|
||||
} catch (Exception e) {
|
||||
|
@ -83,15 +75,50 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查请求路径是否在白名单中
|
||||
*/
|
||||
private boolean isPermitPath(String path) {
|
||||
return PERMIT_PATHS.stream().anyMatch(pattern ->
|
||||
pattern.endsWith("/**")
|
||||
? path.startsWith(pattern.substring(0, pattern.length() - 3))
|
||||
: path.equals(pattern)
|
||||
);
|
||||
return SecurityConstants.WHITE_LIST.stream()
|
||||
.anyMatch(pattern -> matchPath(path, pattern)) ||
|
||||
SecurityConstants.STATIC_RESOURCES.stream()
|
||||
.anyMatch(pattern -> matchPath(path, pattern));
|
||||
}
|
||||
|
||||
private void handleAuthenticationError(HttpServletResponse response, String message) throws IOException {
|
||||
/**
|
||||
* 路径匹配逻辑
|
||||
* 支持通配符 /** 匹配
|
||||
*/
|
||||
private boolean matchPath(String path, String pattern) {
|
||||
return pattern.endsWith("/**")
|
||||
? path.startsWith(pattern.substring(0, pattern.length() - 3))
|
||||
: path.equals(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置认证信息到 SecurityContext
|
||||
*/
|
||||
private void setAuthentication(HttpServletRequest request, String jwt) {
|
||||
Long userId = jwtUtil.getUserIdFromToken(jwt);
|
||||
if (SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = userDetailsService.loadUserById(userId);
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
authentication.setDetails(
|
||||
new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理认证错误
|
||||
* 返回统一的错误响应格式
|
||||
*/
|
||||
private void handleAuthenticationError(HttpServletResponse response, String message)
|
||||
throws IOException {
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
|
@ -99,13 +126,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||
Result<?> result = Result.error(message);
|
||||
response.getWriter().write(objectMapper.writeValueAsString(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getServletPath();
|
||||
// 静态资源不过滤
|
||||
return path.startsWith("/static/") ||
|
||||
path.startsWith("/public/") ||
|
||||
path.startsWith("/error");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue