导入Security模块

This commit is contained in:
bunny 2025-07-18 00:21:00 +08:00
parent 0ed91386b6
commit c3dab7c02a
22 changed files with 930 additions and 0 deletions

View File

@ -0,0 +1,42 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
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.auth</groupId>
<artifactId>auth-module</artifactId>
<version>0.0.1</version>
</parent>
<artifactId>module-security</artifactId>
<packaging>jar</packaging>
<name>module-security</name>
<description>SpringSecurity模块</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.auth</groupId>
<artifactId>dao-base</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@ -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 {
}

View File

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

View File

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

View File

@ -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
*
* <p>该Bean提供了基于SpEL表达式的权限校验模板可用于自定义组合注解</p>
*
* <h3>典型用法</h3>
* <p>通过此配置可以简化自定义权限注解的定义例如</p>
* <pre>{@code
* &#064;Target({ElementType.METHOD, ElementType.TYPE})
* &#064;Retention(RetentionPolicy.RUNTIME)
* &#064;PreAuthorize("hasAnyAuthority( // 使用模板提供的表达式语法
* public @interface HasAnyAuthority {
* String[] auth(); // 接收权限列表参数
* }
* }</pre>
*
* <h3>注意事项</h3>
* <ul>
* <li>需要确保Spring Security的预授权功能已启用</li>
* <li>模板表达式应符合SpEL语法规范</li>
* </ul>
*
* @return PrePostTemplateDefaults 实例用于预/后授权注解的默认配置
* @see org.springframework.security.access.prepost.PreAuthorize
* @see org.springframework.security.access.prepost.PostAuthorize
*/
@Bean
PrePostTemplateDefaults prePostTemplateDefaults() {
return new PrePostTemplateDefaults();
}
/**
* 配置密码编码器Bean
*
* <p>Spring Security提供了多种密码编码器实现推荐使用BCryptPasswordEncoder作为默认选择</p>
*
* <p>特点</p>
* <ul>
* <li>BCryptPasswordEncoder - 使用bcrypt强哈希算法自动加盐是当前最推荐的密码编码器</li>
* <li>Argon2PasswordEncoder - 使用Argon2算法抗GPU/ASIC攻击但需要更多内存</li>
* <li>SCryptPasswordEncoder - 使用scrypt算法内存密集型抗硬件攻击</li>
* <li>Pbkdf2PasswordEncoder - 使用PBKDF2算法较老但广泛支持</li>
* </ul>
*
* <p>注意不推荐使用MD5等弱哈希算法Spring官方也不推荐自定义弱密码编码器</p>
*
* @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();
}
}

View File

@ -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<String> securedPaths = List.of("/api/**");
public static List<String> 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();
}
}

View File

@ -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<MethodInvocation> 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<MethodInvocation> success) {
}
}

View File

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

View File

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

View File

@ -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) {
// 在这里可以设置不同的请求头标识符常见的AuthorizationToken等
String authorizationToken = request.getHeader("Authorization");
if (StringUtils.hasText(authorizationToken)) {
// 如果当前用户信息存在redis中可以通过这个进行退出
String username = jwtTokenService.getUsernameFromToken(authorizationToken);
log.info("username : {}", username);
}
Result<Object> result = Result.success(ResultCodeEnum.SUCCESS_LOGOUT);
ResponseUtil.out(response, result);
}
}

View File

@ -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<Object> result = Result.error(accessDeniedException.getMessage(), ResultCodeEnum.LOGIN_AUTH);
ResponseUtil.out(response, result);
}
}

View File

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

View File

@ -0,0 +1,48 @@
package com.auth.module.security.manger;
/**
* 处理方法调用后的授权检查
* check()方法接收的是MethodInvocationResult对象包含已执行方法的结果
* 用于决定是否允许返回某个方法的结果(后置过滤)
* 这是Spring Security较新的"后置授权"功能
*/
// @Component
// public class PostAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
//
// /**
// * 这里两个实现方法按照Security官方要求进行实现
// * <h4>类说明</h4>
// * 下面的实现是对方法执行前进行权限校验的判断
// * <pre>
// * <code>AuthorizationManager &ltMethodInvocation></code>
// * </pre>
// * 下面的这个是对方法执行后对权限的判断
// * <pre>
// * <code>AuthorizationManager &ltMethodInvocationResult></code>
// * </pre>
// *
// * <h4>注意事项</h4>
// * 将上述两个方法按照自定义的方式进行实现后还需要禁用默认的
// * <pre>
// * &#064;Configuration
// * &#064;EnableMethodSecurity(prePostEnabled = false)
// * class MethodSecurityConfig {
// * &#064;Bean
// * &#064;Role(BeanDefinition.ROLE_INFRASTRUCTURE)
// * Advisor preAuthorize(MyAuthorizationManager manager) {
// * return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
// * }
// *
// * &#064;Bean
// * &#064;Role(BeanDefinition.ROLE_INFRASTRUCTURE)
// * Advisor postAuthorize(MyAuthorizationManager manager) {
// * return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
// * }
// * }
// * </pre>
// */
// @Override
// public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
// return new AuthorizationDecision(true);
// }
// }

