Spring Security的认证流程
Spring Security的登录认证流程始于AbstractAuthenticationProcessingFilter
,默认使用类为UsernamePasswordAuthenticationFilter
,当然也可以自己去自定义。
public abstract class AbstractAuthenticationProcessingFilter {
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 1. 第一步:判断登录路径
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
// 2. 第二步:尝试认证
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (...Exception failed) {{
// 3. 第三步:认证失败后处理
unsuccessfulAuthentication(request, response, failed);
return;
}
// 4. 第四步:认证成功后处理
successfulAuthentication(request, response, chain, authResult);
}
...
}
doFilter()方法中共有4个主要步骤,一个一个来看:
1、判断登录路径
在AbstractAuthenticationProcessingFilter
类存在一个RequestMatcher
引用来保存实现类,在UsernamePasswordAuthenticationFilter
类中存在构造器给RequestMather
进行赋值,只有请求的请求路径为"/login",请求方式为"POST"时才会进入下一步认证。
public abstract class AbstractAuthenticationProcessingFilter{
private RequestMatcher requiresAuthenticationRequestMatcher;
...
}
public class UsernamePasswordAuthenticationFilter{
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
}
那如果我们想要自己去设置登录路径,那怎么办?
- 自定义类继承
AbstractAuthenticationProcessingFilter
抽象类,手动给它赋值
public class RestLoginAuthenticationFilter
extends AbstractAuthenticationProcessingFilter {
public RestLoginAuthenticationFilter() {
super(new AntPathRequestMatcher("/api/user/login", "POST"));
}
...
}
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
@override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAt(new RestLoginAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
...
}
}
- 在SecurityConfigurer中配置loginProcessingUrl选项
在这种情况下,会给AbstractAuthenticationProcessingFilter
的RequestMatcher
字段赋值一个AntPathRequestMatcher
实例,路径为传入的"loginProcessingUrl",方法为"POST"。
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().loginProcessingUrl("/api/login")
.and().authorizeRequests().anyRequest().permitAll();
}
public T loginProcessingUrl(String loginProcessingUrl) {
this.loginProcessingUrl = loginProcessingUrl;
// authFilter 默认为 UsernamePasswordAuthenticationFilter
this.authFilter.setRequiresAuthenticationRequestMatcher(
createLoginProcessingUrlMatcher(loginProcessingUrl));
return getSelf();
}
// FormLoginConfigurer,返回一个AntPathRequestMatcher
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, "POST");
}
以上两种方法是同样的效果。
2、尝试认证
认证的开始于attemptAuthentication()方法。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain){
...
authResult = attemptAuthentication(request, response);
...
}
默认情况下,我们会走向UsernamePasswordAuthenticationFilter
的attemptAuthentication()方法。
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1. 从request中取出key为username和password的value,具体可以在配置文件中配置
String username = obtainUsername(request);
String password = obtainPassword(request);
...
// 2. 根据username和password创建一个Token,
UsernamePasswordAuthenticationToken authRequest = new
UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
// 3. 向下调用authenticate()方法
return this.getAuthenticationManager().authenticate(authRequest);
}
Token,我们根据其继承结构,我们可以看成它是有如下5个属性的UsernamePasswordAuthenticationToken
类,如下:
public class UsernamePasswordAuthenticationToken{
// 1. 主要信息
private final Object principal;
// 2. 凭证
private Object credentials;
// 3. 权限
private final Collection<GrantedAuthority> authorities;
// 4. 用户细节,这里是包含remoteAddress和sessionId的WebAuthenticationDetails
private Object details;
// 5. 是否认证
private boolean authenticated = false;
}
接着往下走,AuthenticationManager
默认的实现类为ProviderManager
,在ProviderManager
类中存在一个集合引用,保存数据为AuthenticationProvider
实例。调用AuthenticationManager
的authenticate() 方法,实质上就是遍历这个集合,逐个调用集合中实例的 authenticate() 方法。
public class ProviderManager {
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication) {
// 1. 第一步,遍历集合,看是否支持当前Token
Class<? extends Authentication> toTest = authentication.getClass();
...
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (XXXException e) {
// 捕获异常操作
}
}
// 2. 第二步,当前情况下,继续走parent
if (result == null && parent != null) {
try {
result = parentResult = parent.authenticate(authentication);
}
catch (XXXException e) {
// 捕获异常操作
}
}
// 3. 第三步,进行简单验证返回Result
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
...
}
}
ProviderManager
的 authenticate() 方法大致分为三步:
-
获取当前Token的class,看看provider们能不能支持,支持就调用他们的authenticate()方法,否则跳过。
-
遍历完集合provider集合之后,如果没有可以认证当前Token的情况(Result = null),看看parent是否有值,有值则调用。
-
如果Result不为null,在简单处理后就返回Result。
-
在默认情况下,集合内的provider会有两个,
AnonymousAuthenticationProvider
和DaoAuthenticationProvider
,这里进行一次debug会更清晰。
3、认证失败
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {
...
unsuccessfulAuthentication(request, response, failed);
...
}
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed)throws IOException, ServletException {
// 1. 清空上下文中的数据
SecurityContextHolder.clearContext();
// 2. rememberMe
rememberMeServices.loginFail(request, response);
// 3. 调用失败处理器
failureHandler.onAuthenticationFailure(request, response, failed);
}
failureHandler
默认的实现类SimpleUrlAuthenticationFailureHandler
,来看下onAuthenticationFailure() 方法,我们可以很容易看明白。
- 先看下defaultFailureUrl有没有配置,没有直接返回错误,可以通过http.formLogin.failureUrl(…) 来进行设置。
- 再看forwardToDestination的值,决定是采用重定向还是转发的形式到defaultFailureUrl。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
...
successfulAuthentication(request, response, chain, authResult);
...
}
public class SimpleUrlAuthenticationFailureHandler implements
AuthenticationFailureHandler {
private String defaultFailureUrl;
private boolean forwardToDestination = false;
...
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
// defaultFailureUrl 配置类可以设置
if (defaultFailureUrl == null) {
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
else {
saveException(request, exception);
// forwardToDestination默认为false
if (forwardToDestination) {
request.getRequestDispatcher(defaultFailureUrl)
.forward(request, response);
}
else {
redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
}
}
}
...
}
4、认证成功
private AuthenticationSuccessHandler successHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
// 1. 在上下文中保存Token
SecurityContextHolder.getContext().setAuthentication(authResult);
// 2. RememberMe操作
rememberMeServices.loginSuccess(request, response, authResult);
// 3. 发布事件
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
// 4. 调用成功处理器
successHandler.onAuthenticationSuccess(request, response, authResult);
}
successHandler默认实现为SavedRequestAwareAuthenticationSuccessHandler
。
public class SavedRequestAwareAuthenticationSuccessHandler {
...
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
// 1. 取出存储的request
SavedRequest savedRequest = requestCache.getRequest(request, response);
...
// 2. 取出路径并重定向
String targetUrl = savedRequest.getRedirectUrl();
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
...
}