在​​Spring Security​​​中,通过​​Authentication​​​来封装用户的验证请求信息,​​Authentication​​​可以是需要验证和已验证的用户请求信息封装。接下来,博主介绍​​Authentication​​接口及其实现类。

Authentication

​Authentication​​​接口源码(​​Authentication​​​接口继承​​Principal​​​接口,​​Principal​​接口表示主体的抽象概念,可用于表示任何实体):

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;

public interface Authentication extends Principal, Serializable {
/**
* 由AuthenticationManager(用于验证Authentication请求)设置,用于指示已授予主体的权限
* 除非已由受信任的AuthenticationManager设置,否则实现类不应依赖此值作为有效值
* 实现应确保对返回的集合数组的修改不会影响Authentication对象的状态,或使用不可修改的实例
* 返回:授予主体的权限,如果令牌尚未经过身份验证,则为空集合
*/
Collection<? extends GrantedAuthority> getAuthorities();

/**
* 证明主体的凭据
* 通常是一个密码,但可以是与AuthenticationManager相关的任何内容
* 调用者应填充凭据
*/
Object getCredentials();

/**
* 存储有关身份验证请求的其他详细信息
* 可能是IP地址、证书序列号等
*/
Object getDetails();

/**
* 被认证的主体的身份
* 在使用用户名和密码的身份验证请求情况下,这将是用户名
* 调用者应填充身份验证请求的主体
* AuthenticationManager实现通常会返回一个包含更丰富信息的Authentication作为应用程序使用的主体
* 大多数身份验证提供程序将创建一个UserDetails对象作为主体
*/
Object getPrincipal();

/**
* 用于向AbstractSecurityInterceptor指示它是否应该向AuthenticationManager提供身份验证令牌
* 通常,AuthenticationManager将在身份验证成功后返回一个不可变的身份验证令牌
* 在这种情况下,该令牌的此方法可以安全地返回true
* 返回true将提高性能,因为不再需要为每个请求调用AuthenticationManager
* 出于安全原因,这个接口的实现应该非常小心地从这个方法返回true
* 除非它们是不可变的,或者有某种方式确保属性自最初创建以来没有被更改
*/
boolean isAuthenticated();

/**
* 实现应始终允许使用false参数调用此方法
* 可以使用它来指定不应信任的身份验证令牌
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

​Authentication​​接口及其实现类如下图所示:

Spring Security:身份验证令牌Authentication介绍与Debug分析_安全

AbstractAuthenticationToken

它是​​Authentication​​接口的基类,使用此类的实现应该是不可变的(模板模式)。

public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer {

// 权限列表
private final Collection<GrantedAuthority> authorities;
// 存储有关身份验证请求的其他详细信息,可能是IP地址、证书序列号等
private Object details;
// 是否已验证,默认为false
private boolean authenticated = false;

/**
* 使用提供的权限列表创建一个身份验证令牌
*/
public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
// 权限列表为null,会将authorities属性设置为AuthorityUtils.NO_AUTHORITIES
// List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList()
// Collections.emptyList()会返回一个空列表,并且不可变
if (authorities == null) {
this.authorities = AuthorityUtils.NO_AUTHORITIES;
return;
}

for (GrantedAuthority a : authorities) {
// 权限列表中存在权限为null,抛出异常
if (a == null) {
throw new IllegalArgumentException(
"Authorities collection cannot contain any null elements");
}
}
ArrayList<GrantedAuthority> temp = new ArrayList<>(
authorities.size());
temp.addAll(authorities);
// 不可修改的权限列表
this.authorities = Collections.unmodifiableList(temp);
}

// 返回主体的权限列表
public Collection<GrantedAuthority> getAuthorities() {
return authorities;
}

// 返回主体的名称
public String getName() {
// 如果主体是UserDetails实例,返回实例的用户名
if (this.getPrincipal() instanceof UserDetails) {
return ((UserDetails) this.getPrincipal()).getUsername();
}
// 如果主体是AuthenticatedPrincipal实例,返回实例的名称
if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
}
// 如果主体是Principal实例,返回实例的名称
if (this.getPrincipal() instanceof Principal) {
return ((Principal) this.getPrincipal()).getName();
}

// 如果主体为null,则返回"",否则返回实例的toString()
return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
}

// 返回主体是否已验证
public boolean isAuthenticated() {
return authenticated;
}

// 设置authenticated属性
public void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
}

// 返回存储有关身份验证请求的其他详细信息
public Object getDetails() {
return details;
}

// 设置details属性
public void setDetails(Object details) {
this.details = details;
}

/**
* 检查credentials、principal和details属性
* 对任何CredentialsContainer实例调用eraseCredentials方法
*/
public void eraseCredentials() {
eraseSecret(getCredentials());
eraseSecret(getPrincipal());
eraseSecret(details);
}

