加入收藏 | 设为首页 | 会员中心 | 我要投稿 北几岛 (https://www.beijidao.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

SpringSecurity中的Authentication信息与登录流程

发布时间:2021-05-21 07:42:59 所属栏目:大数据 来源: https://www.jb51.cc
导读:目录 Authentication 登录流程 一、与认证相关的UsernamePasswordAuthenticationFilter 获取用户名和密码 构造UsernamePasswordAuthenticationToken对象 为details属性赋值 调用authenticate方法进行校验 二、ProviderManager的校验逻辑 三、AuthenticationP

目录
  • Authentication
  • 登录流程
    • 一、与认证相关的UsernamePasswordAuthenticationFilter
      • 获取用户名和密码
      • 构造UsernamePasswordAuthenticationToken对象
      • 为details属性赋值
      • 调用authenticate方法进行校验
    • 二、ProviderManager的校验逻辑
    • 三、AuthenticationProvider的authenticate
  • 用户信息保存
  • 用户信息的获取
    • 为什么多次请求可以获取同样的信息
    • 资源放行的两种方式

本篇文章参考于【江南一点雨】的公众号。

Authentication

使用SpringSecurity可以在任何地方注入Authentication进而获取到当前登录的用户信息,可谓十分强大。

在Authenticaiton的继承体系中,实现类UsernamePasswordAuthenticationToken 算是比较常见的一个了,在这个类中存在两个属性:principal和credentials,其实分别代表着用户和密码。【当然其他的属性存在于其父类中,如authorities@H_502_55@和details@H_502_55@。】

我们需要对这个对象有一个基本地认识,它保存了用户的基本信息。用户在登录的时候,进行了一系列的操作,将信息存与这个对象中,后续我们使用的时候,就可以轻松地获取这些信息了。

那么,用户信息如何存,又是如何取的呢?继续往下看吧。

登录流程

一、与认证相关的UsernamePasswordAuthenticationFilter

通过Servlet中的Filter技术进行实现,通过一系列内置的或自定义的安全Filter,实现接口的认证与授权。

比如:UsernamePasswordAuthenticationFilter@H_502_55@

	public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		//获取用户名和密码
		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}
		username = username.trim();
		//构造UsernamePasswordAuthenticationToken对象
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username,password);

		// 为details属性赋值
		setDetails(request,authRequest);
		// 调用authenticate方法进行校验
		return this.getAuthenticationManager().authenticate(authRequest);
	}
@H_502_55@

获取用户名和密码

从request中提取参数,这也是SpringSecurity默认的表单登录需要通过key/value形式传递参数的原因。

	@Nullable
	protected String obtainPassword(HttpServletRequest request) {
		return request.getParameter(passwordParameter);
	}
	@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(usernameParameter);
	}
@H_502_55@

构造UsernamePasswordAuthenticationToken对象

传入获取到的用户名和密码,而用户名对应UPAT对象中的principal属性,而密码对应credentials属性。

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
    username,password);

//UsernamePasswordAuthenticationToken 的构造器
public UsernamePasswordAuthenticationToken(Object principal,Object credentials) {
    super(null);
    this.principal = principal;
    this.credentials = credentials;
    setAuthenticated(false);
}
@H_502_55@

为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;
}
@H_502_55@

details属性存在于父类之中,主要描述两个信息,一个是remoteAddress 和sessionId。

	public WebAuthenticationDetails(HttpServletRequest request) {
		this.remoteAddress = request.getRemoteAddr();

		HttpSession session = request.getSession(false);
		this.sessionId = (session != null) ? session.getId() : null;
	}
@H_502_55@

调用authenticate方法进行校验

this.getAuthenticationManager().authenticate(authRequest)
@H_502_55@

二、ProviderManager的校验逻辑

public Authentication authenticate(Authentication authentication)
    throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    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;
    }
}
@H_502_55@
  1. 获取Class,判断当前provider是否支持该authentication。

  2. 如果支持,则调用provider的authenticate方法开始校验,校验完成之后,返回一个新的Authentication。

  3. 将旧的token的details属性拷贝到新的token中。

  4. 如果上一步的结果为null,调用provider的parent的authenticate方法继续校验。

  5. 调用eraseCredentials方法擦除凭证信息,也就是密码,具体来说就是让credentials为空。

  6. publishAuthenticationSuccess将登录成功的事件进行广播。

