Spring Security主要实现是认证器和决策器,前者处理登陆验证,后者处理访问资源的控制
Spring Security的登陆请求处理如图
下面来分析一下是怎么实现登录的
拦截请求
登陆请求,默认会被UsernamePasswordAuthenticationFilter
拦截,这个过滤器看名字就知道是一个拦截用户名密码的拦截器, Spring Security的默认登录实现
拦截的作用------创建Authentication上下文类
主要的验证是在attemptAuthentication()
方法里,他会去获取在请求中的用户名密码,并且创建一个该用户的上下文UsernamePasswordAuthenticationToken
,然后在去执行一个验证过程
java
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
//创建上下文
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
可以看看UsernamePasswordAuthenticationToken
这个类,他是继承了AbstractAuthenticationToken
,然后这个父类实现了Authentication
由这个类的方法和属性可得知他就是存储用户验证信息的,认证器的主要功能应该就是验证完成后填充这个类
UsernamePasswordAuthenticationToken的构造方法
回到UsernamePasswordAuthenticationToken
中,这个类有两个构造方法,一个是是刚开始认证调用,一个是认证完成调用
java
public UsernamePasswordAuthenticationToken(Object principal,Object credentials){
super(null);
this.principal=principal;
this.credentials=credentials;
//还没认证
setAuthenticated(false);
}
刚开始调用,会将权限相关的属性设置为null,比如上边的super(null)
java
//super(null),初始化一个空权限集合
if (authorities == null) {
this.authorities = AuthorityUtils.NO_AUTHORITIES;
return;
}
验证完成后,会调用另外一个构造方法
java
//认证完成
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
//将认证的信息填充到上下文
super(authorities);
this.principal = principal;
this.credentials = credentials;
//完成认证
super.setAuthenticated(true);
}
主要的区别是super.setAuthenticated()
验证过程
怎么去填充Authentication
那么UsernamePasswordAuthenticationToken
这个上下文类,由谁去填充的,从拦截器那边的调用
kotlin
return this.getAuthenticationManager().authenticate(authRequest);
可以看到是下一步到AuthenticationManager
中,他是一个接口
java
/**
* Attempts to authenticate the passed {@link Authentication} object, returning a
* fully populated <code>Authentication</code> object (including granted authorities)
* if successful.
**/
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
注释也写的很清楚了,认证完成后会填充Authentication
通过debug查看调用栈,找到他的实现类ProviderManager
由AuthenticationManager委托给AuthenticationProvider
首先看看他的主要属性和构造方法,主要是设置AuthenticationProvider
类
csharp
//初始化AuthenticationProvider集合
private List<AuthenticationProvider> providers = Collections.emptyList();
//设置AuthenticationProvider集合
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
看一下这个类,可以得出一个结论,Authentication
和AuthenticationProvider
是成对存在的,不同的Authentication由特定的AuthenticationProvider处理,这点对如何实现自定义登录很重要
java
/**
* 处理特定Authentication的类
**/
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//支持特定类型的authentication
boolean supports(Class<?> authentication);
}
通过这个关系,可以找到UsernamePasswordAuthenticationToken对应的AuthenticationProvider
typescript
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
....
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
通过遍历AuthenticationProvider集合,找到对应的处理类
然后看回ProviderManager
,主要看实现的authenticate()
方法,主要代码是遍历了List<AuthenticationProvider>
集合,然后根据Authentication找到对应的AuthenticationProvider
scss
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//取出上下文对象,就是上边提到的UsernamePasswordAuthenticationToken
Class<? extends Authentication> toTest = authentication.getClass();
...
for (AuthenticationProvider provider : getProviders()) {
//是否匹配
if (!provider.supports(toTest)) {
continue;
}
...
//调用匹配的provider
result = provider.authenticate(authentication);
//填充上下文
if (result != null) {
//将返回的result,复制给当前的authentication中
copyDetails(authentication, result);
break;
}
}
}
AbstractUserDetailsAuthenticationProvider委托给子类处理
如果找到了对应的provider,会调用provider的authenticate
方法,拿UsernamePasswordAuthenticationToken对应的AbstractUserDetailsAuthenticationProvider举例
java
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
....
//从上下文中获取username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
//调用retrieveUser去验证用户
user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
}
这里的retrieveUser
是抽象方法,所以AbstractUserDetailsAuthenticationProvider将验证用户的方法又抽取出来,给子类去实现,这种实现方式减少各个组件之间的耦合度,增加系统的灵活性和可扩展性
DaoAuthenticationProvider调用验证用户的service
通过继承关系可以知道子类是DaoAuthenticationProvider
,里边的实现,又继续调用验证用户的service
ini
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
接触过 Spring Security框架的,应该熟悉UserDetailsService
这个类,这个类是Spring Security提供给我们扩展的类,通常会去实现这个类来自定义查询用户的方式
也就是说这是个会变化的类,因为他可能会有多个子类,Spring Security将这种变化又抽取出来
loadUserByUsername()
方法是我们自己要实现的逻辑,验证完成后返回UserDetails
类,UserDetails类中有一些显示用户状态的属性,可以根据这个属性来做一些权限处理,如果字段不够用,可以继承这个类来实现
arduino
public interface UserDetails{
//权限集合,我这里没有用的,初始化空集合,AuthorityUtils.NO_AUTHORITIES
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
//账号是否过期
boolean isAccountNonExpired();
//账号被锁定或解锁状态。
boolean isAccountNonLocked();
//密码是否过期
boolean isCredentialsNonExpired();
//是否启用
boolean isEnabled();
}
验证完成的后续
回到AbstractUserDetailsAuthenticationProvider中,这此时,我们已经经过了几层的调用
scss
AbstractUserDetailsAuthenticationProvider
|->retrieveUser()
|->DaoAuthenticationProvider
|->retrieveUser()
|->UserDetailsService
|->loadUserByUsername()
当最后retrieveUser()
方法返回user后,会对user对象做一些校验
scss
//验证用户状态,就是验证UserDetails中的账号属性,禁用就返回
preAuthenticationChecks.check(user);
//验证前端的密码是否匹配
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
在源码中,还有对缓存的处理,如果是由于账号状态或者密码不匹配的问题抛出异常,捕获后判断是否开启了用户缓存,是的话就会再去查询一遍,如果这次通过的话,会将新的信息放到缓存中
返回完整的Authentication
验证完成后,会创建新的Authentication,并填充数据, Spring Security会将新创建的Authentication,复制到当前的会创建新的Authentication中copyDetails(authentication, result);
java
protected Authentication createSuccessAuthentication(Object principal,
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
注意看这次创建UsernamePasswordAuthenticationToken使用的构造方法,使用的是另外一个,将权限集合传入,这个构造方法的关键是super.setAuthenticated(true);
,表示验证完成
Authentication会将权限集合转换为只读集合
java
for (GrantedAuthority a : authorities) {
if (a == null) {
throw new IllegalArgumentException(
"Authorities collection cannot contain any null elements");
}
}
ArrayList<GrantedAuthority> temp = new ArrayList<>(
authorities.size());
temp.addAll(authorities);
//只读
this.authorities = Collections.unmodifiableList(temp);
至此,我们已经完成了对Authentication的验证和填充,最后返回到UsernamePasswordAuthenticationFilter
中通过过滤
最后再看下整个调用链路
总结
通过对源码的分析,发现Spring Security很多地方都是模块化设计,通过一个高层接口一步一步的调用,即减少了耦合度,也增强了扩展性,将会发生变化的类抽取出来的这种开闭原则的设计非常值得学习
整个登录实现都是围绕着填充和验证Authentication
,如果想要自定义登录,通过实现自己的上下文Authentication
和处理类AuthenticationProvider
就可以实现了