Spring Security使用手册
Spring Security是什么?
Spring Security是一个Java框架,用于保护应用程序的安全性。
- 它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。
- Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。
- 它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。
- 此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。
总之,Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。
Spring Security的使用
Spring Boot提供了一个 spring-boot-starter-security starter,聚合了security相关的依赖,在pom文件中添加依赖即可引入。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>在配置类上添加@EnableWebSecurity开启web安全,添加@EnableMethodSecurity开启方法安全。
@Configuration
@EnableWebSecurity//starter默认添加,可不加
@EnableMethodSecurity//启用方法安全
public class SecurityConfig {
}spring security的默认自动配置如下:
//org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration;
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().anyRequest().authenticated();
http.formLogin();
http.httpBasic();
return http.build();
}
}配置密码编码器,用于加密密码和匹配密码:
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
//或
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}在方法上,添加权限验证:
@Service
public class TsysUserServiceImpl implements TsysUserService {
@Autowired
TsysUserMapper tsysUserMapper;
@PreAuthorize("#username==authentication.name")
public TsysUser getUserInfoByUserName(@P("username") String username) {
if (ObjectUtils.isEmpty(username))
return null;
return tsysUserMapper.selectOneByUsername(username);
}
@PreAuthorize("hasAuthority('admin')")
public List<TsysUser> getAllUser() {
return tsysUserMapper.selectList(Wrappers.emptyWrapper());
}
}Spring Security支持的认证协议
LDAP认证
LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)认证是一种用于访问和认证目录服务中的用户和资源信息的网络协议。目录服务通常用于存储和组织大量的用户数据,例如用户帐号、密码、电子邮件地址、电话号码等信息。LDAP认证允许应用程序和服务通过网络连接到目录服务器,并进行身份验证和访问控制。
ASL
ACL是Access Control List(访问控制列表)的缩写,它是用于管理资源(如文件、目录、网络资源等)访问权限的一种权限控制机制。ACL允许系统管理员或资源拥有者指定哪些用户或用户组可以访问资源以及他们可以执行的操作。
CAS
CAS是Central Authentication Service(中央认证服务)的缩写,是一种单点登录(Single Sign-On,SSO)协议和技术。CAS旨在解决多个应用程序之间用户身份验证和会话管理的问题,使用户只需要登录一次,就可以访问多个应用程序而无需重复输入凭据。
OAuth 2.0
OAuth 2.0是什么?
OAuth 2.0是一种授权协议,旨在让用户允许第三方应用程序访问其受保护资源,而无需将其登录凭据直接提供给这些应用程序。它是构建在OAuth 1.0协议基础上的改进版本,被广泛用于支持安全的API访问和实现单点登录(Single Sign-On,SSO)等场景。
OAuth 2.0包含的角色
OAuth 2.0协议涉及以下几个主要角色:
- 资源所有者(Resource Owner):通常是指最终的用户,拥有受保护的资源,如用户的个人数据或照片。
- 客户端(Client):第三方应用程序,希望获得资源所有者的授权访问受保护的资源。
- 授权服务器(Authorization Server):负责对资源所有者进行认证并颁发访问令牌(Access Token)给客户端。
- 资源服务器(Resource Server):存储受保护资源的服务器,用于验证客户端请求的访问令牌并返回受保护资源。
OAuth 2.0的流程
OAuth 2.0流程的主要步骤如下:
客户端向资源所有者请求授权:当用户尝试访问需要授权的资源时,客户端将重定向用户到授权服务器,请求用户对其资源的授权。
资源所有者授权:资源所有者登录到授权服务器,确认是否允许客户端访问其资源。如果用户授权,授权服务器将生成一个授权码(Authorization Code)。
客户端获取访问令牌:客户端将授权码发送回给授权服务器,请求一个访问令牌。授权服务器验证授权码的有效性,并返回一个访问令牌给客户端。
客户端访问资源服务器:客户端使用访问令牌向资源服务器请求受保护资源。资源服务器验证令牌的有效性,并根据授权范围决定是否返回资源。
OAuth 2.0的优点
OAuth 2.0的主要优势在于,资源所有者的凭据(用户名和密码)不会直接传递给客户端,减少了安全风险。而且,资源所有者可以控制哪些资源可以被特定的客户端访问,以及控制访问令牌的有效期和权限范围。这使得OAuth 2.0成为了现代应用程序中实现安全、开放和授权访问的重要协议。
Spring Secutiry的特性
密码认证
Spring Security提供了对 认证(authentication) 的全面支持。认证是指我们如何验证试图访问特定资源的人的身份。一个常见的验证用户的方法是要求用户输入用户名和密码。一旦进行了认证,我们就知道了身份并可以执行授权。
Spring Security的密码存储机制
Spring Security提供PasswordEncoder接口用于对密码进行单向转换,让密码安全地存储。
PasswordEncoder接口提供了三个方法:
encode:用于对输入的密码进行编码加密matches:用于验证输入的密码和存储的密码是否匹配upgradeEncoding:判断输入的密码安全等级是否足够,不够则需要重新输入。
Spring Security中PasswordEncoder的默认实现是DelegatingPasswordEncoder,可以通过PasswordEncoderFactories轻松构建DelegatingPasswordEncoder,默认使用的密码编码器是BCryptPasswordEncoder。
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();创建UserDetails时,也可以方便地使用默认密码编码器。
@Test
void userDetailTest() {
//使用默认的密码编码器
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("chenzhuowen")
.password("123456")
.roles("admin")
.build();
System.out.println(userDetails.getPassword());
}Spring Security支持的密码编码器
使用自适应单向函数作为密码加密算法时,应该将其在你的系统上验证一个密码的时间调整为1秒左右。
- BCryptPasswordEncoder:BCryptPasswordEncoder 的实现使用广泛支持的 bcrypt 算法对密码进行散列。为了使它对密码破解有更强的抵抗力,bcrypt故意做得很慢。
- Argon2PasswordEncoder:Argon2PasswordEncoder 的实现使用 Argon2 算法对密码进行散列。Argon2是 密码哈希大赛 的冠军。为了打败定制硬件上的密码破解,Argon2是一种故意的慢速算法,需要大量的内存。
- Pbkdf2PasswordEncoder:Pbkdf2PasswordEncoder 的实现使用 PBKDF2 算法对密码进行散列。为了抵御密码破解,PBKDF2是一种故意的慢速算法。
- SCryptPasswordEncoder:SCryptPasswordEncoder 的实现使用 scrypt 算法对密码进行散列。为了打败定制硬件上的密码破解,scrypt是一个故意的慢速算法,需要大量的内存。
- 其他 PasswordEncoder:有相当数量的其他 PasswordEncoder 实现,它们的存在是为了做向后兼容,而没有删除这些过时的编码器。
防范漏洞攻击
CSRF(跨站请求伪造)
什么是CSRF攻击?
CSRF(Cross-Site Request Forgery)攻击是一种Web应用程序安全漏洞,也被称为Session Riding或者One-Click攻击。在CSRF攻击中,攻击者通过欺骗用户使其在登录了目标网站的情况下,执行未经授权的操作。这种攻击利用了用户当前登录的身份验证信息(如Cookie或Session ID),通过发送伪造的请求来伪装成受信任的用户,从而执行恶意操作。
CSRF攻击的典型场景如下:
- 用户登录目标网站A,并获取了包含身份验证信息的Cookie。
- 用户在未登出网站A的情况下,访问了恶意网站B。
- 恶意网站B中的代码会发起伪造的请求,向目标网站A发送具有攻击性质的操作,比如修改密码、删除帐户或执行其他敏感操作。
Spring Security如何防范CSRF攻击?
Spring 提供了两种机制来防止CSRF攻击:
- 同步令牌(Synchronizer Token)模式
- 在你的session cookies上指定SameSite属性
同步令牌(Synchronizer Token)模式
同步令牌模式下,用户登录时,应用会生成一个随机的、一次性的令牌(或称CSRF令牌),将令牌与用户会话关联起来。在用户提交表单时,会带上令牌提交到服务器。服务器会检查表单中的令牌与会话中的令牌是否一致。只有一致才会接受并处理请求。
SameSite 属性
服务器在设置cookie时指定samesite属性,以表明来自外部网站的cookie不应该被发送。
samesite有三个属性,分别是:
- Strict:Strict 最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
Set-Cookie: CookieName=CookieValue; SameSite=Strict;- Lax:Lax 规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
Set-Cookie: CookieName=CookieValue; SameSite=Lax;导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。
| 请求类型 | 示例 | 正常情况 | Lax |
|---|---|---|---|
| 链接 | <a href="..."></a> | 发送 Cookie | 发送 Cookie |
| 预加载 | <link rel="prerender" href="..."/> | 发送 Cookie | 发送 Cookie |
| GET 表单 | <form method="GET" action="..."> | 发送 Cookie | 发送 Cookie |
| POST 表单 | <form method="POST" action="..."> | 发送 Cookie | 不发送 |
| iframe | <iframe src="..."></iframe> | 发送 Cookie | 不发送 |
| AJAX | $.get("...") | 发送 Cookie | 不发送 |
| Image | <img src="..."> | 发送 Cookie | 不发送 |
设置了 Strict 或 Lax 以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。
- None:Chrome 计划将 Lax 变为默认设置。这时,网站可以选择显式关闭 SameSite 属性,将其设为 None 。不过,前提是必须同时设置 Secure 属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
Set-Cookie: widget_session=abc123; SameSite=None; Secure哪些地方需要使用进行CSRF保护
- 登录请求
- 注销请求
- 文件上传请求
- 在请求体中放置CSRF令牌
- 在URL中放置CSRF令牌
安全相关的HTTP响应头
默认的HTTP响应头
spring security提供了一套默认的安全相关的HTTP响应头,以提供安全的默认值。 
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 0Cache-Control: no-cache, no-store, max-age=0, must-revalidate:控制缓存,禁用缓存。Pragma: no-cache:指示客户端不使用缓存Expires: 0:指定资源已过期,即使客户端有缓存副本,也必须从服务器获取新的资源。X-Content-Type-Options: nosniff:可以防止浏览器嗅探(sniffing)未明确指定的资源类型。这可以防止某些安全漏洞。Strict-Transport-Security:该选项强制使用HTTPS访问网站。X-Frame-Options: DENY:该选项指示浏览器不允许将页面嵌入到iframe中,以防止点击劫持等攻击。X-XSS-Protection: 0:该选项指示浏览器不要执行内置的跨站脚本(XSS)过滤器,因此可能会增加XSS攻击的风险。通常不推荐在生产环境中使用。
其他HTTP响应头
Content Security Policy (CSP)
内容安全策略(Content Security Policy) (CSP)是一种机制,web应用可以用来缓解内容注入漏洞,如跨站脚本(XSS)。CSP是一种声明性的策略,为web应用程序作者提供了一种设施,以声明并最终告知客户端(user-agent)web应用程序期望加载资源的来源。
Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/如果一个Web应用程序违反了声明的安全策略,下面的响应头指示用户代理将违规报告发送到策略的 report-uri 指令中指定的URL。
Referrer Policy
Referrer Policy 是一种机制,Web应用程序可以用来管理 referrer 字段,它包含用户最后浏览的页面。
Referrer-Policy: same-originReferrer-Policy 响应头指示浏览器让目的地知道用户之前所在的来源。
Feature Policy
Feature Policy 是一种机制,让weh开发者有选择地启用、禁用和修改浏览器中某些API和网络功能的行为。
Feature-Policy: geolocation 'self'accelerometer:控制是否允许访问设备的加速度计数据。camera:控制是否允许访问设备的摄像头。geolocation:控制是否允许访问设备的地理位置信息。gyroscope:控制是否允许访问设备的陀螺仪数据。microphone:控制是否允许访问设备的麦克风。payment:控制是否允许访问支付相关的 API。fullscreen:控制是否允许页面进入全屏模式。sync-xhr:控制是否允许使用同步的 XMLHttpRequest。usb:控制是否允许访问 USB 设备。vr:控制是否允许访问虚拟现实相关的 API。
Permissions Policy
Permissions Policy 是一种机制,让网络开发者有选择地启用、禁用和修改浏览器中某些API和网络功能的行为。
Permissions-Policy: geolocation=(self)Permissions-Policy与Feature-Policy的区别
- Permissions-Policy 提供了更广泛和细粒度的权限控制,不仅仅限于硬件访问,还可以控制脚本、跨域、弹窗等行为。
- Feature-Policy 主要用于控制对浏览器功能或 API 的访问权限,尤其是与硬件相关的功能,如摄像头和麦克风。
- Permissions-Policy 更加综合,可以涵盖多个方面的权限控制,而 Feature-Policy 的范围相对较窄。
清除网站数据
清除网站数据(Clear Site Data) 是一种机制,当HTTP响应包含这个 Header 时,任何浏览器方面的数据(cookie、local storage 等)都可以被删除。
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"自定义Header
Spring Security 有一些机制,可以方便地在你的应用程序中添加更常见的安全 Header。然而,它也提供了钩子来实现添加自定义 Header 信息。
Spring Security与其他组件的整合
spring security的密码学
Spring Security Crypto 模块提供对对称加密、密钥生成和密码编码的支持。
对称加密器(Encryptor)
Spring Security提供了Encryptors类。Encryptors提供了用于构建对称加密器的工厂方法,可以创建BytesEncryptor实例或TextEncryptor实例。
BytesEncryptor用于对字节数组进行加密,TextEncryptor用于对文本字符串进行加密。
BytesEncryptor
Encryptors的stronger方法用于创建BytesEncryptor加密器,该工厂方法接受两个参数,一个是加密密钥,一个是至少8字节的长度的盐。
@Test
public void byteEncryptorTest() {
String salt=KeyGenerators.string().generateKey();
BytesEncryptor bytesEncryptor = Encryptors.stronger("123456", salt);
byte[] encrypt = bytesEncryptor.encrypt("123456".getBytes());
System.out.println("加密后数据:" + encrypt);
String s = Base64.getEncoder().encodeToString(encrypt);
System.out.println("加密后数据转Base64:" + s);
byte[] decrypt = bytesEncryptor.decrypt(encrypt);
System.out.println("解密后数据:"+ new String(decrypt));
}TextEncryptor
Encryptor的text方法用于创建TextEncryptor加密器,该工厂方法同样接收两个参数,一个是加密秘钥,一个是至少8字节长度的盐。
@Test
public void textEncryptorTest() {
String salt = KeyGenerators.string().generateKey();
TextEncryptor textEncryptor = Encryptors.text("123456", salt);
String encrypt = textEncryptor.encrypt("123456");
System.out.println("加密后数据:"+encrypt);
String decrypt = textEncryptor.decrypt(encrypt);
System.out.println("解密后数据:"+decrypt);
}密钥生成(KeyGenerators)
KeyGenerators工具类提供了工厂方法,用于构建字节数组类型秘钥生成器BytesKeyGenerator或字符串类型的秘钥生成器StringKeyGenerator。KeyGenerators是一个线程安全的类。
BytesKeyGenerator
BytesKeyGenerator默认生成的秘钥长度为8字节,可以传入参数调整秘钥长度。
@Test
public void bytesKeyGeneratorTest() {
BytesKeyGenerator bytesKeyGenerator = KeyGenerators.secureRandom(8);
byte[] bytesKey1 = bytesKeyGenerator.generateKey();
System.out.println(bytesKey1);
byte[] bytesKey2 = bytesKeyGenerator.generateKey();
System.out.println(bytesKey2);
}StringKeyGenerator
StringKeyGenerator默认生成的秘钥长度为8字节,底层使用了BytesKeyGenerator,将每一个字节以字符形式显示。
@Test
public void stringKeyGeneratorTest() {
StringKeyGenerator stringKeyGenerator = KeyGenerators.string();
String stringKey1 = stringKeyGenerator.generateKey();
System.out.println(stringKey1);
String stringKey2 = stringKeyGenerator.generateKey();
System.out.println(stringKey2);
}SharedKeyGenerator
KeyGenerators的share方法可以生成一个SharedKeyGenerator,它每次生成的秘钥都是同一个。
@Test
public void sharedKeyGeneratorTest(){
BytesKeyGenerator sharedBytesKeyGenerator = KeyGenerators.shared(8);
byte[] bytesKey1 = sharedBytesKeyGenerator.generateKey();
System.out.println(bytesKey1);
byte[] bytesKey2 = sharedBytesKeyGenerator.generateKey();
System.out.println(bytesKey2);
}密码编码(PasswordEncoder)散列计算
spring security提供了PasswordEncoder接口用于对密码进行散列计算,
BCryptPasswordEncoder
BCryptPasswordEncoder是该接口的实现类,使用了广泛支持的bcrypt算法对密码进行散列。
@Test
void bCryptPasswordEncoder2Test() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(31);
String encode = bCryptPasswordEncoder.encode("123456");
System.out.println("加密后的数据:" + encode);
boolean matches = bCryptPasswordEncoder.matches("123456", encode);
System.out.println("是否匹配:" + matches);
}Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder是该接口的实现类,使用了PBKDF2算法对密码进行散列。
@Test
void pbkdf2PasswordEncoderTest() {
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String encode = pbkdf2PasswordEncoder.encode("123456");
System.out.println("加密后的数据:" + encode);
boolean matches = pbkdf2PasswordEncoder.matches("123456", encode);
System.out.println("是否匹配:" + matches);
}Spring Security与Spring Data整合
spring Security提供的Spring Data集成,允许你在sql中获取当前用户。
要获得这个支持,需要提供一个SecurityEvaluationContextExtension 类型的bean。
@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}使用SpEL表达式获取当前用户信息。
@Repository
public interface MessageRepository extends PagingAndSortingRepository<Message,Long> {
@Query("select m from Message m where m.to.id = ?#{ principal?.id }")
Page<Message> findInbox(Pageable pageable);
}Spring Security的并发API
在多线程环境下,Spring Security提供了三个类以支持。一个是SecurityContextHolder,一个是DelegatingSecurityContextRunnable,一个是DelegatingSecurityContextExecutor。
SecurityContextHolder
SecurityContextHolder是一个用于管理当前用户安全上下文的工具类。它主要用于在应用程序中存储和获取当前用户的安全信息,以便在整个请求处理过程中进行访问控制和身份验证。
SecurityContextHolder是全局性的。在Spring Security中,SecurityContextHolder使用了ThreadLocal来存储和管理当前用户的安全上下文信息。
// 获取当前用户的认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 获取当前用户的主体对象(通常是 UserDetails 的实现类)
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// 获取当前用户的权限列表
List<GrantedAuthority> authorities = (List<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();DelegatingSecurityContextRunnable
该类实现了Runnable接口,接受一个Runnable对象,在run方法中实现对Runnable对象的代理,同时处理SecurityContext上下文信息。
@Override
public void run() {
this.originalSecurityContext = this.securityContextHolderStrategy.getContext();
try {
this.securityContextHolderStrategy.setContext(this.delegateSecurityContext);
this.delegate.run();
}
finally {
SecurityContext emptyContext = this.securityContextHolderStrategy.createEmptyContext();
if (emptyContext.equals(this.originalSecurityContext)) {
this.securityContextHolderStrategy.clearContext();
}
else {
this.securityContextHolderStrategy.setContext(this.originalSecurityContext);
}
this.originalSecurityContext = null;
}
}DelegatingSecurityContextExecutor
该类接受一个Executor对象,代理该对象,同时处理SecurityContext上下文信息。
在我们使用executor.execute(Runnable)执行程序时,SecurityContext首先会被SecurityContextHolder获得,然后该SecurityContext被用于创建DelegatingSecurityContextRunnable。也就是说executor.execute(Runnable)和Runnable调用的方法是在同一用户运行的。
public class DelegatingSecurityContextExecutor extends AbstractDelegatingSecurityContextSupport implements Executor {
@Override
public final void execute(Runnable task) {
this.delegate.execute(wrap(task));
}
}
abstract class AbstractDelegatingSecurityContextSupport {
protected final Runnable wrap(Runnable delegate) {
return DelegatingSecurityContextRunnable.create(delegate, this.securityContext,
this.securityContextHolderStrategy);
}
}Spring Security对Jackson的支持
Spring Security的本地化
Spring Security支持对终端用户可能看到的异常信息进行本地化,默认的异常信息为英文。
在spring-security-core-xx.jar中,org.springframework.security包下,会包含一个messages.properties文件,以及一些常用语言的本地化文件。
如果你要实现异常信息本地化,可以参考这个messages.properties文件,并在注册一个bean引用这个文件。
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:org/springframework/security/messages"/>
</bean>Spring Security架构
认证与授权原理
Security的认证、授权功能是由一组过滤器链组成,只有通过所有的过滤器才能执行到最后的controller。
图中绿色的过滤器是可以配置的,蓝色和橘色的位置不可更改。
- FilterSecurityInterceptor:最后的过滤器,它会决定当前的请求可不可以访问Controller
- ExceptionTranslationFilter:异常过滤器,接收到异常消息时会引导用户进行认证;
- UsernamePasswordAuthenticationFilter
- UserDetailsService
- UserDetails
- Authentication
Spring Security过滤器架构图

- FilterChain:Servlet请求过滤器链。
- DelegatingFilterProxy:代理所有延迟注入到容器中的bean过滤器。
- FilterChainProxy:过滤器链的代理,本身也是一个filter,被注入到容器中,包含在DelegatingFilterProxy中。
- SecurityFilterChain:安全过滤器链,被FilterChainProxy代理,可以有多个。
- SecurityFilter:安全过滤器,被SecurityFilterChain管理,一个安全过滤器链内,可以有多个SecurityFilter实例。
打印Security Filter列表
filter列表在应用程序启动时以INFO级别打印,因此你可以调整控制台日志输出级别以打印SecurityFilter列表,内容如下:
2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]添加自定义Filter到SecurityFilterChain中
- 实现Filter接口,自定义Filter。
public class HelloWorldFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.printf("进入到hello world Filter中,并输出hello world");
chain.doFilter(request, response);
}
}- 将自定义Filter,添加到Filter Chain。
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(new HelloWorldFilter(), AuthorizationFilter.class);
return http.build();
}
}处理Security异常的ExceptionTranslationFilter
ExceptionTranslationFilter是Security Filter Chain中的一个过滤器,用于处理AccessDeniedException和AuthenticationException异常,允许将它们翻译成 HTTP 响应。 
- 若用户没有认证或过滤器链抛出一个
AuthenticationException异常,则SecurityContextHolder会被清空,HttpServletRequest会被缓存起来用于认证成功重发请求,AuthenticationEntryPoint则用于请求用户凭证,例如重定向到用户登录页面。 - 如果是
AccessDeniedException,那么则是拒绝访问。AccessDeniedHandler被调用处理拒绝访问。
保存认证之间的请求的RequestCacheAwareFilter
如果一个请求访问了需要认证的资源,Spring Security会对请求进行保存,在认证成功后重新请求。
在Spring Security中,这是通过使用RequestCache实现来保存HttpServletRequest的。
RequestCacheAwareFilter是过滤器链中的其中一个过滤器,它的作用是使用RequestCache保存HttpServletRequest。
默认情况下使用的是HttpSessionRequestCache,它会将请求保存到RequestCache中。
如果不想保存用户请求,并在用户认证成功后跳转到主页,可以使用NullRequestCache。
查看Spring Security调用日志
因为出于安全考虑,Spring Security 不会在响应体中添加任何关于请求被拒绝的细节。如要查找请求被拒绝的细节,可以通过日志定位。Spring Security 在 DEBUG 和 TRACE 级别提供了对所有 security 相关事件的全面记录。
可以通过以下方式开启security日志
logging.level.org.springframework.security=TRACESpring Security的认证(Authentication)
认证架构

