1. 简介
在Java企业级开发中,安全管理方面的框架非常少,一般来说,主要有三种方案:
- Shiro
- SpringSecunty
- 开发者自已实现
Shiro:本身是一个老牌的安全管理框架,有着众多的优点,例如轻量、简单、易于集成、可以在JavaSE环境中使用等。不过,在微服务时代,Shiro就显得力不从心了,在微服务面前,它无法充分展示自己的优势。
springsecurity:作为spring家族的一员,在和spring家族的其他成员如springBoot、spring Cloud等进行整合时,具有其他框架无可比拟的优势,同时对OAuth2有着良好的支持,再加上springCloud对springsecunty的不断加持(如推出springCloudsecurity),让springsecunty不知不觉中成为微服务项目的首选安全管理方案。
2. 核心功能
- 认证:身份验证(你是谁?)
- 授权:访问控制(你可以做什么?)
支持多种不同的认证方式:
springsecurity集成的主流认证机制主要有如下几种:
- 表单认证
- OAuth2.0认证
- SAML2.0认证
- CAS认证
- RememberMe自动认证
- JAAS认证
- OpenID去中心化认证
- Pre-AuthenticationScenarios认证
- Ⅹ509认证
- HTTP Basic认证
- HTTP Digest认证
如果这些认证方式无法满足我们的需求,可以自定义认证逻辑,特别是当我们和一些“老破旧”的系统进行集成时,自定义认证逻辑就显得非常重要了。
springsecurity支持基于URL的请求授权、支持方法访问授权、支持SpEL访问控制、支持域对象安全(ACL),同时也支持动态权限配置、支持RBAC权限模型等。
3 .整体架构
Authentication
用户的认证信息由Authentication
的实现类保存。
Authentication接口定义如下:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities () ;
Object getCredentials ( ) ;
Object getDetails ( ) ;
Object getPrincipal ( ) ;
boolean isAuthenticated ( ) ;
void setAuthenticated (boolean isAuthenticated) ;
}
这里接囗中定义的方法如下:
- getAuthorities 方法:用來获取用户的权限。
- getCredentials 方法:用來获取用户凭证,一般來说就是密码。
- getDetails 方法:用来获取用户携带的详细信息,可能是当前请求之类等。
- getPrincipal方法:用來获取当前用户,例如是一个用户名或者一个用户对象。
- isAuthenticated :当前用户是否认证成功。
AuthenticationManager
springsecurity中的认证工作主要由AuthenticationManager接囗来负责。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager 只有一个 authenticate 方法可以用来做认证,该方法有三个不同的返回值:
- 返回 Authentication,表示认证成功。
- 抛出 AuthenticationException异常,表示用户输入了无效的凭证。
- 返回 null,表示不能断定。
ProviderManager
AuthenticationManager 最主要的实现类是 ProviderManager
,ProviderManager
管理了众多的 AuthenticationProvider
实例,AuthenticationProvider
有点类似于 AuthenticationManager
,但是它多了一个 supports 方法用来判断是否支持给定的 Authentication 类型。
AuthenticationProvider
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
由 于 Authentication 拥 有 众 多 不 同 的 实 现 类 , 这 些 不同的实现类又由不同的AuthenticationProvider
来处理,所以AuthenticationProvider
会有一个 supports 方法,用来判断当前的 AuthenticationProvider
是否支持对应的 Authentication。
在一次完整的认证流程中,可能会同时存在多个AuthenticationProvider(例如,项目同时支持 form 表单登录和短信验证码登录),多个 AuthenticationProvider 统一由ProviderManager
来管理。同时,ProviderManager 具有一个可选的 parent,如果所有的AuthenticationProvider都认证失败,那么就会调用 parent 进行认证。parent 相当于一个备用认证方式,即各个AuthenticationProvider 都无法处理认证问题的时候,就由 parent 出场收拾残局。
授权
在 Spring Security 的授权体系中,有两个关键接口:
- AccessDecisionManager
- AccessDecisionVoter
AccessDecisionVoter 是一个投票器,投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票;AccessDecisionManager 则是一个决策器,来决定此次访问是否被允许。AccessDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager中会挨个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AccessDecisionVoter和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。
在 Spring Security 中,用户请求一个资源(通常是一个网络接口或者一个 Java 方法)所需要的角色会被封装成一个 ConfigAttribute 对象,在 ConfigAttribute 中只有一个 getAttribute方法,该方法返回一个 String 字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE_前缀,投票器 AccessDecisionVoter 所做的事情,其实就是比较用户所具备的角色和请求某个资源所需的 ConfigAttribute 之间的关系。
web安全
在 Spring Security 中,认证、授权等功能都是基于过滤器来完成的。
开发者所见到的 Spring Security 提供的功能,都是通过这些过滤器来实现的,这些过滤器按照既定的优先级排列,最终形成一个过滤器链。开发者也可以自定义过滤器,并通过@Order注解去调整自定义过滤器在过滤器链中的位置。
需要注意的是,默认过滤器并不是直接放在 Web 项目的原生过滤器链中,而是通过一个FilterChainProxy 来统一管理。Spring Security 中的过滤器链通过 FilterChainProxy 嵌入到 Web项目的原生过滤器链中。
登录数据保存
如果不使用 Spring Security 这一类的安全管理框架,大部分的开发者可能会将登录用户数据保存在Session中,事实上,Spring Security也是这么做的。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。
当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder中。SecurityContextHolder 中的数据保存默认是通过 ThreadLocal 来实现的,使用 ThreadLocal创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContextHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来
保存到 Session 中,然后将 SecurityContextHolder 中的数据清空。