// 判断参数是否instanceof CredentialsContainer
// 如果是,则调用参数的eraseCredentials方法
private void eraseSecret(Object secret) {
if (secret instanceof CredentialsContainer) {
((CredentialsContainer) secret).eraseCredentials();
}
}
...
}
  • ​UserDetails​​​:​​Spring Security​​​使用​​UserDetails​​接口来抽象用户(​​Spring Security:用户UserDetails源码与Debug分析​​)。
  • ​AuthenticatedPrincipal​​​:一旦​​Authentication​​​请求已通过​​AuthenticationManager.authenticate(Authentication)​​​方法成功验证,则表示经过身份验证的​​Principal​​​(实体)。实现者通常提供他们自己的​​Principal​​​表示,其中通常包含描述​​Principal​​​实体的信息,例如名字、地址、电子邮件、电话以及​​ID​​​等,此接口允许实现者公开其自定义的特定属性以通用方式表示​​Principal​​。
  • ​Principal​​​:该接口表示主体的抽象概念,可用于表示任何实体,是​​java.security​​​包下的接口,并非由​​Spring Security​​提供。

UsernamePasswordAuthenticationToken

它是一种​​Authentication​​​实现,继承​​AbstractAuthenticationToken​​​抽象类,旨在简单地表示用户名和密码。​​principal​​​和​​credentials​​​属性应设置为通过其​​toString​​​方法提供相应属性的​​Object​​​,最简单的就是​​String​​类型。

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

private final Object principal;
private Object credentials;

/**
* 任何希望创建UsernamePasswordAuthenticationToken实例的代码都可以安全地使用此构造函数
* 因为isAuthenticated()将返回false
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}

/**
* 此构造函数只能由满足生成可信(即isAuthenticated() = true )身份验证令牌的AuthenticationManager或AuthenticationProvider实现使用
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // 必须使用super来设置
}

// 返回凭证(如密码)
public Object getCredentials() {
return this.credentials;
}

// 返回实体(如用户名)
public Object getPrincipal() {
return this.principal;
}

// 设置isAuthenticated属性,只能设置为false
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
// 无法将此令牌设置为受信任的令牌
// 需要使用有GrantedAuthority列表参数的构造函数
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}

super.setAuthenticated(false);
}

// 重写eraseCredentials方法
// 将凭证直接设置为null
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}

​TestingAuthenticationToken​​​类是设计用于单元测试,对应的身份验证提供程序是​​TestingAuthenticationProvider​​,这里就不过多介绍它了。

RememberMeAuthenticationToken

它是一种​​Authentication​​​实现,继承​​AbstractAuthenticationToken​​​抽象类,表示需要记住的​​Authentication​​​,需要记住的​​Authentication​​​必须提供完全有效的​​Authentication​​​ ,包括适用的​​GrantedAuthority​​。

public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

// 主体
private final Object principal;
// 识别此对象是否由授权客户生成的key的hashCode
private final int keyHash;

/**
* 构造函数
* 参数:
* key – 识别此对象是否由授权客户生成
* principal – 主体(通常是UserDetails)
* authorities — 授予主体的权限
*/
public RememberMeAuthenticationToken(String key, Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);

if ((key == null) || ("".equals(key)) || (principal == null)
|| "".equals(principal)) {
throw new IllegalArgumentException(
"Cannot pass null or empty values to constructor");
}

this.keyHash = key.hashCode();
this.principal = principal;
setAuthenticated(true);
}

/**
* 帮助Jackson反序列化的私人构造函数
* 参数:
* keyHash – 上面给定key的hashCode
* principal – 主体(通常是UserDetails)
* authorities — 授予主体的权限
*/
private RememberMeAuthenticationToken(Integer keyHash, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);

this.keyHash = keyHash;
this.principal = principal;
setAuthenticated(true);
}

/**
* 总是返回一个空String
*/
@Override
public Object getCredentials() {
return "";
}

// 返回keyHash
public int getKeyHash() {
return this.keyHash;
}

// 返回主体
@Override
public Object getPrincipal() {
return this.principal;
}
}

PreAuthenticatedAuthenticationToken

它是一种​​Authentication​​​实现,继承​​AbstractAuthenticationToken​​​抽象类,用于预认证身份验证。有些情况下,希望使用​​Spring Security​​​进行授权,但是在访问应用程序之前,用户已经被某个外部系统可靠地验证过了,将这种情况称为预认证场景,比如可以使用其他平台的账号进行登陆

public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

// 主体
private final Object principal;
// 凭证
private final Object credentials;

