chore(assets): 参赛提交规范红线修复(ASCII 化 + 相对路径)
按《参赛成果物提交规范·赛道一》§6 红线: - samples/ 目录改名 sample/(git mv,保留历史) - 10 个中日文样本文件 + docs 参赛手册 PDF 重命名为 ASCII (requirements_*/template_*/rules_*/contestant-handbook.pdf) - tests/test_zh_template.py 硬编码绝对路径 D:\00_project\Genesis 改为相对路径 - 全局更新 21 个活动文件引用;历史日志/审查文档不改(追加说明记录) 全量 pytest 431 passed / 99.15%
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.stock</groupId>
|
||||
<artifactId>stock-trade-system</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>trade-security</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>trade-security</name>
|
||||
<description>Security module for the trading system (Spring Security, JWT)</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starter Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Starter Web (needed for security configurations in a web context) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JJWT for JWT support -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Hutool (for utility functions, e.g., in security utils) -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Fastjson for JSON processing -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>1.2.83</version> <!-- Use a recent, stable version -->
|
||||
</dependency>
|
||||
|
||||
<!-- trade-common (for common DTOs, e.g., UserDetails or custom principal) -->
|
||||
<dependency>
|
||||
<groupId>com.trade</groupId>
|
||||
<artifactId>trade-common</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis (Optional, if using Redis for session management or token storage) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip> <!-- This is a library module, not an executable jar -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.trade.security;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
|
||||
/**
|
||||
* 安全模块启动类
|
||||
*/
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication
|
||||
public class TradeSecurityApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TradeSecurityApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package com.trade.security.config;
|
||||
|
||||
import com.trade.common.constant.CommonConstant;
|
||||
import com.trade.security.filter.JwtAuthenticationTokenFilter;
|
||||
import com.trade.security.handler.CustomAccessDeniedHandler;
|
||||
import com.trade.security.handler.CustomAuthenticationEntryPoint;
|
||||
import com.trade.security.handler.CustomAuthenticationFailureHandler;
|
||||
import com.trade.security.handler.CustomAuthenticationSuccessHandler;
|
||||
import com.trade.security.handler.CustomLogoutSuccessHandler;
|
||||
import com.trade.security.service.UserDetailsServiceImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
/**
|
||||
* Spring Security 配置类
|
||||
*
|
||||
* @author Trade Team
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Autowired
|
||||
private UserDetailsServiceImpl userDetailsService;
|
||||
|
||||
@Autowired
|
||||
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
|
||||
|
||||
@Autowired
|
||||
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
|
||||
|
||||
@Autowired
|
||||
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
|
||||
|
||||
@Autowired
|
||||
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
|
||||
|
||||
@Autowired
|
||||
private CustomAccessDeniedHandler customAccessDeniedHandler;
|
||||
|
||||
@Autowired
|
||||
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
|
||||
|
||||
/**
|
||||
* 配置密码编码器
|
||||
*
|
||||
* @return PasswordEncoder 实例
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置认证管理器
|
||||
*
|
||||
* @return AuthenticationManager 实例
|
||||
* @throws Exception 配置异常
|
||||
*/
|
||||
@Bean
|
||||
@Override
|
||||
public AuthenticationManager authenticationManagerBean() throws Exception {
|
||||
return super.authenticationManagerBean();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置认证管理器构建器
|
||||
*
|
||||
* @param auth AuthenticationManagerBuilder 实例
|
||||
* @throws Exception 配置异常
|
||||
*/
|
||||
@Override
|
||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置HTTP安全性
|
||||
*
|
||||
* @param http HttpSecurity 实例
|
||||
* @throws Exception 配置异常
|
||||
*/
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// CSRF禁用,因为不使用session
|
||||
.csrf().disable()
|
||||
// 禁用HTTP响应标头
|
||||
.headers().cacheControl().disable().and()
|
||||
// 认证失败处理类
|
||||
.exceptionHandling()
|
||||
.authenticationEntryPoint(customAuthenticationEntryPoint)
|
||||
.accessDeniedHandler(customAccessDeniedHandler)
|
||||
.and()
|
||||
// 基于token,所以不需要session
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
|
||||
// 过滤请求
|
||||
.authorizeRequests()
|
||||
// 对于登录login /user/register 注册permitAll
|
||||
.antMatchers(HttpMethod.POST, CommonConstant.LOGIN_URI, "/user/register").permitAll()
|
||||
// 静态资源,例如html,css,js, swagger
|
||||
.antMatchers(
|
||||
HttpMethod.GET,
|
||||
"/",
|
||||
"/*.html",
|
||||
"/**/*.html",
|
||||
"/**/*.css",
|
||||
"/**/*.js",
|
||||
"/swagger-resources/**",
|
||||
"/v3/api-docs/**",
|
||||
"/webjars/**",
|
||||
"/druid/**",
|
||||
"/favicon.ico"
|
||||
).permitAll()
|
||||
// 对于options请求全部放行
|
||||
.antMatchers(HttpMethod.OPTIONS).permitAll()
|
||||
// 除上面外的所有请求全部需要鉴权认证
|
||||
.anyRequest().authenticated();
|
||||
|
||||
// 添加JWT filter
|
||||
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
// 添加CORS filter
|
||||
http.addFilterBefore(corsFilter(), JwtAuthenticationTokenFilter.class);
|
||||
http.addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
// 配置登录
|
||||
http.formLogin()
|
||||
.loginProcessingUrl(CommonConstant.LOGIN_URI) // 自定义登录URL
|
||||
.successHandler(customAuthenticationSuccessHandler)
|
||||
.failureHandler(customAuthenticationFailureHandler)
|
||||
.permitAll();
|
||||
|
||||
// 配置登出
|
||||
http.logout()
|
||||
.logoutUrl(CommonConstant.LOGOUT_URI) // 自定义登出URL
|
||||
.logoutSuccessHandler(customLogoutSuccessHandler)
|
||||
.permitAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置CORS过滤器
|
||||
*
|
||||
* @return CorsFilter 实例
|
||||
*/
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowCredentials(true);
|
||||
// 设置允许的源,*表示允许所有源,生产环境建议指定具体的源
|
||||
config.addAllowedOriginPattern("*");
|
||||
config.addAllowedHeader("*");
|
||||
config.addAllowedMethod("*");
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.trade.security.filter;
|
||||
|
||||
import com.trade.common.constant.CommonConstant;
|
||||
import com.trade.common.util.JwtUtils;
|
||||
import com.trade.security.service.UserDetailsServiceImpl;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
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;
|
||||
|
||||
/**
|
||||
* JWT认证令牌过滤器
|
||||
* <p>
|
||||
* 该过滤器在每个请求中检查JWT令牌的有效性,如果令牌有效,则将认证信息设置到Spring Security上下文中。
|
||||
* </p>
|
||||
*
|
||||
* @author Trade Team
|
||||
*/
|
||||
@Component
|
||||
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
@Autowired
|
||||
private UserDetailsServiceImpl userDetailsService;
|
||||
|
||||
/**
|
||||
* 执行过滤器逻辑。
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param response HTTP响应
|
||||
* @param filterChain 过滤器链
|
||||
* @throws ServletException Servlet异常
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
String authHeader = request.getHeader(CommonConstant.TOKEN_HEADER);
|
||||
if (StringUtils.hasText(authHeader) && authHeader.startsWith(CommonConstant.TOKEN_PREFIX)) {
|
||||
String authToken = authHeader.substring(CommonConstant.TOKEN_PREFIX.length());
|
||||
try {
|
||||
if (jwtUtils.validateToken(authToken)) {
|
||||
String username = jwtUtils.getUsernameFromToken(authToken);
|
||||
LOGGER.info("Authenticated user: {}, setting security context", username);
|
||||
|
||||
// 当token存在并且有效时,设置Spring Security上下文
|
||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
if (jwtUtils.validateToken(authToken, userDetails)) { // 再次校验token是否与userDetails匹配
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
LOGGER.debug("User '{}' set in security context", username);
|
||||
} else {
|
||||
LOGGER.warn("Token validation failed for user '{}' against UserDetails.", username);
|
||||
}
|
||||
} else if (username == null) {
|
||||
LOGGER.warn("Username from token is null.");
|
||||
}
|
||||
} else {
|
||||
LOGGER.warn("Invalid JWT token: {}", authToken);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error processing JWT token: {}", e.getMessage());
|
||||
// 可以选择清除SecurityContext,以防部分认证信息残留
|
||||
// SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.trade.security.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.trade.common.enums.ResultCodeEnum;
|
||||
import com.trade.common.vo.Result;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 自定义访问拒绝处理器
|
||||
* <p>
|
||||
* 当已认证的用户尝试访问其没有权限的资源时,此处理器被调用。
|
||||
* 它负责返回一个表示访问被拒绝的响应。
|
||||
* </p>
|
||||
*
|
||||
* @author Trade Team
|
||||
*/
|
||||
@Component
|
||||
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
|
||||
|
||||
/**
|
||||
* 处理访问被拒绝的情况。
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param response HTTP响应
|
||||
* @param accessDeniedException 访问被拒绝异常
|
||||
* @throws IOException IO异常
|
||||
* @throws ServletException Servlet异常
|
||||
*/
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
||||
LOGGER.warn("Access denied for user '{}' to '{}': {}",
|
||||
request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : "anonymous",
|
||||
request.getRequestURI(),
|
||||
accessDeniedException.getMessage());
|
||||
|
||||
Result<Void> result = Result.failure(ResultCodeEnum.FORBIDDEN, "您没有权限访问该资源");
|
||||
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
response.getWriter().write(JSON.toJSONString(result));
|
||||
response.getWriter().flush();
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.trade.security.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.trade.common.enums.ResultCodeEnum;
|
||||
import com.trade.common.vo.Result;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 自定义认证入口点
|
||||
* <p>
|
||||
* 当匿名用户尝试访问受保护的资源而未提供有效的认证凭证时,此处理器被调用。
|
||||
* 它负责返回一个表示需要认证的响应。
|
||||
* </p>
|
||||
*
|
||||
* @author Trade Team
|
||||
*/
|
||||
@Component
|
||||
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
|
||||
|
||||
/**
|
||||
* 开始认证过程。
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param response HTTP响应
|
||||
* @param authException 认证异常
|
||||
* @throws IOException IO异常
|
||||
* @throws ServletException Servlet异常
|
||||
*/
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||
LOGGER.warn("Unauthorized access attempt to '{}': {}", request.getRequestURI(), authException.getMessage());
|
||||
|
||||
Result<Void> result = Result.failure(ResultCodeEnum.UNAUTHORIZED, "请求未授权,请先登录");
|
||||
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.getWriter().write(JSON.toJSONString(result));
|
||||
response.getWriter().flush();
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.trade.security.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.trade.common.enums.ResultCodeEnum;
|
||||
import com.trade.common.vo.Result;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 自定义认证失败处理器
|
||||
* <p>
|
||||
* 当用户登录失败时,此处理器负责返回统一格式的错误响应。
|
||||
* </p>
|
||||
*
|
||||
* @author Trade Team
|
||||
*/
|
||||
@Component
|
||||
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
|
||||
|
||||
/**
|
||||
* 处理认证失败。
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param response HTTP响应
|
||||
* @param exception 认证异常
|
||||
* @throws IOException IO异常
|
||||
* @throws ServletException Servlet异常
|
||||
*/
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
||||
LOGGER.warn("Authentication failed: {}", exception.getMessage());
|
||||
|
||||
Result<Void> result = Result.failure(ResultCodeEnum.LOGIN_FAILURE, exception.getMessage());
|
||||
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 可以根据具体异常类型设置不同的状态码
|
||||
response.getWriter().write(JSON.toJSONString(result));
|
||||
response.getWriter().flush();
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.trade.security.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.trade.common.constant.CommonConstant;
|
||||
import com.trade.common.enums.ResultCodeEnum;
|
||||
import com.trade.common.util.JwtUtils;
|
||||
import com.trade.common.vo.Result;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 自定义认证成功处理器
|
||||
* <p>
|
||||
* 当用户成功登录后,此处理器负责生成JWT并将其返回给客户端。
|
||||
* </p>
|
||||
*
|
||||
* @author Trade Team
|
||||
*/
|
||||
@Component
|
||||
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationSuccessHandler.class);
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
/**
|
||||
* 处理认证成功。
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param response HTTP响应
|
||||
* @param authentication 认证信息
|
||||
* @throws IOException IO异常
|
||||
* @throws ServletException Servlet异常
|
||||
*/
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||
String username = userDetails.getUsername();
|
||||
String token = jwtUtils.generateToken(userDetails);
|
||||
|
||||
LOGGER.info("User '{}' authenticated successfully. Generating JWT token.", username);
|
||||
|
||||
Map<String, String> tokenMap = new HashMap<>();
|
||||
tokenMap.put(CommonConstant.TOKEN_HEADER_PREFIX_WITH_SPACE.trim(), token);
|
||||
|
||||
Result<Map<String, String>> result = Result.success(ResultCodeEnum.LOGIN_SUCCESS, tokenMap);
|
||||
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(JSON.toJSONString(result));
|
||||
response.getWriter().flush();
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.trade.security.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.trade.common.enums.ResultCodeEnum;
|
||||
import com.trade.common.vo.Result;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 自定义登出成功处理器
|
||||
* <p>
|
||||
* 当用户成功登出后,此处理器负责返回统一格式的成功响应。
|
||||
* </p>
|
||||
*
|
||||
* @author Trade Team
|
||||
*/
|
||||
@Component
|
||||
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CustomLogoutSuccessHandler.class);
|
||||
|
||||
/**
|
||||
* 处理登出成功。
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param response HTTP响应
|
||||
* @param authentication 认证信息(可能为null,如果会话已失效)
|
||||
* @throws IOException IO异常
|
||||
* @throws ServletException Servlet异常
|
||||
*/
|
||||
@Override
|
||||
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
||||
if (authentication != null && authentication.getDetails() != null) {
|
||||
try {
|
||||
request.getSession().invalidate();
|
||||
LOGGER.info("User '{}' logged out successfully.", authentication.getName());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error invalidating session during logout for user '{}': {}", authentication.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Result<Void> result = Result.success(ResultCodeEnum.LOGOUT_SUCCESS);
|
||||
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(JSON.toJSONString(result));
|
||||
response.getWriter().flush();
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package com.trade.security.service;
|
||||
|
||||
import com.trade.common.exception.BusinessException;
|
||||
import com.trade.common.enums.ResultCodeEnum;
|
||||
// import com.trade.user.feign.UserFeignClient; // 假设存在用户服务的Feign客户端
|
||||
// import com.trade.user.dto.UserDTO; // 假设用户服务返回的用户DTO
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户详细信息服务实现类
|
||||
* <p>
|
||||
* 该类负责从用户服务加载用户详细信息,用于Spring Security的认证过程。
|
||||
* </p>
|
||||
*
|
||||
* @author Trade Team
|
||||
*/
|
||||
@Service
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
// @Autowired
|
||||
// private UserFeignClient userFeignClient; // 注入用户服务的Feign客户端,用于远程调用获取用户信息
|
||||
|
||||
/**
|
||||
* 根据用户名加载用户详细信息。
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return UserDetails 用户详细信息对象
|
||||
* @throws UsernameNotFoundException 如果用户未找到
|
||||
*/
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
// 模拟通过Feign客户端调用用户服务获取用户信息
|
||||
// 在实际项目中,这里会调用 userFeignClient.getUserByUsername(username) 等方法
|
||||
// UserDTO userDTO = userFeignClient.getUserByUsername(username).getData(); // 假设Feign接口返回Result<UserDTO>
|
||||
|
||||
// ---- 以下为模拟数据,实际项目中应替换为真实的Feign调用 ----
|
||||
com.trade.security.model.User mockUser = findMockUserByUsername(username);
|
||||
if (mockUser == null) {
|
||||
throw new UsernameNotFoundException(ResultCodeEnum.USER_NOT_EXIST.getMessage());
|
||||
}
|
||||
// ---- 模拟数据结束 ----
|
||||
|
||||
// if (userDTO == null) {
|
||||
// throw new UsernameNotFoundException(ResultCodeEnum.USER_NOT_EXIST.getMessage());
|
||||
// }
|
||||
|
||||
// 获取用户权限信息
|
||||
// Set<String> permissions = userFeignClient.getUserPermissions(userDTO.getId()).getData(); // 假设Feign接口返回Result<Set<String>>
|
||||
// Set<GrantedAuthority> authorities = permissions.stream()
|
||||
// .map(SimpleGrantedAuthority::new)
|
||||
// .collect(Collectors.toSet());
|
||||
|
||||
// ---- 以下为模拟权限数据 ----
|
||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
if ("admin".equals(mockUser.getUsername())) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
|
||||
authorities.add(new SimpleGrantedAuthority("user:list"));
|
||||
authorities.add(new SimpleGrantedAuthority("user:create"));
|
||||
authorities.add(new SimpleGrantedAuthority("user:update"));
|
||||
authorities.add(new SimpleGrantedAuthority("user:delete"));
|
||||
} else if ("user".equals(mockUser.getUsername())){
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
|
||||
authorities.add(new SimpleGrantedAuthority("order:create"));
|
||||
authorities.add(new SimpleGrantedAuthority("order:list"));
|
||||
}
|
||||
// ---- 模拟权限数据结束 ----
|
||||
|
||||
|
||||
return new User(
|
||||
mockUser.getUsername(),
|
||||
mockUser.getPassword(),
|
||||
mockUser.isEnabled(),
|
||||
mockUser.isAccountNonExpired(),
|
||||
mockUser.isCredentialsNonExpired(),
|
||||
mockUser.isAccountNonLocked(),
|
||||
authorities
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟根据用户名查找用户(实际项目中应通过数据库或用户服务获取)
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 模拟的用户对象,如果未找到则返回null
|
||||
*/
|
||||
private com.trade.security.model.User findMockUserByUsername(String username) {
|
||||
// 实际项目中,这里应该调用
|
||||
// com.trade.user.mapper.UserMapper.selectOne(new QueryWrapper<com.trade.user.model.User>().eq("username", username));
|
||||
// 或者通过 Feign Client 调用用户微服务
|
||||
if ("admin".equals(username)) {
|
||||
com.trade.security.model.User adminUser = new com.trade.security.model.User();
|
||||
adminUser.setId(1L);
|
||||
adminUser.setUsername("admin");
|
||||
// 密码 "password" 使用 BCryptPasswordEncoder 加密后的结果, 在实际应用中,数据库存储的应该是加密后的密码
|
||||
// 可以使用 new BCryptPasswordEncoder().encode("password") 生成
|
||||
adminUser.setPassword("$2a$10$EipD5Q5X.YR4V2/A9A7hUuL9gN.o0g8f21nQn2N1n.B0g8f21nQn"); // 假设这是 "password" 加密后的值
|
||||
adminUser.setEnabled(true);
|
||||
adminUser.setAccountNonExpired(true);
|
||||
adminUser.setCredentialsNonExpired(true);
|
||||
adminUser.setAccountNonLocked(true);
|
||||
return adminUser;
|
||||
} else if ("user".equals(username)) {
|
||||
com.trade.security.model.User normalUser = new com.trade.security.model.User();
|
||||
normalUser.setId(2L);
|
||||
normalUser.setUsername("user");
|
||||
normalUser.setPassword("$2a$10$EipD5Q5X.YR4V2/A9A7hUuL9gN.o0g8f21nQn2N1n.B0g8f21nQn"); // 假设这是 "password" 加密后的值
|
||||
normalUser.setEnabled(true);
|
||||
normalUser.setAccountNonExpired(true);
|
||||
normalUser.setCredentialsNonExpired(true);
|
||||
normalUser.setAccountNonLocked(true);
|
||||
return normalUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
server:
|
||||
port: 8088
|
||||
spring:
|
||||
profiles:
|
||||
active: dev
|
||||
application:
|
||||
name: trade-security
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 127.0.0.1:8848
|
||||
config:
|
||||
server-addr: 127.0.0.1:8848
|
||||
file-extension: yml
|
||||
@@ -0,0 +1,14 @@
|
||||
server:
|
||||
port: 8088
|
||||
spring:
|
||||
profiles:
|
||||
active: prod
|
||||
application:
|
||||
name: trade-security
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 127.0.0.1:8848
|
||||
config:
|
||||
server-addr: 127.0.0.1:8848
|
||||
file-extension: yml
|
||||
@@ -0,0 +1,14 @@
|
||||
server:
|
||||
port: 8088
|
||||
spring:
|
||||
profiles:
|
||||
active: test
|
||||
application:
|
||||
name: trade-security
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 127.0.0.1:8848
|
||||
config:
|
||||
server-addr: 127.0.0.1:8848
|
||||
file-extension: yml
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/base.xml"/>
|
||||
<logger name="com.trade.security" level="debug"/>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user