#Spring Security:身份验证令牌Authentication介绍与Debug分析
在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
接口及其实现类如下图所示:
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
接口来抽象用户()。-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
进行授权,但是在访问应用程序之前,用户已经被某个外部系统可靠地验证过了,将这种情况称为预认证场景,比如CSDN
可以使用其他平台的账号进行登陆,如下图所示:
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分析
项目结构图:
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
实例。 创建的便是
AnonymousAuthenticationToken
实例。
创建完
AnonymousAuthenticationToken
实例之后,请求会继续被其他过滤器处理,这就是Spring Security
提供的过滤器链。在访问接口前,Spring Security
会检查该请求的客户端是否具有访问该接口的权限。很显然是没有权限的,因此访问被拒绝了。 并且请求会被重定向到登录页,填入用户名和密码(配置文件中定义的)。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/5922b0053c9e56f1ff29635f5b86cc46.png)
登陆请求会被UsernamePasswordAuthenticationFilter
处理,该过滤器会创建UsernamePasswordAuthenticationToken
实例,该实例将用于验证。
如果该
UsernamePasswordAuthenticationToken
实例验证成功,将会创建一个新的UsernamePasswordAuthenticationToken
实例,表示身份验证成功的令牌。
登陆请求验证成功后,又会进行重定向,重定向到原来想要访问的接口(资源),即
/message
。 再次重定向的请求又会被过滤器链进行处理,最后会验证通过。
接口便访问成功了。
再来回味一下这段话,就很容易理解
Authentication
的作用了,在Spring Security
中,通过Authentication
来封装用户的验证请求信息,Authentication
可以是需要验证和已验证的用户请求信息封装。
把Debug
分析中的整个流程搞明白,便很容易理解Authentication
的作用,不同的Authentication
实现用于不同的验证时机与场景。
身份验证令牌Authentication
介绍与Debug
分析就到这里,如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。