话不多说,程序员都是从一个helloworld开始
public class AuthHelloWorld {
private static AuthenticationManager manager = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("用户名:");
String name = in.readLine();
System.out.println("密码:");
String password = in.readLine();
try {
// 1.将用户名密码封装成Authentication
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
// 2.AuthenticationManager 身份管理器验证这个Authentication
Authentication result = manager.authenticate(request);
// 4.SecurityContextHolder填充Authentication
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch (AuthenticationException e) {
System.out.println("验证失败: " + e.getMessage());
}
}
System.out.println("验证成功: " +
SecurityContextHolder.getContext().getAuthentication().getName());
}
}
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 {
//3 认证成功后,返回Authentication实例。
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
1.Spring Security的一次验证过程是怎么样的?
上面的测试helloworld代码来表现主要分四个阶段:
1. 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
2. AuthenticationManager身份管理器负责验证上面封装的Authentication。
3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到上下文。
2.核心组件介绍
先抛个类图
2.1 SecurityContextHolder
SecurityContextHolder用于存储安全上下文的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都将被保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在web场景下使用Spring Security,而如果是Swing界面,Spring也提供了支持,SecurityContextHolder的策略则需要被替换。
获取当前用户的信息:因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息,一个典型的获取当前登录用户的姓名的例子如下:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof UserDetails){
String username=((UserDetails)authentication).getUsername();
} else {
String username=authentication.toString();
}
2.2 Authentication
Authentication 是一个接口,实现类都会定义 authorities,credentials,details,principal,authenticated 等字段,具体含义如下:
在验证前,principal 填充的是用户名,credentials 填充的是密码,detail 填充的是用户的 IP 或者经纬度之类的信息。通过验证后,Spring Security 对 Authentication 重新注入,principal 填充用户信息(包含用户名、年龄等), authorities 会填充用户的角色信息,authenticated 会被设置为 true。重新注入的 Authentication 会被填充到 SecurityContext 中。
看看源码定义
package org.springframework.security.core;
public interface Authentication extends Principal, Serializable {
//获取用户权限,一般情况下获取到的是用户的角色信息
Collection<? extends GrantedAuthority> getAuthorities();
//获取证明用户认证的信息,通常情况下获取到的是密码等信息。
Object getCredentials();
//获取用户的额外信息,比如 IP 地址、经纬度等。
Object getDetails();
//获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (暂时理解为,当前应用用户对象的扩展)。
Object getPrincipal();
//获取当前 Authentication 是否已认证。
boolean isAuthenticated();
//设置当前 Authentication 是否已认证。
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
2.3 AuthenticationManager
初次接触Spring Security的朋友相信会被AuthenticationManager,ProviderManager ,AuthenticationProvider …这么多相似的Spring认证类搞得晕头转向,但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录,所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List<AuthenticationProvider>列表,存放多种认证方式,实际上这是委托者模式的应用。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider,在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 维护一个AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 依次认证
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication信息,则直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
//移除密码
((CredentialsContainer) result).eraseCredentials();
}
//发布登录成功事件(扩展点)
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
...
//执行到此,说明没有认证成功,包装异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
//发布登录失败的事件(扩展点)
prepareException(lastException, authentication);
throw lastException;
}
}
ProviderManager 中的List,会依照次序去认证,认证成功则立即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。
到这里,如果不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里做一个分隔线。在上面的代码中我们发现了两个扩展点,在认证成功和失败后会发出对应的event出来,可自己实现对应的handler接收对应的event进行处理
下面来介绍下AuthenticationProvider接口的具体实现
2.4 DaoAuthenticationProvider
AuthenticationProvider最最最常用的一个实现便是DaoAuthenticationProvider。顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路
按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。
如果你已经被这些概念搞得晕头转向了,不妨这么理解DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
2.4 UserDetails与UserDetailsService
public interface UserDetails extends Serializable {
//1.权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//2.用户正确的密码区别于用户输入Authentication的的getCredentials()
String getPassword();
//3.用户名
String getUsername();
//4.用户是否过期
boolean isAccountNonExpired();
//5.是否锁定
boolean isAccountNonLocked();
//6.用户密码是否过期
boolean isCredentialsNonExpired();
//7.账号是否可用(可理解为是否删除)
boolean isEnabled();
}
它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。
//负责从特定的地方(通常是数据库)加载用户信息
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService和AuthenticationProvider两者的职责常常被人们搞混 UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。
OK,看到这里,希望大家对spring security的工作机制有了一定的认识。
我以前没有使用过spring security,目前正好公司项目应用到,自己也做了一次学习,把学习的过程记录下来备忘,也希望能帮助需要入门和了解的同学