/**
* 用于身份验证请求的构造函数
* isAuthenticated()将返回false
*/
public PreAuthenticatedAuthenticationToken(Object aPrincipal, Object aCredentials) {
super(null);
this.principal = aPrincipal;
this.credentials = aCredentials;
}

/**
* 用于身份验证响应的构造函数
* isAuthenticated()将返回true
*/
public PreAuthenticatedAuthenticationToken(Object aPrincipal, Object aCredentials,
Collection<? extends GrantedAuthority> anAuthorities) {
super(anAuthorities);
this.principal = aPrincipal;
this.credentials = aCredentials;
setAuthenticated(true);
}

/**
* 返回凭证
*/
public Object getCredentials() {
return this.credentials;
}

/**
* 返回主体
*/
public Object getPrincipal() {
return this.principal;
}
}

AnonymousAuthenticationToken

它是一种​​Authentication​​​实现,继承​​AbstractAuthenticationToken​​​抽象类,表示匿名​​Authentication​​。

public class AnonymousAuthenticationToken extends AbstractAuthenticationToken implements
Serializable {

private static final long serialVersionUID = 1L;
// 主体
private final Object principal;
// 识别此对象是否由授权客户生成的key的hashCode
private final int keyHash;

/**
* 构造函数
* 参数:
* key – 识别此对象是否由授权客户生成
* principal – 主体(通常是UserDetails)
* authorities — 授予主体的权限
*/
public AnonymousAuthenticationToken(String key, Object principal,
Collection<? extends GrantedAuthority> authorities) {
this(extractKeyHash(key), principal, authorities);
}

/**
* 该构造函数有助于Jackson反序列化
* 参数:
* keyHash – 提供的Key的hashCode,由上面的构造函数提供
* principal – 主体(通常是UserDetails)
* authorities — 授予主体的权限
*/
private AnonymousAuthenticationToken(Integer keyHash, Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);

if (principal == null || "".equals(principal)) {
throw new IllegalArgumentException("principal cannot be null or empty");
}
Assert.notEmpty(authorities, "authorities cannot be null or empty");

this.keyHash = keyHash;
this.principal = principal;
setAuthenticated(true);
}

// 返回参数key的hashCode
private static Integer extractKeyHash(String key) {
Assert.hasLength(key, "key cannot be empty or null");
return key.hashCode();
}

/**
* 总是返回一个空String
*/
@Override
public Object getCredentials() {
return "";
}

// 返回keyHash
public int getKeyHash() {
return this.keyHash;
}

// 返回主体
@Override
public Object getPrincipal() {
return this.principal;
}
}

RunAsUserToken

它是一种​​Authentication​​​实现,继承​​AbstractAuthenticationToken​​​抽象类,用于支持​​RunAsManagerImpl​​​的​​Authentication​​实现。

​RunAsManagerImpl​​​类是​​RunAsManager​​​接口的基本实现,如果发现​​ConfigAttribute.getAttribute()​​​以​​RUN_AS_​​​为前缀,它会生成一个新的​​RunAsUserToken​​​实例,包含与原始​​Authentication​​实例相同的主体、凭证和授予权限列表等。

​RunAsManager​​​接口仅为当前安全对象调用创建一个新的临时​​Authentication​​​实例,此接口允许实现替换,仅适用于当前安全对象调用的​​Authentication​​对象。

这是为了建立具有两层对象的系统,一层是面向公众的,并且具有正常的安全方法,授予的权限预计由外部调用者持有。 另一层是私有的,只希望由面向公众的层中的对象调用,此私有层中的对象仍然需要安全性(否则它们将是公共方法),需要防止被外部调用者直接调用,并且私有层中的对象被授予的权限从不授予外部调用者。

​RunAsManager​​​接口提供了一种以上述方式提升安全性的机制。预计实现将提供相应的具体​​Authentication​​​和​​AuthenticationProvider​​​以便可以对替换的​​Authentication​​​对象进行身份验证。需要实施某种形式的安全性,以确保​​AuthenticationProvider​​​仅接受由​​RunAsManager​​​授权的具体实现创建的​​Authentication​​对象。

public class RunAsUserToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

// 原Authentication对象的类型
private final Class<? extends Authentication> originalAuthentication;
// 凭证
private final Object credentials;
// 主体
private final Object principal;
// 识别此对象是否由授权客户生成的key的hashCode
private final int keyHash;

// 构造方法
public RunAsUserToken(String key, Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities,
Class<? extends Authentication> originalAuthentication) {
super(authorities);
this.keyHash = key.hashCode();
this.principal = principal;
this.credentials = credentials;
this.originalAuthentication = originalAuthentication;
setAuthenticated(true);
}

// 返回凭证
@Override
public Object getCredentials() {
return this.credentials;
}

// 返回keyHash属性
public int getKeyHash() {
return this.keyHash;
}

