From c3dab7c02a37eb8d8a348ef995cc97998154e119 Mon Sep 17 00:00:00 2001 From: bunny <1319900154@qq.com> Date: Fri, 18 Jul 2025 00:21:00 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=AF=BC=E5=85=A5Security?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth-module/module-security/pom.xml | 42 ++++++ .../security/annotation/HasAnyAuthority.java | 15 +++ .../security/annotation/HasUSERAuthorize.java | 15 +++ .../module/security/annotation/IsAdmin.java | 17 +++ .../programmatically/AuthorizationLogic.java | 14 ++ .../AuthorizationManagerConfiguration.java | 21 +++ .../config/SecurityConfiguration.java | 71 ++++++++++ .../config/SecurityWebConfiguration.java | 75 +++++++++++ .../security/event/AuthenticationEvents.java | 63 +++++++++ .../event/SecurityAuthorizationPublisher.java | 21 +++ .../filter/JwtAuthenticationFilter.java | 121 ++++++++++++++++++ .../handler/JwtTokenLogoutHandler.java | 40 ++++++ .../handler/SecurityAccessDeniedHandler.java | 23 ++++ .../SecurityAuthenticationEntryPoint.java | 33 +++++ .../manger/PostAuthorizationManager.java | 48 +++++++ .../manger/PreAuthorizationManager.java | 49 +++++++ .../com/auth/module/security/manger/ReadMe.md | 1 + .../security/password/MD5PasswordEncoder.java | 52 ++++++++ .../security/service/DbUserDetailService.java | 79 ++++++++++++ .../service/InMemoryUserDetailsService.java | 27 ++++ .../security/service/JwtTokenService.java | 96 ++++++++++++++ .../main/resources/application-security.yml | 7 + 22 files changed, 930 insertions(+) create mode 100644 auth-module/module-security/pom.xml create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/annotation/HasAnyAuthority.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/annotation/HasUSERAuthorize.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/annotation/IsAdmin.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/annotation/programmatically/AuthorizationLogic.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/config/AuthorizationManagerConfiguration.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/config/SecurityConfiguration.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/config/SecurityWebConfiguration.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/event/AuthenticationEvents.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/event/SecurityAuthorizationPublisher.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/filter/JwtAuthenticationFilter.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/handler/JwtTokenLogoutHandler.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/handler/SecurityAccessDeniedHandler.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/handler/SecurityAuthenticationEntryPoint.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/manger/PostAuthorizationManager.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/manger/PreAuthorizationManager.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/manger/ReadMe.md create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/password/MD5PasswordEncoder.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/service/DbUserDetailService.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/service/InMemoryUserDetailsService.java create mode 100644 auth-module/module-security/src/main/java/com/auth/module/security/service/JwtTokenService.java create mode 100644 auth-module/module-security/src/main/resources/application-security.yml 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();  // 接收权限列表参数
+     * }
+     * }
+ * + *

注意事项

+ * + * + * @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作为默认选择。

+ * + *

特点:

+ * + * + *

