Java-Security(四):用户认证流程源码分析
让我们带着以下3个问题来阅读本篇文章:
- 1)在Spring Security项目中用户认证过程中是如何执行的呢?
- 2)认证后认证结果如何实现多个请求之间共享?
- 3)如何获取认证信息?
在《Java-Security(二):如何初始化springSecurityFilterChain(FilterChainProxy)》中可以发现SpringSecurity的核心就是一系列过滤链,当一个请求进入时,首先会被过滤链拦截到,拦截到之后会首先经过校验,校验之后才可以访问到用户各种信息。
下图是Spring Security过滤链是Spring Security运行的核心,下图对Spring Security过滤链示意图:
从图中我们可以发现Spring Security框架在用户发送一个请求进入系统时,会经过一些列拦截器拦截后才能访问到我们自己定义的Rest API或者自定义Controller API。上图中Spring Security第一个拦截器是SecurityContextPersistenceFilter,它主要存放用户的认证信息。然后进入第二个拦截器UsernamePasswordAuthenticationFilter,它主要用来拦截Spring Security拦截用户密码表单登录认证使用(默认,当发现请求是Post,请求地址是/login,且参数包含了username/password时,就进入了认证环节)。
一、用户认证流程
UsernamePasswordAuthenticationFilter的认证过程流程图如下:
下边将会对用户登录认证流程结果源码进行分析:
UsernamePasswordAuthenticationFilter父类AbstractAuthenticationProcessingFilter#doFilter()
当请求是Post且请求地址是/login时,会被UsernamePasswordAuthenticationFilter拦截到,进入该拦截器时会首先进入它的父类`AbstractAuthenticationProcessingFilter#doFilter(ServletRequest req, ServletResponse res, FilterChain chain)`;
AbstractAuthenticationProcessingFilter#doFilter()源码:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware { protected ApplicationEventPublisher eventPublisher; protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); private AuthenticationManager authenticationManager; protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private RememberMeServices rememberMeServices = new NullRememberMeServices(); private RequestMatcher requiresAuthenticationRequestMatcher; private boolean continueChainBeforeSuccessfulAuthentication = false; private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy(); private boolean allowSessionCreation = true; private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) { this.setFilterProcessesUrl(defaultFilterProcessesUrl); } protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) { Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null"); this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher; } public void afterPropertiesSet() { Assert.notNull(this.authenticationManager, "authenticationManager must be specified"); } 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)) { // 验证是否请求路径是否复核条件:请求方式POST、地址为/login 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); // 调用子类UsernamePasswordAuthenticationFilter#attemtAuthentication(...) if (authResult == null) { return; } this.sessionStrategy.onAuthentication(authResult, request, response); // 登录成功后,通过SessionStrategry记录登录信息 } catch (InternalAuthenticationServiceException var8) { this.logger.error("An internal error occurred while trying to authenticate the user.", var8); this.unsuccessfulAuthentication(request, response, var8); // 登录失败,执行AuthenticationFailureHandler return; } catch (AuthenticationException var9) { this.unsuccessfulAuthentication(request, response, var9); // 登录失败,执行AuthenticationFailureHandler return; } if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } this.successfulAuthentication(request, response, chain, authResult); // 登录成功后,调用用AuthenticationSuccessHandler } } protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return this.requiresAuthenticationRequestMatcher.matches(request); } public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException; 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); } SecurityContextHolder.getContext().setAuthentication(authResult); // 登录成功后,将认证用户记录到SecurityContextHolder中,等其他请求进来时,可以从SecurityContextHolder中获取认证信息。 this.rememberMeServices.loginSuccess(request, response, authResult); // 登录成功后,执行‘记住我’逻辑 if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); // 登录成功后,执行AuthenticationSuccessHandler } protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); // 登录失败后,清空SecurityContextHolder if (this.logger.isDebugEnabled()) { this.logger.debug("Authentication request failed: " + failed.toString(), failed); this.logger.debug("Updated SecurityContextHolder to contain null Authentication"); this.logger.debug("Delegating to authentication failure handler " + this.failureHandler); } this.rememberMeServices.loginFail(request, response); // 登录失败后,执行‘记住我’逻辑 this.failureHandler.onAuthenticationFailure(request, response, failed); // 登录失败后,执行AuthenticationFailureHandler } protected AuthenticationManager getAuthenticationManager() { return this.authenticationManager; } public void setAuthenticationManager(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } public void setFilterProcessesUrl(String filterProcessesUrl) { this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(filterProcessesUrl)); } public final void setRequiresAuthenticationRequestMatcher(RequestMatcher requestMatcher) { Assert.notNull(requestMatcher, "requestMatcher cannot be null"); this.requiresAuthenticationRequestMatcher = requestMatcher; } public RememberMeServices getRememberMeServices() { return this.rememberMeServices; } public void setRememberMeServices(RememberMeServices rememberMeServices) { Assert.notNull(rememberMeServices, "rememberMeServices cannot be null"); this.rememberMeServices = rememberMeServices; } public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) { this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication; } public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); this.authenticationDetailsSource = authenticationDetailsSource; } public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); } protected boolean getAllowSessionCreation() { return this.allowSessionCreation; } public void setAllowSessionCreation(boolean allowSessionCreation) { this.allowSessionCreation = allowSessionCreation; } public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) { this.sessionStrategy = sessionStrategy; } public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) { Assert.notNull(successHandler, "successHandler cannot be null"); this.successHandler = successHandler; } public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) { Assert.notNull(failureHandler, "failureHandler cannot be null"); this.failureHandler = failureHandler; } protected AuthenticationSuccessHandler getSuccessHandler() { return this.successHandler; } protected AuthenticationFailureHandler getFailureHandler() { return this.failureHandler; } }
1)如果请求地址在`HttpSecurity`中配置的http.formLogin()等信息是否复核条件(默认,验证是否请求方式post、地址为:/login)就会进入认证环节,否则就跳过进入下一个拦截器;
@EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Autowired private LogoutSuccessHandler logoutSuccessHandler; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; .... @Override protected void configure(HttpSecurity http) throws Exception { .... // 这里就是自定义login页面为login.html,请求地址为/login,设定了自定义failureHandler/successHandler等 http.formLogin().loginProcessingUrl("/login") .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and() .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 解决不允许显示在iframe的问题 ... } 。。。 }
2)认证:调用AbstractAuthenticationProcessingFilter子类UsernamePasswordAuthenticationFilter#attemptAuthentication(request, response)进行认证;
2.1)认证成功后,就会将认证成功信息通过sessionStrategy.onAuthentication(authResult, request, response)保存到内存中;
2.2)认证成功后,将认证信息存储到SecurityContextHolder#context中;
2.3)认证成功后,执行‘记住我’;
2.4)认证成功后,还会执行successHandler : AuthenticationSuccessHandler。
2.5)认证失败后,清空SecurityContextHolder#context中信息;
2.6)认证失败后,执行‘记住我’;
2.5)认证失败后,会执行failureHandler : AuthenticationFailureHandler。
调用AbstractAuthenticationProcessingFilter子类UsernamePasswordAuthenticationFilter#attemptAuthentication(request, response)进行认证
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 String usernameParameter = "username"; private String passwordParameter = "password"; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); // 指定进入该Filter的请求url规则:POST请求、请求地址为/login。注意:这里也可以在用户自己配置。 } 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 { String username = this.obtainUsername(request);// 从请求中获取username参数,该参数也可以用户自定义别名 String password = this.obtainPassword(request);// 从请求中获取password参数,该参数也可以用户自定义别名 if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // 将用户、密码包装为UsernamePasswordAuthenticationToken对象 this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); // 调用AuthenticationManager#anthenticate(authRequest) } } protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); } protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); } protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; } public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getUsernameParameter() { return this.usernameParameter; } public final String getPasswordParameter() { return this.passwordParameter; } }
在·attemptAuthentication()·方法内部实现逻辑:
1)验证请求必须是POST,否则抛出异常;
2)包装username/password为UsernamePasswordAuthenticationToken对象;
3)调用AuthenticationManager#authenticate(UsernamePasswordAuthenticationToken authentication)进行认证。
AuthenticationManager#authenticate(UsernamePasswordAuthenticationToken extends Authentication authentication)源码分析:
AuthenticationManager其实是一个接口:
public interface AuthenticationManager { Authentication authenticate(Authentication var1) throws AuthenticationException; }
AuthenticationManager的唯一实现是ProviderManager类
ProviderManager类源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { private static final Log logger = LogFactory.getLog(ProviderManager.class); private AuthenticationEventPublisher eventPublisher; private List<AuthenticationProvider> providers; protected MessageSourceAccessor messages; private AuthenticationManager parent; private boolean eraseCredentialsAfterAuthentication; public ProviderManager(List<AuthenticationProvider> providers) { this(providers, (AuthenticationManager)null); } public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) { this.eventPublisher = new ProviderManager.NullEventPublisher(); this.providers = Collections.emptyList(); this.messages = SpringSecurityMessageSource.getAccessor(); this.eraseCredentialsAfterAuthentication = true; Assert.notNull(providers, "providers list cannot be null"); this.providers = providers; this.parent = parent; this.checkState(); } public void afterPropertiesSet() throws Exception { this.checkState(); } private void checkState() { if (this.parent == null && this.providers.isEmpty()) { throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required"); } } public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); Iterator var6 = this.getProviders().iterator(); // 其中就包含了实现类:DaoAuthenticationProvider while(var6.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var6.next(); if (provider.supports(toTest)) { if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); // 调用DaoAuthenticationProvider#authenticate(UsernamePasswordAuthenticationToken对象) if (result != null) { this.copyDetails(authentication, result); // 认证成功后将result的信息拷贝给UsernamePasswordAuthenticationToken对象 break; } } catch (AccountStatusException var11) { this.prepareException(var11, authentication); throw var11; } catch (InternalAuthenticationServiceException var12) { this.prepareException(var12, authentication); throw var12; } catch (AuthenticationException var13) { lastException = var13; } } } if (result == null && this.parent != null) { try { result = this.parent.authenticate(authentication); } catch (ProviderNotFoundException var9) { } catch (AuthenticationException var10) { lastException = var10; } } if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } this.eventPublisher.publishAuthenticationSuccess(result); return result; } else { if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}")); } this.prepareException((AuthenticationException)lastException, authentication); throw lastException; } } 。。。 }
ProviderManager#List<AuthenticationProvider> providers的AuthenticationProvider实现类包含:
ProviderManager#authenticate(Authentication authentication) 内部实现逻辑:
1)遍历providers,其中DaoAuthenticationProvider就是AuthenticationProvider的一个实现;
2)当调用provider#authenticate(authentication);获取登录用户认证,认证成功后会返回用户信息result;
3)调用this.copyDetails(authentication, result);将result的信息赋值给authentication:UsernamePasswordAuthenticationToken。
需要注意:providers的赋值是AuthenticationManagerBuilder去赋值的,具体可以参考其他源码。
DaoAuthenticationProvider#authentication(authentication)分析:
DaoAuthenticationProvider的authentication()方法实现是定义在它的父类AbstractUserDetailsAuthenticationProvider中。
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { protected final Log logger = LogFactory.getLog(this.getClass()); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private UserCache userCache = new NullUserCache(); private boolean forcePrincipalAsString = false; protected boolean hideUserNotFoundExceptions = true; private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks(); private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks(); private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); public AbstractUserDetailsAuthenticationProvider() { } protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException; public final void afterPropertiesSet() throws Exception { Assert.notNull(this.userCache, "A user cache must be set"); Assert.notNull(this.messages, "A message source must be set"); this.doAfterPropertiesSet(); } public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.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) { // 如果缓存中不存在用户信息,就调用DaoAuthenticationProvider验证 cacheWasUsed = false; try { user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); // 调用DaoAuthenticationProvider#retrieveUser(...) } 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);// preAuthenticationChecks.check(user),验证:!user.isAccountNonLocked()、!user.isEnabled()、!user.isAccountNonExpired()就抛出异常; 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); // postAuthenticationChecks.check(user):验证!user.isCredentialsNonExpired()就抛出异常。 if (!cacheWasUsed) { // 缓存中没有user信息时,就将user放入缓存 this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user); } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; } 。。。 private class DefaultPostAuthenticationChecks implements UserDetailsChecker { private DefaultPostAuthenticationChecks() { } public void check(UserDetails user) { if (!user.isCredentialsNonExpired()) { AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account credentials have expired"); throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired")); } } } private class DefaultPreAuthenticationChecks implements UserDetailsChecker { private DefaultPreAuthenticationChecks() { } public void check(UserDetails user) { if (!user.isAccountNonLocked()) { AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is locked"); throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked")); } else if (!user.isEnabled()) { AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is disabled"); throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled")); } else if (!user.isAccountNonExpired()) { AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is expired"); throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired")); } } } }
1)内部调用UserDetails user=DaoAuthenticationProvider#retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication)
2)如果1)失败就抛出异常;如果1)成功就执行this.preAuthenticationChecks.check(user)、this.postAuthenticationChecks.check(user)
2.1)this.preAuthenticationChecks.check(user):验证!user.isAccountNonLocked()、!user.isEnabled()、!user.isAccountNonExpired()就抛出异常;
2.2)this.postAuthenticationChecks.check(user):验证!user.isCredentialsNonExpired()就抛出异常。
2.3)缓存中没有user信息时,就将user放入缓存。
DaoAuthenticationProvider#retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication)
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { 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); } }
这的UserDetailsService实现情况:
其中我们这里是使用了自定义UserDetailsServiceImpl
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Autowired private PermissionDao permissionDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = userService.getUser(username); if (sysUser == null) { throw new AuthenticationCredentialsNotFoundException("用户名不存在"); } else if (sysUser.getStatus() == Status.LOCKED) { throw new LockedException("用户被锁定,请联系管理员"); } else if (sysUser.getStatus() == Status.DISABLED) { throw new DisabledException("用户已作废"); } LoginUser loginUser = new LoginUser(); BeanUtils.copyProperties(sysUser, loginUser); List<Permission> permissions = permissionDao.listByUserId(sysUser.getId()); loginUser.setPermissions(permissions); return loginUser; } }
在使用自定义UserDetailsService后,需要在项目的config类中指定UserDetailsService实现类。
@EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Autowired private LogoutSuccessHandler logoutSuccessHandler; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private UserDetailsService userDetailsService; @Autowired private TokenFilter tokenFilter; @Autowired private TokenService tokenService; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); // 基于token,所以不需要session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeRequests() .antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**", "/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**", "/statics/**") .permitAll().anyRequest().authenticated(); http.formLogin().loginProcessingUrl("/login") .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and() .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 解决不允许显示在iframe的问题 http.headers().frameOptions().disable(); http.headers().cacheControl(); http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } /** * 登陆成功,返回Token * * @return */ @Bean public AuthenticationSuccessHandler loginSuccessHandler() { return new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Token token = tokenService.saveToken(loginUser); ResponseUtil.responseJson(response, HttpStatus.OK.value(), token); } }; } /** * 登陆失败 * * @return */ @Bean public AuthenticationFailureHandler loginFailureHandler() { return new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { String msg = null; if (exception instanceof BadCredentialsException) { msg = "密码错误"; } else { msg = exception.getMessage(); } ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", msg); ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), info); } }; } /** * 未登录,返回401 * * @return */ @Bean public AuthenticationEntryPoint authenticationEntryPoint() { return new AuthenticationEntryPoint() { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", "请先登录"); ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), info); } }; } /** * 退出处理 * * @return */ @Bean public LogoutSuccessHandler logoutSussHandler() { return new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { ResponseInfo info = new ResponseInfo(HttpStatus.OK.value() + "", "退出成功"); String token = TokenFilter.getToken(request); tokenService.deleteToken(token); ResponseUtil.responseJson(response, HttpStatus.OK.value(), info); } }; } }
上边定义的AuthenticationSuccessHandler中做了特殊处理:
Token token = tokenService.saveToken(loginUser);
ResponseUtil.responseJson(response, HttpStatus.OK.value(), token);
当登录成功后,返回了Token给前端,因此前端与后端交互时采用的token进行验证,因此才需要配置一个TokenFilter来做特殊处理:
这里定义一个拦截类TokenFilter.java(目的是在进入UsernamePasswordAuthenticationFilter拦截器之前提前将token换得authentication对象存入SecurityContextHolder#context中,SpringSecurity内部是采用的authentication去验证的。)
@Component public class TokenFilter extends OncePerRequestFilter { public static final String TOKEN_KEY = "token"; @Autowired private TokenService tokenService; @Autowired private UserDetailsService userDetailsService; private static final Long MINUTES_10 = 10 * 60 * 1000L; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = getToken(request); if (StringUtils.isNotBlank(token)) { LoginUser loginUser = tokenService.getLoginUser(token); if (loginUser != null) { loginUser = checkLoginTime(loginUser); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } /** * 校验时间<br> * 过期时间与当前时间对比,临近过期10分钟内的话,自动刷新缓存 * * @param loginUser * @return */ private LoginUser checkLoginTime(LoginUser loginUser) { long expireTime = loginUser.getExpireTime(); long currentTime = System.currentTimeMillis(); if (expireTime - currentTime <= MINUTES_10) { String token = loginUser.getToken(); loginUser = (LoginUser) userDetailsService.loadUserByUsername(loginUser.getUsername()); loginUser.setToken(token); tokenService.refresh(loginUser); } return loginUser; } /** * 根据参数或者header获取token * * @param request * @return */ public static String getToken(HttpServletRequest request) { String token = request.getParameter(TOKEN_KEY); if (StringUtils.isBlank(token)) { token = request.getHeader(TOKEN_KEY); } return token; } }
二、认证后认证结果如何实现多个请求之间共享?
下面我们来分析下Spring Security认证后如何实现多个请求之间共享登录信息。
UsernamePasswordXXXFilter完整认证流程
其实UsernamePasswordAuthenticationFilter#doFilter()[实际上doFilter()定义在它的父类AbstractAuthenticationProcessingFilter类中]中包含了比较完整的认证流程。下图是对认证流程的一个完整解析:包含了认证失败、认证成功后的处理逻辑。
结合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); // 调用UsernamePasswordAuthenticationFilter#attempAuthentication(...) if (authResult == null) { return; } this.sessionStrategy.onAuthentication(authResult, request, response); // 认证成功后动作1:执行session策略 } catch (InternalAuthenticationServiceException var8) { this.logger.error("An internal error occurred while trying to authenticate the user.", var8); this.unsuccessfulAuthentication(request, response, var8); // 认证失败后动作1:执行this.unsuccessfulAuthentication(...) return; } catch (AuthenticationException var9) { this.unsuccessfulAuthentication(request, response, var9); // 认证失败后动作1:执行this.unsuccessfulAuthentication(...) return; } if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } this.successfulAuthentication(request, response, chain, authResult); // 认证成功后动作2:执行this.successfulAuthentication(...) } }
我们这里重点关系是认证成功后处理动作:
- 认证成功后动作1:执行session策略;
- 认证成功后动作2:执行this.successfulAuthentication(...):
- successfulAuthentication(...)源码:
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); } SecurityContextHolder.getContext().setAuthentication(authResult); // 登录成功后,将认证用户记录到SecurityContextHolder中,等其他请求进来时,可以从SecurityContextHolder中获取认证信息。 this.rememberMeServices.loginSuccess(request, response, authResult); // 登录成功后,执行‘记住我’逻辑 if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); // 登录成功后,执行AuthenticationSuccessHandler }
- 代码逻辑:
- 1)登录成功后,将认证用户记录到SecurityContextHolder中,等其他请求进来时,可以从SecurityContextHolder中获取认证信息;
- 2)登录成功后,执行‘记住我’逻辑;
- 3)登录成功后,执行AuthenticationSuccessHandler
- successfulAuthentication(...)源码:
SecurityContextHolder存储通过认证的用户信息
从上边代码分析可以得知当认证成功后,会将用户登录信息存储到SecurityContextHolder#context中,但要了解SecurityContextHolder如何存储用户信息,还需要查阅该类的实现源码:
public class SecurityContextHolder { public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL"; public static final String MODE_GLOBAL = "MODE_GLOBAL"; public static final String SYSTEM_PROPERTY = "spring.security.strategy"; private static String strategyName = System.getProperty("spring.security.strategy"); private static SecurityContextHolderStrategy strategy; private static int initializeCount = 0; public SecurityContextHolder() { } public static void clearContext() { strategy.clearContext(); } public static SecurityContext getContext() { return strategy.getContext(); } public static int getInitializeCount() { return initializeCount; } private static void initialize() { if (!StringUtils.hasText(strategyName)) { strategyName = "MODE_THREADLOCAL"; // 默认策略实现 } if (strategyName.equals("MODE_THREADLOCAL")) { strategy = new ThreadLocalSecurityContextHolderStrategy(); // 1)本地线程存储策略(内存) private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal(); } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) { strategy = new InheritableThreadLocalSecurityContextHolderStrategy(); // 2)可继承本地线程存储策略(内存)private static final ThreadLocal<SecurityContext> contextHolder = new InheritableThreadLocal(); } else if (strategyName.equals("MODE_GLOBAL")) { strategy = new GlobalSecurityContextHolderStrategy(); // 3)全局策略(静态变量,内存) private static SecurityContext contextHolder = new SecurityContextImpl(); } else { // 4)自定义策略 try { Class<?> clazz = Class.forName(strategyName); Constructor<?> customStrategy = clazz.getConstructor(); strategy = (SecurityContextHolderStrategy)customStrategy.newInstance(); } catch (Exception var2) { ReflectionUtils.handleReflectionException(var2); } } ++initializeCount; } public static void setContext(SecurityContext context) { strategy.setContext(context); } public static void setStrategyName(String strategyName) { SecurityContextHolder.strategyName = strategyName; initialize(); } public static SecurityContextHolderStrategy getContextHolderStrategy() { return strategy; } public static SecurityContext createEmptyContext() { return strategy.createEmptyContext(); } public String toString() { return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]"; } static { initialize(); } }
从查阅代码可以知道SecurityContextHolder#context就是SecurityContextHolderStrategy#context。
默认SecurityContextHolderStrategy提供了三种实现,另外也支持用户自定义:
1)本地线程存储策略(内存) private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();
2)可继承本地线程存储策略(内存)private static final ThreadLocal<SecurityContext> contextHolder = new InheritableThreadLocal();
3)全局策略(静态变量,内存) private static SecurityContext contextHolder = new SecurityContextImpl();
4)自定义策略。
默认SecurityContextHolderStrategy实现为:ThreadLocalSecurityContextHolderStrategy。
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal(); ThreadLocalSecurityContextHolderStrategy() { } public void clearContext() { contextHolder.remove(); } public SecurityContext getContext() { SecurityContext ctx = (SecurityContext)contextHolder.get(); if (ctx == null) { ctx = this.createEmptyContext(); contextHolder.set(ctx); } return ctx; } public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); contextHolder.set(context); } public SecurityContext createEmptyContext() { return new SecurityContextImpl(); } }
将认证后的信息存储到ThreadLocal<SecurityContext>变量中,那么就可以实现其他线程就可以共享该变量。
但是具体另外一个请求进来时,会先经过SecurityContextPersistenceFilter,它主要具有以下功能:使用SecurityContextRepository在session中保存或更新一个SecurityContext域对象(相当于一个容器),并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。 其他的过滤器都需要依赖于它。在 Spring Security 中,虽然安全上下文信息被存储于 Session 中,但我们在实际使用中不应该直接操作 Session,而应当使用 SecurityContextHolder。
三、获取认证用户信息
上边我们知道最终认证通过后Spring Security是把信息存储到了Sesssion中,但是如果要获取认证信息可以通过SecurityContextHolder去拉取:
@GetMapping("/me") public LoginUser getMeDetail() { return UserUtil.getLoginUser(); } public class UserUtil { public static LoginUser getLoginUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { if (authentication instanceof AnonymousAuthenticationToken) { return null; } if (authentication instanceof UsernamePasswordAuthenticationToken) { return (LoginUser) authentication.getPrincipal(); } } return null; } }
上边这种方式只获取到我们想要的特定认证信息,另外也可以通过:
@GetMapping("/me1") public Object getMeDetail(Authentication authentication){ return authentication; }
这种方式会获取用户的全部信息,包括地址等信息。如果我们只想获取用户名和密码以及它的权限,不需要ip地址等太多的信息可以使用下面的方式来获取信息。
@GetMapping("/me2") public UserDetails getMeDetail(@AuthenticationPrincipal UserDetails userDetails){ return userDetails; }
至此,本文深入源码了解到了Spring Seucrity的认证流程,以及认证结果如何在多个请求之间共享的问题。也许上面的内容看的不是很清楚,你可以结合源码来解读,自己看一看源码Spring Security的认证流程会更加清晰。
后续我们将讲解如何自定账户、权限信息:第一篇文章中我们在applicationContext-shiro.xml中配置了账户、密码、用户权限,我们知道这么配置是写死的,在真实项目中需要将账户、密码、权限保存到数据库或者其他系统中,如何实现呢?
参考:
《Spring Security验证流程剖析及自定义验证方法》
《Spring Security修炼手册(四)————Security默认表达式的权限控制》
基础才是编程人员应该深入研究的问题,比如:
1)List/Set/Map内部组成原理|区别
2)mysql索引存储结构&如何调优/b-tree特点、计算复杂度及影响复杂度的因素。。。
3)JVM运行组成与原理及调优
4)Java类加载器运行原理
5)Java中GC过程原理|使用的回收算法原理
6)Redis中hash一致性实现及与hash其他区别
7)Java多线程、线程池开发、管理Lock与Synchroined区别
8)Spring IOC/AOP 原理;加载过程的。。。
【+加关注】。