Spring security 学习笔记(一)

409 阅读6分钟

1.什么是Srping security 的认证和授权?

以下内容引自官网

汉化下来的意思就是

  1. 提示用户使用用户名和密码登录。
  2. 系统(成功)验证用户名的密码正确。
  3. 获取该用户的上下文信息(他们的角色列表等)。
  4. 为用户建立了安全上下文
  5. 用户可能会继续执行某些操作,该操作可能会受到访问控制机制的保护,该访问控制机制会根据当前安全上下文信息检查该操作所需的权限。

1.1 Spring security 的认证

上述前三点为Spring securiyt 的认证环节

具体的实现方式在官网中是这样描述的

  1. 获取用户名和密码,并将其组合到的一个实例UsernamePasswordAuthenticationToken中。
  2. 令牌会传递到的实例AuthenticationManager进行验证。
  3. 成功进行身份验证后, 将AuthenticationManager返回完全填充的Authentication实例。 通过调用SecurityContextHolder.getContext().setAuthentication(…​)并传入返回的身份验证对象来建立安全上下文。

1.2 Spring security 的授权

  1. Spring security 是通过 FilterSecurityInterceptor过滤器 负责http资源安全性的认证处理
  2. FilterSecurityInterceptor通过其继承的抽象类的AbstractSecurityInterceptor.beforeInvocation(Object object)方法进行访问授权,其中涉及了类AuthenticationManager、AccessDecisionManager、SecurityMetadataSource等。

2.Spring security 的核心组件

2.1 SecurityContextHolder

SecurityContextHolder提供对SecurityContext的访问,存储security context(用户信息、角色权限等)

通过官网介绍,并结合上图源码截图SecurityContextHolder具有下列储存策略即工作模式:

1.SecurityContextHolder.MODE_THREADLOCAL(默认):使用ThreadLocal,信息可供此线程下的所有的方法使用,一种与线程绑定的策略,此天然很适合Servlet Web应用,因为当请求处理完成之后,线程会被销毁

2.SecurityContextHolder.MODE_GLOBAL:使用于独立应用

3.SecurityContextHolder.MODE_INHERITABLETHREADLOCAL:具有相同安全标示的线程

修改SecurityContextHolder的工作模式有两种方法 :

1.设置一个系统属性 System.setProperty("spring.security.strategy", "XX");
2. 调用SecurityContextHolder静态方法setStrategyName() (下图源码)

2.2 Authentication

Spring Security使用一个Authentication对象来描述当前用户的相关信息,其包含用户拥有的权限信息列表、用户细节信息(身份信息、认证信息)。Authentication为认证主体在spring security中时最高级别身份/认证的抽象,常见的实现类UsernamePasswordAuthenticationToken

public interface Authentication extends Principal, Serializable { 
    //权限信息列表,默认GrantedAuthority接口的一些实现类
    Collection<? extends GrantedAuthority> getAuthorities(); 
    //密码信息
    Object getCredentials();
    //细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值
    Object getDetails();
    //通常返回值为UserDetails实现类
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

2.3 UserDetails

UserDetails提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息,包含GrantedAuthority。其官方实现类为User,开发者可以实现其接口自定义UserDetails实现类。其接口源码:

public interface UserDetails extends Serializable {

     Collection<? extends GrantedAuthority> getAuthorities();

     String getPassword();

     String getUsername();

     boolean isAccountNonExpired();

     boolean isAccountNonLocked();

     boolean isCredentialsNonExpired();

