SpringSecurity身份认证原理解析
作者:还没秃的小菜鸡
Spring Security身份认证
- 用户名和密码被过滤器获取到,封装成 Authentication ,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
- AuthenticationManager 身份管理器负责验证这个 Authentication
- 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的 权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。
- SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
public class AuthenticationExample { private static AuthenticationManager am = new SampleAuthenticationManager(); public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while (true) { System.out.println("please enter your username:"); String name = in.readLine(); System.out.println("please enter your password:"); String password = null; password = in.readLine(); try { // 封装认证信息,未认证通过 UsernamePasswordAuthenticationToken request = new UsernamePasswordAuthenticationToken(name, password); // 认证逻辑 Authentication result = am.authenticate(request); //当前线程绑定认证信息 SecurityContextHolder.getContext().setAuthentication(result); break; } catch (AuthenticationException e) { System.out.println("Authentication failed: " + e.getMessage()); } } } static class SampleAuthenticationManager implements AuthenticationManager { static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>(); static { AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER")); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 判断条件,用户名和密码是否相同 if (authentication.getName().equals(authentication.getCredentials())){ return new UsernamePasswordAuthenticationToken(authentication.getName(),authentication.getCredentials(),AUTHORITIES); } throw new BadCredentialsException("Bad Credentials"); } } }
测试:
认证流程
SecurityFilterChain 过滤器链
Spring Security采用的是filterChain的设计方式,主要的功能大都由过滤器实现,在启动项目的时候,可以在日志中看到已有的过滤器,可在类似下面的日志里找到 DefaultSecurityFilterChain ,这里面则是SecurityFilterChain
2021-01-07 11:27:30.410 INFO 13880 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@153cd6bb, org.springframework.security.web.context.SecurityContextPersistenceFilter@71f0b72e, org.springframework.security.web.header.HeaderWriterFilter@aa149ed, org.springframework.security.web.authentication.logout.LogoutFilter@2de50ee4, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@151ef57f, org.springframework.security.web.session.ConcurrentSessionFilter@5c73f672, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2f508f3c, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5eed2d86, org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter@36fc05ff, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@61d84e08, org.springframework.security.web.session.SessionManagementFilter@31ff6309, org.springframework.security.web.access.ExceptionTranslationFilter@10fbbdb, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4e1459ea]
把各个过滤器抽取出来,我们可以看到是这样,这也是过滤器链的先后顺序。
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- JwtAuthorizationTokenFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
介绍几个主要的作用
- SecurityContextPersistenceFilter
- Filter的入口和出口,它是用来将SecurityContext(认证的上下文,里面有登录成功后的认证授权信息)对象持久到Session的Filter,同时会把SecurityContext设置给SecurityContextHolder方便我们获取用户认证授权信息
- UsernamePasswordAuthenticationFilter
- 默认拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括username,password等封装成UsernamePasswordAuthenticationToken,然后调用AuthenticationManager的认证方法进行认证
- BasicAuthenticationFilter
- 基本认证,支持httpBasic认证方式的Filter
- RememberAuthenticationFilter
- 记住我功能实现的Filter
- AnonymousAuthenticationFilter
- 匿名Filter,用来处理匿名访问的资源,如果用户未登录,SecurityContext中没有Authentication,就会创建匿名的Token(AnonymousAuthenticationToken),然后通过SecurityContextHodler设置到SecurityContext中。
- ExceptionTranslationFilter
- 用来捕获FilterChain所有的异常,进行处理,但是只会处理 AuthenticationException和AccessDeniedException,异常,其他的异常 会继续抛出。
- FilterSecurityInterceptor
用来做授权的Filter,通过父类(AbstractSecurityInterceptor.beforeInvocation)调用AccessDecisionManager.decide方法对用户进行授权。
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter ,顾名思义,是用来处理用户名密码登录的过滤器。所有的Filter核心方法都是 doFilter ,该过滤器的doFilter在其父抽象类中,过滤器只需实现 attemptAuthentication 方法即可。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // ===================================================================================== //从登录请求中获取参数:username,password的名字 public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; //默认支持POST登录 private boolean postOnly = true; //默认拦截/login请求,Post方式 public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } // ~ Methods // ======================================================================================================== public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //判断请求是否是POST if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //获取到用户名和密码 String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); //用户名和密码封装Token UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); //设置details属性 // Allow subclasses to set the "details" property setDetails(request, authRequest); //调用AuthenticationManager().authenticate进行认证,参数就是Token对象 return this.getAuthenticationManager().authenticate(authRequest); }
AuthenticationManager
请求通过UsernamePasswordAuthenticationFilter调用AuthenticationManager,默认走的实现类是ProviderManager,它会找到能支持当前认证的AuthenticationProvider实现类调用器authenticate方法执行认证,认证成功后会清除密码,然后抛出AuthenticationSuccessEvent事件
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { ...省略... //这里authentication 是封装了登录请求的认证参数, //即:UsernamePasswordAuthenticationFilter传入的Token对象 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(); //找到所有的AuthenticationProvider ,选择合适的进行认证 for (AuthenticationProvider provider : getProviders()) { //是否支持当前认证 if (!provider.supports(toTest)) { continue; } ```java if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { //调用provider执行认证 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } ...省略... } ...省略... //result就是Authentication ,使用的实现类依然是UsernamepasswordAuthenticationToken, //封装了认证成功后的用户的认证信息和授权信息 if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication //这里在擦除登录密码 ((CredentialsContainer) result).eraseCredentials(); } // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it if (parentResult == null) { //发布事件 eventPublisher.publishAuthenticationSuccess(result); } return result; }
DaoAuthenticationProvider
请求到达AuthenticationProvider,默认实现是DaoAuthenticationProvider,它的作用是根据传入的Token中的username调用UserDetailService加载数据库中的认证授权信息(UserDetails),然后使用PasswordEncoder对比用户登录密码是否正确
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { //密码编码器 private PasswordEncoder passwordEncoder; //UserDetailsService ,根据用户名加载UserDetails对象,从数据库加载的认证授权信息 private UserDetailsService userDetailsService; //认证检查方法 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); ```java throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } //获取密码 String presentedPassword = authentication.getCredentials().toString(); //通过passwordEncoder比较密码,presentedPassword是用户传入的密码,userDetails.getPassword()是从数据库加载到的密码 //passwordEncoder编码器不一样比较密码的方式也不一样 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")); } } //检索用户,参数为用户名和Token对象 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { //调用UserDetailsService的loadUserByUsername方法, //根据用户名检索数据库中的用户,封装成UserDetails 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); } } //创建认证成功的认证对象Authentication,使用的实现是UsernamepasswordAuthenticationToken, //封装了认证成功后的认证信息和授权信息,以及账户的状态等 @Override protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword()); if (upgradeEncoding) { String presentedPassword = authentication.getCredentials().toString(); String newPassword = this.passwordEncoder.encode(presentedPassword); user = this.userDetailsPasswordService.updatePassword(user, newPassword); } return super.createSuccessAuthentication(principal, authentication, user); } ...省略...
这里提供了三个方法
- additionalAuthenticationChecks:通过passwordEncoder比对密码
- retrieveUser:根据用户名调用UserDetailsService加载用户认证授权信息
- createSuccessAuthentication:登录成功,创建认证对象Authentication
然而你发现 DaoAuthenticationProvider 中并没有authenticate认证方法,真正的认证逻辑是通过父类AbstractUserDetailsAuthenticationProvider.authenticate方法完成的
AbstractUserDetailsAuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { //认证逻辑 public Authentication authenticate(Authentication authentication) throws AuthenticationException { //得到传入的用户名 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); //从缓存中得到UserDetails boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; ```java try { //检索用户,底层会调用UserDetailsService加载数据库中的UserDetails对象,保护认证信息和授权信息 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { ...省略... } try { //前置检查,主要检查账户是否锁定,账户是否过期等 preAuthenticationChecks.check(user); //比对密码在这个方法里面比对的 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { ...省略... } //后置检查 postAuthenticationChecks.check(user); if (!cacheWasUsed) { //设置UserDetails缓存 this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } //认证成功,创建Auhentication认证对象 return createSuccessAuthentication(principalToReturn, authentication, user); }
SecurityContextHolder
认证成功,请求会重新回到UsernamePasswordAuthenticationFilter,然后会通过其父类AbstractAuthenticationProcessingFilter.successfulAuthentication方法将认证对象封装成SecurityContext设置到SecurityContextHolder中 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { ```java if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } //认证成功,吧Authentication 设置到SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authResult); //处理记住我业务逻辑 rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //重定向登录成功地址 successHandler.onAuthenticationSuccess(request, response, authResult); }
然后后续请求又会回到SecurityContextPersistenceFilter,它就可以从SecurityContextHolder获取到SecurityContext持久到SecurityContextRepository(默认实现是HttpSessionSecurityContextRepository基于Session存储)
到此这篇关于SpringSecurity身份认证原理解析的文章就介绍到这了,更多相关SpringSecurity身份认证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- SpringSecurity 默认登录认证的实现原理解析
- SpringSecurity实现权限认证与授权的使用示例
- Spring Security内存中认证的实现
- SpringBoot整合SpringSecurity认证与授权
- SpringSecurity+jwt+redis基于数据库登录认证的实现
- springsecurity第三方授权认证的项目实践
- Spring Security实现身份认证和授权的示例代码
- SpringSecurity实现前后端分离登录token认证详解
- Spring Security认证机制源码层探究
- SpringBoot security安全认证登录的实现方法
- Spring Security添加二次认证的项目实践