diff --git a/orion-ops-framework/orion-ops-common/src/main/java/com/orion/ops/framework/common/security/LoginUser.java b/orion-ops-framework/orion-ops-common/src/main/java/com/orion/ops/framework/common/security/LoginUser.java new file mode 100644 index 00000000..d443d8f7 --- /dev/null +++ b/orion-ops-framework/orion-ops-common/src/main/java/com/orion/ops/framework/common/security/LoginUser.java @@ -0,0 +1,37 @@ +package com.orion.ops.framework.common.security; + +import lombok.Data; + +import java.util.List; + +/** + * 当前登录用户 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/6 18:36 + */ +@Data +public class LoginUser { + + /** + * id + */ + private Long id; + + /** + * 用户名 + */ + private String username; + + /** + * 花名 + */ + private String nickname; + + /** + * 角色 + */ + private List roles; + +} diff --git a/orion-ops-framework/orion-ops-common/src/main/java/com/orion/ops/framework/common/security/SecurityHolder.java b/orion-ops-framework/orion-ops-common/src/main/java/com/orion/ops/framework/common/security/SecurityHolder.java new file mode 100644 index 00000000..0a8a9935 --- /dev/null +++ b/orion-ops-framework/orion-ops-common/src/main/java/com/orion/ops/framework/common/security/SecurityHolder.java @@ -0,0 +1,26 @@ +package com.orion.ops.framework.common.security; + +/** + * SecurityUtils 的 bean 对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 15:20 + */ +public interface SecurityHolder { + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + LoginUser getLoginUser(); + + /** + * 获取当前用户id + * + * @return id + */ + Long getLoginUserId(); + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/config/AuthorizeRequestsCustomizer.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/config/AuthorizeRequestsCustomizer.java new file mode 100644 index 00000000..fb03bdc9 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/config/AuthorizeRequestsCustomizer.java @@ -0,0 +1,23 @@ +package com.orion.ops.framework.security.config; + +import org.springframework.core.Ordered; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * 自定义安全策略配置 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 12:58 + */ +public abstract class AuthorizeRequestsCustomizer + implements Customizer.ExpressionInterceptUrlRegistry>, Ordered { + + @Override + public int getOrder() { + return 0; + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/config/OrionSecurityAutoConfiguration.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/config/OrionSecurityAutoConfiguration.java new file mode 100644 index 00000000..d4dde797 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/config/OrionSecurityAutoConfiguration.java @@ -0,0 +1,220 @@ +package com.orion.ops.framework.security.config; + +import com.orion.ops.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; +import com.orion.ops.framework.security.core.filter.TokenAuthenticationFilter; +import com.orion.ops.framework.security.core.handler.AuthenticationEntryPointHandler; +import com.orion.ops.framework.security.core.handler.ForbiddenAccessDeniedHandler; +import com.orion.ops.framework.security.core.service.SecurityFrameworkService; +import com.orion.ops.framework.security.core.service.SecurityFrameworkServiceDelegate; +import com.orion.ops.framework.security.core.service.SecurityHolderDelegate; +import com.orion.ops.framework.security.core.strategy.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.MethodInvokingFactoryBean; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +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.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 项目安全配置类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/6 15:05 + */ +@AutoConfiguration +@EnableConfigurationProperties(SecurityConfig.class) +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class OrionSecurityAutoConfiguration { + + @Resource + private SecurityConfig securityConfig; + + /** + * @return 认证失败处理器 + */ + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new AuthenticationEntryPointHandler(); + } + + /** + * @return 权限不足处理器 + */ + @Bean + public AccessDeniedHandler accessDeniedHandler() { + return new ForbiddenAccessDeniedHandler(); + } + + /** + * @return 密码加密器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(securityConfig.getPasswordEncoderLength()); + } + + /** + * AuthenticationManager 不是bean + * 重写父类方法可注入 AuthenticationManager + * + * @param authenticationConfiguration configuration + * @return AuthenticationManagerBean + * @throws Exception Exception + */ + @Bean + public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + /** + * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法 + * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略 + * + * @return 替换策略 + */ + @Bean + public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() { + MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean(); + methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class); + methodInvokingFactoryBean.setTargetMethod("setStrategyName"); + methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName()); + return methodInvokingFactoryBean; + } + + /** + * @param impl impl + * @return 安全框架服务 + */ + @Bean("ss") + @Primary + @ConditionalOnBean(SecurityFrameworkService.class) + public SecurityFrameworkServiceDelegate securityFrameworkService(SecurityFrameworkService impl) { + return new SecurityFrameworkServiceDelegate(impl); + } + + /** + * @param delegate delegate + * @return token 认证过滤器 + */ + @Bean + @ConditionalOnBean(SecurityFrameworkService.class) + public TokenAuthenticationFilter authenticationTokenFilter(SecurityFrameworkService delegate) { + return new TokenAuthenticationFilter(delegate); + } + + /** + * @return security holder 代理用于内部 framework 调用 + */ + @Bean + public SecurityHolderDelegate securityHolder() { + return new SecurityHolderDelegate(); + } + + /** + * @return 静态资源安全策略 + */ + @Bean + public StaticResourceAuthorizeRequestsCustomizer staticResourceAuthorizeRequestsCustomizer() { + return new StaticResourceAuthorizeRequestsCustomizer(); + } + + /** + * @param applicationContext applicationContext + * @return 匿名接口安全策略 + */ + @Bean + public PermitAllAnnotationAuthorizeRequestsCustomizer permitAllAnnotationAuthorizeRequestsCustomizer(ApplicationContext applicationContext) { + return new PermitAllAnnotationAuthorizeRequestsCustomizer(applicationContext); + } + + /** + * @return 配置文件安全策略 + */ + @Bean + public ConfigAuthorizeRequestsCustomizer configAuthorizeRequestsCustomizer() { + return new ConfigAuthorizeRequestsCustomizer(securityConfig); + } + + /** + * @return websocket 安全策略 + */ + @Bean + public WebsocketAuthorizeRequestsCustomizer websocketAuthorizeRequestsCustomizer() { + return new WebsocketAuthorizeRequestsCustomizer(); + } + + /** + * @param adminSeverContextPath adminSeverContextPath + * @return 控制台安全策略 + */ + @Bean + public ConsoleAuthorizeRequestsCustomizer consoleAuthorizeRequestsCustomizer(@Value("${spring.boot.admin.context-path:''}") String adminSeverContextPath) { + return new ConsoleAuthorizeRequestsCustomizer(adminSeverContextPath); + } + + /** + * 配置安全配置 + *

+ * anyRequest | 匹配所有请求路径 + * access | SpringEl 表达式结果为 true 时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数, 参数表示权限, 则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数, 参数表示角色, 则其中任何一个角色可以访问 + * hasAuthority | 如果有参数, 参数表示权限, 则其权限可以访问 + * hasIpAddress | 如果有参数, 参数表示IP地址, 如果用户IP和参数匹配, 则可以访问 + * hasRole | 如果有参数, 参数表示角色, 则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过 remember-me 登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Bean + protected SecurityFilterChain filterChain(List authorizeRequestsCustomizers, + AuthenticationEntryPoint authenticationEntryPoint, + AccessDeniedHandler accessDeniedHandler, + TokenAuthenticationFilter authenticationTokenFilter, + HttpSecurity httpSecurity) throws Exception { + return httpSecurity + // 开启跨域 + .cors().and() + // 因为不使用session 禁用CSRF + .csrf().disable() + // 基于 token 机制所以不需要 session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + // 不设置响应报头 + .headers().frameOptions().disable().and() + // 认证失败处理器 + .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) + // 权限不足处理器 + .accessDeniedHandler(accessDeniedHandler).and() + // 设置请求权限策略 + .authorizeRequests(registry -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) + // 兜底规则 必须认证 + .authorizeRequests() + .anyRequest() + .authenticated().and() + // 在密码认证器之前添加 token 过滤器 + .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/config/SecurityConfig.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/config/SecurityConfig.java new file mode 100644 index 00000000..7cc2e307 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/config/SecurityConfig.java @@ -0,0 +1,34 @@ +package com.orion.ops.framework.security.config; + +import com.orion.ops.framework.common.utils.ConfigUtils; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +/** + * 安全配置 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/6 15:55 + */ +@Data +@ConfigurationProperties("orion.security") +public class SecurityConfig { + + /** + * 加密复杂度 + */ + private Integer passwordEncoderLength = 4; + + /** + * 匿名接口 + */ + private List permitUrl; + + public void setPermitUrl(List permitUrl) { + this.permitUrl = ConfigUtils.parseStringList(permitUrl); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java new file mode 100644 index 00000000..6d73b793 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java @@ -0,0 +1,50 @@ +package com.orion.ops.framework.security.core.context; + +import com.alibaba.ttl.TransmittableThreadLocal; +import com.orion.lang.utils.Valid; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.context.SecurityContextImpl; + +/** + * 使用 TransmittableThreadLocal 实现 Security Context 持有者策略 + * 避免异步执行时 ThreadLocal 的丢失问题 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/6 15:55 + */ +public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { + + /** + * 使用 TransmittableThreadLocal 作为上下文 + */ + private static final ThreadLocal CONTEXT_HOLDER = new TransmittableThreadLocal<>(); + + @Override + public void clearContext() { + CONTEXT_HOLDER.remove(); + } + + @Override + public SecurityContext getContext() { + SecurityContext ctx = CONTEXT_HOLDER.get(); + if (ctx == null) { + ctx = this.createEmptyContext(); + CONTEXT_HOLDER.set(ctx); + } + return ctx; + } + + @Override + public void setContext(SecurityContext context) { + Valid.notNull(context, "Only non-null SecurityContext instances are permitted"); + CONTEXT_HOLDER.set(context); + } + + @Override + public SecurityContext createEmptyContext() { + return new SecurityContextImpl(); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/filter/TokenAuthenticationFilter.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 00000000..400decca --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,48 @@ +package com.orion.ops.framework.security.core.filter; + +import com.orion.lang.utils.Strings; +import com.orion.ops.framework.common.security.LoginUser; +import com.orion.ops.framework.security.core.service.SecurityFrameworkService; +import com.orion.ops.framework.security.core.utils.SecurityUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 认证过滤器 + * 验证 token 有效后将其加入上下文 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/6 18:39 + */ +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + private final SecurityFrameworkService securityFrameworkService; + + public TokenAuthenticationFilter(SecurityFrameworkService securityFrameworkService) { + this.securityFrameworkService = securityFrameworkService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 获取请求头 token + String token = SecurityUtils.obtainAuthorization(request); + if (!Strings.isBlank(token)) { + // 通过 token 获取用户信息 + LoginUser loginUser = securityFrameworkService.getUserByToken(token); + // 设置上下文 + if (loginUser != null) { + SecurityUtils.setLoginUser(loginUser, request); + } + } + // todo 全局异常返回 mock模式 + // 继续执行 + chain.doFilter(request, response); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/handler/AuthenticationEntryPointHandler.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/handler/AuthenticationEntryPointHandler.java new file mode 100644 index 00000000..e4ef3429 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/handler/AuthenticationEntryPointHandler.java @@ -0,0 +1,30 @@ +package com.orion.ops.framework.security.core.handler; + +import com.orion.ops.framework.common.constant.ErrorCode; +import com.orion.web.servlet.web.Servlets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 认证失败处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/6 16:01 + */ +@Slf4j +public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { + log.debug("AuthenticationEntryPoint-commence-未登录 {}", request.getRequestURI(), e); + Servlets.writeHttpWrapper(response, ErrorCode.UNAUTHORIZED.getWrapper()); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/handler/ForbiddenAccessDeniedHandler.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/handler/ForbiddenAccessDeniedHandler.java new file mode 100644 index 00000000..c9c76a92 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/handler/ForbiddenAccessDeniedHandler.java @@ -0,0 +1,30 @@ +package com.orion.ops.framework.security.core.handler; + +import com.orion.ops.framework.common.constant.ErrorCode; +import com.orion.ops.framework.security.core.utils.SecurityUtils; +import com.orion.web.servlet.web.Servlets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 权限不足处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/6 16:01 + */ +@Slf4j +public class ForbiddenAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException { + log.warn("AccessDeniedHandlerImpl-handle-无权限 {} {}", SecurityUtils.getLoginUserId(), request.getRequestURI()); + Servlets.writeHttpWrapper(response, ErrorCode.FORBIDDEN.getWrapper()); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/service/SecurityFrameworkService.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/service/SecurityFrameworkService.java new file mode 100644 index 00000000..79df6196 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/service/SecurityFrameworkService.java @@ -0,0 +1,42 @@ +package com.orion.ops.framework.security.core.service; + +import com.orion.ops.framework.common.security.LoginUser; + +/** + * 权限校验服务 + *

+ * 在业务层定义 bean + * 使用 @PreAuthorize("@ss.hasPermission('xxx')") + *

+ * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/6 18:25 + */ +public interface SecurityFrameworkService { + + /** + * 检查是否有权限 + * + * @param permission 权限 + * @return has + */ + boolean hasPermission(String permission); + + /** + * 检查是否有角色 + * + * @param role 角色 + * @return has + */ + boolean hasRole(String role); + + /** + * 通过 token 获取用户信息 + * + * @param token token + * @return user + */ + LoginUser getUserByToken(String token); + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/service/SecurityFrameworkServiceDelegate.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/service/SecurityFrameworkServiceDelegate.java new file mode 100644 index 00000000..a3ea20ea --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/service/SecurityFrameworkServiceDelegate.java @@ -0,0 +1,35 @@ +package com.orion.ops.framework.security.core.service; + +import com.orion.ops.framework.common.security.LoginUser; + +/** + * 权限校验服务委托类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 11:02 + */ +public class SecurityFrameworkServiceDelegate implements SecurityFrameworkService { + + private final SecurityFrameworkService delegate; + + public SecurityFrameworkServiceDelegate(SecurityFrameworkService delegate) { + this.delegate = delegate; + } + + @Override + public boolean hasPermission(String permission) { + return delegate.hasPermission(permission); + } + + @Override + public boolean hasRole(String role) { + return delegate.hasRole(role); + } + + @Override + public LoginUser getUserByToken(String token) { + return delegate.getUserByToken(token); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/service/SecurityHolderDelegate.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/service/SecurityHolderDelegate.java new file mode 100644 index 00000000..cd3f60f7 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/service/SecurityHolderDelegate.java @@ -0,0 +1,26 @@ +package com.orion.ops.framework.security.core.service; + +import com.orion.ops.framework.common.security.LoginUser; +import com.orion.ops.framework.common.security.SecurityHolder; +import com.orion.ops.framework.security.core.utils.SecurityUtils; + +/** + * SecurityHolder 委托类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 15:33 + */ +public class SecurityHolderDelegate implements SecurityHolder { + + @Override + public LoginUser getLoginUser() { + return SecurityUtils.getLoginUser(); + } + + @Override + public Long getLoginUserId() { + return SecurityUtils.getLoginUserId(); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/ConfigAuthorizeRequestsCustomizer.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/ConfigAuthorizeRequestsCustomizer.java new file mode 100644 index 00000000..8b1c601b --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/ConfigAuthorizeRequestsCustomizer.java @@ -0,0 +1,29 @@ +package com.orion.ops.framework.security.core.strategy; + +import com.orion.ops.framework.security.config.AuthorizeRequestsCustomizer; +import com.orion.ops.framework.security.config.SecurityConfig; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * 配置文件 认证策略 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 13:04 + */ +public class ConfigAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { + + private final SecurityConfig securityConfig; + + public ConfigAuthorizeRequestsCustomizer(SecurityConfig securityConfig) { + this.securityConfig = securityConfig; + } + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // 配置文件 无需认证 + registry.antMatchers(securityConfig.getPermitUrl().toArray(new String[0])).permitAll(); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/ConsoleAuthorizeRequestsCustomizer.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/ConsoleAuthorizeRequestsCustomizer.java new file mode 100644 index 00000000..b8ddc75b --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/ConsoleAuthorizeRequestsCustomizer.java @@ -0,0 +1,36 @@ +package com.orion.ops.framework.security.core.strategy; + +import com.orion.ops.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * 控制台 认证策略 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 13:04 + */ +public class ConsoleAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { + + private final String adminSeverContextPath; + + public ConsoleAuthorizeRequestsCustomizer(String adminSeverContextPath) { + this.adminSeverContextPath = adminSeverContextPath; + } + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + registry + // swagger 接口文档 + .antMatchers("/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**").permitAll() + .antMatchers("/swagger-resources/**", "/webjars/**", "/*/api-docs").anonymous() + // druid 监控 + .antMatchers("/druid/**").anonymous() + // actuator 安全配置 TODO TEST + .antMatchers("/actuator", "/actuator/**").anonymous() + // admin 安全配置 TODO TEST + .antMatchers(adminSeverContextPath, adminSeverContextPath + "/**").anonymous(); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/PermitAllAnnotationAuthorizeRequestsCustomizer.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/PermitAllAnnotationAuthorizeRequestsCustomizer.java new file mode 100644 index 00000000..845df935 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/PermitAllAnnotationAuthorizeRequestsCustomizer.java @@ -0,0 +1,105 @@ +package com.orion.ops.framework.security.core.strategy; + +import com.orion.ops.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import javax.annotation.security.PermitAll; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * API @PermitAll 认证策略 + * + * @author Jiahang Li + * @version 1.0.0 + * @see javax.annotation.security.PermitAll + * @since 2023/7/7 13:04 + */ +public class PermitAllAnnotationAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { + + private final ApplicationContext applicationContext; + + public PermitAllAnnotationAuthorizeRequestsCustomizer(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // 获取匿名接口 + Map> permitAllUrls = getPermitAllUrlsFromAnnotations(); + // @PermitAll 无需认证 + registry.antMatchers(permitAllUrls.get(null).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll(); + } + + /** + * 通过注解获取所有匿名接口 + * + * @return 匿名接口 + */ + private Map> getPermitAllUrlsFromAnnotations() { + Set getList = new HashSet<>(); + Set postList = new HashSet<>(); + Set putList = new HashSet<>(); + Set deleteList = new HashSet<>(); + Set requestList = new HashSet<>(); + // 获取 RequestMappingHandlerMapping + RequestMappingHandlerMapping requestMappingHandlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class); + // 获得接口对应的 HandlerMethod + Map handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); + // 获得有 @PermitAll 注解的接口 + handlerMethodMap.forEach((mapping, method) -> { + // 非 @PermitAll 则跳过 + if (!method.hasMethodAnnotation(PermitAll.class)) { + return; + } + if (mapping.getPatternsCondition() == null) { + return; + } + Set urls = mapping.getPatternsCondition().getPatterns(); + Set methods = mapping.getMethodsCondition().getMethods(); + // 为空证明为 @RequestMapping + if (methods.isEmpty()) { + requestList.addAll(urls); + } + // 根据请求方法过滤 + methods.forEach(requestMethod -> { + switch (requestMethod) { + case GET: + getList.addAll(urls); + break; + case POST: + postList.addAll(urls); + break; + case PUT: + putList.addAll(urls); + break; + case DELETE: + deleteList.addAll(urls); + break; + } + }); + }); + // 设置返回 + Map> result = new HashMap<>(); + result.put(HttpMethod.GET, getList); + result.put(HttpMethod.POST, postList); + result.put(HttpMethod.PUT, putList); + result.put(HttpMethod.DELETE, deleteList); + result.put(null, requestList); + return result; + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/StaticResourceAuthorizeRequestsCustomizer.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/StaticResourceAuthorizeRequestsCustomizer.java new file mode 100644 index 00000000..977a360b --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/StaticResourceAuthorizeRequestsCustomizer.java @@ -0,0 +1,23 @@ +package com.orion.ops.framework.security.core.strategy; + +import com.orion.ops.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * 静态资源 认证策略 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 13:04 + */ +public class StaticResourceAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // 静态资源可匿名访问 + registry.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll(); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/WebsocketAuthorizeRequestsCustomizer.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/WebsocketAuthorizeRequestsCustomizer.java new file mode 100644 index 00000000..677974d7 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/strategy/WebsocketAuthorizeRequestsCustomizer.java @@ -0,0 +1,22 @@ +package com.orion.ops.framework.security.core.strategy; + +import com.orion.ops.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * websocket 认证策略 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 13:04 + */ +public class WebsocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // websocket 允许匿名访问 + registry.antMatchers("/keep-alive/**").permitAll(); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/utils/SecurityUtils.java b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/utils/SecurityUtils.java new file mode 100644 index 00000000..bd468654 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/java/com/orion/ops/framework/security/core/utils/SecurityUtils.java @@ -0,0 +1,97 @@ +package com.orion.ops.framework.security.core.utils; + +import com.orion.lang.constant.StandardHttpHeader; +import com.orion.lang.utils.Strings; +import com.orion.ops.framework.common.constant.Const; +import com.orion.ops.framework.common.security.LoginUser; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; + +/** + * 安全工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 11:13 + */ +public class SecurityUtils { + + private SecurityUtils() { + } + + /** + * 获取 token + * + * @param request request + * @return token + */ + public static String obtainAuthorization(HttpServletRequest request) { + String authorization = request.getHeader(StandardHttpHeader.AUTHORIZATION); + // todo mock + authorization = "Bearer 1213"; + if (Strings.isEmpty(authorization)) { + return null; + } + if (!authorization.contains(Const.BEARER)) { + return null; + } + return authorization.substring(7).trim(); + } + + /** + * 获得当前认证信息 + * + * @return 认证信息 + */ + public static Authentication getAuthentication() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + return context.getAuthentication(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + public static LoginUser getLoginUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获取当前用户id + * + * @return id + */ + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 authentication + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + // 设置上下文 + SecurityContextHolder.getContext().setAuthentication(authentication); + } + +} diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/resources/META-INF/spring-configuration-metadata.json b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000..a82ef10d --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,22 @@ +{ + "groups": [ + { + "name": "orion.security", + "type": "com.orion.ops.framework.security.config.SecurityConfig", + "sourceType": "com.orion.ops.framework.security.config.SecurityConfig" + } + ], + "properties": [ + { + "name": "orion.security.password-encoder-length", + "type": "java.lang.Integer", + "description": "加密复杂度.", + "defaultValue": 4 + }, + { + "name": "orion.security.permit-url", + "type": "java.util.List", + "description": "匿名接口." + } + ] +} \ No newline at end of file diff --git a/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..023efb97 --- /dev/null +++ b/orion-ops-framework/orion-ops-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orion.ops.framework.security.config.OrionSecurityAutoConfiguration \ No newline at end of file diff --git a/orion-ops-launch/src/main/java/com/orion/ops/launch/service/EmptySecurityImpl.java b/orion-ops-launch/src/main/java/com/orion/ops/launch/service/EmptySecurityImpl.java new file mode 100644 index 00000000..ae7dcc3d --- /dev/null +++ b/orion-ops-launch/src/main/java/com/orion/ops/launch/service/EmptySecurityImpl.java @@ -0,0 +1,39 @@ +package com.orion.ops.launch.service; + +import com.orion.lang.utils.collect.Lists; +import com.orion.ops.framework.common.security.LoginUser; +import com.orion.ops.framework.security.core.service.SecurityFrameworkService; +import org.springframework.stereotype.Component; + +/** + * TODO 基建模块实现 现在默认实现 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 10:57 + */ +@Component +public class EmptySecurityImpl implements SecurityFrameworkService { + + @Override + public boolean hasPermission(String permission) { + return true; + } + + @Override + public boolean hasRole(String role) { + return true; + } + + @Override + public LoginUser getUserByToken(String token) { + // TODO MOCK + LoginUser user = new LoginUser(); + user.setId(123L); + user.setUsername("username"); + user.setNickname("nickname"); + user.setRoles(Lists.of("r1", "r2")); + return user; + } + +} diff --git a/orion-ops-launch/src/main/resources/application.yaml b/orion-ops-launch/src/main/resources/application.yaml index 8311bdca..e2169be5 100644 --- a/orion-ops-launch/src/main/resources/application.yaml +++ b/orion-ops-launch/src/main/resources/application.yaml @@ -66,7 +66,7 @@ spring: time-to-live: 1h output: ansi: - enabled: detect + enabled: DETECT mybatis-plus: configuration: @@ -92,7 +92,7 @@ springdoc: knife4j: enable: true setting: - language: zh_cn + language: ZH_CN logging: file: @@ -144,3 +144,7 @@ orion: nameAppendTraceId: true storagePath: ${user.home} basePath: /orion/storage/orion-ops-pro + security: + password-encoder-length: 4 + # 匿名接口 + permit-url: