【源码】Spring Security 官方文档阅读并查源码解读认证流程

25 篇文章 0 订阅
23 篇文章 2 订阅

前言

官网:

Spring Security Reference Version 5.2.15.RELEASE

建议

这篇文章只适合看不懂官网,或者同步在看官网的伙伴。

1. 获取认证过的用户主体

1.1 借 SecurityContextHolder 从线程中获得与程序交互的主体信息 Authentication

该类内部组合了一个SecurityContextHolderStrategy 也是就存储上下文信息的策略,默认是是用ThreadLocalSecurityContextHolderStrategy

  • 默认策略使用ThreadLocal存储系统用户信息
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();
}    

1.2 Authentication 封装了当前系统用户信息

  • 获取当前经过身份验证的用户的名称
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) { // principal 是主体的意思
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
  • Spring Security 中的大多数身份验证机制都返回一个实例UserDetails作为主体。(并不意味者只能返回这个UserDetails.class 的主体)
    重要的是getContext().getAuthentication()返回的是一个Object,为返回自己系统用户提供了便利

1.3 什么是UserDetail?

Think of UserDetails as the adapter between your own user database and what Spring Security needs inside the SecurityContextHolder.

官网说道,可以将UserDetail 视为数据库中的用户和Spring Security 定义的用户的适配器。

  • 言下之意是,你需要提供给框架的用户信息,用UserDetail封装起来。

1.4 利用实现 UserDetailsService 声明自己的UserDetails

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • 值得一提,UserDetailsService 并不提供认证功能
    Spring Security 提供了许多UserDetailsService 的实现如:
    InMemoryDaoImplJdbcDaoImpl 然而,大多数用户倾向于编写自己的实现。

1.5 如何从线程中返回数据库中的系统用户?

Remember the advantage that whatever your UserDetailsService returns can always be obtained from the SecurityContextHolder using the above code fragment.

官网说道,无论你从接口返回什么,都能在SecurityContextHolder中获得,也就是在线程中获得。但是返回值已经被限定成UserDetails 了?所以,需要定义的返回值应该是上文提到的SecurityContextHolder.getContext().getAuthentication() 也就是接口Authentication 的实现类。后文将提到如何定义

1.6 Authentication 封装了当前系统用户的权限信息

Authentication#getAuthorities()

public interface Authentication extends Principal, Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
}

This method provides an array of GrantedAuthority objects. A GrantedAuthority is, not surprisingly, an authority that is granted to the principal. Such authorities are usually “roles”, such as ROLE_ADMINISTRATOR or ROLE_HR_SUPERVISOR.

官网说道,getAuthorities() 返回一组用户权限信息,一般是以角色进行区分,如系统管理员角色、HR角色。

  • 关于角色和权限点的设计可以考RBAC模型

1.7 上文小结

结合上下文解读官网的原文:
在这里插入图片描述

  • SecurityContextHolder 提供了访问 SecurityContext 访问的入口。默认的是组合了一个策略,默认是使用ThreadLocal策略
    在这里插入图片描述
    在这里插入图片描述

  • SecurityContext 存放用户认证信息的上下文,常常用于存放请求的安全信息,这个安全信息的主体是Authentication
    在这里插入图片描述
    在这里插入图片描述

  • Authentication 是以Spring Security 的模式向开发者呈现系统用户主体。具体的方法名为getPrincipal()。默认情况下,该方法返回的是UserDetails 的对象。由于返回值是Object ,我们也可以改写实现。

public interface Authentication extends Principal, Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	Object getCredentials();
	Object getDetails();
	Object getPrincipal(); // 很重要!!
	boolean isAuthenticated(); 
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
  • GrantedAuthority 用户主体在应用范围中被授予的权限点

  • UserDetails Spring Security 声明了这个对象,是为了开发者向框架提供必要的信息,以适配框架的API。

  • UserDetailsService 设置UserDetails必要信息的业务逻辑。

2 认证流程

2.1 较为熟悉的认证场景

在这里插入图片描述

  1. 支持用户账号密码登录
  2. 系统可验证账号密码是否正确
  3. 可以获得该用户的上下文信息(例如角色列表)
  4. 为用户建立安全的上下文
  5. 用户继续执行一些操作,这些操作可能受到访问控制机制的保护,该机制根据当前的用户的上下文信息(如用角色控制权限)。

