详解SpringSecurity中的Authentication信息与登录流程

Authentication

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

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

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

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

登录流程

一、与认证相关的UsernamePasswordAuthenticationFilter

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

比如:UsernamePasswordAuthenticationFilter

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);
	}

获取用户名和密码

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

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

构造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);
}

为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) {
		this.remoteAddress = request.getRemoteAddr();

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

调用authenticate方法进行校验

this.getAuthenticationManager().authenticate(authRequest)

二、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;
 }
}

获取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 = (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);
}

从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 {
	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, 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 {
 //将登陆成功的用户信息存储在SecurityContextHolder.getContext()中
	SecurityContextHolder.getContext().setAuthentication(authResult);
	rememberMeServices.loginSuccess(request, response, authResult);
	// Fire event
	if (this.eventPublisher != null) {
		eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
				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")
public Hr getCurrentHr(Authentication authentication) {
 return ((Hr) authentication.getPrincipal());
}

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

前面已经谈到,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 = 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<>();

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

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

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 (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());
		}
	}
}
  • SecurityContextPersistenceFilter 继承自 GenericFilterBean,而 GenericFilterBean 则是 Filter 的实现,所以 SecurityContextPersistenceFilter 作为一个过滤器,它里边最重要的方法就是 doFilter 了。
  • doFilter 方法中,它首先会从 repo 中读取一个 SecurityContext 出来,这里的 repo 实际上就是 HttpSessionSecurityContextRepository,读取 SecurityContext 的操作会进入到 readSecurityContextFromSession(httpSession) 方法中。
  • 在这里我们看到了读取的核心方法 Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);,这里的 springSecurityContextKey 对象的值就是 SPRING_SECURITY_CONTEXT,读取出来的对象最终会被转为一个 SecurityContext 对象。
  • SecurityContext 是一个接口,它有一个唯一的实现类 SecurityContextImpl,这个实现类其实就是用户信息在 session 中保存的 value。
  • 在拿到 SecurityContext 之后,通过 SecurityContextHolder.setContext 方法将这个 SecurityContext 设置到 ThreadLocal 中去,这样,在当前请求中,Spring Security 的后续操作,我们都可以直接从 SecurityContextHolder 中获取到用户信息了。
  • 接下来,通过 chain.doFilter 让请求继续向下走(这个时候就会进入到 UsernamePasswordAuthenticationFilter 过滤器中了)。
  • 在过滤器链走完之后,数据响应给前端之后,finally 中还有一步收尾操作,这一步很关键。这里从 SecurityContextHolder 中获取到 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登录流程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot + Spring Security 基本使用及个性化登录配置详解

    Spring Security 基本介绍 这里就不对Spring Security进行过多的介绍了,具体的可以参考官方文档 我就只说下SpringSecurity核心功能: 认证(你是谁) 授权(你能干什么) 攻击防护(防止伪造身份) 基本环境搭建 这里我们以SpringBoot作为项目的基本框架,我这里使用的是maven的方式来进行的包管理,所以这里先给出集成Spring Security的方式 <dependencies> ... <dependency> <groupI

  • 解析SpringSecurity自定义登录验证成功与失败的结果处理问题

    一.需要自定义登录结果的场景 在我之前的文章中,做过登录验证流程的源码解析.其中比较重要的就是 当我们登录成功的时候,是由AuthenticationSuccessHandler进行登录结果处理,默认跳转到defaultSuccessUrl配置的路径对应的资源页面(一般是首页index.html). 当我们登录失败的时候,是由AuthenticationfailureHandler进行登录结果处理,默认跳转到failureUrl配置的路径对应的资源页面(一般是登录页login.html). 但是

  • SpringBoot + SpringSecurity 短信验证码登录功能实现

    实现原理 在之前的文章中,我们介绍了普通的帐号密码登录的方式: SpringBoot + Spring Security 基本使用及个性化登录配置. 但是现在还有一种常见的方式,就是直接通过手机短信验证码登录,这里就需要自己来做一些额外的工作了. 对SpringSecurity认证流程详解有一定了解的都知道,在帐号密码认证的过程中,涉及到了以下几个类:UsernamePasswordAuthenticationFilter(用于请求参数获取),UsernamePasswordAuthentica

  • 详解spring security 配置多个AuthenticationProvider

    前言 发现很少关于spring security的文章,基本都是入门级的,配个UserServiceDetails或者配个路由控制就完事了,而且很多还是xml配置,国内通病...so,本文里的配置都是java配置,不涉及xml配置,事实上我也不会xml配置 spring security的大体介绍 spring security本身如果只是说配置,还是很简单易懂的(我也不知道网上说spring security难,难在哪里),简单不需要特别的功能,一个WebSecurityConfigurerA

  • Spring Security OAuth2集成短信验证码登录以及第三方登录

    前言 基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spring Boot环境下使用OAuth2.0,提供了开箱即用的组件.但是在开发过程中我们会发现由于Spring Security OAuth2的组件特别全面,这样就导致了扩展很不方便或者说是不太容易直指定扩展的方案,例如: 图片验证码登录 短信验证码登录 微信小程序登录 第三方系统登录 CAS单点登录

  • 详解SpringSecurity中的Authentication信息与登录流程

    Authentication 使用SpringSecurity可以在任何地方注入Authentication进而获取到当前登录的用户信息,可谓十分强大. 在Authenticaiton的继承体系中,实现类UsernamePasswordAuthenticationToken 算是比较常见的一个了,在这个类中存在两个属性:principal和credentials,其实分别代表着用户和密码.[当然其他的属性存在于其父类中,如authorities和details.] 我们需要对这个对象有一个基本地

  • 详解RocketMQ中的消费者启动与消费流程分析

    目录 一.简介 1.1 RocketMQ 简介 1.2 工作流程 二.消费者启动流程 2.1 实例化消费者 2.2 设置NameServer和订阅topic过程 2.2.1 添加tag 2.2.2 发送心跳至Broker 2.2.3上传过滤器类至FilterServer 2.3 注册回调实现类 2.4 消费者启动 三.pull/push 模式消费 3.1 pull模式-DefaultMQPullConsumer 3.2 push模式-DefaultMQPushConsumer 3.3 小结 四.

  • 详解SpringSecurity如何实现前后端分离

    目录 Spring Security存在的问题 改造Spring Security的认证方式 1. 登录请求改成JSON方式 1.1 新建JSON版Filter - JsonUsernamePasswordAuthenticationFilter 1.2 新建Configurer来注册Filter - JsonUsernamePasswordLoginConfigurer 1.3 将自定义Configurer注册到HttpSecurity上 2. 关闭页面重定向 2.1 当前用户未登录 2.2

  • 详解springSecurity之java配置篇

    一 前言 本篇是springSecurity知识的入门第二篇,主要内容是如何使用java配置的方式进行配置springSeciruty,然后通过一个简单的示例自定义登陆页面,覆盖原有springSecurity默认的登陆页面:学习这篇的基础是 知识追寻者之前发布 过 的<springSecurity入门篇> 二 java配置 2.1配置账号密码 如下所示, 使用 @EnableWebSecurity 在配置类上开启security配置功能: 在配置类中定义bean 名为 UserDetails

  • 详解AngularJS中的表单验证(推荐)

    AngularJS自带了很多验证,什么必填,最大长度,最小长度...,这里记录几个有用的正则式验证 1.使用angularjs的表单验证 正则式验证 只需要配置一个正则式,很方便的完成验证,理论上所有的验证都可以用正则式完成 //javascript $scope.mobileRegx = "^1(3[0-9]|4[57]|5[0-35-9]|7[01678]|8[0-9])\\d{8}$"; $scope.emailRegx = "^[a-z]([a-z0-9]*[-_]?

  • 详解Struts2中对未登录jsp页面实现拦截功能

    Struts2中拦截器大家都很经常使用,但是拦截器只能拦截action不能拦截jsp页面.这个时候就有点尴尬了,按道理来说没登录的用户只能看login界面不能够通过输入URL进行界面跳转,这显然是不合理的.这里介绍Struts2中Filter实现jsp页面拦截的功能.(有兴趣的人可以去研究Filter过滤器的其它用法,因为利用过滤器也可以实现action拦截的功能) 下面直接上代码,边看边分析实现步骤和原理. 1.web.xml中的配置信息: <filter> <filter-name&

  • 详解JSP 中Spring工作原理及其作用

    详解JSP 中Spring工作原理及其作用 1.springmvc请所有的请求都提交给DispatcherServlet,它会委托应用系统的其他模块负责负责对请求进行真正的处理工作. 2.DispatcherServlet查询一个或多个HandlerMapping,找到处理请求的Controller. 3.DispatcherServlet请请求提交到目标Controller 4.Controller进行业务逻辑处理后,会返回一个ModelAndView 5.Dispathcher查询一个或多个

  • 详解oracle中通过触发器记录每个语句影响总行数

    详解oracle中通过触发器记录每个语句影响总行数 需求产生: 业务系统中,有一步"抽数"流程,就是把一些数据从其它服务器同步到本库的目标表.这个过程有可能 多人同时抽数,互相影响.有测试人员反应,原来抽过的数,偶尔就无缘无故的找不到了,有时又会出来重复行.这个问题产生肯定是抽数逻辑问题以及并行的问题了!但他们提了一个简单的需求:想知道什么时候数据被删除了,什么时候插入了,我需要监控"表的每一次变更"! 技术选择: 第一就想到触发器,这样能在不涉及业务系统的代码情况

  • 详解Java中Collections.sort排序

    Comparator是个接口,可重写compare()及equals()这两个方法,用于比价功能:如果是null的话,就是使用元素的默认顺序,如a,b,c,d,e,f,g,就是a,b,c,d,e,f,g这样,当然数字也是这样的. compare(a,b)方法:根据第一个参数小于.等于或大于第二个参数分别返回负整数.零或正整数. equals(obj)方法:仅当指定的对象也是一个 Comparator,并且强行实施与此 Comparator 相同的排序时才返回 true. Collections.

  • 详解Android中Intent对象与Intent Filter过滤匹配过程

    如果对Intent不是特别了解,可以参见博文<详解Android中Intent的使用方法>,该文对本文要使用的action.category以及data都进行了详细介绍.如果想了解在开发中常见Intent的使用,可以参见<Android中Intent习惯用法>. 本文内容有点长,希望大家可以耐心读完. 本文在描述组件在manifest中注册的Intent Filter过滤器时,统一用intent-filter表示. 一.概述 我们知道,Intent是分两种的:显式Intent和隐式

随机推荐