- SecurityContextHolder:
SecurityContextHolder是Spring Security存储认证用户细节SecurityContext的地方,它默认使用ThreadLocal来存储这些细节,因此在同一线程下,SecurityContext总是可用且相同的。
可通过
SecurityContextHolder.setStrategyName()改变默认的MODE_THREADLOCAL模式。
- SecurityConext:用户安全上下文,是从
SecurityContextHolder中获得的,包含当前认证用户的Authentication(认证)。 - Authentication:可以是
AuthenticationManager的输入,以提供用户提供的认证凭证。或来自SecurityContext以获取当前用户。
Authentication包含以下信息:
- principal: 识别用户。当用用户名/密码进行认证时,这通常是
UserDetails的一个实例。- credentials: 通常是一个密码。在许多情况下,这在用户被认证后被清除,以确保它不会被泄露。
- authorities:
GrantedAuthority实例是用户被授予的高级权限。可以是角色(role)或作用域(scope)。
- authorities:
GrantedAuthority实例是用户被授予的高级权限,可以是角色(role)或作用域(scope)。
- AuthenticationManager:认证管理器,是一个接口,用于定义 Spring Security 的 Filter 如何执行 认证 的API,返回一个认证
Authentication。 - ProviderManager:是
AuthenticationManager接口的常用的实现,可以管理多个认证提供者AuthenticationProvider,对不同类型得Authentication进行认证。
AuthenticationManager在进行认证时,会遍历
List<AuthenticationProvider>内的每一个AuthenticationProvider,调用遍历到的AuthenticationProvider的supports()方法,以检查其是否支持认证当前Authentication类型的对象,如果支持,则调用authenticate()方法,对Authentication对象进行认证处理逻辑。
- AuthenticationProvider:你可以在
ProviderManager中注入多个AuthenticationProvider实例,用于执行特定类型的认证。例如,DaoAuthenticationProvider支持基于用户名/密码的认证,而JwtAuthenticationProvider支持认证JWT令牌。 - AuthenticationEntryPoint:用于处理认证失败抛出的异常,
AuthenticationEntryPoint用于发送一个要求客户端提供凭证的HTTP响应。
- 客户端在请求资源时已携带凭证的情况下,Spring Security不需要提供要求客户端提供凭证的HTTP响应。
- 在其他情况下,客户端请求未经授权、认证的资源时,AuthenticationEntryPoint则会向客户端发送提供凭证的HTTP响应。
- AbstractAuthenticationProcessingFilter:一个用于认证的基本 Filter。


