diff --git a/auth-module/module-security/pom.xml b/auth-module/module-security/pom.xml
new file mode 100644
index 0000000..e4ef192
--- /dev/null
+++ b/auth-module/module-security/pom.xml
@@ -0,0 +1,42 @@
+
+ 4.0.0
+
+ com.auth
+ auth-module
+ 0.0.1
+
+
+ module-security
+ jar
+ module-security
+ SpringSecurity模块
+
+
+ UTF-8
+ 17
+ 17
+ 17
+
+
+
+
+ com.auth
+ dao-base
+ 0.0.1
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.thymeleaf.extras
+ thymeleaf-extras-springsecurity6
+
+
+
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/annotation/HasAnyAuthority.java b/auth-module/module-security/src/main/java/com/auth/module/security/annotation/HasAnyAuthority.java
new file mode 100644
index 0000000..b3f325b
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/annotation/HasAnyAuthority.java
@@ -0,0 +1,15 @@
+package com.auth.module.security.annotation;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@PreAuthorize("hasAnyAuthority({auth})")
+public @interface HasAnyAuthority {
+ String[] auth();
+}
\ No newline at end of file
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/annotation/HasUSERAuthorize.java b/auth-module/module-security/src/main/java/com/auth/module/security/annotation/HasUSERAuthorize.java
new file mode 100644
index 0000000..c2df7be
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/annotation/HasUSERAuthorize.java
@@ -0,0 +1,15 @@
+package com.auth.module.security.annotation;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@PreAuthorize("hasAuthority('USER') || hasAuthority('{value}')")
+public @interface HasUSERAuthorize {
+ String value();
+}
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/annotation/IsAdmin.java b/auth-module/module-security/src/main/java/com/auth/module/security/annotation/IsAdmin.java
new file mode 100644
index 0000000..8ad897b
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/annotation/IsAdmin.java
@@ -0,0 +1,17 @@
+package com.auth.module.security.annotation;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 判断当前是否是Admin用户
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@PreAuthorize("hasAuthority('ADMIN')")
+public @interface IsAdmin {
+}
\ No newline at end of file
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/annotation/programmatically/AuthorizationLogic.java b/auth-module/module-security/src/main/java/com/auth/module/security/annotation/programmatically/AuthorizationLogic.java
new file mode 100644
index 0000000..4338bd1
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/annotation/programmatically/AuthorizationLogic.java
@@ -0,0 +1,14 @@
+package com.auth.module.security.annotation.programmatically;
+
+import org.springframework.stereotype.Component;
+
+@Component("auth")
+public class AuthorizationLogic {
+
+ public boolean decide(String name) {
+ // 直接使用name的实现
+ // System.out.println(name);
+ return name.equalsIgnoreCase("user");
+ }
+
+}
\ No newline at end of file
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/config/AuthorizationManagerConfiguration.java b/auth-module/module-security/src/main/java/com/auth/module/security/config/AuthorizationManagerConfiguration.java
new file mode 100644
index 0000000..8622a0b
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/config/AuthorizationManagerConfiguration.java
@@ -0,0 +1,21 @@
+package com.auth.module.security.config;
+
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+// @EnableMethodSecurity(prePostEnabled = false)
+public class AuthorizationManagerConfiguration {
+
+ // @Bean
+ // @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ // Advisor preAuthorize(PreAuthorizationManager manager) {
+ // return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
+ // }
+ //
+ // @Bean
+ // @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ // Advisor postAuthorize(PostAuthorizationManager manager) {
+ // return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
+ // }
+
+}
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/config/SecurityConfiguration.java b/auth-module/module-security/src/main/java/com/auth/module/security/config/SecurityConfiguration.java
new file mode 100644
index 0000000..3e79e13
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/config/SecurityConfiguration.java
@@ -0,0 +1,71 @@
+package com.auth.module.security.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authorization.method.PrePostTemplateDefaults;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+public class SecurityConfiguration {
+
+ /**
+ * 注册一个用于Spring Security预授权/后授权的模板元注解默认配置Bean。
+ *
+ *
该Bean提供了基于SpEL表达式的权限校验模板,可用于自定义组合注解。
+ *
+ * 典型用法
+ * 通过此配置可以简化自定义权限注解的定义,例如:
+ * {@code
+ * @Target({ElementType.METHOD, ElementType.TYPE})
+ * @Retention(RetentionPolicy.RUNTIME)
+ * @PreAuthorize("hasAnyAuthority( // 使用模板提供的表达式语法
+ * public @interface HasAnyAuthority {
+ * String[] auth(); // 接收权限列表参数
+ * }
+ * }
+ *
+ * 注意事项
+ *
+ * - 需要确保Spring Security的预授权功能已启用
+ * - 模板表达式应符合SpEL语法规范
+ *
+ *
+ * @return PrePostTemplateDefaults 实例,用于预/后授权注解的默认配置
+ * @see org.springframework.security.access.prepost.PreAuthorize
+ * @see org.springframework.security.access.prepost.PostAuthorize
+ */
+ @Bean
+ PrePostTemplateDefaults prePostTemplateDefaults() {
+ return new PrePostTemplateDefaults();
+ }
+
+ /**
+ * 配置密码编码器Bean
+ *
+ * Spring Security提供了多种密码编码器实现,推荐使用BCryptPasswordEncoder作为默认选择。
+ *
+ * 特点:
+ *
+ * - BCryptPasswordEncoder - 使用bcrypt强哈希算法,自动加盐,是当前最推荐的密码编码器
+ * - Argon2PasswordEncoder - 使用Argon2算法,抗GPU/ASIC攻击,但需要更多内存
+ * - SCryptPasswordEncoder - 使用scrypt算法,内存密集型,抗硬件攻击
+ * - Pbkdf2PasswordEncoder - 使用PBKDF2算法,较老但广泛支持
+ *
+ *
+ * 注意:不推荐使用MD5等弱哈希算法,Spring官方也不推荐自定义弱密码编码器。
+ *
+ * @return PasswordEncoder 密码编码器实例
+ * @see BCryptPasswordEncoder
+ */
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ // 其他编码器示例(根据需求选择一种):
+ // return new Argon2PasswordEncoder(16, 32, 1, 1 << 14, 2);
+ // return new SCryptPasswordEncoder();
+ // return new Pbkdf2PasswordEncoder("secret", 185000, 256);
+
+ // 实际项目中只需返回一个密码编码器
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/config/SecurityWebConfiguration.java b/auth-module/module-security/src/main/java/com/auth/module/security/config/SecurityWebConfiguration.java
new file mode 100644
index 0000000..1965275
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/config/SecurityWebConfiguration.java
@@ -0,0 +1,75 @@
+package com.auth.module.security.config;
+
+
+import com.auth.module.security.filter.JwtAuthenticationFilter;
+import com.auth.module.security.handler.SecurityAccessDeniedHandler;
+import com.auth.module.security.handler.SecurityAuthenticationEntryPoint;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+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.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+import java.util.List;
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity(jsr250Enabled = true)
+@RequiredArgsConstructor
+public class SecurityWebConfiguration {
+
+ public static List securedPaths = List.of("/api/**");
+ public static List noAuthPaths = List.of("/*/login");
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ @Bean
+ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+
+ http
+ // 前端段分离不需要---禁用明文验证
+ .httpBasic(AbstractHttpConfigurer::disable)
+ // 前端段分离不需要---禁用默认登录页
+ .formLogin(AbstractHttpConfigurer::disable)
+ // 前端段分离不需要---禁用退出页
+ .logout(AbstractHttpConfigurer::disable)
+ // 前端段分离不需要---csrf攻击
+ .csrf(AbstractHttpConfigurer::disable)
+ // 跨域访问权限,如果需要可以关闭后自己配置跨域访问
+ .cors(AbstractHttpConfigurer::disable)
+ // 前后端分离不需要---因为是无状态的
+ // .sessionManagement(AbstractHttpConfigurer::disable)
+ .sessionManagement(session ->
+ session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+ .authorizeHttpRequests(authorizeRequests ->
+ // 访问路径为 /api 时需要进行认证
+ authorizeRequests
+ // 不认证登录接口
+ .requestMatchers(noAuthPaths.toArray(String[]::new)).permitAll()
+ // ❗只认证 securedPaths 下的所有接口
+ // =======================================================================
+ // 也可以在这里写多参数传入,如:"/api/**","/admin/**"
+ // 但是在 Spring过滤器中,如果要放行不需要认证请求,但是需要认证的接口必需要携带token。
+ // 做法是在这里定义要认证的接口,如果要做成动态可以放到数据库。
+ // =======================================================================
+ .requestMatchers(securedPaths.toArray(String[]::new)).authenticated()
+ // 其余请求都放行
+ .anyRequest().permitAll()
+ )
+ .exceptionHandling(exception -> {
+ // 请求未授权接口
+ exception.authenticationEntryPoint(new SecurityAuthenticationEntryPoint());
+ // 没有权限访问
+ exception.accessDeniedHandler(new SecurityAccessDeniedHandler());
+ })
+ .addFilterAfter(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+ ;
+
+ return http.build();
+ }
+}
\ No newline at end of file
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/event/AuthenticationEvents.java b/auth-module/module-security/src/main/java/com/auth/module/security/event/AuthenticationEvents.java
new file mode 100644
index 0000000..437560a
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/event/AuthenticationEvents.java
@@ -0,0 +1,63 @@
+package com.auth.module.security.event;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.context.event.EventListener;
+import org.springframework.security.authorization.AuthorizationDecision;
+import org.springframework.security.authorization.event.AuthorizationDeniedEvent;
+import org.springframework.security.authorization.event.AuthorizationGrantedEvent;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+@Slf4j
+@Component
+public class AuthenticationEvents {
+
+ /**
+ * 监听拒绝授权内容
+ *
+ * @param failure 授权失败
+ */
+ @EventListener
+ public void onFailure(AuthorizationDeniedEvent failure) {
+ try {
+ // getSource 和 getObject意思一样,一种是传入泛型自动转换一种是要手动转换
+ Object source = failure.getSource();
+
+ // 直接获取泛型对象
+ MethodInvocation methodInvocation = failure.getObject();
+ Method method = methodInvocation.getMethod();
+ Object[] args = methodInvocation.getArguments();
+
+ log.warn("方法调用被拒绝: {}.{}, 参数: {}",
+ method.getDeclaringClass().getSimpleName(),
+ method.getName(),
+ Arrays.toString(args));
+
+ // 这里面的信息,和接口 /api/security/current-user 内容一样
+ Authentication authentication = failure.getAuthentication().get();
+
+ AuthorizationDecision authorizationDecision = failure.getAuthorizationDecision();
+ // ExpressionAuthorizationDecision [granted=false, expressionAttribute=hasAuthority('ADMIN')]
+ System.out.println(authorizationDecision);
+
+ log.warn("授权失败 - 用户: {}, 权限: {}", authentication.getName(), authorizationDecision);
+ } catch (Exception e) {
+ log.info(e.getMessage());
+ }
+ }
+
+ /**
+ * 监听授权的内容
+ * 如果要监听授权成功的内容,这个内容可能相当的多,毕竟正常情况授权成功的内容还是比较多的。
+ * 既然内容很多又要监听,如果真的需要,一定要处理好业务逻辑,不要被成功的消息淹没。
+ *
+ * @param success 授权成功
+ */
+ @EventListener
+ public void onSuccess(AuthorizationGrantedEvent success) {
+ }
+}
\ No newline at end of file
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/event/SecurityAuthorizationPublisher.java b/auth-module/module-security/src/main/java/com/auth/module/security/event/SecurityAuthorizationPublisher.java
new file mode 100644
index 0000000..46ef91e
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/event/SecurityAuthorizationPublisher.java
@@ -0,0 +1,21 @@
+package com.auth.module.security.event;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authorization.AuthorizationEventPublisher;
+import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
+import org.springframework.stereotype.Component;
+
+/**
+ * 如果要监听授权和拒绝的授权需要发布一个像下面这样的事件
+ * 之后使用 Spring 的 @EventListener
+ */
+@Component
+public class SecurityAuthorizationPublisher {
+
+ @Bean
+ public AuthorizationEventPublisher authorizationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
+ return new SpringAuthorizationEventPublisher(applicationEventPublisher);
+ }
+
+}
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/filter/JwtAuthenticationFilter.java b/auth-module/module-security/src/main/java/com/auth/module/security/filter/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..057ba8f
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/filter/JwtAuthenticationFilter.java
@@ -0,0 +1,121 @@
+package com.auth.module.security.filter;
+
+import com.auth.common.context.BaseContext;
+import com.auth.common.exception.AuthenticSecurityException;
+import com.auth.common.exception.MyAuthenticationException;
+import com.auth.common.model.common.result.ResultCodeEnum;
+import com.auth.module.security.config.SecurityWebConfiguration;
+import com.auth.module.security.handler.SecurityAuthenticationEntryPoint;
+import com.auth.module.security.service.DbUserDetailService;
+import com.auth.module.security.service.JwtTokenService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtTokenService jwtTokenService;
+ private final DbUserDetailService userDetailsService;
+ private final SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException, AuthenticSecurityException {
+ // 先校验不需要认证的接口
+ RequestMatcher[] requestNoAuthMatchers = SecurityWebConfiguration.noAuthPaths.stream()
+ .map(AntPathRequestMatcher::new)
+ .toArray(RequestMatcher[]::new);
+ OrRequestMatcher noAuthRequestMatcher = new OrRequestMatcher(requestNoAuthMatchers);
+ if (noAuthRequestMatcher.matches(request)) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ // 获取需要认证的接口
+ RequestMatcher[] requestSecureMatchers = SecurityWebConfiguration.securedPaths.stream()
+ .map(AntPathRequestMatcher::new)
+ .toArray(RequestMatcher[]::new);
+ OrRequestMatcher secureRequestMatcher = new OrRequestMatcher(requestSecureMatchers);
+
+ // 公开接口直接放行
+ if (!secureRequestMatcher.matches(request)) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ final String authHeader = request.getHeader("Authorization");
+ // 如果当前请求不包含验证Token直接返回
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ throw new AuthenticSecurityException(ResultCodeEnum.LOGIN_AUTH);
+ }
+
+ // 当前请求的Token
+ final String jwtToken = authHeader.substring(7);
+
+ try {
+ // 检查当前Token是否过期
+ if (jwtTokenService.isExpired(jwtToken)) {
+ // 💡如果过期不需要进行判断和验证,需要直接放行可以像下面这样写
+ // ===================================================
+ // filterChain.doFilter(request, response);
+ // return;
+ // ===================================================
+ throw new AuthenticSecurityException(ResultCodeEnum.AUTHENTICATION_EXPIRED);
+ }
+
+ // 解析当前Token中的用户名
+ String username = jwtTokenService.getUsernameFromToken(jwtToken);
+ Long userId = jwtTokenService.getUserIdFromToken(jwtToken);
+
+ // 当前用户名存在,并且 Security上下文为空,设置认证相关信息
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ // 调用用户信息进行登录
+ UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+ UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+ userDetails,
+ null,
+ userDetails.getAuthorities()
+ );
+ authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+
+ // 设置认证用户信息
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ BaseContext.setUsername(username);
+ BaseContext.setUserId(userId);
+ }
+
+ filterChain.doFilter(request, response);
+ }
+ // ⚠️IMPORTANT:
+ // ==========================================================================
+ // 在 catch 块中,securityAuthenticationEntryPoint.commence() 已经处理了错误响应
+ // 所以应该 直接返回,避免继续执行后续逻辑。
+ // ==========================================================================
+ catch (RuntimeException exception) {
+ securityAuthenticationEntryPoint.commence(
+ request,
+ response,
+ new MyAuthenticationException(exception.getMessage(), exception)
+ );
+ }
+ }
+}
diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/handler/JwtTokenLogoutHandler.java b/auth-module/module-security/src/main/java/com/auth/module/security/handler/JwtTokenLogoutHandler.java
new file mode 100644
index 0000000..d6f3894
--- /dev/null
+++ b/auth-module/module-security/src/main/java/com/auth/module/security/handler/JwtTokenLogoutHandler.java
@@ -0,0 +1,40 @@
+package com.auth.module.security.handler;
+
+import com.auth.common.model.common.result.Result;
+import com.auth.common.model.common.result.ResultCodeEnum;
+import com.auth.common.utils.ResponseUtil;
+import com.auth.module.security.service.JwtTokenService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+/**
+ * 实现注销处理器
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtTokenLogoutHandler implements LogoutHandler {
+
+ private final JwtTokenService jwtTokenService;
+
+ @Override
+ public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
+ // 在这里可以设置不同的请求头标识符,常见的:Authorization、Token等
+ String authorizationToken = request.getHeader("Authorization");
+
+ if (StringUtils.hasText(authorizationToken)) {
+ // 如果当前用户信息存在redis中可以通过这个进行退出
+ String username = jwtTokenService.getUsernameFromToken(authorizationToken);
+ log.info("username : {}", username);
+ }
+
+ Result