commit e2c4a2405ea6a202795b31b0b7bb3c01cd7d7d1b
Author: wangxiang <1827945911@qq.com>
Date: Sat Sep 16 15:20:09 2023 +0800
init: oot
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..6b2a714
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,13 @@
+
+
+ * cas系统枚举 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/16 + */ +@Getter +@RequiredArgsConstructor +public enum CasSystemEnum { + + /** + * sso认证系统 + */ + KICC("KICC", "主kicc系统"), + + /** + * 子系统1 + */ + KICS("KICS", "子系统1"), + + /** + * 子系统2 + */ + KLAB("KLAB", "子系统2"); + + /** + * 名称 + */ + private final String name; + + /** + * 描述 + */ + private final String description; + +} diff --git a/src/main/java/com/cloud/kicc/common/core/enums/ExceptionEnum.java b/src/main/java/com/cloud/kicc/common/core/enums/ExceptionEnum.java new file mode 100644 index 0000000..48f31df --- /dev/null +++ b/src/main/java/com/cloud/kicc/common/core/enums/ExceptionEnum.java @@ -0,0 +1,35 @@ +package com.cloud.kicc.common.core.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + *+ * API错误页面响应状态枚举 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/16 + */ +@Getter +@RequiredArgsConstructor +public enum ExceptionEnum { + + UNAUTHORIZED_ACCESS(401, "禁止访问"), + PAGE_NOT_ACCESS(403, "页面无法访问"), + PAGE_NOT_FOUND(404, "网页未找到"), + ERROR(500, "错误"), + NET_WORK_ERROR(10000, "前端Js错误"), + PAGE_NOT_DATA(10100, "无数据页面"); + + /** + * 状态 + */ + private final int value; + + /** + * 描述 + */ + private final String description; + +} diff --git a/src/main/java/com/cloud/kicc/common/data/entity/CasUser.java b/src/main/java/com/cloud/kicc/common/data/entity/CasUser.java new file mode 100644 index 0000000..1ec8cc8 --- /dev/null +++ b/src/main/java/com/cloud/kicc/common/data/entity/CasUser.java @@ -0,0 +1,186 @@ +package com.cloud.kicc.common.data.entity; + +import com.cloud.kicc.common.core.enums.CasSystemEnum; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + *+ * CAS统一认证用户数据 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/16 + */ +@Setter +@Getter +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +public class CasUser extends User { + + private static final long serialVersionUID = 1L; + + /** 用户ID */ + private String id; + + /** 昵称 */ + private String nickName; + + /** 邮箱 */ + private String email; + + /** 手机号 */ + private String phone; + + /** 性别 */ + private String sex; + + /** 头像地址 */ + private String avatar; + + /** 最后登陆ip */ + private String loginIp; + + /** 最后登陆时间 */ + private LocalDateTime loginTime; + + /** 状态 */ + private String ssoStatus; + + /** 创建ID */ + private String ssoCreateById; + + /** 创建人 */ + private String ssoCreateByName; + + /** 创建时间 */ + private LocalDateTime ssoCreateTime; + + /** 更新ID */ + private String ssoUpdateById; + + /** 更新人 */ + private String ssoUpdateByName; + + /** 更新时间 */ + private LocalDateTime ssoUpdateTime; + + /** 备注 */ + private String remarks; + + /** 角色ID */ + private String roleId; + + /** 多租户ID */ + private String tenantId; + + /** sso扩展信息 */ + private Map+ * 扩展资源服务注解 + * 添加方法安全级别 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@Documented +@Inherited +@EnableResourceServer +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@EnableGlobalMethodSecurity(prePostEnabled = true) +@Import({ ResourceServerAutoConfiguration.class, KiccSecurityBeanDefinitionRegistrar.class }) +public @interface EnableKiccResourceServer { + +} diff --git a/src/main/java/com/klab/security/common/annotation/Inner.java b/src/main/java/com/klab/security/common/annotation/Inner.java new file mode 100644 index 0000000..cac2721 --- /dev/null +++ b/src/main/java/com/klab/security/common/annotation/Inner.java @@ -0,0 +1,30 @@ +package com.klab.security.common.annotation; + +import java.lang.annotation.*; + +/** + *+ * 服务调用不鉴权注解 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Inner { + + /** + * 是否AOP统一处理 + * @return false, true + */ + boolean value() default true; + + /** + * 为后续扩展做准备,需要特殊判空的字段(预留) + * @return {} + */ + String[] field() default {}; + +} diff --git a/src/main/java/com/klab/security/common/aspect/SecurityInnerAspect.java b/src/main/java/com/klab/security/common/aspect/SecurityInnerAspect.java new file mode 100644 index 0000000..59b5727 --- /dev/null +++ b/src/main/java/com/klab/security/common/aspect/SecurityInnerAspect.java @@ -0,0 +1,59 @@ +package com.klab.security.common.aspect; + +import cn.hutool.core.util.StrUtil; +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.klab.security.common.annotation.Inner; +import lombok.RequiredArgsConstructor; +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.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.security.access.AccessDeniedException; + +import javax.servlet.http.HttpServletRequest; + +/** + *+ * 内部接口调用注解切面处理 + * 设置是否是内部接口,不提供给外部访问 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@Slf4j +@Aspect +@Configuration +@RequiredArgsConstructor + +public class SecurityInnerAspect implements Ordered { + + private final HttpServletRequest request; + + @SneakyThrows + @Around("@within(inner) || @annotation(inner)") + public Object around(ProceedingJoinPoint point, Inner inner) { + // 实际注入的inner实体由表达式后一个注解决定,即是方法上的@Inner注解实体,若方法上无@Inner注解,则获取类上的 + if (inner == null) { + Class> clazz = point.getTarget().getClass(); + inner = AnnotationUtils.findAnnotation(clazz, Inner.class); + } + String header = request.getHeader(SecurityConstants.FROM); + // 设置是否是内部接口,不提供给外部访问 + if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) { + log.warn("访问接口 {} 没有权限", point.getSignature().getName()); + throw new AccessDeniedException("Access is denied"); + } + return point.proceed(); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 1; + } + +} diff --git a/src/main/java/com/klab/security/common/config/ResourceServerAutoConfiguration.java b/src/main/java/com/klab/security/common/config/ResourceServerAutoConfiguration.java new file mode 100644 index 0000000..1a5d82e --- /dev/null +++ b/src/main/java/com/klab/security/common/config/ResourceServerAutoConfiguration.java @@ -0,0 +1,50 @@ +package com.klab.security.common.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.klab.security.common.exp.*; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; +import org.springframework.security.oauth2.provider.token.TokenStore; + +/** + *+ * 扩展资源服务器自动配置 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@Configuration +@EnableConfigurationProperties(PermitAllUrlProperties.class) +public class ResourceServerAutoConfiguration { + + /** 校验接口是否存在权限 */ + @Bean("pms") + public PermissionService permissionService() { + return new PermissionService(); + } + + /** 对公开权限的请求获取token不进行校验 */ + @Bean + public KiccBearerTokenExtractor kiccBearerTokenExtractor(PermitAllUrlProperties urlProperties) { + return new KiccBearerTokenExtractor(urlProperties); + } + + /** 细粒化客户端认证异常处理 */ + @Bean + public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint(ObjectMapper objectMapper) { + return new ResourceAuthExceptionEntryPoint(objectMapper); + } + + /** 扩展资源服务器令牌服务 */ + @Bean + @Primary + public ResourceServerTokenServices resourceServerTokenServices(TokenStore tokenStore, JdbcTemplate jdbcTemplate) { + return new KiccLocalResourceServerTokenServices(tokenStore, jdbcTemplate); + } + +} diff --git a/src/main/java/com/klab/security/common/config/SecurityMessageSourceConfiguration.java b/src/main/java/com/klab/security/common/config/SecurityMessageSourceConfiguration.java new file mode 100644 index 0000000..bef71d9 --- /dev/null +++ b/src/main/java/com/klab/security/common/config/SecurityMessageSourceConfiguration.java @@ -0,0 +1,31 @@ +package com.klab.security.common.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET; + +/** + *+ * 设置安全模块国际化配置 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@Configuration +@ConditionalOnWebApplication(type = SERVLET) +public class SecurityMessageSourceConfiguration implements WebMvcConfigurer { + + @Bean + public MessageSource messageSource() { + ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); + messageSource.addBasenames("classpath:org/springframework/security/messages"); + return messageSource; + } + +} diff --git a/src/main/java/com/klab/security/common/config/TokenStoreAutoConfiguration.java b/src/main/java/com/klab/security/common/config/TokenStoreAutoConfiguration.java new file mode 100644 index 0000000..bc19e9e --- /dev/null +++ b/src/main/java/com/klab/security/common/config/TokenStoreAutoConfiguration.java @@ -0,0 +1,28 @@ +package com.klab.security.common.config; + +import com.cloud.kicc.common.core.constant.CacheConstants; +import com.klab.security.common.override.KiccRedisTokenStore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.security.oauth2.provider.token.TokenStore; + +/** + *+ * 令牌存储自动配置 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@Configuration +public class TokenStoreAutoConfiguration { + + @Bean + public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) { + KiccRedisTokenStore tokenStore = new KiccRedisTokenStore(redisConnectionFactory); + tokenStore.setPrefix(CacheConstants.OAUTH_ACCESS); + return tokenStore; + } + +} diff --git a/src/main/java/com/klab/security/common/constant/SecurityConstants.java b/src/main/java/com/klab/security/common/constant/SecurityConstants.java new file mode 100644 index 0000000..76de600 --- /dev/null +++ b/src/main/java/com/klab/security/common/constant/SecurityConstants.java @@ -0,0 +1,145 @@ +package com.klab.security.common.constant; + +/** + *+ * 安全常量 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/29 + */ +public interface SecurityConstants { + + /** + * 角色前缀 + */ + String ROLE = "ROLE_"; + + /** + * 子系统sso角色权限查询声明 + */ + String SSO_PERMISSION_FIND_STATEMENT = "select distinct\n" + + "\tmenu_code \n" + + "from\n" + + "\tt_sys_role_menu sr\n" + + "\tleft join t_sys_menu sm \n" + + "\ton sr.menu_id = sm.menu_id\n" + + "where sm.state = 1 and sr.role_id in (select role_id from t_sys_role_user where user_id = ?) and coalesce(menu_code, '') <> ''"; + + String SSO_USER_FIND_STATEMENT = "select * from sso_enhanced_user_view where cas_user_id = ? and state = 1"; + + String SSO_USER_FIND_STATEMENT_BY_USER_ID = "select * from sso_enhanced_user_view where user_id = ? and state = 1"; + + /** + * 项目的license + */ + String PROJECT_LICENSE = "长沙康来生物有限公司"; + + /** + * 内部接口调用密钥 + */ + String FROM_IN = "kG8qA6qG1aP5aR3g"; + + /** + * 内部接口调用Key标志 + */ + String FROM = "from"; + + /** + * 请求header + */ + String HEADER_FROM_IN = FROM + "=" + FROM_IN; + + /** + * 默认登录URL + */ + String OAUTH_TOKEN_URL = "/oauth/token"; + + /** + * grant_type + */ + String REFRESH_TOKEN = "refresh_token"; + + /** + * 手机号登录 + */ + String APP = "app"; + + /** + * {bcrypt} 加密的特征码 + */ + String BCRYPT = "{bcrypt}"; + + /** + * sys_oauth_client_details 表的字段,不包括client_id、client_secret + */ + String CLIENT_FIELDS = "client_id, CONCAT('{noop}',client_secret) as client_secret, resource_ids, scope, " + + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, " + + "refresh_token_validity, additional_information, autoapprove"; + + /** + * JdbcClientDetailsService 查询语句 + */ + String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS + " from sys_oauth_client_details"; + + /** + * 默认的查询语句 + */ + String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id"; + + /** + * 按条件client_id 查询 + */ + String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?"; + + /*** + * 资源服务器默认bean名称 + */ + String RESOURCE_SERVER_CONFIGURER = "resourceServerConfigurerAdapter"; + + /** + * 用户信息 + */ + String DETAILS_USER = "user_info"; + + /** + * 协议字段 + */ + String DETAILS_LICENSE = "license"; + + /** + * 验证码有效期,默认 60秒 + */ + long CODE_TIME = 60; + + /** + * 手机验证码长度 + */ + String PHONE_CODE_SIZE = "6"; + + /** + * 客户端模式 + */ + String CLIENT_CREDENTIALS = "client_credentials"; + + /** + * 客户端ID + */ + String CLIENT_ID = "clientId"; + + /** + * 绑定kicc康来内部多租户 + */ + String TENANT_ID = "1510456530575347712"; + + /** + * 模拟测试账户 + */ + String MOCK_USERNAME = "admin"; + + /** + * 模拟测试密码 + */ + String MOCK_PASSWORD = "kanglai123"; + +} diff --git a/src/main/java/com/klab/security/common/exception/KiccAuth2Exception.java b/src/main/java/com/klab/security/common/exception/KiccAuth2Exception.java new file mode 100644 index 0000000..a93a11b --- /dev/null +++ b/src/main/java/com/klab/security/common/exception/KiccAuth2Exception.java @@ -0,0 +1,33 @@ +package com.klab.security.common.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.Getter; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; + +/** + *+ * 自定义OAuth2Exception + *
+ * + * @Author: wangxiang4 + * @Date: 2022/2/17 + */ +public class KiccAuth2Exception extends OAuth2Exception { + + @Getter + private String errorCode; + + public KiccAuth2Exception(String msg) { + super(msg); + } + + public KiccAuth2Exception(String msg, Throwable t) { + super(msg, t); + } + + public KiccAuth2Exception(String msg, String errorCode) { + super(msg); + this.errorCode = errorCode; + } + +} diff --git a/src/main/java/com/klab/security/common/exception/UnConfiguredUserDataException.java b/src/main/java/com/klab/security/common/exception/UnConfiguredUserDataException.java new file mode 100644 index 0000000..ce9eeb8 --- /dev/null +++ b/src/main/java/com/klab/security/common/exception/UnConfiguredUserDataException.java @@ -0,0 +1,33 @@ +package com.klab.security.common.exception; + +import org.springframework.http.HttpStatus; + +/** + *+ * 未配置用户数据 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/9/2 + */ +public class UnConfiguredUserDataException extends KiccAuth2Exception { + + public UnConfiguredUserDataException(String msg) { + super(msg); + } + + public UnConfiguredUserDataException(String msg, Throwable t) { + super(msg, t); + } + + @Override + public String getOAuth2ErrorCode() { + return "un_configured_user_data"; + } + + @Override + public int getHttpErrorCode() { + return HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value(); + } + +} diff --git a/src/main/java/com/klab/security/common/exp/KiccBearerTokenExtractor.java b/src/main/java/com/klab/security/common/exp/KiccBearerTokenExtractor.java new file mode 100644 index 0000000..19b34c9 --- /dev/null +++ b/src/main/java/com/klab/security/common/exp/KiccBearerTokenExtractor.java @@ -0,0 +1,38 @@ +package com.klab.security.common.exp; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; + +import javax.servlet.http.HttpServletRequest; + +/** + *+ * 重写 {@link BearerTokenExtractor} + * 对公开权限的请求不进行抓取校验直接返回null放过 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +public class KiccBearerTokenExtractor extends BearerTokenExtractor { + + private final PathMatcher pathMatcher; + + private final PermitAllUrlProperties urlProperties; + + public KiccBearerTokenExtractor(PermitAllUrlProperties urlProperties) { + this.urlProperties = urlProperties; + this.pathMatcher = new AntPathMatcher(); + } + + @Override + public Authentication extract(HttpServletRequest request) { + boolean match = urlProperties.getUrls().stream() + .anyMatch(url -> pathMatcher.match(url, request.getRequestURI())); + + return match ? null : super.extract(request); + } + +} diff --git a/src/main/java/com/klab/security/common/exp/KiccLocalResourceServerTokenServices.java b/src/main/java/com/klab/security/common/exp/KiccLocalResourceServerTokenServices.java new file mode 100644 index 0000000..99979e5 --- /dev/null +++ b/src/main/java/com/klab/security/common/exp/KiccLocalResourceServerTokenServices.java @@ -0,0 +1,118 @@ +package com.klab.security.common.exp; + +import cn.hutool.json.JSONUtil; +import com.cloud.kicc.common.core.constant.SecurityConstants; +import com.cloud.kicc.common.core.enums.CasSystemEnum; +import com.cloud.kicc.common.core.jackson.KiccJavaTimeModule; +import com.cloud.kicc.common.data.entity.CasUser; +import com.cloud.kicc.common.data.entity.KicsUser; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.klab.security.common.exception.UnConfiguredUserDataException; +import com.klab.security.common.override.jackson2.SimpleGrantedAuthorityMixin; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.beans.BeanUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; +import org.springframework.security.oauth2.provider.token.TokenStore; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + *+ * 本地资源服务器令牌服务 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@RequiredArgsConstructor +public class KiccLocalResourceServerTokenServices implements ResourceServerTokenServices { + + private final TokenStore tokenStore; + + private final JdbcTemplate jdbcTemplate; + + @SneakyThrows + public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { + // 根据token加载身份验证 + OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken); + if (oAuth2Authentication == null) { + return null; + } + OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request(); + // 检测是否是属于认证的CasUser实体用户 + if (!(oAuth2Authentication.getPrincipal() instanceof CasUser)) { + return oAuth2Authentication; + } + CasUser casUser = (CasUser) oAuth2Authentication.getPrincipal(); + + // 设置SSO子系统扩展用户信息 + if (casUser.getExPrincipals().get(CasSystemEnum.KICS) == null) { + List+ * 资源服务配置适配器 + * 覆盖内部不合适的实现,实现适配 + * 1. 支持remoteTokenServices 负载均衡,后期出现 + * 2. 支持 获取用户全部信息 + * 3. 接口对外暴露,不校验 Authentication Header 头 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@Slf4j +public class KiccResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter { + + @Autowired + protected ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint; + + @Autowired + private PermitAllUrlProperties permitAllUrl; + + @Autowired + private KiccBearerTokenExtractor kiccBearerTokenExtractor; + + @Autowired + private ResourceServerTokenServices resourceServerTokenServices; + + /** + * 默认的配置,对外暴露 + * @param httpSecurity + */ + @Override + @SneakyThrows + public void configure(HttpSecurity httpSecurity) { + // 允许使用iframe 嵌套,避免swagger-ui 不被加载的问题 + httpSecurity.cors().and().headers().frameOptions().disable(); + // 设置所有的请求必须通过授权认证才可以访问 + ExpressionUrlAuthorizationConfigurer+ * 安全 Bean 定义注册器 + * 加载自定义Bean + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@Slf4j +public class KiccSecurityBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { + + /** + * 根据注解值动态注入资源服务器的相关属性 + * @param metadata 注解信息 + * @param registry 注册器 + */ + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + // 覆盖资源认证服务配置适配器 + if (registry.isBeanNameInUse(SecurityConstants.RESOURCE_SERVER_CONFIGURER)) { + log.warn("本地存在资源服务器配置,覆盖默认配置:" + SecurityConstants.RESOURCE_SERVER_CONFIGURER); + return; + } + + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClass(KiccResourceServerConfigurerAdapter.class); + registry.registerBeanDefinition(SecurityConstants.RESOURCE_SERVER_CONFIGURER, beanDefinition); + + } + +} diff --git a/src/main/java/com/klab/security/common/exp/PermissionService.java b/src/main/java/com/klab/security/common/exp/PermissionService.java new file mode 100644 index 0000000..09b0760 --- /dev/null +++ b/src/main/java/com/klab/security/common/exp/PermissionService.java @@ -0,0 +1,59 @@ +package com.klab.security.common.exp; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + *+ * 接口权限判断 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +public class PermissionService { + + /** + * 判断接口是否有任意xxx,xxx权限 + * @param permissions 权限 + * @return {boolean} + */ + public boolean hasPermission(String... permissions) { + if (ArrayUtil.isEmpty(permissions)) { + return false; + } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return false; + } + Collection extends GrantedAuthority> authorities = authentication.getAuthorities(); + return authorities.stream().map(GrantedAuthority::getAuthority).filter(StringUtils::hasText) + .anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x)); + } + + /** + * 验证用户是否具有以下任意一个权限 + * + * @param permissions 以 ,为分隔符的权限列表 + * @return 用户是否具有以下任意一个权限 + */ + public boolean hasAnyPerm(String permissions) { + if (StrUtil.isEmpty(permissions)) { + return false; + } + for (String permission : permissions.split(",")) { + if (permission != null && hasPermission(permission)) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/com/klab/security/common/exp/PermitAllUrlProperties.java b/src/main/java/com/klab/security/common/exp/PermitAllUrlProperties.java new file mode 100644 index 0000000..d89117f --- /dev/null +++ b/src/main/java/com/klab/security/common/exp/PermitAllUrlProperties.java @@ -0,0 +1,72 @@ +package com.klab.security.common.exp; + +import cn.hutool.core.util.ReUtil; +import com.klab.security.common.annotation.Inner; +import lombok.Getter; +import lombok.Setter; +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.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + *+ * 资源服务器对外直接暴露URL,如果设置 contex-path 要特殊处理 + * 主要处理拿取加了Inner注解的方法上的url映射地址,以及配置文件中配置的security.oauth2.ignore.urls,进行对外开放不拦截 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@Slf4j +@ConfigurationProperties(prefix = "security.oauth2.ignore") +public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware { + + private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); + + private ApplicationContext applicationContext; + + @Getter + @Setter + private List+ * 客户端认证异常处理 + * 可以根据 AuthenticationException 不同细化异常处理 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +@RequiredArgsConstructor +public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper; + + @Override + @SneakyThrows + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json; charset=utf-8"); + R+ * @link + * 重写RedisTokenStore ,主要解决 #1814 oauth2中client_id_to_access数据膨胀问题 + *
+ * + * @Author: wangxiang4 + * @Since: 2023/8/24 + */ +public class KiccRedisTokenStore implements TokenStore { + + private static final String ACCESS = "access:"; + + private static final String AUTH_TO_ACCESS = "auth_to_access:"; + + private static final String AUTH = "auth:"; + + private static final String REFRESH_AUTH = "refresh_auth:"; + + private static final String REFRESH = "refresh:"; + + private static final String REFRESH_TO_ACCESS = "refresh_to_access:"; + + private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access_z:"; + + private static final String UNAME_TO_ACCESS = "uname_to_access_z:"; + + private static final boolean springDataRedis_2_0 = ClassUtils.isPresent( + "org.springframework.data.redis.connection.RedisStandaloneConfiguration", + RedisTokenStore.class.getClassLoader()); + + private final RedisConnectionFactory connectionFactory; + + private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator(); + + private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy(); + + private String prefix = ""; + + private Method redisConnectionSet_2_0; + + public KiccRedisTokenStore(RedisConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + if (springDataRedis_2_0) { + this.loadRedisConnectionMethods_2_0(); + } + } + + public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) { + this.authenticationKeyGenerator = authenticationKeyGenerator; + } + + public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) { + this.serializationStrategy = serializationStrategy; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + private void loadRedisConnectionMethods_2_0() { + this.redisConnectionSet_2_0 = ReflectionUtils.findMethod(RedisConnection.class, "set", byte[].class, + byte[].class); + } + + private RedisConnection getConnection() { + return connectionFactory.getConnection(); + } + + private byte[] serialize(Object object) { + return serializationStrategy.serialize(object); + } + + private byte[] serializeKey(String object) { + return serialize(prefix + object); + } + + private OAuth2AccessToken deserializeAccessToken(byte[] bytes) { + return serializationStrategy.deserialize(bytes, OAuth2AccessToken.class); + } + + private OAuth2Authentication deserializeAuthentication(byte[] bytes) { + return serializationStrategy.deserialize(bytes, OAuth2Authentication.class); + } + + private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) { + return serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class); + } + + private byte[] serialize(String string) { + return serializationStrategy.serialize(string); + } + + private String deserializeString(byte[] bytes) { + return serializationStrategy.deserializeString(bytes); + } + + @Override + public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { + String key = authenticationKeyGenerator.extractKey(authentication); + byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key); + byte[] bytes; + RedisConnection conn = getConnection(); + try { + bytes = conn.get(serializedKey); + } finally { + conn.close(); + } + OAuth2AccessToken accessToken = deserializeAccessToken(bytes); + if (accessToken != null) { + OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue()); + if ((storedAuthentication == null + || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) { + // Keep the stores consistent (maybe the same user is + // represented by this authentication but the details have + // changed) + storeAccessToken(accessToken, authentication); + } + + } + return accessToken; + } + + @Override + public OAuth2Authentication readAuthentication(OAuth2AccessToken token) { + return readAuthentication(token.getValue()); + } + + @Override + public OAuth2Authentication readAuthentication(String token) { + byte[] bytes; + RedisConnection conn = getConnection(); + try { + bytes = conn.get(serializeKey(AUTH + token)); + } finally { + conn.close(); + } + return deserializeAuthentication(bytes); + } + + @Override + public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) { + return readAuthenticationForRefreshToken(token.getValue()); + } + + public OAuth2Authentication readAuthenticationForRefreshToken(String token) { + RedisConnection conn = getConnection(); + try { + byte[] bytes = conn.get(serializeKey(REFRESH_AUTH + token)); + return deserializeAuthentication(bytes); + } finally { + conn.close(); + } + } + + @Override + public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { + byte[] serializedAccessToken = serialize(token); + byte[] serializedAuth = serialize(authentication); + byte[] accessKey = serializeKey(ACCESS + token.getValue()); + byte[] authKey = serializeKey(AUTH + token.getValue()); + byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication)); + byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication)); + byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId()); + RedisConnection conn = getConnection(); + try { + conn.openPipeline(); + if (springDataRedis_2_0) { + try { + this.redisConnectionSet_2_0.invoke(conn, accessKey, serializedAccessToken); + this.redisConnectionSet_2_0.invoke(conn, authKey, serializedAuth); + this.redisConnectionSet_2_0.invoke(conn, authToAccessKey, serializedAccessToken); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + else { + conn.set(accessKey, serializedAccessToken); + conn.set(authKey, serializedAuth); + conn.set(authToAccessKey, serializedAccessToken); + } + + if (token.getExpiration() != null) { + int seconds = token.getExpiresIn(); + long expirationTime = token.getExpiration().getTime(); + + if (!authentication.isClientOnly()) { + conn.zAdd(approvalKey, expirationTime, serializedAccessToken); + } + conn.zAdd(clientId, expirationTime, serializedAccessToken); + + conn.expire(accessKey, seconds); + conn.expire(authKey, seconds); + conn.expire(authToAccessKey, seconds); + conn.expire(clientId, seconds); + conn.expire(approvalKey, seconds); + } + else { + conn.zAdd(clientId, -1, serializedAccessToken); + if (!authentication.isClientOnly()) { + conn.zAdd(approvalKey, -1, serializedAccessToken); + } + } + OAuth2RefreshToken refreshToken = token.getRefreshToken(); + if (refreshToken != null && refreshToken.getValue() != null) { + byte[] auth = serialize(token.getValue()); + byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue()); + if (springDataRedis_2_0) { + try { + this.redisConnectionSet_2_0.invoke(conn, refreshToAccessKey, auth); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + else { + conn.set(refreshToAccessKey, auth); + } + if (refreshToken instanceof ExpiringOAuth2RefreshToken) { + ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken; + Date expiration = expiringRefreshToken.getExpiration(); + if (expiration != null) { + int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L) + .intValue(); + conn.expire(refreshToAccessKey, seconds); + } + } + } + conn.closePipeline(); + } finally { + conn.close(); + } + } + + private static String getApprovalKey(OAuth2Authentication authentication) { + String userName = authentication.getUserAuthentication() == null ? "" + : authentication.getUserAuthentication().getName(); + return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName); + } + + private static String getApprovalKey(String clientId, String userName) { + return clientId + (userName == null ? "" : ":" + userName); + } + + @Override + public void removeAccessToken(OAuth2AccessToken accessToken) { + removeAccessToken(accessToken.getValue()); + } + + @Override + public OAuth2AccessToken readAccessToken(String tokenValue) { + byte[] key = serializeKey(ACCESS + tokenValue); + byte[] bytes; + RedisConnection conn = getConnection(); + try { + bytes = conn.get(key); + } finally { + conn.close(); + } + return deserializeAccessToken(bytes); + } + + public void removeAccessToken(String tokenValue) { + byte[] accessKey = serializeKey(ACCESS + tokenValue); + byte[] authKey = serializeKey(AUTH + tokenValue); + RedisConnection conn = getConnection(); + try { + conn.openPipeline(); + conn.get(accessKey); + conn.get(authKey); + conn.del(accessKey); + // Don't remove the refresh token - it's up to the caller to do that + conn.del(authKey); + List