dsl 4 months ago
parent 493c0f6c61
commit f9dddfa7e4

@ -2,12 +2,15 @@ package com.aurora.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 定义一个注解AccessLimit用于方法级别的访问频率限制
@Target(ElementType.METHOD) // 该注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时保留,这样可以通过反射读取
@Documented // 该注解会包含在 JavaDoc 中
public @interface AccessLimit {
// 限制的时间窗口,单位为秒,例如 60 表示 1 分钟内
int seconds();
// 在指定的时间窗口内,允许的最大访问次数,例如 10 表示最多 10 次
int maxCount();
}
}

@ -2,10 +2,12 @@ package com.aurora.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 定义一个注解OptLog用于标记需要进行操作日志记录的方法
@Target(ElementType.METHOD) // 该注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时保留以便通过反射读取通常用于AOP切面
@Documented // 该注解会包含在 JavaDoc 中
public @interface OptLog {
// 操作类型,比如:"新增文章"、"删除评论"、"用户登录",默认值为空字符串
String optType() default "";
}
}

@ -22,31 +22,69 @@ import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;
/**
*
*/
@Aspect
@Component
public class ExceptionLogAspect {
/**
* Spring
*/
@Autowired
private ApplicationContext applicationContext;
/**
* com.aurora.controller
*
*/
@Pointcut("execution(* com.aurora.controller..*.*(..))")
public void exceptionLogPointcut() {
// 切入点方法体为空,仅用于定义切入点
}
/**
*
*
*
* @param joinPoint
* @param e
*/
@AfterThrowing(value = "exceptionLogPointcut()", throwing = "e")
public void saveExceptionLog(JoinPoint joinPoint, Exception e) {
// 获取当前请求的上下文
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) Objects.requireNonNull(requestAttributes).resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 从请求上下文中获取 HttpServletRequest 对象
HttpServletRequest request = (HttpServletRequest) Objects.requireNonNull(requestAttributes)
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 创建一个 ExceptionLog 实体对象,用于存储异常日志信息
ExceptionLog exceptionLog = new ExceptionLog();
// 获取方法的签名信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取方法上的 @ApiOperation 注解,用于获取操作的描述
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
// 设置操作的 URI即请求的路径
exceptionLog.setOptUri(Objects.requireNonNull(request).getRequestURI());
// 获取当前类的全限定名和方法名,组合成完整的操作方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = method.getName();
methodName = className + "." + methodName;
// 设置操作的完整方法名称
exceptionLog.setOptMethod(methodName);
// 设置请求的方法类型,如 GET、POST 等
exceptionLog.setRequestMethod(Objects.requireNonNull(request).getMethod());
// 获取方法的参数,如果第一个参数是 MultipartFile 类型,则记录为 "file"
// 否则,将所有参数序列化为 JSON 字符串
if (joinPoint.getArgs().length > 0) {
if (joinPoint.getArgs()[0] instanceof MultipartFile) {
exceptionLog.setRequestParam("file");
@ -54,16 +92,26 @@ public class ExceptionLogAspect {
exceptionLog.setRequestParam(JSON.toJSONString(joinPoint.getArgs()));
}
}
// 如果方法上有 @ApiOperation 注解,则设置操作的描述
if (Objects.nonNull(apiOperation)) {
exceptionLog.setOptDesc(apiOperation.value());
} else {
// 否则,设置为空字符串
exceptionLog.setOptDesc("");
}
// 获取异常的堆栈信息,用于调试和排查
exceptionLog.setExceptionInfo(ExceptionUtil.getTrace(e));
// 获取客户端的 IP 地址
String ipAddress = IpUtil.getIpAddress(request);
exceptionLog.setIpAddress(ipAddress);
// 获取 IP 地址的来源信息,如国家、城市等
exceptionLog.setIpSource(IpUtil.getIpSource(ipAddress));
// 通过应用上下文发布一个异常日志事件,以便其他监听器处理
applicationContext.publishEvent(new ExceptionLogEvent(exceptionLog));
}
}
}

