SpringSecurity原理解析(二):认证流程
attemptAuthentication 方法在子类UsernamePasswordAuthenticationFilter 中实现的。attemptAuthentication方法的作用是获取Authentication对象其实就是对应的认证过程,用该AuthenticationProvider的authenticate函数认证,如果认证成功则整个认证过程结束。AuthenticationMa
1、SpringSecurity认证流程包含哪几个子流程?
1)账号验证
2)密码验证
3)记住我—>Cookie记录
4)登录成功—>页面跳转
2、UsernamePasswordAuthenticationFilter
在SpringSecurity中处理认证逻辑是在UsernamePasswordAuthenticationFilter这个过滤
器中实现的,UsernamePasswordAuthenticationFilter 继承于
AbstractAuthenticationProcessingFilter 这个父类。
当请求进来时,在doFilter 方法中会对请求进行拦截,判断请求是否需要认证,若不需要
认证,则放行;否则执行认证逻辑;
1
注意:UsernamePasswordAuthenticationFilter 类中是没有 doFilter 方法的,doFilter
方法是继承自父类 UsernamePasswordAuthenticationFilter 的。
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 {
//处理认证,然后返回 Authentication 认证对象
//重点
authResult = this.attemptAuthentication(request, response);
//认证失败
if (authResult == null) {
return;
}
//认证成功之后注册session
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
//
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//认证成功后的处理
this.successfulAuthentication(request, response, chain, authResult);
}
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
//将认证成功后的对象保存到 SecurityContext中
SecurityContextHolder.getContext().setAuthentication(authResult);
//处理 remember-me属性
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
//认证成功后,页面跳转
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
上边的核心代码是下边这一行:
attemptAuthentication方法的作用是获取Authentication对象其实就是对应的认证过程,
attemptAuthentication 方法在子类UsernamePasswordAuthenticationFilter 中实现的。
attemptAuthentication 方法代码如下:
//认证逻辑
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//如果我们设置了该认证请求只能以post方式提交,且当前请求不是post请求,表示当前请求不符合
//认证要求,直接抛出异常,认证失败
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
//执行到这里表示开始执行认证逻辑
//从请求中获取用户名和密码
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//将用户名和密码包装成 UsernamePasswordAuthenticationToken 对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//设置用户提交的信息到 UsernamePasswordAuthenticationToken 中
this.setDetails(request, authRequest);
//getAuthenticationManager():获取认证管理器
//authenticate:真正处理认证的方法
return this.getAuthenticationManager().authenticate(authRequest);
}
}
1、
3、AuthenticationManager
AuthenticationManager接口中就定义了一个方法authenticate方法,用于处理认证的请求;
AuthenticationManager 接口定义如下:
public interface AuthenticationManager {
//处理认证请求
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
在这里AuthenticationManager的默认实现是ProviderManager.而在ProviderManager的
authenticate方法中实现的操作是循环遍历成员变量List<AuthenticationProvider> providers
。该providers中如果有一个AuthenticationProvider的supports函数返回true,那么就会调
用该AuthenticationProvider的authenticate函数认证,如果认证成功则整个认证过程结束。
如果不成功,则继续使用下一个合适的AuthenticationProvider进行认证,只要有一个认证
成功则为认证成功。
authenticate 方法定义如下:
//执行认证逻辑
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//获取 Authentication 对象的类型
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
//getProviders():获取系统支持的各种认证方式,如:QQ、微信、微博等等
for (AuthenticationProvider provider : getProviders()) {
//判断当前的 provider认证处理器 是否支持当前请求的认证类型,若不支持,则跳过
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
//执行到这里说明当前认证处理器支持当前请求的认证,
try {
//执行认证操作
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
//。。。。。省略 。。。。。
}
catch (InternalAuthenticationServiceException e) {
//。。。。。省略 。。。。。
}
catch (AuthenticationException e) {
//。。。。。省略 。。。。。
}
}
//如果循环结束后还没找到支持当前请求的认证处理器provider ,且父类不为空,则
//尝试调用父类的认证方法进行认证处理
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
//。。。。。省略 。。。。。
}
catch (AuthenticationException e) {
//。。。。。省略 。。。。。
}
}
//清空密码凭证
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
//
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
//
//异常处理
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
//
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
在上边的代码中,我们重点看的是下边这一行:
result = provider.authenticate(authentication);
因为是用户认证,所以这里authenticate方法走是AbstractUserDetailsAuthenticationProvider
类中的实现,
AbstractUserDetailsAuthenticationProvider.authenticate 方法定义如下所示:
//认证操作
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 获取提交的账号
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 = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
//。。。。。省略 。。。。。
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
//如果账号存在,即账号认证成功,则这里就开始密码认证
//密码校验前的前置检查,检查账号是否过期、是否锁定等
preAuthenticationChecks.check(user);
//密码校验
//user: 数据库中的数据
//authentication: 表单提交的数据
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
//。。。。。省略 。。。。。
}
//检查凭证是否过期
postAuthenticationChecks.check(user);
//将用户保存到缓存中
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
//创建具体的 Authentication 对象
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// user.getAuthorities():返回用户的权限
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
密码前置校验如下图所示:
然后进入到retrieveUser方法中,retrieveUser和additionalAuthenticationChecks 方法
具体的实现是DaoAuthenticationProvider 类中实现的,如下所示
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// getUserDetailsService会获取到我们自定义的UserServiceImpl对象,也就是会走我们自定义的认证方法了
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
//。。。。。省略 。。。。。
}
catch (InternalAuthenticationServiceException ex) {
//。。。。。省略 。。。。。
}
catch (Exception ex) {
//。。。。。省略 。。。。。
}
}
//具体的密码校验逻辑
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
//凭证为空(即密码没传进来),则直接抛出异常
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
//获取表单提交的密码
String presentedPassword = authentication.getCredentials().toString();
//拿表单提交的密码,与数据库中的密码进行匹配,若匹配失败,则抛出异常
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)