作者:Dayeh@小米安全中心 首席音乐家
Spring Security 5.1.4 RELEASE
对于使用Java进行开发的同学来说,Spring算是一个比较热门的选择,包括现在流行的Spring Boot更是能快速上手,并让开发者更好的关注业务核心的开发,而免去了原来冗杂的配置过程。从Spring 4开始推荐使用代码进行配置,更是降低了配置的难度,远离了让人看得头大的xml配置文件。
这篇文章主要想系统的介绍一下Spring Security这个框架。当需要进行一些认证授权的开发时,常用的Java安全框架主要有Apache Shiro和Spring Security。两者相比,Shiro更为轻量化,简单易用,而Spring Security作为Spring的亲儿子,功能更强大,和Spring项目的结合度更好,而使用的学习成本相较于Shiro会略高一些。
在讲代码之前,想先介绍一下Spring Security中的一些基本组件及服务,以便于更好理解后文的代码。基本架构的介绍,主要来自于官方文档,进行了选择性的翻译,参考的版本是Spring Security 5.1.4 RELEASE,感兴趣的同学也可以直接前往阅读英文原文。
1 基本架构
1.1 核心组件
从Spring Security 3.0开始,组件spring-security-corejar包中的内容进行了精简,不再包含任何web应用安全,LDAP或命名空间配置的代码。
SecurityContextHolder
最基本的对象,用来存储当前的安全上下文(security context),包含了当前登录的用户信息。默认使用ThreadLocal保存细节信息,因此这些信息对于同一个线程内容调用的方法都是可用的。当用户的请求处理完成后,框架会自动清理线程而不必用户关心。但是由于某些应用由于其对线程的使用方式,并不适合使用ThreadLocal,则需要根据情况,在启动前设置SecurityContextHolder的策略。
在SecurityContextHolder中保存了当前活跃用户的信息。Spring Security使用一个Authentication对象来表示这些信息。在程序的任何地方,都可以用以下代码来获取当前认证用户的信息。
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
UserDetailsService
在接口UserDetailService中只有一个方法,接收一个字符串返回一个UserDetails对象,当认证成功后,UserDetails会被用来构造一个Authentication对象保存在SecurityContextHolder中。
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
UserDetailsService的实现,主要是用来加载用户信息,并向其他组件提供这些信息,并不能进行认证用户的操作,认证的操作由AuthenticationManager完成。
如果用户想自定义一个认证流程,则需要实现AuthenticationProvider接口。
GrantedAuthority
Authentication的getAuthorities( )方法返回GrantedAuthority的对象数组。GrantAuthority对象一般由UserDetailsService加载。
总结
ScurityContextHolder获取SecurityContext
SecurityContext保存了Authentication以及其他一些请求相关的安全信息
Authentication表示一个认证用户信息
GrantedAuthority表示授予用户的权限信息
UserDetails包含了构建Authentication对象需要的必要信息,这些信息来自于应用的DAO或其他数据源
UserDetailsService根据传入用户名字符串构建一个UserDetails对象
1.2 认证环节
一个基本的认证环节包括:
- 用户输入用户名和密码
- 系统验证用户名密码正确
- 系统获取该用户的角色、权限等信息。
以上三个步骤完成了一个认证过程,在Spring Security中,相应地完成了以下动作:
- 后端获取到用户名和密码并用之生成一个UsernamePasswordAuthenticationToken对象,UsernamePasswordAuthenticationToken是Authentication的一个实现类。
- token被传入AuthenticationManager的实例中进行校验
- 认证成功后,AuthenticationManager会返回一个Authentication实例,其中包括了用户所有细节信息,包括角色、权限等
- 通过调用SecurityContextHolder.getContext().setAuthentication(…)建立安全上下文,传入Authentication对象
完成以上过程后,当前用户认证完成。以下代码示范了一个认证环节最基本的流程(并非SpringSecurity框架源码)
import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
public class AuthenticationExample {
// 0. 创建一个AuthenticationManager实例,之后用于用户校验 (具体实现在下方)
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
// 1. 用户在界面输入用户名密码
while(true) {
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
// 2. 用户名和密码生成一个UsernamePasswordAuthenticationToken对象
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
// 3. 使用AuthenticationManager实例校验token
Authentication result = am.authenticate(request);
// 4. 校验成功,将包含用户信息的Authentication实例加入security context
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch(AuthenticationException e) {
// 认证失败,捕获异常
System.out.println("Authentication failed: " + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains: " +
SecurityContextHolder.getContext().getAuthentication());
}
}
// AuthenticationManager的实现
class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
public Authentication authenticate(Authentication auth) throws AuthenticationException {
//该方法内写认证通过的条件,此处demo判断条件是,用户名等于密码即认证通过
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
直接设置SecurityContextHolder
Spring Security并不关心一个Authentication实例是如何放进SecurityContextHolder的,只要保证在SecurityContextHolder中有一个有效的Authentication表示一个认证的用户即可,然后AbstractSecurityInterceptor便可用来授权用户的操作了。因此,开发者亦可选择使用其他认证框架提供的认证信息。开发者只需要写使用一个过滤器获取来自第三方的用户信息,然后构造一个Authentication对象放入到SecurityContextHolder即可。但是如果不使用内置的认证,有些原本自动完成的事情就需要由开发者来处理了,比如需要在一开始创建HTTP session来缓存上下文。
1.3 Web应用中的认证
在处理Web应用中的认证,Spring Security中主要参与的有ExceptionTranslationFilter和AuthenticationEntryPoint,以及一个认证机制,用来调用AuthenticationManager完成核心的认证部分。
ExceptionTranslationFilter
是一个Spring Security的过滤器,用来检测所有抛出的Spring Security的异常,通常这些异常都由AbstractSecurityInterceptor抛出。AbstractSecurityInterceptor只负责抛出异常,而ExceptionTranslationFilter则负责确定如何处理异常,比如当前用户认证了但权限不够,则返回403错误码,或者当前用户还未认证,则发起一个AuthenticationPoint。
Authentication Mechanism
当浏览器提交用户的认证信息后,服务器需要收集这些信息,而在Spring Security中,从客户端获取认证信息的功能称为“认证机制” (authentication mechanism)。比如Basic authentication,当收集到客户端提交的认证信息后,后端就会创建一个Authentication对象,然后交给AuthenticationManager校验。
随后authentication mechanism会收到一个包含完整信息的Authentication对象,并认为请求合法,然后把Authentication放入SecurityContextHolder,随后原请求便会发起重试。如果AuthenticationManager拒绝了请求,那么认证机制便会要求客户端重试。
保存用户认证信息
一般在一个Web应用中,用户登录后,服务器会缓存用户的认证信息,用户后续的操作通过其session id进行身份认证。Spring Security框架中,保存SecurityContext的任务交给SecurityContextPersistenceFilter,其默认将安全上下文信息保存为HttpSessio属性。每当请求来,它都会通过SecurityContextHolder来获取认证信息,并在请求结束后,清除SecurityContextHolder。出于安全考虑,不要直接从HttpSession中去获取安全上下文,而应该通过SecurityContextHolder获取。
1.4 权限控制(授权)
Spring Security的权限控制,依赖于AOP。权限控制可以应用在方法调用上,也可用在web请求上。Spring Security中主要负责进行权限控制决定的是AccessDecisionManager。
Secure Objects
安全对象指一切可以加上安全配置的对象,最常见的例子是方法的调用和web请求。
每个支持的安全对象都有一个自己的拦截器,这个拦截器是AbstractSecurityInterceptor子类,当AbstractSecurityInterceptor被调用的时候,SecurityContextHolder中会包含一个有效的Authentication对象,如果当前用户主体已经被认证。AbstractSecurityInterceptor在处理安全对象的请求时候,流程如下:
- 查看和当前请求关联的配置属性(configuration attributes)
- 将当前的安全对象、Authentication对象以及配置属性提交给AccessDecisionManager,由其做一个授权的决定。
- 在调用发生的位置可选的更换Authentication对象
- 完成授权后,允许安全对象的调用进行。
- 当调用返回后,即刻调用AfterInvocationManager(如果配置了)
Configuration Attributes
配置属性用接口ConfigAttribute表示,可以理解为被AbstractSecurityInterceptor使用的具有特殊意义的字符串。AbsractSecurityInterceptor中配置了SecurityMetadataSource用来查看安全对象的配置属性。配置属性可以用来简单表示一个角色名,或者更复杂的意义,它的用处取决于AccessDecisionManager实现的复杂性。或者简单来说,配置属性只是表示特殊含义的,比如角色名的字符串,但是其具体如何解读,取决于AccessDedcisionManager的实现。举个简单得例子,当我们使用默认的AccessDedcisionManager的实现时,可以在一个方法或者一个url请求的注释里加入配置属性ROLE_A,这表示,只有当用户的GrantedAuthority匹配ROLE_A的时候,才被允许使用这个方法或调用这个请求。这里只做简单得说明,具体使用在后文中展开。
Security interceptors and the “secure object” model
下图是安全拦截器和安全对象的模型,给出了各个组件之间的关系,有个别组件在简介中没有提到,将在后文的使用说明中展开。