init
This commit is contained in:
50
common/common-security/pom.xml
Normal file
50
common/common-security/pom.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>common</artifactId>
|
||||
<groupId>cn.fateverse</groupId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>common-security</artifactId>
|
||||
<description>common0security授权鉴权模块</description>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!--security 依赖-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||
</dependency>
|
||||
<!--Token生成与解析-->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.fateverse</groupId>
|
||||
<artifactId>common-core</artifactId>
|
||||
</dependency>
|
||||
<!--Redis Common-->
|
||||
<dependency>
|
||||
<groupId>cn.fateverse</groupId>
|
||||
<artifactId>common-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.fateverse</groupId>
|
||||
<artifactId>admin-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.fateverse.common.security.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 服务调用不鉴权注解
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/29
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Anonymity {
|
||||
|
||||
/**
|
||||
* 是否AOP统一处理
|
||||
*
|
||||
* @return false, true
|
||||
*/
|
||||
boolean value() default true;
|
||||
|
||||
/**
|
||||
* 需要特殊判空的字段(预留)
|
||||
*
|
||||
* @return {}
|
||||
*/
|
||||
String[] field() default {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.fateverse.common.security.annotation;
|
||||
|
||||
import cn.fateverse.common.security.configure.SecurityAutoConfiguration;
|
||||
import cn.fateverse.common.security.configure.TaskExecutePoolConfiguration;
|
||||
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 是否启动鉴权中心的权限校验功能
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/28
|
||||
*/
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@EnableWebMvc
|
||||
@EnableDubbo
|
||||
@Import({SecurityAutoConfiguration.class, TaskExecutePoolConfiguration.class})
|
||||
public @interface EnableSecurity {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cn.fateverse.common.security.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* RequestMapping接口的开关
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2024/1/15 16:26
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface MappingSwitch {
|
||||
|
||||
/**
|
||||
* 描述信息
|
||||
*/
|
||||
String value() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package cn.fateverse.common.security.aspect;
|
||||
|
||||
import cn.fateverse.common.security.annotation.Anonymity;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2022/10/29
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
public class SecurityInnerAspect implements Ordered {
|
||||
|
||||
@SneakyThrows
|
||||
@Around("@within(anonymity) || @annotation(anonymity)")
|
||||
public Object around(ProceedingJoinPoint point, Anonymity anonymity) {
|
||||
// 实际注入的inner实体由表达式后一个注解决定,即是方法上的@Inner注解实体,若方法上无@Inner注解,则获取类上的
|
||||
if (anonymity == null) {
|
||||
Class<?> clazz = point.getTarget().getClass();
|
||||
anonymity = AnnotationUtils.findAnnotation(clazz, Anonymity.class);
|
||||
}
|
||||
if (!anonymity.value()) {
|
||||
log.warn("访问接口 {} 没有权限", point.getSignature().getName());
|
||||
throw new AccessDeniedException("Access is denied");
|
||||
}
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE + 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.fateverse.common.security.configure;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/27
|
||||
*/
|
||||
public class CorsFilterConfiguration {
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*/
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowCredentials(true);
|
||||
// 设置访问源地址
|
||||
// config.addAllowedOrigin("*")不起作用替换为config.addAllowedOriginPattern("*")
|
||||
config.addAllowedOriginPattern("*");
|
||||
// 设置访问源请求头
|
||||
config.addAllowedHeader("*");
|
||||
// 设置访问源请求方法
|
||||
config.addAllowedMethod("*");
|
||||
// 对接口配置跨域设置
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package cn.fateverse.common.security.configure;
|
||||
|
||||
import cn.fateverse.common.security.annotation.MappingSwitch;
|
||||
import cn.fateverse.common.security.entity.MappingSwitchInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* MappingSwitch开关处理类
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2024/1/15 17:20
|
||||
*/
|
||||
@Slf4j
|
||||
public class MappingSwitchConfiguration implements InitializingBean, ApplicationContextAware {
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private String applicationName = "";
|
||||
|
||||
@Qualifier("fateverseExecutor")
|
||||
@Resource
|
||||
private ThreadPoolTaskExecutor taskExecuteExecutor;
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, MappingSwitchInfo> redisTemplate;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
taskExecuteExecutor.submit(() -> {
|
||||
RequestMappingHandlerMapping mapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
|
||||
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
|
||||
Map<String, MappingSwitchInfo> mappingSwitchInfoMap = new HashMap<>();
|
||||
map.forEach((info, handlerMethod) -> {
|
||||
//判断方法上是否存在注解
|
||||
MappingSwitch method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), MappingSwitch.class);
|
||||
if (method != null) {
|
||||
MappingSwitchInfo mappingSwitchInfo = new MappingSwitchInfo();
|
||||
mappingSwitchInfo.setClassName(handlerMethod.getBeanType().getName());
|
||||
mappingSwitchInfo.setMethodName(handlerMethod.getMethod().getName());
|
||||
mappingSwitchInfo.setApplicationName(applicationName);
|
||||
mappingSwitchInfo.setState(Boolean.TRUE);
|
||||
mappingSwitchInfo.setDescription(method.value());
|
||||
//获取到uri
|
||||
PatternsRequestCondition patternsCondition = info.getPatternsCondition();
|
||||
if (patternsCondition != null) {
|
||||
Set<String> uris = patternsCondition.getPatterns();
|
||||
mappingSwitchInfo.setUris(uris);
|
||||
}
|
||||
//获取到请求类型
|
||||
RequestMethodsRequestCondition infoMethodsCondition = info.getMethodsCondition();
|
||||
Set<RequestMethod> methods = infoMethodsCondition.getMethods();
|
||||
if (!methods.isEmpty()) {
|
||||
Set<String> methodSet = methods.stream().map(Enum::toString).collect(Collectors.toSet());
|
||||
mappingSwitchInfo.setHttpMethods(methodSet);
|
||||
}
|
||||
//获取到当前的key
|
||||
String key = MappingSwitchInfo.getKey(applicationName, handlerMethod, Boolean.TRUE);
|
||||
//初始化
|
||||
initRedisCache(key, mappingSwitchInfo);
|
||||
mappingSwitchInfo.setType(MappingSwitchInfo.MappingSwitchType.METHOD);
|
||||
//添加到临时缓存中
|
||||
mappingSwitchInfoMap.put(key, mappingSwitchInfo);
|
||||
}
|
||||
//判断类上是否存在注解
|
||||
MappingSwitch controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), MappingSwitch.class);
|
||||
if (controller != null) {
|
||||
//获取到key
|
||||
String key = MappingSwitchInfo.getKey(applicationName, handlerMethod, Boolean.FALSE);
|
||||
if (!mappingSwitchInfoMap.containsKey(key)) {
|
||||
// 创建存储对象
|
||||
MappingSwitchInfo mappingSwitchInfo = new MappingSwitchInfo();
|
||||
mappingSwitchInfo.setClassName(handlerMethod.getBeanType().getName());
|
||||
mappingSwitchInfo.setApplicationName(applicationName);
|
||||
mappingSwitchInfo.setState(Boolean.TRUE);
|
||||
mappingSwitchInfo.setDescription(controller.value());
|
||||
//获取到RequestMapping,以获取到当前Controller的请求路径
|
||||
RequestMapping requestMapping = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), RequestMapping.class);
|
||||
if (requestMapping != null) {
|
||||
String[] value = requestMapping.value();
|
||||
Set<String> uris = new HashSet<>();
|
||||
Collections.addAll(uris, value);
|
||||
mappingSwitchInfo.setUris(uris);
|
||||
}
|
||||
//初始化对象
|
||||
initRedisCache(key, mappingSwitchInfo);
|
||||
mappingSwitchInfo.setType(MappingSwitchInfo.MappingSwitchType.CLASS);
|
||||
//将对象添加到临时缓存
|
||||
mappingSwitchInfoMap.put(key, mappingSwitchInfo);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!mappingSwitchInfoMap.isEmpty()) {
|
||||
Set<String> keys = new HashSet<>();
|
||||
try (Cursor<String> cursor = redisTemplate.scan(ScanOptions.scanOptions()
|
||||
.match(MappingSwitchInfo.MappingSwitchConstant.MAPPING_SWITCH + applicationName + "*")
|
||||
.build())) {
|
||||
while (cursor.hasNext()) {
|
||||
keys.add(cursor.next());
|
||||
}
|
||||
}
|
||||
//先删除当前应用名称下的数据
|
||||
redisTemplate.delete(keys);
|
||||
//重新新增
|
||||
redisTemplate.opsForValue().multiSet(mappingSwitchInfoMap);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化缓存对象
|
||||
*
|
||||
* @param key redis key
|
||||
* @param info 存储对象
|
||||
*/
|
||||
private void initRedisCache(String key, MappingSwitchInfo info) {
|
||||
//获取redis中当前的key
|
||||
MappingSwitchInfo cacheInfo = redisTemplate.opsForValue().get(key);
|
||||
//如果redis中的状态为false,则不能更改redis中的状态
|
||||
if (cacheInfo != null && !cacheInfo.getState()) {
|
||||
info.setState(Boolean.FALSE);
|
||||
}
|
||||
info.setKey(key);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
Environment environment = applicationContext.getEnvironment();
|
||||
String applicationName = environment.getProperty("spring.application.name");
|
||||
if (ObjectUtils.isEmpty(applicationName)) {
|
||||
log.error("applicationName can not be null");
|
||||
throw new RuntimeException("applicationName can not be null");
|
||||
}
|
||||
this.applicationName = applicationName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package cn.fateverse.common.security.configure;
|
||||
|
||||
import cn.fateverse.common.security.aspect.SecurityInnerAspect;
|
||||
import cn.fateverse.common.security.configure.properties.DemoSwitchProperties;
|
||||
import cn.fateverse.common.security.configure.properties.PermitAllUrlProperties;
|
||||
import cn.fateverse.common.security.filter.AuthenticationTokenFilter;
|
||||
import cn.fateverse.common.security.handle.*;
|
||||
import cn.fateverse.common.security.service.PermissionService;
|
||||
import cn.fateverse.common.security.service.TokenService;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2023-05-21
|
||||
*/
|
||||
@ConfigurationPropertiesScan(basePackageClasses = {DemoSwitchProperties.class})
|
||||
@EnableConfigurationProperties(PermitAllUrlProperties.class)
|
||||
public class SecurityAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 鉴权具体的实现逻辑
|
||||
*
|
||||
* @return (# ss.xxx)
|
||||
*/
|
||||
@Bean("ss")
|
||||
public PermissionService permissionService() {
|
||||
return new PermissionService();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* token验证处理
|
||||
*
|
||||
* @return tokenService
|
||||
*/
|
||||
@Bean
|
||||
public TokenService tokenService() {
|
||||
return new TokenService();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 认证失败处理类
|
||||
*
|
||||
* @return authenticationEntryPointImpl
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationEntryPointImpl authenticationEntryPointImpl() {
|
||||
return new AuthenticationEntryPointImpl();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* token 验证拦截器
|
||||
*
|
||||
* @return authenticationTokenFilter
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationTokenFilter authenticationTokenFilter() {
|
||||
return new AuthenticationTokenFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* inner注解拦截器
|
||||
*
|
||||
* @return securityInnerAspect
|
||||
*/
|
||||
//@Bean
|
||||
public SecurityInnerAspect securityInnerAspect() {
|
||||
return new SecurityInnerAspect();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GlobalExceptionHandler globalExceptionHandler() {
|
||||
return new GlobalExceptionHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ResultResponseAdvice resultResponseAdvice() {
|
||||
return new ResultResponseAdvice();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogoutSuccessHandlerImpl getLogoutSuccessHandler(TokenService tokenService) {
|
||||
return new LogoutSuccessHandlerImpl(tokenService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MappingSwitchConfiguration mappingSwitchService(){
|
||||
return new MappingSwitchConfiguration();
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public MappingSwitchInterceptor mappingSwitchInterceptor() {
|
||||
return new MappingSwitchInterceptor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package cn.fateverse.common.security.configure;
|
||||
|
||||
import cn.fateverse.common.security.configure.properties.PermitAllUrlProperties;
|
||||
import cn.fateverse.common.security.filter.AuthenticationTokenFilter;
|
||||
import cn.fateverse.common.security.handle.AuthenticationEntryPointImpl;
|
||||
import cn.fateverse.common.security.handle.LogoutSuccessHandlerImpl;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 安全认证配置
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/27
|
||||
*/
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
@EnableConfigurationProperties(PermitAllUrlProperties.class)
|
||||
public class SecurityCloudConfiguration {
|
||||
|
||||
@Resource
|
||||
private PermitAllUrlProperties permitAllUrl;
|
||||
|
||||
@Resource
|
||||
private AuthenticationEntryPointImpl authenticationHandler;
|
||||
|
||||
/**
|
||||
* 跨域过滤器
|
||||
*/
|
||||
@Resource
|
||||
private CorsFilter corsFilter;
|
||||
|
||||
|
||||
@Resource
|
||||
private LogoutSuccessHandlerImpl logoutSuccessHandler;
|
||||
/**
|
||||
* token认证过滤器
|
||||
*/
|
||||
@Resource
|
||||
private AuthenticationTokenFilter authenticationTokenFilter;
|
||||
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
|
||||
httpSecurity
|
||||
// CSRF禁用,因为不使用session
|
||||
.csrf().disable()
|
||||
// 认证失败处理类
|
||||
.exceptionHandling().authenticationEntryPoint(authenticationHandler)
|
||||
.accessDeniedHandler(authenticationHandler).and()
|
||||
// 基于token,所以不需要session
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
|
||||
// 过滤请求
|
||||
.authorizeRequests()
|
||||
.antMatchers(permitAllUrl.getUrls()).permitAll()
|
||||
.antMatchers(
|
||||
HttpMethod.GET,
|
||||
"/*.html",
|
||||
"/**/*.html",
|
||||
"/**/*.css",
|
||||
"/**/*.js"
|
||||
).permitAll()
|
||||
//除上面外的所有请求全部需要鉴权认证
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.headers().frameOptions().disable();
|
||||
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
|
||||
// 添加JWT filter
|
||||
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
// 添加CORS filter
|
||||
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
|
||||
httpSecurity.addFilterBefore(corsFilter, AuthenticationTokenFilter.class);
|
||||
return httpSecurity.build();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public BCryptPasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
|
||||
@Component
|
||||
@ConditionalOnMissingBean({UserDetailsService.class})
|
||||
public static class BaseUserDetailsService implements UserDetailsService {
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package cn.fateverse.common.security.configure;
|
||||
|
||||
import cn.fateverse.common.security.configure.properties.TaskThreadPoolProperties;
|
||||
import com.alibaba.cloud.nacos.NacosConfigManager;
|
||||
import com.alibaba.cloud.nacos.NacosConfigProperties;
|
||||
import com.alibaba.nacos.api.config.listener.Listener;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2023-08-03
|
||||
*/
|
||||
@EnableAsync
|
||||
@EnableConfigurationProperties({TaskThreadPoolProperties.class})
|
||||
public class TaskExecutePoolConfiguration implements InitializingBean {
|
||||
|
||||
|
||||
@Resource
|
||||
private TaskThreadPoolProperties properties;
|
||||
|
||||
private ThreadPoolTaskExecutor executor;
|
||||
|
||||
@Resource
|
||||
private NacosConfigManager nacosConfigManager;
|
||||
|
||||
@Resource
|
||||
private NacosConfigProperties nacosConfigProperties;
|
||||
|
||||
|
||||
@Bean({"fateverseExecutor","taskExecuteExecutor"})
|
||||
public ThreadPoolTaskExecutor taskExecuteExecutor() {
|
||||
return executor;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(properties.getCorePoolSize());
|
||||
executor.setMaxPoolSize(properties.getMaxPoolSize());
|
||||
executor.setQueueCapacity(properties.getQueueCapacity());
|
||||
executor.setKeepAliveSeconds(properties.getKeepAliveSeconds());
|
||||
executor.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("fateverse_%d").build());
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.initialize();
|
||||
Environment environment = nacosConfigProperties.getEnvironment();
|
||||
String active = environment.getProperty("spring.profiles.active");
|
||||
String applicationName = environment.getProperty("spring.application.name");
|
||||
String fileExtension = nacosConfigProperties.getFileExtension();
|
||||
nacosConfigManager.getConfigService().addListener(applicationName + "-" + active + "." + fileExtension, nacosConfigProperties.getGroup(),
|
||||
new Listener() {
|
||||
@Override
|
||||
public Executor getExecutor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveConfigInfo(String configInfo) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package cn.fateverse.common.security.configure;
|
||||
|
||||
import cn.fateverse.common.security.handle.MappingSwitchInterceptor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Jackson,处理受Swagger中@EnableWebMvc注解影响
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2023-05-05
|
||||
*/
|
||||
@Slf4j
|
||||
public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Resource
|
||||
private MappingSwitchInterceptor mappingSwitchInterceptor;
|
||||
|
||||
/**
|
||||
* @EnableWebMvc 使用该注解后,需要手动配置 addInterceptors() 和 addResourceHandlers()
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new HandlerInterceptor() {
|
||||
@Override
|
||||
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
|
||||
return HandlerInterceptor.super.preHandle(request, response, handler);
|
||||
}
|
||||
}).addPathPatterns("/**")
|
||||
.excludePathPatterns("/v2/**")
|
||||
.excludePathPatterns("/v3/**");
|
||||
registry.addInterceptor(mappingSwitchInterceptor).addPathPatterns("/**");
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充全局 objectMapper
|
||||
* <a href="https://stackoverflow.com/questions/45734108/spring-boot-not-using-configured-jackson-objectmapper-with-enablewebmvc">...</a>
|
||||
*
|
||||
* @param converters
|
||||
*/
|
||||
@Override
|
||||
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
converters.stream().filter(p -> p instanceof MappingJackson2HttpMessageConverter)
|
||||
.map(p -> (MappingJackson2HttpMessageConverter) p).forEach(p -> p.setObjectMapper(objectMapper));
|
||||
WebMvcConfigurer.super.extendMessageConverters(converters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.fateverse.common.security.configure.properties;
|
||||
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* demo开关配置
|
||||
*/
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "demo.switch")
|
||||
public class DemoSwitchProperties {
|
||||
|
||||
private Boolean enable = false;
|
||||
|
||||
|
||||
private Set<String> excludeIdentifier;
|
||||
|
||||
|
||||
public Boolean getEnable() {
|
||||
return enable;
|
||||
}
|
||||
|
||||
public void setEnable(Boolean enable) {
|
||||
this.enable = enable;
|
||||
}
|
||||
|
||||
public Set<String> getExcludeIdentifier() {
|
||||
return excludeIdentifier;
|
||||
}
|
||||
|
||||
public void setExcludeIdentifier(Set<String> excludeIdentifier) {
|
||||
this.excludeIdentifier = excludeIdentifier;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package cn.fateverse.common.security.configure.properties;
|
||||
|
||||
import cn.fateverse.common.security.annotation.Anonymity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.condition.RequestCondition;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2022/10/29
|
||||
*/
|
||||
@Slf4j
|
||||
@ConfigurationProperties(prefix = "security.auth.ignore")
|
||||
public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware {
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private static final String PATTERN = "\\{(.*?)\\}";
|
||||
|
||||
private static final String[] DEFAULT_IGNORE_URLS = new String[]{"/actuator/**", "/error", "/v3/api-docs", "/login", "/captchaImage"};
|
||||
|
||||
private final Set<String> urls = new HashSet<>();
|
||||
|
||||
private String mvcPath = "";
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
urls.addAll(Arrays.asList(DEFAULT_IGNORE_URLS));
|
||||
RequestMappingHandlerMapping mapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
|
||||
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
|
||||
|
||||
map.keySet().forEach(info -> {
|
||||
HandlerMethod handlerMethod = map.get(info);
|
||||
// 获取方法上边的注解 替代path variable 为 *
|
||||
Anonymity method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymity.class);
|
||||
if (method != null) {
|
||||
/**
|
||||
* todo 加入swagger后 info.getPathPatternsCondition()获取为null,
|
||||
* 但是getPatternsCondition()获取正常,所以替换获取数据的位置,
|
||||
* 同时替换掉下方的getPatternValues()方法为getPatterns()方法
|
||||
*/
|
||||
PatternsRequestCondition patternsCondition = info.getPatternsCondition();
|
||||
if (patternsCondition != null) {
|
||||
patternsCondition.getPatterns().forEach(url -> urls.add(mvcPath + url.replaceAll(PATTERN, "*")));
|
||||
}
|
||||
PathPatternsRequestCondition pathPatternsCondition = info.getPathPatternsCondition();
|
||||
if (pathPatternsCondition != null) {
|
||||
pathPatternsCondition.getPatternValues().forEach(url -> urls.add(mvcPath + url.replaceAll(PATTERN, "*")));
|
||||
}
|
||||
}
|
||||
//Optional.ofNullable(method).ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition())
|
||||
// .getPatternValues().forEach(url -> urls.add(url.replaceAll(PATTERN, "*"))));
|
||||
// 获取类上边的注解, 替代path variable 为 *
|
||||
Anonymity controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymity.class);
|
||||
if (controller != null) {
|
||||
RequestMapping requestMapping = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), RequestMapping.class);
|
||||
if (requestMapping != null) {
|
||||
String[] value = requestMapping.value();
|
||||
for (String path : value) {
|
||||
urls.add(mvcPath + path + "/**");
|
||||
urls.add(mvcPath + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
Environment environment = applicationContext.getEnvironment();
|
||||
String path = environment.getProperty("spring.mvc.servlet.path");
|
||||
if (!ObjectUtils.isEmpty(path) && !"/".equals(path)) {
|
||||
mvcPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getUrls() {
|
||||
return urls.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public void setUrls(List<String> urls) {
|
||||
this.urls.addAll(urls);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package cn.fateverse.common.security.configure.properties;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2023-08-03
|
||||
*/
|
||||
@RefreshScope
|
||||
@ConfigurationProperties("task.pool")
|
||||
public class TaskThreadPoolProperties {
|
||||
|
||||
private int corePoolSize;
|
||||
|
||||
private int maxPoolSize;
|
||||
|
||||
private int keepAliveSeconds;
|
||||
|
||||
private int queueCapacity;
|
||||
|
||||
public int getCorePoolSize() {
|
||||
return corePoolSize;
|
||||
}
|
||||
|
||||
public void setCorePoolSize(int corePoolSize) {
|
||||
this.corePoolSize = corePoolSize;
|
||||
}
|
||||
|
||||
public int getMaxPoolSize() {
|
||||
return maxPoolSize;
|
||||
}
|
||||
|
||||
public void setMaxPoolSize(int maxPoolSize) {
|
||||
this.maxPoolSize = maxPoolSize;
|
||||
}
|
||||
|
||||
public int getKeepAliveSeconds() {
|
||||
return keepAliveSeconds;
|
||||
}
|
||||
|
||||
public void setKeepAliveSeconds(int keepAliveSeconds) {
|
||||
this.keepAliveSeconds = keepAliveSeconds;
|
||||
}
|
||||
|
||||
public int getQueueCapacity() {
|
||||
return queueCapacity;
|
||||
}
|
||||
|
||||
public void setQueueCapacity(int queueCapacity) {
|
||||
this.queueCapacity = queueCapacity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package cn.fateverse.common.security.entity;
|
||||
|
||||
import cn.fateverse.admin.entity.User;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2022/10/27
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class LoginUser implements UserDetails {
|
||||
|
||||
/**
|
||||
* 用户唯一标识
|
||||
*/
|
||||
private String uuid;
|
||||
|
||||
/**
|
||||
* 登录时间
|
||||
*/
|
||||
private Long loginTime;
|
||||
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
private Long expireTime;
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
private User user;
|
||||
|
||||
/**
|
||||
* 登录ip
|
||||
*/
|
||||
private String ipddr;
|
||||
/**
|
||||
* 登录地点
|
||||
*/
|
||||
private String loginLocation;
|
||||
/**
|
||||
* 浏览器类型
|
||||
*/
|
||||
private String browser;
|
||||
/**
|
||||
* 操作系统
|
||||
*/
|
||||
private String os;
|
||||
|
||||
/**
|
||||
* 权限列表
|
||||
*/
|
||||
private Set<String> permissions;
|
||||
|
||||
|
||||
public LoginUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
List<String> roleList = new ArrayList<>();
|
||||
roleList.add("ROLE_ACTIVITI_USER");
|
||||
roleList.add("GROUP_activitiTeam");
|
||||
return roleList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return this.user.getPassword();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return this.user.getUserName();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 账户是否未过期,过期无法验证
|
||||
*/
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定用户是否解锁,锁定的用户无法进行身份验证
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 是否可用 ,禁用的用户不能身份验证
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package cn.fateverse.common.security.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
* MappingSwitch信息封装类
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2024/1/15 17:33
|
||||
*/
|
||||
@Data
|
||||
public class MappingSwitchInfo {
|
||||
|
||||
/**
|
||||
* key作为唯一编号
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* 应用名称
|
||||
*/
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 类名
|
||||
*/
|
||||
private String className;
|
||||
|
||||
/**
|
||||
* 方法名称
|
||||
*/
|
||||
private String methodName;
|
||||
|
||||
/**
|
||||
* 描述MappingSwitch注解的value可以为空
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* HandlerMethod中的uri
|
||||
*/
|
||||
private Set<String> uris;
|
||||
|
||||
/**
|
||||
* 当前开关类型
|
||||
*/
|
||||
private MappingSwitchType type;
|
||||
|
||||
/**
|
||||
* 当前方法请求类型
|
||||
*/
|
||||
private Set<String> httpMethods;
|
||||
|
||||
/**
|
||||
* 当前方法的状态,true为正常放行,false为关闭
|
||||
*/
|
||||
private Boolean state;
|
||||
|
||||
/**
|
||||
* 变更时间
|
||||
*/
|
||||
private Date operTime;
|
||||
/**
|
||||
* 操作人员
|
||||
*/
|
||||
private String operName;
|
||||
|
||||
|
||||
|
||||
public static class MappingSwitchConstant {
|
||||
/**
|
||||
* redis 的前缀
|
||||
*/
|
||||
public static String MAPPING_SWITCH = "mapping:switch:";
|
||||
}
|
||||
|
||||
|
||||
public static enum MappingSwitchType {
|
||||
/**
|
||||
* 类注解
|
||||
*/
|
||||
CLASS,
|
||||
|
||||
/**
|
||||
* 方法注解
|
||||
*/
|
||||
METHOD
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取redis key的方法
|
||||
* key 生成规则
|
||||
* 类注解: {前缀}:{applicationName}:{className}
|
||||
* 方法注解: {前缀}:{applicationName}:{className}:{methodStr}
|
||||
*
|
||||
* @param applicationName 应用名称
|
||||
* @param handlerMethod 请求方法
|
||||
* @param isMethod 是否为方法注解
|
||||
* @return key
|
||||
*/
|
||||
public static String getKey(String applicationName, HandlerMethod handlerMethod, Boolean isMethod) {
|
||||
String packageName = handlerMethod.getBeanType().getPackage().getName() + ".";
|
||||
String name = handlerMethod.getBeanType().getName();
|
||||
String className = name.replace(packageName, "");
|
||||
String methodStr = "";
|
||||
if (isMethod) {
|
||||
Method method = handlerMethod.getMethod();
|
||||
StringJoiner joiner = new StringJoiner(", ", "(", ")");
|
||||
for (Class<?> paramType : method.getParameterTypes()) {
|
||||
joiner.add(paramType.getSimpleName());
|
||||
}
|
||||
methodStr = ":" + method.getName() + joiner;
|
||||
}
|
||||
return MappingSwitchInfo.MappingSwitchConstant.MAPPING_SWITCH + applicationName + ":" + className + methodStr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.fateverse.common.security.filter;
|
||||
|
||||
import cn.fateverse.common.core.utils.ObjectUtils;
|
||||
import cn.fateverse.common.security.service.TokenService;
|
||||
import cn.fateverse.common.security.entity.LoginUser;
|
||||
import cn.fateverse.common.security.utils.SecurityUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2022/10/27
|
||||
*/
|
||||
@Slf4j
|
||||
public class AuthenticationTokenFilter extends OncePerRequestFilter {
|
||||
@Resource
|
||||
private TokenService tokenService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
String token = tokenService.getToken(request);
|
||||
if (!ObjectUtils.isEmpty(token)) {
|
||||
LoginUser loginUser = tokenService.getLoginUser(token);
|
||||
log.info("接口 : {}-{},被请求", request.getMethod(), request.getRequestURI());
|
||||
if (!ObjectUtils.isEmpty(loginUser) && ObjectUtils.isEmpty(SecurityUtils.getAuthentication())) {
|
||||
tokenService.verifyToken(loginUser);
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
|
||||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package cn.fateverse.common.security.handle;
|
||||
|
||||
import cn.hutool.core.text.StrFormatter;
|
||||
import cn.fateverse.common.core.result.Result;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 认证失败处理类 返回未授权
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/27
|
||||
*/
|
||||
public class AuthenticationEntryPointImpl implements AccessDeniedHandler, AuthenticationEntryPoint, Serializable {
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
|
||||
accessDenied(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
|
||||
accessDenied(request, response);
|
||||
}
|
||||
|
||||
|
||||
public void accessDenied(HttpServletRequest request, HttpServletResponse response) {
|
||||
String msg = StrFormatter.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
|
||||
renderString(response,Result.unauthorized(msg));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将字符串渲染到客户端
|
||||
*
|
||||
* @param response 渲染对象
|
||||
* @param result 返回的错误对象
|
||||
*/
|
||||
public static void renderString(HttpServletResponse response, Result<String> result) {
|
||||
try {
|
||||
response.setStatus(result.getStatus().value());
|
||||
response.setContentType("application/json");
|
||||
response.setCharacterEncoding("utf-8");
|
||||
response.getWriter().print(JSON.toJSONString(result));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package cn.fateverse.common.security.handle;
|
||||
|
||||
|
||||
import cn.fateverse.common.core.enums.ResultEnum;
|
||||
import cn.fateverse.common.core.exception.CustomException;
|
||||
import cn.fateverse.common.core.exception.UserPasswordNotMatchException;
|
||||
import cn.fateverse.common.core.result.Result;
|
||||
import cn.fateverse.common.core.utils.ObjectUtils;
|
||||
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
|
||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
|
||||
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.SignatureException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/30
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
public GlobalExceptionHandler() {
|
||||
log.info("开始初始化全局异常处理器");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* /**
|
||||
* 业务异常
|
||||
*/
|
||||
@ExceptionHandler(CustomException.class)
|
||||
public Result<String> businessException(CustomException e) {
|
||||
if (ObjectUtils.isEmpty(e.getCode())) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
return Result.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径不存在
|
||||
*
|
||||
* @param e
|
||||
* @return
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public Result<Object> handlerNoFoundException(Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.notFound( "路径不存在,请检查路径是否正确");
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权失败
|
||||
*
|
||||
* @param e
|
||||
* @return
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public Result<String> handleAuthorizationException(AccessDeniedException e) {
|
||||
log.error(e.getMessage());
|
||||
return Result.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccountExpiredException.class)
|
||||
public Result<String> handleAccountExpiredException(AccountExpiredException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(UsernameNotFoundException.class)
|
||||
public Result<String> handleUsernameNotFoundException(UsernameNotFoundException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(UserPasswordNotMatchException.class)
|
||||
public Result<String> handleUserPasswordNotMatchException(UserPasswordNotMatchException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.error("用户名密码错误!");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 自定义验证异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public Result<String> validatedBindException(BindException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
String message = e.getAllErrors().get(0).getDefaultMessage();
|
||||
return Result.error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<String> validExceptionHandler(MethodArgumentNotValidException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
String message = e.getBindingResult().getFieldError().getDefaultMessage();
|
||||
return Result.error(message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(SignatureException.class)
|
||||
public Result<String> jwtExceptionHandler(JwtException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.unauthorized("token解析失败,认证失败,无法访问系统资源");
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public Result<String> runtimeExceptionHandler(RuntimeException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
if (e instanceof UndeclaredThrowableException) {
|
||||
return sentinelExceptionHandler(e);
|
||||
}
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
//@ExceptionHandler(Exception.class)
|
||||
//public Result<String> handleException(Exception e) {
|
||||
// log.error(e.getMessage(), e);
|
||||
// return Result.error(ResultConstant.SYS_ERROR);
|
||||
//}
|
||||
|
||||
private Result<String> sentinelExceptionHandler(RuntimeException undeclared) {
|
||||
Throwable throwable = ((UndeclaredThrowableException) undeclared).getUndeclaredThrowable();
|
||||
Result<String> result = null;
|
||||
if (throwable instanceof FlowException) {
|
||||
result = Result.error(ResultEnum.SENTINEL_FLOW.status, ResultEnum.SENTINEL_FLOW.msg);
|
||||
} else if (throwable instanceof ParamFlowException) {
|
||||
result = Result.error(ResultEnum.SENTINEL_PARAM_FLOW.status, ResultEnum.SENTINEL_PARAM_FLOW.msg);
|
||||
} else if (throwable instanceof DegradeException) {
|
||||
result = Result.error(ResultEnum.SENTINEL_DEGRADE.status, ResultEnum.SENTINEL_DEGRADE.msg);
|
||||
} else if (throwable instanceof SystemBlockException) {
|
||||
result = Result.error(ResultEnum.SENTINEL_SYSTEM.status, ResultEnum.SENTINEL_SYSTEM.msg);
|
||||
} else if (throwable instanceof AuthorityException) {
|
||||
result = Result.error(ResultEnum.SENTINEL_AUTHORITY.status, ResultEnum.SENTINEL_AUTHORITY.msg);
|
||||
} else {
|
||||
return Result.error(throwable.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.fateverse.common.security.handle;
|
||||
|
||||
import cn.fateverse.common.security.service.TokenService;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2022/10/27
|
||||
*/
|
||||
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
|
||||
|
||||
private final TokenService tokenService;
|
||||
|
||||
public LogoutSuccessHandlerImpl(TokenService tokenService) {
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
String token = tokenService.getToken();
|
||||
tokenService.delLoginUser(token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package cn.fateverse.common.security.handle;
|
||||
|
||||
import cn.fateverse.common.security.annotation.MappingSwitch;
|
||||
import cn.fateverse.common.security.entity.MappingSwitchInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* MappingSwitch注解拦截器
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2024/1/15 18:08
|
||||
*/
|
||||
@Slf4j
|
||||
public class MappingSwitchInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Resource
|
||||
private Environment environment;
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, MappingSwitchInfo> redisTemplate;
|
||||
|
||||
private String applicationName;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
String applicationName = environment.getProperty("spring.application.name");
|
||||
if (ObjectUtils.isEmpty(applicationName)) {
|
||||
log.error("applicationName can not be null");
|
||||
throw new RuntimeException("applicationName can not be null");
|
||||
}
|
||||
this.applicationName = applicationName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
//判断当前方法是否为HandlerMethod
|
||||
if (handler instanceof HandlerMethod) {
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
//判断当前方法上是否有MappingSwitch注解
|
||||
MappingSwitch method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), MappingSwitch.class);
|
||||
if (method != null) {
|
||||
checkMethodState(handlerMethod, Boolean.TRUE);
|
||||
}
|
||||
//判断Controller类上是否有MappingSwitch注解
|
||||
MappingSwitch controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), MappingSwitch.class);
|
||||
if (controller != null) {
|
||||
checkMethodState(handlerMethod, Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
return HandlerInterceptor.super.preHandle(request, response, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查方法的装填
|
||||
*
|
||||
* @param handlerMethod 方法对象
|
||||
* @param isMethod 是否为方法, true为方法注解,false为Controller类注解
|
||||
*/
|
||||
private void checkMethodState(HandlerMethod handlerMethod, Boolean isMethod) {
|
||||
MappingSwitchInfo mappingSwitchInfo = redisTemplate.opsForValue().get(MappingSwitchInfo.getKey(applicationName, handlerMethod, isMethod));
|
||||
if (mappingSwitchInfo != null && !mappingSwitchInfo.getState()) {
|
||||
throw new RuntimeException("当前接口关闭,请稍后再试");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.fateverse.common.security.handle;
|
||||
|
||||
import cn.fateverse.common.core.result.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
@ConditionalOnClass(WebMvcAutoConfiguration.class)
|
||||
public class ResultResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
public ResultResponseAdvice() {
|
||||
log.info("开始加载返回全局拦截器");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
|
||||
if (body instanceof Result) {
|
||||
Result<Object> result = (Result<Object>) body;
|
||||
response.setStatusCode(result.getStatus());
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package cn.fateverse.common.security.service;
|
||||
|
||||
|
||||
import cn.fateverse.common.security.configure.properties.DemoSwitchProperties;
|
||||
import cn.fateverse.common.security.utils.SecurityUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2022/10/27
|
||||
*/
|
||||
public class PermissionService {
|
||||
|
||||
@Resource
|
||||
private DemoSwitchProperties properties;
|
||||
|
||||
|
||||
/**
|
||||
* 所有权限标识
|
||||
*/
|
||||
private static final String ALL_PERMISSION = "*:*:*";
|
||||
|
||||
/**
|
||||
* 自定义鉴权方法
|
||||
*
|
||||
* @param permission
|
||||
* @return
|
||||
*/
|
||||
public boolean hasPermission(String permission) {
|
||||
if (ObjectUtils.isEmpty(permission)) {
|
||||
return false;
|
||||
}
|
||||
checkDemoSwitch(permission);
|
||||
Set<String> permissions = SecurityUtils.getPermissions();
|
||||
if (CollectionUtils.isEmpty(permissions)) {
|
||||
return false;
|
||||
}
|
||||
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
|
||||
}
|
||||
|
||||
|
||||
private void checkDemoSwitch(String permission) {
|
||||
if (!properties.getEnable()) {
|
||||
return;
|
||||
}
|
||||
if (permission.endsWith(":del")) {
|
||||
throw new RuntimeException("演示模式,不允许删除!");
|
||||
}
|
||||
if (properties.getExcludeIdentifier() != null && properties.getExcludeIdentifier().contains(permission)) {
|
||||
throw new RuntimeException("演示模式,不允许操作!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package cn.fateverse.common.security.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.fateverse.common.core.constant.CacheConstants;
|
||||
import cn.fateverse.common.core.constant.Constants;
|
||||
import cn.fateverse.common.core.exception.CustomException;
|
||||
import cn.fateverse.common.core.utils.HttpServletUtils;
|
||||
import cn.fateverse.common.core.utils.ObjectUtils;
|
||||
import cn.fateverse.common.security.entity.LoginUser;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* token验证处理
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/27
|
||||
*/
|
||||
@Slf4j
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(prefix = "token")
|
||||
public class TokenService {
|
||||
|
||||
/**
|
||||
* 令牌自定义标识
|
||||
*/
|
||||
private String header;
|
||||
|
||||
/**
|
||||
* 令牌秘钥
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* 令牌有效期(默认30分钟)
|
||||
*/
|
||||
private long expireTime;
|
||||
|
||||
protected static final long MILLIS_SECOND = 1000;
|
||||
|
||||
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
|
||||
|
||||
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
|
||||
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, LoginUser> redisTemplate;
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户身份信息
|
||||
*
|
||||
* @return 用户信息
|
||||
*/
|
||||
public LoginUser getLoginUser(HttpServletRequest request) {
|
||||
// 获取请求携带的令牌
|
||||
String token = getToken(request);
|
||||
return getLoginUser(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
public void userLogout() {
|
||||
delLoginUser(getToken());
|
||||
}
|
||||
|
||||
public LoginUser getLoginUser(String token) {
|
||||
if (null != token && !StrUtil.isEmpty(token)) {
|
||||
Claims claims = parseToken(token);
|
||||
if (null == claims) {
|
||||
return null;
|
||||
}
|
||||
// 解析对应的权限以及用户信息
|
||||
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
|
||||
return getLoginUserUUid(uuid);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户uuid
|
||||
*
|
||||
* @param uuid uuid
|
||||
* @return 用户信息
|
||||
*/
|
||||
public LoginUser getLoginUserUUid(String uuid) {
|
||||
String userKey = getTokenKey(uuid);
|
||||
return redisTemplate.opsForValue().get(userKey);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置用户身份信息
|
||||
*/
|
||||
public void setLoginUser(LoginUser loginUser) {
|
||||
if (!ObjectUtils.isEmpty(loginUser) && !StrUtil.isEmpty(loginUser.getUuid())) {
|
||||
refreshToken(loginUser);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户身份信息
|
||||
*/
|
||||
public void delLoginUser(String token) {
|
||||
if (!StrUtil.isEmpty(token)) {
|
||||
String userKey = getTokenKey(token);
|
||||
redisTemplate.delete(userKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建令牌
|
||||
*
|
||||
* @param loginUser 用户信息
|
||||
* @return 令牌
|
||||
*/
|
||||
public String createToken(LoginUser loginUser) {
|
||||
refreshToken(loginUser);
|
||||
Map<String, Object> claims = new HashMap<>(1);
|
||||
claims.put(Constants.LOGIN_USER_KEY, loginUser.getUuid());
|
||||
return createToken(claims);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
|
||||
*
|
||||
* @param loginUser 用户信息
|
||||
*/
|
||||
public void verifyToken(LoginUser loginUser) {
|
||||
long expireTime = loginUser.getExpireTime();
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
|
||||
refreshToken(loginUser);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌有效期
|
||||
*
|
||||
* @param loginUser 登录信息
|
||||
*/
|
||||
public void refreshToken(LoginUser loginUser) {
|
||||
loginUser.setLoginTime(System.currentTimeMillis());
|
||||
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
|
||||
// 根据uuid将loginUser缓存
|
||||
String userKey = getTokenKey(loginUser.getUuid());
|
||||
redisTemplate.opsForValue().set(userKey, loginUser, expireTime, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从数据声明生成令牌
|
||||
*
|
||||
* @param claims 数据声明
|
||||
* @return 令牌
|
||||
*/
|
||||
private String createToken(Map<String, Object> claims) {
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.signWith(SignatureAlgorithm.HS512, secret).compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从令牌中获取数据声明
|
||||
*
|
||||
* @param token 令牌
|
||||
* @return 数据声明
|
||||
*/
|
||||
private Claims parseToken(String token) {
|
||||
try {
|
||||
return Jwts.parser()
|
||||
.setSigningKey(secret)
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new CustomException("token过期", 401);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取请求token
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return token 令牌信息
|
||||
*/
|
||||
public String getToken(HttpServletRequest request) {
|
||||
String token = request.getHeader(header);
|
||||
if (!StrUtil.isEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
|
||||
token = token.replace(Constants.TOKEN_PREFIX, "");
|
||||
}
|
||||
if ("null".equals(token)) {
|
||||
return null;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取token
|
||||
*
|
||||
* @return 令牌信息
|
||||
*/
|
||||
public String getToken() {
|
||||
return getToken(getRequest());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取request请求体
|
||||
*
|
||||
* @return 请求体
|
||||
*/
|
||||
public HttpServletRequest getRequest() {
|
||||
return HttpServletUtils.getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* token在获取header中的名称
|
||||
*
|
||||
* @return 获取header的名称
|
||||
*/
|
||||
public String getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接用户信息在redis中的key
|
||||
*
|
||||
* @param uuid uuid
|
||||
* @return 获取到令牌在redis中的key
|
||||
*/
|
||||
private String getTokenKey(String uuid) {
|
||||
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
|
||||
}
|
||||
|
||||
|
||||
public void setHeader(String header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
public void setSecret(String secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
public void setExpireTime(long expireTime) {
|
||||
this.expireTime = expireTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package cn.fateverse.common.security.utils;
|
||||
|
||||
import cn.fateverse.common.core.exception.CustomException;
|
||||
import cn.fateverse.common.core.utils.HttpServletUtils;
|
||||
import cn.fateverse.common.security.entity.LoginUser;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 安全服务工具类
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/29
|
||||
*/
|
||||
public class SecurityUtils {
|
||||
|
||||
public static Long getUserId() {
|
||||
try {
|
||||
return getLoginUser().getUser().getUserId();
|
||||
} catch (Exception e) {
|
||||
throw new CustomException("获取用户账户异常", HttpStatus.UNAUTHORIZED.value());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取到用户权限信息
|
||||
*
|
||||
* @return 用户权限信息
|
||||
*/
|
||||
public static Set<String> getPermissions() {
|
||||
try {
|
||||
return Objects.requireNonNull(getLoginUser()).getPermissions();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户名
|
||||
*
|
||||
* @return 用户名
|
||||
*/
|
||||
public static String getUsername() {
|
||||
try {
|
||||
return getLoginUser().getUsername();
|
||||
} catch (Exception e) {
|
||||
throw new CustomException("获取用户账户异常", HttpStatus.UNAUTHORIZED.value());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录用户信息
|
||||
*
|
||||
* @return 登录用户信息
|
||||
*/
|
||||
public static LoginUser getLoginUser() {
|
||||
try {
|
||||
return (LoginUser) getAuthentication().getPrincipal();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前线程Authentication
|
||||
*
|
||||
* @return 授权对象
|
||||
*/
|
||||
public static Authentication getAuthentication() {
|
||||
return SecurityContextHolder.getContext().getAuthentication();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成BCryptPasswordEncoder密码
|
||||
*
|
||||
* @param password 密码
|
||||
* @return 加密字符串
|
||||
*/
|
||||
public static String encryptPassword(String password) {
|
||||
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
return passwordEncoder.encode(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断密码是否相同
|
||||
*
|
||||
* @param rawPassword 真实密码
|
||||
* @param encodedPassword 加密后字符
|
||||
* @return 结果
|
||||
*/
|
||||
public static boolean matchesPassword(String rawPassword, String encodedPassword) {
|
||||
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
return passwordEncoder.matches(rawPassword, encodedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* todo 是否为管理员,还需要进行角色的校验
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 结果
|
||||
*/
|
||||
public static boolean isAdmin(Long userId) {
|
||||
return userId != null && 1L == userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为管理员
|
||||
*
|
||||
* @return 管理状态
|
||||
*/
|
||||
public static boolean isAdmin() {
|
||||
return isAdmin(getUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取http响应对象
|
||||
*
|
||||
* @return response
|
||||
*/
|
||||
public static HttpServletResponse getResponse() {
|
||||
return HttpServletUtils.getResponse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
cn.fateverse.common.security.configure.SecurityCloudConfiguration
|
||||
cn.fateverse.common.security.configure.CorsFilterConfiguration
|
||||
cn.fateverse.common.security.configure.WebMvcConfiguration
|
||||
Reference in New Issue
Block a user