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
,感兴趣的同学也可以直接前往阅读英文原文。
从Spring Security 3.0开始,组件 spring-security-core
jar包中的内容进行了精简,不再包含任何web应用安全,LDAP或命名空间配置的代码。
最基本的对象,用来存储当前的安全上下文(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(); }
在接口 UserDetailService
中只有一个方法,接收一个字符串返回一个 UserDetails
对象,当认证成功后, UserDetails
会被用来构造一个 Authentication
对象保存在 SecurityContextHolder
中。
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
UserDetailsService
的实现,主要是用来加载用户信息,并向其他组件提供这些信息,并不能进行认证用户的操作,认证的操作由 AuthenticationManager
完成。
如果用户想自定义一个认证流程,则需要实现 AuthenticationProvider
接口。
Authentication
的 getAuthorities( )
方法返回 GrantedAuthority
的对象数组。 GrantAuthority
对象一般由 UserDetailsService
加载。
ScurityContextHolder
获取 SecurityContext
SecurityContext
保存了 Authentication
以及其他一些请求相关的安全信息
Authentication
表示一个认证用户信息
GrantedAuthority
表示授予用户的权限信息
UserDetails
包含了构建 Authentication
对象需要的必要信息,这些信息来自于应用的DAO或其他数据源
UserDetailsService
根据传入用户名字符串构建一个 UserDetails
对象
一个基本的认证环节包括:
用户输入用户名和密码
系统验证用户名密码正确
系统获取该用户的角色、权限等信息。
以上三个步骤完成了一个认证过程,在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来缓存上下文。
在处理Web应用中的认证,Spring Security中主要参与的有 ExceptionTranslationFilter
和 AuthenticationEntryPoint
,以及一个认证机制,用来调用 AuthenticationManager
完成核心的认证部分。
是一个Spring Security的过滤器,用来检测所有抛出的Spring Security的异常,通常这些异常都由 AbstractSecurityInterceptor
抛出。 AbstractSecurityInterceptor
只负责抛出异常,而 ExceptionTranslationFilter
则负责确定如何处理异常,比如当前用户认证了但权限不够,则返回403错误码,或者当前用户还未认证,则发起一个 AuthenticationPoint
。
当浏览器提交用户的认证信息后,服务器需要收集这些信息,而在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
获取。
Spring Security的权限控制,依赖于AOP。权限控制可以应用在方法调用上,也可用在web请求上。Spring Security中主要负责进行权限控制决定的是 AccessDecisionManager
。
安全对象指一切可以加上安全配置的对象,最常见的例子是方法的调用和web请求。
每个支持的安全对象都有一个自己的拦截器,这个拦截器是 AbstractSecurityInterceptor
子类,当 AbstractSecurityInterceptor
被调用的时候, SecurityContextHolder
中会包含一个有效的 Authentication
对象,如果当前用户主体已经被认证。 AbstractSecurityInterceptor
在处理安全对象的请求时候,流程如下:
查看和当前请求关联的配置属性(configuration attributes)
将当前的安全对象、 Authentication
对象以及配置属性提交给 AccessDecisionManager
,由其做一个授权的决定。
在调用发生的位置可选的更换 Authentication
对象
完成授权后,允许安全对象的调用进行。
当调用返回后,即刻调用 AfterInvocationManager
(如果配置了)
配置属性用接口 ConfigAttribute
表示,可以理解为被 AbstractSecurityInterceptor
使用的具有特殊意义的字符串。 AbsractSecurityInterceptor
中配置了 SecurityMetadataSource
用来查看安全对象的配置属性。配置属性可以用来简单表示一个角色名,或者更复杂的意义,它的用处取决于 AccessDecisionManager
实现的复杂性。或者简单来说,配置属性只是表示特殊含义的,比如角色名的字符串,但是其具体如何解读,取决于 AccessDedcisionManager
的实现。举个简单得例子,当我们使用默认的 AccessDedcisionManager
的实现时,可以在一个方法或者一个url请求的注释里加入配置属性ROLE_A,这表示,只有当用户的 GrantedAuthority
匹配ROLE_A的时候,才被允许使用这个方法或调用这个请求。这里只做简单得说明,具体使用在后文中展开。
下图是安全拦截器和安全对象的模型,给出了各个组件之间的关系,有个别组件在简介中没有提到,将在后文的使用说明中展开。