三、AuthenticationProvider的authenticate

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
    //从Authenticaiton中提取登录的用户名。
	String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
			: authentication.getName();
    //返回登录对象
	user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
    //校验user中的各个账户状态属性是否正常
	preAuthenticationChecks.check(user);
    //密码比对
	additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
    //密码比对
	postAuthenticationChecks.check(user);
	Object principalToReturn = user;
    //表示是否强制将Authentication中的principal属性设置为字符串
	if (forcePrincipalAsString) {
		principalToReturn = user.getUsername();
	}
    //构建新的UsernamePasswordAuthenticationToken
	return createSuccessAuthentication(principalToReturn,authentication,user);
}
@H_502_55@
  1. 从Authenticaiton中提取登录的用户名。
  2. retrieveUser@H_502_55@方法将会调用loadUserByUsername@H_502_55@方法,这里将会返回登录对象。
  3. preAuthenticationChecks.check(user);@H_502_55@校验user中的各个账户状态属性是否正常,如账号是否被禁用,账户是否被锁定,账户是否过期等。
  4. additionalAuthenticationChecks@H_502_55@用于做密码比对,密码加密解密校验就在这里进行。
  5. postAuthenticationChecks.check(user);@H_502_55@用于密码比对。
  6. forcePrincipalAsString@H_502_55@表示是否强制将Authentication中的principal属性设置为字符串,默认为false,也就是说默认登录之后获取的用户是对象,而不是username。
  7. 构建新的UsernamePasswordAuthenticationToken@H_502_55@。

用户信息保存

我们来到UsernamePasswordAuthenticationFilter 的父类AbstractAuthenticationProcessingFilter 中,

public void doFilter(ServletRequest req,ServletResponse res,FilterChain chain)
		throws IOException,ServletException {
	HttpServletRequest request = (HttpServletRequest) req;
	HttpServletResponse response = (HttpServletResponse) res;
	Authentication authResult;
	try {
        //实际触发了上面提到的attemptAuthentication方法
		authResult = attemptAuthentication(request,response);
		if (authResult == null) {
			return;
		}
		sessionStrategy.onAuthentication(authResult,request,response);
	}
    //登录失败
	catch (InternalAuthenticationServiceException Failed) {
		unsuccessfulAuthentication(request,response,Failed);
		return;
	}
	catch (AuthenticationException Failed) {
		unsuccessfulAuthentication(request,Failed);
		return;
	}
	if (continueChainBeforeSuccessfulAuthentication) {
		chain.doFilter(request,response);
	}
    //登录成功
	successfulAuthentication(request,chain,authResult);
}
@H_502_55@

关于登录成功调用的方法:

protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult)
		throws IOException,ServletException {
    //将登陆成功的用户信息存储在SecurityContextHolder.getContext()中
	SecurityContextHolder.getContext().setAuthentication(authResult);
	rememberMeServices.loginSuccess(request,authResult);
	// Fire event
	if (this.eventPublisher != null) {
		eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
				authResult,this.getClass()));
	}
    //登录成功的回调方法
	successHandler.onAuthenticationSuccess(request,authResult);
}
@H_502_55@

我们可以通过SecurityContextHolder.getContext().setAuthentication(authResult);@H_502_55@得到两点结论:

  • 如果我们想要获取用户信息,我们只需要调用SecurityContextHolder.getContext().getAuthentication()@H_502_55@即可。
  • 如果我们想要更新用户信息,我们只需要调用SecurityContextHolder.getContext().setAuthentication(authResult);@H_502_55@即可。

用户信息的获取

前面说到,我们可以利用Authenticaiton轻松得到用户信息,主要有下面几种方法:

  • 通过上下文获取。
SecurityContextHolder.getContext().getAuthentication();
@H_502_55@
  • 直接在Controller注入Authentication。
@GetMapping("/hr/info")
public Hr getCurrentHr(Authentication authentication) {
    return ((Hr) authentication.getPrincipal());
}
@H_502_55@

为什么多次请求可以获取同样的信息

前面已经谈到,SpringSecurity将登录用户信息存入SecurityContextHolder 中,本质上,其实是存在ThreadLocal中,为什么这么说呢?

原因在于,SpringSecurity采用了策略模式,在SecurityContextHolder 中定义了三种不同的策略,而如果我们不配置,默认就是MODE_THREADLOCAL@H_502_55@模式。

	
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(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<>();
@H_502_55@

了解这个之后,又有一个问题抛出:ThreadLocal能够保证同一线程的数据是一份,那进进出出之后,线程更改,又如何保证登录的信息是正确的呢。

这里就要说到一个比较重要的过滤器:SecurityContextPersistenceFilter@H_502_55@,它的优先级很高,仅次于WebAsyncManagerIntegrationFilter@H_502_55@。也就是说,在进入后面的过滤器之前,将会先来到这个类的doFilter方法。

public class SecurityContextPersistenceFilter extends GenericFilterBean {
	public void doFilter(ServletRequest req,FilterChain chain)
			throws IOException,ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
        if (request.getAttribute(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.doFilter(holder.getRequest(),holder.getResponse());
		}
		finally {
            //收尾工作,获取SecurityContext
			SecurityContext contextAfterChainExecution = SecurityContextHolder
					.getContext();
            //清空SecurityContext
			SecurityContextHolder.clearContext();
            //重新存进session中
			repo.saveContext(contextAfterChainExecution,holder.getRequest(),holder.getResponse());
		}
	}
}
@H_502_55@
  1. SecurityContextPersistenceFilter@H_502_55@ 继承自 GenericFilterBean@H_502_55@,而 GenericFilterBean @H_502_55@则是 Filter 的实现,所以 SecurityContextPersistenceFilter @H_502_55@作为一个过滤器,它里边最重要的方法就是 doFilter @H_502_55@了。
  2. doFilter@H_502_55@ 方法中,它首先会从 repo 中读取一个 SecurityContext@H_502_55@ 出来,这里的 repo 实际上就是 HttpSessionSecurityContextRepository@H_502_55@,读取 SecurityContext@H_502_55@ 的操作会进入到 readSecurityContextFromSession(httpSession)@H_502_55@ 方法中。
  3. 在这里我们看到了读取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);@H_502_55@,这里的 springSecurityContextKey@H_502_55@ 对象的值就是 SPRING_SECURITY_CONTEXT@H_502_55@,读取出来的对象最终会被转为一个 SecurityContext @H_502_55@对象。
  4. SecurityContext@H_502_55@ 是一个接口,它有一个唯一的实现类 SecurityContextImpl@H_502_55@,这个实现类其实就是用户信息在 session 中保存的 value。
  5. 在拿到 SecurityContext@H_502_55@ 之后,通过 SecurityContextHolder.setContext @H_502_55@方法将这个 SecurityContext@H_502_55@ 设置到 ThreadLocal@H_502_55@ 中去,这样,在当前请求中,Spring Security 的后续操作,我们都可以直接从 SecurityContextHolder@H_502_55@ 中获取到用户信息了。
  6. 接下来,通过 chain.doFilter@H_502_55@ 让请求继续向下走(这个时候就会进入到 UsernamePasswordAuthenticationFilter@H_502_55@ 过滤器中了)。
  7. 在过滤器链走完之后,数据响应给前端之后,finally 中还有一步收尾操作,这一步很关键。这里从 SecurityContextHolder@H_502_55@ 中获取到 SecurityContext@H_502_55@,获取到之后,会把 SecurityContextHolder @H_502_55@清空,然后调用 repo.saveContext@H_502_55@ 方法将获取到的 SecurityContext @H_502_55@存入 session 中。

总结

每个请求到达服务端的时候,首先从session中找出SecurityContext ,为了本次请求之后都能够使用,设置到SecurityContextHolder 中。

当请求离开的时候,SecurityContextHolder 会被清空,且SecurityContext 会被放回session中,方便下一个请求来获取。

资源放行的两种方式

用户登录的流程只有走过滤器链,才能够将信息存入session中,因此我们配置登录请求的时候需要使用configure(HttpSecurity http),因为这个配置会走过滤器链。

http.authorizeRequests()
        .antMatchers("/hello").permitAll()
        .anyRequest().authenticated()
@H_502_55@

而 configure(WebSecurity web)不会走过滤器链,适用于静态资源的放行。

@Override
public void configure(WebSecurity web) throws Exception {
 	web.ignoring().antMatchers("/index.html","/img/**","/fonts/**","/favicon.ico");
}
@H_502_55@

(编辑:北几岛)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读