     boolean isEnabled();
}

UserDetails与Authentication接口功能类似,其实含义即是Authentication为用户提交的认证凭证(账号密码),UserDetails为系统中用户正确认证凭证   其中在getAuthorities()方法中获取到GrantedAuthority列表是代表用户访问应用程序权限范围,此类权限通常是“role(角色)”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。GrantedAuthority接口常见的实现类SimpleGrantedAuthority。

2.4 UserDetailsS​​ervice

在UserDetailsService中的loadUserByUsername方法获取正确的认证凭证。

这个接口用户自己实现过后,返回一个UserDetails实例,将用于构建Authentication存储在中的对象SecurityContextHolder

*注意: UserDetailsService,它纯粹是用于用户数据的DAO,除了将数据提供给框架内的其他组件外,不执行其他功能。特别是,它不会对用户进行身份验证,是由AuthenticationManager进行用户身份验证。在许多情况下,如果需要自定义身份验证过程,则直接实现AuthenticationProvider更有意义。

3.Spring security 的核心服务类

3.1 AuthenticationManager,ProviderManager和AuthenticationProvider

AuthenticationManager是认证相关的核心接口,是认证一切的起点。

在Spring Security中的默认实现为ProviderManager,而ProviderManager不是处理身份验证请求本身,它委托给已配置的AuthenticationProviders。

每次验证都将引发异常或返回完全填充的Authentication对象。填充Authentication对象则需要UserDetails和UserDetailsService?验证身份验证请求的最常见方法是加载相应UserDetails的密码,并对照用户输入的密码检查加载的密码。这是DaoAuthenticationProvider使用的方法。加载的UserDetails对象-尤其是GrantedAuthority包含的对象-将在构建完全填充的Authentication对象时使用,该对象将从成功的身份验证返回并存储在中SecurityContext。

public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	Class<? extends Authentication> toTest = authentication.getClass();
	AuthenticationException lastException = null;
	Authentication result = null;
	//AuthenticationProvider列表依次认证
	for (AuthenticationProvider provider : getProviders()) {
		if (!provider.supports(toTest)) {
			continue;
		}
		try {
		    //每个AuthenticationProvider进行认证
			result = provider.authenticate(authentication)
			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
		}
		....
		catch (AuthenticationException e) {
			lastException = e;
		}
	}
    //进行父类AuthenticationProvider进行认证
	if (result == null && parent != null) {
		// Allow the parent to try.
		try {
			result = parent.authenticate(authentication);
		}
		catch (AuthenticationException e) {
			lastException = e;
		}
	}
	   // 如果有Authentication信息,则直接返回
	if (result != null) {
		if (eraseCredentialsAfterAuthentication
				&& (result instanceof CredentialsContainer)) {
				//清除密码
			((CredentialsContainer) result).eraseCredentials();
		}
		//发布登录成功事件
		eventPublisher.publishAuthenticationSuccess(result);
		return result;
	}
        //如果都没认证成功,抛出异常
	if (lastException == null) {
		lastException = new ProviderNotFoundException(messages.getMessage(
				"ProviderManager.providerNotFound",
				new Object[] { toTest.getName() },
				"No AuthenticationProvider found for {0}"));
	}
	prepareException(lastException, authentication);
	throw lastException;
    }  

ProviderManager 中的AuthenticationProvider列表,会依照次序去认证,默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功,而且AuthenticationProvider认证成功后返回一个Authentication实体,并为了安全会进行清除密码。如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。

4.Security 核心过滤器

4.1 FilterSecurityInterceptor

FilterSecurityInterceptor是Spring security授权模块入口,该类根据访问的用户的角色,权限授权访问那些资源(访问特定路径应该具备的权限)。

FilterSecurityInterceptor封装FilterInvocation对象进行操作,所有的请求到了这一个filter,如果这个filter之前没有执行过的话,那么首先执行其父类AbstractSecurityInterceptor提供的InterceptorStatusToken token = super.beforeInvocation(fi),在此方法中使用AuthenticationManager获取Authentication中用户详情,使用ConfigAttribute封装已定义好访问权限详情,并使用AccessDecisionManager.decide()方法进行访问权限控制。 FilterSecurityInterceptor源码分析:

public void invoke(FilterInvocation fi) throws IOException, ServletException {
	if ((fi.getRequest() != null)
			&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
			&& observeOncePerRequest) {
		fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
	}
	else {
		// first time this request being called, so perform security checking
		if (fi.getRequest() != null && observeOncePerRequest) {
			fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
        //回调其继承的抽象类AbstractSecurityInterceptor的方法
		InterceptorStatusToken token = super.beforeInvocation(fi);

		try {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}

		super.afterInvocation(token, null);
	}
}

AbstractSecurityInterceptor源码分析:

protected InterceptorStatusToken beforeInvocation(Object object) {
	....
	//获取所有访问权限(url-role)属性列表(已定义在数据库或者其他地方)
	Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
			.getAttributes(object);
	....
	//获取该用户访问信息(包括url,访问权限)
	Authentication authenticated = authenticateIfRequired();

	// Attempt authorization
	try {
	    //进行授权访问
		this.accessDecisionManager.decide(authenticated, object, attributes);
	}catch
	....
}

4.2 UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter使用username和password表单登录使用的过滤器,也是最为常用的过滤器。其源码:

其主要代码为创建UsernamePasswordAuthenticationToken的Authentication实体以及调用AuthenticationManager进行authenticate认证,根据认证结果执行successfulAuthentication或者unsuccessfulAuthentication,无论成功失败,一般的实现都是转发或者重定向等处理

总结

以上文章内容基本都为Spring securiyt 官网汉化后搬运,参考了一些博客写下来的,在写这篇文章的过程中发现其实好多人写的文章都很有误导性而且大多都是直接上图教学怎么写代码,我在搭建项目使用的过程中踩了好多坑都找不到原因,最后才发现,最好的教程其实就是去官网查资料,并且根据官网内容去翻阅源码对比学习,后续会继续更新spring boot + spring security + jwt 项目的搭建