// 返回原Authentication对象的类型
public Class<? extends Authentication> getOriginalAuthentication() {
return this.originalAuthentication;
}

// 返回主体
@Override
public Object getPrincipal() {
return this.principal;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder(super.toString());
String className = this.originalAuthentication == null ? null
: this.originalAuthentication.getName();
sb.append("; Original Class: ").append(className);

return sb.toString();
}
}

JaasAuthenticationToken

​UsernamePasswordAuthenticationToken​​​的扩展,用来携带用户登录的​​Jaas LoginContext​​。

public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

// 用户登录的Jaas LoginContext
private final transient LoginContext loginContext;

// 构造函数
public JaasAuthenticationToken(Object principal, Object credentials,
LoginContext loginContext) {
super(principal, credentials);
this.loginContext = loginContext;
}

// 构造函数
public JaasAuthenticationToken(Object principal, Object credentials,
List<GrantedAuthority> authorities, LoginContext loginContext) {
super(principal, credentials, authorities);
this.loginContext = loginContext;
}

// 返回用户登录的Jaas LoginContext
public LoginContext getLoginContext() {
return loginContext;
}
}

Debug分析

项目结构图:

Spring Security:身份验证令牌Authentication介绍与Debug分析_spring_02

​pom.xml​​:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.kaven</groupId>
<artifactId>security</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
</parent>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

</project>

​application.yml​​:

spring:
security:
user:
name: kaven
password: itkaven
logging:
level:
org:
springframework:
security: DEBUG

​SecurityConfig​​​(​​Spring Security​​的配置类,不是必须的,因为会有默认的配置):

package com.kaven.security.config;

import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 任何请求都需要进行验证
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
// 记住身份验证
.rememberMe(Customizer.withDefaults())
// 基于表单登陆的身份验证方式
.formLogin(Customizer.withDefaults());
}
}

​MessageController​​(定义接口):

package com.kaven.security.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessageController {
@GetMapping("/message")
public String getMessage() {
return "hello spring security";
}
}

启动类:

package com.kaven.security;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

​Debug​​​方式启动应用,访问​​http://localhost:8080/message​​​。请求会被​​AnonymousAuthenticationFilter​​​处理,该过滤器会创建​​Authentication​​实例。

Spring Security:身份验证令牌Authentication介绍与Debug分析_身份验证_03


创建的便是​​AnonymousAuthenticationToken​​实例。

Spring Security:身份验证令牌Authentication介绍与Debug分析_安全_04


Spring Security:身份验证令牌Authentication介绍与Debug分析_安全_05


创建完​​AnonymousAuthenticationToken​​​实例之后,请求会继续被其他过滤器处理,这就是​​Spring Security​​​提供的过滤器链。在访问接口前,​​Spring Security​​会检查该请求的客户端是否具有访问该接口的权限。很显然是没有权限的,因此访问被拒绝了。

Spring Security:身份验证令牌Authentication介绍与Debug分析_spring_06


并且请求会被重定向到登录页,填入用户名和密码(配置文件中定义的)。

Spring Security:身份验证令牌Authentication介绍与Debug分析_构造函数_07

登陆请求会被​​UsernamePasswordAuthenticationFilter​​​处理,该过滤器会创建​​UsernamePasswordAuthenticationToken​​实例,该实例将用于验证。

Spring Security:身份验证令牌Authentication介绍与Debug分析_java_08

Spring Security:身份验证令牌Authentication介绍与Debug分析_构造函数_09


如果该​​UsernamePasswordAuthenticationToken​​​实例验证成功,将会创建一个新的​​UsernamePasswordAuthenticationToken​​实例,表示身份验证成功的令牌。

Spring Security:身份验证令牌Authentication介绍与Debug分析_spring_10


Spring Security:身份验证令牌Authentication介绍与Debug分析_spring_11


登陆请求验证成功后,又会进行重定向,重定向到原来想要访问的接口(资源),即​​/message​​。

Spring Security:身份验证令牌Authentication介绍与Debug分析_构造函数_12


再次重定向的请求又会被过滤器链进行处理,最后会验证通过。

Spring Security:身份验证令牌Authentication介绍与Debug分析_安全_13


接口便访问成功了。

Spring Security:身份验证令牌Authentication介绍与Debug分析_java_14


再来回味一下这段话,就很容易理解​​Authentication​​​的作用了,在​​Spring Security​​​中,通过​​Authentication​​​来封装用户的验证请求信息,​​Authentication​​可以是需要验证和已验证的用户请求信息封装。

把​​Debug​​​分析中的整个流程搞明白,便很容易理解​​Authentication​​​的作用,不同的​​Authentication​​实现用于不同的验证时机与场景。

身份验证令牌​​Authentication​​​介绍与​​Debug​​分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。