对SpringSecurity源码的理解对使用SpringSecurity可以说是非常重要,这篇博客就根据其源码,浅析一下其认证流程,对于自己实现一些功能会很有帮助。
为了方便理解,先放一张流程图
可以看到程序运行到核心过滤器AbstractAuthenticationProcessingFilter 抽象类的时候调用.doFilter()方法,这一步可理解为认证的第一步,以下是源码。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
//判断是否需要认证
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
//重点代码,尝试认证
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
//请求失败的handler
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//请求成功的handler
this.successfulAuthentication(request, response, chain, authResult);
}
}
由于其没有实现attemptAuthentication,使用的是其子类UsernamePasswordAuthenticationFilter实现的方法。
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());
} else {
//从POST请求体中获取用户名和密码参数
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//用用户名和密码构造一个token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
//使用get方法获得AuthenticationManager来调用authenticate方法认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
使用getAuthenticationManager()默认情况下注入 Spring 容器的 AuthenticationManager 是 ProviderManager。以下是ProviderManager的authenticate方法。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
Iterator var8 = this.getProviders().iterator();
//ProviderManager将从AuthenticationProvider列表中循环取出其实现类,并尝试认证
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
//核心代码,认证
result = provider.authenticate(authentication);
if (result != null) {
//把认证结果复制到刚刚创建的token里
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (AuthenticationException var14) {
lastException = var14;
}
}
}
这里讲AuthenticationProvider列表中的一个实现,AbstractUserDetailsAuthenticationProvider。以下是其authentic方法。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//判断authentication的类型是否为UsernamePasswordAuthenticationToken
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
//从token中获取用户名
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
//从缓存中获取用户
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//若缓存中没有此用户,则调用方法获得user,这里使用的是其子类DaoAuthentication的实现
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
下面是DaoAuthentication的retrieveUser方法实现代码。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//可以看到这里使用UserDetailsService的loadUserByUsername获取User。
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
UserDetailService有多个实现类,其中一个便是InMemoryUserDetailsManager,即是熟悉的配置文件中配置在内存中的用户的获取。
讲到这里SpringSecurity认证流程基本也讲完了,我们可以总结最重要的两点即是从请求中提取用户信息的Filter类以及从本地获取用户信息的UserDetailService接口。若我们要实现自己的业务流程。便可首先从这两个点下手。
继承UsernamePasswordAuthenticationFilter以达到实现自己控制提取请求中的信息,比如前后端分离中提取json中的信息。
实现UserDetailService接口以通过自己想要的方式获取用户,比如通过自己的方法从数据库中获取用户。