@ -24,36 +24,85 @@ import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* @OptLog
*/
@Aspect
@Component
public class OperationLogAspect {
/**
* Spring
*/
@Autowired
private ApplicationContext applicationContext;
/**
* 使 @OptLog
* @OptLog
*/
@Pointcut("@annotation(com.aurora.annotation.OptLog)")
public void operationLogPointCut() {
// 切入点方法体为空,仅用于定义切入点
}
/**
*
* @OptLog
*
* @param joinPoint
* @param keys
*/
@AfterReturning(value = "operationLogPointCut()", returning = "keys")
@SuppressWarnings("unchecked")
public void saveOperationLog(JoinPoint joinPoint, Object keys) {
// 获取当前请求的上下文
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) Objects.requireNonNull(requestAttributes).resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 从请求上下文中获取 HttpServletRequest 对象
HttpServletRequest request = (HttpServletRequest) Objects.requireNonNull(requestAttributes)
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 创建一个 OperationLog 实体对象,用于存储操作日志信息
OperationLog operationLog = new OperationLog();
// 获取方法的签名信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取类上的 @Api 注解,用于获取操作所属的模块
Api api = (Api) signature.getDeclaringType().getAnnotation(Api.class);
// 获取方法上的 @ApiOperation 注解,用于获取操作的描述
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
// 获取方法上的 @OptLog 注解,用于获取操作类型
OptLog optLog = method.getAnnotation(OptLog.class);
// 设置操作所属的模块,通常来自 @Api 注解的 tags
operationLog.setOptModule(api.tags()[0]);
// 设置操作类型,来自 @OptLog 注解的 optType
operationLog.setOptType(optLog.optType());
// 设置操作描述,来自 @ApiOperation 注解的 value
operationLog.setOptDesc(apiOperation.value());
// 获取目标类的全限定名和方法名,组合成完整的操作方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = method.getName();
methodName = className + "." + methodName;
operationLog.setRequestMethod(Objects.requireNonNull(request).getMethod());
// 设置操作的完整方法名称
operationLog.setOptMethod(methodName);
// 获取请求的方法类型,如 GET、POST 等
operationLog.setRequestMethod(Objects.requireNonNull(request).getMethod());
// 获取请求的 URI即请求的路径
operationLog.setOptUri(request.getRequestURI());
// 获取方法的参数,如果第一个参数是 MultipartFile 类型,则记录为 "file"
// 否则,将所有参数序列化为 JSON 字符串
if (joinPoint.getArgs().length > 0) {
if (joinPoint.getArgs()[0] instanceof MultipartFile) {
operationLog.setRequestParam("file");
@ -61,14 +110,22 @@ public class OperationLogAspect {
operationLog.setRequestParam(JSON.toJSONString(joinPoint.getArgs()));
}
}
// 设置方法的返回值,序列化为 JSON 字符串
operationLog.setResponseData(JSON.toJSONString(keys));
// 获取当前登录用户的详细信息包括用户ID和昵称
operationLog.setUserId(UserUtil.getUserDetailsDTO().getId());
operationLog.setNickname(UserUtil.getUserDetailsDTO().getNickname());
// 获取客户端的 IP 地址
String ipAddress = IpUtil.getIpAddress(request);
operationLog.setIpAddress(ipAddress);
// 获取 IP 地址的来源信息,如国家、城市等
operationLog.setIpSource(IpUtil.getIpSource(ipAddress));
operationLog.setOptUri(request.getRequestURI());
// 通过应用上下文发布一个操作日志事件,以便其他监听器处理
applicationContext.publishEvent(new OperationLogEvent(operationLog));
}
}
}

