This commit is contained in:
ovo 2024-12-06 21:23:13 +08:00
parent 31c52d4115
commit 73717d6e6f
3 changed files with 121 additions and 56 deletions

View File

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

View File

@ -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" // 错误页面
);
}

View File

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