前4点是认证过程

2.2 Spring Security 如何实现上述的认证过程

  1. 用户名和密码被组装成UsernamePasswordAuthenticationToken,该类是一个实现了Authentication 接口的类。
  2. 上述的token被传递到AuthenticationManager 的实例中被验证(该实现类会实现具体的验证逻辑)
  3. 验证通过后,会颁发被认证的Authentication实现类,具体的:接口实现boolean isAuthenticated();true
  4. SecurityContextHolder.getContext().setAuthentication(…​) 设置Authentication实现类 也就是上文说到的从线程中可以拿到的对象

十分重要的是,官方的伪代码给出了提醒: 所谓的认证过程就是Authentication实现类 的属性被完全赋值的过程(赋值完全就代表是已认证)

// 用于鉴权, 封装了鉴权逻辑
private static AuthenticationManager am = new SampleAuthenticationManager();
class SampleAuthenticationManager implements AuthenticationManager {
    static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
    static {
        // 默认是有用户的角色
        AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
    }

    public Authentication authenticate(Authentication auth) throws AuthenticationException {
        // 只是简单对比用户是否相同,getName()在抽象类里面,实现是用credentials 所以一定返回true
        if (auth.getName().equals(auth.getCredentials())) {
            return new UsernamePasswordAuthenticationToken(auth.getName(),
                    auth.getCredentials(), AUTHORITIES);
        }
        throw new BadCredentialsException("Bad Credentials");
    }
}
// 使用官方的用户名密码实现创建token
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
// request和result 都是Authentication的实现类
Authentication result = am.authenticate(request);
// 将认证好的用户设置线程上下文    SecurityContextHolder.getContext().setAuthentication(result);

2.3 使用自定义的认证提醒需要在AbstractSecurityInterceptor之前执行

In fact, Spring Security doesn’t mind how you put the Authentication object inside the SecurityContextHolder. The only critical requirement is that the SecurityContextHolder contains an Authentication which represents a principal before the AbstractSecurityInterceptor (which we’ll see more about later) needs to authorize a user operation.

言外之意就是,如果要实现自己的Filter需要在责任链到达AbstractSecurityInterceptor的实现类之前,把Authentication 放进 SecurityContextHolder 中。

3. 在 web应用中使用 Spring Security

官网将用户访问网站并获取认证划分为了七个步骤帮助理解,
并声明了两个过滤器,一个认证机制可以参与这个过程。
在这里插入图片描述

  1. 访问主页并打开连接
  2. 请求到达服务器,被认为该请求访问被保护的资源
  3. 由于你未认证,服务器会响应对应的HTTP 响应码(如401)或重定向到(302)特殊的页面(如登录页)
  4. 对【3】的补充:根据身份验证机制的不同,你可以重定向到登录页填表单登录(如账号密码),也可以让浏览器以某种方式确认信息(BASIC身份验证、cookie、证书),有关认证方式
  5. 对【4】的补充:浏览器用POST请求,把需要验证的凭据放在from表单里或者请求头中,等待服务器校验。
  6. 服务器校验凭据无效,通常会让浏览器重试或者重复【2】
  7. 服务器校验凭据有效,则需要看当前用户有没有访问被保护资源的权限,没有的话就返回403的错误码。
  • 值得一提的是,校验凭据无效指的根据凭证是找不到系统用户(token没有对应的value),无权限指的是用对应的token找到了用户,但是该用户的权限不足。

过滤器:ExceptionTranslationFilterAuthenticationEntryPoint
认证机制:“authentication mechanism”

3.1 哪些是Spring Security在Web应用的配置属性

官网说的的配置属性,指安全的元信息,也就是角色信息的定义: 如 access='ROLE_A,ROLE_B’

  • 配置属性给谁用?
    AbstractSecurityInterceptor 的实现类用
  • 配置属性如何查?
    ConfigAttribute 的实现类查
  • 配置属性有什么意义?
    需要根据AccessDecisionManager的实现类的业务逻辑来定义