View File

@ -0,0 +1,49 @@
package com.auth.module.security.manger;
/**
* 处理方法调用前的授权检查
* check()方法接收的是MethodInvocation对象包含即将执行的方法调用信息
* 用于决定是否允许执行某个方法
* 这是传统的"前置授权"模式
*/
// @Component
// public class PreAuthorizationManager implements AuthorizationManager<MethodInvocation> {
//
// /**
// * 这里两个实现方法按照Security官方要求进行实现
// * <h4>类说明</h4>
// * 下面的实现是对方法执行前进行权限校验的判断
// * <pre>
// * <code>AuthorizationManager &ltMethodInvocation></code>
// * </pre>
// * 下面的这个是对方法执行后对权限的判断
// * <pre>
// * <code>AuthorizationManager &ltMethodInvocationResult></code>
// * </pre>
// *
// * <h4>注意事项</h4>
// * 将上述两个方法按照自定义的方式进行实现后还需要禁用默认的
// * <pre>
// * &#064;Configuration
// * &#064;EnableMethodSecurity(prePostEnabled = false)
// * class MethodSecurityConfig {
// * &#064;Bean
// * &#064;Role(BeanDefinition.ROLE_INFRASTRUCTURE)
// * Advisor preAuthorize(MyAuthorizationManager manager) {
// * return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
// * }
// *
// * &#064;Bean
// * &#064;Role(BeanDefinition.ROLE_INFRASTRUCTURE)
// * Advisor postAuthorize(MyAuthorizationManager manager) {
// * return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
// * }
// * }
// * </pre>
// */
// @Override
// public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
// return new AuthorizationDecision(true);
// }
//
// }

View File

@ -0,0 +1 @@
如果需要重写验证逻辑(自定义)使用这里面的类,并在配置类`AuthorizationManagerConfiguration`解开注释,

View File

@ -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;
/**
* <h1>MD5密码编码器实现</h1>
*
* <strong>安全警告</strong>此类使用MD5算法进行密码哈希已不再安全不推荐用于生产环境
*
* <p>MD5算法因其计算速度快且易受彩虹表攻击而被认为不安全即使密码哈希本身是单向的
* 但现代计算能力使得暴力破解和预先计算的彩虹表攻击变得可行</p>
*
* <p>Spring Security推荐使用BCryptPBKDF2Argon2或Scrypt等自适应单向函数替代MD5</p>
*
* @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;
}
}

View File

@ -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<String> list = new ArrayList<>();
// 设置用户角色
List<String> roles = findUserRolesByUserId(userId);
// 设置用户权限
List<String> permissions = findPermissionByUserId(userId);
list.addAll(roles);
list.addAll(permissions);
Set<SimpleGrantedAuthority> authorities = list.stream().map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
// 设置用户权限
userEntity.setAuthorities(authorities);
// 返回时将用户密码置为空
userEntity.setPassword(null);
return userEntity;
}
/**
* 根据用户id查找该用户的角色内容
*
* @param userId 用户id
* @return 当前用户的角色信息
*/
public List<String> findUserRolesByUserId(Long userId) {
List<RoleEntity> roleList = userMapper.selectRolesByUserId(userId);
return roleList.stream().map(RoleEntity::getRoleCode).toList();
}
/**
* 根据用户id查找该用户的权限内容
*
* @param userId 用户id
* @return 当前用户的权限信息
*/
public List<String> findPermissionByUserId(Long userId) {
List<PermissionEntity> permissionList = userMapper.selectPermissionByUserId(userId);
return permissionList.stream().map(PermissionEntity::getPermissionCode).toList();
}
}

View File

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

View File

@ -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<String> roles, List<String> 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<String, Object> 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);
}
}

View File

@ -0,0 +1,7 @@
jwtToken:
# 密钥
secret: aVeryLongAndSecureRandomStringWithAtLeast32Characters
# 主题
subject: SecurityBunny
# 过期事件 7天
expired: 604800