用户/密码方式认证
读取用户/密码的机制
Spring Security内置了3种从HttpServletRequest中读取用户和密码机制,如下:
- 表单登录(Form Login)
- Basic认证
- 摘要(Digest)认证
表单登录(Form Login)
Spring Security的Basic认证由UsernamePasswordAuthenticationFilter过滤器提供支持,它是AbstractAuthenticationProcessingFilter的一个实现类,流程图如下: 
默认情况下,Spring Security表单登录被启用。然而,只要提供任何基于Servlet的配置,就必须明确提供基于表单的登录。
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(withDefaults());
// ...
}表单登录的请求方法必须为post方法,参数名必须为username和passowrd。
Basic认证
什么是Basic认证
HTTP基础认证(Basic HTTP Authentication)是一种在HTTP请求中用于验证用户身份的简单认证机制。它是通过在HTTP请求头中添加一个"Authorization"字段来实现的。
这种认证机制通常用于保护Web应用程序或网站的受限资源,要求用户提供用户名和密码以验证其身份。
具体步骤如下:
- 客户端发送HTTP请求到服务器。
- 服务器返回一个状态码401(未授权)的响应,要求客户端提供身份验证信息。
- 客户端在请求头中添加一个"Authorization"字段,该字段的值是"Basic"加上一个Base64编码的字符串,其中字符串格式为"username:password"。
- 服务器接收到认证信息后,解码Base64字符串,提取出用户名和密码。
- 服务器验证用户名和密码是否有效,如果有效则返回所请求的资源,如果无效则返回一个错误状态码。
HTTP基础认证的认证信息是以Base64编码的形式传输的,所以它并不是一种安全的认证机制。在非安全的网络环境下,建议使用更强大的认证方式。
Spring Security对Basic认证的支持
Spring Security的Basic认证由BasicAuthenticationFilter过滤器提供支持,流程图如下: 
默认情况下,Spring Security的HTTP Basic认证支持已经启用。然而,只要提供任何基于Servlet的配置,就必须明确提供HTTP Basic。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.httpBasic(withDefaults());
return http.build();
}摘要(Digest)认证
什么是摘要(Digest)认证
摘要认证(Digest Authentication)是一种用于在不安全的网络环境下进行安全身份验证的HTTP认证机制。与基本HTTP认证相比,摘要认证更加安全,因为它不会在网络上明文传输密码。摘要认证通过使用哈希算法和随机数来保护用户的凭据。
摘要认证的工作原理如下:
- 客户端发送HTTP请求到服务器。
- 服务器返回一个状态码401(未授权)的响应,要求客户端提供身份验证信息。
- 响应头中包含一个"WWW-Authenticate"字段,其中指定了使用摘要认证,并包含必要的参数,如realm(领域)、**nonce(随机数)**等。
- 客户端收到响应后,会使用用户名、密码以及其他相关信息计算一个哈希值,然后将这个哈希值添加到请求头的"Authorization"字段中。
- 服务器收到请求后,会根据之前生成的随机数(nonce)、领域(realm)以及密码等信息,重新计算哈希值。如果两个哈希值匹配,服务器会验证通过并返回所请求的资源。
摘要要认证是一种相对安全的HTTP身份验证机制,通过使用哈希算法和随机数来保护用户凭据,适用于在不安全网络环境下进行身份验证。
Spring Security对摘要(Digest)认证的支持
Spring Security的Basic认证由DigestAuthenticationFilter过滤器提供支持
Spring Security的用户/密码存储和查询机制
每个支持从请求中获取用户名和密码信息的认证机制都可以搭配任意的密码存储和查询机制,Spring Security支持一下存储和查询机制:
- 使用 内存认证 的 Simple Storage。
- 使用 JDBC认证 的关系型数据库
- 使用 UserDetailsService 的自定义数据存储
- 使用 LDAP认证 的 LDAP storage。
内存认证
Spring Security提供InMemoryUserDetailsManager类以使用内存来存储基于用户/密码的用户认证信息(UserDetails)。 

