java security 详解_Java-Security(四):用户认证流程源码分析

让我们带着以下3个问题来阅读本篇文章:

1)在Spring Security项目中用户认证过程中是如何执行的呢?

2)认证后认证结果如何实现多个请求之间共享?

3)如何获取认证信息?

在《Java-Security(二):如何初始化springSecurityFilterChain(FilterChainProxy)》中可以发现SpringSecurity的核心就是一系列过滤链,当一个请求进入时,首先会被过滤链拦截到,拦截到之后会首先经过校验,校验之后才可以访问到用户各种信息。

下图是Spring Security过滤链是Spring Security运行的核心,下图对Spring Security过滤链示意图:

bc30b8a1bf4cdfb0fa5dfcf693bcd581.png

从图中我们可以发现Spring Security框架在用户发送一个请求进入系统时,会经过一些列拦截器拦截后才能访问到我们自己定义的Rest API或者自定义Controller API。上图中Spring Security第一个拦截器是SecurityContextPersistenceFilter,它主要存放用户的认证信息。然后进入第二个拦截器UsernamePasswordAuthenticationFilter,它主要用来拦截Spring Security拦截用户密码表单登录认证使用(默认,当发现请求是Post,请求地址是/login,且参数包含了username/password时,就进入了认证环节)。

一、用户认证流程

UsernamePasswordAuthenticationFilter的认证过程流程图如下:

96120ede9dfd5f11ba4b266e90373bb4.png

下边将会对用户登录认证流程结果源码进行分析:

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类

9dc74529cadad9256f88d549450c937a.png

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实现类包含:

87d50b1a5b5e17ee4b2f038e8dd9f3b1.png

ProviderManager#authenticate(Authentication authentication) 内部实现逻辑:

1)遍历providers,其中DaoAuthenticationProvider就是AuthenticationProvider的一个实现;

2)当调用provider#authenticate(authentication);获取登录用户认证,认证成功后会返回用户信息result;

3)调用this.copyDetails(authentication, result);将result的信息赋值给authentication:UsernamePasswordAuthenticationToken。

需要注意:providers的赋值是AuthenticationManagerBuilder去赋值的,具体可以参考其他源码。

87ef764611427150cf5964507871d46c.png

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实现情况:

06c6686f9fd95087e79ebfebabb70ebd.png

其中我们这里是使用了自定义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);

38560ec99705edc98089925b21402eab.png

当登录成功后,返回了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类中]中包含了比较完整的认证流程。下图是对认证流程的一个完整解析:包含了认证失败、认证成功后的处理逻辑。

792764671890be24aeb61b20e7e5c22b.png

结合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中配置了账户、密码、用户权限,我们知道这么配置是写死的,在真实项目中需要将账户、密码、权限保存到数据库或者其他系统中,如何实现呢?

参考:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值