注意:不推荐使用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 result = Result.success(ResultCodeEnum.SUCCESS_LOGOUT); + ResponseUtil.out(response, result); + } +} diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/handler/SecurityAccessDeniedHandler.java b/auth-module/module-security/src/main/java/com/auth/module/security/handler/SecurityAccessDeniedHandler.java new file mode 100644 index 0000000..4aa8b56 --- /dev/null +++ b/auth-module/module-security/src/main/java/com/auth/module/security/handler/SecurityAccessDeniedHandler.java @@ -0,0 +1,23 @@ +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 jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +@Slf4j +public class SecurityAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) { + log.error("SecurityAccessDeniedHandler:{}", accessDeniedException.getLocalizedMessage()); + + // 无权访问接口 + Result result = Result.error(accessDeniedException.getMessage(), ResultCodeEnum.LOGIN_AUTH); + ResponseUtil.out(response, result); + } +} diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/handler/SecurityAuthenticationEntryPoint.java b/auth-module/module-security/src/main/java/com/auth/module/security/handler/SecurityAuthenticationEntryPoint.java new file mode 100644 index 0000000..c5ec62d --- /dev/null +++ b/auth-module/module-security/src/main/java/com/auth/module/security/handler/SecurityAuthenticationEntryPoint.java @@ -0,0 +1,33 @@ +package com.auth.module.security.handler; + +import com.auth.common.exception.MyAuthenticationException; +import com.auth.common.model.common.result.Result; +import com.auth.common.model.common.result.ResultCodeEnum; +import com.auth.common.utils.ResponseUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { + log.error("SecurityAuthenticationEntryPoint:{}", authException.getLocalizedMessage()); + Result result; + + // 自定义认证异常 + if (authException instanceof MyAuthenticationException) { + result = Result.error(null, authException.getMessage()); + ResponseUtil.out(response, result); + } + + // 未认证---未登录 + result = Result.error(authException.getMessage(), ResultCodeEnum.LOGIN_AUTH); + ResponseUtil.out(response, result); + } +} diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/manger/PostAuthorizationManager.java b/auth-module/module-security/src/main/java/com/auth/module/security/manger/PostAuthorizationManager.java new file mode 100644 index 0000000..a52d0d8 --- /dev/null +++ b/auth-module/module-security/src/main/java/com/auth/module/security/manger/PostAuthorizationManager.java @@ -0,0 +1,48 @@ +package com.auth.module.security.manger; + +/** + * 处理方法调用后的授权检查 + * check()方法接收的是MethodInvocationResult对象,包含已执行方法的结果 + * 用于决定是否允许返回某个方法的结果(后置过滤) + * 这是Spring Security较新的"后置授权"功能 + */ +// @Component +// public class PostAuthorizationManager implements AuthorizationManager { +// +// /** +// * 这里两个实现方法按照Security官方要求进行实现 +// *

类说明:

+// * 下面的实现是对方法执行前进行权限校验的判断 +// *
+//      *     AuthorizationManager <MethodInvocation>
+//      * 
+// * 下面的这个是对方法执行后对权限的判断 +// *
+//      *     AuthorizationManager <MethodInvocationResult>
+//      * 
+// * +// *

注意事项:

+// * 将上述两个方法按照自定义的方式进行实现后,还需要禁用默认的。 +// *
+//      * @Configuration
+//      * @EnableMethodSecurity(prePostEnabled = false)
+//      * class MethodSecurityConfig {
+//      *     @Bean
+//      *     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+//      *    Advisor preAuthorize(MyAuthorizationManager manager) {
+//      * 		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
+//      *    }
+//      *
+//      *    @Bean
+//      *    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+//      *    Advisor postAuthorize(MyAuthorizationManager manager) {
+//      * 		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
+//      *    }
+//      * }
+//      * 
+// */ +// @Override +// public AuthorizationDecision check(Supplier authentication, MethodInvocationResult invocation) { +// return new AuthorizationDecision(true); +// } +// } \ No newline at end of file diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/manger/PreAuthorizationManager.java b/auth-module/module-security/src/main/java/com/auth/module/security/manger/PreAuthorizationManager.java new file mode 100644 index 0000000..413e821 --- /dev/null +++ b/auth-module/module-security/src/main/java/com/auth/module/security/manger/PreAuthorizationManager.java @@ -0,0 +1,49 @@ +package com.auth.module.security.manger; + +/** + * 处理方法调用前的授权检查 + * check()方法接收的是MethodInvocation对象,包含即将执行的方法调用信息 + * 用于决定是否允许执行某个方法 + * 这是传统的"前置授权"模式 + */ +// @Component +// public class PreAuthorizationManager implements AuthorizationManager { +// +// /** +// * 这里两个实现方法按照Security官方要求进行实现 +// *

类说明:

+// * 下面的实现是对方法执行前进行权限校验的判断 +// *
+//      *     AuthorizationManager <MethodInvocation>
+//      * 
+// * 下面的这个是对方法执行后对权限的判断 +// *
+//      *     AuthorizationManager <MethodInvocationResult>
+//      * 
+// * +// *

注意事项:

+// * 将上述两个方法按照自定义的方式进行实现后,还需要禁用默认的。 +// *
+//      * @Configuration
+//      * @EnableMethodSecurity(prePostEnabled = false)
+//      * class MethodSecurityConfig {
+//      *     @Bean
+//      *     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+//      *    Advisor preAuthorize(MyAuthorizationManager manager) {
+//      * 		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
+//      *    }
+//      *
+//      *    @Bean
+//      *    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+//      *    Advisor postAuthorize(MyAuthorizationManager manager) {
+//      * 		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
+//      *    }
+//      * }
+//      * 
+// */ +// @Override +// public AuthorizationDecision check(Supplier authentication, MethodInvocation invocation) { +// return new AuthorizationDecision(true); +// } +// +// } \ No newline at end of file diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/manger/ReadMe.md b/auth-module/module-security/src/main/java/com/auth/module/security/manger/ReadMe.md new file mode 100644 index 0000000..1547f4b --- /dev/null +++ b/auth-module/module-security/src/main/java/com/auth/module/security/manger/ReadMe.md @@ -0,0 +1 @@ +如果需要重写验证逻辑(自定义)使用这里面的类,并在配置类`AuthorizationManagerConfiguration`解开注释, \ No newline at end of file diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/password/MD5PasswordEncoder.java b/auth-module/module-security/src/main/java/com/auth/module/security/password/MD5PasswordEncoder.java new file mode 100644 index 0000000..9ba4f30 --- /dev/null +++ b/auth-module/module-security/src/main/java/com/auth/module/security/password/MD5PasswordEncoder.java @@ -0,0 +1,52 @@ +package com.auth.module.security.password; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; + +import java.util.HexFormat; + +/** + *

MD5密码编码器实现

+ * + * 安全警告:此类使用MD5算法进行密码哈希,已不再安全,不推荐用于生产环境。 + * + *

MD5算法因其计算速度快且易受彩虹表攻击而被认为不安全。即使密码哈希本身是单向的, + * 但现代计算能力使得暴力破解和预先计算的彩虹表攻击变得可行。

+ * + *

Spring Security推荐使用BCrypt、PBKDF2、Argon2或Scrypt等自适应单向函数替代MD5。

+ * + * @see PasswordEncoder + * 一般仅用于遗留系统兼容,新系统应使用更安全的密码编码器 + */ +public class MD5PasswordEncoder implements PasswordEncoder { + + @Override + public String encode(CharSequence rawPassword) { + if (rawPassword == null) { + throw new IllegalArgumentException("原始密码不能为null"); + } + + byte[] md5Digest = DigestUtils.md5Digest(rawPassword.toString().getBytes()); + return HexFormat.of().formatHex(md5Digest); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + if (rawPassword == null) { + throw new IllegalArgumentException("原始密码不能为null"); + } + + if (!StringUtils.hasText(encodedPassword)) { + return false; + } + + return encodedPassword.equalsIgnoreCase(encode(rawPassword)); + } + + @Override + public boolean upgradeEncoding(String encodedPassword) { + // MD5已不安全,始终返回true建议升级到更安全的算法 + return true; + } +} \ No newline at end of file diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/service/DbUserDetailService.java b/auth-module/module-security/src/main/java/com/auth/module/security/service/DbUserDetailService.java new file mode 100644 index 0000000..fccdc1a --- /dev/null +++ b/auth-module/module-security/src/main/java/com/auth/module/security/service/DbUserDetailService.java @@ -0,0 +1,79 @@ +package com.auth.module.security.service; + +import com.auth.common.model.entity.base.PermissionEntity; +import com.auth.common.model.entity.base.RoleEntity; +import com.auth.common.model.entity.base.UserEntity; +import com.auth.dao.base.mapper.v1.UserMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +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 org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@Transactional +@RequiredArgsConstructor +public class DbUserDetailService implements UserDetailsService { + + // TODO 找不到UserMapper + private final UserMapper userMapper; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 查询当前用户 + UserEntity userEntity = userMapper.selectByUsername(username); + + // 判断当前用户是否存在 + if (userEntity == null) { + throw new UsernameNotFoundException("用户不存在"); + } + + Long userId = userEntity.getId(); + + List list = new ArrayList<>(); + // 设置用户角色 + List roles = findUserRolesByUserId(userId); + // 设置用户权限 + List permissions = findPermissionByUserId(userId); + list.addAll(roles); + list.addAll(permissions); + + Set authorities = list.stream().map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 设置用户权限 + userEntity.setAuthorities(authorities); + // 返回时将用户密码置为空 + userEntity.setPassword(null); + return userEntity; + } + + /** + * 根据用户id查找该用户的角色内容 + * + * @param userId 用户id + * @return 当前用户的角色信息 + */ + public List findUserRolesByUserId(Long userId) { + List roleList = userMapper.selectRolesByUserId(userId); + return roleList.stream().map(RoleEntity::getRoleCode).toList(); + } + + /** + * 根据用户id查找该用户的权限内容 + * + * @param userId 用户id + * @return 当前用户的权限信息 + */ + public List findPermissionByUserId(Long userId) { + List permissionList = userMapper.selectPermissionByUserId(userId); + return permissionList.stream().map(PermissionEntity::getPermissionCode).toList(); + } +} diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/service/InMemoryUserDetailsService.java b/auth-module/module-security/src/main/java/com/auth/module/security/service/InMemoryUserDetailsService.java new file mode 100644 index 0000000..6cc3127 --- /dev/null +++ b/auth-module/module-security/src/main/java/com/auth/module/security/service/InMemoryUserDetailsService.java @@ -0,0 +1,27 @@ +package com.auth.module.security.service; + +// @Service +// @RequiredArgsConstructor +// public class InMemoryUserDetailsService implements UserDetailsService { +// +// private final PasswordEncoder passwordEncoder; +// +// @Override +// public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { +// // 1. 这里应该根据username从数据库或其他存储中查询用户信息 +// // 以下是模拟数据,实际应用中应从数据库查询 +// +// // 2. 如果用户不存在,抛出UsernameNotFoundException +// if (!"bunny".equalsIgnoreCase(username)) { +// throw new UsernameNotFoundException("User not found: " + username); +// } +// +// // 3. 构建UserDetails对象返回 +// return User.builder() +// .username(username) // 使用传入的用户名 +// .password(passwordEncoder.encode("123456")) // 密码应该已经加密存储,这里仅为示例 +// .roles("USER") // 角色会自动添加ROLE_前缀 +// .authorities("read", "write") // 添加具体权限 +// .build(); +// } +// } \ No newline at end of file diff --git a/auth-module/module-security/src/main/java/com/auth/module/security/service/JwtTokenService.java b/auth-module/module-security/src/main/java/com/auth/module/security/service/JwtTokenService.java new file mode 100644 index 0000000..c550577 --- /dev/null +++ b/auth-module/module-security/src/main/java/com/auth/module/security/service/JwtTokenService.java @@ -0,0 +1,96 @@ +package com.auth.module.security.service; + +import com.auth.common.utils.JwtTokenUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +@Configuration +@ConfigurationProperties(prefix = "jwt-token") +public class JwtTokenService { + + @Value("${jwtToken.secret}") + public String secret; + + @Value("${jwtToken.subject}") + public String subject; + + // private final SecretKey securityKey = Keys.hmacShaKeyFor("Bunny-Auth-Server-Private-SecretKey".getBytes(StandardCharsets.UTF_8)); + // JWT 的 秘钥 + @Value("${jwtToken.expired}") + public Long expired; + + /** + * 创建Token + * + * @param userId 用户Id + * @param username 用户名 + * @return 令牌Token + */ + public String createToken(Long userId, String username, + List roles, List permissions) { + SecretKey key = getSecretKey(); + // return JwtTokenUtil.createToken(userId, username, subject, key, expired); + return JwtTokenUtil.createToken(userId, username, roles, permissions, subject, key, expired); + } + + /** + * 获取安全密钥 + * + * @return {@link SecretKey} + */ + private SecretKey getSecretKey() { + byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); + return new SecretKeySpec(secretBytes, "HmacSHA256"); + } + + /** + * 根据Token获取用户名 + * + * @param token 令牌 + * @return 用户名 + */ + public String getUsernameFromToken(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.getUsername(token, secretKey); + } + + /** + * 根据Token获取用户Id + * + * @param token 令牌 + * @return 用户Id + */ + public Long getUserIdFromToken(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.getUserId(token, secretKey); + } + + /** + * 根据Token获取用户Id + * + * @param token 令牌 + * @return 用户Id + */ + public Map getMapByToken(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.getMapByToken(token, secretKey); + } + + /** + * 判断当前Token是否国企 + * + * @param token 令牌 + * @return 是否国企 + */ + public boolean isExpired(String token) { + SecretKey secretKey = getSecretKey(); + return JwtTokenUtil.isExpired(token, secretKey); + } +} diff --git a/auth-module/module-security/src/main/resources/application-security.yml b/auth-module/module-security/src/main/resources/application-security.yml new file mode 100644 index 0000000..1119e41 --- /dev/null +++ b/auth-module/module-security/src/main/resources/application-security.yml @@ -0,0 +1,7 @@ +jwtToken: + # 密钥 + secret: aVeryLongAndSecureRandomStringWithAtLeast32Characters + # 主题 + subject: SecurityBunny + # 过期事件 7天 + expired: 604800 \ No newline at end of file