InMemoryUserDetailsManager内部使用了Map存储User信息。
@Test
void inMemoryUserDetailsManager() {
UserDetails user = User.builder().username("user").password("user").roles("USER").build();
UserDetails admin = User.builder().username("admin").password("admin").roles("ADMIN", "USER").build();
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(user, admin);
}JDBC认证
Spring Security提供了JdbcUserDetailsManager类实现通过jdbc查询用户、认证用户信息。它继承自JdbcDaoImpl、实现了UserDetailsManager类。
要使用JdbcUserDetailsManager,需要在数据库中创建相关表,初始化脚本在org/springframework/security/core/userdetails/jdbc/users.ddl。
@Test
void jdbcUserDetailsManager() {
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);
UserDetails user = User.builder().username("user").password("user").roles("USER").build();
UserDetails admin = User.builder().username("admin").password("admin").roles("ADMIN", "USER").build();
jdbcUserDetailsManager.createUser(user);
jdbcUserDetailsManager.createUser(admin);
UserDetails user1 = jdbcUserDetailsManager.loadUserByUsername("user");
System.out.println(user1);
}这里的DataSource对象由spring-boot-starter-jdbc、mysql-connector-java为我们初始化好,配置文件中配置好数据库连接信息。
重写UserDetailsService,自定义认证方式
自定义UserDetailsService并注入到容器中,Provider可以使用此服务查询用户信息。
@Bean
UserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}LDAP认证
用户安全信息持久化
Spring Security提供了SecurityContextRepository接口,用于管理和存储用户的安全上下文信息SecurityContext,它的主要作用是在用户登录成功后将安全上下文信息存储起来,在需求时将其检索处理啊。DelegatingSecurityContextRepository是它的默认实现,它可以将请求委托给各个SecurityContextRepository实现类,如:
- HttpSessionSecurityContextRepository
- NullSecurityContextRepository
- RequestAttributeSecurityContextRepository
HttpSessionSecurityContextRepository
实现了将SecurityContext与HttpSession相关联。当用户身份认证成功后,Spring Security会将用户的安全上下文信息存储在HTTP会话中,可以保证同一会话期间用户的安全上下文信息保持一致,用户后续的请求安全上下文一致。
NullSecurityContextRepository
这个实现类即使用户已经进行了身份认证也不会保存或检索安全上下文信息,一般用于OAuth认证或测试环境
RequestAttributeSecurityContextRepository
它用于将用户的安全上下文信息存储在HTTP请求的属性中。与其他实现不同,它不会使用会话来存储安全上下文信息,而是将它们直接附加到当前HTTP请求对象的属性上,通过是HttpServletRequest对象的属性。这样,安全上下文信息会随着请求一起传递,但不会跨请求保持一致。用户的同一次请求的请求转发可以保持上下文一致。
DelegatingSecurityContextRepository
DelegatingSecurityContextRepository是SecurityContextRepository的默认实现类,也是SecurityContextRepository代理类,可以保存多个SecurityContextRepository,按指定顺序从委托中检索。
public final class DelegatingSecurityContextRepository implements SecurityContextRepository {
private final List<SecurityContextRepository> delegates;
public DelegatingSecurityContextRepository(SecurityContextRepository... delegates) {
this(Arrays.asList(delegates));
}
...
}在Spring Security6中,默认配置了RequestAttributeSecurityContextRepository和HttpSessionSecurityContextRepository。
SecurityContextPersistenceFilter
SecurityContextPersistenceFilter是Spring Security中的一个关键组件,负责在请求期间持久化安全上下文信息SecurityContext,在请求前和处理完成后(响应被发送之前)将安全上下文持久化到适当的存储位置。SecurityContext。 
SecurityContextPersistenceFilter从SecurityContextRepository中获取SecurityContext,然后将它设置在SecurittyContextHolder中。如果程序运行过后SecurityContext发生了变化,过滤器会将SecurittyContextHolder中的SecurityContext传递给SecurityContextRepository保存。
SecurityContextHolderFilter
SecurityContextHolderFilter用于确保在每个HTTP请求期间持久化安全上下文信息,用于在请求进入应用程序之前将当前的安全上下文(通常是包含认证信息的 SecurityContext)绑定到线程。 
SecurityContextHolderFilter从SecurityContextRepository中获取SecurityContext,然后将其设置在SecurityContextHolder。
//存储安全上下文信息,并实现与请求绑定
SecurityContextHolder.setContext(securityContext);
securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse);SecurityContextPersistenceFilter和SecurityContextHolderFilter不可以同时配置
- SecurityContextHolderFilter:位于过滤链较前位置,用于在请求进入应用程序之前将当前的安全上下文绑定到线程中。后续逻辑通过
SecurityContextHolder.getContext().getAuthentication()获取认证信息- SecurityContextPersistenceFilter:位于过滤链较后位置,用于请求处理完成后将安全上下文持久到适当的位置,比如保存到HttpSession中。同一用户的后续请求可以从会话中获得他们的安全上下文。
会话管理
配置有状态管理
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class HttpSecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
//...
httpSecurity.securityContext().securityContextRepository(securityContextRepository());
//...
DefaultSecurityFilterChain securityFilterChain = httpSecurity.build();
return securityFilterChain;
}
@Bean
public SecurityContextRepository securityContextRepository() {
//保持会话安全上下文一致
HttpSessionSecurityContextRepository httpSessionSecurityContextRepository = new HttpSessionSecurityContextRepository();
//保持请求安全上下文内一致
RequestAttributeSecurityContextRepository requestAttributeSecurityContextRepository = new RequestAttributeSecurityContextRepository();
ArrayList<SecurityContextRepository> securityContextRepositoryArrayList = new ArrayList<>();
securityContextRepositoryArrayList.add(httpSessionSecurityContextRepository);
securityContextRepositoryArrayList.add(requestAttributeSecurityContextRepository);
DelegatingSecurityContextRepository delegatingSecurityContextRepository = new DelegatingSecurityContextRepository(securityContextRepositoryArrayList);
return delegatingSecurityContextRepository;
}
}在Controller中手动存储Authentication
@PostMapping("/login")
public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) {
//1. 从请求体中获取用户名和密码,创建一个未被认证的UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
loginRequest.getUsername(), loginRequest.getPassword());
//2. 使用AuthenticationManager对未认证的token进行认证,返回一个认证后的authentication
Authentication authentication = authenticationManager.authenticate(token);
//3. 创建一个空的安全上下文对象
SecurityContext context = securityContextHolderStrategy.createEmptyContext();
//4. 在安全上下文对象内设置认证信息
context.setAuthentication(authentication);
//5. 在安全上下文对象保持者中设置安全上下文
securityContextHolderStrategy.setContext(context);
//6. 使用安全上下文资源库将安全上下文、请求、响应进行绑定。
securityContextRepository.saveContext(context, request, response);
}配置无状态认证
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}当你将会话管理策略设置为SessionCreationPolicy.STATELESS时,Spring Security会将认证和授权的会话状态设置为无状态,也就是不会在服务器端保留用户的会话信息。
会话固定攻击(使用session有状态认证时)
会话固定 攻击是一种潜在的风险,恶意攻击者有可能通过访问网站创建一个会话,然后说服另一个用户用同一个会话登录(例如,通过向他们发送一个包含会话标识符作为参数的链接)。Spring Security 默认通过创建一个新的会话或在用户登录时改变 session ID 来自动防止这种情况。
spring security提供4种会话固定攻击保护策略:
- changeSessionId:不创建一个新的会话。相反,使用 Servlet 容器提供的会话固定保护(HttpServletRequest#changeSessionId())修改会话的session-id并返回给客户端。这在servlet3.1和较新的容器中是默认的。
- newSession:创建一个新的干净的会话,不复制原会话的属性到新会话,但会复制安全上下文信息
- migrateSession:创建一个新的会话,同时复制原会话属性到新会话。
在HttpSecurity快速配置
//newSession
httpSecurity.sessionManagement().sessionFixation().newSession();
//migrateSession
httpSecurity.sessionManagement().sessionFixation().migrateSession()
//changeSessionId
httpSecurity.sessionManagement().sessionFixation().changeSessionId();通过注册bean向HttpSecurity配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class HttpSecurityConfiguration {
@Autowired
private ApplicationContext applicationContext;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
//...
httpSecurity.sessionManagement().sessionAuthenticationStrategy(sessionAuthenticationStrategy());
//...
DefaultSecurityFilterChain securityFilterChain = httpSecurity.build();
return securityFilterChain;
}
/*
会话保护攻击保护策略
*/
@Bean
public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
ChangeSessionIdAuthenticationStrategy changeSessionIdAuthenticationStrategy = new ChangeSessionIdAuthenticationStrategy();
//添加事件发布器后更新sessionid会发送一个事件
changeSessionIdAuthenticationStrategy.setApplicationEventPublisher(applicationContext);
ArrayList<SessionAuthenticationStrategy> sessionAuthenticationStrategyArrayList = new ArrayList<>();
sessionAuthenticationStrategyArrayList.add(changeSessionIdAuthenticationStrategy);
CompositeSessionAuthenticationStrategy compositeSessionAuthenticationStrategy = new CompositeSessionAuthenticationStrategy(sessionAuthenticationStrategyArrayList);
return compositeSessionAuthenticationStrategy;
}
}Remeber-me功能
注销
相关类、接口
LogoutFilter、LogoutConfigurer、LogoutHandler、SecurityContextLogoutHandler、DefaultLogoutPageGeneratingFilter
自定义注销uri
LogoutFilter在过滤链中出现在AutheorizationFilter之前,所以默认情况下不需要明确允许/logout端点。\
http
.logout((logout) -> logout.logoutUrl("/my/logout/uri"))添加注释时的清理动作
可以添加配置,在logout时执行自定义的清理动作。
CookieClearingLogoutHandler cookies = new CookieClearingLogoutHandler("our-custom-cookie");
http
.logout((logout) -> logout.addLogoutHandler(cookies))认证事件
对于每个认证的成功或失败,分别触发一个 AuthenticationSuccessEvent 或 AuthenticationFailureEvent 事件。
要监听这些事件,需要向容器中注入一个事件发布者AuthenticationEventPublisher,默认实现是DefaultAuthenticationEventPublisher。
@Bean
public AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher){
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}然后通过Spring的@EventListener监听事件。
@Component
public class AuthenticaitionEvent {
@EventListener
public void onSuccess(AuthenticationSuccessEvent authenticationSuccessEvent) {
System.out.println("登录成功" +" "+ authenticationSuccessEvent);
}
public void onFailuer(AbstractAuthenticationFailureEvent abstractAuthenticationFailureEvent) {
System.out.println("登录失败" +" "+ abstractAuthenticationFailureEvent);
}
}Spring Security的授权(Authorization)
- AuthorizationManager(权限管理器)
- RequestMatcherDelegatingAuthorizationManager (AuthorizationManager的委托代理实现,实现对请求进行匹配,找到最适合的权限管理器进行权限验证)
- 权限管理器相关实现类

