Spring Secutity 和 Apache Shiro 是 Java 领域的两大主流开源安全框架, 也是权限系统设计的主要技术选型. 本文主要介绍 Spring Secutity 的实现原理, 并基于 Spring Secutity 设计基于 RBAC 的权限系统.
一, 技术选型
为何把 Spring Secutity 作为权限系统的技术选型, 主要考虑了以下几个方面:
数据鉴权的能力: Spring Secutity 支持数据鉴权, 即细粒度权限控制.
Spring 生态基础: Spring Secutity 可以和 Spring 生态无缝集成.
多样认证能力: Spring Secutity 支持多样认证方式, 如预认证方式可以与第三方认证系统集成.
二, 核心架构
权限系统一般包含两大核心模块: 认证 (Authentication) 和鉴权(Authorization).
认证: 认证模块负责验证用户身份的合法性, 生成认证令牌, 并保存到服务端会话中(如 TLS).
鉴权: 鉴权模块负责从服务端会话内获取用户身份信息, 与访问的资源进行权限比对.
官方给出的 Spring Security 的核心架构图如下:
核心架构解读:
AuthenticationManager: 负责认证管理, 解析用户登录信息(封装在 Authentication), 读取用户, 角色, 权限信息进行认证, 认证结果被回填到 Authentication, 保存在 SecurityContext.
AccessDecisionManager: 负责鉴权投票表决, 汇总投票器的结果, 实现一票通过(默认), 多票通过, 一票否决策略.
SecurityInterceptor: 负责权限拦截, 包括 web URL 拦截和方法调用拦截. 通过 ConfigAttributes 获取资源的描述信息, 借助于 AccessDecisionManager 进行鉴权拦截.
SecurityContext: 安全上下文, 保存认证结果. 提供了全局上下文, 线程继承上下文, 线程独立上下文 (默认) 三种策略.
Authentication: 认证信息, 保存用户的身份标示, 权限列表, 证书, 认证通过标记等信息.
SecuredResource: 被安全管控的资源, 如 Web URL, 用户, 角色, 自定义领域对象等.
ConfigAttributes: 资源属性配置, 描述安全管控资源的信息, 为 SecurityInterceptor 提供拦截逻辑的输入.
三, 设计原理
通过对源码的分析, 我把 Spring Security 的核心领域模型设计整理如下:
全局抽象模型解读:
配置: AuthenticationConfiguration 负责认证系统的全局配置, GlobalMethodSecurityConfiguration 负责方法调用拦截的全局配置.
构建: AuthenticationConfiguration 通过 AuthenticationManagerBuilder 构建认证管理器 AuthenticationManager,GlobalMethodSecurityConfiguration 会自动初始化 AbstractSecurityInterceptor 进行方法调用拦截.
Web 拦截: HttpSecurity 对 Web 进行安全配置, 内置了大量 GenericFilterBean 过滤器对 URL 进行拦截. 负责认证的过滤器会通过 AuthenticationManager 进行认证, 并将认证结果保存到 SecurityContext.
方法拦截: Spring 通过 AOP 技术 (cglib/aspectj) 对标记为 @PreAuthorize,@PreFilter,@PostAuthorize,@PostFilter 等注解的方法进行拦截, 通过 AbstractSecurityInterceptor 调用 AuthenticationManager 进行身份认证(如果必要的话).
认证: 认证管理器 AuthenticationManager 内置了多种认证器 AuthenticationProvider, 只要其中一个认证通过, 认证便成功. 不同的 AuthenticationProvider 获取各自需要的信息 (HTTP 请求, 数据库查询, 远程服务等) 进行认证, 认证结果全部封装在 Authentication. 需要加载用户, 角色, 权限信息的认证器 (如密码认证, 预认证等) 需要对接 UserDetailsManager 接口实现用户 CRUD 功能.
鉴权: 权限拦截器 AbstractSecurityInterceptor 通过读取不同的 SecurityMetadataSource 加载需要被鉴权资源的描述信息 ConfigAttribute, 然后把认证信息 Authentication, 资源描述 ConfigAttribute, 资源对象本身传递给 AccessDecisionManager 进行表决. AccessDecisionManager 内置了多个投票器 AccessDecisionVoter, 投票器会将鉴权信息中的 ConfigAttribute 转换为 SpringEL 的格式, 通过表达式处理器 SecurityExpressionHandler 执行基于表达式的鉴权逻辑, 鉴权逻辑会通过反射的方式转发到 SecurityExpressionRoot 的各个操作上去.
定制: 通过 WebSecurityConfigureAdapter 可以定制 HTTP 安全配置 HttpSecurity 和认证管理器生成器 AuthenticationManagerBuilder; 通过 AbstractPreAuthenticatedProcessingFilter 可以定制预认证过滤器; 通过 UserDetailsManager 和 UserDetails 接口可以对接自定义数据源; 通过 GrantedAuthority 定制权限信息; 通过 PermissionEvaluator 可以定制自定义领域模型的访问控制逻辑.
四, 应用集成
理清 Spring Security 的定制点后, 就可以在系统内部集成 Spring Security 了.
这里使用预认证的方式, 以适配第三方认证系统. AbstractPreAuthenticatedProcessingFilter 提供了预认证的扩展点, 基于该抽象类实现一个自定义认证过滤器.
- public class MyPreAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
- @Override
- protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
- // 从第三方系统获取用户 ID
- return userId;
- }
- @Override
- protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
- return "";
- }
- }
Spring Security 会根据预认证过滤器 getPreAuthenticatedPrincipal 返回的用户 ID 信息, 加载用户角色等初始信息. 这里需要实现 UserDetailsManager 接口, 提供用户信息管理器.
@Service public class MyUserManager implements UserDetailsManager { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 从数据库加载用户信息 return user; } // 其他管理接口 }
UserDetails 内包含了 GrantedAuthority 接口类型的权限信息抽象, 一般可以基于它自定义角色和权限. Spring Security 使用一种接口形式表达角色和权限, 角色和权限的差别是角色的 ID 是以 "ROLE_" 为前缀.
public class MyRole implements GrantedAuthority { private final String role; @Override public String getAuthority() { return "ROLE_" + role; } } public class MyAuthority implements GrantedAuthority { private final String authority; @Override public String getAuthority() { return authority; } }
接下来注册自定义认证过滤器和用户管理器, 这里需要实现 WebSecurityConfigurerAdapter 进行 Web 安全配置.
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, mode = AdviceMode.PROXY) public class MySecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserDetailsManager userDetailsManager; @Bean protected AuthenticationProvider createPreAuthProvider() { // 注册用户管理器 PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider(); provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsManager)); return provider; } @Override protected void configure(HttpSecurity http) throws Exception { // 注册预认证过滤器 http.addFilter(new MyPreAuthFilter(authenticationManager())); } }
这样, 最简单的 Spring Security 框架集成内系统内部已经完成了. 在系统的任意服务接口上可以使用如下方式进行鉴权.
public interface MyService { @PreAuthorize("hasAuthority('QUERY')") Object getById(String id); @PreAuthorize("hasRole('ADMIN')") void deleteById(String id); }
PreAuthorize 注解表示调用前鉴权, Spring 使用默认使用动态代理技术生成鉴权逻辑. 注解内配置了 SpringEL 表达式来定制鉴权方式. 上述代码中, hasAuthority 会检查用户是否有 QUERY 权限, hasRole 会检查用户是否有 ADMIN 角色.
使用动态代理的方式进行 AOP, 只允许在接口层面进行权限拦截, 如果想在任意的方法上进行权限拦截, 那么就需要借助于 AspectJ 的方式进行 AOP. 首先将注解 EnableGlobalMethodSecurity 的 mode 设置为 AdviceMode.ASPECTJ, 然后添加 JVM 启动参数, 这样就可以在任意方法上使用 Spring Security 的注解了.
javaagent:/path/to/org/aspectj/aspectjweaver/1.9.4/aspectjweaver-1.9.4.jar
以上还是只是以用户的身份信息 (角色 / 权限) 进行权限, 灵活度有限, 也发挥不了 Spring Security 的数据鉴权的能力. 要使用数据鉴权, 需要实现一个 Spring Bean.
@Component public class MyPermissionEvaluator implements PermissionEvaluator { @Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { // 自定义数据鉴权 return false; } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { // 自定义数据鉴权 return false; } }
PermissionEvaluator 会被自动注册到 Spring Security 框架, 并允许在注解内使用如下方式进行鉴权.
@PreAuthorize("hasPermission(#id,'QUERY')") Object func1(String id) { } @PreAuthorize("hasPermission(#id,'TABLE','QUERY')") Object func2(String id) { }
其中, func1 的注解表示校验用户是否对 id 有 QUERY 权限, 代码逻辑路由到 MyPermissionEvaluator 的第一个接口. func2 的注解表示校验用户是否对 TABLE 类型的 id 有 QUERY 权限, 代码逻辑路由到 MyPermissionEvaluator 的第二个接口. PermissionEvaluator 提供了权限系统中数据鉴权的扩展点, 稍后会描述如何利用该扩展点定制基于 RBAC 的权限系统.
五, 权限系统
构建基于 RBAC(Role Based Access Control)的权限系统, 需要明确用户, 角色, 权限, 资源这几个核心的概念类的含义和它们之间的关系.
资源: 权限系统内需要安全控制的客体, 一般是系统内的数据或功能.
权限: 描述了资源上的操作抽象, 一般是一种动作.
授权: 是权限和资源的组合, 表示对资源的某一个操作.
角色: 描述了一组授权的集合, 表示一类特殊概念的功能集.
用户: 权限系统的主体, 一般是当前系统的访问用户, 用户可以拥有多种角色.
以下是我们设计的基于 RABC 的权限核心领域模型:
一般情况下, 系统内需要权限管控的资源是无法用户自定义的, 因为资源会耦合大量的业务逻辑, 所以我们提供了自 资源工厂, 通过配置化的方式构建业务模块所需的资源. 而用户, 角色, 权限, 以及授权记录都是可以通过相应的管理器进行查询更新.
另外, 资源抽象允许表达资源的继承和组合关系, 继而表达更复杂的资源模型, 资源统一鉴权的流程为:
执行鉴权时, 首先看资源是原子资源还是组合资源.
对于原子资源, 先查询是否有授权记录, 再查看角色预授权是否包含当前授权, 存在一种便成功.
没有授权记录和角色预授权的原子资源, 尝试用父资源 (如果有的话) 代替鉴权, 否则鉴权失败.
对于组合资源, 先进行资源展开, 获取子资源列表.
遍历子资源列表, 并依次对子资源进行鉴权, 子资源鉴权结果汇总后, 即组合资源鉴权结果.
综上, 基于统一资源抽象和资源配置化构建, 可以实现资源的统一构建, 继而实现统一鉴权.
六, 总结回顾
本文从 Spring Security 的架构和原理出发, 描述了开源安全框架对于认证和鉴权模块的设计思路和细节. 并提供了系统内集成 Spring Security 的方法, 结合 RBAC 通用权限系统模型, 讨论了统一资源构建和统一鉴权的设计和实现. 如果你也需要设计一个新的权限系统, 希望本文对你有所帮助.
来源: http://netsecurity.51cto.com/art/201912/607308.htm