This commit is contained in:
clay
2024-03-06 17:44:09 +08:00
commit adaec0eadd
1493 changed files with 219939 additions and 0 deletions

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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("当前接口关闭,请稍后再试");
}
}
}

View File

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

View File

@@ -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("演示模式,不允许操作!");
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
cn.fateverse.common.security.configure.SecurityCloudConfiguration
cn.fateverse.common.security.configure.CorsFilterConfiguration
cn.fateverse.common.security.configure.WebMvcConfiguration