详解SpringSecurity中的Authentication信息与登录流程
Authentication
使⽤SpringSecurity可以在任何地⽅注⼊Authentication进⽽获取到当前登录的⽤户信息,可谓⼗分强⼤。
在Authenticaiton的继承体系中,实现类UsernamePasswordAuthenticationToken 算是⽐较常见的⼀个了,在这个类中存在两个属性:principal和credentials,其实分别代表着⽤户和密码。【当然其他的属性存在于其⽗类中,如authorities和details。】
我们需要对这个对象有⼀个基本地认识,它保存了⽤户的基本信息。⽤户在登录的时候,进⾏了⼀系列的操作,将信息存与这个对象中,后续我们使⽤的时候,就可以轻松地获取这些信息了。
那么,⽤户信息如何存,⼜是如何取的呢?继续往下看吧。
登录流程
⼀、与认证相关的UsernamePasswordAuthenticationFilter
通过Servlet中的Filter技术进⾏实现,通过⼀系列内置的或⾃定义的安全Filter,实现接⼝的认证与授权。
⽐如:UsernamePasswordAuthenticationFilter
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !Method().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + Method());
}
//获取⽤户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = im();
//构造UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 为details属性赋值
setDetails(request, authRequest);
// 调⽤authenticate⽅法进⾏校验
AuthenticationManager().authenticate(authRequest);
}
获取⽤户名和密码
从request中提取参数,这也是SpringSecurity默认的表单登录需要通过key/value形式传递参数的原因。
@Nullable
protected String obtainPassword(HttpServletRequest request) {
Parameter(passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
Parameter(usernameParameter);
}
构造UsernamePasswordAuthenticationToken对象
传⼊获取到的⽤户名和密码,⽽⽤户名对应UPAT对象中的principal属性,⽽密码对应credentials属性。
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
//UsernamePasswordAuthenticationToken 的构造器
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
为details属性赋值
// Allow subclasses to set the "details" property 允许⼦类去设置这个属性
setDetails(request, authRequest);
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
//AbstractAuthenticationToken 是UsernamePasswordAuthenticationToken的⽗类
public void setDetails(Object details) {
this.details = details;
}
details属性存在于⽗类之中,主要描述两个信息,⼀个是remoteAddress 和sessionId。
public WebAuthenticationDetails(HttpServletRequest request) {
HttpSession session = Session(false);
this.sessionId = (session != null) ? Id() : null;
}
调⽤authenticate⽅法进⾏校验
⼆、ProviderManager的校验逻辑
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = Class();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : 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
&& (result instanceof CredentialsContainer)) {
//调⽤eraseCredentials⽅法擦除凭证信息
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
//publishAuthenticationSuccess将登录成功的事件进⾏⼴播。
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
}
获取Class,判断当前provider是否⽀持该authentication。
如果⽀持,则调⽤provider的authenticate⽅法开始校验,校验完成之后,返回⼀个新的Authentication。
将旧的token的details属性拷贝到新的token中。
如果上⼀步的结果为null,调⽤provider的parent的authenticate⽅法继续校验。
调⽤eraseCredentials⽅法擦除凭证信息,也就是密码,具体来说就是让credentials为空。
publishAuthenticationSuccess将登录成功的事件进⾏⼴播。
三、AuthenticationProvider的authenticate
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//从Authenticaiton中提取登录的⽤户名。
String username = (Principal() == null) ? "NONE_PROVIDED"
: Name();
//返回登录对象
user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
//校验user中的各个账户状态属性是否正常
preAuthenticationChecks.check(user);
//密码⽐对
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
//密码⽐对
postAuthenticationChecks.check(user);
Object principalToReturn = user;
//表⽰是否强制将Authentication中的principal属性设置为字符串
if (forcePrincipalAsString) {
principalToReturn = Username();
}
//构建新的UsernamePasswordAuthenticationToken
return createSuccessAuthentication(principalToReturn, authentication, user);
}
从Authenticaiton中提取登录的⽤户名。retrieveUser⽅法将会调⽤loadUserByUsername⽅法,这⾥将会返回登录对象。preAuthenticationChecks.check(user);校验user中的各个账户状态属性是否正常,如账号是否被禁⽤,账户是否被锁定,账户是否过期等。additionalAuthenticationChecks⽤于做密码⽐对,密码加密解密校验就在这⾥进⾏。postAuthenticationChecks.check(user);⽤于密码⽐对。forcePrincipalAsString表⽰是否强制将Authentication中的principal属性设置为字符串,默认为false,也就是说默认登录之后获取的⽤户是对象,⽽不是username。构建新的UsernamePasswordAuthenticationToken。
⽤户信息保存
我们来到UsernamePasswordAuthenticationFilter 的⽗类AbstractAuthenticationProcessingFilter 中,
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
计算机中spring是什么意思HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
Authentication authResult;
try {
//实际触发了上⾯提到的attemptAuthentication⽅法
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
}
//登录失败
catch (InternalAuthenticationServiceException failed) {
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
unsuccessfulAuthentication(request, response, failed);
return;
}
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//登录成功
successfulAuthentication(request, response, chain, authResult);
}
关于登录成功调⽤的⽅法:
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
//将登陆成功的⽤户信息存储在Context()中
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, Class()));
}
//登录成功的回调⽅法
}
我们可以通过Context().setAuthentication(authResult);得到两点结论:
如果我们想要获取⽤户信息,我们只需要调⽤Context().getAuthentication()即可。
如果我们想要更新⽤户信息,我们只需要调⽤Context().setAuthentication(authResult);即可。
⽤户信息的获取
前⾯说到,我们可以利⽤Authenticaiton轻松得到⽤户信息,主要有下⾯⼏种⽅法:
通过上下⽂获取。
直接在Controller注⼊Authentication。
@GetMapping("/hr/info")
public Hr getCurrentHr(Authentication authentication) {
return ((Hr) Principal());
}
为什么多次请求可以获取同样的信息
前⾯已经谈到,SpringSecurity将登录⽤户信息存⼊SecurityContextHolder 中,本质上,其实是存在ThreadLocal中,为什么这么说呢?
原因在于,SpringSecurity采⽤了策略模式,在SecurityContextHolder 中定义了三种不同的策略,⽽如果我们不配置,默认就是MODE_THREADLOCAL模式。
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 = Property(SYSTEM_PROPERTY);
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
/
/ Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
}
}
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
了解这个之后,⼜有⼀个问题抛出:ThreadLocal能够保证同⼀线程的数据是⼀份,那进进出出之后,线程更改,⼜如何保证登录的信息是正确的呢。
这⾥就要说到⼀个⽐较重要的过滤器:SecurityContextPersistenceFilter,它的优先级很⾼,仅次于WebAsyncManagerIntegrationFilter。也就是说,在进⼊后⾯的过滤器之前,将会先来到这个类的doFilt
er⽅法。public class SecurityContextPersistenceFilter extends GenericFilterBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (Attribute(FILTER_APPLIED) != null) {
// 确保这个过滤器只应对⼀个请求
chain.doFilter(request, response);
return;
}
//分岔路⼝之后,表⽰应对多个请求
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
//⽤户信息在 session 中保存的 value。
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
//将当前⽤户信息存⼊上下⽂
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.Request(), Response());
}
finally {
//收尾⼯作,获取SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
//清空SecurityContext
SecurityContextHolder.clearContext();
//重新存进session中
repo.saveContext(contextAfterChainExecution, Request(),
}
}
}
SecurityContextPersistenceFilter继承⾃GenericFilterBean,⽽GenericFilterBean 则是 Filter 的实现,所以SecurityContextPersistenceFilter 作为⼀个过滤器,它⾥边最重要的⽅法就是doFilter 了。
在doFilter⽅法中,它⾸先会从 repo 中读取⼀个SecurityContext出来,这⾥的 repo 实际上就是HttpSessionSecurityContextRepository,读取SecurityContext的操作会进⼊到
readSecurityContextFromSession(httpSession)⽅法中。
在这⾥我们看到了读取的核⼼⽅法Object contextFromSession = Attribute(springSecurityContextKey);,这⾥的springSecurityContextKey对象的值就是SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为⼀个SecurityContext 对象。
SecurityContext是⼀个接⼝,它有⼀个唯⼀的实现类SecurityContextImpl,这个实现类其实就是⽤户信息在 session 中保存的 value。
在拿到SecurityContext之后,通过SecurityContextHolder.setContext ⽅法将这个SecurityContext设置到 ThreadLocal中去,这样,在当前请求中,Spring Security 的后续操作,我们都可以直接从SecurityContextHolder中获取到⽤户信息了。
接下来,通过 chain.doFilter让请求继续向下⾛(这个时候就会进⼊到UsernamePasswordAuthenticationFilter过滤器中了)。
在过滤器链⾛完之后,数据响应给前端之后,finally 中还有⼀步收尾操作,这⼀步很关键。这⾥从Secur
ityContextHolder中获取到SecurityContext,获取到之后,会把SecurityContextHolder 清空,然后调⽤repo.saveContext⽅法将获取到的SecurityContext 存⼊ session 中。
总结:
每个请求到达服务端的时候,⾸先从session中出SecurityContext ,为了本次请求之后都能够使⽤,设置到SecurityContextHolder 中。
当请求离开的时候,SecurityContextHolder 会被清空,且SecurityContext 会被放回session中,⽅便下⼀个请求来获取。
资源放⾏的两种⽅式
⽤户登录的流程只有⾛过滤器链,才能够将信息存⼊session中,因此我们配置登录请求的时候需要使⽤configure(HttpSecurity http),因为这个配置会⾛过滤器链。
http.authorizeRequests()
.antMatchers("/hello").permitAll()
.anyRequest().authenticated()
⽽ configure(WebSecurity web)不会⾛过滤器链,适⽤于静态资源的放⾏。
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/index.html","/img/**","/fonts/**","/favicon.ico");
}
到此这篇关于SpringSecurity中的Authentication信息与登录流程的⽂章就介绍到这了,更多相关SpringSecurity登录流程内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论