- GrantedAuthority(权限表,返回String形式)
- SimpleGrantedAuthority(GrantedAuthority的简单实现)
- AccessDecisionManager(访问决策,已弃用)
- AccessDecisionVoter(访问决策,已弃用)
- AuthorizationDecision(访问决策)
- GrantedAuthorityDefaults 角色编码默认前缀
ROLE_ - RoleVoter
- RoleHierarchyVoter
- RoleHierarchy(角色层次)
- RoleHierarchyImpl
@Bean
static RoleHierarchy roleHierarchy() {
return new RoleHierarchyImpl("ROLE_ADMIN > permission:read");
}- security的默认授权检验AuthorizationFilter不仅在每个request上运行,而且在每个dispatche上运行
- 基于过滤器AuthorizationFilter,是过滤器链的最后一个。当httpSecurity使用authorizeHttpRequests时,使用AuthorizationFilter。
- 以前版本中默认使用的是拦截器FilterSecurityInterceptor。当httpSecurity使用authorizeRequests时,使用FilterSecurityInterceptor。
- 路径匹配
authorize.requestMatchers("/endpoint").hasAuthority('USER') - 使用ant模糊匹配路径
authorize.requestMatchers("/resource/**").hasAuthority("USER") - 使用ant提取路径中的值
authorize.requestMatchers("/resource/{name}").access(new WebExpressionAuthorizationManager("#name == authentication.name")) - 正则表达式匹配
authorize..requestMatchers(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+")).hasAuthority("USER") - Http方法匹配
authorize.requestMatchers(HttpMethod.GET).hasAuthority("read") - 按 Dispatcher 类型进行匹配,放行FORWARD和ERROR
authorize.authorizedispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll() - 自定义RequestMatcher
RequestMatcher printview = (request) -> request.getParameter("print") != null; - RequestMatcher
- MvcRequestMatcher
- AntRequestMatcher
- Spring Security可以配置请求级别和方法级别的授权验证。
DSL内置的授权请求规则
- SecurityExpressionRoot(Spring Security适用的Spring-EL表达式)
- WebSecurityExpressionRoot
- permitAll:该请求不需要授权,是一个公共端点。需要注意的是,在这种情况下,永远不会从session中检索Authentication。
- denyAll:该请求在任务情况下都是不允许访问。需要注意的是,在这种情况下,永远不会从session中检索Authentication。
- hasAuthority:要求请求的Authentication有一个符合给定值的GrantedAuthority
- hasRole:hasAuthority的一个快捷方式,其前缀为
ROLE_或其他配置的默认前缀。 - hasAnyAuthority:要求请求的Authentication有任意一个符合给定值的GrantedAuthority
- hasAnyRole:是hasAnyAuthority的一个快捷方式,其前缀为
ROLE_或其他配置的默认前缀。 - hasPermission:用于对象级授权的 PermissionEvaluator 实例的钩子。
- access:该请求使用这个自定义的 AuthorizationManager 来确定访问权限。
- authentication:与此方法调用相关的Authentication实例。
- principal:与此方法调用相关的Authentication#getPrincipal。
使用请求参数验证授权
在Spring Security可以通过@P注解参数,在spel表达式中使用#标识参数,使用参数完成授权验证。
@PreAuthorize("#name==authentication.name")
public String p6Method(@P("name") String name) {
System.out.println(SecurityContextHolder.getContext().getAuthentication().getName());
return "p6";
}这里会验证name参数与authentication的认证用户名是否相同。相同时可以执行该方法,不相同时就会抛出权限不足。
方法级别安全授权管理架构图

@EnableMethodSecurity激活方法级别授权,security-starter默认不激活方法级授权@PreAuthorize方法执行前校验@PostAuthorize方法执行后检验@PreFilter方法执行前对数组、集合、Map和stream的入参进行过滤校验,通过才可访问。@PostFilter方法执行后对返回的数组、集合、map和stream结果进行过滤校验,通过才可访问。@Secured,其与@PreAuthorize功能相似,但更推荐使用@PreAuthorize。@Secured需要设置@EnableMethodSecurity(securedEnable=true)才能使用。@EnableMethodSecurity(jsr250Enabled = true)启用jsr-250注解,包含@RolesAllowed、@PermitAll、@DenyAll注解。- 注解是基于SpringAOP的拦截器
- 多个方法安全注解串联时,所有方法安全注解都通过,才能对调用授权。
- 不支持重复使用相同的注解
- 这些注解可以注解在方法、类、接口、元注解上。
- 如果既在类上和方法上添加了相同的注解,那么方法上的注解会覆盖类上的注解。
- 如果一个类从两个不同的接口继承了注解,会启动失败。
自定义启用你想使用注解
- 禁用Method Security的预配置。
@EnableMethodSecurity(prePostEnabled = false)- 注入你想要注解的拦截器。
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor preAuthorizeMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor postAuthorizeMethodInterceptor() {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor preFilterMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.preFilter();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor postFilterMethodInterceptor() {
return AuthorizationManagerAfterMethodInterceptor.postFilter();
}在Spel中使用自定义方法鉴权bean
- 声明一个bean,方法接受一个MethodSecurityExpressionOperations参数。
@Component("authz")
public class AuthorizationLogic {
public boolean decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
}
}- 在授权注解中使用bean
@Controller
public class MyController {
@PreAuthorize("@authz.decide(#root)")
@GetMapping("/endpoint")
public String endpoint() {
// ...
}
}自定义授权管理器AuthorizationManager授权方法
- 如要通过数据库判断用户权限或其他策略判断用户权限,可以自定义我们的授权管理器,实现AuthorizationManager接口,并将其注入到容器中。
@Component
public class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final Logger logger = LoggerFactory.getLogger(MyAuthorizationManager.class);
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
List<String> authorities = authentication.get().getAuthorities().stream().map(grantedAuthority -> grantedAuthority.getAuthority()).toList();
String requestURI = requestAuthorizationContext.getRequest().getRequestURI();
String method = requestAuthorizationContext.getRequest().getMethod();
String resourceStr = requestURI + ":" + method;
boolean contains = authorities.contains(resourceStr);
return new AuthorizationDecision(contains);
}
}将请求的uri和请求方法拼接起来(如:/api/interface1:GET),与后台的接口权限信息比对,如有权限则返回一个已授权的AuthorizationDecision对象。
2. 发布方法拦截器,并在其上添加一个你希望使用的授权管理器,参数传入自定义的授权管理器MyAuthorizationManager,以替换@PreAuthorize和@PostAuthorize的工作方式。
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(MyAuthorizationManager manager) {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(MyAuthorizationManager manager) {
return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
}
}- 在spring security安全配置类上,针对特定路径,使用access引用自定义的AuthorizationManager授权管理器。
这里针对所有请求,配置的自定义的授权管理器。
使用AspectJ授权方法
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)添加该注解,Spring Security将以AspectJ advice的形式发布其advisor,以便相应地织入他们。
指定授权管理运行的顺序
要想在Spring Security前启动事务,可以修改事务的顺序,以便在授权管理运行抛出AccessDeniedException时,事务可以回滚。
@EnableTransactionManagement(order = 0)用安全方法替代
@EnableGloablMethodSecurity已被弃用,应使用@EnableMethodSecurity。新注解默认激活Spring的pre-post注解,并在内部使用AuthorizationManager。
@EnableGlobalMethodSecurity(prePostEnabled = true)
//等价于
@EnabelMethodSecurity@EnableGlobalMethodSecurity(securedEnabled = true)
//等价于
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)域对象安全
spring security的acl服务是在spring-security-acl-xxx.jar中提供的。spring security的域对象安全功能是基于访问控制列表(ACL)实现的,它允许开发者为应用程序中的领域对象或实体定义细粒度的访问控制策略,以便只有特定用户能够执行查询、更新、删除等操作。
spring security acl的主要功能
spring security acl主要提供以下三个功能:
- 提供一种有效检索所有域对象的ACL条目的方法(以及修改这些ACL)。
- 在方法被调用之前,确保某个委托人(principal)被允许使用你的对象的一种方法。(有点类似于@PreAuthorize)
- 一种确保特定委托人(principal)在方法被调用后被允许处理你的对象(或它们返回的东西)的方式。(有点类似于@PostAuthorize)
spring securit acl基于数据库运行
数据库是spring security acl模块运行的核心,它默认依赖四张主要表:
- ACL_SID:唯一地识别系统中的任何委托人或授权,即资源使用主体。
- ACL_CLASS:唯一地识别系统中的任何领域对象类,即资源类型。
- ACL_OBJECT_IDENTITY:存储每个独特的域对象实例的信息,即资源实体。
- ACL_ENTRY:存储分配给每个接受者的个人权限,通过Integer bit掩码存储权限,即主体对实体的权限。
spring security acl的主要接口
spring security中使用的主要接口:
- ACL
- AccessControlEntry
- Permission
- Sid
- ObjectIdentity
- AclService
- MutableAclService
入门例子
// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;
// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}
// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);spring security没有提供其他特殊的集成来增删改查acl信息,因此要集成acl到系统中,需要手工编写类似于上述的代码。自定义 AccessDecisionVoter 或 AfterInvocationProvider以在方法调用前后校验权限。
授权事件
- 对于每个被授予的授权,都会触发一个
AuthorizationGrantedEvent事件。 - 对于每个被拒绝的授权,都会触发一个
AuthorizationDeniedEvent事件。
你可以实现并注入一个AuthorizationEventPublisher来监听这些事件。
也可以使用SpringAuthorizationEventPublisher进行发布,它会通过spring的ApplicaitonEventPublisher来发布授权事件,然后可以通过@EventListener来监听事件。
@Bean
public AuthorizationEventPublisher authorizationEventPublisher
(ApplicationEventPublisher applicationEventPublisher) {
return new SpringAuthorizationEventPublisher(applicationEventPublisher);
}
@Component
public class AuthenticationEvents {
@EventListener
public void onFailure(AuthorizationDeniedEvent failure) {
// ...
}
}因为AuthorizationEventPublisher事件会相当嘈杂,默认是不被发布的,所以你也可以自定义实现一个AuthorizationEventPublisher过滤想要发布的事件。
Spring Security OAuth2

