让我们带着以下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 implementsApplicationEventPublisherAware, MessageSourceAware {protectedApplicationEventPublisher eventPublisher;protected AuthenticationDetailsSource authenticationDetailsSource = newWebAuthenticationDetailsSource();privateAuthenticationManager authenticationManager;protected MessageSourceAccessor messages =SpringSecurityMessageSource.getAccessor();private RememberMeServices rememberMeServices = newNullRememberMeServices();privateRequestMatcher requiresAuthenticationRequestMatcher;private boolean continueChainBeforeSuccessfulAuthentication = false;private SessionAuthenticationStrategy sessionStrategy = newNullAuthenticatedSessionStrategy();private boolean allowSessionCreation = true;private AuthenticationSuccessHandler successHandler = newSavedRequestAwareAuthenticationSuccessHandler();private AuthenticationFailureHandler failureHandler = newSimpleUrlAuthenticationFailureHandler();protectedAbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {this.setFilterProcessesUrl(defaultFilterProcessesUrl);
}protectedAbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher,"requiresAuthenticationRequestMatcher cannot be null");this.requiresAuthenticationRequestMatcher =requiresAuthenticationRequestMatcher;
}public voidafterPropertiesSet() {
Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
}public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throwsIOException, 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 booleanrequiresAuthentication(HttpServletRequest request, HttpServletResponse response) {return this.requiresAuthenticationRequestMatcher.matches(request);
}public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throwsAuthenticationException, IOException, ServletException;protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throwsIOException, 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) throwsIOException, 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
}protectedAuthenticationManager getAuthenticationManager() {return this.authenticationManager;
}public voidsetAuthenticationManager(AuthenticationManager authenticationManager) {this.authenticationManager =authenticationManager;
}public voidsetFilterProcessesUrl(String filterProcessesUrl) {this.setRequiresAuthenticationRequestMatcher(newAntPathRequestMatcher(filterProcessesUrl));
}public final voidsetRequiresAuthenticationRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher,"requestMatcher cannot be null");this.requiresAuthenticationRequestMatcher =requestMatcher;
}publicRememberMeServices getRememberMeServices() {return this.rememberMeServices;
}public voidsetRememberMeServices(RememberMeServices rememberMeServices) {
Assert.notNull(rememberMeServices,"rememberMeServices cannot be null");this.rememberMeServices =rememberMeServices;
}public void setContinueChainBeforeSuccessfulAuthentication(booleancontinueChainBeforeSuccessfulAuthentication) {this.continueChainBeforeSuccessfulAuthentication =continueChainBeforeSuccessfulAuthentication;
}public voidsetApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {this.eventPublisher =eventPublisher;
}public void setAuthenticationDetailsSource(AuthenticationDetailsSourceauthenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource,"AuthenticationDetailsSource required");this.authenticationDetailsSource =authenticationDetailsSource;
}public voidsetMessageSource(MessageSource messageSource) {this.messages = newMessageSourceAccessor(messageSource);
}protected booleangetAllowSessionCreation() {return this.allowSessionCreation;
}public void setAllowSessionCreation(booleanallowSessionCreation) {this.allowSessionCreation =allowSessionCreation;
}public voidsetSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {this.sessionStrategy =sessionStrategy;
}public voidsetAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler,"successHandler cannot be null");this.successHandler =successHandler;
}public voidsetAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler,"failureHandler cannot be null");this.failureHandler =failureHandler;
}protectedAuthenticationSuccessHandler getSuccessHandler() {return this.successHandler;
}protectedAuthenticationFailureHandler getFailureHandler() {return this.failureHandler;
}
}
1)如果请求地址在`HttpSecurity`中配置的http.formLogin()等信息是否复核条件(默认,验证是否请求方式post、地址为:/login)就会进入认证环节,否则就跳过进入下一个拦截器;
@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extendsWebSecurityConfigurerAdapter {
@AutowiredprivateAuthenticationSuccessHandler authenticationSuccessHandler;
@AutowiredprivateAuthenticationFailureHandler authenticationFailureHandler;
@AutowiredprivateLogoutSuccessHandler logoutSuccessHandler;
@AutowiredprivateAuthenticationEntryPoint authenticationEntryPoint;
....
@Overrideprotected void configure(HttpSecurity http) throwsException {
....//这里就是自定义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 extendsAbstractAuthenticationProcessingFilter {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;publicUsernamePasswordAuthenticationFilter() {super(new AntPathRequestMatcher("/login", "POST")); //指定进入该Filter的请求url规则:POST请求、请求地址为/login。注意:这里也可以在用户自己配置。
}public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throwsAuthenticationException {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)
}
}protectedString obtainPassword(HttpServletRequest request) {return request.getParameter(this.passwordParameter);
}protectedString obtainUsername(HttpServletRequest request) {return request.getParameter(this.usernameParameter);
}protected voidsetDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}public voidsetUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter,"Username parameter must not be empty or null");this.usernameParameter =usernameParameter;
}public voidsetPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter,"Password parameter must not be empty or null");this.passwordParameter =passwordParameter;
}public void setPostOnly(booleanpostOnly) {this.postOnly =postOnly;
}public finalString getUsernameParameter() {return this.usernameParameter;
}public finalString getPasswordParameter() {return this.passwordParameter;
}
}
在·attemptAuthentication()·方法内部实现逻辑:
1)验证请求必须是POST,否则抛出异常;
2)包装username/password为UsernamePasswordAuthenticationToken对象;
3)调用AuthenticationManager#authenticate(UsernamePasswordAuthenticationToken authentication)进行认证。
AuthenticationManager#authenticate(UsernamePasswordAuthenticationToken extends Authentication authentication)源码分析:
AuthenticationManager其实是一个接口:
public interfaceAuthenticationManager {
Authentication authenticate(Authentication var1)throwsAuthenticationException;
}
AuthenticationManager的唯一实现是ProviderManager类
ProviderManager类源码:
public class ProviderManager implementsAuthenticationManager, MessageSourceAware, InitializingBean {private static final Log logger = LogFactory.getLog(ProviderManager.class);privateAuthenticationEventPublisher eventPublisher;private Listproviders;protectedMessageSourceAccessor messages;privateAuthenticationManager parent;private booleaneraseCredentialsAfterAuthentication;public ProviderManager(Listproviders) {this(providers, (AuthenticationManager)null);
}public ProviderManager(Listproviders, AuthenticationManager parent) {this.eventPublisher = newProviderManager.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() throwsException {this.checkState();
}private voidcheckState() {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) throwsAuthenticationException {
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);throwvar11;
}catch(InternalAuthenticationServiceException var12) {this.prepareException(var12, authentication);throwvar12;
}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 instanceofCredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}this.eventPublisher.publishAuthenticationSuccess(result);returnresult;
}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);throwlastException;
}
}
。。。
}
ProviderManager#List 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 implementsAuthenticationProvider, InitializingBean, MessageSourceAware {protected final Log logger = LogFactory.getLog(this.getClass());protected MessageSourceAccessor messages =SpringSecurityMessageSource.getAccessor();private UserCache userCache = newNullUserCache();private boolean forcePrincipalAsString = false;protected boolean hideUserNotFoundExceptions = true;private UserDetailsChecker preAuthenticationChecks = newAbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();private UserDetailsChecker postAuthenticationChecks = newAbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();private GrantedAuthoritiesMapper authoritiesMapper = newNullAuthoritiesMapper();publicAbstractUserDetailsAuthenticationProvider() {
}protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throwsAuthenticationException;public final void afterPropertiesSet() throwsException {
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) throwsAuthenticationException {
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"));
}throwvar6;
}
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) {throwvar7;
}
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);
}protectedAuthentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result= new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());returnresult;
}
。。。private class DefaultPostAuthenticationChecks implementsUserDetailsChecker {privateDefaultPostAuthenticationChecks() {
}public voidcheck(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 implementsUserDetailsChecker {privateDefaultPreAuthenticationChecks() {
}public voidcheck(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) throwsAuthenticationException {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{returnloadedUser;
}
}catch(UsernameNotFoundException var4) {this.mitigateAgainstTimingAttack(authentication);throwvar4;
}catch(InternalAuthenticationServiceException var5) {throwvar5;
}catch(Exception var6) {throw newInternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
这的UserDetailsService实现情况:
其中我们这里是使用了自定义UserDetailsServiceImpl
@Servicepublic class UserDetailsServiceImpl implementsUserDetailsService {
@AutowiredprivateUserService userService;
@AutowiredprivatePermissionDao permissionDao;
@Overridepublic UserDetails loadUserByUsername(String username) throwsUsernameNotFoundException {
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= newLoginUser();
BeanUtils.copyProperties(sysUser, loginUser);
List permissions =permissionDao.listByUserId(sysUser.getId());
loginUser.setPermissions(permissions);returnloginUser;
}
}
在使用自定义UserDetailsService后,需要在项目的config类中指定UserDetailsService实现类。
@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extendsWebSecurityConfigurerAdapter {
@AutowiredprivateAuthenticationSuccessHandler authenticationSuccessHandler;
@AutowiredprivateAuthenticationFailureHandler authenticationFailureHandler;
@AutowiredprivateLogoutSuccessHandler logoutSuccessHandler;
@AutowiredprivateAuthenticationEntryPoint authenticationEntryPoint;
@AutowiredprivateUserDetailsService userDetailsService;
@AutowiredprivateTokenFilter tokenFilter;
@AutowiredprivateTokenService tokenService;
@BeanpublicBCryptPasswordEncoder bCryptPasswordEncoder() {return newBCryptPasswordEncoder();
}
@Overrideprotected void configure(HttpSecurity http) throwsException {
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);
}
@Overrideprotected void configure(AuthenticationManagerBuilder auth) throwsException {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}/*** 登陆成功,返回Token
*
*@return
*/@BeanpublicAuthenticationSuccessHandler loginSuccessHandler() {return newAuthenticationSuccessHandler() {
@Overridepublic voidonAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication)throwsIOException, ServletException {
LoginUser loginUser=(LoginUser) authentication.getPrincipal();
Token token=tokenService.saveToken(loginUser);
ResponseUtil.responseJson(response, HttpStatus.OK.value(), token);
}
};
}/*** 登陆失败
*
*@return
*/@BeanpublicAuthenticationFailureHandler loginFailureHandler() {return newAuthenticationFailureHandler() {
@Overridepublic voidonAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception)throwsIOException, ServletException {
String msg= null;if (exception instanceofBadCredentialsException) {
msg= "密码错误";
}else{
msg=exception.getMessage();
}
ResponseInfo info= new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", msg);
ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), info);
}
};
}/*** 未登录,返回401
*
*@return
*/@BeanpublicAuthenticationEntryPoint authenticationEntryPoint() {return newAuthenticationEntryPoint() {
@Overridepublic voidcommence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException)throwsIOException, ServletException {
ResponseInfo info= new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", "请先登录");
ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), info);
}
};
}/*** 退出处理
*
*@return
*/@BeanpublicLogoutSuccessHandler logoutSussHandler() {return newLogoutSuccessHandler() {
@Overridepublic voidonLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication)throwsIOException, 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去验证的。)
@Componentpublic class TokenFilter extendsOncePerRequestFilter {public static final String TOKEN_KEY = "token";
@AutowiredprivateTokenService tokenService;
@AutowiredprivateUserDetailsService userDetailsService;private static final Long MINUTES_10 = 10 * 60 * 1000L;
@Overrideprotected voiddoFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throwsServletException, IOException {
String token=getToken(request);if(StringUtils.isNotBlank(token)) {
LoginUser loginUser=tokenService.getLoginUser(token);if (loginUser != null) {
loginUser=checkLoginTime(loginUser);
UsernamePasswordAuthenticationToken authentication= newUsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}/*** 校验时间
* 过期时间与当前时间对比,临近过期10分钟内的话,自动刷新缓存
*
*@paramloginUser
*@return
*/
privateLoginUser 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);
}returnloginUser;
}/*** 根据参数或者header获取token
*
*@paramrequest
*@return
*/
public staticString getToken(HttpServletRequest request) {
String token=request.getParameter(TOKEN_KEY);if(StringUtils.isBlank(token)) {
token=request.getHeader(TOKEN_KEY);
}returntoken;
}
}
二、认证后认证结果如何实现多个请求之间共享?
下面我们来分析下Spring Security认证后如何实现多个请求之间共享登录信息。
UsernamePasswordXXXFilter完整认证流程
其实UsernamePasswordAuthenticationFilter#doFilter()[实际上doFilter()定义在它的父类AbstractAuthenticationProcessingFilter类中]中包含了比较完整的认证流程。下图是对认证流程的一个完整解析:包含了认证失败、认证成功后的处理逻辑。
结合AbstractAuthenticationProcessingFilter#doFilter(...)代码进行分析:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throwsIOException, 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) throwsIOException, 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
SecurityContextHolder存储通过认证的用户信息
从上边代码分析可以得知当认证成功后,会将用户登录信息存储到SecurityContextHolder#context中,但要了解SecurityContextHolder如何存储用户信息,还需要查阅该类的实现源码:
public classSecurityContextHolder {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 staticSecurityContextHolderStrategy strategy;private static int initializeCount = 0;publicSecurityContextHolder() {
}public static voidclearContext() {
strategy.clearContext();
}public staticSecurityContext getContext() {returnstrategy.getContext();
}public static intgetInitializeCount() {returninitializeCount;
}private static voidinitialize() {if (!StringUtils.hasText(strategyName)) {
strategyName= "MODE_THREADLOCAL"; //默认策略实现
}if (strategyName.equals("MODE_THREADLOCAL")) {
strategy= new ThreadLocalSecurityContextHolderStrategy(); //1)本地线程存储策略(内存) private static final ThreadLocal contextHolder = new ThreadLocal();
} else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
strategy= new InheritableThreadLocalSecurityContextHolderStrategy(); //2)可继承本地线程存储策略(内存)private static final ThreadLocal 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 voidsetContext(SecurityContext context) {
strategy.setContext(context);
}public static voidsetStrategyName(String strategyName) {
SecurityContextHolder.strategyName=strategyName;
initialize();
}public staticSecurityContextHolderStrategy getContextHolderStrategy() {returnstrategy;
}public staticSecurityContext createEmptyContext() {returnstrategy.createEmptyContext();
}publicString toString() {return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]";
}static{
initialize();
}
}
从查阅代码可以知道SecurityContextHolder#context就是SecurityContextHolderStrategy#context。
默认SecurityContextHolderStrategy提供了三种实现,另外也支持用户自定义:
1)本地线程存储策略(内存) private static final ThreadLocal contextHolder = new ThreadLocal();
2)可继承本地线程存储策略(内存)private static final ThreadLocal contextHolder = new InheritableThreadLocal();
3)全局策略(静态变量,内存) private static SecurityContext contextHolder = new SecurityContextImpl();
4)自定义策略。
默认SecurityContextHolderStrategy实现为:ThreadLocalSecurityContextHolderStrategy。
final class ThreadLocalSecurityContextHolderStrategy implementsSecurityContextHolderStrategy {private static final ThreadLocal contextHolder = newThreadLocal();
ThreadLocalSecurityContextHolderStrategy() {
}public voidclearContext() {
contextHolder.remove();
}publicSecurityContext getContext() {
SecurityContext ctx=(SecurityContext)contextHolder.get();if (ctx == null) {
ctx= this.createEmptyContext();
contextHolder.set(ctx);
}returnctx;
}public voidsetContext(SecurityContext context) {
Assert.notNull(context,"Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}publicSecurityContext createEmptyContext() {return newSecurityContextImpl();
}
}
将认证后的信息存储到ThreadLocal变量中,那么就可以实现其他线程就可以共享该变量。
但是具体另外一个请求进来时,会先经过SecurityContextPersistenceFilter,它主要具有以下功能:使用SecurityContextRepository在session中保存或更新一个SecurityContext域对象(相当于一个容器),并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。 其他的过滤器都需要依赖于它。在 Spring Security 中,虽然安全上下文信息被存储于 Session 中,但我们在实际使用中不应该直接操作 Session,而应当使用 SecurityContextHolder。
三、获取认证用户信息
上边我们知道最终认证通过后Spring Security是把信息存储到了Sesssion中,但是如果要获取认证信息可以通过SecurityContextHolder去拉取:
@GetMapping("/me")publicLoginUser getMeDetail() {returnUserUtil.getLoginUser();
}public classUserUtil {public staticLoginUser getLoginUser() {
Authentication authentication=SecurityContextHolder.getContext().getAuthentication();if (authentication != null) {if (authentication instanceofAnonymousAuthenticationToken) {return null;
}if (authentication instanceofUsernamePasswordAuthenticationToken) {return(LoginUser) authentication.getPrincipal();
}
}return null;
}
}
上边这种方式只获取到我们想要的特定认证信息,另外也可以通过:
@GetMapping("/me1")publicObject getMeDetail(Authentication authentication){returnauthentication;
}
这种方式会获取用户的全部信息,包括地址等信息。如果我们只想获取用户名和密码以及它的权限,不需要ip地址等太多的信息可以使用下面的方式来获取信息。
@GetMapping("/me2")publicUserDetails getMeDetail(@AuthenticationPrincipal UserDetails userDetails){returnuserDetails;
}
至此,本文深入源码了解到了Spring Seucrity的认证流程,以及认证结果如何在多个请求之间共享的问题。也许上面的内容看的不是很清楚,你可以结合源码来解读,自己看一看源码Spring Security的认证流程会更加清晰。
后续我们将讲解如何自定账户、权限信息:第一篇文章中我们在applicationContext-shiro.xml中配置了账户、密码、用户权限,我们知道这么配置是写死的,在真实项目中需要将账户、密码、权限保存到数据库或者其他系统中,如何实现呢?
参考: