1、Authentication
存放用户信息类的顶层接口为Authentication,我们从这个类往下找,发现常用的实现类有UsernamePasswordAuthenticationToken。
我们对与之关联的5个类进行观察:
-
Authentication
-
AbstractAuthenticationToken
-
UsernamePasswordAuthenticationToken
-
GrantedAuthority
-
SimpleGrantedAuthority
我们可以发现,在UsernamePasswordAuthenticationToken类中存在5个属性:authorities(权限和角色的集合),details(细节),authenticated(是否已认证),principal(主要信息),credentials(凭证),其余的都是些getter/setter方法。
看下权限/角色集合属性,Collection< GrantedAuthority >中的GrantedAuthority 类,你去查看它的简单实现类源码(SimpleGrantedAuthority)会发现,它仅仅只有一个属性:String role,其余都是getter/setter方法。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private final Collection<GrantedAuthority> authorities;
private Object details;
private boolean authenticated = false;
private final Object principal;
private Object credentials;
...
}
public final class SimpleGrantedAuthority implements GrantedAuthority {
private final String role;
...
}
2、登录流程
Spring Security本质是一系列的过滤器,与登录认证相关的过滤器是UsernamePasswordAuthenticationFilter。
过滤器,我们的关注点还是要从doFilter()方法开始,AbstractAuthenticationProcessingFilter就是doFilter()方法实现的类。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
private void doFilter(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 1. 判断当前的过滤器能处理当前路径的请求
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 2.认证用户
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
// 3. 调用认证成功的回调
successfulAuthentication(request, response, chain, authenticationResult);
}
// 4. 调用认证失败后的回调
catch (InternalAuthenticationServiceException failed) {
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
unsuccessfulAuthentication(request, response, ex);
}
}
}
根据上面的代码,doFilter()方法中的代码逻辑很简单:
1、判断当前请求路径能不能处理
2、调用attemptAuthentication()方法认证用户
3、认证成功,则调用successfulAuthentication();认证失败,则调用unsuccessfulAuthentication()方法
下面,我们来看下attemptAuthentication()方法:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 1. 判断类和请求的方式是否一致为POST
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
// 2. 获取用户名
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
// 2. 获取密码
String password = this.obtainPassword(request);
password = password != null ? password : "";
// 3. 设置属性Token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// 3. 设置details
this.setDetails(request, authRequest);
// 4. 执行校验
return this.getAuthenticationManager().authenticate(authRequest);
}
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
通过上面的源码,我们可以得到:
1、通过obtainPassword()和obtainUsername()方法从请求获取值,其本质上就是request.getParameter()方法,但这同时也要求请求中的数据只能已key-value的方式来传递,不行看这-传送门。
2、在UsernamePasswordAuthenticationToken中,username对应principal属性,password对应credentials。
3、doFilter()先判断当前请求是否为POST,再调用obtainPassword()和obtainUsername()来获取用户名和密码来创建UsernamePasswordAuthenticationToken对象。
4、调用setDetails()方法给details属性赋值,其实际保存的对象为WebAuthenticationDetails实例,主要属性有remoteAddress和sessionId。
5、调用authenticate()方法执行认证流程
public class WebAuthenticationDetails implements Serializable {
private final String remoteAddress;
private final String sessionId;
public WebAuthenticationDetails(HttpServletRequest request) {
// 获取ip地址,如果存在代理情况,则获取不到真实的ip地址
this.remoteAddress = request.getRemoteAddr();
HttpSession session = request.getSession(false);
this.sessionId = (session != null) ? session.getId() : null;
}
}
最后一步调用的authenticate()方法,执行的对象为是AuthenticationManager的实现,根据接口我们发现它的实现,它仅实现的类为ProviderManager,其余都是静态内部类,那就是它了,我们看下它的authenticate()方法。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 1. 获取UsernamePasswordAuthenticationToken的class对象
Class<? extends Authentication> toTest = authentication.getClass();
...
// providers是AuthenticationProvider的集合
int size = this.providers.size();
// 2. 遍历provider集合
for (AuthenticationProvider provider : getProviders()) {
// 3. 判断当前provider是否支持该UsernamePasswordAuthenticationToken,同类或是其子类
if (!provider.supports(toTest)) {
continue;
}
...
// 4. provider调用认证方法,方法返回一个Authentication实例
result = provider.authenticate(authentication);
// 4. Authentication不为空,则复制details过去
if (result != null) {
copyDetails(authentication, result);
break;
}
...
}
// 5. parent是ProviderManager,会再次进行当前方法
if (result == null && parent != null) {
result = parentResult = parent.authenticate(authentication);
}
// 6.调用 eraseCredentials方法擦除凭证信息,也就是credentails属性
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
// 7. 登录成功调用publishAuthenticationSuccess()方法广播出去
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
...
}
通过以上源码,得到以下结论:
1、ProviderManager中存在providers属性来保存provider对象
2、authenticate()方法的实质就是将集合的provider一个一个调用authenticate()方法,直到遍历结束或某个provider返回的result不为null且设置details成功。
3、遍历集合的要点:一:判断当前provider是否能处理当前Authentication类型;二:provider是否能返回Authentication实例。
4、当认证成功,查出Authentication中credentails属性,并调用广播方法。
那Provider的authenticate的认证方法又是怎样的呢?下面来看
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 1. 获取principal属性值
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
// 2. 根据用户名从缓存中获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
// 3. 最后调用 loadUserByUsername(),也就是需要我们实现的那个UserDetailsService接口
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
// 4. 检查用户状态,如账户是否被禁用、账户是否被锁定、账户是否过期等等
this.preAuthenticationChecks.check(user);
// 4. 做密码比对
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
// 4. 检查密码是否过期
this.postAuthenticationChecks.check(user);
// 5. 往缓存中添加用户数据
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
// 4. 是否强制将 Authentication 中的 principal 属性设置为字符串
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 6. 创建一个token
return createSuccessAuthentication(principalToReturn, authentication, user);
}
通过以上的源码,我们可以发现authenticate()方法,
1、获取用户名
2、从缓存中获取用户信息,没有则从调用loadUserByUsername()方法获取
3、检查各种机制,检查完毕没有问题后添加信息到缓存中
4、创建一个Token返回
3、信息的保存与获取
在AbstractAuthenticationProcessingFilter抽象类的doFIlter()方法中定义了验证的主要流程,我们去查看认证成功后调用的方法successfulAuthentication(),它后面有一步SecurityContextHolder.getContext().setAuthentication(authResult);
就是保存用户信息到上下文的,你也可以试着通过SecurityContextHolder.getContext().getAuthentication();
来获取用户信息。
successfulAuthentication(request, response, chain, authenticationResult);
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 保存用户信息
SecurityContextHolder.getContext().setAuthentication(authResult);
...
}
// 获取信息
SecurityContextHolder.getContext().getAuthentication();
4、登录认证流程总结
Spring Security是一大串的过滤器,那一切的开始都是doFilter()方法,在Spring Security中用于用户认证的过滤器是UsernamePasswordAuthenticationFilter
,但它并没有实现doFilter()方法,方法实现在于它的父类:AbstractAuthenticationProcessingFilter
。
doFiter()方法:该方法中,无非定义3件事:①:用户认证的流程;②:认证成功怎么办;③:认证失败怎么办。
这三件事,我们都可以进行配置如何进行。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
// 第一件事
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
// 第二件事
successfulAuthentication(request, response, chain, authenticationResult);
}
// 第三件事
catch (InternalAuthenticationServiceException failed) {
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
unsuccessfulAuthentication(request, response, ex);
}
}
我们顺着第一件事继续看下去,我们会找到UsernamePasswordAuthenticationFilter
的attemptAuthentication()方法。
attemptAuthentication()方法:它无非有做了两件事:①:获取Authentication所需要的信息并填上去;②:调用下一级认证方法。
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 第一件事
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
// 第二件事
return this.getAuthenticationManager().authenticate(authRequest);
}
我们再顺着第二件往下看,我们会找到AuthenticationManager
接口和它的唯一实现子类ProviderManager
。
authenticate():该类中存在属性providers来保存AuthenticationProvider
接口的实例。该方法中只做了一件事:遍历providers集合,调用他们的authenticate()方法,不过有两种可能:①:不支持当前authentication,parent来,再次进入该方法:②:认证成功,清除credentials属性并调用publishAuthenticationSuccess()方法进行广播。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
int size = this.providers.size();
// 第一件事
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
// 第一种情况
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
// 第二中情况
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
}
我们再顺着provider来看下,它的接口是AuthenticationProvider
,可以来看下DaoAuthenticationProvider是如何实现的。
authenticate()方法:事儿不多,三件事:①:获取用户名,先去缓存里查,没有再到定义的方法loadUserByUsername()里查;②:对查询出来的用户信息各种认证,如果缓存中没有,则添加进去;③:返回一个Authentication实例
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取用户名
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
// 第一件事
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
...
}
}
// 第二件事
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 第三件事
return createSuccessAuthentication(principalToReturn, authentication, user);
}
Spring Security总体的登录流程就是这样!!!