OAuth2登录
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>httpSecurity下的oauth2配置
- 当使用
http.oauth2Login()配置时,会引入OAuth2LoginAuthenticationFilter,它会连续访问acc_token&user-info-url,获取用户信息,填充Authentication - 当使用
http.oauth2Client()配置时,会引入OAuth2AuthorizationCodeGrantFilter,它只负责获取access_token并用repository存起来,不会进行认证,认证处理要自行实现。
应用配置文件的核心配置
spring.security.oauth2.client.registration
oauth2客户端登记信息
| 配置项 | 解析 |
|---|---|
| client-id | 在提供商后台配置的客户端id |
| client-secret | 在提供商后台获取到的客户端密码 |
| client-authentication-method | 用于验证客户和提供者的方法。支持的值是 client_secret_basic、client_secret_post、 private_key_jwt、client_secret_jwt 和 none。 |
| authorization-grant-type | 客户端授权类型。OAuth 2.0授权框架定义了 四种授权许可 类型。支持的值是 authorization_code、client_credentials、password,以及扩展许可类型 urn:ietf:params:oauth:grant-type:jwt-bearer。 |
| redirect-uri | 客户端注册的重定向uri。授权服务器在终端用户认证和授权客户端后,将终端用户的用户代理重定向到此uri。 |
| scope | 域。客户端在授权请求流程中要求的范围。 |
| client-name | 用于客户端的描述性名称,OAuth2.0登录页面显示的第三方登录的名称 |
spring-security.oauth2.client.provider
Oauth2授权提供者
| 配置项 | 解析 |
|---|---|
| authorization-uri | 授权服务提供者的授权端点uri |
| token-uri | 授权服务提供者的令牌(token)端点uri |
| jwk-set-uri | 用于从授权服务器获取 JSON Web Key (JWK) 集的URI,其中包含用于验证ID令牌的 JSON Web Signature (JWS) 和(可选)用户信息响应的加密密钥。 |
| issuer-uri | 返回 OpenID Connect 1.0 提供者或 OAuth 2.0 授权服务器的签发者标识符URI。 |
| user-info-uri | UserInfo 端点 URI,用于访问认证的最终用户的claim和属性 |
| user-info-authentication-method | 向 UserInfo 端点发送访问令牌时使用的认证方法。支持的值是 header、form 和 query。 |
| user-name-attribute | UserInfo 响应中返回的引用最终用户的名称或标识符的属性的名称。 |
可以通过发现 OpenID Connect Provider 的 配置端点 或授权服务器的 元数据端点 来初始配置 ClientRegistration。
javaClientRegistration clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build();
通过代码配置OAuth2客户端,覆盖Spring Boot2.x的自动配置
将会涉及到这些类:
- CommonOAuth2Provider:是一个枚举类,它为一些知名的供应商预先定义了一套默认的客户端属性。对于google,只需要在注册信息配置client-id和client-secret即可。
- ClientRegistrationRepository:OAuth2客户端注册库,它是一个接口,接口中有一个方法,该方法接受一个registrationId,返回一个ClientRegistration对象。
- ClientRegistration:OAuth2客户端注册的表示
- SecurityFilterChain:安全过滤器链
配置步骤:
- 注册一个
ClientRegistrationRepository的Bean,返回ClientRegistration。 - 注册
SecurityFilterChain的Bean,使用http.oauth2Login(withDefaults());开启OAuth2协议。
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
return http.build();
}
}高级配置
自定义登录页面
OAuth2.0的登陆页面默认由DefaultLoginPageGeneratingFilter自动生成,默认登录页面会显示每个配置的OAuth客户端,其ClientRegistation.clientName为链接,能够发起授权请求。
每个OAuth客户端链接的目的地址默认:
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"其中OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI的值为:/oauth2/authorization。
假如registrationId为google,则目标地址为**/oauth2/authorization/google**。
处理认证请求地址也可以通过authorizationEndpointConfig.baseuri配置,并且需要提供一个带有@RequestMapping("[自定义地址]") 的 @Controller,能够渲染自定义的登录页面。
自定义重定向端点
重定向端点是指,用户授权后,OAuth接受授权码的地址。
默认的授权响应baseuri是/login/oauth2/code/*,这在OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI中定义。
要想自定义重定向端点可以通过redirectionEndpointConfig.baseuri配置。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.redirectionEndpoint(redirection -> redirection
.baseUri("/login/oauth2/callback/*")
...
)
);
return http.build();
}并且确保ClientRegistration.redirectUri 与自定义授权响应 baseUri 相匹配。
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
.build();映射用户权限
用户成功通过OAuth2提供商的认证后,OAuth2User.getAuthorities()可以获得一个由OAuth2UserRequest.getAccessToken().getScopes()填充的授予权限列表,其前缀为SCOPE_。这些授权可用被映射到一组新的GrantedAuthority中,在完成认证时提供给OAuth2AuthenticationToken。
有两种方式定义用户权限映射:
- 通过自定义
GrantedAuthoritiesMapper并注入容器提供一个映射关系。 - 基于Delegation的策略与OAuth2UserService
OAuth2.0 UserService
OAuth2UserService是一个接口,接口定义了一个获取OAuth2User对象的方法。
DefaultOAuth2UserService是OAuth2UserService接口的默认实现,通过restTemplate发起网络请求,从资源服务器获取用户信息。
OpenID Connect 1.0 UserService
OidcUserService是一个OAuth2UserService 的实现,支持OpenID Connect 1.0提供者。
OpenID Connect 1.0 和OAuth 2.0有什么区别?
OpenID Connect 1.0和OAuth 2.0都是用于安全认证和授权的协议,但它们在功能和实现上存在一些区别。
- 功能:OpenID Connect 1.0基于OAuth 2.0协议之上,是一个更简单的身份层,它允许客户端根据授权服务器的认证结果最终确认终端用户的身份,以及获取基本的用户信息。此外,它还支持所有类型的客户端,包括Web、移动、JavaScript等。而OAuth 2.0主要用于授权(Authorization),它为客户端提供了安全的方式来访问受保护的资源,而不需要分享其访问令牌。
- 实现:OpenID Connect 1.0可扩展,允许使用某些可选功能,如身份数据加密、OpenID提供商发现、会话管理等。这些功能在OAuth 2.0中并未直接提供,需要额外进行扩展和实现。
总结来说,OpenID Connect 1.0更注重身份认证和用户信息获取,而OAuth 2.0更注重授权和资源访问控制。
ID Token 签名验证
OpenID Connect 1.0认证引入了 ID Token,它是一个安全令牌,包含授权服务器在客户端使用时对终端用户进行认证的要求。
ID令牌被表示为 JSON Web Token(JWT),并且必须使用 JSON Web Signature (JWS)进行签名。
OidcIdTokenDecoderFactory 提供一个用于 OidcIdToken 签名验证的 JwtDecoder
OpenID Connect 1.0 注销
自定义OidcClientInitiatedLogoutSuccessHandler实现向RP发起注销操作。
OAuth2客户端
核心的接口和类
- ClientRegistration:是一个在 OAuth 2.0 或 OpenID Connect 1.0 提供商(Provider)处注册的客户端的表示。它保存了客户端id、客户端secret、授权类型、重定向uri、scope、客户端name、提供商明细信息等信息。
- ClientRegistrationRepository:作为OAuth 2.0 / OpenID Connect 1.0 ClientRegistration 的存储库(repository)。
- InMemoryClientRegistrationRepository:
ClientRegistrationRepository的默认实现 - OAuth2AuthorizedClient:
OAuth2AuthorizedClient是一个授权客户端的代表。当终端用户(资源所有者)已经授权给客户端访问其受保护的资源时,客户端就被认为是被授权的。其将OAuth2AccessToken、OAuth2RefreshToken、ClientRegistration和资源所有者关联在一起。 - OAuth2AuthorizedClientRepository:负责在网络请求之间持久保存
OAuth2AuthorizedClient。 - OAuth2AuthorizedClientService:主要作用是在应用层面管理
OAuth2AuthorizedClient - InMemoryOAuth2AuthorizedClientService:
OAuth2AuthorizedClientService的默认实现 - JdbcOAuth2AuthorizedClientService:
OAuth2AuthorizedClientService的实现,实现持久化OAuth2AuthorizedClient到数据库中。 - OAuth2AuthorizedClientManager:
OAuth2AuthorizedClientManager负责OAuth2AuthorizedClient的整体管理。 - DefaultOAuth2AuthorizedClientManager:是
OAuth2AuthorizedClientManager的默认实现,它包括以下功能:- 使用
OAuth2AuthorizedClientProvider授权一个OAuth2.0客户端 - 使用
OAuth2AuthorizedClientService或OAuth2AuthorizedClientRepository持久化OAuth2AuthorizedClient - 授权成功时,委托给
OAuth2AuthorizationSuccessHandler进行处理。 - 授权失败时,委托给
OAuth2AuthorizationFailureHandler进行处理。
- 使用
- AuthorizedClientServiceOAuth2AuthorizedClientManager:它是OAuth2AuthorizedClientManager的一个实现,服务应用是一个常见的用例。服务应用程序通常在后台运行,没有任何用户互动,并且通常在系统级账户而不是用户账户下运行
- DefaultOAuth2AuthorizedClientManager:它是OAuth2AuthorizedClientManager的一个实现,使用OAuth2AuthorizedClientRepository保存OAuth2AuthorizedClient信息。
- OAuth2AuthorizedClientProvider:实现了授权(或重新授权)OAuth 2.0客户端的策略。通常实现一个授权许可类型,如 authorization_code、 client_credentials 等。接口中的authorize方法,返回一个已授权的OAuth2客户端。
- AuthorizationCodeOAuth2AuthorizedClientProvider:是
OAuth2AuthorizedClientProvider的一个实现,用于授权码认证。 - ClientCredentialsOAuth2AuthorizedClientProvider:是OAuth2AuthorizedClientProvider的一个实现,用于证书式认证。
- OAuth2AuthorizationRequestRedirectFilter:授权码模式过滤器
- OAuth2AuthorizationRequest:定义请求授权的uri和请求参数
- OAuth2AccessTokenResponseClient:它是一个接口,接口中有一个方法,接受一个授权请求对象,返回访问令牌响应,用于从授权服务端获得令牌。
- DefaultAuthorizationCodeTokenResponseClient:它是
OAuth2AccessTokenResponseClient的一个实现,实现通过RestOperations从授权服务器的Token端点将授权码转换为Access Token。 - DefaultRefreshTokenTokenResponseClient:
OAuth2AccessTokenResponseClient的一个实现,使用RestOperations从授权服务器的令牌端点刷新Access Token。
OAuth2AuthorizedClientProvider的认证流程
- 通过OAuth2AuthorizeRequest.withClientRegistrationId("okta")创建一个OAuth2AuthorizeRequest对象
- DefaultOAuth2AuthorizedClientManager接收OAuth2AuthorizeRequest,从OAuth2AuthorizeRequest中获取信息创建出OAuth2AuthorizationContext对象
- DefaultOAuth2AuthorizedClientManager使用OAuth2AuthorizedClientProvider对OAuth2AuthorizationContext进行认证
- DelegatingOAuth2AuthorizedClientProvider是OAuth2AuthorizedClientProvider的实现类,它会遍历多个OAuth2AuthorizedClientProvider对OAuth2AuthorizationContext进行认证。
- OAuth2AuthorizedClientProvider认证前会先检查grant_type自己支不支持再进行认证操作。
- 假如是PasswordOAuth2AuthorizedClientProvider,它会先从OAuth2AuthorizationContext中获取到用户、密码信息。
- PasswordOAuth2AuthorizedClientProvider将获得的账号、密码和clientRegistration组合成一个OAuth2PasswordGrantRequest请求对象
- 之后PasswordOAuth2AuthorizedClientProvider将OAuth2PasswordGrantRequest对现象发送给DefaultPasswordTokenResponseClient(实现了OAuth2AccessTokenResponseClient接口)
- DefaultPasswordTokenResponseClient通过RestTemplate从授权服务器获取到访问令牌信息,返回一个OAuth2AccessTokenResponse对象
- DefaultOAuth2AuthorizedClientManager从OAuth2AccessTokenResponse中获取令牌信息,创建一个新的OAuth2AuthorizedClient对象并返回。
uri模板变量
DefaultOAuth2AuthorizationRequestResolver通过使用UriComponentsBuilder支持uri模板变量用于redirect-uri。
spring:
security:
oauth2:
client:
registration:
okta:
...
redirect-uri: "{baseScheme}://{baseHost}{basePort}{basePath}/authorized/{registrationId}"
...其中,{baseUrl} 可解析为{baseScheme}://{baseHost}{basePort}{basePath}
获取访问令牌的方式
可以通过四种方式获取AccessToken:
- 授权代码授予->访问令牌
- 客户端凭证授予
- 资源所有者的密码授予
- JWT Bearer
授权代码->访问令牌
- OAuth2AuthorizationRequestRedirectFilter使用OAuth2AuthorizationRequestResolver来处理HttpServletRequest,并通过将最终用户的用户代理重定向到授权服务器的授权端点来启动授权码授予流程。
- DefaultOAuth2AuthorizationRequestResolver是OAuth2AuthorizationRequestResolver的默认实现,它在默认路径
/oauth2/authorization/{registrationId}上进行匹配,提取registrationId,并使用它来创建关联了ClientRegistration的 OAuth2AuthorizationRequest,之后用户请求重定向到授权服务器的授权端点。 - 用户授权后,授权服务器携带授权码使浏览器重定向到redirect_url,默认为
{baseUrl}/{action}/oauth2/code/{registrationId},服务器获取到授权代码后,通过授权代码交换为Access Token。获取访问令牌OAuth2AccessTokenResponseClient的默认实现是DefaultAuthorizationCodeTokenResponseClient,它使用RestOperations实例在授权服务器的Token端点将授权代码交换为Access Token - 刷新令牌授予的OAuth2AccessTokenResponseClient的默认实现是 DefaultRefreshTokenTokenResponseClient,它使用RestOperations实例在授权服务器的Token端点刷新Access Token。
HttpServletRequest->OAuth2AuthorizationRequestRedirectFilter->DefaultOAuth2AuthorizationRequestResolver->OAuth2AuthorizationRequest->sendRedirect->HttpServletRequest->OAuth2AuthorizeRequest->OAuth2AuthorizationContext->DefaultOAuth2AuthorizedClientManager->AuthorizationCodeOAuth2AuthorizedClientProvider->OAuth2AuthorizationCodeGrantRequest->DefaultAuthorizationCodeTokenResponseClient->OAuth2AccessTokenResponseClient->OAuth2AuthorizedClient
客户端凭证授予
客户端凭证OAuth2AccessTokenResponseClient的默认实现是DefaultClientCredentialsTokenResponseClient,它适用restOperations从授权服务器的令牌端点请求获得Access Token。
资源所有者密码授予
资源所有者密码授予访问令牌OAuth2AccessTokenResponseClient的默认实现是DefaultPasswordTokenResponseClient,它使用restOperations从授权服务器的令牌端点请求获Access Token。
JWT Bearer授予
JWT Bearer授予OAuth2AccessTokenResponseClient的默认实现是DefaultJwtBearerTokenResponseClient,它使用restOperations从授权服务器的令牌端点请求获Access Token。
Oauth2 客户端认证
Oauth2 客户端授权
解析授权客户端
@RegisteredOAuth2AuthorizedClient注解提供了将方法参数解析为OAuth2AuthorizedClient类型的参数值的能力。与使用 OAuth2AuthorizedClientManager或OAuth2AuthorizedClientService来访问OAuth2AuthorizedClient相比,这是一个方便的选择。
@Controller
public class OAuth2ClientController {
@GetMapping("/")
public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}为Servlet环境整合WebClient
提供授权客户端
默认授权客户端
OAuth2资源服务器 Bearer Token
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>Spring Security支持通过使用两种形式的OAuth 2.0 Bearer Token 来保护端点。
- JWT
- Opaque (不透明)Token
JWT形式
认证过程
BearerTokenAuthenticationFilter从请求头解析出Bearer Token,如果获取不到则继续执行过滤器链的流程。如果获取到,则把Token将其转换为BearerTokenAuthenticationToken- 之后将
BearerTokenAuthenticationToken发送给ProviderManager,ProviderManager使用JwtAuthenticationProvider对BearerTokenAuthenticationToken进行认证 - 认证过程使用
JwtDecoder对jwt进行解码、验证、确认,具体实现类为NimbusJwtDecoder。 - NimbusJwtDecoder先对token进行解析,获取header部分的加密算法信息,之后生成对应类型的JWT(PlainJWT、SignedJWT、EncryptedJWT),之后再使用JWTProcessor对JWT对象进行签名验证,验证成功后获取到claim部分信息。
- 使用claim信息、header信息、token组成一个spring security的JWT对象。
- 之后使用OAuth2TokenValidator类,对jwt对象的时间、签发人等信息进行验证
- 以上验证通过后,返回spring security的JWT对象。
- 之后使用
JwtAuthenticationConverter将jwt转换为有认证用户信息、授权权限的集合的JwtAuthenticationToken。 JwtAuthenticationToken类型的Authentication,最终被BearerTokenAuthenticationFilter设置在SecurityContextHolder上,就完成token认证了。
核心接口和类
- BearerTokenAuthenticationFilter:过滤请求,获取Bearer Toker,将Token发送
ProviderManager进行验证,最后验证获得Authentication。 - BearerTokenAuthenticationToken:封装Bearer Token信息,接受token值。
- ProviderManager:认证管理器
- JwtAuthenticationProvider:JWT认证提供者,将BearerTokenAuthenticationToken转换为JwtAuthenticationToken。
- JwtDecoder:对jwt进行解码、验证、确认
- JwtAuthenticationConverter:将jwt转换为授权权限的集合
- JwtAuthenticationToken:认证通过的JWT
- JWT:spring security封装的jwt对象,Map形式存储header和claimSet信息。
- SecurityContextHolder:安全上下文
- OAuth2TokenValidator:验证JWT,默认验证iss、exp、nbf。
- DelegatingOAuth2TokenValidator:
OAuth2TokenValidator的代理实现,实现对多个OAuth2TokenValidator的管理和调用。 - MappedJwtClaimSetConverter:实现对Claim值与Java类型的映射。
默认的OAuth2 JWT资源服务器配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}Opaque Token形式
自定义Bearer Tokens的解析
自定义一个DefaultBearerTokenResolver ,并将其注入容器中,可以实现从其他请求头字段中或从Form中获取Beared Token。
Bearer Token的传播
WebClient支持
使用ServletBearerExchangeFilterFunction并添加WebClient中的filter中,可以在使用WebClient调用下游服务时,从Authentication提取AbstractOAuth2Token凭证。
RestTemplate支持
ServletBearerExchangeFilterFunction不直接支持RestTemplate,但可以在RestTemplate中添加拦截器实现在请求头添加Bearer Token。
Bearer Token失效
当验证到Token失效时,资源服务器会抛出一个InvalidBearerTokenException异常,也会发布一个AuthenticationFailureBadCredentialsEvent事件。
Spring Security SAML2
Spring Security 漏洞防护
跨站请求伪造(CSRF)
HTTP安全响应头
HTTP
Http防火墙
- 调用第三方认证接口,提交client-id、client-authentication-method、authorization-grant-type、redirect-uri、scope、client-name
SpringSecurity的测试方法
测试配置
要使用Spring Security的测试之前,需要添加一些配置。
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class WithMockUserTests {
// ...
}@ExtendWith(SpringExtension.class):指示 spring-test 模块应该创建一个 ApplicationContext。@ContextConfiguration指示 spring-test 使用何种配置来创建 ApplicationContext。由于没有指定配置,将尝试默认的配置位置。这与使用现有的Spring Test支持没有区别
@WithMockUser
使用@WithMockUser模拟已授权的用户,这会创建一个已授权的Authenticaiton。
//授权用户信息
@WithMockUser(username="owner")
@Test
void readAccountWhenOwnedThenReturns() {
Account account = this.bankService.readAccount("12345678");
// ... assertions
}@WithMockUser有很多属性供我们去配置
- username:用户名
- roles:角色数组,后台会加上
ROLE_前缀 - authorities:授权数组
@WithAnonymousUser
@WithAnonymousUser该注解将允许以匿名用户身份运行。
@WithUserDetails
自定义一个UserDetails去运行测试
@WithSecurityContext
最灵活的选项,自定义一个SecurityContext。
微服务安全控制思路
- 使用OAuth2协议生成jwt令牌,实现无状态服务
- 使用对称加密或非对称加密对jwt命令签名
- 网关进行认证,微服务鉴权
- 网关对令牌认证通过后,传递令牌到微服务
- 统一认证请求的参数,将认证dto转json串传入请求的username属性
- 重写DaoAuthenticationProvider,从authentic中获取username属性,转成认证dto,从认证dto中获取认证信息,根据不同的传参,使用不同userdetailservice实现类,获取用户信息,或认证。
- 拓展UserDetailService,在UserDetailService中,访问数据库,获取用户信息,将用户信息转json串,填充到UserDetail的username属性中,最后返回userdetail对象。
认证过程
- 首先请求会经过AbstractAuthenticationProcessingFilter

- AbstractAuthenticationProcessingFilter根据请求路径判断是否对请求进行拦截,UsernamePasswordAuthenticationFilter对向
/login发送的POST请求进行拦截。
- 之后过滤器会调用attemptAuthentication()方法执行具体的认证逻辑,该方法为抽象方法,需要在子类内执行。
- UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter的实现类,实现了attemptAuthentication()方法

- 在UsernamePasswordAuthenticationFilter的attemptAuthentication()方法中,会获取请求参数username和password的值,生成一个未认证的Authentication对象(这里是UsernamePasswordAuthenticationToken对象)传送给ProviderManager,调用它的authenticate()方法,去完成认证过程,方法会返回认证成功的Authenticaition对象,或返回表示认证失败的null。
- ProviderManager可以包含多个AuthenticationProvider,使用AuthenticationProvider对请求认证前,先执行suppoerts()方法,判断是否支持验证,若支持再执行具体的认证过程authenticate()

- ProviderManager提交UsernamePasswordAuthenticationToken对象给DaoAuthenticationProvider,DaoAuthenticationProvider继承自
AuthenticationProvider,实现了authenticate()方法。 - DaoAuthenticationProvider的support()方法,会检查Authenticaition对象是否为UsernamePasswordAuthenticationToken类或其子类的对象。

- AuthenticationProvider认证通过后返回一个已认证的Authentication,认证不通过返回空。这个Authenticaiton会一直往外抛,直到AbstractAuthenticationProcessingFilter。
- AbstractAuthenticationProcessingFilter判断返回的Authentication的状态,执行认证通过逻辑和认证不通过逻辑

- 调用SessionAuthenticationStrategy.onAuthentication(),执行会话固定攻击保护的配置
- 执行successfulAuthentication处理认证成功的逻辑。

this.securityContextHolderStrategy.setContext(context);将安全上下文保存到SecurityContextHolder中this.securityContextRepository.saveContext(context, request, response);将安全上下文与请求或会话绑定this.successHandler.onAuthenticationSuccess(request, response, authResult);执行AuthenticationSuccessHandler的逻辑,一般为重定向到某个端点。
实现自定义的登录方式
要自定义登录方式可以参考UsernamePasswordAuthenticationFilter
- 参考UsernamePasswordAuthenticationFilter,自定义一个继承AbstractAuthenticationProcessingFilter的过滤器,实现里面的attemptAuthentication()方法。方法中获取请求参数,生成未认证的AbstractAuthenticationToken对象,传给ProviderManager执行认证过程
- 参考UsernamePasswordAuthenticationToken,自定义一个继承AbstractAuthenticationToken的Authenticaiton类
- 参考AbstractUserDetailsAuthenticationProvider和DaoAuthenticationProvider,自定义一个AuthenticationProvider对象,实现其中的authenticate()和support()对象
- 参考InMemoryUserDetailsManager实现一个服务类,获取用户信息,进行验证。
简单点讲就是:
- 过滤器拦截指定登录路径的请求,获取请求的参数,生成一个状态为未认证的Token
- 然后将Token传给ProviderManager,ProviderManager再转发给AuthenticationProvider去完成认证
- AuthenticationProvider再调用UserDetailService获取用户信息、验证用户密码,认证成功后,返回一个已认证的Token或抛出认证失败的异常。
- 返回的已认证的Authentication对象,里面要用认证状态、用户身份表示、权限信息、身份验证信息(继承CredentialsContainer会自动擦除)
- 最后AbstractAuthenticationProcessingFilter将这个已认证的Authenticaiton对象放在SecurityContextHolder中,放在SecurityContextRepository中。
登录认证过程有一些组件是可以注入到容器中公用起来的。
- PasswordEncoder:定义密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}- SecurityContextRepository:定义Authenticaiton与会话、请求的映射关系的保存策略
@Bean
public SecurityContextRepository securityContextRepository() {
//保持会话安全上下文一致
HttpSessionSecurityContextRepository httpSessionSecurityContextRepository = new HttpSessionSecurityContextRepository();
//保持请求安全上下文内一致
RequestAttributeSecurityContextRepository requestAttributeSecurityContextRepository = new RequestAttributeSecurityContextRepository();
ArrayList<SecurityContextRepository> securityContextRepositoryArrayList = new ArrayList<>();
securityContextRepositoryArrayList.add(httpSessionSecurityContextRepository);
securityContextRepositoryArrayList.add(requestAttributeSecurityContextRepository);
DelegatingSecurityContextRepository delegatingSecurityContextRepository = new DelegatingSecurityContextRepository(securityContextRepositoryArrayList);
return delegatingSecurityContextRepository;
}- SessionAuthenticationStrategy:定义防止会话攻击的保护策略
/*
会话保护攻击保护策略
*/
@Bean
public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
ChangeSessionIdAuthenticationStrategy changeSessionIdAuthenticationStrategy = new ChangeSessionIdAuthenticationStrategy();
//添加事件发布器后更新sessionid会发送一个事件
changeSessionIdAuthenticationStrategy.setApplicationEventPublisher(applicationContext);
ArrayList<SessionAuthenticationStrategy> sessionAuthenticationStrategyArrayList = new ArrayList<>();
sessionAuthenticationStrategyArrayList.add(changeSessionIdAuthenticationStrategy);
CompositeSessionAuthenticationStrategy compositeSessionAuthenticationStrategy = new CompositeSessionAuthenticationStrategy(sessionAuthenticationStrategyArrayList);
return compositeSessionAuthenticationStrategy;
}- AuthenticationSuccessHandler:登录成功后的处理逻辑
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler(){
SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler();
savedRequestAwareAuthenticationSuccessHandler.setDefaultTargetUrl("/loginSuccess");
return savedRequestAwareAuthenticationSuccessHandler;
}- AuthenticationManager:HttpSecurity默认情况会在build时从容器中获取AuthenticationManager对象,如果没有则创建一个ProviderManager并注入到容器中。如果注入ProviderManager,则至少需要提供一个AuthenticationProvider。