具体的出入口定义如下:

The AbstractSecurityInterceptor is configured with a SecurityMetadataSource which it uses to look up the attributes for a secure object. Usually this configuration will be hidden from the user.

官网的说法是 AbstractSecurityInterceptor 是借SecurityMetadataSource 完成配置的,看看源码的表现:

public abstract class AbstractSecurityInterceptor {
	public abstract SecurityMetadataSource obtainSecurityMetadataSource();
}
  • SecurityMetadataSource 作为抽象方法的返回值,强制子类实现,言下之意是框架的安全元信息是经过SecurityMetadataSource进行包装返回给框架。这些配置属性的内容都是对用户隐藏的

For example, when we saw something like <intercept-url pattern='/secure/**' access='ROLE_A,ROLE_B'/> in the namespace introduction, this is saying that the configuration attributes ROLE_A and ROLE_B apply to web requests matching the given pattern. Configuration attributes will be entered as annotations on secured methods or as access attributes on secured URLs.

  • 这个配置属性是说形如/secure/** 的URL,需要ROLE_A,ROLE_B的角色(没有说是且还是或的关系,下文解释)

In practice, with the default AccessDecisionManager configuration, this means that anyone who has a GrantedAuthority matching either of these two attributes will be allowed access.

  • 结合上文,默认的实现下,ROLE_A,ROLE_B 是或的关系即可访问。

  • 标记了ROLE_的前缀,RoleVoter 才会处理,以上说的规则仅在AccessDecisionManager 启用时才生效。

3.2 AccessDecisionManager 与 AbstractSecurityInterceptor 职责划分

Assuming AccessDecisionManager decides to allow the request, the AbstractSecurityInterceptor will normally just proceed with the request…This might be useful in reasonably unusual situations, such as if a services layer method needs to call a remote system and present a different identity.

官方给了一个场景
AccessDecisionManager 决定请求是否被允许(也就是鉴权)
AbstractSecurityInterceptor 只是处理请求
但是比较特殊的情况是AccessDecisionManager 获取的token还要经过一个第三方机构换成另外一个token参与鉴权,这个任务是交给RunAsManager

3.3 AfterInvocationManager 对返回结果做兜底策略。

看源码:

public abstract class AbstractSecurityInterceptor {
	private AfterInvocationManager afterInvocationManager;
}
protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
		finallyInvocation(token); // continue to clean in this method for passivity
		if (afterInvocationManager != null) {
			// Attempt after invocation handling
			try {
				returnedObject = afterInvocationManager.decide(token.getSecurityContext()
						.getAuthentication(), token.getSecureObject(), token
						.getAttributes(), returnedObject);
			}
					return returnedObject;
	}
  • 参数列表returnedObject 接受一个返回对象,最后可由AfterInvocationManager 进行处理。

3.4 AbstractSecurityInterceptor 的三大实现类对应的三种切面

官网原图

在这里插入图片描述

  • Filter 切面,基于Servlet架构
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
		Filter
  • Interceptor ,基于Spring AOP
public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements
		MethodInterceptor
public interface MethodInterceptor extends Interceptor
  • Aspectj, 基于Spring+AspectJ 是区别于Spring AOP的另一种织入切面的方式,在5.2.0的版本 暂时找不到这个类的源码。

4. 官方对核心接口的实现

官方会重点讲以下三个抽象类的实现类。
AuthenticationManager
UserDetailsService
AccessDecisionManager

4.1 AuthenticationManager

  • 互相关系
    AuthenticationManager 的实现类 ProviderManager
    ProviderManager 委派 一个AuthenticationProvider列表去处理认证请求。每一个Provider 要么抛出异常,要么返回一个完整的Authentication对象(即认证通过)。

看源码:

public class ProviderManager{
private List<AuthenticationProvider> providers = Collections.emptyList();

	public Authentication {
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}
			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {} 
			catch (AuthenticationException e) {}
		}
		// 省略...
		throw lastException;
	}

}
  • 认证的细节都在provider.authenticate(authentication) 中,官方给了DaoAuthenticationProvider 的实现
    但是这个Provider并没有直接改authenticate()的实现,那么在DaoAuthenticationProvider 应该是改了抽象类中的模板方法
  • DaoAuthenticationProvider
protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) {
			String presentedPassword = authentication.getCredentials().toString();

		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			// 省略
		}
}			
protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication) {
	UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		if (upgradeEncoding) {
		}
		return super.createSuccessAuthentication(principal, authentication, user);
}
  • 父类 AbstractUserDetailsAuthenticationProviderauthenticate() 都必须用到以上的方法。
public Authentication authenticate(Authentication authentication){
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();
		UserDetails user = this.userCache.getUserFromCache(username);
		user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
		additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

至于委派Provider去做认证的环节,应该要参考策略模式,有空研究了就回来补充

4.2 ProviderManager 的执行链

默认的执行链:

if you are not using the namespace then you would declare it like so:

<bean id="authenticationManager"
        class="org.springframework.security.authentication.ProviderManager">
    <constructor-arg>
        <list>
            <ref local="daoAuthenticationProvider"/>
            <ref local="anonymousAuthenticationProvider"/>
            <ref local="ldapAuthenticationProvider"/>
        </list>
    </constructor-arg>
</bean>

In the above example we have three providers. They are tried in the order shown (which is implied by the use of a List), with each provider able to attempt authentication, or skip authentication by simply returning null. If all implementations return null, the ProviderManager will throw a ProviderNotFoundException. If you’re interested in learning more about chaining providers, please refer to the ProviderManager Javadoc.

以上这段话的意思是,每个Provider如果想跳过当前的校验,返回null 即可,但是所有实现都返回null整个执行链条会抛出异常。言下之意是,开发者至少实现一种Provider

4.3 认证通过后 ProviderManager 会清理敏感信息

By default (from Spring Security 3.1 onwards) the ProviderManager will attempt to clear any sensitive credentials information from the Authentication object which is returned by a successful authentication request. This prevents information like passwords being retained longer than necessary.

官方提示如果要缓存用户的token

  1. 在进入ProviderManager 之前拷贝
  2. 在ProviderManager 返回Authentication 之前
  3. 禁用 eraseCredentialsAfterAuthentication 属性

4.4 DaoAuthenticationProvider

Sercurity中最简单的Provider 实现。

It authenticates the user simply by comparing the password submitted in a UsernamePasswordAuthenticationToken against the one loaded by the UserDetailsService. Configuring the provider is quite simple:

  • 默认的实现
    UsernamePasswordAuthenticationToken 中存储的密码对比UserDetailsService 中加载的密码(返回的UserDetail)做对比

  • 配置

<bean id="daoAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/> <!-密码编码器是可选的配置-->
</bean>

4.5 最简单的UserDetailsService 实现

In-Memory Authentication 基于内存的认证用户

<user-service id="userDetailsService">
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
NoOpPasswordEncoder should be used. This is not safe for production, but makes reading
in samples easier. Normally passwords should be hashed using BCrypt -->
<user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" />
</user-service>

{noop} 表示对密码不进行编码

通过外部化配置:

<user-service id="userDetailsService" properties="users.properties"/>

对应的properties文件:

jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE_USER,enabled

4.5 JdbcDaoImpl

依赖数据库的认证用户获取源,这里跟JDBC的配置有关的内容就不展示了。

Internally Spring JDBC is used, so it avoids the complexity of a fully-featured object relational mapper (ORM) just to store user details. If your application does use an ORM tool, you might prefer to write a custom UserDetailsService to reuse the mapping files you’ve probably already created.

Security 可以做到ORM的功能,但是还是建议自己系统有ORM的情况下自定义一个UserDetailsService 来获取用户列表。言下之意是,最好自己维护自己定义的user表在数据库中,查出来后用UserDetailsService 的标准返回即可(即返回一个UserDetail

5. 认证实战

光是认证实战的内容就特别多,光看文档没有用,用以上的知识把单体的认证流程跑通了再回来补充。
在这里插入图片描述

后记

Sercurity 认证的内容还挺多的,并且很清晰得看出来官方把认证和授权给解耦了。后期会跟进授权的知识,并用在微服务中实现单点登录(Oauth2.0)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值