Spring Security的架构与实现
在熟悉了Spring Security的配置和运行了一些应用之后,你肯定想要进一步了解整个框架是如何运行的。像大多数软件一样,Spring Security框架中有一些常用的接口、类和抽象概念。接下来我们将要研究它们是如何协同工作来支持认证和访问控制的。
概述
运行时环境
Spring Security 3.0需要Java 5.0及以上的版本。因为Spring Security旨在独立运行,所以不需要添加任何特殊的配置文件到运行时环境中。也不需要配置特殊的JAAS或者将Spring Security放到classpath下。
如果使用的是EJB容器或者Servlet容器,也不需要任何特殊的配置文件,或者引入Spring Security。所有需要的文件都包含在应用程序中。
这种设计提供了最大的部署灵活性,因为可以将应用的包复制到任何系统并立即运行。
核心组件
在Spring Security 3.0中,spring-security-core
jar包进行了最大程度的精简。不再包含任何与web应用安全、LDAP或者命名空间配置相关的代码。接下来将看到一些核心模块中的Java类型,它们代表着框架的骨架,如果想进行命名空间配置之外的深度开发,需要先弄明白它们是什么(虽然实际上不需要直接与这些类交互)。
SecurityContextHolder, SecurityContext和Authentication对象
最基础的对象就是SecurityContextHolder
,其中存储了应用的安全上下文,包含最近使用应用的 principal。默认情况下,使用ThreadLocal
存储这些信息,也就是说,同一个线程中的方法可以使用此线程的安全上下文,即使上下文没有作为参数显式传递到方法中。用这种方式使用ThreadLocal
是相当安全的,因为在处理当前principal的请求之后清除线程。Spring Security会自动处理这些工作。
principal,通常是指一个用户、设备或者其他可以与应用交互的系统。
有些应用由于用特殊的方式使用了线程所以不适合使用ThreadLocal
。例如,Swing 客户端希望所有线程使用同一个安全上下文。SecurityContextHolder
支持在启动时配置策略,指定存储上下文的方式。独立应用可以使用SecurityContextHolder.MODE_GLOBAL
策略。其他应用可能希望安全线程创建的线程也具有相同的安全特性,可以使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
。有两种方式可以将默认的SecurityContextHolder.MODE_THREADLOCAL
模式转换为其他模式:设置系统属性;调用SecurityContextHolder
的静态方法。大部分应用不需要改变默认模式,但是如果有需要,可以查阅SecurityContextHolder
的JavaDoc。
获取当前用户的相关信息
SecurityContextHolder
中存储了当前与应用交互的principal的详细信息。Spring Security使用一个Authentication
对象来表示这个信息。通常情况下不需要自己创建Authentication
,但可能需要获取它。在应用的任何地方,使用以下代码块来获取当前认证用户的名字:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
调用getContext()
返回的对象是SecurityContext
接口的一个实例,这个对象就是保存在线程中的。接下来将看到,Spring Security中的认证大都返回一个UserDetails
的实例作为principal。
UserDetailsService
上面的代码块中,还有一个需要注意的地方就是,可以从Authentication
对象中获取principal。大多数情况下可以转换成UserDetails
对象。UserDetails
是Spring Security中的一个核心接口。它表示一个principal,但是是可扩展的、特定于应用的。可以认为UserDetails
是数据库中用户表记录和Spring Security在SecurityContextHolder
中所必须信息的适配器。当它表示数据库中用户表的记录的时候,通常会将UserDetails
转换成应用中提供的原始对象,以便于调用某些业务方法(例如:getEmail()
,getEmployeeNumber()
等等)。
现在,你肯能会问,应该什么时候提供一个UserDetails
对象呢?如何提供呢?以上所说的都是声明式的,不必编写任何Java代码,其中发生了什么呢?最简单的回答是,有一个特殊的接口UserDetailsService
。这个接口中仅有的一个方法接收一个String类型的用户名参数,返回UserDetails
:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
在Spring Security中,这是获取用户信息最常用的方法,这个方法遍布在框架中,只要框架需要用户信息,就会使用这个方法。
如果认证成功,UserDetails
会被用来创建Authentication
对象,这个对象存放在SecurityContextHolder
中(有关详情,查阅)。Spring Security提供了许多UserDetailsSerivice
接口的实现,包括使用内存中map的实现(InMemoryDaoImpl
)和使用JDBC的实现(JdbcDaoImpl
)。但开发者更喜欢自己编写,在已存在的表示员工、客户、或其他应用用户的DAO上实现。记住不管自定义UserDetailsService
返回什么,都可以用上面的代码块从SecurityContextHolder
中获取。
对于
UserDetailsService
经常会有一些困惑。它是纯粹的用户数据的DAO,除了将用户数据提供给框架内的其他组件,没有任何其他功能。
GrantedAuthority
除了principal,Authentication
提供的另外一个比较重要的方法是getAuthorities()
。这个方法提供一个GrantedAuthority
对象的数组。GrantedAuthority
是授权给某个principal的权限。这些权限通常是角色,例如:ROLE_ADMINISTRATOR
或者ROLE_HR_SUPERVISOR
。稍后将为这些角色配置web权限,方法权限和域对象权限。Spring Security能解析这些权限,并希望出现这些权限。GrantedAuthority
对象通常由UserDetailsService
加载。
通常情况下,GrantedAuthority
对象是应用范围的权限。没有特定于给定的域对象。所以,不要用一个GrantedAuthority
对象来表示工号为54的Employee
对象,这样依赖会有成千上万个这样的权限,最终导致内存耗尽(在最好的情况下,也会使得应用在认证上耗费很长的时间)。 Spring Security专门设计用于处理此类需求,但应该使用项目的域对象安全性来实现此功能。
小结
目前为止,已经讲解了下面这些Spring Security的主要模块:
- SecurityContextHolder
,用于获取SecurityContext
。
- SecurityContext
,存放了Authentication
和特定于请求的安全信息。
- Authentication
,特定于Spring Security的principal。
- GrantedAuthority
,对某个principal的应用范围内的授权许可。
- UserDetail
,提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息。
- UserDetailsService
,接受String类型的用户名,创建并返回UserDetail
。
现在已经了解了这些安全组件,接下来看看认证的流程。
认证
Spring Security适用于许多不同的认证环境中。虽然建议使用Spring Security进行身份验证,而不是与原有的容器管理身份验证集成,但仍然支持与原有身份验证系统集成。
在Spring Security中,什么是认证
考虑一个大家都熟悉的认证场景:
1. 提示用户使用用户名和密码登录。
2. 系统验证此用户的密码正确。
3. 获取该用户的上下文信息(角色等等)。
4. 用户继续操作,可能执行一些受访问控制机制保护的操作根据当前安全上下文信息检查该操作所需要的许可。
前三项构成了安全认证过程,因此接下来将了解在Spring Security中这些步骤是如何进行的。
1. 获取用户名和密码,并构建一个UsernamePasswordAuthenticationToken
实例(之前提到过的Authentication
接口的一个实例)。
2. token传递给AuthenticationManager
实例进行验证。
3. AuthenticationManager
在成功验证后返回填充好的Authentication实例。
4. 传入返回的authentication实例,通过调用SecurityContextHolder.getContext().setAuthentication(…)
方法构建SecurityContext。
从这个时候开始,用户就是认证过的了。下面是代码示例:
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 {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
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 {
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
Authentication result = am.authenticate(request);
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());
}
}
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 {
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
这里编写了一个小程序,要求用户输入用户名和密码然后顺序往下执行。这里实现的AuthenticationManager
会认证任何用户名和密码相同的用户。并赋予每个用户一个角色。这个程序的输出如下:
Please enter your username:
bob
Please enter your password:
password
Authentication failed: Bad Credentials
Please enter your username:
bob
Please enter your password:
bob
Successfully authenticated. Security context contains: \
org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: \
Principal: bob; Password: [PROTECTED]; \
Authenticated: true; Details: null; \
Granted Authorities: ROLE_USER
通常不需要写这样的代码。认证过程会在内部执行,例如web认证的filter。这里展示这段代码回答了“在Spring Security中真正构成认证过程的是什么”这个问题。当SecurityContextHolder
包含一个完全填充的authentication
对象时,这个用户就是经过认证的。
直接设置SecurityContextHolder的内容
实际上,Spring Security不关心如何将Authentication
对象放到SecurityContextHolder
中。唯一的要求是,SecurityContextHolder
中要存储Authentication
。表示AbstractSecurityInterceptor
(详情见下文)需要授权用户操作之前的principal。
可以编写自己的过滤器或MVC控制器,提供与不基于Spring Security的身份验证系统的互操作性。仅需要编写一个filter从本地读取第三方用户信息,构建Spring Security的Authentication
对象,并将其放置到SecurityContextHolder
中。如果编写了自定义的filter,还需要考虑认证架构中自动处理掉的一些事情。比如,在响应之前,需要先创建一个HttpSession来缓存请求之间的上下文(响应提交之后不能创建session)。
如果想要了解AuthenticationManager
实现的真实示例,可以查阅核心服务章节。
web应用中的认证
现在研究一下在web应用中使用Spring Security的情况(不用web.xml
启用security)。用户如何认证?安全上下文如何构建?
考虑一个典型的web应用认证过程:
1. 访问主页,点击一个链接。
2. 服务器收到请求,服务器确定需要一些受保护的资源。
3. 因为还未认证,服务器返回一个响应,要求用户进行认证。这个响应可以是一个Http返回码,也可以重定向到一个特殊的web页面。
4. 取决于认证机制,浏览器会重定向到一个特殊的web页面,这个页面可以填写表单;或者浏览器会以某种方式获取你的身份(通过BASIC anthencitation对话框、cookie、或者X.509 certificate)。
5. 浏览器回送响应到服务器。这可能是一个包含表单中填写信息的HTTP POST请求,或者包含认证信息的HTTP头。
6. 服务器会验证当前的凭证是否是有效的。如果是,会到下一步。如果不是,浏览器会重新进行认证步骤(回到步骤2)。
7. 触发认证过程的原始请求会重试。如果有足够的权限,请求会成功。否则,会收到返回的HTTP Code 403,也就是说禁止访问。
对于以上的大多数步骤,Spring Security都有不同的类负责。主要有(按照被使用的顺序):ExceptionTranslationFilter
,AuthenticationEntryPoint
,和负责调用上文提到过的AuthenticationManager
的“authentication mechanism”。
ExceptionTranslationFilter
ExceptionTranslationFilter
是一个Spring Security filter,负责侦测所有抛出的Spring Security异常。通常授权服务的主要提供者AbstractSecurityInterceptor
会抛出这些异常。下一章会讨论AbstractSecurityInterceptor
,但现在只需要知道它会抛出java异常但不了解任何HTTP或者如何认证一个principal。ExceptionTranslationFilter
提供了这些服务,并负责返回错误码403(如果principal已经经过认证,但没有足够的访问权限,参照上面的第7步),或者启动AuthenticationEntryPoint
(如果principal尚未认证,将回到第三步)。
AuthenticationEntryPoint
AuthenticationEntryPoint
负责列表中的第三步。可以想象,每个web应用都有默认的认证策略。主要的身份验证系统都有自己的AuthenticationEntryPoint
实现,以执行第三步中的动作。
认证机制
一旦浏览器提交了认证凭证(Http表单post请求或者Http头),服务器需要收集这些认证信息。现在到了第六步。在Spring Security中,对从用户端(通常是浏览器)收集用户认证信息的方法有一个特殊的名字,称作“认证机制”。例如基于表单的登录和BASIC认证。当从用户端收集到认证信息后,就会构建Authentication
请求对象,并交给AuthenticationManager
。
当认证机制返回完全填充的Authentication
对象,则表示请求是有效的,将Authentication
对象放到SecurityContextHolder
中,并触发重试原始的请求(第七步)。如果AuthenticationManager
拒绝这个请求,认证机制会要求用户端重新从第二步开始。
在请求之间存储SecurityContext
根据应用程序的类型,可能需要采用策略来在用户操作之间存储安全上下文。在典型的Web应用程序中,用户登录过后,会由其session ID标识。服务器在会话时间内缓存principal信息。在Spring Security中,在请求之间存储SecurityContext
的工作由SecurityContextPersistenceFilter
负责,默认将上下文存储为HTTP请求之间的HttpSession
。它为每个请求恢复上下文到SecurityContextHolder
中,更重要的是,当请求完成时还会清除SecurityContextHolder
。出于安全目的,不应该直接与HttpSession
交互。通常会使用SecurityContextHolder
。
许多其他类型的应用(例如无状态的RESTful web服务)不使用Http Session,且为每一个请求都重新认证。但是,在filter chain中引入SecurityContextPersistenceFilter
来确保每个请求之后都清除了SecurityContextHolder
仍然很重要。
在单个session中接受并发请求的应用中,同一个
SecurityContext
实例会被线程共享。虽然使用了ThreadLocal
,但每个请求从HttpSession
中获取到的都是同一个实例。如果使用SecurityContextHolder.getContext()
,并在返回的上下文对象上调用setAuthentication(anAuthentication)
方法,那么Authentication
对象会在共享同一个SecurityContext
实例的并发线程中修改。可以自定义SecurityContextPersistenceFilter
的行为来创建为每个请求创建一个全新的SecurityContext
,防止一个线程中的变动影响到其他线程。或者改动上下文的时候创建一个新的实例。SecurityContextHolder.createEmptyContext()
方法始终返回一个全新的上下文实例。
Spring Security中的访问控制(授权)
在Spring Security中,主要负责做出授权决定的就是AccessDecisionManager
。它有一个decide
方法接受一个代表principal请求访问Authentication
的对象,一个“安全对象”(见下文)和一个适用于该对象的安全元数据属性列表(例如访问需要的授权角色)。
AOP Advice和安全
如果熟悉AOP,就会知道这几种不同类型的advice:before,after,throws和around。around advice非常有用,因为advisor可以选择是否继续进行方法的调用,是否修改返回值,是否抛出异常。Spring Security为方法调用和web请求提供了around advice。Spring Security使用了Spring标准的APO支持来为方法执行进行around advice,还通过标准的filter为web请求实现around advice。
如果不熟悉AOP,知道Spring Security可以帮助保护方法执行和web请求就行了。大部分人对于在service层保护方法的执行更感兴趣。因为在当前Java EE应用,service层是大多数业务逻辑所在。如果只需要保护service层的方法执行,Spring AOP就足够了。如果需要直接保护域对象,可以考虑一下AspectJ。
可以选择通过ApectJ或Spring AOP执行方法授权,或者通过使用filter来执行web请求的授权。还可以将这些方法组合起来。主流的使用模式是执行一些web请求授权,和一些service层Spring AOP的方法执行授权。
AbstractSecurityInterceptor与安全对象
什么是“安全对象”?Spring Security使用这个术语来表示任何可以应用安全保护(例如授权)的对象。最常见的例子是方法执行和web请求。
每个被支持的安全对象类型都有自己的拦截器类,这些拦截器类都是AbstractSecurityInterceptor
的子类。当AbstractSecurityInterceptor
被调用的时候,如果principal已经认证了,SecurityContextHolder
会存储一个有效的Authentication
。
AbstractSecurityInterceptor
为处理安全对象提供了一致的工作流程:
1. 查找与当前请求关联的“配置属性”
2. 将安全对象,当前Authentication
和配置属性提交给到AccessDecisionManager
以进行授权决策
3. 在调用时改变Authentication
(可选)
4. 允许安全对象调用继续(如果访问被授权)
5. 调用返回后,如果有配置,就调用AfterInvocationManager
。如果调用抛出异常,AfterInvocationManager
不会执行。
什么是配置属性
“配置属性”可以被认为是对于被AbstractSecurityInterceptor
使用的类有特殊意义的字符串。在框架中由ConfigAttribute
接口表示。取决于AccessDecisionManager
实现的复杂程度,它可能是简单的角色名或更复杂的含义。AbstractSecurityInterceptor
配置了一个SecurityMetadataSource
,这个对象可以用来查找某个安全对象的属性。通常这个配置是对用户隐藏的。配置属性会作为被保护方法的注解或者被保护URL的访问属性加入。例如,当在命名空间介绍中看到类似<intercept-url pattern='/secure/' access='ROLE_A,ROLE_B'/>
的时候,意味着配置属性ROLE_A
和ROLE_B
适用于给定模式匹配的web请求。在实际开发中,使用默认AccessDecisionManager
配置,意味着,任何人的GrantedAuthority
,只要匹配这两个属性中的任何一个,就会被允许访问。严格来说,它们仅仅是属性而已,并且它们的解释取决于AccessDecisionManager
的实现。前缀ROLE_
表示这些属性是角色,应该由Spring Security的RoleVoter
使用。这仅在使用基于voter的AccessDecisionManager
时才会有效,AccessDecisionManager
如何实现在授权章节中。
RunAsManager
假设AccessDecisionManager
允许请求,AbstractSecurityInterceptor
通常只会继续这个请求。话虽如此,但在某些极端情况下,用户可能希望使用不同的Authentication
替换SecurityContext
中的Authentication
,这由AccessDecisionManager
调用RunAsManager
处理。这在合理的极端情况下是有用的,例如某个服务层方法需要以不同的身份调用远程系统。因为Spring Security自动将安全身份标识自动从某个服务器传播到其他服务器(假设使用了正确配置的RMI活着HttpInvoker远程协议客户端),这可能很有用。
AfterInvocationManager
安全方法调用继续并返回,这可能意味着某个方法完成了或者某个filter链的继续,AbstractSecurityInterceptor
获得最后的机会来处理调用。此阶段,AbstractSecurityInterceptor
可能会修改返回对象。我们可能希望这种情况发生,因为无法在安全对象调用的“途中”进行授权决策。为了高度可拔插性,AbstractSecurityInterceptor
会将控制权传递给AfterInvocationManager
,以便在需要时修改对象。这个类甚至能完全替换掉对象,或者抛出异常,或者不以任何方式改变它。只有在调用成功后才会执行调用后检查。如果有任何的异常,这个检查会被跳过。
AbstractSecurityInterceptor
及其相关对象如图所示。
扩展安全对象模型
只有开发人员考虑采用全新的拦截和授权请求方式才需要直接使用安全对象。例如,可以构建新的安全对象以保护对消息系统的调用。任何需要安全保护并且支持调用拦截(如AOP的around advice语义)的事物都能够成为安全对象。但大多数Spring应用只使用当前完全透明的支持的三种安全对象类型(AOP Alliance MethodInvocation,AspectJ JoinPoint和Web请求FilterInvocation)。
本地化
略
核心服务
现在已经概览了Spring Security体系结构及其核心类,接下来仔细研究几个核心接口机器实现,特别是AuthenticationManager
,UserDetailsService
和AccessDecitionManager
。
AuthenticationManager, ProviderManager 和 AuthenticationProvider
AuthenticationManager
只是一个接口,因此实现可以任意选择,但它在实践中如何工作呢?如果我们需要检查多个认证数据库或不同认证服务(如数据库和LDAP服务器)的组合,该怎么办?
在Spring Security中默认实现是ProviderManager
,但它并没有自己处理认证请求,而是代理给了配置的一组AuthenticationProdicer
,依次查询它们是否能进行认证。每个provider将抛出异常或返回完全填充的Authentication
对象。还能想起来UserDetails
和UserDetailsService
吗?如果想不起来的话,回头看一看上一章中的相关内容吧。验证一个认证请求最常见的方法是加载正确的UserDetails
并检查根据加载的密码检查用户输入的密码。DaoAuthenticationProvider
使用的就是这个方法(见下文)。加载的UserDetails
对象特别是它所包含的GrantedAuthority
,会在构建完全填充的Authentication
对象时用到,这个Authentication
对象在验证成功时被返回,并存储到SecurityContext
中。
如果使用命名空间,会在内部创建和维护一个ProviderMaanger
,可以通过authentication命名空间在xml中配置添加provider。在这种情况下,不应该在应用上下文中自己声明ProviderManager
bean。然而,如果没有使用authentication命名空间配置,可以如下声明:
<bean id="authenticationManager"
class="org.springframework.security.authentication.ProviderManager">
<constructor-arg>
<list>
<ref local="daoAuthenticationProvider"/>
<ref local="anonymousAuthenticationProvider"/>
<ref local="ldapAuthenticationProvider"/>
</list>
</constructor-arg>
</bean>
在上面的实例中有三个provider。它们按照出现的顺序执行(使用List
表示),每个provider都进行认证,或者返回null
跳过认证。如果所有实现都返回null,ProviderManager
会抛出一个ProviderNotFountException
。如果对链接provider感兴趣,可以查阅ProviderManager
的JavaDoc。
诸如web表单登录处理filter之类的认证机制,会被注入ProviderManager
的引用,并调用它来处理认证请求。需要的的provider提供程序有时可以与身份验证机制互换,而在其他时候,它们将依赖于特定的身份验证机制。例如,DaoAuthenticationProvider
和LdapAuthenticationProvider
兼容任何提交用户名、密码认证请求的机制,因此可以使用基于表单的登录或HTTP Basic 认证。另一方面,有些认证机制创建只能被某一类型的AuthenticationProvicer
处理的认证请求对象。一个例子是JA-SIG CAS,它使用服务票据的概念,因此只能由CasAuthenticationProvider
进行身份验证。不不要太在意这个问题,因为如果忘记注册合适的provider,尝试进行认证的时候会抛出ProviderNotFoundException
。
清除成功认证的Credentials
默认情况下(从Spring Security 3.0开始),ProviderManager
会尝试从成功认证请求返回的Authentication
对象中清除敏感的从credentials信息。这防止了类似密码这样的敏感信息的不必要的保留。
但是在使用用户对象缓存的时候,可能会有问题,例如,在无状态的应用中提升性能。如果Authentication
包含缓存中某个对象的引用(例如一个UserDetails
实例)并且这个对象的credentials已经移除,则无法再对缓存的值进行认证。如果使用了缓存,需要注意这一点。要解决这个问题可以在缓存实现中或者在创建返回Authentication
对象的AuthenticationProvider
中创建被引用对象的副本。或者禁用ProviderManager
中的eraseCredentialsAfterAuthentication
属性。详细信息请参阅Javadoc。
DaoAuthenticationProvider
Spring Security对AuthenticationProvider
最简单的实现就是DaoAuthenticationProvider
,它也是该框架最早支持的provider之一。其使用UserDetailsService
(作为一个DAO)来查找用户名,密码和GrantedAuthority
;通过简单地比较提交的UsernamePasswordAuthenticationToken
与UserDetailsService
加载的密码来认证用户。配置这个provider非常简单:
<bean id="daoAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>
PasswordEncoder
是可选的。PasswordEncoder
提供对UserDetailsService
返回的UserDetails
对象中密码的编码和解码。后面会详细介绍。
UserDetailsService的实现
正如上文所述,绝大多数认证的provider都使用了UserDetails
和UserDetailsService
接口。回想一下UserDetailsService
接口只有一个方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
返回的UserDetails
是一个接口,提供保证非空的认证信息(例如:用户名、密码、权限以及账户是否启用)的一系列getter方法。大多数的认证provider会使用UserDetailsService
,即使用户名和密码实际上不会被用在认证中。它们可能为了获取GrantedAuthority
信息而使用返回的UserDetails
对象,因为某些其他系统(如LDAP或X.509或CAS等)承担了实际的验证凭证(Credentials )的责任。
鉴于UserDetailsService
实现起来非常简单,使用者应该能使用自己的持久层策略来轻松地查询到认证信息。同时,Spring Security也提供了一些有用的基础实现。
In-Memory Authentication
使用自定义的UserDetailsService
实现来从持久层引擎中获取信息非常简单,但是许多应用不需要这么复杂。特别是构建prototype应用或者刚开始集成Spring Security,不想要花费过多的时间来配置数据库或者实现UserDetailsService
。对于这种情况,可以使用安全命名空间中的user-service元素:
<user-service id="userDetailsService">
<user name="jimi" password="jimispassword" authorities="ROLE_USER,ROLE_ADMIN" />
<user name="bob" password="bobspassword" authorities="ROLE,USER" />
</user-service>
同样也支持外部properties文件:
<user-service id="userDetailsService" properties="users.properties" />
properties文件应该包含如下格式的数据:
username=password,grantedAuthrity[,grantedAuthority][,enabled|disabled]
例如:
jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE+USER,enabled
JdbcDaoImpl
Spring Security也提供了从JDBC数据源获取认证信息的UserDetaislService
实现。内部使用Spring JDBC,因此避免了ORM的复杂性,只用于存储用户信息。如果应用使用了ORM工具,可能需要自定义UserDetailsService
来重用已有的映射文件。回到JdbcDaoImpl,示例配置如下所示:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="userDetailsService"
class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
可以通过修改上边的DriverManagerDataSource
以使用不同的关系型数据库管理系统。还可以与任何其他Spring配置一样使用从JNDI获取的全局数据源。
权限组
默认情况下,JdbcDaoImpl
为单个用户加载权限,并假设权限直接映射到用户(请参阅数据库schema附录)。另一种方法是将权限划分为组并将组分配给用户。有些开发人员更喜欢这种方法来管理用户权限。有关如何启用组权限的更多信息,请参阅JdbcDaoImpl
的Javadoc。权限组的schema也包含在附录中。
密码编码
Spring Security的PasswordEncoding
接口被用来支持密码的使用,密码会在持久化阶段以某种方式编码。绝对不能以纯文本方式存储密码。应该始终使用单向密码哈希算法(例如使用內建盐值的bcrtpy,并且每个密码的盐值都不一样)。不要使用普通的哈希函数,如MD5或SHA,加盐的版本也不能使用。Bcrypt被有意设计为缓慢且防止离线密码破解的,而标准的哈希算法非常快而且很容易用于在某些硬件上并发测试上千个密码。有些开发者认为这不适用于自己的系统,因为密码库很安全而且没有被离线攻击的风险。如果真的这样认为的话,建议了解一下因为不安全的存储密码而受到攻击的知名网站。使用org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
是一个很好的选择。在其他常见的编程语言中也有兼容的实现,因此从互操作性来说它也是一个很好的选择。
如果使用的是已经有哈希密码的系统,则需要使用与当前算法匹配的encoder(至少在将用户迁移到更安全的方案之前,通常这会涉及到要求用户设置新密码,因为hash是不可逆的)。Spring Security有一个包含传统密码编码实现的包,即org.springframework.security.authentication.encoding
。可以使用新的(bcrypt)或者旧的PasswordEncoder
类型注入到DaoAuthenticationProvider
中。
什么是哈希
密码哈希并不是Spring Security独有的,但对这个概念不熟悉的使用者经常会对其非常疑惑。哈希(或摘要)算法是一个单向函数,其从一些输入数据(例如密码)产生一段固定长度的输出数据(哈希)。例如,字符串“password”(十六进制)的MD5哈希值是
5f4dcc3b5aa765d61d8327deb882cf99
在某种意义上,哈希是“单向的”,即在给定哈希值的情况下,计算出原始值或者实际上任何可能产生该哈希值的值是非常困难的(实际上是不可能的)。这个性质使哈希值对于身份验证非常有用。它们可以作为明文密码的替代方法存储在用户数据库中,即使这些值被泄露,也不会立即泄露可用于登录的真实密码。注意,这也意味着无法在编码后恢复密码。
hash加盐
使用密码哈希的一个潜在问题是,如果输入常用字,很容易绕过哈希的单向性。用户通常喜欢设置相似的密码,并且可以在网络上大量获取以前被黑客攻击的网站中的密码字典。例如,如果通过googl搜索hash值5f4dcc3b5aa765d61d8327deb882cf99
,很快会找到原始值”password”。同理,攻击者可以构建hash字典来查找原始密码。解决这个问题的一个办法是使用适当健壮的密码策略来防止使用常用字。另外一种方法是在计算hash值时使用“盐”。“盐”是每个用户已知已知数据的附加字符串,在计算hash之前与密码组合。理想情况下,数据应尽可能随机,但实际上任何盐值通常都比没有要好。使用盐值意味着攻击者必须为每个盐值构建一个hash字典,使攻击更加困难(但并非不可能)。
Bcrypt在编码时自动为每个密码生成一个随机盐值,并以标准格式将其存储在bcrypt字符串中。
处理盐值的传统方法是将
SaltSource
注入到DaoAuthenticationProvider
,它将获取特定用户的盐值并将其传递给PasswordEncoder
。使用bcrypt意味着使用者不必担心盐值处理的细节(例如存储值的位置),因为它都是在内部完成的。所以我们建议使用bcrypt,除非系统可以单独存储盐值。
哈希和认证
当某个认证provider(例如Spring Security的DaoAuthenticationProvider
)需要根据某用户已知信息校验提交的身份验证请求中的密码时,存储的密码是以某种方式编码的,提交上来的密码也需要通过同样的算法编码。Spring Security不能控制持久化的值,所以是否兼容取决于使用者。如果在Spring Security的认证配置中加入了密码哈希,但是在数据库中存储的是明文密码,认证永远都不会成功。考虑另外一种情况,数据库使用MD5对密码进行编码,并且应用配置为使用Spring Security的Md5PasswordEncoder
,也可能出现问题。数据库中可能使用的是Base64编码,而encoder使用十六进制字符串(默认值)。或者数据库中使用大写而encoder输出的是小写的。最好是在进行应用认证之前,编写测试代码测试一下对于给定的密码和盐值,配置的密码encoder的输出和数据库中存储的值匹配。使用像bcrypt这样的标准可以避免这些问题。
如果想要直接在java中生成编码后的密码存储到数据库中,可以使用PasswordEncoder
中的encode
方法。
支持Jackson
Spring Security为持久化Spring Security相关的类提供了对Jackson的支持。这可以提高在使用分布式session(即session复制,SpringSession等)时序列化Spring Security相关类的性能。
如果要使用这个特性,将JacksonJacksonModules.getModules(ClassLoader)
注册为Jackson Modules
:
ObjectMapper objectMapper = new ObjectMapper();
ClassLoader loader=getClass().getClassLoader();
List<Module> modules=SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);
// ... use ObjectMapper as normally ...
SecurityContext context = new SecurityContextImpl();
// ...
String json = mapper.writeValueAsString(context);