之前写的demo,相信对于初学者,有很多疑惑,而且,现在基本上都是前后端分离的项目了,像之前那个样子要配置登录页面和跳转页面是行不通的,一般我们只需要编写一个登录接口,获取前端传过来的用户名密码,然后传给security,再获取到security的验证结果,最后封装结果返回给前端。
但是,能做到上面的前提是要对security的认证流程有一定了解,所以我们今天啥都不写,就讲讲认证流程。
前端进行登录操作的时候,会执行UsernamePasswordAuthenticationFilter
过滤器,这个过滤器的代码比较少:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
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);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
这里定义了一个路径:DEFAULT_ANT_PATH_REQUEST_MATCHER
,传给了父类,根据它的定义,大胆猜测一下,可能是拦截了一个路径为/login
,类型为post的接口,我们稍后再看它的父类拿到这个做了什么。
在attemptAuthentication
这个方法里,主要做了4件事儿:
request
中获取到用户名和密码,字段名为username
和password
(所以前端传过来的字段必须是这两个)UsernamePasswordAuthenticationToken
AuthenticationManager
去认证,并返回认证信息AuthenticationManager
也放到后面再说,现在我们已经知道这个方法就是获取前端传来的用户名密码,并且验证。那这个方法是什么时候调用的呢?这个得去它的父类AbstractAuthenticationProcessingFilter
看,先看看类继承关系:
可以看到AbstractAuthenticationProcessingFilter
继承了Filter
,直接去它的doFilter
方法看:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//1.验证url是否是我们需要的
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//2.执行实际身份验证 返回已验证用户的已填充身份验证令牌,说明验证成功,验证失败则泡出异常
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// 身份验证过程仍在进行中
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//3.验证成功之后 发布事件通知监听者做处理,successHandler做回调,跳转到成功页面
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
我们对上面的步骤一步一步的分析,先是验证url:
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
这个requiresAuthenticationRequestMatcher
就是从构造方法中传过来的,也就是说这个地方就判断了是否为登录操作,如果是,就进行下面的身份验证。
身份验证是一个抽象方法,具体实现交给它的子类:
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException;
这个方法就是我们刚才在UsernamePasswordAuthenticationFilter
中看到的方法了,验证完成返回一个Authentication
对象。
上面提到了Authentication
类,它是描述当前用户的相关信息的,包含了用户拥有的权限信息列表、用户细节信息。常见的实现类有UsernamePasswordAuthenticationToken
,有兴趣的可以深入看一下源码。
最后就是验证成功之后的处理:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
//我们之前在securityconfig中配置的回调调用
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
这个地方的SecurityContextHolder
获取了SecurityContext
,并且调用方法存储了用户信息。既然存储用户信息,肯定就要和用户线程关联,所以SecurityContextHolder
提供了以下工作模式:
我们一般也使用第一种工作模式,在默认ThreadLocal
策略中,我们可以很方便的通过SecurityContextHolder
获取到当前的登录用户信息:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
后面的事件发布和事件监听,有兴趣的兄弟可以自己去看源码了,这里也就不再研究了。
回到刚刚没有说的AuthenticationManager
,它是认证相关的核心接口,是认证一切的起点。常见的认证流程都是AuthenticationManager
的实现类ProviderManager
处理。
AuthenticationManager
接口,定义如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
ProviderManager
类实现:
@Override
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;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
//对每个AuthenticationProvider进行认证,直到认证成功
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
//如果上面没有认证成功,那么进行父类AuthenticationProvider认证
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// 认证完成,清除密码
((CredentialsContainer) result).eraseCredentials();
}
//如果父AuthenticationManager已认证成功,则发布事件。如果事件已发布,则检查防止重复发送。
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// 所有流程走完,未通过身份验证就抛出异常。
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
ProviderManager
中有一个AuthenticationProvider
列表,会依照次序去认证,并且只需要通过一个AuthenticationProvider
的认证,即可被认为是登录成功。为了安全会进行清除密码。如果所有provider都验证不通过,那么就直接抛出异常。
这个provider定义如下:
public interface AuthenticationProvider {
/**
* 执行与以下合同相同的身份验证
* {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
* .
* @param authentication 接受验证的对象
* @return 返回经过验证的对象. 可能为空
* @throws AuthenticationException 验证失败抛出异常
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
/**
* 如果此AuthenticationProvide支持验证该Authentication对象,则返回true。
*返回true不代表一定支持身份验证,它只是表明它可以支持对其进行更仔细的评估。
*/
boolean supports(Class<?> authentication);
}
它主要有下面几个实现:
DaoAuthenticationProvider
:默认实现,使用账号密码认证方式,到数据库库获取认证数据信息。AnonymousAuthenticationProvider
:游客身份登录认证方式RememberMeAuthenticationProvider
:从cookies获取认证方式我们一般也是从数据库中获取用户数据验证,所以就挑它来看。DaoAuthenticationProvider
类继承结构如下:
先看一下AbstractUserDetailsAuthenticationProvider
中authenticate
的实现:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
//从authentication中获取到用户名
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.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
//检查用户账号状态,是否被禁用等
this.preAuthenticationChecks.check(user);
//交给具体的子类去验证身份是否正确
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
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);
}
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
总结一下:
第二步和第四步需要看一下DaoAuthenticationProvider
的实现:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
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) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
}
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
事情似乎越来越清晰了,这里调用了UserDetailsService
的loadUserByUsername
方法,并传入了username
,我们之前自己实现了UserDetailsService
接口,从数据库中将用户查询出来了。这个地方可以知道,查询出来之后,执行additionalAuthenticationChecks
方法通过passwordEncoder
验证了密码是否正确。至此,整个验证流程已经走完了。
上面的整个流程都是我们基于UsernamePasswordAuthenticationFilter
来做分析的,实际上,在Spring security中还有很多的filter,通过这些filter,spring security不仅实现了认证的逻辑,还实现了常见的web攻击防护。
下面列举一些常用的filter:
filter | 说明 |
---|---|
SecurityContextPersistenceFilter | 用于将SecurityContext放入到session中 |
UsernamePasswordAuthenticationFilter | 登录认证的filter |
RememberMeAuthenticationFilter | 通过cookie来实现记住我 的功能的Filter |
AnonymousAuthenticationFilter | 匿名认证处理过滤器,当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中 |
SessionManagementFilter | 会话管理Filter,持久化用户登录信息,可以保存到session中,也可以保存到cookie或者redis中 |
ExceptionTranslationFilter | 异常处理过滤器,主要拦截后续过滤器(FilterSecurityInterceptor)操作中抛出的异常。 |
FilterSecurityInterceptor | 安全拦截过滤器类,访问的url权限不足时会抛出异常 |
上面那么多filter,它们在FilterChain中的先后顺序是非常重要的。对于每一个系统或者用户自定义的filter,spring security都要求必须指定一个order,用来做排序。对于系统的filter的顺序,是在一个FilterComparator类中定义的:
FilterComparator() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(ChannelProcessingFilter.class, order.next());
order.next(); // gh-8105
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
order.next(); // gh-8105
this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter",
order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(SwitchUserFilter.class, order.next());
}
我们如果要自定义security的filter,也必须指定加入到那个filter的前面或者后面。
拓展:用户登录认证通过之后,下次进入的时候是怎么判断用户无需再次认证的呢?有兴趣的朋友可以自己研究一下。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.lingxiaomz.top/articleContent/?id=105785665230333890
内容来源于网络,如有侵权,请联系作者删除!