@ -1,6 +1,5 @@
package com.aurora.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
@ -14,30 +13,45 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
import java.util.Collections;
/**
* Swagger API 线
* Swagger 2 Knife4j
*/
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {
/**
* Docket Bean Swagger
* API
*
* @return Docket
*/
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.protocols(Collections.singleton("https"))
.host("https://www.linhaojun.top")
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.aurora.controller"))
.paths(PathSelectors.any())
.build();
return new Docket(DocumentationType.SWAGGER_2) // 指定使用 Swagger 2 规范
.protocols(Collections.singleton("https")) // 指定支持的协议,这里只支持 HTTPS
.host("https://www.linhaojun.top") // 指定 API 的主机地址
.apiInfo(apiInfo()) // 设置 API 文档的基本信息,如标题、描述、联系人等
.select() // 开始选择哪些接口会被扫描并生成文档
.apis(RequestHandlerSelectors.basePackage("com.aurora.controller")) // 指定扫描的包路径,这里只扫描 com.aurora.controller 包下的 Controller
.paths(PathSelectors.any()) // 指定扫描所有路径,也可以根据需求使用 PathSelectors.ant(...) 等进行筛选
.build(); // 构建 Docket 实例
}
/**
* ApiInfo API
* URL
*
* @return ApiInfo
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("aurora文档")
.description("aurora")
.contact(new Contact("花未眠", "", "1909925152@qq.com"))
.termsOfServiceUrl("https://www.linhaojun.top/api")
.version("1.0")
.build();
.title("aurora文档") // API 文档的标题,通常显示在文档首页
.description("aurora") // API 文档的描述,简要介绍该 API 的用途或功能
.contact(new Contact("花未眠", "", "1909925152@qq.com")) // 联系人信息,包括姓名、网址(可为空)、邮箱
.termsOfServiceUrl("https://www.linhaojun.top/api") // 服务条款的 URL通常为 API 的使用条款或相关文档链接
.version("1.0") // API 的版本号
.build(); // 构建 ApiInfo 实例
}
}
}

@ -7,14 +7,31 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableTransactionManagement
@Configuration
/**
* MyBatis-Plus MyBatis-Plus
*
*/
@EnableTransactionManagement // 启用 Spring 的事务管理功能,允许使用 @Transactional 注解管理事务
@Configuration // 声明这是一个 Spring 配置类Spring 会加载该类中定义的 Bean
public class MybatisPlusConfig {
/**
* MybatisPlusInterceptor Bean
* MyBatis-Plus
*
*
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建 MybatisPlusInterceptor 实例,它是 MyBatis-Plus 的核心拦截器容器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件,用于支持 MyBatis-Plus 的分页查询功能
// DbType.MYSQL 表示当前使用的数据库类型是 MySQL根据实际情况可替换为其他数据库类型
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 返回配置好的拦截器实例
return interceptor;
}

@ -1,6 +1,5 @@
package com.aurora.config;
import com.aurora.interceptor.PaginationInterceptor;
import com.aurora.interceptor.AccessLimitInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
@ -9,28 +8,54 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC Spring MVC
* CORSInterceptors
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer { //这是注释
//这是注释
public class WebMvcConfig implements WebMvcConfigurer { // 这是注释
// 自动注入自定义的分页拦截器,可能用于处理分页逻辑(如解析分页参数)
@Autowired
private PaginationInterceptor paginationInterceptor;
// 自动注入自定义的访问限流拦截器,可能用于接口防刷、频率控制等
@Autowired
private AccessLimitInterceptor accessLimitInterceptor;
/**
* CORS, Cross-Origin Resource Sharing
* 访 API
*
* @param registry CORS
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 对所有路径(/**)启用跨域配置
registry.addMapping("/**")
// 允许跨域请求携带凭证(如 cookies、HTTP认证等注意与 allowedOrigins("*") 一起使用时有限制
.allowCredentials(true)
// 允许所有请求头(如 Content-Type, Authorization 等)
.allowedHeaders("*")
// 允许所有来源即任何域名、IP、端口都可访问生产环境建议指定具体域名
.allowedOrigins("*")
// 允许所有 HTTP 请求方法(如 GET, POST, PUT, DELETE, OPTIONS 等)
.allowedMethods("*");
}
/**
* Spring MVC
*
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册分页拦截器,用于处理分页相关逻辑(如自动解析页码、页大小等参数)
registry.addInterceptor(paginationInterceptor);
// 注册访问限流拦截器,用于限制接口的访问频率,防止恶意请求或接口滥用
registry.addInterceptor(accessLimitInterceptor);
}
}
}

@ -23,70 +23,127 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security CSRF
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 自动注入认证失败处理器,用于处理认证失败的请求
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationFailureHandler authenticationFailureHandler;
// 自动注入认证成功处理器,用于处理认证成功的请求
@Autowired
private AccessDeniedHandler accessDeniedHandler;
private AuthenticationSuccessHandler authenticationSuccessHandler;
// 自动注入访问被拒绝处理器,用于处理权限不足的请求
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
private AccessDeniedHandler accessDeniedHandler;
// 自动注入认证入口点,用于处理未认证的请求
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
private AuthenticationEntryPoint authenticationEntryPoint;
// 自动注入 JWT 认证过滤器,用于处理 JWT 令牌的认证
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* URL
* FilterInvocationSecurityMetadataSource
*
* @return Bean
*/
@Bean
public FilterInvocationSecurityMetadataSource securityMetadataSource() {
return new FilterInvocationSecurityMetadataSourceImpl();
}
/**
* 访访
* AccessDecisionManager
*
* @return 访 Bean
*/
@Bean
public AccessDecisionManager accessDecisionManager() {
return new AccessDecisionManagerImpl();
}
/**
* AuthenticationManager Bean使
* AuthenticationManager
*
* @return AuthenticationManager Bean
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
*
* 使 BCrypt
*
* @return BCryptPasswordEncoder Bean
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* HttpSecurity
*
* @param http HttpSecurity
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置表单登录相关设置
http.formLogin()
// 设置登录处理的 URL通常为用户提交登录表单的路径
.loginProcessingUrl("/users/login")
// 设置认证成功后的处理器
.successHandler(authenticationSuccessHandler)
// 设置认证失败后的处理器
.failureHandler(authenticationFailureHandler);
// 配置请求授权规则
http.authorizeRequests()
// 使用 ObjectPostProcessor 来注入自定义的 FilterInvocationSecurityMetadataSource 和 AccessDecisionManager
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
// 设置安全元数据源,定义哪些 URL 需要哪些权限
fsi.setSecurityMetadataSource(securityMetadataSource());
// 设置访问决策管理器,决定用户是否有权限访问某个资源
fsi.setAccessDecisionManager(accessDecisionManager());
return fsi;
}
})
// 对所有请求进行权限配置,这里设置为允许所有请求通过
.anyRequest().permitAll()
.and()
.csrf().disable().exceptionHandling()
// 禁用 CSRF 保护,适用于 RESTful API不需要浏览器端的会话
.csrf().disable()
// 配置异常处理,包括认证失败和权限不足的处理
.exceptionHandling()
// 设置认证入口点,处理未认证的请求
.authenticationEntryPoint(authenticationEntryPoint)
// 设置访问被拒绝处理器,处理权限不足的请求
.accessDeniedHandler(accessDeniedHandler)
.and()
// 配置会话管理策略,这里设置为无状态,适用于 RESTful API
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 在 UsernamePasswordAuthenticationFilter 之前添加 JWT 认证过滤器
// 这样可以优先处理 JWT 令牌的认证,而不是依赖传统的表单登录
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
}

@ -1,22 +1,45 @@
package com.aurora.exception;
import com.aurora.enums.StatusCodeEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
*
* RuntimeExceptionUnchecked Exception
*/
@Getter
@AllArgsConstructor
public class BizException extends RuntimeException {
/**
* StatusCodeEnum.FAIL
*
*/
private Integer code = StatusCodeEnum.FAIL.getCode();
/**
*
* final便
*/
private final String message;
/**
*
* 使StatusCodeEnum.FAIL.getCode()
*
* @param message
*/
public BizException(String message) {
this.message = message;
}
/**
* StatusCodeEnum
*
*
* @param statusCodeEnum
*/
public BizException(StatusCodeEnum statusCodeEnum) {
this.code = statusCodeEnum.getCode();
this.message = statusCodeEnum.getDesc();

@ -6,20 +6,47 @@ public class TaskException extends Exception {
private final Code code;
/**
*
*
*
* @param msg
* @param code
*/
public TaskException(String msg, Code code) {
this(msg, code, null);
}
/**
*
*
* @param msg
* @param code
* @param exception null
*/
public TaskException(String msg, Code code, Exception exception) {
super(msg, exception);
this.code = code;
}
/**
*
*
* @return
*/
public Code getCode() {
return code;
}
/**
*
*/
public enum Code {
TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE
TASK_EXISTS, // 任务已存在
NO_TASK_EXISTS, // 任务不存在
TASK_ALREADY_STARTED, // 任务已经启动
UNKNOWN, // 未知错误
CONFIG_ERROR, // 配置错误
TASK_NODE_NOT_AVAILABLE // 任务节点不可用
}
}

@ -12,29 +12,68 @@ import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* 访 Spring Security AccessDecisionManager
* 访 URL
*/
@Component
public class AccessDecisionManagerImpl implements AccessDecisionManager {
/**
* 访访
* 访 AccessDeniedException
*
* @param authentication Authorities
* @param object FilterInvocation URL
* @param configAttributes 访 ConfigAttribute
* @throws AccessDeniedException 访
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
// 从当前用户的认证信息中提取所有的权限GrantedAuthority并将权限字符串收集到一个 List 中
// GrantedAuthority 是 Spring Security 中表示一个权限或角色的接口,通常权限字符串以 "ROLE_" 开头表示角色
List<String> permissionList = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
for (ConfigAttribute item : collection) {
// 遍历访问该资源所需的权限配置集合
for (ConfigAttribute item : configAttributes) {
// 检查用户的权限列表中是否包含当前所需的权限(即 ConfigAttribute 的属性值)
if (permissionList.contains(item.getAttribute())) {
// 如果用户具备所需的权限,直接返回,允许访问该资源
return;
}
}
// 如果遍历完所有所需的权限后,用户不具备任何一个所需的权限,则抛出 AccessDeniedException 异常,表示权限不足,访问被拒绝
throw new AccessDeniedException("权限不足");
}
/**
* AccessDecisionManager ConfigAttribute
* true ConfigAttribute
*
* @param configAttribute ConfigAttribute
* @return true ConfigAttribute
*/
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
/**
* AccessDecisionManager Class
* true
*
* @param aClass FilterInvocation.class
* @return true
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
}

@ -11,11 +11,30 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 访 Spring Security AccessDeniedHandler
* 访 JSON
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
/**
* 访
* 访
* Spring Security JSON
*
* @param request HTTP
* @param response HTTP
* @param accessDeniedException 访
* @throws IOException I/O
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
// 设置响应的内容类型为 JSON通常为 "application/json"
response.setContentType(CommonConstant.APPLICATION_JSON);
// 将一个表示“权限不足”的 ResultVO 对象序列化为 JSON 字符串,并写入 HTTP 响应体
// ResultVO.fail("权限不足") 创建一个表示操作失败的 ResultVO 实例,其中包含错误信息“权限不足”
response.getWriter().write(JSON.toJSONString(ResultVO.fail("权限不足")));
}
}
}

@ -12,11 +12,32 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Spring Security AuthenticationEntryPoint
* 访 JSON
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
/**
* 访
* 访
* Spring Security JSON
*
* @param request HTTP
* @param response HTTP
* @param authException
* @throws IOException I/O
* @throws ServletException Servlet
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 设置响应的内容类型为 JSON通常为 "application/json"
response.setContentType(CommonConstant.APPLICATION_JSON);
// 将一个表示“用户未登录”的 ResultVO 对象序列化为 JSON 字符串,并写入 HTTP 响应体
// ResultVO.fail(40001, "用户未登录") 创建一个表示操作失败的 ResultVO 实例,
// 其中包含错误码 40001 和错误信息“用户未登录”
response.getWriter().write(JSON.toJSONString(ResultVO.fail(40001, "用户未登录")));
}
}
}

@ -23,36 +23,87 @@ import java.io.IOException;
import java.util.Objects;
/**
* Spring Security AuthenticationSuccessHandler
* JSON
*
*/
@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
// 自动注入 UserAuthMapper用于更新用户的最后登录信息
@Autowired
private UserAuthMapper userAuthMapper;
// 自动注入 TokenService用于生成用户的认证令牌Token
@Autowired
private TokenService tokenService;
/**
*
* Spring Security JSON
*
*
* @param request HTTP
* @param response HTTP
* @param authentication
* @throws IOException I/O
* @throws ServletException Servlet
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 使用 BeanCopyUtil 将 UserUtil 中的 UserDetailsDTO 复制为 UserInfoDTO 对象
// UserInfoDTO 通常用于返回给客户端,包含用户的基本信息
UserInfoDTO userLoginDTO = BeanCopyUtil.copyObject(UserUtil.getUserDetailsDTO(), UserInfoDTO.class);
// 检查 authentication 对象是否非空
if (Objects.nonNull(authentication)) {
// 从 authentication 对象中获取 UserDetailsDTO通常包含用户的详细信息
UserDetailsDTO userDetailsDTO = (UserDetailsDTO) authentication.getPrincipal();
// 使用 TokenService 生成用户的认证令牌Token
String token = tokenService.createToken(userDetailsDTO);
// 将生成的 Token 设置到 UserInfoDTO 对象中,以便返回给客户端
userLoginDTO.setToken(token);
}
// 设置响应的内容类型为 JSON通常为 "application/json"
response.setContentType(CommonConstant.APPLICATION_JSON);
// 将一个表示成功的 ResultVO 对象序列化为 JSON 字符串,并写入 HTTP 响应体
// ResultVO.ok(userLoginDTO) 创建一个表示操作成功的 ResultVO 实例,其中包含用户信息
response.getWriter().write(JSON.toJSONString(ResultVO.ok(userLoginDTO)));
// 调用异步方法,更新用户的最后登录信息
updateUserInfo();
}
/**
*
* 使 @Async 线
* 线线
* IP IP
*/
@Async
public void updateUserInfo() {
// 使用 UserUtil 获取当前认证的用户详情
UserAuth userAuth = UserAuth.builder()
// 设置用户的唯一标识 ID
.id(UserUtil.getUserDetailsDTO().getId())
// 设置用户的 IP 地址,通常从请求中获取
.ipAddress(UserUtil.getUserDetailsDTO().getIpAddress())
// 设置用户的 IP 来源,如国家、城市等
.ipSource(UserUtil.getUserDetailsDTO().getIpSource())
// 设置用户的最后登录时间,通常为当前时间
.lastLoginTime(UserUtil.getUserDetailsDTO().getLastLoginTime())
.build();
// 使用 UserAuthMapper 更新用户认证信息表中的记录
// 假设 updateById 方法根据用户的 ID 更新对应的记录
userAuthMapper.updateById(userAuth);
}
}
}

@ -10,25 +10,64 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Objects;
/**
* 使 @RestControllerAdvice @ExceptionHandler
*
* BizExceptionMethodArgumentNotValidException
* Exception
*/
@Log4j2
@RestControllerAdvice
public class ControllerAdviceHandler {
/**
* BizException
* BizException
*
* @param e BizException
* @return ResultVO
*/
@ExceptionHandler(value = BizException.class)
public ResultVO<?> errorHandler(BizException e) {
// 调用 ResultVO.fail 方法,传入 BizException 中的错误码和错误信息,生成一个失败的响应对象
return ResultVO.fail(e.getCode(), e.getMessage());
}
/**
* MethodArgumentNotValidException
* 使 @Valid
*
*
* @param e MethodArgumentNotValidException
* @return ResultVO
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<?> errorHandler(MethodArgumentNotValidException e) {
return ResultVO.fail(StatusCodeEnum.VALID_ERROR.getCode(), Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
// 从异常中获取绑定结果BindingResult并提取第一个字段错误FieldError的默认错误信息
// 使用 Objects.requireNonNull 确保 FieldError 不为 null避免潜在的 NullPointerException
return ResultVO.fail(
StatusCodeEnum.VALID_ERROR.getCode(), // 使用预定义的参数校验错误码
Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage() // 获取具体的错误信息
);
}
/**
* Exception
*
*
* e.printStackTrace() 便
*
* @param e Exception
* @return ResultVO
*/
@ExceptionHandler(value = Exception.class)
public ResultVO<?> errorHandler(Exception e) {
// 打印异常堆栈信息到控制台,便于调试和问题定位
e.printStackTrace();
return ResultVO.fail(StatusCodeEnum.SYSTEM_ERROR.getCode(), StatusCodeEnum.SYSTEM_ERROR.getDesc());
// 调用 ResultVO.fail 方法,传入系统错误码和系统错误描述,生成一个表示系统错误的失败响应
return ResultVO.fail(
StatusCodeEnum.SYSTEM_ERROR.getCode(), // 使用预定义的系统错误码
StatusCodeEnum.SYSTEM_ERROR.getDesc() // 使用预定义的系统错误描述
);
}
}
}

@ -15,52 +15,82 @@ import javax.annotation.PostConstruct;
import java.util.Collection;
import java.util.List;
/**
* FilterInvocationSecurityMetadataSource
* URL
*/
@Component
public class FilterInvocationSecurityMetadataSourceImpl implements FilterInvocationSecurityMetadataSource {
// 自动注入 RoleMapper用于从数据库中获取资源与角色的映射关系
@Autowired
private RoleMapper roleMapper;
// 静态列表,用于缓存资源与角色的映射关系,避免每次请求都查询数据库
private static List<ResourceRoleDTO> resourceRoleList;
/**
* @PostConstruct
* resourceRoleList
*/
@PostConstruct
private void loadResourceRoleList() {
// 调用 RoleMapper 的 listResourceRoles 方法,从数据库中获取资源与角色的映射列表
resourceRoleList = roleMapper.listResourceRoles();
}
/**
*
*
*/
public void clearDataSource() {
resourceRoleList = null;
}
/**
* FilterInvocation HTTP
* ConfigAttribute
* 访访
*
* @param object FilterInvocation HTTP
* @return null
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 检查资源与角色的映射列表是否为空,如果为空,则重新加载
if (CollectionUtils.isEmpty(resourceRoleList)) {
this.loadResourceRoleList();
}
// 将传入的对象强制转换为 FilterInvocation 类型,以获取请求的详细信息
FilterInvocation fi = (FilterInvocation) object;
// 获取请求的 HTTP 方法(如 GET、POST 等)
String method = fi.getRequest().getMethod();
// 获取请求的 URL 路径
String url = fi.getRequest().getRequestURI();
// 创建 AntPathMatcher 实例,用于匹配 URL 路径
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 遍历资源与角色的映射列表,查找与当前请求匹配的权限配置
for (ResourceRoleDTO resourceRoleDTO : resourceRoleList) {
// 使用 AntPathMatcher 匹配请求的 URL 是否符合资源RoleDTO中定义的 URL 模式
// 同时检查请求的 HTTP 方法是否与资源RoleDTO中定义的方法一致
if (antPathMatcher.match(resourceRoleDTO.getUrl(), url) && resourceRoleDTO.getRequestMethod().equals(method)) {
// 获取该资源所需的角色列表
List<String> roleList = resourceRoleDTO.getRoleList();
// 如果角色列表为空,表示该资源对所有用户都是受限的,返回一个表示禁用的 ConfigAttribute
if (CollectionUtils.isEmpty(roleList)) {
return SecurityConfig.createList("disable");
}
// 将角色列表转换为 ConfigAttribute 列表,表示该资源需要的角色权限
return SecurityConfig.createList(roleList.toArray(new String[]{}));
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
// 如果没有找到匹配的权限配置,返回 null表示该请求不需要任何权限或未定义权限

@ -8,19 +8,52 @@ import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatis-Plus MetaObjectHandler
* createTimeupdateTime
*
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
*
*
*
* @param metaObject MyBatis
*/
@Override
public void insertFill(MetaObject metaObject) {
// 记录日志,表示开始执行插入填充操作
log.info("start insert fill ....");
// 使用 strictInsertFill 方法,严格填充 "createTime" 字段
// 参数说明:
// - metaObject: MyBatis 的元对象
// - "createTime": 需要填充的字段名
// - LocalDateTime.class: 字段的类型
// - LocalDateTime.now(): 填充的值,即当前时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
}
/**
*
*
*
* @param metaObject MyBatis
*/
@Override
public void updateFill(MetaObject metaObject) {
// 记录日志,表示开始执行更新填充操作
log.info("start update fill ....");
// 使用 strictUpdateFill 方法,严格填充 "updateTime" 字段
// 参数说明:
// - metaObject: MyBatis 的元对象
// - "updateTime": 需要填充的字段名
// - LocalDateTime.class: 字段的类型
// - LocalDateTime.now(): 填充的值,即当前时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
}
Loading…
Cancel
Save