✨ 导入Security模块
This commit is contained in:
parent
0ed91386b6
commit
c3dab7c02a
|
@ -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>
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
// }
|
||||
|
||||
}
|
|
@ -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
|
||||
* @Target({ElementType.METHOD, ElementType.TYPE})
|
||||
* @Retention(RetentionPolicy.RUNTIME)
|
||||
* @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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Object> result = Result.success(ResultCodeEnum.SUCCESS_LOGOUT);
|
||||
ResponseUtil.out(response, result);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 <MethodInvocation></code>
|
||||
// * </pre>
|
||||
// * 下面的这个是对方法执行后对权限的判断
|
||||
// * <pre>
|
||||
// * <code>AuthorizationManager <MethodInvocationResult></code>
|
||||
// * </pre>
|
||||
// *
|
||||
// * <h4>注意事项:</h4>
|
||||
// * 将上述两个方法按照自定义的方式进行实现后,还需要禁用默认的。
|
||||
// * <pre>
|
||||
// * @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);
|
||||
// * }
|
||||
// * }
|
||||
// * </pre>
|
||||
// */
|
||||
// @Override
|
||||
// public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
|
||||
// return new AuthorizationDecision(true);
|
||||
// }
|
||||
// }
|
|
@ -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 <MethodInvocation></code>
|
||||
// * </pre>
|
||||
// * 下面的这个是对方法执行后对权限的判断
|
||||
// * <pre>
|
||||
// * <code>AuthorizationManager <MethodInvocationResult></code>
|
||||
// * </pre>
|
||||
// *
|
||||
// * <h4>注意事项:</h4>
|
||||
// * 将上述两个方法按照自定义的方式进行实现后,还需要禁用默认的。
|
||||
// * <pre>
|
||||
// * @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);
|
||||
// * }
|
||||
// * }
|
||||
// * </pre>
|
||||
// */
|
||||
// @Override
|
||||
// public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
|
||||
// return new AuthorizationDecision(true);
|
||||
// }
|
||||
//
|
||||
// }
|
|
@ -0,0 +1 @@
|
|||
如果需要重写验证逻辑(自定义)使用这里面的类,并在配置类`AuthorizationManagerConfiguration`解开注释,
|
|
@ -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推荐使用BCrypt、PBKDF2、Argon2或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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
// }
|
||||
// }
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
jwtToken:
|
||||
# 密钥
|
||||
secret: aVeryLongAndSecureRandomStringWithAtLeast32Characters
|
||||
# 主题
|
||||
subject: SecurityBunny
|
||||
# 过期事件 7天
|
||||
expired: 604800
|
Loading…
Reference in New Issue