详解SpringSecurity中的Authentication信息与登录流程
Authentication
使用SpringSecurity可以在任何地方注入Authentication进而获取到当前登录的用户信息,可谓十分强大。
在Authenticaiton的继承体系中,实现类UsernamePasswordAuthenticationToken算是比较常见的一个了,在这个类中存在两个属性:principal和credentials,其实分别代表着用户和密码。【当然其他的属性存在于其父类中,如authorities和details。】
我们需要对这个对象有一个基本地认识,它保存了用户的基本信息。用户在登录的时候,进行了一系列的操作,将信息存与这个对象中,后续我们使用的时候,就可以轻松地获取这些信息了。
那么,用户信息如何存,又是如何取的呢?继续往下看吧。
登录流程
一、与认证相关的UsernamePasswordAuthenticationFilter
通过Servlet中的Filter技术进行实现,通过一系列内置的或自定义的安全Filter,实现接口的认证与授权。
比如:UsernamePasswordAuthenticationFilter
publicAuthenticationattemptAuthentication(HttpServletRequestrequest,
HttpServletResponseresponse)throwsAuthenticationException{
if(postOnly&&!request.getMethod().equals("POST")){
thrownewAuthenticationServiceException(
"Authenticationmethodnotsupported:"+request.getMethod());
}
//获取用户名和密码
Stringusername=obtainUsername(request);
Stringpassword=obtainPassword(request);
if(username==null){
username="";
}
if(password==null){
password="";
}
username=username.trim();
//构造UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationTokenauthRequest=newUsernamePasswordAuthenticationToken(
username,password);
//为details属性赋值
setDetails(request,authRequest);
//调用authenticate方法进行校验
returnthis.getAuthenticationManager().authenticate(authRequest);
}
获取用户名和密码
从request中提取参数,这也是SpringSecurity默认的表单登录需要通过key/value形式传递参数的原因。
@Nullable
protectedStringobtainPassword(HttpServletRequestrequest){
returnrequest.getParameter(passwordParameter);
}
@Nullable
protectedStringobtainUsername(HttpServletRequestrequest){
returnrequest.getParameter(usernameParameter);
}
构造UsernamePasswordAuthenticationToken对象
传入获取到的用户名和密码,而用户名对应UPAT对象中的principal属性,而密码对应credentials属性。
UsernamePasswordAuthenticationTokenauthRequest=newUsernamePasswordAuthenticationToken(
username,password);
//UsernamePasswordAuthenticationToken的构造器
publicUsernamePasswordAuthenticationToken(Objectprincipal,Objectcredentials){
super(null);
this.principal=principal;
this.credentials=credentials;
setAuthenticated(false);
}
为details属性赋值
//Allowsubclassestosetthe"details"property允许子类去设置这个属性
setDetails(request,authRequest);
protectedvoidsetDetails(HttpServletRequestrequest,
UsernamePasswordAuthenticationTokenauthRequest){
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
//AbstractAuthenticationToken是UsernamePasswordAuthenticationToken的父类
publicvoidsetDetails(Objectdetails){
this.details=details;
}
details属性存在于父类之中,主要描述两个信息,一个是remoteAddress和sessionId。
publicWebAuthenticationDetails(HttpServletRequestrequest){
this.remoteAddress=request.getRemoteAddr();
HttpSessionsession=request.getSession(false);
this.sessionId=(session!=null)?session.getId():null;
}
调用authenticate方法进行校验
this.getAuthenticationManager().authenticate(authRequest)
二、ProviderManager的校验逻辑
publicAuthenticationauthenticate(Authenticationauthentication)
throwsAuthenticationException{
ClasstoTest=authentication.getClass();
AuthenticationExceptionlastException=null;
AuthenticationExceptionparentException=null;
Authenticationresult=null;
AuthenticationparentResult=null;
booleandebug=logger.isDebugEnabled();
for(AuthenticationProviderprovider:getProviders()){
//获取Class,判断当前provider是否支持该authentication
if(!provider.supports(toTest)){
continue;
}
//如果支持,则调用provider的authenticate方法开始校验
result=provider.authenticate(authentication);
//将旧的token的details属性拷贝到新的token中。
if(result!=null){
copyDetails(authentication,result);
break;
}
}
//如果上一步的结果为null,调用provider的parent的authenticate方法继续校验。
if(result==null&&parent!=null){
result=parentResult=parent.authenticate(authentication);
}
if(result!=null){
if(eraseCredentialsAfterAuthentication
&&(resultinstanceofCredentialsContainer)){
//调用eraseCredentials方法擦除凭证信息
((CredentialsContainer)result).eraseCredentials();
}
if(parentResult==null){
//publishAuthenticationSuccess将登录成功的事件进行广播。
eventPublisher.publishAuthenticationSuccess(result);
}
returnresult;
}
}
获取Class,判断当前provider是否支持该authentication。
如果支持,则调用provider的authenticate方法开始校验,校验完成之后,返回一个新的Authentication。
将旧的token的details属性拷贝到新的token中。
如果上一步的结果为null,调用provider的parent的authenticate方法继续校验。
调用eraseCredentials方法擦除凭证信息,也就是密码,具体来说就是让credentials为空。
publishAuthenticationSuccess将登录成功的事件进行广播。
三、AuthenticationProvider的authenticate
publicAuthenticationauthenticate(Authenticationauthentication)
throwsAuthenticationException{
//从Authenticaiton中提取登录的用户名。
Stringusername=(authentication.getPrincipal()==null)?"NONE_PROVIDED"
:authentication.getName();
//返回登录对象
user=retrieveUser(username,(UsernamePasswordAuthenticationToken)authentication);
//校验user中的各个账户状态属性是否正常
preAuthenticationChecks.check(user);
//密码比对
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken)authentication);
//密码比对
postAuthenticationChecks.check(user);
ObjectprincipalToReturn=user;
//表示是否强制将Authentication中的principal属性设置为字符串
if(forcePrincipalAsString){
principalToReturn=user.getUsername();
}
//构建新的UsernamePasswordAuthenticationToken
returncreateSuccessAuthentication(principalToReturn,authentication,user);
}
从Authenticaiton中提取登录的用户名。retrieveUser方法将会调用loadUserByUsername方法,这里将会返回登录对象。preAuthenticationChecks.check(user);校验user中的各个账户状态属性是否正常,如账号是否被禁用,账户是否被锁定,账户是否过期等。additionalAuthenticationChecks用于做密码比对,密码加密解密校验就在这里进行。postAuthenticationChecks.check(user);用于密码比对。forcePrincipalAsString表示是否强制将Authentication中的principal属性设置为字符串,默认为false,也就是说默认登录之后获取的用户是对象,而不是username。构建新的UsernamePasswordAuthenticationToken。
用户信息保存
我们来到UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter中,
publicvoiddoFilter(ServletRequestreq,ServletResponseres,FilterChainchain)
throwsIOException,ServletException{
HttpServletRequestrequest=(HttpServletRequest)req;
HttpServletResponseresponse=(HttpServletResponse)res;
AuthenticationauthResult;
try{
//实际触发了上面提到的attemptAuthentication方法
authResult=attemptAuthentication(request,response);
if(authResult==null){
return;
}
sessionStrategy.onAuthentication(authResult,request,response);
}
//登录失败
catch(InternalAuthenticationServiceExceptionfailed){
unsuccessfulAuthentication(request,response,failed);
return;
}
catch(AuthenticationExceptionfailed){
unsuccessfulAuthentication(request,response,failed);
return;
}
if(continueChainBeforeSuccessfulAuthentication){
chain.doFilter(request,response);
}
//登录成功
successfulAuthentication(request,response,chain,authResult);
}
关于登录成功调用的方法:
protectedvoidsuccessfulAuthentication(HttpServletRequestrequest,
HttpServletResponseresponse,FilterChainchain,AuthenticationauthResult)
throwsIOException,ServletException{
//将登陆成功的用户信息存储在SecurityContextHolder.getContext()中
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request,response,authResult);
//Fireevent
if(this.eventPublisher!=null){
eventPublisher.publishEvent(newInteractiveAuthenticationSuccessEvent(
authResult,this.getClass()));
}
//登录成功的回调方法
successHandler.onAuthenticationSuccess(request,response,authResult);
}
我们可以通过SecurityContextHolder.getContext().setAuthentication(authResult);得到两点结论:
- 如果我们想要获取用户信息,我们只需要调用SecurityContextHolder.getContext().getAuthentication()即可。
- 如果我们想要更新用户信息,我们只需要调用SecurityContextHolder.getContext().setAuthentication(authResult);即可。
用户信息的获取
前面说到,我们可以利用Authenticaiton轻松得到用户信息,主要有下面几种方法:
通过上下文获取。
SecurityContextHolder.getContext().getAuthentication();
直接在Controller注入Authentication。
@GetMapping("/hr/info")
publicHrgetCurrentHr(Authenticationauthentication){
return((Hr)authentication.getPrincipal());
}
为什么多次请求可以获取同样的信息
前面已经谈到,SpringSecurity将登录用户信息存入SecurityContextHolder中,本质上,其实是存在ThreadLocal中,为什么这么说呢?
原因在于,SpringSecurity采用了策略模式,在SecurityContextHolder中定义了三种不同的策略,而如果我们不配置,默认就是MODE_THREADLOCAL模式。
publicstaticfinalStringMODE_THREADLOCAL="MODE_THREADLOCAL";
publicstaticfinalStringMODE_INHERITABLETHREADLOCAL="MODE_INHERITABLETHREADLOCAL";
publicstaticfinalStringMODE_GLOBAL="MODE_GLOBAL";
publicstaticfinalStringSYSTEM_PROPERTY="spring.security.strategy";
privatestaticStringstrategyName=System.getProperty(SYSTEM_PROPERTY);
privatestaticvoidinitialize(){
if(!StringUtils.hasText(strategyName)){
//Setdefault
strategyName=MODE_THREADLOCAL;
}
if(strategyName.equals(MODE_THREADLOCAL)){
strategy=newThreadLocalSecurityContextHolderStrategy();
}
}
privatestaticfinalThreadLocalcontextHolder=newThreadLocal<>();
了解这个之后,又有一个问题抛出:ThreadLocal能够保证同一线程的数据是一份,那进进出出之后,线程更改,又如何保证登录的信息是正确的呢。
这里就要说到一个比较重要的过滤器:SecurityContextPersistenceFilter,它的优先级很高,仅次于WebAsyncManagerIntegrationFilter。也就是说,在进入后面的过滤器之前,将会先来到这个类的doFilter方法。
publicclassSecurityContextPersistenceFilterextendsGenericFilterBean{
publicvoiddoFilter(ServletRequestreq,ServletResponseres,FilterChainchain)
throwsIOException,ServletException{
HttpServletRequestrequest=(HttpServletRequest)req;
HttpServletResponseresponse=(HttpServletResponse)res;
if(request.getAttribute(FILTER_APPLIED)!=null){
//确保这个过滤器只应对一个请求
chain.doFilter(request,response);
return;
}
//分岔路口之后,表示应对多个请求
HttpRequestResponseHolderholder=newHttpRequestResponseHolder(request,
response);
//用户信息在session中保存的value。
SecurityContextcontextBeforeChainExecution=repo.loadContext(holder);
try{
//将当前用户信息存入上下文
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(),holder.getResponse());
}
finally{
//收尾工作,获取SecurityContext
SecurityContextcontextAfterChainExecution=SecurityContextHolder
.getContext();
//清空SecurityContext
SecurityContextHolder.clearContext();
//重新存进session中
repo.saveContext(contextAfterChainExecution,holder.getRequest(),
holder.getResponse());
}
}
}
- SecurityContextPersistenceFilter继承自GenericFilterBean,而GenericFilterBean则是Filter的实现,所以SecurityContextPersistenceFilter作为一个过滤器,它里边最重要的方法就是doFilter了。
- 在doFilter方法中,它首先会从repo中读取一个SecurityContext出来,这里的repo实际上就是HttpSessionSecurityContextRepository,读取SecurityContext的操作会进入到readSecurityContextFromSession(httpSession)方法中。
- 在这里我们看到了读取的核心方法ObjectcontextFromSession=httpSession.getAttribute(springSecurityContextKey);,这里的springSecurityContextKey对象的值就是SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为一个SecurityContext对象。
- SecurityContext是一个接口,它有一个唯一的实现类SecurityContextImpl,这个实现类其实就是用户信息在session中保存的value。
- 在拿到SecurityContext之后,通过SecurityContextHolder.setContext方法将这个SecurityContext设置到ThreadLocal中去,这样,在当前请求中,SpringSecurity的后续操作,我们都可以直接从SecurityContextHolder中获取到用户信息了。
- 接下来,通过chain.doFilter让请求继续向下走(这个时候就会进入到UsernamePasswordAuthenticationFilter过滤器中了)。
- 在过滤器链走完之后,数据响应给前端之后,finally中还有一步收尾操作,这一步很关键。这里从SecurityContextHolder中获取到SecurityContext,获取到之后,会把SecurityContextHolder清空,然后调用repo.saveContext方法将获取到的SecurityContext存入session中。
总结:
每个请求到达服务端的时候,首先从session中找出SecurityContext,为了本次请求之后都能够使用,设置到SecurityContextHolder中。
当请求离开的时候,SecurityContextHolder会被清空,且SecurityContext会被放回session中,方便下一个请求来获取。
资源放行的两种方式
用户登录的流程只有走过滤器链,才能够将信息存入session中,因此我们配置登录请求的时候需要使用configure(HttpSecurityhttp),因为这个配置会走过滤器链。
http.authorizeRequests()
.antMatchers("/hello").permitAll()
.anyRequest().authenticated()
而configure(WebSecurityweb)不会走过滤器链,适用于静态资源的放行。
@Override
publicvoidconfigure(WebSecurityweb)throwsException{
web.ignoring().antMatchers("/index.html","/img/**","/fonts/**","/favicon.ico");
}
到此这篇关于SpringSecurity中的Authentication信息与登录流程的文章就介绍到这了,更多相关SpringSecurity登录流程内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。