Spring Security中文文档
创始人
2024-02-18 06:43:17

Spring Security中文文档

来源:https://www.springcloud.cc/spring-security.html#overall-architecture

作者

Ben Alex, Luke Taylor, Rob Winch, Gunnar Hillert, Joe Grandja, Jay Bryant

5.1.2.RELEASE

本文档的副本可供您自己使用并分发给他人,前提是您不对此类副本收取任何费用,并且每份副本均包含本版权声明,无论是以印刷版还是电子版分发。


目录

I.前言
1. Spring Security社区
1.1.获得帮助1.2.成为参与者1.3.源代码1.4.Apache 2许可证1.5.社交媒体
2. Spring Security 5.1中的新内容
2.1.Servlet的2.2.WebFlux2.3.集成
3.获得Spring Security
3.1.发布编号3.2.用于Maven
3.2.1.Spring Boot与Maven3.2.2.没有Spring Boot的Maven3.2.3.Maven Repositories
3.3.摇篮
3.3.1.Spring Boot与Gradle3.3.2.没有Spring Boot的Gradle3.3.3.Gradle Repositories
4.项目模块
4.1.核心 - spring-security-core。罐子4.2.Remoting - spring-security-remoting。jar4.3.Web - spring-security-web。jar4.4.配置 - spring-security-config。罐子4.5.LDAP - spring-security-ldap。jar4.6.OAuth 2.0核心 - spring-security-oauth2-core.jar4.7.OAuth 2.0客户端 - spring-security-oauth2-client.jar4.8.OAuth 2.0 JOSE - spring-security-oauth2-jose.jar4.9.ACL - spring-security-acl .jar4.10.CAS - spring-security-cas .jar4.11.OpenID - spring-security-openid。jar4.12.测试 - spring-security-test.jar
5.样本申请
5.1.教程示例5.2.往来5.3.LDAP示例5.4.OpenID示例5.5.CAS样本5.6.JAAS样本5.7.预认证样本
II。Servlet应用程序
6. Java配置
6.1.Hello Web安全Java配置
6.1.1.AbstractSecurityWebApplicationInitializer6.1.2.没有现有的AbstractSecurityWebApplicationInitializer Spring6.1.3.带有Spring MVC的AbstractSecurityWebApplicationInitializer
6.2.HttpSecurity6.3.Java配置和表单登录6.4.授权请求6.5.处理注销
6.5.1.LogoutHandler6.5.2.LogoutSuccessHandler6.5.3.进一步注销相关参考
6.6.OAuth 2.0客户端
6.6.1.ClientRegistration6.6.2.ClientRegistrationRepository6.6.3.OAuth2AuthorizedClient6.6.4.OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService6.6.5.RegisteredOAuth2AuthorizedClient6.6.6.AuthorizationRequestRepository6.6.7.OAuth2AuthorizationRequestResolver6.6.8.OAuth2AccessTokenResponseClient
6.7.OAuth 2.0登录
6.7.1.Spring Boot 2.x样本
初始设置设置重定向URI配置application.yml启动应用程序
6.7.2.Spring Boot 2.x属性映射6.7.3.CommonOAuth2Provider6.7.4.配置自定义提供程序Properties6.7.5.覆盖Spring Boot 2.x自动配置
注册ClientRegistrationRepository @Bean提供WebSecurityConfigurerAdapter完全覆盖自动配置
6.7.6.没有Spring Boot 2.x的Java配置6.7.7.其他资源
6.8.OAuth 2.0资源服务器
6.8.1.依赖6.8.2.最小配置
指定授权服务器启动期望运行期望
6.8.3.指定授权服务器JWK直接设置Uri6.8.4.覆盖或替换引导自动配置
使用jwkSetUri()使用decoder()公开JwtDecoder @Bean
6.8.5.配置授权
手动提取权限
6.8.6.配置验证
自定义时间戳验证配置自定义验证程序
6.8.7.配置声明集映射
自定义单个声明的转换添加声明删除索赔重命名声明
6.8.8.配置超时
6.9.认证
6.9.1.内存中身份验证6.9.2.JDBC身份验证6.9.3.LDAP身份验证6.9.4.的AuthenticationProvider6.9.5.的UserDetailsS​​ervice
6.10.多个HttpSecurity6.11.方法安全
6.11.1.EnableGlobalMethodSecurity6.11.2.GlobalMethodSecurityConfiguration
6.12.后处理配置对象6.13.自定义DSL
7.安全命名空间配置
7.1.介绍
7.1.1.命名空间的设计
7.2.安全命名空间配置入门
7.2.1.web。xml配置7.2.2. A Minimal Configuration7.2.3.表单和基本登录选项
设置默认的登录后目的地
7.2.4.注销处理7.2.5.使用其他身份验证提供商
添加密码编码器
7.3.高级Web功能
7.3.1.记住我的身份验证7.3.2.添加HTTP / HTTPS通道安全性7.3.3.会话管理
检测超时并发会话控制会话固定攻击保护
7.3.4.OpenID支持
属性交换
7.3.5.响应标题7.3.6.添加自己的过滤器
设置自定义AuthenticationEntryPoint
7.4.方法安全
7.4.1. The Element
使用protect-pointcut添加安全性切入点
7.5.默认AccessDecisionManager
7.5.1.自定义AccessDecisionManager
7.6.身份验证管理器和命名空间
8.架构和实施
8.1.技术概述
8.1.1.运行环境8.1.2.核心组件
SecurityContextHolder,SecurityContext和Authentication ObjectsUserDetailsS​​ervice一个GrantedAuthority摘要
8.1.3.认证
什么是Spring Security中的身份验证?直接设置SecurityContextHolder内容
8.1.4.Web应用程序中的身份验证
的ExceptionTranslationFilter的AuthenticationEntryPoint认证机制在请求之间存储SecurityContext
8.1.5.Spring Security中的访问控制(授权)
安全和AOP建议安全对象和AbstractSecurityInterceptor
8.1.6.本土化
8.2.核心服务
8.2.1.AuthenticationManager,ProviderManager和AuthenticationProvider
成功验证时擦除凭据比如DaoAuthenticationProvider
8.2.2.UserDetailsS​​ervice实现
内存中身份验证JdbcDaoImpl
8.2.3.密码编码
密码历史DelegatingPasswordEncoderBCryptPasswordEncoderPbkdf2PasswordEncoderSCryptPasswordEncoder其他PasswordEncoders
8.2.4.Jackson支持
9.测试
9.1.测试方法安全性
9.1.1.安全测试设置9.1.2.@WithMockUser9.1.3.@WithAnonymousUser9.1.4.@WithUserDetails9.1.5.@WithSecurityContext9.1.6.测试元注释
9.2.Spring MVC测试集成
9.2.1.设置MockMvc和Spring Security9.2.2.SecurityMockMvcRequestPostProcessors
使用CSRF保护进行测试在Spring MVC测试中以用户身份运行测试在Spring MVC使用RequestPostProcessor进行测试时以用户身份运行测试HTTP基本身份验证
9.2.3.SecurityMockMvcRequestBuilders
测试基于表单的认证测试注销
9.2.4.SecurityMockMvcResultMatchers
未经认证的断言认证断言
10. Web应用程序安全性
10.1.安全过滤器链
10.1.1.的DelegatingFilterProxy10.1.2.的FilterChainProxy
绕过过滤链
10.1.3.过滤订购10.1.4.请求匹配和HttpFirewall10.1.5.与其他基于过滤器的框架一起使用10.1.6.高级命名空间配置
10.2.核心安全过滤器
10.2.1.FilterSecurityInterceptor10.2.2.的ExceptionTranslationFilter
的AuthenticationEntryPointAccessDeniedHandlerSavedRequest和RequestCache接口
10.2.3.SecurityContextPersistenceFilter
SecurityContextRepository
10.2.4.UsernamePasswordAuthenticationFilter
认证成功与失败的应用流程
10.3.Servlet API集成
10.3.1.Servlet 2.5+集成
HttpServletRequest.getRemoteUser()HttpServletRequest.getUserPrincipal()HttpServletRequest.isUserInRole(字符串)
10.3.2.Servlet 3+集成
HttpServletRequest.authenticate(HttpServletRequest的,HttpServletResponse的)HttpServletRequest.login(字符串,字符串)HttpServletRequest.logout()AsyncContext.start(可运行)异步Servlet支持
10.3.3.Servlet 3.1+集成
HttpServletRequest的#changeSessionId()
10.4.基本和摘要式身份验证
10.4.1.BasicAuthenticationFilter一样
组态
10.4.2.DigestAuthenticationFilter
组态
10.5.记住我的身份验证
10.5.1.概观10.5.2.简单的基于哈希的令牌方法10.5.3.持久令牌方法10.5.4.记住我的接口和实现
TokenBasedRememberMeServices对PersistentTokenBasedRememberMeServices
10.6.跨站请求伪造(CSRF)
10.6.1.CSRF攻击10.6.2.同步器令牌模式10.6.3.何时使用CSRF保护
CSRF保护和JSONCSRF和无状态浏览器应用程序
10.6.4.使用Spring Security CSRF保护
使用正确的HTTP谓词配置CSRF保护包括CSRF令牌
10.6.5.CSRF警告
超时在登录注销Multipart(文件上传)HiddenHttpMethodFilter
10.6.6.覆盖默认值
10.7.CORS10.8.安全HTTP响应标头
10.8.1.默认安全标头
缓存控制内容类型选项HTTP严格传输安全(HSTS)HTTP公钥固定(HPKP)X-框架,选项X-XSS-保护内容安全政策(CSP)推荐人政策功能政策
10.8.2.自定义标题
静态标题标题作家DelegatingRequestMatcherHeaderWriter
10.9.会话管理
10.9.1.SessionManagementFilter10.9.2.SessionAuthenticationStrategy10.9.3.并发控制
查询当前经过身份验证的用户及其会话的SessionRegistry
10.10.匿名身份验证
10.10.1.概观10.10.2.组态10.10.3.AuthenticationTrustResolver
10.11.WebSocket安全
10.11.1.WebSocket配置10.11.2.WebSocket身份验证10.11.3.WebSocket授权
WebSocket授权说明出站邮件
10.11.4.实施同源政策
为什么同源?Spring WebSocket允许来源将CSRF添加到Stomp Headers在WebSockets中禁用CSRF
10.11.5.与SockJS合作
SockJS和框架选项SockJS和放松CSRF
11.授权
11.1.授权架构
11.1.1.当局11.1.2.预调用处理
AccessDecisionManager基于投票的AccessDecisionManager实现
11.1.3.在调用处理之后11.1.4.分层角色
11.2.安全对象实现
11.2.1.AOP联盟(MethodInvocation)安全拦截器
显式MethodSecurityInterceptor配置
11.2.2.AspectJ(JoinPoint)安全拦截器
11.3.基于表达的访问控制
11.3.1.概观
常见的内置表达式
11.3.2.Web安全表达
参考Web安全表达式中的BeanWeb安全表达式中的路径变量
11.3.3.方法安全表达式
@Pre和@Post Annotations内置表达式
12.其他主题
12.1.域对象安全性(ACL)
12.1.1.概观12.1.2.关键概念12.1.3.入门
12.2.预认证方案
12.2.1.预认证框架类
AbstractPreAuthenticatedProcessingFilterPreAuthenticatedAuthenticationProviderHttp403ForbiddenEntryPoint
12.2.2.具体实施
请求标头身份验证(Siteminder)Java EE容器身份验证
12.3.LDAP身份验证
12.3.1.概观12.3.2.使用LDAP与Spring Security12.3.3.配置LDAP服务器
使用嵌入式测试服务器使用绑定身份验证加载机构
12.3.4.实现类
LdapAuthenticator实现连接到LDAP服务器LDAP搜索对象LdapAuthoritiesPopulator在Spring Bean配置LDAP属性和自定义用户详细信息
12.3.5.Active Directory身份验证
ActiveDirectoryLdapAuthenticationProvider
12.4.OAuth 2.0登录 - 高级配置
12.4.1.OAuth 2.0登录页面12.4.2.重定向端点12.4.3.UserInfo端点
映射用户权限配置自定义OAuth2UserOAuth 2.0 UserServiceOpenID Connect 1.0 UserService
13.用于Servlet环境的WebClient
13.1.WebClient OAuth2安装程序13.2.隐含的OAuth2AuthorizedClient13.3.显式OAuth2AuthorizedClient13.4.clientRegistrationId13.5.JSP标记库
13.5.1.声明Taglib13.5.2.授权标签
禁用用于测试的标记授权
13.5.3.身份验证标记13.5.4.accesscontrollist标签13.5.5.csrfInput标记13.5.6.csrfMetaTags标签
13.6.Java身份验证和授权服务(JAAS)提供程序
13.6.1.概观13.6.2.AbstractJaasAuthenticationProvider
JAAS CallbackHandlerJAAS AuthorityGranter
13.6.3.DefaultJaasAuthenticationProvider
InMemoryConfigurationDefaultJaasAuthenticationProvider示例配置
13.6.4.JaasAuthenticationProvider13.6.5.作为主题运行
13.7.CAS认证
13.7.1.概观13.7.2.CAS的工作原理
Spring Security和CAS交互序列
13.7.3.CAS客户端的配置
服务票证认证单点注销使用CAS对无状态服务进行身份验证代理票证认证
13.8.X.509身份验证
13.8.1.概观13.8.2.将X.509身份验证添加到Web应用程序13.8.3.在Tomcat中设置SSL
13.9.运行身份验证替换
13.9.1.概观13.9.2.组态
13.10.Spring Security加密模块
13.10.1.介绍13.10.2.加密机
BytesEncryptorTextEncryptor
13.10.3.关键发电机
BytesKeyGeneratorStringKeyGenerator
13.10.4.密码编码
13.11.并发支持
13.11.1.DelegatingSecurityContextRunnable13.11.2.DelegatingSecurityContextExecutor13.11.3.Spring Security并发类
13.12.Spring MVC整合
13.12.1.@EnableWebMvcSecurity13.12.2.MvcRequestMatcher13.12.3.@AuthenticationPrincipal13.12.4.Spring MVC异步集成13.12.5.Spring MVC和CSRF整合
自动令牌包含解决CsrfToken
14. Spring Data整合
14.1.Spring Data和Spring Security配置14.2.@Query中的安全表达式
15.附录
15.1.安全数据库Schema
15.1.1.用户Schema
对于Oracle数据库集团当局
15.1.2.持久登录(记住我)Schema15.1.3.ACL Schema
的HyperSQLPostgreSQL的MySQL和MariaDBMicrosoft SQL ServerOracle数据库
15.2.安全命名空间
15.2.1.Web应用程序安全性
Element Element
15.2.2.WebSocket安全
15.2.3.认证服务
15.2.4.方法安全
使用安全方法
15.2.5.LDAP命名空间选项
使用。定义LDAP服务器
15.3.Spring Security依赖性
15.3.1.spring-security-core15.3.2.spring-security-remoting15.3.3.spring-security-web15.3.4.spring-security-ldap15.3.5.spring-security-config15.3.6.spring-security-acl15.3.7.spring-security-cas15.3.8.spring-security-openid15.3.9.spring-security-taglibs
15.4.代理服务器配置15.5.Spring Security常见问题
15.5.1.一般的问题
Spring Security会照顾我的所有应用安全要求吗?为什么不使用web。xml安全性?需要什么Java和Spring Framework版本?我是Spring Security的新手,我需要构建一个支持通过HTTPS进行CAS单点登录的应用程序,同时允许在本地对某些URL进行基本身份验证,对多个后端用户信息源(LDAP和JDBC)进行身份验证。我复制了一些我找到的配置文件,但它不起作用。
15.5.2.常见问题
当我尝试登录时,收到一条错误消息“Bad Credentials”。怎么了?当我尝试登录时,我的应用程序进入“无限循环”,发生了什么?我收到一条例外消息“访问被拒绝(用户是匿名的);”。怎么了?为什么即使在我退出应用程序后仍能看到安全页面?我收到一条消息“在SecurityContext中找不到身份验证对象”的异常。怎么了?我无法使用LDAP身份验证。会话管理我正在使用Spring Security的并发会话控制来阻止用户一次登录多次。当我通过Spring Security进行身份验证时,为什么会话ID会发生变化?我正在使用Tomcat(或其他一些servlet容器)并为我的登录页面启用了HTTPS,之后又切换回HTTP。我不是在HTTP和HTTPS之间切换,但我的会话仍然迷失我正在尝试使用并发会话控制支持,但它不会让我重新登录,即使我确定我已经注销并且没有超过允许的会话。Spring Security正在创建一个会话,即使我已将其配置为不通过将create-session属性设置为never。执行POST时我得到403 Forbidden我正在使用RequestDispatcher将请求转发到另一个URL,但我的安全约束没有被应用。I have added Spring Security’s element to my application context but if I add security annotations to my Spring MVC controller beans (Struts actions etc.) then they don’t seem to have an effect.我有一个肯定已经过身份验证的用户,但是当我尝试在某些请求期间访问SecurityContextHolder时,身份验证为空。使用URL属性时,授权JSP标记不遵守我的方法安全注释。
15.5.3.Spring Security架构问题
我如何知道X包中的哪个包?命名空间元素如何映射到传统的bean配置?“ROLE_”是什么意思,为什么我需要在我的角色名称上使用它?如何知道要添加到我的应用程序中的哪些依赖项与Spring Security一起使用?运行嵌入式ApacheDS LDAP服务器需要哪些依赖项?什么是UserDetailsS​​ervice,我需要一个吗?
15.5.4.常见的“Howto”请求
我需要登录的信息不仅仅是用户名。如何应用不同的拦截 - 网址约束,只有请求的网址的片段值不同(例如/ foo #bar和/ foo#blah?如何在UserDetailsS​​ervice中访问用户的IP地址(或其他web - 请求数据)?如何从UserDetailsS​​ervice访问HttpSession?如何在UserDetailsS​​ervice中访问用户的密码?如何动态定义应用程序中的安全URL?如何对LDAP进行身份验证,但是从数据库加载用户角色?我想修改由命名空间创建的bean的属性,但架构中没有任何内容可以支持它。
III。反应性应用
16. WebFlux安全
16.1.最小的WebFlux安全配置16.2.明确的WebFlux安全配置
17.默认安全标头
17.1.缓存控制17.2.内容类型选项17.3.HTTP严格传输安全(HSTS)17.4.X-框架,选项17.5.X-XSS-保护17.6.内容安全政策(CSP)
17.6.1.配置内容安全策略17.6.2.其他资源
17.7.推荐人政策
17.7.1.配置推荐人策略
17.8.功能政策
17.8.1.配置功能策略
18.重定向到HTTPS19. OAuth2 WebFlux
19.1.OAuth 2.0登录
19.1.1.Spring Boot 2.0样本
初始设置设置重定向URI配置application.yml启动应用程序
19.1.2.使用OpenID提供程序配置19.1.3.显式OAuth2登录配置
19.2.OAuth2客户端19.3.OAuth2资源服务器
19.3.1.依赖19.3.2.最小配置
指定授权服务器启动期望运行期望指定授权服务器JWK直接设置Uri覆盖或替换引导自动配置配置授权配置验证
20. @ RegisteredOAuth2AuthorizedClient21. WebClient
21.1.WebClient OAuth2安装程序21.2.隐含的OAuth2AuthorizedClient21.3.显式OAuth2AuthorizedClient21.4.clientRegistrationId
22. EnableReactiveMethodSecurity23.反应性测试支持
23.1.测试反应方法安全性23.2.WebTestClientSupport
23.2.1.认证23.2.2.CSRF支持

Spring Security是一个框架,提供针对常见攻击的身份验证,授权和保护。通过对命令式和反应式应用程序的一流支持,它是保护基于Spring的应用程序的事实标准。

第一部分前言

本节讨论Spring Security的后勤问题。

1. Spring Security社区

欢迎来到Spring Security社区!本节讨论如何充分利用我们庞大的社区。

1.1获得帮助

如果您需要Spring Security的帮助,我们随时为您提供帮助。以下是获得帮助的一些最佳步骤:

  • 阅读我们的参考文档
  • 尝试我们的许多示例应用程序之一
  • 使用标签spring-security 在https://stackoverflow.com上 提问
  • 通过https://github.com/spring-projects/spring-security/issues报告错误和增强请求

1.2成为参与者

我们欢迎您参与Spring Security项目。有很多贡献方式,包括回答StackOverflow上的问题,编写新代码,改进现有代码,协助编写文档,开发示例或教程,报告错误或简单地提出建议。

1.3源代码

Spring Security的源代码可以在GitHub上找到https://github.com/spring-projects/spring-security/

1.4 Apache 2许可证

Spring Security是在Apache 2.0许可下发布的开源软件。

1.5社交媒体

您可以在Twitter上关注@SpringSecurity和Spring Security团队以及时了解最新消息。您还可以关注@SpringCentral以了解整个Spring投资组合的最新信息。

2. Spring Security 5.1中的新内容

Spring Security 5.1提供了许多新功能。以下是该版本的亮点。

2.1 Servlet

  • 通过UserDetailsPasswordService自动升级密码存储
  • OAuth 2.0客户端

    • 可自定义的授权和令牌请求
    • authorization_code给予支持
    • client_credentials给予支持
  • OAuth 2.0资源服务器 - 支持JWT编码的承载令牌
  • 添加了OAuth2 WebClient集成
  • HTTP防火墙可防止HTTP动词篡改和跨站点跟踪
  • ExceptionTranslationFilter支持通过RequestMatcher选择AccessDeniedHandler
  • CSRF支持排除某些请求
  • 添加了对功能策略的支持
  • 添加了@Transient身份验证令牌
  • 默认登录页面的现代外观

2.2 WebFlux

  • 通过ReactiveUserDetailsPasswordService自动升级密码存储
  • 添加了OAuth2支持

    • 添加了OAuth2客户端支持
    • 添加了OAuth2资源服务器支持
    • 添加了OAuth2 WebClient集成
  • @WithUserDetails 现在适用于ReactiveUserDetailsService
  • 添加了CORS支持
  • 添加了对以下HTTP标头的支持

    • 内容安全政策
    • 功能政策
    • 推荐人政策
  • 重定向到HTTPS
  • @AuthenticationPrincipal的改进

    • 支持解析bean
    • 支持解决errorOnInvalidType

2.3整合

  • Jackson支持适用于BadCredentialsException
  • @WithMockUser 支持在测试中设置SecurityContext时进行自定义。例如,@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)将在JUnit的@Before之后和测试执行之前设置用户。
  • 可以使用自定义环境变量配置 LDAP身份验证
  • X.509身份验证支持将主体派生为策略

3.获得Spring Security

本节讨论了获取Spring Security二进制文件时需要了解的所有信息。有关如何获取源代码,请参见第1.3节“源代码”。

3.1发布编号

Spring Security版本的格式为MAJOR.MINOR.PATCH

  • 主要版本可能包含重大更改。通常这些是为了提供改进的安全性以匹配现代安全实践。
  • MINOR版本包含增强功能,但被视为被动更新
  • PATCH级别应该完全兼容,前向和后向,可能的例外是修复错误

3.2使用Maven

像大多数开源项目一样,Spring Security将其依赖项部署为Maven工件。以下部分提供了有关如何在使用Maven时使用Spring Security的详细信息。

3.2.1 Spring Boot与Maven

Spring Boot提供了一个spring-boot-starter-security启动程序,它将Spring Security相关的依赖项聚合在一起。利用启动器的最简单和首选方法是使用IDE集成(Eclipse,IntelliJ,NetBeans)或通过https://start.spring.io使用Spring Initializr。

或者,可以手动添加启动器:

pom.xml中。 

org.springframework.bootspring-boot-starter-security

由于Spring Boot提供Maven BOM来管理依赖版本,因此无需指定版本。如果您希望覆盖Spring Security版本,可以通过提供Maven属性来实现:

pom.xml中。 

5.1.2.RELEASE

由于Spring Security仅在主要版本中进行重大更改,因此使用Spring Security和Spring Boot的较新版本是安全的。但是,有时可能还需要更新Spring Framework的版本。这可以通过添加Maven属性轻松完成:

pom.xml中。 

5.1.3.RELEASE

如果您正在使用LDAP,OpenID等其他功能,则还需要包含相应的第4章“ 项目模块”

3.2.2没有Spring Boot的Maven

使用不带Spring Boot的Spring Security时,首选方法是利用Spring Security的BOM来确保在整个项目中使用Spring Security的一致版本。

pom.xml中。 

org.springframework.securityspring-security-bom5.1.2.RELEASEpomimport

最小Spring Security Maven依赖项集通常如下所示:

pom.xml中。 

org.springframework.securityspring-security-weborg.springframework.securityspring-security-config

如果您正在使用LDAP,OpenID等其他功能,则还需要包含相应的第4章“ 项目模块”

Spring Security建立在Spring Framework 5.1.3.RELEASE之上,但通常应该适用于任何较新版本的Spring Framework 5.x许多用户将遇到的问题是Spring Security的传递依赖性解决了Spring Framework 5.1.3.RELEASE可能导致奇怪的类路径问题。解决此问题的最简单方法是使用pom.xml部分中的spring-framework-bom,如下所示:

pom.xml中。 

org.springframeworkspring-framework-bom5.1.3.RELEASEpomimport

这将确保Spring Security的所有传递依赖性使用Spring 5.1.3.RELEASE模块。

[注意]注意

这种方法使用Maven的“物料清单”(BOM)概念,仅适用于Maven 2.0.9+。有关如何解析依赖关系的其他详细信息,请参阅Maven的依赖关系机制简介文档。

3.2.3 Maven Repositories

所有GA版本(即以.RELEASE结尾的版本)都部署到Maven Central,因此不需要在您的pom中声明其他Maven存储库。

如果您使用的是SNAPSHOT版本,则需要确保定义了Spring快照存储库,如下所示:

pom.xml中。 

spring-snapshotSpring Snapshot Repositoryhttps://repo.spring.io/snapshot

如果您使用里程碑或候选发布版本,则需要确保定义了Spring Milestone存储库,如下所示:

pom.xml中。 

spring-milestoneSpring Milestone Repositoryhttps://repo.spring.io/milestone

3.3 Gradle

像大多数开源项目一样,Spring Security将其依赖关系部署为Maven工件,允许获得一流的Gradle支持。以下部分提供有关如何在使用Gradle时使用Spring Security的详细信息。

3.3.1 Spring Boot与Gradle

Spring Boot提供了一个spring-boot-starter-security启动程序,它将Spring Security相关的依赖项聚合在一起。利用启动器的最简单和首选方法是使用IDE集成(Eclipse,IntelliJ,NetBeans)或通过https://start.spring.io使用Spring Initializr。

或者,可以手动添加启动器:

的build.gradle。 

dependencies {compile "org.springframework.boot:spring-boot-starter-security"
}

由于Spring Boot提供Maven BOM来管理依赖版本,因此无需指定版本。如果您希望覆盖Spring Security版本,可以通过提供Gradle属性来执行此操作:

的build.gradle。 

ext['spring-security.version']='5.1.2.RELEASE'

由于Spring Security仅在主要版本中进行重大更改,因此使用Spring Security和Spring Boot的较新版本是安全的。但是,有时可能还需要更新Spring Framework的版本。这也可以通过添加Gradle属性轻松完成:

的build.gradle。 

ext['spring.version']='5.1.3.RELEASE'

如果您正在使用LDAP,OpenID等其他功能,则还需要包含相应的第4章“ 项目模块”

3.3.2没有Spring Boot的Gradle

使用不带Spring Boot的Spring Security时,首选方法是利用Spring Security的BOM来确保在整个项目中使用Spring Security的一致版本。这可以通过使用Dependency Management Plugin来完成。

的build.gradle。 

plugins {id "io.spring.dependency-management" version "1.0.6.RELEASE"
}dependencyManagement {imports {mavenBom 'org.springframework.security:spring-security-bom:5.1.2.RELEASE'}
}

最小Spring Security Maven依赖项集通常如下所示:

的build.gradle。 

dependencies {compile "org.springframework.security:spring-security-web"compile "org.springframework.security:spring-security-config"
}

如果您正在使用LDAP,OpenID等其他功能,则还需要包含相应的第4章“ 项目模块”

Spring Security建立在Spring Framework 5.1.3.RELEASE之上,但通常应该适用于任何较新版本的Spring Framework 5.x许多用户将遇到的问题是Spring Security的传递依赖性解决了Spring Framework 5.1.3.RELEASE可能导致奇怪的类路径问题。解决此问题的最简单方法是在pom.xml部分中使用spring-framework-bom,如下所示:这可以通过使用依赖关系管理插件来完成。

的build.gradle。 

plugins {id "io.spring.dependency-management" version "1.0.6.RELEASE"
}dependencyManagement {imports {mavenBom 'org.springframework:spring-framework-bom:5.1.3.RELEASE'}
}

这将确保Spring Security的所有传递依赖性使用Spring 5.1.3.RELEASE模块。

3.3.3 Gradle Repositories

所有GA版本(即以.RELEASE结尾的版本)都部署到Maven Central,因此使用mavenCentral()存储库足以支持GA版本。

的build.gradle。 

repositories {mavenCentral()
}

如果您使用的是SNAPSHOT版本,则需要确保定义了Spring快照存储库,如下所示:

的build.gradle。 

repositories {maven { url 'https://repo.spring.io/snapshot' }
}

如果您使用里程碑或候选发布版本,则需要确保定义了Spring Milestone存储库,如下所示:

的build.gradle。 

repositories {maven { url 'https://repo.spring.io/milestone' }
}

4.项目模块

在Spring Security 3.0中,代码库被细分为单独的jar,它们更清楚地区分不同的功能区域和第三方依赖项。如果您使用Maven构建项目,那么这些是您将添加到pom.xml的模块。即使您没有使用Maven,我们也建议您查阅pom.xml文件以了解第三方依赖项和版本。或者,一个好主意是检查示例应用程序中包含的库。

4.1核心 - spring-security-core .jar

包含核心身份验证和access-contol类和接口,远程支持和基本配置API。任何使用Spring Security的应用程序都需要。支持独立应用程序,远程客户端,方法(服务层)安全性和JDBC用户配置。包含顶级包:

  • org.springframework.security.core
  • org.springframework.security.access
  • org.springframework.security.authentication
  • org.springframework.security.provisioning

4.2 Remoting - spring-security-remoting。jar

提供与Spring Remoting的整合。除非您正在编写使用Spring远程处理的远程客户端,否则您不需要此操作。主要包是org.springframework.security.remoting

4.3 Web - spring-security-web。jar

包含过滤器和相关的web - 安全基础结构代码。任何具有servlet API依赖性的东西。如果您需要Spring Security web身份验证服务和基于URL的访问控制,则需要它。主要包是org.springframework.security.web

4.4配置 - spring-security-config .jar

包含安全名称空间解析代码和Java配置代码。如果您使用Spring Security XML命名空间进行配置或Spring Security的Java配置支持,则需要它。主要包是org.springframework.security.config。这些类都不打算直接用于应用程序。

4.5 LDAP - spring-security-ldap。jar

LDAP身份验证和配置代码。如果需要使用LDAP身份验证或管理LDAP用户条目,则为必需。顶级包是org.springframework.security.ldap

4.6 OAuth 2.0核心 - spring-security-oauth2-core.jar

spring-security-oauth2-core.jar包含为OAuth 2.0授权框架OpenID Connect Core 1.0提供支持的核心类和接口。使用OAuth 2.0OpenID Connect Core 1.0的应用程序(例如客户端,资源服务器和授权服务器)需要它。顶级包是org.springframework.security.oauth2.core

4.7 OAuth 2.0客户端 - spring-security-oauth2-client.jar

spring-security-oauth2-client.jar是Spring Security对OAuth 2.0授权框架OpenID Connect Core 1.0的客户端支持。应用程序需要利用OAuth 2.0登录和/或OAuth客户端支持。顶级包是org.springframework.security.oauth2.client

4.8 OAuth 2.0 JOSE - spring-security-oauth2-jose.jar

spring-security-oauth2-jose.jar包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。的圣何塞框架旨在提供安全地传输双方之间的权利要求的方法。它由一系列规范构建:

  • JSON Web令牌(JWT)
  • JSON Web签名(JWS)
  • JSON Web加密(JWE)
  • JSON Web密钥(JWK)

它包含顶级包:

  • org.springframework.security.oauth2.jwt
  • org.springframework.security.oauth2.jose

4.9 ACL - spring-security-acl。jar

专门的域对象ACL实现。用于将安全性应用于应用程序中的特定域对象实例。顶级包是org.springframework.security.acls

4.10 CAS - spring-security-cas .jar

Spring Security CAS客户端集成。如果要对CAS单点登录服务器使用Spring Security web身份验证。顶级包是org.springframework.security.cas

4.11 OpenID - spring-security-openid .jar

OpenID web身份验证支持。用于针对外部OpenID服务器对用户进行身份验证。org.springframework.security.openid.需要OpenID4Java。

4.12测试 - spring-security-test.jar

支持使用Spring Security进行测试。

5.样本申请

项目中有几个示例web应用程序。为避免过大的下载,分发zip文件中仅包含“教程”和“联系人”示例。其他可以直接从您可以获得的源构建,如介绍中所述。自己构建项目很容易,有关项目web站点http://spring.io/spring-security/的更多信息。本章中提到的所有路径都与项目源目录相关。

5.1教程样本

教程示例是一个很好的基本示例,可帮助您入门。它始终使用简单的命名空间配置 已编译的应用程序包含在分发zip文件中,可以部署到web容器(spring-security-samples-tutorial-3.1.x.war)中。基于表单的身份验证机制与常用的记住我身份验证提供程序结合使用,以使用cookie自动记住登录。

我们建议您从教程示例开始,因为XML很小且易于遵循。最重要的是,您可以轻松地将这一个XML文件(及其相应的web.xml条目)添加到现有应用程序中。只有在实现此基本集成时,我们才建议您尝试添加方法授权或域对象安全性。

5.2联系人

Contacts Sample是一个高级示例,它说明了除基本应用程序安全性之外的域对象访问控制列表(ACL)的更强大功能。该应用程序提供了一个界面,用户可以使用该界面管理简单的联系人数据库(域对象)。

要进行部署,只需将WAR文件从Spring Security发行版复制到容器的webapps目录中。战争应该被称为spring-security-samples-contacts-3.1.x.war(附加的版本号将根据您使用的版本而有所不同)。

启动容器后,检查应用程序是否可以加载。访问http:// localhost:8080 / contacts(或适用于您的web容器和您部署的WAR的URL)。

接下来,单击“调试”。系统将提示您进行身份验证,并在该页面上建议一系列用户名和密码。只需使用其中任何一个进行身份验证即可查看生成的页面。它应包含类似于以下内容的成功消息:

Security Debug InformationAuthentication object is of type:
org.springframework.security.authentication.UsernamePasswordAuthenticationTokenAuthentication object as a String:org.springframework.security.authentication.UsernamePasswordAuthenticationToken@1f127853:
Principal: org.springframework.security.core.userdetails.User@b07ed00: Username: rod; \
Password: [PROTECTED]; Enabled: true; AccountNonExpired: true;
credentialsNonExpired: true; AccountNonLocked: true; \
Granted Authorities: ROLE_SUPERVISOR, ROLE_USER; \
Password: [PROTECTED]; Authenticated: true; \
Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0: \
RemoteIpAddress: 127.0.0.1; SessionId: 8fkp8t83ohar; \
Granted Authorities: ROLE_SUPERVISOR, ROLE_USERAuthentication object holds the following granted authorities:ROLE_SUPERVISOR (getAuthority(): ROLE_SUPERVISOR)
ROLE_USER (getAuthority(): ROLE_USER)Success! Your web filters appear to be properly configured!

成功收到上述消息后,返回示例应用程序的主页并单击“管理”。然后,您可以试用该应用程序。请注意,仅显示当前登录用户可用的联系人,并且只有ROLE_SUPERVISOR的用户才有权删除其联系人。在幕后,MethodSecurityInterceptor正在保护业务对象。

该应用程序允许您修改与不同联系人关联的访问控制列表。请务必通过查看应用程序上下文XML文件来尝试并了解其工作原理。

5.3 LDAP示例

LDAP示例应用程序提供基本配置,并使用传统bean在同一应用程序上下文文件中设置命名空间配置和等效配置。这意味着在此应用程序中实际配置了两个相同的身份验证提供程

5.4 OpenID示例

OpenID示例演示了如何使用命名空间配置OpenID以及如何为Google,Yahoo和MyOpenID身份提供程序设置属性交换配置(如果愿意,可以尝试添加其他配置)。它使用基于JQuery的openid-selector项目来提供用户友好的登录页面,允许用户轻松选择提供者,而不是键入完整的OpenID标识符。

该应用程序与普通身份验证方案的不同之处在于,它允许任何用户访问该站点(前提是他们的OpenID身份验证成功)。第一次登录时,您将收到“欢迎[您的姓名]”消息。如果您注销并重新登录(具有相同的OpenID身份),则应更改为“欢迎回来”。这是通过使用custom UserDetailsService,它为任何用户分配一个标准角色,并在内部将身份存储在一个地图中。显然,真正的应用程序会使用数据库。请查看源表单的更多信息。这个类还考虑了这个事实可以从不同的提供程序返回不同的属性,并构建用于相应地向用户发出的名称。

5.5 CAS样品

CAS示例要求您同时运行CAS服务器和CAS客户端。它不包含在发布包里,你的描述应该检查项目代码的介绍。您将在sample/cas目录下找到相关文件。还有一个Readme.txt文件,解释了如何直接从源代码树运行服务器和客户端,完成SSL支持。

5.6 JAAS样本

JAAS示例是如何将JAAS LoginModule与Spring Security一起使用的非常简单的示例。如果用户名等于密码,则提供的LoginModule将成功验证用户,否则抛出LoginException。本示例中使用的AuthorityGranter始终授予角色ROLE_USER。示例应用程序还演示了如何通过将jaas-api-provision设置为“true” 来作为LoginModule返回的JAAS主题运行。

5.7预认证样本

此示例应用程序演示了如何从预身份验证框架中连接Bean 以使用来自Java EE容器的登录信息。用户名和角色是容器设置的用户名和角色。

代码在samples/preauth

第二部分。Servlet应用程序

6. Java配置

在Spring 3.1中,Spring Framework添加了对Java配置的一般支持。自Spring Security 3.2以来,Spring Security Java配置支持使用户无需使用任何XML即可轻松配置Spring Security。

如果您熟悉第7章安全命名空间配置,那么您应该发现它与安全Java配置支持之间有很多相似之处。

[注意]注意

Spring Security提供了许多演示使用Spring Security Java配置的示例应用程序。

6.1 Hello Web安全Java配置

第一步是创建我们的Spring Security Java配置。该配置创建一个称为springSecurityFilterChain的Servlet过滤器,它负责应用程序中的所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。您可以在下面找到Spring Security Java配置的最基本示例:

import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;@EnableWebSecurity
public class WebSecurityConfig implements WebMvcConfigurer {@Beanpublic UserDetailsService userDetailsService() throws Exception {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());return manager;}
}

这种配置确实没什么用,但它做了很多。您可以在下面找到以下功能的摘要:

  • 要求对应用程序中的每个URL进行身份验证
  • 为您生成登录表单
  • 允许具有Username 用户密码 密码的用户使用基于表单的身份验证进行身份验证
  • 允许用户注销
  • CSRF攻击预防
  • 会话固定保护
  • 安全标头集成

    • 用于安全请求的 HTTP严格传输安全性
    • X-Content-Type-Options集成
    • 缓存控制(稍后可由应用程序覆盖以允许缓存静态资源)
    • X-XSS-Protection集成
    • X-Frame-Options集成有助于防止Clickjacking
  • 与以下Servlet API方法集成

    • HttpServletRequest的#getRemoteUser()
    • HttpServletRequest.html#getUserPrincipal()
    • HttpServletRequest.html#的isUserInRole(java.lang.String中)
    • HttpServletRequest.html#login(java.lang.String,java.lang.String)
    • HttpServletRequest.html#注销()

6.1.1 AbstractSecurityWebApplicationInitializer

下一步是在战争中注册springSecurityFilterChain。这可以在Java配置中使用Spring在Servlet 3.0+环境中的WebApplicationInitializer支持来完成。毫不奇怪,Spring Security提供了一个基类AbstractSecurityWebApplicationInitializer,可确保springSecurityFilterChain为您注册。我们使用AbstractSecurityWebApplicationInitializer的方式取决于我们是否已经使用Spring或者Spring Security是我们应用程序中唯一的Spring组件。

  • 第6.1.2节“没有现有的AbstractSecurityWebApplicationInitializer Spring” - 如果您还没有使用Spring,请使用这些说明
  • 第6.1.3节“使用Spring MVC的AbstractSecurityWebApplicationInitializer” - 如果您已经在使用Spring,请使用这些说明

6.1.2没有现有Spring的AbstractSecurityWebApplicationInitializer

如果您不使用Spring或Spring MVC,则需要将WebSecurityConfig传入超类以确保获取配置。你可以在下面找到一个例子:

import org.springframework.security.web.context.*;public class SecurityWebApplicationInitializerextends AbstractSecurityWebApplicationInitializer {public SecurityWebApplicationInitializer() {super(WebSecurityConfig.class);}
}

SecurityWebApplicationInitializer将执行以下操作:

  • 自动为应用程序中的每个URL注册springSecurityFilterChain过滤器
  • 添加一个加载WebSecurityConfig的ContextLoaderListener 。

6.1.3使用Spring MVC的AbstractSecurityWebApplicationInitializer

如果我们在我们的应用程序的其他地方使用Spring,我们可能已经有WebApplicationInitializer正在加载我们的Spring配置。如果我们使用以前的配置,我们会收到错误。相反,我们应该使用现有的ApplicationContext注册Spring Security。例如,如果我们使用Spring MVC,我们的SecurityWebApplicationInitializer将如下所示:

import org.springframework.security.web.context.*;public class SecurityWebApplicationInitializerextends AbstractSecurityWebApplicationInitializer {}

这只会为应用程序中的每个URL注册springSecurityFilterChain过滤器。之后,我们将确保在我们现有的ApplicationInitializer中加载WebSecurityConfig。例如,如果我们使用Spring MVC,它将被添加到getRootConfigClasses()

public class MvcWebApplicationInitializer extendsAbstractAnnotationConfigDispatcherServletInitializer {@Overrideprotected Class[] getRootConfigClasses() {return new Class[] { WebSecurityConfig.class };}// ... other overrides ...
}

6.2 HttpSecurity

到目前为止,我们的WebSecurityConfig仅包含有关如何验证用户身份的信息。Spring Security如何知道我们要求所有用户都经过身份验证?Spring Security如何知道我们想要支持基于表单的身份验证?原因是WebSecurityConfigurerAdapterconfigure(HttpSecurity http)方法中提供了一个默认配置,如下所示:

protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
}

上面的默认配置:

  • 确保对我们的应用程序的任何请求都要求用户进行身份验证
  • 允许用户使用基于表单的登录进行身份验证
  • 允许用户使用HTTP基本身份验证进行身份验证

您会注意到此配置与XML命名空间配置非常相似:



使用and()方法表示关闭XML标记的Java配置,它允许我们继续配置父标记。如果您阅读代码,它也是有道理的。我想配置授权请求配置表单登录配置HTTP基本身份验证。

6.3 Java配置和表单登录

当您被提示登录时,您可能想知道登录表单的来源,因为我们没有提及任何HTML文件或JSP。由于Spring Security的默认配置未明确设置登录页面的URL,因此Spring Security会根据启用的功能自动生成一个URL,并使用处理提交的登录的URL的标准值,默认值登录后用户将被发送到的目标URL,依此类推。

虽然自动生成的登录页面便于快速启动和运行,但大多数应用程序都希望提供自己的登录页面。为此,我们可以更新我们的配置,如下所示:

protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login") 1.permitAll();        2
}

1

更新的配置指定登录页面的位置。

2

我们必须授予所有用户(即未经身份验证的用户)访问我们的登录页面的权限。formLogin().permitAll()方法允许为与基于表单的登录相关联的所有URL授予对所有用户的访问权限。

使用JSP为我们当前配置实现的示例登录页面如下所示:

[注意]注意

下面的登录页面代表我们当前的配置。如果某些默认设置不符合我们的需求,我们可以轻松更新配置。


1 2

Invalid username and password.

3

You have been logged out.

4

5

name="${_csrf.parameterName}"value="${_csrf.token}"/>

1

/login URL的POST将尝试对用户进行身份验证

2

如果查询参数error存在,则尝试进行身份验证并失败

3

如果查询参数logout存在,则用户已成功注销

4

用户名必须作为名为username的HTTP参数出现

五

密码必须作为名为password的HTTP参数出现

6

我们必须在“包含CSRF令牌”一节中了解更多信息,请参阅第10.6节“跨站点请求伪造(CSRF)”部分的参考

6.4授权请求

我们的示例仅要求用户进行身份验证,并且已针对应用程序中的每个URL进行了身份验证。我们可以通过向http.authorizeRequests()方法添加多个子项来指定网址的自定义要求。例如:

protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()                                                                1.antMatchers("/resources/**", "/signup", "/about").permitAll()                  2.antMatchers("/admin/**").hasRole("ADMIN")                                      3.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")            4.anyRequest().authenticated()                                                   5.and()// ....formLogin();
}

1

http.authorizeRequests()方法有多个子项,每个匹配器按其声明的顺序进行考虑。

2

我们指定了任何用户都可以访问的多种URL模式。具体来说,如果URL以“/ resources /”开头,等于“/ signup”或等于“/ about”,则任何用户都可以访问请求。

3

任何以“/ admin /”开头的URL都将仅限于具有“ROLE_ADMIN”角色的用户。您会注意到,由于我们正在调用hasRole方法,因此我们不需要指定“ROLE_”前缀。

4

任何以“/ db /”开头的URL都要求用户同时拥有“ROLE_ADMIN”和“ROLE_DBA”。您会注意到,由于我们使用的是hasRole表达式,因此我们不需要指定“ROLE_”前缀。

五

任何尚未匹配的URL只需要对用户进行身份验证

6.5处理注销

使用WebSecurityConfigurerAdapter时,会自动应用注销功能。默认情况下,访问URL /logout将通过以下方式记录用户:

  • 使HTTP会话无效
  • 清理已配置的任何RememberMe身份验证
  • 清除SecurityContextHolder
  • 重定向到/login?logout

但是,与配置登录功能类似,您还可以使用各种选项来进一步自定义注销要求:

protected void configure(HttpSecurity http) throws Exception {http.logout()                                                                1.logoutUrl("/my/logout")                                                 2.logoutSuccessUrl("/my/index")                                           3.logoutSuccessHandler(logoutSuccessHandler)                              4.invalidateHttpSession(true)                                             5.addLogoutHandler(logoutHandler)                                         6.deleteCookies(cookieNamesToClear)                                       7.and()...
}

1

提供注销支持。使用WebSecurityConfigurerAdapter时会自动应用此选项。

2

触发注销的URL(默认为/logout)。如果启用了CSRF保护(默认),则该请求也必须是POST。有关更多信息,请参阅JavaDoc。

3

注销后重定向到的URL。默认值为/login?logout。有关更多信息,请参阅JavaDoc。

4

我们指定一个自定义LogoutSuccessHandler。如果指定了此项,则忽略logoutSuccessUrl()。有关更多信息,请参阅JavaDoc。

五

指定在注销时是否使HttpSession无效。默认情况下这是真的。配置SecurityContextLogoutHandler封面。有关更多信息,请参阅JavaDoc。

6

添加LogoutHandler。默认情况下,SecurityContextLogoutHandler被添加为最后一个LogoutHandler

7

允许指定在注销成功时删除的cookie的名称。这是显式添加CookieClearingLogoutHandler的快捷方式。

[注意]注意

===当然也可以使用XML Namespace表示法配置注销。有关更多详细信息,请参阅Spring Security XML命名空间部分中的logout元素的文档。===

通常,为了自定义注销功能,您可以添加LogoutHandler和/或LogoutSuccessHandler实现。对于许多常见场景,这些处理程序在使用流畅的API时应用于幕后。

6.5.1 LogoutHandler

通常,LogoutHandler实现表示能够参与注销处理的类。预计将调用它们以进行必要的清理。因此,他们不应该抛出异常。提供了各种实现:

  • 对PersistentTokenBasedRememberMeServices
  • TokenBasedRememberMeServices
  • CookieClearingLogoutHandler
  • CsrfLogoutHandler
  • SecurityContextLogoutHandler

有关详细信息,请参见第10.5.4节“记住我的接口和实现”。

而不是直接提供LogoutHandler实现,流畅的API还提供了快捷方式,提供了各自的LogoutHandler实现。例如,deleteCookies()允许指定在注销成功时删除的一个或多个cookie的名称。与添加CookieClearingLogoutHandler相比,这是一种捷径。

6.5.2 LogoutSuccessHandler

LogoutFilter成功注销后调用LogoutSuccessHandler,以处理例如重定向或转发到适当的目的地。请注意,该接口几乎与LogoutHandler相同,但可能引发异常。

提供以下实现:

  • SimpleUrlLogoutSuccessHandler
  • HttpStatusReturningLogoutSuccessHandler

如上所述,您无需直接指定SimpleUrlLogoutSuccessHandler。相反,fluent API通过设置logoutSuccessUrl()来提供快捷方式。这将设置SimpleUrlLogoutSuccessHandler。发生注销后,提供的URL将重定向到。默认值为/login?logout

在REST API类型场景中,HttpStatusReturningLogoutSuccessHandler可能很有趣。成功注销后,LogoutSuccessHandler允许您提供要返回的纯HTTP状态代码,而不是在成功注销时重定向到URL。如果未配置,则默认返回状态代码200。

6.5.3进一步注销相关参考

  • 注销处理
  • 测试注销
  • HttpServletRequest.logout()
  • 第10.5.4节“记住我的接口和实现”
  • 登录 CSRF警告部分
  • 部分单点注销(CAS协议)
  • Spring Security XML命名空间部分中 的logout元素的文档

6.6 OAuth 2.0客户端

OAuth 2.0客户端功能为OAuth 2.0授权框架中定义的客户端角色提供支持。

可以使用以下主要功能:

  • 授权代码授予
  • 客户凭证授权
  • Servlet环境的WebClient扩展(用于发出受保护的资源请求)

HttpSecurity.oauth2Client()提供了许多用于自定义OAuth 2.0 Client的配置选项。以下代码显示了oauth2Client() DSL可用的完整配置选项:

@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Client().clientRegistrationRepository(this.clientRegistrationRepository()).authorizedClientRepository(this.authorizedClientRepository()).authorizedClientService(this.authorizedClientService()).authorizationCodeGrant().authorizationRequestRepository(this.authorizationRequestRepository()).authorizationRequestResolver(this.authorizationRequestResolver()).accessTokenResponseClient(this.accessTokenResponseClient());}
}

以下部分详细介绍了每种可用的配置选项:

  • 第6.6.1节“ClientRegistration”
  • 第6.6.2节“ClientRegistrationRepository”
  • 第6.6.3节“OAuth2AuthorizedClient”
  • 第6.6.4节“OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService”
  • 第6.6.5节“RegisteredOAuth2AuthorizedClient”
  • 第6.6.6节“AuthorizationRequestRepository”
  • 第6.6.7节“OAuth2AuthorizationRequestResolver”
  • 第6.6.8节“OAuth2AccessTokenResponseClient”

6.6.1 ClientRegistration

ClientRegistration表示在OAuth 2.0或OpenID Connect 1.0提供程序中注册的客户端。

客户端注册保存信息,例如客户端ID,客户端密钥,授权授权类型,重定向URI,范围,授权URI,令牌URI和其他详细信息。

ClientRegistration及其属性定义如下:

public final class ClientRegistration {private String registrationId;  1private String clientId;    2private String clientSecret;    3private ClientAuthenticationMethod clientAuthenticationMethod;  4private AuthorizationGrantType authorizationGrantType;  5private String redirectUriTemplate; 6private Set scopes; 7private ProviderDetails providerDetails;private String clientName;  8public class ProviderDetails {private String authorizationUri;    9private String tokenUri;    10private UserInfoEndpoint userInfoEndpoint;private String jwkSetUri;   11private Map configurationMetadata;  12public class UserInfoEndpoint {private String uri; 13private AuthenticationMethod authenticationMethod;  14private String userNameAttributeName;   15}}
}

1

registrationId:唯一标识ClientRegistration的ID。

2

clientId:客户端标识符。

3

clientSecret:客户机密。

4

clientAuthenticationMethod:用于使用Provider对客户端进行身份验证的方法。支持的值是基本后期

五

authorizationGrantType:OAuth 2.0授权框架定义了四种授权授权类型。支持的值是authorization_code,implicit和client_credentials。

6

redirectUriTemplate:客户端注册的重定向URI,授权服务器将最终用户的用户代理重定向到最终用户对客户端进行身份验证和授权访问之后。

7

scopes:客户在授权请求流程中请求的范围,例如openid,电子邮件或配置文件。

8

clientName:用于客户端的描述性名称。该名称可能在某些情况下使用,例如在自动生成的登录页面中显示客户端的名称时。

9

authorizationUri:授权服务器的授权端点URI。

10

tokenUri:授权服务器的令牌端点URI。

11

jwkSetUri:用于从授权服务器检索JSON Web密钥(JWK)集的URI ,其中包含用于验证ID 的JSON Web签名(JWS)的加密密钥令牌和可选的UserInfo响应。

12

configurationMetadata:OpenID提供程序配置信息。仅当配置了Spring Boot 2.x属性spring.security.oauth2.client.provider.[providerId].issuerUri时,才能使用此信息。

13

(userInfoEndpoint)uri:UserInfo端点URI,用于访问经过身份验证的最终用户的声明/属性。

14

(userInfoEndpoint)authenticationMethod:将访问令牌发送到UserInfo端点时使用的身份验证方法。支持的值是标题表单查询

15

userNameAttributeName:UserInfo响应中返回的属性的名称,该属性引用最终用户的名称或标识符。

6.6.2 ClientRegistrationRepository

ClientRegistrationRepository充当OAuth 2.0 / OpenID Connect 1.0 ClientRegistration的存储库。

[注意]注意

客户端注册信息最终由关联的授权服务器存储和拥有。此存储库提供检索主客户端注册信息的子集的功能,该子集与授权服务器一起存储。

Spring Boot 2.x auto-configuration将spring.security.oauth2.client.registration.[registrationId]下的每个属性绑定到ClientRegistration的实例,然后在ClientRegistrationRepository内组成每个ClientRegistration个实例。

[注意]注意

ClientRegistrationRepository的默认实现是InMemoryClientRegistrationRepository

自动配置还在ApplicationContext中将ClientRegistrationRepository注册为@Bean,以便在应用程序需要时可用于依赖注入。

以下清单显示了一个示例:

@Controller
public class OAuth2ClientController {@Autowiredprivate ClientRegistrationRepository clientRegistrationRepository;@RequestMapping("/")public String index() {ClientRegistration googleRegistration =this.clientRegistrationRepository.findByRegistrationId("google");...return "index";}
}

6.6.3 OAuth2AuthorizedClient

OAuth2AuthorizedClient是授权客户的代表。当最终用户(资源所有者)已授权客户端访问其受保护资源时,将认为客户端已获得授权。

OAuth2AuthorizedClient用于将OAuth2AccessToken(和可选的OAuth2RefreshToken)与ClientRegistration(客户端)和资源所有者相关联,后者是授予授权的Principal最终用户。

6.6.4 OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService

OAuth2AuthorizedClientRepository负责在web个请求之间保持OAuth2AuthorizedClient个。鉴于OAuth2AuthorizedClientService的主要作用是在应用程序级别管理OAuth2AuthorizedClient(s)。

从开发人员的角度来看,OAuth2AuthorizedClientRepositoryOAuth2AuthorizedClientService提供了查找与客户端关联的OAuth2AccessToken的功能,以便可以使用它来启动受保护的资源请求。

[注意]注意

Spring Boot 2.x自动配置在ApplicationContext中注册OAuth2AuthorizedClientRepository和/或OAuth2AuthorizedClientService @Bean

开发人员还可以在ApplicationContext中注册OAuth2AuthorizedClientRepositoryOAuth2AuthorizedClientService @Bean(覆盖Spring Boot 2.x自动配置),以便能够查找OAuth2AccessToken与特定的ClientRegistration(客户端)相关联。

以下清单显示了一个示例:

@Controller
public class OAuth2LoginController {@Autowiredprivate OAuth2AuthorizedClientService authorizedClientService;@RequestMapping("/userinfo")public String userinfo(OAuth2AuthenticationToken authentication) {// authentication.getAuthorizedClientRegistrationId() returns the// registrationId of the Client that was authorized during the oauth2Login() flowOAuth2AuthorizedClient authorizedClient =this.authorizedClientService.loadAuthorizedClient(authentication.getAuthorizedClientRegistrationId(),authentication.getName());OAuth2AccessToken accessToken = authorizedClient.getAccessToken();...return "userinfo";}
}

6.6.5 RegisteredOAuth2AuthorizedClient

@RegisteredOAuth2AuthorizedClient注释提供了将方法参数解析为类型OAuth2AuthorizedClient的参数值的功能。与通过OAuth2AuthorizedClientService查找OAuth2AuthorizedClient相比,这是一个方便的选择。

@Controller
public class OAuth2LoginController {@RequestMapping("/userinfo")public String userinfo(@RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient) {OAuth2AccessToken accessToken = authorizedClient.getAccessToken();...return "userinfo";}
}

@RegisteredOAuth2AuthorizedClient注释由OAuth2AuthorizedClientArgumentResolver处理,并提供以下功能:

  • 如果客户端尚未获得授权,将自动请求OAuth2AccessToken

    • 对于authorization_code,这涉及触发授权请求重定向以启动流
    • 对于client_credentials,使用DefaultClientCredentialsTokenResponseClient直接从令牌端点获取访问令牌

6.6.6 AuthorizationRequestRepository

从授权请求启动到收到授权响应时(回调),AuthorizationRequestRepository负责OAuth2AuthorizationRequest的持久性。

[小费]小费

OAuth2AuthorizationRequest用于关联和验证授权响应。

AuthorizationRequestRepository的默认实现是HttpSessionOAuth2AuthorizationRequestRepository,它将OAuth2AuthorizationRequest存储在HttpSession中。

如果您想提供AuthorizationRequestRepository的自定义实现,将OAuth2AuthorizationRequest的属性存储在Cookie中,您可以按以下示例所示进行配置:

@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Client().authorizationCodeGrant().authorizationRequestRepository(this.cookieAuthorizationRequestRepository())...}private AuthorizationRequestRepository cookieAuthorizationRequestRepository() {return new HttpCookieOAuth2AuthorizationRequestRepository();}
}

6.6.7 OAuth2AuthorizationRequestResolver

OAuth2AuthorizationRequestResolver的主要作用是从提供的web请求中解析OAuth2AuthorizationRequest。默认实现DefaultOAuth2AuthorizationRequestResolver匹配(默认)路径/oauth2/authorization/{registrationId},提取registrationId并使用它来为关联的ClientRegistration构建OAuth2AuthorizationRequest

OAuth2AuthorizationRequestResolver可以实现的主要用例之一是能够使用超出OAuth 2.0授权框架中定义的标准参数的附加参数来定制授权请求。

例如,OpenID Connect为授权代码流定义了额外的OAuth 2.0请求参数,这些参数扩展自OAuth 2.0授权框架中定义的标准参数。其中一个扩展参数是prompt参数。

[注意]注意

可选的。空格分隔,区分大小写的ASCII字符串值列表,指定授权服务器是否提示最终用户进行重新认证和同意。定义的值为:none,login,consent,select_account

以下示例显示如何通过包含请求参数prompt=consent来实现OAuth2AuthorizationRequestResolver来自定义oauth2Login()的授权请求。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate ClientRegistrationRepository clientRegistrationRepository;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().oauth2Login().authorizationEndpoint().authorizationRequestResolver(new CustomAuthorizationRequestResolver(this.clientRegistrationRepository));    1}
}public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;public CustomAuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {this.defaultAuthorizationRequestResolver =new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization");}@Overridepublic OAuth2AuthorizationRequest resolve(HttpServletRequest request) {OAuth2AuthorizationRequest authorizationRequest =this.defaultAuthorizationRequestResolver.resolve(request);  2return authorizationRequest != null ?   3customAuthorizationRequest(authorizationRequest) :null;}@Overridepublic OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {OAuth2AuthorizationRequest authorizationRequest =this.defaultAuthorizationRequestResolver.resolve(request, clientRegistrationId);    4return authorizationRequest != null ?   5customAuthorizationRequest(authorizationRequest) :null;}private OAuth2AuthorizationRequest customAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {Map additionalParameters =new LinkedHashMap<>(authorizationRequest.getAdditionalParameters());additionalParameters.put("prompt", "consent");  6return OAuth2AuthorizationRequest.from(authorizationRequest)    7.additionalParameters(additionalParameters) 8.build();}
}

1

配置自定义OAuth2AuthorizationRequestResolver

2 4

尝试使用DefaultOAuth2AuthorizationRequestResolver解决OAuth2AuthorizationRequest

3 五

如果解决了OAuth2AuthorizationRequest而不是返回自定义版本,则返回null

6

将自定义参数添加到现有OAuth2AuthorizationRequest.additionalParameters

7

创建默认OAuth2AuthorizationRequest的副本,返回OAuth2AuthorizationRequest.Builder以进行进一步修改

8

覆盖默认additionalParameters

[小费]小费

OAuth2AuthorizationRequest.Builder.build()构造OAuth2AuthorizationRequest.authorizationRequestUri,它代表完整的授权请求URI,包括使用application/x-www-form-urlencoded格式的所有查询参数。

上面的示例显示了在标准参数之上添加自定义参数的常见用例。但是,如果您需要删除或更改标准参数或者您的要求更高级,则可以通过简单地覆盖OAuth2AuthorizationRequest.authorizationRequestUri属性来完全控制构建授权请求URI。

以下示例显示了前一个示例中customAuthorizationRequest()方法的变体,而是覆盖了OAuth2AuthorizationRequest.authorizationRequestUri属性。

private OAuth2AuthorizationRequest customAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest) {String customAuthorizationRequestUri = UriComponentsBuilder.fromUriString(authorizationRequest.getAuthorizationRequestUri()).queryParam("prompt", "consent").build(true).toUriString();return OAuth2AuthorizationRequest.from(authorizationRequest).authorizationRequestUri(customAuthorizationRequestUri).build();
}

6.6.8 OAuth2AccessTokenResponseClient

OAuth2AccessTokenResponseClient的主要作用是在授权服务器的令牌端点处为访问令牌凭证交换授权授予凭证。

authorization_code授权的OAuth2AccessTokenResponseClient的默认实现是DefaultAuthorizationCodeTokenResponseClient,它使用RestOperations在令牌端点交换访问令牌的授权码。

DefaultAuthorizationCodeTokenResponseClient非常灵活,因为它允许您自定义令牌请求的预处理和/或令牌响应的后处理。

如果您需要自定义令牌请求的预处理,则可以为DefaultAuthorizationCodeTokenResponseClient.setRequestEntityConverter()提供自定义Converter>。默认实现OAuth2AuthorizationCodeGrantRequestEntityConverter构建标准OAuth 2.0访问令牌请求的RequestEntity表示。但是,提供自定义Converter将允许您扩展标准令牌请求并添加自定义参数。

[重要]重要

自定义Converter必须返回OAuth 2.0访问令牌请求的有效RequestEntity表示,该表示由预期的OAuth 2.0提供程序理解。

另一方面,如果您需要自定义令牌响应的后处理,则需要为DefaultAuthorizationCodeTokenResponseClient.setRestOperations()提供自定义配置的RestOperations。默认RestOperations配置如下:

RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(),new OAuth2AccessTokenResponseHttpMessageConverter()));restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
[小费]小费

发送OAuth 2.0访问令牌请求时使用Spring MVC FormHttpMessageConverter是必需的。

对于OAuth 2.0访问令牌响应,OAuth2AccessTokenResponseHttpMessageConverterHttpMessageConverter。您可以为OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()提供自定义Converter, OAuth2AccessTokenResponse>,用于将OAuth 2.0访问令牌响应参数转换为OAuth2AccessTokenResponse

OAuth2ErrorResponseErrorHandlerResponseErrorHandler,可以处理OAuth 2.0错误(400错误请求)。它使用OAuth2ErrorHttpMessageConverter将OAuth 2.0 Error参数转换为OAuth2Error

无论您是自定义DefaultAuthorizationCodeTokenResponseClient还是提供自己的OAuth2AccessTokenResponseClient实现,都需要对其进行配置,如以下示例所示:

@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Client().authorizationCodeGrant().accessTokenResponseClient(this.customAccessTokenResponseClient())...}private OAuth2AccessTokenResponseClient customAccessTokenResponseClient() {...}
}

6.7 OAuth 2.0登录

OAuth 2.0登录功能为应用程序提供了使用OAuth 2.0提供程序(例如GitHub)或OpenID Connect 1.0提供程序(例如Google)上的现有帐户登录应用程序的功能。OAuth 2.0 Login实现了用例:“使用Google登录”或“使用GitHub登录”。

[注意]注意

OAuth 2.0登录是使用授权代码授予实现的,如OAuth 2.0授权框架和OpenID Connect Core 1.0中所指定。

6.7.1 Spring Boot 2.x样本

Spring Boot 2.x为OAuth 2.0登录带来了完整的自动配置功能。

本节介绍如何使用Google作为身份验证提供程序配置OAuth 2.0登录示例,并介绍以下主题:

  • 初始设置
  • 设置重定向URI
  • 配置application.yml
  • 启动应用程序

初始设置

要使用Google的OAuth 2.0身份验证系统进行登录,您必须在Google API控制台中设置项目以获取OAuth 2.0凭据。

[注意]注意

Google的OAuth 2.0身份验证实施符合OpenID Connect 1.0规范,并通过OpenID认证。

按照OpenID Connect页面上的说明操作,从“设置OAuth 2.0”部分开始。

完成“获取OAuth 2.0凭据”说明后,您应该拥有一个新的OAuth客户端,其中包含客户端ID和客户端密钥的凭据。

设置重定向URI

重定向URI是应用程序中的路径,最终用户的用户代理在使用Google进行身份验证并在“同意”页面上授予了对OAuth客户端(在上一步中创建)的访问权限后重定向回的路径。

在“设置重定向URI”子部分中,确保将“ 授权重定向URI”字段设置为http://localhost:8080/login/oauth2/code/google

[小费]小费

默认重定向URI模板为{baseUrl}/login/oauth2/code/{registrationId}。该registrationId是用于唯一标识符ClientRegistration。

配置application.yml

现在您已经有了一个新的OAuth客户端与Google,您需要配置应用程序以使用OAuth客户端进行身份验证流程。为此:

  1. 转到application.yml并设置以下配置:

    spring:
      security:
        oauth2:
          client:
            registration:   1
              google:   2
                client-id: google-client-id
                client-secret: google-client-secret
    

    例6.1。OAuth客户端属性

    1

    spring.security.oauth2.client.registration是OAuth客户端属性的基本属性前缀。

    2

    基本属性前缀后面是ClientRegistration的ID ,例如google。

  2. 使用您之前创建的OAuth 2.0凭据替换client-idclient-secret属性中的值。

启动应用程序

启动Spring Boot 2.x示例并转到http://localhost:8080。然后,您将被重定向到默认的自动生成的登录页面,该页面显示Google的链接。

点击Google链接,然后您将重定向到Google进行身份验证。

使用您的Google帐户凭据进行身份验证后,显示给您的下一页是“同意”屏幕。“同意”屏幕会要求您允许或拒绝访问您之前创建的OAuth客户端。单击“ 允许”以授权OAuth客户端访问您的电子邮件地址和基本配置文件信息。

此时,OAuth客户端从UserInfo端点检索您的电子邮件地址和基本配置文件信息,并建立经过身份验证的会话。

6.7.2 Spring Boot 2.x属性映射

下表概述了Spring Boot 2.x OAuth客户端属性到ClientRegistration属性的映射。

Spring Boot 2.xClientRegistration

spring.security.oauth2.client.registration.[registrationId]

registrationId

spring.security.oauth2.client.registration.[registrationId].client-id

clientId

spring.security.oauth2.client.registration.[registrationId].client-secret

clientSecret

spring.security.oauth2.client.registration.[registrationId].client-authentication-method

clientAuthenticationMethod

spring.security.oauth2.client.registration.[registrationId].authorization-grant-type

authorizationGrantType

spring.security.oauth2.client.registration.[registrationId].redirect-uri

redirectUriTemplate

spring.security.oauth2.client.registration.[registrationId].scope

scopes

spring.security.oauth2.client.registration.[registrationId].client-name

clientName

spring.security.oauth2.client.provider.[providerId].authorization-uri

providerDetails.authorizationUri

spring.security.oauth2.client.provider.[providerId].token-uri

providerDetails.tokenUri

spring.security.oauth2.client.provider.[providerId].jwk-set-uri

providerDetails.jwkSetUri

spring.security.oauth2.client.provider.[providerId].user-info-uri

providerDetails.userInfoEndpoint.uri

spring.security.oauth2.client.provider.[providerId].user-info-authentication-method

providerDetails.userInfoEndpoint.authenticationMethod

spring.security.oauth2.client.provider.[providerId].userNameAttribute

providerDetails.userInfoEndpoint.userNameAttributeName

6.7.3 CommonOAuth2Provider

CommonOAuth2Provider为众多知名提供商预定义了一组默认客户端属性:Google,GitHub,Facebook和Okta。

例如,authorization-uritoken-uriuser-info-uri对于提供商而言不会经常更改。因此,提供默认值以减少所需的配置是有意义的。

如前所述,当我们配置Google客户端时,只需要client-idclient-secret属性。

以下清单显示了一个示例:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: google-client-id
            client-secret: google-client-secret
[小费]小费

客户端属性的自动默认无缝地在这里工作,因为registrationId(google)匹配CommonOAuth2Provider中的GOOGLE enum(不区分大小写)。

对于您可能希望指定其他registrationId的情况,例如google-login,您仍然可以通过配置provider属性来利用客户端属性的自动默认。

以下清单显示了一个示例:

spring:
  security:
    oauth2:
      client:
        registration:
          google-login: 1
            provider: google    2
            client-id: google-client-id
            client-secret: google-client-secret

1

registrationId设置为google-login

2

provider属性设置为google,它将利用CommonOAuth2Provider.GOOGLE.getBuilder()中设置的客户端属性的自动默认值。

6.7.4配置自定义提供程序Properties

有些OAuth 2.0提供程序支持多租户,这会为每个租户(或子域)产生不同的协议端点。

例如,向Okta注册的OAuth客户端被分配给特定的子域并拥有自己的协议端点。

对于这些情况,Spring Boot 2.x提供了以下用于配置自定义提供程序属性的基本属性:spring.security.oauth2.client.provider.[providerId]

以下清单显示了一个示例:

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
        provider:
          okta: 1
            authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
            token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
            user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
            user-name-attribute: sub
            jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys

1

基本属性(spring.security.oauth2.client.provider.okta)允许自定义配置协议端点位置。

6.7.5覆盖Spring Boot 2.x自动配置

OAuth客户端支持的Spring Boot 2.x自动配置类为OAuth2ClientAutoConfiguration

它执行以下任务:

  • 从配置的OAuth客户端属性中注册由ClientRegistration组成的ClientRegistrationRepository @Bean
  • 提供WebSecurityConfigurerAdapter @Configuration并通过httpSecurity.oauth2Login()启用OAuth 2.0登录。

如果您需要根据具体要求覆盖自动配置,可以通过以下方式执行此操作:

  • 注册ClientRegistrationRepository @Bean
  • 提供WebSecurityConfigurerAdapter
  • 完全覆盖自动配置

注册ClientRegistrationRepository @Bean

以下示例显示如何注册ClientRegistrationRepository @Bean

@Configuration
public class OAuth2LoginConfig {@Beanpublic ClientRegistrationRepository clientRegistrationRepository() {return new InMemoryClientRegistrationRepository(this.googleClientRegistration());}private ClientRegistration googleClientRegistration() {return ClientRegistration.withRegistrationId("google").clientId("google-client-id").clientSecret("google-client-secret").clientAuthenticationMethod(ClientAuthenticationMethod.BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}").scope("openid", "profile", "email", "address", "phone").authorizationUri("https://accounts.google.com/o/oauth2/v2/auth").tokenUri("https://www.googleapis.com/oauth2/v4/token").userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo").userNameAttributeName(IdTokenClaimNames.SUB).jwkSetUri("https://www.googleapis.com/oauth2/v3/certs").clientName("Google").build();}
}

提供WebSecurityConfigurerAdapter

以下示例说明如何使用@EnableWebSecurity提供WebSecurityConfigurerAdapter并通过httpSecurity.oauth2Login()启用OAuth 2.0登录:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().oauth2Login();}
}

完全覆盖自动配置

以下示例显示如何通过注册ClientRegistrationRepository @Bean并提供WebSecurityConfigurerAdapter来完全覆盖自动配置。

@Configuration
public class OAuth2LoginConfig {@EnableWebSecuritypublic static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().oauth2Login();}}@Beanpublic ClientRegistrationRepository clientRegistrationRepository() {return new InMemoryClientRegistrationRepository(this.googleClientRegistration());}private ClientRegistration googleClientRegistration() {return ClientRegistration.withRegistrationId("google").clientId("google-client-id").clientSecret("google-client-secret").clientAuthenticationMethod(ClientAuthenticationMethod.BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}").scope("openid", "profile", "email", "address", "phone").authorizationUri("https://accounts.google.com/o/oauth2/v2/auth").tokenUri("https://www.googleapis.com/oauth2/v4/token").userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo").userNameAttributeName(IdTokenClaimNames.SUB).jwkSetUri("https://www.googleapis.com/oauth2/v3/certs").clientName("Google").build();}
}

6.7.6没有Spring Boot 2.x的Java配置

如果您无法使用Spring Boot 2.x并希望在CommonOAuth2Provider(例如Google)中配置其中一个预定义的提供商,请应用以下配置:

@Configuration
public class OAuth2LoginConfig {@EnableWebSecuritypublic static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().oauth2Login();}}@Beanpublic ClientRegistrationRepository clientRegistrationRepository() {return new InMemoryClientRegistrationRepository(this.googleClientRegistration());}@Beanpublic OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);}@Beanpublic OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);}private ClientRegistration googleClientRegistration() {return CommonOAuth2Provider.GOOGLE.getBuilder("google").clientId("google-client-id").clientSecret("google-client-secret").build();}
}

6.7.7其他资源

以下附加资源描述了高级配置选项:

  • OAuth 2.0登录页面
  • 重定向端点
  • UserInfo端点:

    • 映射用户权限
    • 配置自定义OAuth2User
    • OAuth 2.0 UserService
    • OpenID Connect 1.0 UserService

6.8 OAuth 2.0资源服务器

Spring Security支持使用JWT编码的OAuth 2.0 承载令牌保护端点。

在应用程序将其权限管理联合到授权服务器(例如,Okta或Ping Identity)的情况下,这很方便。资源服务器可以查询此授权服务器,以在提供请求时验证权限。

[注意]注意

可以在OAuth 2.0 Resource Server Servlet示例中找到完整的工作示例

6.8.1依赖性

大多数资源服务器支持都收集到spring-security-oauth2-resource-server。但是,对解码和验证JWT的支持是spring-security-oauth2-jose,这意味着两者都是必要的,以便拥有一个支持JWT编码的承载令牌的工作资源服务器。

6.8.2最小配置

使用Spring Boot时,将应用程序配置为资源服务器包含两个基本步骤。首先,包括所需的依赖项,然后指出授权服务器的位置。

指定授权服务器

要指定要使用的授权服务器,只需执行以下操作:

security:
  oauth2:
    resourceserver:
      jwt:
        issuer-uri: https://idp.example.com

其中https://idp.example.com是授权服务器将发出的iss JWT令牌声明中包含的值。资源服务器将使用此属性进一步自我配置,发现授权服务器的公钥,并随后验证传入的JWT。

[注意]注意

要使用issuer-uri属性,https://idp.example.com/.well-known/openid-configuration是授权服务器支持的端点也必须如此。此端点称为提供者配置端点。

就是这样!

启动期望

使用此属性和这些依赖项时,Resource Server将自动配置自身以验证JWT编码的承载令牌。

它通过确定性的启动过程实现了这一点:

  1. 点击Provider Configuration端点https://idp.example.com/.well-known/openid-configuration,处理jwks_url属性的响应
  2. 配置验证策略以查询jwks_url以获取有效的公钥
  3. 配置验证策略以针对https://idp.example.com验证每个JWT iss声明。

此过程的结果是授权服务器必须启动并接收请求才能使Resource Server成功启动。

[注意]注意

如果授权服务器在资源服务器查询时关闭(给定适当的超时),则启动将失败。

运行期望

启动应用程序后,Resource Server将尝试处理包含Authorization: Bearer标头的任何请求:

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

只要指示此方案,资源服务器将尝试根据承载令牌规范处理请求。

给定格式良好的JWT令牌,资源服务器将

  1. 根据在启动期间从jwks_url端点获取的公钥验证其签名,并与JWTs头匹配
  2. 验证JWT expnbf时间戳以及JWT iss声明,以及
  3. 将每个范围映射到前缀为SCOPE_的权限。
[注意]注意

当授权服务器提供新密钥时,Spring Security将自动旋转用于验证JWT令牌的密钥。

结果Authentication#getPrincipal默认为Spring Security Jwt对象,Authentication#getName映射到JWT的sub属性(如果存在)。

从这里开始,考虑跳转到:

如何配置而不将Resource Server启动与授权服务器的可用性绑定

如何配置Spring Boot

6.8.3指定授权服务器JWK直接设置Uri

如果授权服务器不支持Provider Configuration端点,或者Resource Server必须能够独立于授权服务器启动,则可以将issuer-uri交换为jwk-set-uri

security:
  oauth2:
    resourceserver:
      jwt:
        jwk-set-uri: https://idp.example.com/.well-known/jwks.json
[注意]注意

JWK Set uri不是标准化的,但通常可以在授权服务器的文档中找到

因此,资源服务器不会在启动时ping授权服务器。但是,它也将不再验证JWT中的iss声明(因为资源服务器不再知道发行者值应该是什么)。

[注意]注意

此属性也可以直接在DSL上提供。

6.8.4覆盖或替换引导自动配置

Spring Boot代表资源服务器生成了@Bean@Bean

第一个是WebSecurityConfigurerAdapter,它将应用程序配置为资源服务器:

protected void configure(HttpSecurity http) {http.authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer().jwt();
}

如果应用程序没有公开WebSecurityConfigurerAdapter bean,那么Spring Boot将公开上面的默认值。

替换它就像在应用程序中公开bean一样简单:

@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {protected void configure(HttpSecurity http) {http.authorizeRequests().mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read").anyRequest().authenticated().and().oauth2ResourceServer().jwt().jwtAuthenticationConverter(myConverter());}
}

对于以/messages/开头的任何网址,上述内容要求message:read的范围。

oauth2ResourceServer DSL上的方法也将覆盖或替换自动配置。

例如,第二个@Bean Spring Boot创建的是JwtDecoder,它将String令牌解码为经过验证的Jwt实例:

@Bean
public JwtDecoder jwtDecoder() {return JwtDecoders.fromOidcIssuerLocation(issuerUri);
}

如果应用程序没有公开JwtDecoder bean,那么Spring Boot将公开上面的默认值。

并且可以使用jwkSetUri()覆盖其配置或使用decoder()替换它的配置。

使用jwkSetUri()

授权服务器的JWK Set Uri可以配置为配置属性,也可以在DSL中提供:

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {protected void configure(HttpSecurity http) {http.authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer().jwt().jwkSetUri("https://idp.example.com/.well-known/jwks.json");}
}

使用jwkSetUri()优先于任何配置属性。

使用decoder()

jwkSetUri()更强大的是decoder(),它将完全取代JwtDecoder的任何Boot自动配置:

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {protected void configure(HttpSecurity http) {http.authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer().jwt().decoder(myCustomDecoder());}
}

当需要更深层次的配置(如验证,映射或请求超时)时,这很方便。

公开JwtDecoder @Bean

或者,暴露JwtDecoder @Beandecoder()具有相同的效果:

@Bean
public JwtDecoder jwtDecoder() {return new NimbusJwtDecoderJwkSupport(jwkSetUri);
}

6.8.5配置授权

从OAuth 2.0授权服务器发出的JWT通常具有scopescp属性,表示已授予其范围(或权限),例如:

{ …​, "scope" : "messages contacts"}

在这种情况下,资源服务器将尝试将这些范围强制转换为已授权的权限列表,并在每个范围前添加字符串“SCOPE_”。

这意味着要使用从JWT派生的作用域保护端点或方法,相应的表达式应包含此前缀:

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {protected void configure(HttpSecurity http) {http.authorizeRequests().mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts").mvcMatchers("/messages/**").hasAuthority("SCOPE_messages").anyRequest().authenticated().and().oauth2ResourceServer().jwt();}
}

或者类似于方法安全性:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List getMessages(...) {}

手动提取权限

但是,在许多情况下,此默认值不足。例如,某些授权服务器不使用scope属性,而是拥有自己的自定义属性。或者,在其他时候,资源服务器可能需要使属性或属性的组合适应内部化的权限。

为此,DSL公开了jwtAuthenticationConverter()

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {protected void configure(HttpSecurity http) {http.authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer().jwt().jwtAuthenticationConverter(grantedAuthoritiesExtractor());}
}Converter grantedAuthoritiesExtractor() {return new GrantedAuthoritiesExtractor();
}

负责将Jwt转换为Authentication

我们可以简单地重写这一点,以改变授予权限的方式:

static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter {protected Collection extractAuthorities(Jwt jwt) {Collection authorities = (Collection)jwt.getClaims().get("mycustomclaim");return authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());}
}

为了获得更大的灵活性,DSL支持完全用任何实现Converter的类替换转换器:

static class CustomAuthenticationConverter implements Converter {public AbstractAuthenticationToken convert(Jwt jwt) {return new CustomAuthenticationToken(jwt);}
}

6.8.6配置验证

使用最小Spring Boot配置,指示授权服务器的颁发者uri,资源服务器将默认验证iss声明以及expnbf时间戳声明。

在需要自定义验证的情况下,Resource Server附带两个标准验证器,并且还接受自定义OAuth2TokenValidator实例。

自定义时间戳验证

JWT通常有一个有效窗口,nbf声明中指示的窗口的开头和exp声明中指出的结尾。

但是,每个服务器都可能遇到时钟漂移,这可能导致令牌过期到一个服务器,但不会到另一个服务器。随着协作服务器数量在分布式系统中的增加,这可能会导致一些实施灼伤。

资源服务器使用JwtTimestampValidator验证令牌的有效性窗口,并且可以使用clockSkew配置它以缓解上述问题:

@Bean
JwtDecoder jwtDecoder() {NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport)JwtDecoders.withOidcIssuerLocation(issuerUri);OAuth2TokenValidator withClockSkew = new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(Duration.ofSeconds(60)),new IssuerValidator(issuerUri));jwtDecoder.setJwtValidator(withClockSkew);return jwtDecoder;
}
[注意]注意

默认情况下,资源服务器配置30秒的时钟偏差。

配置自定义验证程序

使用OAuth2TokenValidator API添加对aud声明的检查很简单:

public class AudienceValidator implements OAuth2TokenValidator {OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);public OAuth2TokenValidatorResult validate(Jwt jwt) {if (jwt.getAudience().contains("messaging")) {return OAuth2TokenValidatorResult.success();} else {return OAuth2TokenValidatorResult.failure(error);}}
}

然后,要添加到资源服务器,需要指定JwtDecoder实例:

@Bean
JwtDecoder jwtDecoder() {NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport)JwtDecoders.withOidcIssuerLocation(issuerUri);OAuth2TokenValidator audienceValidator = new AudienceValidator();OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);jwtDecoder.setJwtValidator(withAudience);return jwtDecoder;
}

6.8.7配置声明集映射

Spring Security使用Nimbus库解析JWT并验证其签名。因此,Spring Security受Nimbus对每个字段值的解释以及如何将每个字段强制转换为Java类型。

例如,因为Nimbus与Java 7兼容,所以它不使用Instant来表示时间戳字段。

并且完全可以使用不同的库或JWT处理,这可能会使自己的强制决策需要调整。

或者,很简单,资源服务器可能希望根据特定域的原因添加或删除JWT中的声明。

出于这些目的,Resource Server支持使用MappedJwtClaimSetConverter映射JWT声明集。

自定义单个声明的转换

默认情况下,MappedJwtClaimSetConverter会尝试将声明强制转换为以下类型:

Claim

Java Type

aud

Collection

exp

Instant

iat

Instant

iss

String

jti

String

nbf

Instant

sub

String

可以使用MappedJwtClaimSetConverter.withDefaults配置个人声明的转化策略:

@Bean
JwtDecoder jwtDecoder() {NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));jwtDecoder.setJwtClaimSetConverter(converter);return jwtDecoder;
}

这将保留所有默认值,但它将覆盖sub的默认声明转换器。

添加声明

MappedJwtClaimSetConverter也可用于添加自定义声明,例如,以适应现有系统:

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));

删除索赔

使用相同的API删除声明也很简单:

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));

重命名声明

在更复杂的场景中,例如一次咨询多个声明或重命名声明,Resource Server接受任何实现Converter, Map>的类:

public class UsernameSubClaimAdapter implements Converter, Map> {private final MappedJwtClaimSetConverter delegate =MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());public Map convert(Map claims) {Map convertedClaims = this.delegate.convert(claims);String username = (String) convertedClaims.get("user_name");convertedClaims.put("sub", username);return convertedClaims;}
}

然后,可以像平常一样提供实例:

@Bean
JwtDecoder jwtDecoder() {NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);jwtDecoder.setJwtClaimSetConverter(new UsernameSubClaimAdapter());return jwtDecoder;
}

6.8.8配置超时

默认情况下,Resource Server使用每个30秒的连接和套接字超时来协调授权服务器。

在某些情况下,这可能太短。此外,它没有考虑更复杂的模式,如退避和发现。

要调整Resource Server连接到授权服务器的方式,NimbusJwtDecoderJwkSupport接受RestOperations的实例:

@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {RestOperations rest = builder.setConnectionTimeout(60000).setReadTimeout(60000).build();NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);jwtDecoder.setRestOperations(rest);return jwtDecoder;
}

6.9认证

到目前为止,我们只看了最基本的身份验证配置。我们来看一些稍微更高级的配置身份验证选项。

6.9.1内存中认证

我们已经看到了为单个用户配置内存中身份验证的示例。以下是配置多个用户的示例:

@Bean
public UserDetailsService userDetailsService() throws Exception {// ensure the passwords are encoded properlyUserBuilder users = User.withDefaultPasswordEncoder();InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(users.username("user").password("password").roles("USER").build());manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build());return manager;
}

6.9.2 JDBC身份验证

您可以找到支持基于JDBC的身份验证的更新。以下示例假定您已在应用程序中定义了DataSource。该JDBC-javaconfig样品提供了使用基于JDBC认证的一个完整的示例。

@Autowired
private DataSource dataSource;@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {// ensure the passwords are encoded properlyUserBuilder users = User.withDefaultPasswordEncoder();auth.jdbcAuthentication().dataSource(dataSource).withDefaultSchema().withUser(users.username("user").password("password").roles("USER")).withUser(users.username("admin").password("password").roles("USER","ADMIN"));
}

6.9.3 LDAP认证

您可以找到支持基于LDAP的身份验证的更新。的LDAP的javaconfig样品提供了使用基于LDAP的认证的完整的例子。

@Autowired
private DataSource dataSource;@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {auth.ldapAuthentication().userDnPatterns("uid={0},ou=people").groupSearchBase("ou=groups");
}

上面的示例使用以下LDIF和嵌入式Apache DS LDAP实例。

users.ldif。 

dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groupsdn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: peopledn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: passworddn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: passworddn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=orgdn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org

6.9.4 AuthenticationProvider

您可以通过将自定义AuthenticationProvider公开为bean来定义自定义身份验证。例如,假设SpringAuthenticationProvider实现AuthenticationProvider,以下将自定义身份验证:

[注意]注意

仅在未填充AuthenticationManagerBuilder时使用此选项

@Bean
public SpringAuthenticationProvider springAuthenticationProvider() {return new SpringAuthenticationProvider();
}

6.9.5 UserDetailsS​​ervice

您可以通过将自定义UserDetailsService公开为bean来定义自定义身份验证。例如,假设SpringDataUserDetailsService实现UserDetailsService,以下将自定义身份验证:

[注意]注意

仅在未填充AuthenticationManagerBuilder并且未定义AuthenticationProviderBean时使用此选项。

@Bean
public SpringDataUserDetailsService springDataUserDetailsService() {return new SpringDataUserDetailsService();
}

您还可以通过将PasswordEncoder暴露为bean来自定义密码的编码方式。例如,如果使用bcrypt,则可以添加bean定义,如下所示:

@Bean
public BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();
}

6.10多个HttpSecurity

我们可以配置多个HttpSecurity实例,就像我们可以有多个块一样。关键是多次延长WebSecurityConfigurationAdapter。例如,以下是具有以/api/开头的URL的不同配置的示例。

@EnableWebSecurity
public class MultiHttpSecurityConfig {@Bean                                                             1public UserDetailsService userDetailsService() throws Exception {// ensure the passwords are encoded properlyUserBuilder users = User.withDefaultPasswordEncoder();InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(users.username("user").password("password").roles("USER").build());manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build());return manager;}@Configuration@Order(1)                                                        2public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {protected void configure(HttpSecurity http) throws Exception {http.antMatcher("/api/**")                               3.authorizeRequests().anyRequest().hasRole("ADMIN").and().httpBasic();}}@Configuration                                                   4public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin();}}
}

1

正常配置身份验证

2

创建包含@OrderWebSecurityConfigurerAdapter实例,以指定应首先考虑哪个WebSecurityConfigurerAdapter

3

http.antMatcher表示此HttpSecurity仅适用于以/api/开头的网址

4

创建WebSecurityConfigurerAdapter的另一个实例。如果URL不以/api/开头,则将使用此配置。此配置在ApiWebSecurityConfigurationAdapter之后考虑,因为它在1之后具有@Order值(没有@Order默认为持续)。

6.11方法安全

从版本2.0开始,Spring Security大大提高了对服务层方法的安全性的支持。它支持JSR-250注释安全性以及框架的原始@Secured注释。从3.0开始,您还可以使用基于表达式的新注释。您可以将安全性应用于单个bean,使用intercept-methods元素来装饰bean声明,或者可以使用AspectJ样式切入点在整个服务层中保护多个bean。

6.11.1 EnableGlobalMethodSecurity

我们可以在任何@Configuration实例上使用@EnableGlobalMethodSecurity注释启用基于注释的安全性。例如,以下内容将启用Spring Security的@Secured注释。

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}

然后,在方法(类或接口)上添加注释会相应地限制对该方法的访问。Spring Security的本机注释支持为该方法定义了一组属性。这些将传递给AccessDecisionManager,以便做出实际决定:

public interface BankService {@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

可以使用支持JSR-250注释

@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}

这些是基于标准的,允许应用简单的基于角色的约束,但没有强大的Spring Security本机注释。要使用新的基于表达式的语法,您可以使用

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}

和等效的Java代码

public interface BankService {@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);@PreAuthorize("isAnonymous()")
public Account[] findAccounts();@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

6.11.2 GlobalMethodSecurityConfiguration

有时您可能需要执行比@EnableGlobalMethodSecurity注释允许更复杂的操作。对于这些实例,您可以扩展GlobalMethodSecurityConfiguration,确保子类上存在@EnableGlobalMethodSecurity注释。例如,如果要提供自定义MethodSecurityExpressionHandler,可以使用以下配置:

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {@Overrideprotected MethodSecurityExpressionHandler createExpressionHandler() {// ... create and return custom MethodSecurityExpressionHandler ...return expressionHandler;}
}

有关可以覆盖的方法的其他信息,请参阅GlobalMethodSecurityConfiguration Javadoc。

6.12后处理配置对象

Spring Security的Java配置不会公开它配置的每个对象的每个属性。这简化了大多数用户的配置。毕竟,如果每个属性都被暴露,用户可以使用标准bean配置。

虽然有充分的理由不直接公开每个属性,但用户可能仍需要更高级的配置选项。为了解决这个问题,Spring Security引入了ObjectPostProcessor的概念,可以用来修改或替换Java配置创建的许多Object实例。例如,如果要在FilterSecurityInterceptor上配置filterSecurityPublishAuthorizationSuccess属性,可以使用以下命令:

@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().withObjectPostProcessor(new ObjectPostProcessor() {public  O postProcess(O fsi) {fsi.setPublishAuthorizationSuccess(true);return fsi;}});
}

6.13自定义DSL

您可以在Spring Security中提供自己的自定义DSL。例如,您可能看起来像这样:

public class MyCustomDsl extends AbstractHttpConfigurer {private boolean flag;@Overridepublic void init(H http) throws Exception {// any method that adds another configurer// must be done in the init methodhttp.csrf().disable();}@Overridepublic void configure(H http) throws Exception {ApplicationContext context = http.getSharedObject(ApplicationContext.class);// here we lookup from the ApplicationContext. You can also just create a new instance.MyFilter myFilter = context.getBean(MyFilter.class);myFilter.setFlag(flag);http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);}public MyCustomDsl flag(boolean value) {this.flag = value;return this;}public static MyCustomDsl customDsl() {return new MyCustomDsl();}
}
[注意]注意

这实际上是如何实现像HttpSecurity.authorizeRequests()这样的方法。

然后可以像这样使用自定义DSL:

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.apply(customDsl()).flag(true).and()...;}
}

代码按以下顺序调用:

  • 调用`Config`s configure方法中的代码
  • 调用`MyCustomDsl的init方法中的代码
  • 调用`MyCustomDsl的configure方法中的代码

如果需要,您可以WebSecurityConfiguerAdapter默认使用SpringFactories添加MyCustomDsl。例如,您将在名为META-INF/spring.factories的类路径上创建一个资源,其中包含以下内容:

META-INF / spring.factories。 

org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyCustomDsl

希望禁用默认值的用户可以明确地这样做。

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.apply(customDsl()).disable()...;}
}

7.安全命名空间配置

7.1简介

自Spring Framework版本2.0起,命名空间配置已可用。它允许您使用其他XML模式中的元素来补充传统的Spring bean应用程序上下文语法。您可以在Spring 参考文档中找到更多信息。命名空间元素可以简单地用于允许更简洁的方式来配置单个bean,或者更有力地用于定义替代配置语法,该语法更紧密地匹配问题域并且隐藏用户的底层复杂性。一个简单的元素可能会隐藏多个bean和处理步骤被添加到应用程序上下文的事实。例如,将以下元素从安全名称空间添加到应用程序上下文将启动嵌入式LDAP服务器,以便在应用程序中测试使用:


这比连接等效的Apache Directory Server bean简单得多。ldap-server元素上的属性支持最常见的替代配置要求,并且用户不必担心他们需要创建哪些bean以及bean属性名称是什么。 [1]。在编辑应用程序上下文文件时使用良好的XML编辑器应该提供有关可用属性和元素的信息。我们建议您试用Spring工具套件,因为它具有处理标准Spring命名空间的特殊功能。

要在应用程序上下文中开始使用安全命名空间,您需要在类路径上使用spring-security-config jar。然后,您需要做的就是将架构声明添加到应用程序上下文文件中:

...

在您将看到的许多示例中(以及示例应用程序中),我们经常使用“security”作为默认命名空间而不是“beans”,这意味着我们可以在所有安全命名空间元素上省略前缀,从而制作内容更容易阅读。如果您将应用程序上下文划分为单独的文件并在其中一个文件中包含大部分安全配置,则可能还需要执行此操作。然后,您的安全应用程序上下文文件将如下所示

...

我们假设从现在开始在本章中使用了这种语法。

7.1.1命名空间的设计

命名空间旨在捕获框架的最常见用法,并提供简化和简洁的语法,以便在应用程序中启用它们。该设计基于框架内的大规模依赖性,可分为以下几个方面:

  • Web / HTTP安全 - 最复杂的部分。设置用于应用框架身份验证机制的过滤器和相关服务bean,保护URL,呈现登录和错误页面等等。
  • 业务对象(方法)安全性 - 保护服务层的选项。
  • AuthenticationManager - 处理来自框架其他部分的身份验证请求。
  • AccessDecisionManager - 为web和方法安全性提供访问决策。将注册一个默认值,但您也可以选择使用使用普通Spring bean语法声明的自定义一个。
  • AuthenticationProvider s - 身份验证管理器对用户进行身份验证的机制。命名空间提供了对多个标准选项的支持,也提供了添加使用传统语法声明的自定义bean的方法。
  • UserDetailsS​​ervice - 与身份验证提供程序密切相关,但通常也需要其他bean。

我们将在以下部分中看到如何配置它们。

7.2安全命名空间配置入门

在本节中,我们将介绍如何构建命名空间配置以使用框架的一些主要功能。假设您最初希望尽快启动并运行,并将认证支持和访问控制添加到现有的web应用程序,并进行一些测试登录。然后,我们将了解如何更改以对数据库或其他安全存储库进行身份验证。在后面的部分中,我们将介绍更高级的命名空间配置选项。

7.2.1 web。xml配置

您需要做的第一件事是将以下过滤器声明添加到您的web.xml文件中:


springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy

springSecurityFilterChain
/*

这为Spring Security web基础设施提供了一个钩子。DelegatingFilterProxy是一个Spring Framework类,它委托给一个过滤器实现,在你的应用程序上下文中定义为一个Spring bean。在这种情况下,bean被命名为“springSecurityFilterChain”,它是由命名空间创建的内部基础结构bean,用于处理web安全性。请注意,您不应自己使用此bean名称。将此添加到web.xml后,您就可以开始编辑应用程序上下文文件了。使用元素配置Web安全服务。

7.2.2最小配置

启用web安全性所需的全部内容是






这表示我们希望应用程序中的所有URL都是安全的,需要角色ROLE_USER来访问它们,我们希望使用带有用户名和密码的表单登录到应用程序,并且我们希望注册的注销URL将允许我们退出应用程序。元素是所有与web相关的命名空间功能的父元素。元素定义了pattern,它使用ant路径样式语法[2]与传入请求的URL进行匹配。您还可以使用正则表达式匹配作为替代方法(有关详细信息,请参阅命名空间附录)。access属性定义与给定模式匹配的请求的访问要求。使用默认配置时,这通常是以逗号分隔的角色列表,其中一个角色必须允许用户发出请求。前缀“ROLE_”是一个标记,表示应该与用户的权限进行简单比较。换句话说,应该使用正常的基于角色的检查。Spring Security中的访问控制不仅限于使用简单角色(因此使用前缀来区分不同类型的安全属性)。稍后我们将看到解释如何变化脚注:[access属性中逗号分隔值的解释取决于所使用的-1-的实现。在Spring Security 3.0中,该属性也可以用-2-填充。

[注意]注意

===

您可以使用多个元素为不同的URL集定义不同的访问要求,但它们将按列出的顺序进行评估,并将使用第一个匹配项。所以你必须把最具体的比赛放在最上面。您还可以添加method属性以限制与特定HTTP方法(GETPOSTPUT等)的匹配。

===

要添加一些用户,您可以直接在命名空间中定义一组测试数据:





这是存储相同密码的安全方式的示例。密码以{bcrypt}为前缀,指示DelegatingPasswordEncoder,它支持任何配置的PasswordEncoder匹配,密码使用BCrypt进行哈希处理:





如果您熟悉框架的命名空间前版本,那么您可能已经大致猜测了这里发生了什么。元素负责创建FilterChainProxy及其使用的过滤器bean。由于过滤器位置是预定义的,因此不正确的过滤器排序等常见问题不再是问题。

元素创建一个DaoAuthenticationProvider bean,元素创建一个InMemoryDaoImpl。所有authentication-provider元素必须是元素的子元素,这将创建ProviderManager并向其注册身份验证提供程序。您可以在命名空间附录中找到有关bean创建的更多详细信息。如果您想要开始了解框架中的重要类以及它们的使用方式,特别是如果您想稍后自定义内容,则值得交叉检查。

上面的配置定义了两个用户,他们的密码和他们在应用程序中的角色(将用于访问控制)。也可以使用user-service上的properties属性从标准属性文件加载用户信息。有关文件格式的更多详细信息,请参阅内存中身份验证部分。使用元素意味着身份验证管理器将使用用户信息来处理身份验证请求。您可以使用多个元素来定义不同的身份验证源,并依次查阅每个身份验证源。

此时,您应该可以启动应用程序,并且您将需要登录才能继续。尝试一下,或尝试尝试项目附带的“教程”示例应用程序。

7.2.3表单和基本登录选项

当您被提示登录时,您可能想知道登录表单的来源,因为我们没有提及任何HTML文件或JSP。事实上,由于我们没有明确设置登录页面的URL,Spring Security会根据启用的功能自动生成一个URL,并使用处理提交的登录的URL的标准值,默认目标URL用户将在登录后发送,等等。但是,命名空间提供了大量支持,允许您自定义这些选项。例如,如果要提供自己的登录页面,可以使用:






另请注意,我们添加了一个额外的intercept-url元素,表示任何对登录页面的请求都应该可供匿名用户[3]以及AuthenticatedVoter类使用,以获取有关如何处理值IS_AUTHENTICATED_ANONYMOUSLY的更多详细信息]。否则,请求将与模式/ **匹配,并且无法访问登录页面本身!这是一个常见的配置错误,将导致应用程序中出现无限循环。如果您的登录页面看起来是安全的,Spring Security将在日志中发出警告。通过为模式定义单独的http元素,也可以使所有与特定模式匹配的请求完全绕过安全过滤器链:






从Spring Security 3.1开始,现在可以使用多个http元素为不同的请求模式定义单独的安全过滤器链配置。如果http元素中省略了pattern属性,则它匹配所有请求。创建不安全模式是此语法的一个简单示例,其中模式映射到空过滤器链[4]。我们将在有关安全过滤器链的章节中更详细地介绍这种新语法。

重要的是要意识到这些不安全的请求将完全忽略任何Spring Security web相关配置或requires-channel等附加属性,因此您将无法访问有关当前用户或呼叫的信息请求期间的安全方法。如果您仍希望应用安全过滤器链,请使用access='IS_AUTHENTICATED_ANONYMOUSLY'作为替代。

如果要使用基本身份验证而不是表单登录,请将配置更改为





然后,基本身份验证将优先,并将用于在用户尝试访问受保护资源时提示登录。如果您希望使用表单登录,则仍可在此配置中使用表单登录,例如通过嵌入在另一个web页面中的登录表单。

设置默认的登录后目的地

如果尝试访问受保护资源时未提示表单登录,则default-target-url选项将起作用。这是用户成功登录后将使用的URL,默认为“/”。您还可以通过将always-use-default-target属性设置为“true”来配置事物,以便用户始终在此页面结束(无论登录是“按需”还是明确选择登录)。如果您的应用程序始终要求用户在“主页”页面启动,则此选项非常有用,例如:






要进一步控制目标,可以使用authentication-success-handler-ref属性作为default-target-url的替代。引用的bean应该是AuthenticationSuccessHandler的实例。您可以在Core Filters一章以及命名空间附录中找到更多相关信息,以及有关如何在身份验证失败时自定义流的信息。

7.2.4注销处理

logout元素通过导航到特定URL添加了对注销的支持。默认的注销URL为/logout,但您可以使用logout-url属性将其设置为其他内容。有关其他可用属性的更多信息,请参见命名空间附录。

7.2.5使用其他身份验证提供程序

实际上,除了添加到应用程序上下文文件中的一些名称之外,您还需要一个更具伸缩性的用户信息源。您很可能希望将用户信息存储在数据库或LDAP服务器中。LDAP命名空间配置在LDAP章节中处理,因此我们不在此处介绍它。如果您的应用程序上下文中有Spring Security的UserDetailsService自定义实现,名为“myUserDetailsS​​ervice”,那么您可以使用以下方法对此进行身份验证



如果要使用数据库,则可以使用





其中“securityDataSource”是应用程序上下文中DataSource bean的名称,指向包含标准Spring Security 用户数据表的数据库。或者,您可以使用user-service-ref属性配置Spring Security JdbcDaoImpl bean并指向该bean:






您还可以使用标准AuthenticationProvider bean,如下所示



其中myAuthenticationProvider是应用程序上下文中实现AuthenticationProvider的bean的名称。您可以使用多个authentication-provider元素,在这种情况下,将按照声明的顺序查询提供程序。有关如何使用命名空间配置Spring Security AuthenticationManager的更多信息,请参见第7.6节“身份验证管理器和命名空间”。

添加密码编码器

应始终使用为此目的设计的安全散列算法对密码进行编码(不是像SHA或MD5这样的标准算法)。这是元素支持的。使用bcrypt编码的密码,原始身份验证提供程序配置如下所示:





对于大多数情况,bcrypt是一个不错的选择,除非你有一个强制你使用不同算法的遗留系统。如果您使用简单的散列算法,或者更糟糕的是,存储纯文本密码,那么您应该考虑迁移到更安全的选项,如bcrypt。

7.3高级Web功能

7.3.1记住我的身份验证

有关remember-me命名空间配置的信息,请参阅单独的Remember-Me章节。

7.3.2添加HTTP / HTTPS通道安全性

如果您的应用程序同时支持HTTP和HTTPS,并且您要求只能通过HTTPS访问特定URL,则使用上的requires-channel属性直接支持此功能:




...

有了这种配置,如果用户尝试使用HTTP访问与“/ secure / **”模式匹配的任何内容,它们将首先被重定向到HTTPS URL [5]。可用选项为“http”,“https”或“any”。使用值“any”表示可以使用HTTP或HTTPS。

如果您的应用程序使用HTTP和/或HTTPS的非标准端口,则可以指定端口映射列表,如下所示:


...



请注意,为了确保安全,应用程序根本不应使用HTTP或在HTTP和HTTPS之间切换。它应该以HTTPS(用户输入HTTPS URL)开始,并始终使用安全连接,以避免任何中间人攻击的可能性。

7.3.3会话管理

检测超时

您可以配置Spring Security以检测无效会话ID的提交,并将用户重定向到适当的URL。这是通过session-management元素实现的:


...


请注意,如果您使用此机制来检测会话超时,则可能会错误地报告错误,如果用户注销,然后在不关闭浏览器的情况下重新登录。这是因为当您使会话无效时,会话cookie不会被清除,即使用户已注销,也会重新提交。您可以在注销时显式删除JSESSIONID cookie,例如在注销处理程序中使用以下语法:




不幸的是,这不能保证与每个servlet容器一起使用,因此您需要在您的环境中对其进行测试

[注意]注意

===如果您在代理后面运行应用程序,您也可以通过配置代理服务器来删除会话cookie。例如,使用Apache HTTPD的mod_headers,以下指令将通过在对注销请求的响应中使其失效来删除JSESSIONID cookie(假设应用程序部署在路径/tutorial下):


Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"

===

并发会话控制

如果您希望对单个用户登录应用程序的能力施加约束,Spring Security通过以下简单添加支持此开箱即用。首先,您需要将以下侦听器添加到web.xml文件中,以使Spring Security更新有关会话生命周期事件的信息:


org.springframework.security.web.session.HttpSessionEventPublisher


然后将以下行添加到应用程序上下文中:


...



这将阻止用户多次登录 - 第二次登录将导致第一次失效。通常您更愿意阻止第二次登录,在这种情况下您可以使用


...



然后将拒绝第二次登录。“被拒绝”是指如果使用基于表单的登录,用户将被发送到authentication-failure-url。如果第二次认证是通过另一种非交互机制发生的,例如“记住我”,则会向客户端发送“未授权”(401)错误。如果您想要使用错误页面,则可以将属性session-authentication-error-url添加到session-management元素。

如果您使用自定义身份验证筛选器进行基于表单的登录,则必须明确配置并发会话控制支持。更多详细信息可在“ 会话管理”一章中找到。

会话固定攻击保护

会话固定攻击是潜在的风险,恶意攻击者可能通过访问站点来创建会话,然后说服其他用户使用相同的会话登录(通过向他们发送包含会话标识符作为参数的链接,例)。Spring Security通过在用户登录时创建新会话或以其他方式更改会话ID来自动防止这种情况。如果您不需要此保护,或者它与某些其他要求冲突,您可以使用{来控制行为上的2 /}属性,有四个选项

  • none - 不要做任何事。原始会话将保留。
  • newSession - 创建一个新的“干净”会话,而不复制现有会话数据(Spring Security - 相关属性仍将被复制)。
  • migrateSession - 创建新会话并将所有现有会话属性复制到新会话。这是Servlet 3.0或旧容器中的默认设置。
  • changeSessionId - 不要创建新会话。而是使用Servlet容器(HttpServletRequest#changeSessionId())提供的会话固定保护。此选项仅适用于Servlet 3.1(Java EE 7)和更新的容器。在旧容器中指定它将导致异常。这是Servlet 3.1和更新容器中的默认设置。

发生会话固定保护时,会导致在应用程序上下文中发布SessionFixationProtectionEvent。如果您使用changeSessionId,这种保护会导致任何javax.servlet.http.HttpSessionIdListener S是通知,所以如果你的代码侦听这两个事件谨慎使用。有关其他信息,请参阅会话管理章节。

7.3.4 OpenID支持

除了普通的基于表单的登录之外,命名空间支持OpenID登录,只需进行简单的更改:





然后,您应该使用OpenID提供程序(例如myopenid.com)注册自己,并将用户信息添加到内存中


您应该能够使用myopenid.com站点进行身份验证登录。通过在openid-login元素上设置user-service-ref属性,也可以选择特定的UserDetailsService bean来使用OpenID。有关更多信息,请参阅上一节有关身份验证提供程 请注意,我们已从上述用户配置中省略了password属性,因为此组用户数据仅用于加载用户的权限。将在内部生成随机密码,以防止您意外地将此用户数据用作配置中其他位置的身份验证源。

属性交换

支持OpenID 属性交换。例如,以下配置将尝试从OpenID提供程序检索电子邮件和全名,以供应用程序使用:





每个OpenID属性的“类型”是由特定模式确定的URI,在本例中为http://axschema.org/。如果必须检索属性以进行成功验证,则可以设置required属性。支持的确切架构和属性取决于您的OpenID提供程序。属性值作为身份验证过程的一部分返回,之后可以使用以下代码进行访问:

OpenIDAuthenticationToken token =(OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
List attributes = token.getAttributes();

OpenIDAttribute包含属性类型和检索到的值(或多值属性的值)。在我们查看技术概述章节中的核心Spring Security组件时,我们将看到更多关于如何使用SecurityContextHolder类的信息。如果您希望使用多个身份提供程序,也支持多个属性交换配置。您可以提供多个attribute-exchange元素,每个元素使用identifier-matcher属性。它包含一个正则表达式,该表达式将与用户提供的OpenID标识符进行匹配。请参阅代码库中的OpenID示例应用程序以获取示例配置,为Google,Yahoo和MyOpenID提供程序提供不同的属性列表。

7.3.5响应标头

有关如何自定义headers元素的其他信息,请参阅参考的第10.8节“安全HTTP响应标头”部分。

7.3.6添加自己的过滤器

如果您以前使用过Spring Security,那么您将知道该框架维护了一系列过滤器以便应用其服务。您可能希望将自己的过滤器添加到特定位置的堆栈,或者使用当前没有命名空间配置选项的Spring Security过滤器(例如,CAS)。或者您可能希望使用标准命名空间过滤器的自定义版本,例如由元素创建的UsernamePasswordAuthenticationFilter,利用明确使用bean可用的一些额外配置选项。如何通过名称空间配置来完成此操作,因为过滤器链未直接公开?

使用命名空间时,始终严格执行过滤器的顺序。在创建应用程序上下文时,过滤器bean按名称空间处理代码进行排序,标准Spring Security过滤器在名称空间中都有一个别名和一个众所周知的位置。

[注意]注意

===在以前的版本中,排序发生在创建过滤器实例之后,在应用程序上下文的后处理期间。在版本3.0+中,现在在实例化类之前,在bean元数据级别完成排序。这对于如何将自己的过滤器添加到堆栈有影响,因为在解析元素期间必须知道整个过滤器列表,因此语法在3.0中略有改变。===

表7.1“标准过滤器别名和排序”中显示了创建过滤器的过滤器,别名和名称空间元素/属性。过滤器按它们在过滤器链中出现的顺序列出。

表7.1。标准过滤器别名和订购

别号过滤类命名空间元素或属性

CHANNEL_FILTER

ChannelProcessingFilter

http/intercept-url@requires-channel

SECURITY_CONTEXT_FILTER

SecurityContextPersistenceFilter

http

CONCURRENT_SESSION_FILTER

ConcurrentSessionFilter

session-management/concurrency-control

HEADERS_FILTER

HeaderWriterFilter

http/headers

CSRF_FILTER

CsrfFilter

http/csrf

LOGOUT_FILTER

LogoutFilter

http/logout

X509_FILTER

X509AuthenticationFilter

http/x509

PRE_AUTH_FILTER

AbstractPreAuthenticatedProcessingFilter Subclasses

N/A

CAS_FILTER

CasAuthenticationFilter

N/A

FORM_LOGIN_FILTER

UsernamePasswordAuthenticationFilter

http/form-login

BASIC_AUTH_FILTER

BasicAuthenticationFilter

http/http-basic

SERVLET_API_SUPPORT_FILTER

SecurityContextHolderAwareRequestFilter

http/@servlet-api-provision

JAAS_API_SUPPORT_FILTER

JaasApiIntegrationFilter

http/@jaas-api-provision

REMEMBER_ME_FILTER

RememberMeAuthenticationFilter

http/remember-me

ANONYMOUS_FILTER

AnonymousAuthenticationFilter

http/anonymous

SESSION_MANAGEMENT_FILTER

SessionManagementFilter

session-management

EXCEPTION_TRANSLATION_FILTER

ExceptionTranslationFilter

http

FILTER_SECURITY_INTERCEPTOR

FilterSecurityInterceptor

http

SWITCH_USER_FILTER

SwitchUserFilter

N/A

您可以使用custom-filter元素和其中一个名称将自己的过滤器添加到堆栈,以指定过滤器应显示的位置:




如果希望在堆栈中的另一个过滤器之前或之后插入过滤器,也可以使用afterbefore属性。名称“FIRST”和“LAST”可与position属性一起使用,以指示您希望过滤器分别出现在整个堆栈之前或之后。

[小费]避免过滤器位置冲突

===

如果要插入的自定义过滤器可能与命名空间创建的标准过滤器之一占据相同的位置,则重要的是不要错误地包含命名空间版本。删除任何创建要替换其功能的过滤器的元素。

请注意,您无法替换使用元素本身创建的过滤器 - SecurityContextPersistenceFilterExceptionTranslationFilterFilterSecurityInterceptor。默认情况下会添加其他一些过滤器,但您可以禁用它们。默认情况下会添加AnonymousAuthenticationFilter,除非您禁用会话固定保护,否则还会在过滤器链中添加SessionManagementFilter

===

如果要替换需要身份验证入口点的命名空间过滤器(即,未经身份验证的用户尝试访问安全资源而触发身份验证过程),则还需要添加自定义入口点bean。

设置自定义AuthenticationEntryPoint

如果您没有使用表单登录,OpenID或通过命名空间进行基本身份验证,您可能需要使用传统的bean语法定义身份验证过滤器和入口点,并将它们链接到命名空间,如我们刚才所见。可以使用元素上的entry-point-ref属性设置相应的AuthenticationEntryPoint

CAS示例应用程序是使用带有命名空间的自定义bean的一个很好的示例,包括此语法。如果您不熟悉身份验证入口点,则会在技术概述一章中讨论它们。

7.4方法安全性

从版本2.0开始,Spring Security大大提高了对服务层方法的安全性的支持。它为JSR-250注释安全性以及框架的原始@Secured注释提供支持。从3.0开始,您还可以使用基于表达式的新注释。您可以将安全性应用于单个bean,使用intercept-methods元素来装饰bean声明,或者可以使用AspectJ样式切入点在整个服务层中保护多个bean。

7.4.1 元素

此元素用于在应用程序中启用基于注释的安全性(通过在元素上设置适当的属性),还可以将安全性切入点声明组合在一起,这些声明将应用于整个应用程序上下文。您应该只声​​明一个元素。以下声明将支持Spring Security的@Secured


然后,在方法(类或接口)上添加注释会相应地限制对该方法的访问。Spring Security的本机注释支持为该方法定义了一组属性。这些将传递给AccessDecisionManager,以便做出实际决定:

public interface BankService {@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

可以使用支持JSR-250注释


这些是基于标准的,允许应用简单的基于角色的约束,但没有强大的Spring Security本机注释。要使用新的基于表达式的语法,您可以使用


和等效的Java代码

public interface BankService {@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);@PreAuthorize("isAnonymous()")
public Account[] findAccounts();@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

如果您需要定义简单的规则,而不是根据用户的权限列表检查角色名称,那么基于表达式的注释是一个不错的选择。

[注意]注意

===仅对定义为Spring bean的实例(在启用了method-security的同一应用程序上下文中)保护带注释的方法。如果要保护不是由Spring创建的实例(例如,使用new运算符),则需要使用AspectJ。===

[注意]注意

===您可以在同一个应用程序中启用多种类型的注释,但是任何接口或类只应使用一种类型,否则行为将无法明确定义。如果找到适用于特定方法的两个注释,则只应用其中一个注释。===

使用protect-pointcut添加安全性切入点

protect-pointcut的使用特别强大,因为它允许您只使用简单的声明将安全性应用于许多bean。请考虑以下示例:




这将保护在应用程序上下文中声明的bean上的所有方法,这些bean的类在com.mycompany包中,其类名以“Service”结尾。只有具有ROLE_USER角色的用户才能调用这些方法。与URL匹配一样,最具体的匹配必须首先出现在切入点列表中,因为将使用第一个匹配表达式。安全注释优先于切入点。

7.5默认AccessDecisionManager

本节假设您已了解Spring Security内访问控制的基础架构。如果不这样做,您可以跳过它并稍后再回过头来看,因为本节仅对需要进行一些自定义以便使用简单基于角色的安全性的人员非常重要。

当您使用命名空间配置时,将自动为您注册AccessDecisionManager的默认实例,并将根据您在{3中指定的访问属性,用于为方法调用和web URL访问做出访问决策。 /}和protect-pointcut声明(如果使用注释安全方法,则在注释中)。

默认策略是使用带有RoleVoterAuthenticatedVoterAffirmativeBased AccessDecisionManager。您可以在授权章节中找到有关这些内容的更多信息。

7.5.1自定义AccessDecisionManager

如果您需要使用更复杂的访问控制策略,那么很容易为方法和web安全性设置替代方案。

对于方法安全性,可以通过将global-method-security上的access-decision-manager-ref属性设置为应用程序上下文中相应AccessDecisionManager bean的id来执行此操作:


...

web安全性的语法是相同的,但在http元素上:


...

7.6验证管理器和命名空间

在Spring Security中提供身份验证服务的主要接口是AuthenticationManager。这通常是Spring Security的ProviderManager类的一个实例,如果您之前使用过该框架,那么您可能已经熟悉了它。如果没有,稍后将在技术概述章节中介绍。使用authentication-manager命名空间元素注册bean实例。如果您通过命名空间使用HTTP或方法安全性,则不能使用自定义AuthenticationManager,但这不应该是一个问题,因为您可以完全控制所使用的AuthenticationProvider

您可能希望使用ProviderManager注册其他AuthenticationProvider bean,并且可以使用带有ref属性的元素来执行此操作,其中属性的值是提供者bean的名称你想要添加。例如:




...

另一个常见的要求是上下文中的另一个bean可能需要引用AuthenticationManager。您可以轻松注册AuthenticationManager的别名,并在应用程序上下文的其他位置使用此名称。


...


...


[1]您可以在第12.3节“LDAP身份验证”一章中找到有关使用ldap-server元素的更多信息。

[2]有关如何实际执行匹配的更多详细信息,请参阅Web应用程序基础结构一章中第10.1.4节“请求匹配和HttpFirewall”一节。

[3]参见第10.10节“匿名认证”一章

[4]使用多个元素是一个重要特性,例如,允许命名空间同时支持同一应用程序中的有状态和无状态路径。在intercept-url元素上使用属性filters="none"的先前语法与此更改不兼容,并且在3.1中不再受支持。

[5]有关如何实现通道处理的更多详细信息,请参阅ChannelProcessingFilter的Javadoc和相关类。

8.架构和实施

熟悉设置和运行某些基于命名空间配置的应用程序之后,您可能希望更多地了解框架在命名空间外观背后的实际工作方式。与大多数软件一样,Spring Security具有某些中心接口,类和概念抽象,这些都是整个框架中常用的。在参考指南的这一部分中,我们将查看其中的一些内容,并了解它们如何协同工作以支持Spring Security内的身份验证和访问控制。

8.1技术概述

8.1.1运行时环境

Spring Security 3.0需要Java 5.0 Runtime Environment或更高版本。由于Spring Security旨在以自包含方式运行,因此无需将任何特殊配置文件放入Java运行时环境中。特别是,无需配置特殊的Java身份验证和授权服务(JAAS)策略文件,也不需要将Spring Security放入常见的类路径位置。

同样,如果您使用的是EJB容器或Servlet容器,则无需在任何地方放置任何特殊配置文件,也不需要在服务器类加载器中包含Spring Security。所有必需的文件都将包含在您的应用程序中。

这种设计提供了最大的部署时间灵活性,因为您可以简单地将目标工件(无论是JAR,WAR还是EAR)从一个系统复制到另一个系统,它将立即起作用。

8.1.2核心组件

在Spring Security 3.0中,spring-security-core jar的内容被剥离到最低限度。它不再包含与web相关的任何代码 - 应用程序安全性,LDAP或命名空间配置。我们将在这里看一下您在核心模块中可以找到的一些Java类型。它们代表了框架的构建块,因此如果您需要超越简单的命名空间配置,那么即使您实际上不需要直接与它们进行交互,您也必须了解它们是什么。

SecurityContextHolder,SecurityContext和Authentication Objects

最基本的对象是SecurityContextHolder。这是我们存储应用程序当前安全上下文的详细信息的地方,其中包括当前使用该应用程序的主体的详细信息。默认情况下,SecurityContextHolder使用ThreadLocal来存储这些详细信息,这意味着安全上下文始终可用于同一执行线程中的方法,即使安全上下文未作为参数显式传递那些方法。如果在处理当前委托人的请求之后小心地清除线程,以这种方式使用ThreadLocal是非常安全的。当然,Spring Security会自动为您解决这个问题,因此无需担心。

某些应用程序并不完全适合使用ThreadLocal,因为它们使用线程的特定方式。例如,Swing客户端可能希望Java虚拟机中的所有线程都使用相同的安全上下文。SecurityContextHolder可以在启动时配置策略,以指定您希望如何存储上下文。对于独立应用程序,您将使用SecurityContextHolder.MODE_GLOBAL策略。其他应用程序可能希望安全线程生成的线程也采用相同的安全标识。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL来实现的。您可以通过两种方式从默认SecurityContextHolder.MODE_THREADLOCAL更改模式。第一个是设置系统属性,第二个是在SecurityContextHolder上调用静态方法。大多数应用程序不需要更改默认值,但如果这样做,请查看JavaDoc for SecurityContextHolder以了解更多信息。

获取有关当前用户的信息

SecurityContextHolder内,我们存储了当前与应用程序交互的主体的详细信息。Spring Security使用Authentication对象来表示此信息。您通常不需要自己创建Authentication对象,但用户查询Authentication对象是相当常见的。您可以使用以下代码块(从应用程序的任何位置)获取当前经过身份验证的用户的名称,例如:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

调用getContext()返回的对象是SecurityContext接口的实例。这是保存在线程本地存储中的对象。正如我们将在下面看到的,Spring Security中的大多数认证机制都返回UserDetails的实例作为主体。

UserDetailsS​​ervice

上面代码片段中需要注意的另一个问题是,您可以从Authentication对象获取主体。校长只是Object。大多数情况下,这可以转换为UserDetails对象。UserDetails是Spring Security中的核心界面。它代表一个主体,但是以可扩展和特定于应用程序的方式。可以将UserDetails视为您自己的用户数据库与SecurityContextHolder内Spring Security所需的适配器之间的适配器。作为来自您自己的用户数据库的东西的表示,您经常会将UserDetails转换为您的应用程序提供的原始对象,因此您可以调用特定于业务的方法(如getEmail()getEmployeeNumber()和等等)。

到现在为止你可能想知道,所以我什么时候提供UserDetails对象?我怎么做?我以为你说这个东西是声明性的,我不需要编写任何Java代码 - 是什么给出的?简短的回答是有一个名为UserDetailsService的特殊界面。此接口上唯一的方法接受基于String的用户名参数并返回UserDetails

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

这是在Spring Security内为用户加载信息的最常用方法,只要需要有关用户的信息,您就会看到它在整个框架中使用。

上成功的认证,UserDetails被用来建立存储在SecurityContextHolder(关于这一点的Authentication对象下面)。好消息是我们提供了许多UserDetailsService实现,包括一个使用内存映射(InMemoryDaoImpl)和另一个使用JDBC(JdbcDaoImpl)的实现。但是,大多数用户倾向于自己编写,他们的实现通常只是位于代表其员工,客户或应用程序其他用户的现有数据访问对象(DAO)之上。记住使用上面的代码片段始终可以从SecurityContextHolder获得UserDetailsService返回的优点。

[注意]注意

UserDetailsService经常有些混乱。它纯粹是用户数据的DAO,除了将数据提供给框架内的其他组件之外,不执行任何其他功能。特别是,它不会对用户进行身份验证,这是由AuthenticationManager完成的。在许多情况下,如果您需要自定义身份验证过程,直接实现AuthenticationProvider会更有意义。

一个GrantedAuthority

除了校长之外,Authentication提供的另一个重要方法是getAuthorities()。此方法提供GrantedAuthority个对象的数组。毫不奇怪,GrantedAuthority是授予校长的权力。这些权力通常是“角色”,例如ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。稍后将为web授权,方法授权和域对象授权配置这些角色。Spring Security的其他部分能够解释这些权威,并期望它们存在。GrantedAuthority对象通常由UserDetailsService加载。

通常GrantedAuthority对象是应用程序范围的权限。它们不是特定于给定的域对象。因此,你不可能有一个GrantedAuthority代表Employee对象编号54的权限,因为如果有数千个这样的权限,你很快就会耗尽内存(或者,至少,因为应用程序需要很长时间来验证用户身份)。当然,Spring Security专门用于处理这个常见要求,但您可以使用项目的域对象安全功能来实现此目的。

摘要

回顾一下,到目前为止我们看到的Spring Security的主要构建块是:

  • SecurityContextHolder,提供SecurityContext的访问权限。
  • SecurityContext,保存Authentication和可能的特定于请求的安全信息。
  • Authentication,以特定于Spring Security的方式代表校长。
  • GrantedAuthority,以反映授予主体的应用程序范围的权限。
  • UserDetails,提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息。
  • UserDetailsService,在基于String的用户名(或证书ID等)中传递时创建UserDetails

既然您已经了解了这些重复使用的组件,那么让我们仔细看看身份验证过程。

8.1.3认证

Spring Security可以参与许多不同的身份验证环境。虽然我们建议人们使用Spring Security进行身份验证,而不是与现有的容器管理身份验证集成,但它仍然受到支持 - 与您自己的专有身份验证系统集成。

什么是Spring Security中的身份验证?

让我们考虑一个每个人都熟悉的标准身份验证方案。

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

前三项构成了身份验证过程,因此我们将在Spring Security内查看这些过程是如何发生的。

  1. 获取用户名和密码并将其合并到UsernamePasswordAuthenticationToken的实例中(我们之前看到的Authentication接口的实例)。
  2. 令牌被传递给AuthenticationManager的实例以进行验证。
  3. AuthenticationManager在成功验证后返回完全填充的Authentication实例。
  4. 通过调用SecurityContextHolder.getContext().setAuthentication(…​)建立安全上下文,传入返回的身份验证对象。

从那时起,用户被认为是经过身份验证的。我们来看一些代码作为例子。

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();public static void main(String[] args) throws Exception {BufferedReader in = new BufferedReader(new InputStreamReader(System.in));while(true) {System.out.println("Please enter your username:");String name = in.readLine();System.out.println("Please enter your password:");String password = in.readLine();try {Authentication request = new UsernamePasswordAuthenticationToken(name, password);Authentication result = am.authenticate(request);SecurityContextHolder.getContext().setAuthentication(result);break;} catch(AuthenticationException e) {System.out.println("Authentication failed: " + e.getMessage());}}System.out.println("Successfully authenticated. Security context contains: " +SecurityContextHolder.getContext().getAuthentication());
}
}class SampleAuthenticationManager implements AuthenticationManager {
static final List AUTHORITIES = new ArrayList();static {AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}public Authentication authenticate(Authentication auth) throws AuthenticationException {if (auth.getName().equals(auth.getCredentials())) {return new UsernamePasswordAuthenticationToken(auth.getName(),auth.getCredentials(), AUTHORITIES);}throw new BadCredentialsException("Bad Credentials");
}
}

在这里,我们编写了一个小程序,要求用户输入用户名和密码并执行上述顺序。我们在这里实现的AuthenticationManager将验证用户名和密码相同的任何用户。它为每个用户分配一个角色。上面的输出将是这样的:

Please enter your username:
bob
Please enter your password:
password
Authentication failed: Bad Credentials
Please enter your username:
bob
Please enter your password:
bob
Successfully authenticated. Security context contains: \
org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: \
Principal: bob; Password: [PROTECTED]; \
Authenticated: true; Details: null; \
Granted Authorities: ROLE_USER

请注意,您通常不需要编写任何类似的代码。该过程通常在内部进行,例如在web认证过滤器中。我们刚刚在这里包含了代码,以表明在Spring Security中实际构成身份验证的问题有一个非常简单的答案。当SecurityContextHolder包含完全填充的Authentication对象时,将对用户进行身份验证。

直接设置SecurityContextHolder内容

实际上,Spring Security并不介意如何将Authentication对象放在SecurityContextHolder中。唯一的关键要求是SecurityContextHolder包含Authentication,它代表AbstractSecurityInterceptor之前的主体(我们将在后面看到更多)需要授权用户操作。

您可以(以及许多用户)编写自己的过滤器或MVC控制器,以提供与不基于Spring Security的身份验证系统的互操作性。例如,您可能正在使用容器管理的身份验证,这使得当前用户可以从ThreadLocal或JNDI位置使用。或者,您可能会为拥有传统专有身份验证系统的公司工作,这是一个您无法控制的企业“标准”。在这种情况下,让Spring Security工作很容易,并且仍然提供授权功能。您需要做的就是编写一个过滤器(或等效的),从一个位置读取第三方用户信息,构建一个特定于Spring Security的Authentication对象,并将其放入SecurityContextHolder。在这种情况下,您还需要考虑内置身份验证基础结构通常会自动处理的事情。例如,在将响应写入客户端脚注之前,您可能需要先强制创建一个HTTP会话来缓存请求之间的上下文:[一旦提交响应,就无法创建会话。

如果您想知道如何在现实世界的例子中实现AuthenticationManager,我们将在核心服务章节中看一下。

8.1.4 Web应用程序中的身份验证

现在让我们来探讨在web应用程序中使用Spring Security的情况(未启用web.xml安全性)。如何对用户进行身份验证并建立安全上下文?

考虑典型的web应用程序的身份验证过程:

  1. 您访问主页,然后单击链接。
  2. 请求转到服务器,服务器确定您已请求受保护的资源。
  3. 由于您目前尚未通过身份验证,因此服务器会发回一个响应,指示您必须进行身份验证。响应将是HTTP响应代码,或重定向到特定的web页面。
  4. 根据身份验证机制,您的浏览器将重定向到特定的web页面,以便您可以填写表单,或者浏览器将以某种方式检索您的身份(通过BASIC身份验证对话框,cookie,X. 509证书等)。
  5. 浏览器将向服务器发回响应。这将是包含您填写的表单内容的HTTP POST,或者包含您的身份验证详细信息的HTTP标头。
  6. 接下来,服务器将决定所呈现的凭证是否有效。如果它们有效,则下一步将会发生。如果它们无效,通常会要求您的浏览器再次尝试(因此您将返回上面的第二步)。
  7. 将重试您进行身份验证过程的原始请求。希望您已通过足够授权的权限进行身份验证以访问受保护资源。如果您有足够的访问权限,请求将成功。否则,您将收到HTTP错误代码403,这意味着“禁止”。

Spring Security有不同的类负责上述大多数步骤。主要参与者(按照他们使用的顺序)是ExceptionTranslationFilterAuthenticationEntryPoint和“认证机制”,它负责调用我们在上一节中看到的AuthenticationManager

的ExceptionTranslationFilter

ExceptionTranslationFilter是一个Spring Security过滤器,负责检测抛出的任何Spring Security异常。AbstractSecurityInterceptor通常会抛出此类异常,这是授权服务的主要提供者。我们将在下一节讨论AbstractSecurityInterceptor,但是现在我们只需要知道它产生Java异常并且对HTTP一无所知或者如何对主体进行身份验证。相反,ExceptionTranslationFilter提供此服务,特别负责返回错误代码403(如果主体已经过身份验证,因此根本没有足够的访问权限 - 按照上面的步骤7),或者启动AuthenticationEntryPoint(如果校长尚未通过认证,因此我们需要开始第三步)。

的AuthenticationEntryPoint

AuthenticationEntryPoint负责上面列表中的第三步。可以想象,每个web应用程序都有一个默认的身份验证策略(好吧,这可以像Spring Security中的几乎所有其他配置一样配置,但现在让我们保持简单)。每个主要身份验证系统都有自己的AuthenticationEntryPoint实现,通常执行步骤3中描述的操作之一。

认证机制

一旦您的浏览器提交您的身份验证凭据(作为HTTP表单帖子或HTTP标头),服务器上就需要“收集”这些身份验证详细信息。到目前为止,我们已经在上面的列表中的第六步了。在Spring Security中,我们有一个特殊的名称,用于从用户代理(通常是web浏览器)收集身份验证详细信息,并将其称为“身份验证机制”。例如基于表单的登录和基本身份验证。一旦从用户代理收集了身份验证详细信息,就会构建一个Authentication“请求”对象,然后将其呈现给AuthenticationManager

在认证机制收到完全填充的Authentication对象后,它将认为请求有效,将Authentication放入SecurityContextHolder,并使原始请求重试(上面的步骤7)。另一方面,如果AuthenticationManager拒绝了请求,则认证机制将要求用户代理重试(上面的步骤2)。

在请求之间存储SecurityContext

根据应用程序的类型,可能需要采用策略来在用户操作之间存储安全上下文。在典型的web应用程序中,用户登录一次,然后由其会话ID标识。服务器缓存持续时间会话的主体信息。在Spring Security中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter上,默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession属性。它会为每个请求恢复上下文SecurityContextHolder,并且最重要的是,在请求完成时清除SecurityContextHolder。出于安全考虑,您不应直接与HttpSession进行交互。没有理由这样做 - 总是使用SecurityContextHolder

许多其他类型的应用程序(例如,无状态RESTful web服务)不使用HTTP会话,并将在每个请求上重新进行身份验证。但是,链中包含SecurityContextPersistenceFilter以确保在每次请求后清除SecurityContextHolder仍然很重要。

[注意]注意

在一个会话中接收并发请求的应用程序中,将在线程之间共享相同的SecurityContext实例。即使使用ThreadLocal,它也是从每个线程的HttpSession检索的相同实例。如果您希望临时更改运行线程的上下文,则会产生影响。如果您只使用SecurityContextHolder.getContext(),并在返回的上下文对象上调用setAuthentication(anAuthentication),则Authentication对象将在共享相同SecurityContext实例的所有并发线程中更改。您可以自定义SecurityContextPersistenceFilter的行为,为每个请求创建一个全新的SecurityContext,防止一个线程中的更改影响另一个请求。或者,您可以在临时更改上下文的位置创建新实例。方法SecurityContextHolder.createEmptyContext()总是返回一个新的上下文实例。

8.1.5 Spring Security中的访问控制(授权)

负责在Spring Security中做出访问控制决策的主界面是AccessDecisionManager。它有一个decide方法,它接受一个代表请求访问的主体的Authentication对象,一个“安全对象”(见下文)和一个适用于该对象的安全元数据属性列表(例如角色列表)这是获得访问所必需的。

安全和AOP建议

如果您熟悉AOP,您会发现有不同类型的建议可用:之前,之后,投掷和周围。around建议非常有用,因为顾问可以选择是否继续进行方法调用,是否修改响应,以及是否抛出异常。Spring Security提供了方法调用和web请求的周围建议。我们使用Spring的标准AOP支持为方法调用提供了一个周围的建议,我们使用标准过滤器为web请求提供了建议。

对于那些不熟悉AOP的人来说,理解的关键点是Spring Security可以帮助您保护方法调用以及web请求。大多数人都对在服务层上保护方法调用感兴趣。这是因为服务层是大多数业务逻辑驻留在当前一代Java EE应用程序中的地方。如果您只需要在服务层中保护方法调用,那么Spring的标准AOP就足够了。如果您需要直接保护域对象,您可能会发现AspectJ值得考虑。

您可以选择使用AspectJ或Spring AOP执行方法授权,也可以选择使用过滤器执行web请求授权。您可以将这些方法中的零个,一个,两个或三个一起使用。主流使用模式是执行一些web请求授权,以及服务层上的一些Spring AOP方法调用授权。

安全对象和AbstractSecurityInterceptor

那么什么 “安全对象”呢?Spring Security使用该术语来指代可以对其应用安全性(例如授权决策)的任何对象。最常见的示例是方法调用和web请求。

每个受支持的安全对象类型都有自己的拦截器类,它是AbstractSecurityInterceptor的子类。重要的是,在调用AbstractSecurityInterceptor时,如果主体已经过身份验证,则SecurityContextHolder将包含有效的Authentication

AbstractSecurityInterceptor为处理安全对象请求提供了一致的工作流程,通常:

  1. 查找与当前请求关联的“配置属性”
  2. 将安全对象,当前Authentication和配置属性提交到AccessDecisionManager以进行授权决策
  3. (可选)更改发生调用的Authentication
  4. 允许安全对象调用继续(假设已授予访问权限)
  5. 调用返回后,调用AfterInvocationManager(如果已配置)。如果调用引发异常,则不会调用AfterInvocationManager
什么是配置属性?

“配置属性”可以被认为是对AbstractSecurityInterceptor使用的类具有特殊含义的String。它们由框架内的接口ConfigAttribute表示。它们可能是简单的角色名称或具有更复杂的含义,具体取决于AccessDecisionManager实现的复杂程度。AbstractSecurityInterceptor配置了SecurityMetadataSource,用于查找安全对象的属性。通常,此配置将对用户隐藏。配置属性将作为安全方法的注释输入,或作为安全URL的访问属性输入。例如,当我们在命名空间简介中看到类似的内容时,这就是说配置属性ROLE_AROLE_B适用于匹配给定模式的web请求。实际上,使用默认的AccessDecisionManager配置,这意味着任何具有GrantedAuthority匹配这两个属性之一的人都将被允许访问。严格地说,它们只是属性,解释依赖于AccessDecisionManager实现。前缀ROLE_的使用是一个标记,表示这些属性是角色,应由Spring Security的RoleVoter使用。这仅在使用基于选民的AccessDecisionManager时才有意义。我们将在授权章节中看到AccessDecisionManager的实现方式。

RunAsManager

假设AccessDecisionManager决定允许请求,AbstractSecurityInterceptor通常只会继续请求。话虽如此,在极少数情况下,用户可能希望将SecurityContext内的Authentication替换为Authentication,这由AccessDecisionManager调用RunAsManager处理。这在合理的异常情况下可能很有用,例如服务层方法需要调用远程系统并呈现不同的身份。因为Spring Security会自动将安全身份从一个服务器传播到另一个服务器(假设您正在使用正确配置的RMI或HttpInvoker远程协议客户端),这可能很有用。

AfterInvocationManager

在安全对象调用继续进行然后返回 - 这可能意味着方法调用完成或过滤器链继续进行 - AbstractSecurityInterceptor获得最后一次机会来处理调用。在这个阶段,AbstractSecurityInterceptor对可能修改返回对象感兴趣。我们可能希望这种情况发生,因为无法在安全对象调用的“途中”进行授权决策。作为高度可插拔的,AbstractSecurityInterceptor会将控制传递给AfterInvocationManager以在需要时实际修改对象。这个类甚至可以完全替换对象,或抛出异常,或者不以任何方式更改它。只有在调用成功时才会执行调用后检查。如果发生异常,将跳过其他检查。

AbstractSecurityInterceptor及其相关对象如图8.1所示,“安全拦截器和”安全对象“模型”

图8.1。安全拦截器和“安全对象”模型

摘要安全拦截器
扩展安全对象模型

只有开发人员考虑采用全新的拦截和授权请求方式才需要直接使用安全对象。例如,可以构建新的安全对象以保护对消息传递系统的调用。任何需要安全性并且还提供拦截调用的方法(如围绕建议语义的AOP)都能够成为安全对象。话虽如此,大多数Spring应用程序将完全透明地使用当前支持的三种安全对象类型(AOP Alliance MethodInvocation,AspectJ JoinPoint和web请求FilterInvocation)。

8.1.6本地化

Spring Security支持最终用户可能会看到的异常消息的本地化。如果您的应用程序是为讲英语的用户设计的,则无需执行任何操作,因为默认情况下所有安全消息均为英语。如果您需要支持其他语言环境,则需要了解的所有内容都包含在本节中。

可以对所有异常消息进行本地化,包括与身份验证失败和访问被拒绝相关的消息(授权失败)。专注于开发人员或系统部署人员的异常和日志消息(包括错误的属性,接口合同违规,使用错误的构造函数,启动时间验证,调试级别日志记录)不是本地化的,而是在Spring Security内用英语进行硬编码的代码。

spring-security-core-xx.jar中运送你会发现一个org.springframework.security包,其中包含一个messages.properties文件,以及一些常用语言的本地化版本。这应该由您的ApplicationContext引用,因为Spring Security类实现Spring的MessageSourceAware接口,并期望消息解析器在应用程序上下文启动时被依赖注入。通常,您需要做的就是在应用程序上下文中注册bean以引用消息。一个例子如下所示:




messages.properties根据标准资源包命名,表示Spring Security消息支持的默认语言。此默认文件为英文。

如果您希望自定义messages.properties文件或支持其他语言,您应该复制该文件,相应地重命名,并在上面的bean定义中注册它。此文件中没有大量的消息密钥,因此本地化不应被视为主要的主动。如果您确实执行了此文件的本地化,请考虑通过记录JIRA任务并附加适当命名的本地化版本messages.properties来与社区共享您的工作。

Spring Security依赖于Spring的本地化支持,以便实际查找相应的消息。为了使其工作,您必须确保传入请求中的区域设置存储在Spring的org.springframework.context.i18n.LocaleContextHolder中。Spring MVC的DispatcherServlet会自动为您的应用程序执行此操作,但由于在此之前调用了Spring Security的过滤器,因此需要将LocaleContextHolder设置为包含正确的Locale过滤器被调用。您可以自己在过滤器中执行此操作(必须在web.xml中的Spring Security过滤器之前),或者您可以使用Spring的RequestContextFilter。有关使用Spring本地化的更多详细信息,请参阅Spring Framework文档。

“contacts”示例应用程序设置为使用本地化消息。

8.2核心服务

现在我们对Spring Security架构及其核心类进行了高级概述,让我们仔细研究一个或两个核心接口及其实现,特别是AuthenticationManagerUserDetailsServiceAccessDecisionManager。这些文件会在本文档的其余部分定期出现,因此了解它们的配置方式以及它们的运行方式非常重要。

8.2.1 AuthenticationManager,ProviderManager和AuthenticationProvider

AuthenticationManager只是一个界面,所以实现可以是我们选择的任何东西,但它在实践中如何运作?如果我们需要检查多个身份验证数据库或不同身份验证服务(如数据库和LDAP服务器)的组合,该怎么办?

Spring Security中的默认实现称为ProviderManager,而不是处理身份验证请求本身,它会委托给已配置的AuthenticationProvider列表,每个列表都会被查询以查看它是否可以执行认证。每个提供程序将抛出异​​常或返回完全填充的Authentication对象。还记得我们的好朋友UserDetailsUserDetailsService吗?如果没有,请回到上一章并刷新记忆。验证身份验证请求的最常用方法是加载相应的UserDetails并检查加载的密码与用户输入的密码。这是DaoAuthenticationProvider使用的方法(见下文)。加载的UserDetails对象 - 特别是它包含的GrantedAuthority - 将在构建完全填充的Authentication对象时使用,该对象从成功的身份验证返回并存储在SecurityContext中。

如果您正在使用命名空间,则会在内部创建和维护ProviderManager的实例,并使用命名空间身份验证提供程序元素向其添加提供程序(请参阅命名空间章节)。在这种情况下,您不应在应用程序上下文中声明ProviderManager bean。但是,如果您没有使用命名空间,那么您将声明它如下:



在上面的例子中,我们有三个提供者。它们按照显示的顺序进行尝试(使用List表示),每个提供程序都可以尝试进行身份验证,或者只需返回null即可跳过身份验证。如果所有实现都返回null,则ProviderManager将抛出ProviderNotFoundException。如果您有兴趣了解有关链接提供程序的更多信息,请参阅ProviderManager Javadoc。

身份验证机制(例如web表单登录处理过滤器)将注入ProviderManager的引用,并将调用它来处理其身份验证请求。您需要的提供程序有时可以与身份验证机制互换,而在其他时候,它们将依赖于特定的身份验证机制。例如,DaoAuthenticationProviderLdapAuthenticationProvider与提交简单用户名/密码身份验证请求的任何机制兼容,因此可以使用基于表单的登录或HTTP基本身份验证。另一方面,一些认证机制创建一个认证请求对象,该对象只能由单一类型的AuthenticationProvider解释。一个例子是JA-SIG CAS,它使用服务票据的概念,因此只能通过CasAuthenticationProvider进行身份验证。您不必过于担心这一点,因为如果您忘记注册合适的提供商,那么在尝试进行身份验证时,您只需收到ProviderNotFoundException

成功验证时擦除凭据

默认情况下(从Spring Security 3.1开始)ProviderManager将尝试清除成功身份验证请求返回的Authentication对象中的任何敏感凭据信息。这可以防止密码保留的时间超过必要的时间。

当您使用用户对象的缓存时,这可能会导致问题,例如,提高无状态应用程序的性能。如果Authentication包含对缓存中对象的引用(例如UserDetails实例)并且其凭据已删除,则将无法再对缓存的值进行身份验证。如果使用缓存,则需要考虑这一点。一个显而易见的解决方案是首先在缓存实现中或在创建返回的Authentication对象的AuthenticationProvider中创建对象的副本。或者,您可以在ProviderManager上禁用eraseCredentialsAfterAuthentication属性。有关更多信息,请参阅Javadoc。

比如DaoAuthenticationProvider

Spring Security实现的最简单的AuthenticationProviderDaoAuthenticationProvider,这也是该框架最早支持的之一。它利用UserDetailsService(作为DAO)来查找用户名,密码和GrantedAuthority。它只需将UsernamePasswordAuthenticationToken中提交的密码与UserDetailsService加载的密码进行比较,即可对用户进行身份验证。配置提供程序非常简单:





PasswordEncoder是可选的。PasswordEncoder提供从配置的UserDetailsService返回的UserDetails对象中显示的密码的编码和解码。这将在下面更详细地讨论。

8.2.2 UserDetailsS​​ervice实现

如本参考指南前面所述,大多数身份验证提供程序都利用UserDetailsUserDetailsService接口。回想一下UserDetailsService的合同是一种单一的方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

返回的UserDetails是一个接口,提供保证非空提供身份验证信息的getter,例如用户名,密码,授予的权限以及用户帐户是启用还是禁用。大多数身份验证提供程序将使用UserDetailsService,即使用户名和密码实际上未用作身份验证决策的一部分。他们可能只使用返回的UserDetails对象来获取其GrantedAuthority信息,因为其他一些系统(如LDAP或X.509或CAS等)承担了实际验证凭据的责任。

鉴于UserDetailsService实现起来非常简单,用户应该很容易使用自己选择的持久性策略检索身份验证信息。话虽如此,Spring Security确实包含了一些有用的基础实现,我们将在下面介绍。

内存中身份验证

易于使用创建一个自定义UserDetailsService实现,从所选的持久性引擎中提取信息,但许多应用程序不需要这样的复杂性。如果您正在构建原型应用程序或刚刚开始集成Spring Security,而您真的不想花时间配置数据库或编写UserDetailsService实现,则尤其如此。对于这种情况,一个简单的选择是使用安全命名空间中的user-service元素:






这也支持使用外部属性文件:


属性文件应包含表单中的条目

username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]

例如

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

JdbcDaoImpl

Spring Security还包括可以从JDBC数据源获取身份验证信息的UserDetailsService。使用内部Spring JDBC,因此它避免了仅用于存储用户详细信息的全功能对象关系映射器(ORM)的复杂性。如果您的应用程序确实使用了ORM工具,您可能更愿意编写自定义UserDetailsService来重用您可能已经创建的映射文件。返回JdbcDaoImpl,示例配置如下所示:









您可以通过修改上面显示的DriverManagerDataSource来使用不同的关系数据库管理系统。您还可以使用从JNDI获取的全局数据源,与任何其他Spring配置一样。

权威组织

默认情况下,JdbcDaoImpl会为单个用户加载权限,并假设权限直接映射到用户(请参阅数据库架构附录)。另一种方法是将权限划分为组并将组分配给用户。有些人更喜欢这种方法来管理用户权利。有关如何启用组权限的更多信息,请参阅JdbcDaoImpl Javadoc。组架构也包含在附录中。

8.2.3密码编码

Spring Security的PasswordEncoder界面用于执行密码的单向转换,以允许密码安全存储。鉴于PasswordEncoder是单向转换,当密码转换需要双向(即存储用于向数据库进行身份验证的凭证)时,并不打算这样做。通常,PasswordEncoder用于存储在验证时需要与用户提供的密码进行比较的密码。

密码历史

多年来,存储密码的标准机制已经发展。在开始时,密码以纯文本格式存储。假设密码是安全的,因为数据存储密码保存在所需的凭据中以访问它。但是,恶意用户能够找到使用SQL注入等攻击获取用户名和密码的大量“数据转储”的方法。随着越来越多的用户凭证成为公共安全专家意识到我们需要做更多的工作来保护用户密码。

然后鼓励开发人员在通过单向散列(如SHA-256)运行密码后存储密码。当用户尝试进行身份验证时,散列密码将与他们键入的密码的哈希值进行比较。这意味着系统只需要存储密码的单向散列。如果发生了破坏,则只暴露密码的单向哈希。由于哈希是一种方式,并且在计算上难以猜测给定哈希的密码,因此在系统中找出每个密码是不值得的。为了打败这个新系统,恶意用户决定创建名为Rainbow Tables的查找表。他们不是每次都在猜测每个密码,而是计算密码一次并将其存储在查找表中。

为了降低Rainbow Tables的有效性,鼓励开发人员使用salted密码。不是仅使用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为盐)。salt和用户的密码将通过哈希函数运行,该哈希函数产生唯一的哈希值。盐将以明文形式存储在用户密码旁边。然后,当用户尝试进行身份验证时,散列密码将与存储的salt的哈希值和他们键入的密码进行比较。独特的盐意味着Rainbow Tables不再有效,因为每个盐和密码组合的哈希值都不同。

在现代,我们意识到加密哈希(如SHA-256)不再安全。原因是,使用现代硬件,我们可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地单独破解每个密码。

现在鼓励开发人员利用自适应单向函数来存储密码。使用自适应单向函数验证密码是故意的资源(即CPU,内存等)密集型。自适应单向函数允许配置“工作因子”,随着硬件变得越来越好。建议将“工作因素”调整为大约1秒钟以验证系统上的密码。这种折衷是为了让攻击者难以破解密码,但不是那么昂贵,这给你自己的系统带来了过重的负担。Spring Security试图为“工作因素”提供一个良好的起点,但鼓励用户为自己的系统定制“工作因素”,因为不同系统的性能会有很大差异。应该使用的自适应单向函数的示例包括 bcrypt, PBKDF2, scrypt和Argon2。

由于自适应单向函数是有意为资源密集型的,因此为每个请求验证用户名和密码会显着降低应用程序的性能。没有任何Spring Security(或任何其他库)可以加快密码验证,因为通过使验证资源密集来获得安全性。鼓励用户交换短期凭证(即会话,OAuth令牌等)的长期凭证(即用户名和密码)。短期凭证可以快速验证,而不会有任何安全损失。

DelegatingPasswordEncoder

在Spring Security 5.0之前,默认PasswordEncoderNoOpPasswordEncoder,需要纯文本密码。根据密码历史记录部分,您可能希望默认PasswordEncoder现在类似于BCryptPasswordEncoder。但是,这忽略了三个现实世界的问题:

  • 有许多使用旧密码编码的应用程序无法轻松迁移
  • 密码存储的最佳实践将再次发生变化。
  • 由于框架Spring Security不能经常进行重大更改

而Spring Security引入了DelegatingPasswordEncoder,通过以下方式解决了所有问题:

  • 确保使用当前密码存储建议对密码进行编码
  • 允许验证现代和传统格式的密码
  • 允许将来升级编码

您可以使用PasswordEncoderFactories轻松构造DelegatingPasswordEncoder的实例。

PasswordEncoder passwordEncoder =PasswordEncoderFactories.createDelegatingPasswordEncoder();

或者,您可以创建自己的自定义实例。例如:

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());PasswordEncoder passwordEncoder =new DelegatingPasswordEncoder(idForEncode, encoders);
密码存储格式

密码的一般格式是:

{id}encodedPassword

这样id是用于查找应该使用PasswordEncoder的标识符,encodedPassword是所选PasswordEncoder的原始编码密码。id必须位于密码的开头,以{开头,以}结尾。如果找不到id,则id将为空。例如,以下可能是使用不同id编码的密码列表。所有原始密码都是“密码”。

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 1
{noop}password 2
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 3
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  4
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 5

1

第一个密码的PasswordEncoder id为bcrypt,encodedPassword为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。匹配时会委托给BCryptPasswordEncoder

2

第二个密码的noop id为noop,encodedPassword为password。匹配时会委托给NoOpPasswordEncoder

3

第三个密码的PasswordEncoder id为pbkdf2,encodedPassword为5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配时会委托给Pbkdf2PasswordEncoder

4

第四个密码的PasswordEncoder id为scrypt,encodedPassword为$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=匹配时,会委托给SCryptPasswordEncoder

五

最终密码的PasswordEncoder id为sha256,encodedPassword为97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。匹配时会委托给StandardPasswordEncoder

[注意]注意

一些用户可能担心为潜在的黑客提供存储格式。这不是问题,因为密码的存储不依赖于算法是秘密的。此外,大多数格式很容易让攻击者在没有前缀的情况下弄清楚。例如,BCrypt密码通常以$2a$开头。

密码编码

传递给构造函数的idForEncode确定将使用哪个PasswordEncoder来编码密码。在我们上面构造的DelegatingPasswordEncoder中,这意味着编码password的结果将被委托给BCryptPasswordEncoder并以{bcrypt}作为前缀。最终结果如下:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
密码匹配

匹配是基于{id}以及构造函数中提供的idPasswordEncoder的映射完成的。我们在“密码存储格式”一节中的示例提供了如何完成此操作的工作示例。默认情况下,使用密码调用matches(CharSequence, String)和未映射的id(包括空id)的结果将导致IllegalArgumentException。可以使用DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)自定义此行为。

通过使用id,我们可以匹配任何密码编码,但使用最现代的密码编码对密码进行编码。这很重要,因为与加密不同,密码哈希的设计使得没有简单的方法来恢复明文。由于无法恢复明文,因此难以迁移密码。虽然用户很容易迁移NoOpPasswordEncoder,但我们默认选择将其包含在内,以便简化入门体验。

入门体验

如果您正在整理演示或示例,那么花些时间来散列用户密码有点麻烦。有一些便利机制可以使这更容易,但这仍然不适合生产。

User user = User.withDefaultPasswordEncoder().username("user").password("password").roles("user").build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

如果要创建多个用户,还可以重用该构建器。

UserBuilder users = User.withDefaultPasswordEncoder();
User user = users.username("user").password("password").roles("USER").build();
User admin = users.username("admin").password("password").roles("USER","ADMIN").build();

这会对存储的密码进行哈希处理,但密码仍会在内存和已编译的源代码中公开。因此,对于生产环境而言仍然不被认为是安全的。对于生产,您应该在外部散列密码。

故障排除

如其中一个密码没有id,如“密码存储格式”一节中所述,会发生以下错误。

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

解决错误的最简单方法是切换到显式提供密码编码的PasswordEncoder。解决问题的最简单方法是弄清楚当前如何存储密码并明确提供正确的PasswordEncoder。如果从Spring Security 4.2.x迁移,则可以通过公开NoOpPasswordEncoder bean恢复到先前的行为。例如,如果您使用的是Java配置,则可以创建如下配置:

[警告]警告

恢复到NoOpPasswordEncoder不被认为是安全的。您应该转而使用DelegatingPasswordEncoder来支持安全密码编码。

@Bean
public static NoOpPasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();
}

如果您使用的是XML配置,则可以使用id passwordEncoder公开PasswordEncoder


或者,您可以使用正确的ID为所有密码添加前缀,并继续使用DelegatingPasswordEncoder。例如,如果您使用的是BCrypt,则可以从以下内容中迁移密码:

$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

有关映射的完整列表,请参阅PasswordEncoderFactories上的Javadoc 。

BCryptPasswordEncoder

BCryptPasswordEncoder实现使用广泛支持的bcrypt算法来散列密码。为了使它更能抵抗密码破解,bcrypt故意慢。与其他自适应单向函数一样,应调整大约需要1秒钟来验证系统上的密码。

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder实现使用PBKDF2算法来散列密码。为了打败密码破解,PBKDF2是一种故意慢的算法。与其他自适应单向函数一样,应调整大约需要1秒钟来验证系统上的密码。当需要FIPS认证时,该算法是一个不错的选择。

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

SCryptPasswordEncoder

SCryptPasswordEncoder实现使用scrypt算法来散列密码。为了在自定义硬件上破解密码破解scrypt是一种故意慢的算法,需要大量内存。与其他自适应单向函数一样,应调整大约需要1秒钟来验证系统上的密码。

// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

其他PasswordEncoders

存在大量其他PasswordEncoder实现,完全用于向后兼容。它们都被弃用,表明它们不再被认为是安全的。但是,由于很难迁移现有的遗留系统,因此没有计划删除它们。

8.2.4 Jackson支持

Spring Security增加了Jackson对持续Spring Security相关课程的支持。在使用分布式会话(即会话复制,Spring Session等)时,这可以提高序列化Spring Security相关类的性能。

要使用它,请将SecurityJackson2Modules.getModules(ClassLoader)注册为Jackson模块。

ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
List modules = SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);// ... use ObjectMapper as normally ...
SecurityContext context = new SecurityContextImpl();
// ...
String json = mapper.writeValueAsString(context);

9.测试

本节介绍Spring Security提供的测试支持。

[小费]小费

要使用Spring Security测试支持,必须包含spring-security-test-5.1.2.RELEASE.jar作为项目的依赖项。

9.1测试方法安全性

本节演示如何使用Spring Security的测试支持来测试基于安全性的方法。我们首先介绍一个MessageService,要求用户进行身份验证才能访问它。

public class HelloMessageService implements MessageService {@PreAuthorize("authenticated")public String getMessage() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();return "Hello " + authentication;}
}

getMessage的结果是对当前Spring Security Authentication说“Hello”的字符串。输出的示例如下所示。

Hello org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ca25360: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER

9.1.1安全测试设置

在我们使用Spring Security测试支持之前,我们必须执行一些设置。下面是一个例子:

@RunWith(SpringJUnit4ClassRunner.class) 1
@ContextConfiguration 2
public class WithMockUserTests {

这是如何设置Spring Security测试的基本示例。亮点是:

1

@RunWith指示弹簧测试模块应该创建ApplicationContext。这与使用现有的Spring测试支持没有什么不同。有关其他信息,请参阅Spring参考

2

@ContextConfiguration指示spring-test用于创建ApplicationContext的配置。由于未指定任何配置,因此将尝试使用默认配置位置。这与使用现有的Spring测试支持没有什么不同。有关其他信息,请参阅Spring参考

[注意]注意

Spring Security使用WithSecurityContextTestExecutionListener挂钩Spring测试支持,这将确保我们的测试是使用正确的用户运行的。它通过在运行测试之前填充SecurityContextHolder来实现。如果您使用的是反应式方法安全性,则还需要ReactorContextTestExecutionListener填充ReactiveSecurityContextHolder。测试完成后,它将清除SecurityContextHolder。如果您只需要Spring Security相关支持,则可以将@ContextConfiguration替换为@SecurityTestExecutionListeners

请记住,我们在HelloMessageService中添加了@PreAuthorize注释,因此需要经过身份验证的用户才能调用它。如果我们运行以下测试,我们希望以下测试通过:

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {messageService.getMessage();
}

9.1.2 @WithMockUser

问题是“作为特定用户,我们怎样才能最轻松地运行测试?” 答案是使用@WithMockUser。以下测试将以用户名“user”,密码“password”和角色“ROLE_USER”的用户身份运行。

@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}

具体如下:

  • 具有用户名“user”的用户不必存在,因为我们正在模拟用户
  • SecurityContext中填充的Authentication类型为UsernamePasswordAuthenticationToken
  • Authentication上的校长是Spring Security的User对象
  • User将具有“user”的用户名,密码“password”,并且使用名为“ROLE_USER”的单个GrantedAuthority

我们的例子很好,因为我们能够利用很多默认值。如果我们想用不同的用户名运行测试怎么办?以下测试将使用用户名“customUser”运行。同样,用户不需要实际存在。

@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {String message = messageService.getMessage();
...
}

我们还可以轻松自定义角色。例如,将使用用户名“admin”和角色“ROLE_USER”和“ROLE_ADMIN”调用此测试。

@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {String message = messageService.getMessage();...
}

如果我们不希望值自动以ROLE_为前缀,我们可以利用authority属性。例如,将使用用户名“admin”和权限“USER”和“ADMIN”调用此测试。

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {String message = messageService.getMessage();...
}

当然,在每个测试方法上放置注释可能有点单调乏味。相反,我们可以将注释放在类级别,每个测试都将使用指定的用户。例如,以下内容将使用用户名为“admin”,密码为“password”以及角色“ROLE_USER”和“ROLE_ADMIN”的用户运行每个测试。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {

默认情况下,SecurityContextTestExecutionListener.beforeTestMethod事件期间设置。这相当于在JUnit的@Before之前发生的事情。您可以将此更改发生在JUnit的@Before之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution事件期间。

@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

9.1.3 @WithAnonymousUser

使用@WithAnonymousUser允许以匿名用户身份运行。当您希望与特定用户运行大多数测试但希望以匿名用户身份运行一些测试时,这尤其方便。例如,以下将使用@WithMockUser和匿名用户匿名用户运行withMockUser1和withMockUser2 。

@RunWith(SpringJUnit4ClassRunner.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {@Testpublic void withMockUser1() {}@Testpublic void withMockUser2() {}@Test@WithAnonymousUserpublic void anonymous() throws Exception {// override default to run as anonymous user}
}

默认情况下,SecurityContextTestExecutionListener.beforeTestMethod事件期间设置。这相当于在JUnit的@Before之前发生的事情。您可以将此更改发生在JUnit的@Before之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution事件期间。

@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

9.1.4 @WithUserDetails

虽然@WithMockUser是一种非常方便的入门方式,但它可能并不适用于所有情况。例如,应用程序通常期望Authentication主体属于特定类型。这样做是为了使应用程序可以将主体引用为自定义类型并减少Spring Security上的耦合。

自定义主体通常由自定义UserDetailsService返回,返回实现UserDetails和自定义类型的对象。对于这种情况,使用自定义UserDetailsService创建测试用户很有用。这正是@WithUserDetails所做的。

假设我们将一个UserDetailsService暴露为bean,将使用类型为UsernamePasswordAuthenticationTokenAuthentication调用以及使用用户名“user”从UserDetailsService返回的主体调用以下测试。

@Test
@WithUserDetails
public void getMessageWithUserDetails() {String message = messageService.getMessage();...
}

我们还可以自定义用于从UserDetailsService查找用户的用户名。例如,此测试将使用从UserDetailsService以“customUsername”用户名返回的主体执行。

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {String message = messageService.getMessage();...
}

我们还可以提供一个显式的bean名称来查找UserDetailsService。例如,此测试将使用带有bean名称“myUserDetailsS​​ervice”的UserDetailsService查找“customUsername”的用户名。

@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {String message = messageService.getMessage();...
}

@WithMockUser一样,我们也可以将我们的注释放在类级别,以便每个测试都使用相同的用户。但是,与@WithMockUser不同,@WithUserDetails要求用户存在。

默认情况下,SecurityContextTestExecutionListener.beforeTestMethod事件期间设置。这相当于在JUnit的@Before之前发生的事情。您可以将此更改发生在JUnit的@Before之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution事件期间。

@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)

9.1.5 @WithSecurityContext

我们已经看到@WithMockUser是一个很好的选择,如果我们不使用自定义Authentication校长。接下来我们发现@WithUserDetails允许我们使用自定义UserDetailsService创建我们的Authentication主体,但要求用户存在。我们现在将看到一个允许最大灵活性的选项。

我们可以创建自己的注释,使用@WithSecurityContext创建我们想要的任何SecurityContext。例如,我们可能会创建一个名为@WithMockCustomUser的注释,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {String username() default "rob";String name() default "Rob Winch";
}

您可以看到@WithMockCustomUser使用@WithSecurityContext注释进行注释。这是Spring Security测试支持的信号,我们打算为测试创建一个SecurityContext@WithSecurityContext注释要求我们指定一个SecurityContextFactory,它将根据我们的@WithMockCustomUser注释创建一个新的SecurityContext。您可以在下面找到我们的WithMockCustomUserSecurityContextFactory实施:

public class WithMockCustomUserSecurityContextFactoryimplements WithSecurityContextFactory {@Overridepublic SecurityContext createSecurityContext(WithMockCustomUser customUser) {SecurityContext context = SecurityContextHolder.createEmptyContext();CustomUserDetails principal =new CustomUserDetails(customUser.name(), customUser.username());Authentication auth =new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities());context.setAuthentication(auth);return context;}
}

我们现在可以使用我们的新注释来注释测试类或测试方法,Spring Security的WithSecurityContextTestExecutionListener将确保我们的SecurityContext被适当填充。

在创建自己的WithSecurityContextFactory实现时,很高兴知道它们可以使用标准Spring注释进行注释。例如,WithUserDetailsSecurityContextFactory使用@Autowired注释来获取UserDetailsService

final class WithUserDetailsSecurityContextFactoryimplements WithSecurityContextFactory {private UserDetailsService userDetailsService;@Autowiredpublic WithUserDetailsSecurityContextFactory(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}public SecurityContext createSecurityContext(WithUserDetails withUser) {String username = withUser.value();Assert.hasLength(username, "value() must be non-empty String");UserDetails principal = userDetailsService.loadUserByUsername(username);Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());SecurityContext context = SecurityContextHolder.createEmptyContext();context.setAuthentication(authentication);return context;}
}

默认情况下,SecurityContextTestExecutionListener.beforeTestMethod事件期间设置。这相当于在JUnit的@Before之前发生的事情。您可以将此更改发生在JUnit的@Before之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution事件期间。

@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)

9.1.6测试元注释

如果经常在测试中重复使用同一个用户,则不必重复指定属性。例如,如果有许多与管理用户相关的测试,用户名为“admin”,角色为ROLE_USERROLE_ADMIN,则必须编写:

@WithMockUser(username="admin",roles={"USER","ADMIN"})

我们可以使用元注释,而不是在任何地方重复这一点。例如,我们可以创建一个名为WithMockAdmin的元注释:

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }

现在我们可以使用@WithMockAdmin与更详细@WithMockUser相同的方式。

元注释适用于上述任何测试注释。例如,这意味着我们也可以为@WithUserDetails("admin")创建元注释。

9.2 Spring MVC测试集成

Spring Security提供与Spring MVC测试的全面整合

9.2.1设置MockMvc和Spring Security

要将Spring Security与Spring MVC测试一起使用,必须将Spring Security FilterChainProxy添加为Filter。还需要添加Spring Security的TestSecurityContextHolderPostProcessor以支持在带有注释的Spring MVC测试中以用户身份运行。这可以使用Spring Security的SecurityMockMvcConfigurers.springSecurity()来完成。例如:

[注意]注意

Spring Security的测试支持需要spring-test-4.1.3.RELEASE或更高版本。

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class CsrfShowcaseTests {@Autowiredprivate WebApplicationContext context;private MockMvc mvc;@Beforepublic void setup() {mvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()) 1.build();}...

1

SecurityMockMvcConfigurers.springSecurity()将执行我们将Spring Security与Spring MVC测试集成所需的所有初始设置

9.2.2 SecurityMockMvcRequestPostProcessors

Spring MVC Test提供了一个名为RequestPostProcessor的便捷界面,可用于修改请求。Spring Security提供了许多RequestPostProcessor实现,使测试更容易。为了使用Spring Security的RequestPostProcessor实现,请确保使用以下静态导入:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;

使用CSRF保护进行测试

在测试任何非安全HTTP方法并使用Spring Security的CSRF保护时,您必须确保在请求中包含有效的CSRF令牌。要使用以下命令将有效的CSRF令牌指定为请求参数:

mvc.perform(post("/").with(csrf()))

如果您愿意,可以在标题中包含CSRF令牌:

mvc.perform(post("/").with(csrf().asHeader()))

您还可以使用以下方法测试提供无效的CSRF令牌:

mvc.perform(post("/").with(csrf().useInvalidToken()))

在Spring MVC测试中以用户身份运行测试

通常希望将测试作为特定用户运行。填充用户有两种简单的方法:

  • 在Spring MVC使用RequestPostProcessor进行测试时以用户身份运行
  • 在Spring MVC使用注释进行测试时以用户身份运行

在Spring MVC使用RequestPostProcessor进行测试时以用户身份运行

有许多选项可用于将用户与当前HttpServletRequest相关联。例如,以下将以用户(不需要存在)的形式运行,用户名为“user”,密码为“password”,角色为“ROLE_USER”:

[注意]注意

支持通过将用户与HttpServletRequest相关联来工作。要将请求关联到SecurityContextHolder,您需要确保SecurityContextPersistenceFilterMockMvc实例相关联。一些方法是:

  • 调用apply(springSecurity())
  • 将Spring Security的FilterChainProxy添加到MockMvc
  • 使用MockMvcBuilders.standaloneSetup时,将SecurityContextPersistenceFilter手动添加到MockMvc实例可能有意义
mvc.perform(get("/").with(user("user")))

您可以轻松进行自定义。例如,以下将以用户名(admin,不需要存在)运行,用户名为“admin”,密码为“pass”,角色为“ROLE_USER”和“ROLE_ADMIN”。

mvc.perform(get("/admin").with(user("admin").password("pass").roles("USER","ADMIN")))

如果您有自己想要使用的自定义UserDetails,也可以轻松指定。例如,以下将使用指定的UserDetails(不需要存在)与具有指定UserDetails的主体的UsernamePasswordAuthenticationToken一起运行:

mvc.perform(get("/").with(user(userDetails)))

您可以使用以下方式以匿名用户身份运行:

mvc.perform(get("/").with(anonymous()))

如果您使用默认用户运行并希望以匿名用户身份执行一些请求,则此功能尤其有用。

如果您需要自定义Authentication(不需要存在),可以使用以下命令执行此操作:

mvc.perform(get("/").with(authentication(authentication)))

您甚至可以使用以下内容自定义SecurityContext

mvc.perform(get("/").with(securityContext(securityContext)))

我们还可以使用MockMvcBuilders的默认请求确保以每个请求的特定用户身份运行。例如,以下将以用户名(admin,不需要存在)运行,用户名为“admin”,密码为“password”,角色为“ROLE_ADMIN”:

mvc = MockMvcBuilders.webAppContextSetup(context).defaultRequest(get("/").with(user("user").roles("ADMIN"))).apply(springSecurity()).build();

如果您发现在许多测试中使用的是同一个用户,建议将用户移动到某个方法。例如,您可以在自己的名为CustomSecurityMockMvcRequestPostProcessors的类中指定以下内容:

public static RequestPostProcessor rob() {return user("rob").roles("ADMIN");
}

现在,您可以在SecurityMockMvcRequestPostProcessors上执行静态导入,并在测试中使用它:

import static sample.CustomSecurityMockMvcRequestPostProcessors.*;...mvc.perform(get("/").with(rob()))
在Spring MVC使用注释进行测试时以用户身份运行

作为使用RequestPostProcessor创建用户的替代方法,您可以使用第9.1节“测试方法安全性”中所述的注释。例如,以下将使用用户名“user”,密码“password”和角色“ROLE_USER”运行测试:

@Test
@WithMockUser
public void requestProtectedUrlWithUser() throws Exception {
mvc.perform(get("/"))...
}

或者,以下将使用用户名“user”,密码“password”和角色“ROLE_ADMIN”运行测试:

@Test
@WithMockUser(roles="ADMIN")
public void requestProtectedUrlWithUser() throws Exception {
mvc.perform(get("/"))...
}

测试HTTP基本身份验证

虽然始终可以使用HTTP Basic进行身份验证,但记住标题名称,格式和编码值有点乏味。现在可以使用Spring Security的httpBasic RequestPostProcessor完成此操作。例如,下面的代码段:

mvc.perform(get("/").with(httpBasic("user","password")))

将通过确保在HTTP请求上填充以下标头,尝试使用HTTP Basic使用用户名“user”和密码“password”对用户进行身份验证:

Authorization: Basic dXNlcjpwYXNzd29yZA==

9.2.3 SecurityMockMvcRequestBuilders

Spring MVC测试还提供了一个RequestBuilder接口,可用于创建测试中使用的MockHttpServletRequest。Spring Security提供了一些RequestBuilder实现,可用于简化测试。为了使用Spring Security的RequestBuilder实现,请确保使用以下静态导入:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;

测试基于表单的认证

您可以使用Spring Security的测试支持轻松创建测试基于表单的身份验证的请求。例如,以下内容将使用用户名“user”,密码“password”和有效的CSRF令牌向“/ login”提交POST:

mvc.perform(formLogin())

可以轻松自定义请求。例如,以下内容将使用用户名“admin”,密码“pass”和有效的CSRF令牌向“/ auth”提交POST:

mvc.perform(formLogin("/auth").user("admin").password("pass"))

我们还可以自定义包含用户名和密码的参数名称。例如,这是上面的请求被修改为包括HTTP参数“u”上的用户名和HTTP参数“p”上的密码。

mvc.perform(formLogin("/auth").user("u","admin").password("p","pass"))

测试注销

虽然使用标准Spring MVC测试相当简单,但您可以使用Spring Security的测试支持来简化测试注销。例如,以下内容将使用有效的CSRF令牌向“/ logout”提交POST:

mvc.perform(logout())

您还可以自定义要发布到的URL。例如,下面的代码段将使用有效的CSRF令牌向“/ signout”提交POST:

mvc.perform(logout("/signout"))

9.2.4 SecurityMockMvcResultMatchers

有时希望对请求做出各种与安全相关的断言。为了满足这种需求,Spring Security测试支持实现了Spring MVC Test的ResultMatcher接口。为了使用Spring Security的ResultMatcher实现,请确保使用以下静态导入:

import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;

未经认证的断言

有时,断言没有与MockMvc调用的结果相关联的经过身份验证的用户可能很有价值。例如,您可能希望测试提交无效的用户名和密码,并验证没有用户通过身份验证。使用Spring Security的测试支持可以使用以下内容轻松完成此操作:

mvc.perform(formLogin().password("invalid")).andExpect(unauthenticated());

认证断言

通常我们必须断言经过身份验证的用户存在。例如,我们可能想验证我们是否已成功验证。我们可以使用以下代码片段验证基于表单的登录是否成功:

mvc.perform(formLogin()).andExpect(authenticated());

如果我们想要断言用户的角色,我们可以优化我们之前的代码,如下所示:

mvc.perform(formLogin().user("admin")).andExpect(authenticated().withRoles("USER","ADMIN"));

或者,我们可以验证用户名:

mvc.perform(formLogin().user("admin")).andExpect(authenticated().withUsername("admin"));

我们也可以结合断言:

mvc.perform(formLogin().user("admin").roles("USER","ADMIN")).andExpect(authenticated().withUsername("admin"));

我们还可以对身份验证进行任意断言

mvc.perform(formLogin()).andExpect(authenticated().withAuthentication(auth ->assertThat(auth).isInstanceOf(UsernamePasswordAuthenticationToken.class)));

10. Web应用程序安全性

大多数Spring Security用户将在使用HTTP和Servlet API的应用程序中使用该框架。在本部分中,我们将了解Spring Security如何为应用程序的web层提供身份验证和访问控制功能。我们将查看命名空间的外观,并查看实际组装的类和接口,以提供web - 层安全性。在某些情况下,有必要使用传统的bean配置来提供对配置的完全控制,因此我们还将看到如何在没有命名空间的情况下直接配置这些类。

10.1安全过滤器链

Spring Security的web基础结构完全基于标准的servlet过滤器。它不在内部使用servlet或任何其他基于servlet的框架(例如Spring MVC),因此它与任何特定的web技术没有强大的链接。它处理HttpServletRequestHttpServletResponse s并不关心请求是来自浏览器,web服务客户端,HttpInvoker还是AJAX应用程序。

Spring Security在内部维护一个过滤器链,其中每个过滤器都有特定的责任,并根据所需的服务在配置中添加或删除过滤器。过滤器的顺序很重要,因为它们之间存在依赖关系。如果您一直在使用命名空间配置,那么将自动为您配置过滤器,您不必明确定义任何Spring bean,但这可能是您希望完全控制安全过滤器链的时候,因为您正在使用命名空间中不支持的功能,或者您正在使用自己的自定义版本的类。

10.1.1 DelegatingFilterProxy

使用servlet过滤器时,显然需要在web.xml中声明它们,否则servlet容器将忽略它们。在Spring Security中,过滤器类也是在应用程序上下文中定义的Spring bean,因此能够利用Spring丰富的依赖注入工具和生命周期接口。Spring的DelegatingFilterProxy提供了web.xml与应用程序上下文之间的链接。

使用DelegatingFilterProxy时,您会在web.xml文件中看到类似的内容:


myFilter
org.springframework.web.filter.DelegatingFilterProxy

myFilter
/*

请注意,过滤器实际上是DelegatingFilterProxy,而不是实际实现过滤器逻辑的类。DelegatingFilterProxy的作用是将Filter的方法委托给从Spring应用程序上下文中获取的bean。这使bean能够受益于Spring web应用程序上下文生命周期支持和配置灵活性。bean必须实现javax.servlet.Filter,并且它必须与filter-name元素中的名称相同。有关更多信息,请阅读DelegatingFilterProxy的Javadoc

10.1.2 FilterChainProxy

Spring Security的web基础设施只能通过委托给FilterChainProxy的实例来使用。安全过滤器本身不应使用。从理论上讲,您可以在应用程序上下文文件中声明所需的每个Spring Security过滤器bean,并为每个过滤器添加相应的DelegatingFilterProxy条目到web.xml,确保它们的排序正确,但这将是如果你有很多过滤器,那么很麻烦并且会很快弄乱web.xml文件。FilterChainProxy允许我们向web.xml添加一个条目,并完全处理应用程序上下文文件以管理我们的web安全bean。它使用DelegatingFilterProxy进行连线,就像上面的示例一样,但是将filter-name设置为bean名称“filterChainProxy”。然后,在应用程序上下文中使用相同的bean名称声明过滤器链。这是一个例子:





命名空间元素filter-chain用于方便地设置应用程序中所需的安全过滤器链。 [6]。它将特定的URL模式映射到根据filters元素中指定的bean名称构建的过滤器列表,并将它们组合在SecurityFilterChain类型的bean中。pattern属性采用Ant路径,最具体的URI应首先出现[7]。在运行时,FilterChainProxy将找到与当前web请求匹配的第一个URI模式,并且filters属性指定的过滤器bean列表将应用于该请求。过滤器将按照定义的顺序调用,因此您可以完全控制应用于特定URL的过滤器链。

您可能已经注意到我们在过滤器链中声明了两个SecurityContextPersistenceFilter s(ASCallowSessionCreation的缩写,属性为SecurityContextPersistenceFilter)。由于web服务永远不会在未来的请求中出现jsessionid,因此为这样的用户代理创建HttpSession将是浪费的。如果您的大批量应用程序需要最大的可扩展性,我们建议您使用上面显示的方法。对于较小的应用程序,使用单个SecurityContextPersistenceFilter(默认allowSessionCreationtrue)可能就足够了。

请注意,FilterChainProxy不会在配置的过滤器上调用标准过滤器生命周期方法。我们建议您使用Spring的应用程序上下文生命周期接口作为替代,就像使用任何其他Spring bean一样。

当我们查看如何使用命名空间配置设置web安全性时,我们使用名为“springSecurityFilterChain”的DelegatingFilterProxy。您现在应该能够看到这是由命名空间创建的FilterChainProxy的名称。

绕过过滤链

您可以使用属性filters = "none"作为提供过滤器bean列表的替代方法。这将完全省略安全过滤器链中的请求模式。请注意,与此路径匹配的任何内容都将不会应用任何身份验证或授权服务,并且可以自由访问。如果要在请求期间使用SecurityContext内容的内容,则它必须已通过安全筛选器链。否则,将不会填充SecurityContextHolder并且内容将为null。

10.1.3过滤器排序

过滤器在链中定义的顺序非常重要。无论您实际使用哪种过滤器,订单应如下:

  • ChannelProcessingFilter,因为它可能需要重定向到不同的协议
  • SecurityContextPersistenceFilter,因此可以在web请求开头的SecurityContextHolder中设置SecurityContext,并且SecurityContext的任何更改都可以复制到HttpSession当web请求结束时(准备好与下一个web请求一起使用)
  • ConcurrentSessionFilter,因为它使用SecurityContextHolder功能并需要更新SessionRegistry以反映来自校长的持续请求
  • 身份验证处理机制 - UsernamePasswordAuthenticationFilterCasAuthenticationFilterBasicAuthenticationFilter等 - 以便SecurityContextHolder可以修改为包含有效的Authentication请求令牌
  • SecurityContextHolderAwareRequestFilter,如果您使用它将Spring Security识别HttpServletRequestWrapper安装到您的servlet容器中
  • JaasApiIntegrationFilter,如果SecurityContextHolder位于SecurityContextHolder,则会将FilterChain视为JaasAuthenticationToken中的Subject
  • RememberMeAuthenticationFilter,如果没有早期的身份验证处理机制更新SecurityContextHolder,并且请求提供了一个启用记住我服务的cookie,则会在那里放置一个合适的记忆Authentication对象
  • AnonymousAuthenticationFilter,如果没有早期的身份验证处理机制更新SecurityContextHolder,那么匿名Authentication对象将被放置在那里
  • ExceptionTranslationFilter,捕获任何Spring Security异常,以便可以返回HTTP错误响应或启动适当的AuthenticationEntryPoint
  • FilterSecurityInterceptor,用于保护web URI并在访问被拒绝时引发异常

10.1.4请求匹配和HttpFirewall

Spring Security有几个区域,您定义的模式将针对传入请求进行测试,以决定如何处理请求。当FilterChainProxy决定应该通过哪个过滤器链以及FilterSecurityInterceptor决定哪个安全约束适用于请求时,会发生这种情况。在针对您定义的模式进行测试时,了解机制是什么以及使用什么URL值非常重要。

Servlet规范定义了HttpServletRequest的几个属性,这些属性可以通过getter方法访问,我们可能希望与之匹配。这些是contextPathservletPathpathInfoqueryString。Spring Security仅对保护应用程序中的路径感兴趣,因此忽略contextPath。不幸的是,servlet规范没有准确定义servletPathpathInfo的值将包含特定请求URI的内容。例如,URL的每个路径段可以包含参数,如RFC 2396 [8]中所定义。规范没有明确说明这些是否应该包含在servletPathpathInfo值中,并且不同servlet容器之间的行为也不同。存在这样的危险:当应用程序部署在不从这些值中删除路径参数的容器中时,攻击者可以将它们添加到请求的URL中,以使模式匹配成功或意外失败。 [9]。传入URL的其他变体也是可能的。例如,它可能包含路径遍历序列(如/../)或多个正斜杠(//),这也可能导致模式匹配失败。一些容器在执行servlet映射之前将这些规范化,但其他容器则没有。为了防止出现这些问题,FilterChainProxy使用HttpFirewall策略检查并包装请求。默认情况下会自动拒绝未规范化的请求,并且会删除路径参数和重复斜杠以进行匹配。 [10]。因此,必须使用FilterChainProxy来管理安全过滤器链。请注意,servletPathpathInfo值由容器解码,因此您的应用程序不应包含任何包含分号的有效路径,因为这些部分将被删除以用于匹配目的。

如上所述,默认策略是使用Ant样式路径进行匹配,这可能是大多数用户的最佳选择。该策略在类AntPathRequestMatcher中实现,该类使用Spring的AntPathMatcher来执行模式与连接的servletPathpathInfo的不区分大小写的匹配,忽略queryString

如果由于某种原因,您需要更强大的匹配策略,则可以使用正则表达式。战略实施是RegexRequestMatcher。有关更多信息,请参阅此类的Javadoc。

实际上,我们建议您在服务层使用方法安全性,以控制对应用程序的访问,而不是完全依赖于web - 应用程序级别定义的安全性约束。URL发生变化,很难考虑应用程序可能支持的所有可能的URL以及如何操作请求。您应该尝试限制自己使用一些简单易懂的简单蚂蚁路径。始终尝试使用“默认拒绝”方法,其中您最后定义了一个全能通配符(/ )并拒绝访问。

在服务层定义的安全性更强大,更难以绕过,因此您应该始终利用Spring Security的方法安全选项。

HttpFirewall还通过拒绝HTTP响应标头中的新行字符来阻止HTTP响应拆分。

默认情况下,使用StrictHttpFirewall。此实现拒绝看似恶意的请求。如果它对您的需求过于严格,那么您可以自定义拒绝的请求类型。但是,重要的是要知道这可以打开您的应用程序直至攻击。例如,如果您希望利用Spring MVC的矩阵变量,可以在XML中使用以下配置:


通过公开StrictHttpFirewall bean,Java Configuration可以实现同样的目的。

@Bean
public StrictHttpFirewall httpFirewall() {StrictHttpFirewall firewall = new StrictHttpFirewall();firewall.setAllowSemicolon(true);return firewall;
}

StrictHttpFirewall提供了有效HTTP方法的白名单,可以防止跨站点跟踪(XST)和HTTP动词篡改。默认的有效方法是“DELETE”,“GET”,“HEAD”,“OPTIONS”,“PATCH”,“POST”和“PUT”。如果您的应用程序需要修改有效方法,则可以配置自定义StrictHttpFirewall bean。例如,以下内容仅允许HTTP“GET”和“POST”方法:


通过公开StrictHttpFirewall bean,Java Configuration可以实现同样的目的。

@Bean
public StrictHttpFirewall httpFirewall() {StrictHttpFirewall firewall = new StrictHttpFirewall();firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));return firewall;
}
[小费]小费

如果您使用的是new MockHttpServletRequest(),它当前会将HTTP方法创建为空字符串“”。这是一种无效的HTTP方法,将被Spring Security拒绝。您可以通过将其替换为new MockHttpServletRequest("GET", "")来解决此问题。有关要求改进此问题的问题,请参阅SPR_16851。

如果必须允许任何HTTP方法(不推荐),则可以使用StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)。这将完全禁用HTTP方法的验证。

10.1.5与其他基于过滤器的框架一起使用

如果您正在使用其他基于过滤器的框架,那么您需要确保首先使用Spring Security过滤器。这样可以及时填充SecurityContextHolder以供其他过滤器使用。示例是使用SiteMesh来装饰您的web页面或像Wicket这样的web框架,它使用过滤器来处理其请求。

10.1.6高级命名空间配置

正如我们之前在命名空间章节中看到的那样,可以使用多个http元素为不同的URL模式定义不同的安全配置。每个元素在内部FilterChainProxy内创建一个过滤器链,并在应该映射到它的URL模式中创建。元素将按声明的顺序添加,因此必须首先声明最具体的模式。这是另一个例子,对于与上面类似的情况,应用程序同时支持无状态RESTful API以及用户使用表单登录的普通web应用程序。












10.2核心安全过滤器

有一些关键过滤器将始终用于使用Spring Security的web应用程序,因此我们将首先查看这些及其支持类和接口。我们不会涵盖所有功能,因此如果您想获得完整的图片,请务必查看Javadoc。

10.2.1 FilterSecurityInterceptor

在讨论访问控制时,我们已经简要地看过FilterSecurityInterceptor ,我们已经将它与命名空间一起使用,其中元素被组合在内部进行配置。现在我们将看到如何显式配置它以使用FilterChainProxy及其伴随过滤器ExceptionTranslationFilter。典型配置示例如下所示:







FilterSecurityInterceptor负责处理HTTP资源的安全性。它需要引用AuthenticationManagerAccessDecisionManager。它还提供了适用于不同HTTP URL请求的配置属性。请参阅技术介绍中有关这些内容的原始讨论。

FilterSecurityInterceptor可以通过两种方式配置配置属性。第一个,如上所示,使用命名空间元素。这类似于命名空间章节中的元素,但子元素仅使用patternaccess属性。逗号用于分隔适用于每个HTTP URL的不同配置属性。第二个选项是编写自己的SecurityMetadataSource,但这超出了本文档的范围。无论使用何种方法,SecurityMetadataSource都负责返回包含与单个安全HTTP URL关联的所有配置属性的List

应该注意的是,FilterSecurityInterceptor.setSecurityMetadataSource()方法实际上需要一个FilterInvocationSecurityMetadataSource的实例。这是一个子类SecurityMetadataSource的标记接口。它只是表示SecurityMetadataSource理解FilterInvocation s。为了简单起见,我们将继续将FilterInvocationSecurityMetadataSource称为SecurityMetadataSource,因为区别与大多数用户无关。

命名空间语法创建的SecurityMetadataSource通过将请求URL与配置的pattern属性相匹配来获取特定FilterInvocation的配置属性。这与命名空间配置的行为方式相同。缺省情况是将所有表达式视为Apache Ant路径,并且对于更复杂的情况也支持正则表达式。request-matcher属性用于指定正在使用的模式类型。无法在同一定义中混合表达式语法。例如,使用正则表达式而不是Ant路径的先前配置将编写如下:








始终按照定义的顺序评估模式。因此,重要的是在列表中定义的更具体的模式比不太具体的模式更高。这反映在上面的示例中,其中更具体的/secure/super/模式看起来高于不太具体的/secure/模式。如果它们被反转,则/secure/模式将始终匹配,并且永远不会评估/secure/super/模式。

10.2.2 ExceptionTranslationFilter

ExceptionTranslationFilter位于安全过滤器堆栈中的FilterSecurityInterceptor之上。它不执行任何实际的安全实施,但处理安全拦截器抛出的异常并提供合适的HTTP响应。









的AuthenticationEntryPoint

如果用户请求安全的HTTP资源但未对其进行身份验证,则将调用AuthenticationEntryPoint。安全拦截器将在调用堆栈的下方抛出适当的AuthenticationExceptionAccessDeniedException,在入口点触发commence方法。这样做的目的是向用户提供适当的响应,以便开始身份验证。我们在这里使用的是LoginUrlAuthenticationEntryPoint,它将请求重定向到不同的URL(通常是登录页面)。使用的实际实现将取决于您希望在应用程序中使用的身份验证机制。

AccessDeniedHandler

如果用户已经过身份验证并且他们尝试访问受保护资源,会发生什么?在正常使用中,这不应该发生,因为应用程序工作流应限制为用户有权访问的操作。例如,可能会向没有管理员角色的用户隐藏指向管理页面的HTML链接。但是,您不能依赖隐藏链接来保证安全性,因为用户总是有可能直接输入URL以试图绕过限制。或者他们可能会修改RESTful URL以更改某些参数值。您的应用程序必须受到这些情况的保护,否则肯定是不安全的。您通常会使用简单的web层安全性将约束应用于基本URL,并在服务层接口上使用更具体的基于方法的安全性来真正确定允许的内容。

如果抛出AccessDeniedException并且用户已经过身份验证,那么这意味着已尝试对其没有足够权限的操作。在这种情况下,ExceptionTranslationFilter将调用第二个策略AccessDeniedHandler。默认情况下,使用AccessDeniedHandlerImpl,它只向客户端发送403(禁止)响应。或者,您可以显式配置实例(如上例所示)并设置错误页面URL,它将请求转发到[11]。这可以是简单的“访问被拒绝”页面,例如JSP,或者它可以是更复杂的处理程序,例如MVC控制器。当然,您可以自己实现界面并使用自己的实现。

当您使用命名空间配置应用程序时,也可以提供自定义AccessDeniedHandler。有关详细信息,请参阅命名空间附录。

SavedRequest和RequestCache接口

ExceptionTranslationFilter职责的另一个责任是在调用AuthenticationEntryPoint之前保存当前请求。这允许在用户进行身份验证后恢复请求(请参阅先前的web身份验证概述)。一个典型的例子是用户使用表单登录,然后通过默认值SavedRequestAwareAuthenticationSuccessHandler重定向到原始URL(见下文)。

RequestCache封装了存储和检索HttpServletRequest实例所需的功能。默认情况下,使用HttpSessionRequestCache,它将请求存储在HttpSession中。当用户被重定向到原始URL时,RequestCacheFilter的作用是实际从缓存中恢复已保存的请求。

在正常情况下,您不需要修改任何此功能,但保存请求处理是“尽力而为”的方法,并且可能存在默认配置无法处理的情况。使用这些接口使其从Spring Security 3.0开始完全可插拔。

10.2.3 SecurityContextPersistenceFilter

我们在“ 技术概述”一章中介绍了这个非常重要的过滤器的用途,因此您可能希望在此时重新阅读该部分。我们先来看看如何配置它以便与FilterChainProxy一起使用。基本配置只需要bean本身


如前所述,此过滤器有两个主要任务。它负责在HTTP请求之间存储SecurityContext内容,并在请求完成时清除SecurityContextHolder。清除存储上下文的ThreadLocal是必不可少的,因为否则可能将线程替换到servlet容器的线程池中,同时仍附加特定用户的安全上下文。然后可以在稍后阶段使用该线程,使用错误的凭证执行操作。

SecurityContextRepository

从Spring Security 3.0开始,加载和存储安全上下文的工作现在被委托给一个单独的策略接口:

public interface SecurityContextRepository {SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);void saveContext(SecurityContext context, HttpServletRequest request,HttpServletResponse response);
}

HttpRequestResponseHolder只是传入请求和响应对象的容器,允许实现用包装类替换它们。返回的内容将传递给过滤器链。

默认实现是HttpSessionSecurityContextRepository,它将安全上下文存储为HttpSession属性[12]。此实现最重要的配置参数是allowSessionCreation属性,默认为true,因此如果需要一个会话来为经过身份验证的用户存储安全上下文,则允许该类创建会话(它不会创建一个,除非进行了身份验证并且安全上下文的内容已更改)。如果您不想创建会话,则可以将此属性设置为false





或者,您可以提供NullSecurityContextRepository的实例,即空对象实现,即使在请求期间已创建会话,也会阻止安全上下文的存储。

10.2.4 UsernamePasswordAuthenticationFilter

我们现在已经看到了三个主要的过滤器,这些过滤器始终存在于Spring Security web配置中。这些也是由命名空间元素自动创建的三个,不能用替代品替换。现在唯一缺少的是实际的身份验证机制,允许用户进行身份验证。此过滤器是最常用的身份验证过滤器,也是最常定制的过滤器[13]。它还提供了命名空间中元素使用的实现。配置它需要三个阶段。

  • 使用登录页面的URL配置LoginUrlAuthenticationEntryPoint,就像我们上面所做的那样,并将其设置在ExceptionTranslationFilter上。
  • 实现登录页面(使用JSP或MVC控制器)。
  • 在应用程序上下文中配置UsernamePasswordAuthenticationFilter的实例
  • 将过滤器bean添加到过滤器链代理(确保您注意顺序)。

登录表单只包含usernamepassword输入字段,并发布到过滤器监控的URL(默认情况下为/login)。基本过滤器配置如下所示:




认证成功与失败的应用流程

筛选器调用已配置的AuthenticationManager来处理每个身份验证请求。身份验证成功或身份验证失败后的目标分别由AuthenticationSuccessHandlerAuthenticationFailureHandler策略接口控制。过滤器具有允许您设置这些属性的属性,因此您可以完全自定义行为[14]。提供了一些标准实现,例如SimpleUrlAuthenticationSuccessHandlerSavedRequestAwareAuthenticationSuccessHandlerSimpleUrlAuthenticationFailureHandlerExceptionMappingAuthenticationFailureHandlerDelegatingAuthenticationFailureHandler。查看这些类的Javadoc以及AbstractAuthenticationProcessingFilter以了解它们的工作原理和支持的功能。

如果身份验证成功,生成的Authentication对象将被放入SecurityContextHolder。然后将调用配置的AuthenticationSuccessHandler以将用户重定向或转发到适当的目标。默认情况下,使用SavedRequestAwareAuthenticationSuccessHandler,这意味着在要求用户登录之前,用户将被重定向到他们请求的原始目的地。

[注意]注意

ExceptionTranslationFilter缓存用户的原始请求。当用户进行身份验证时,请求处理程序会使用此缓存的请求来获取原始URL并重定向到该URL。然后重建原始请求并将其用作替代方案。

如果身份验证失败,将调用配置的AuthenticationFailureHandler

10.3 Servlet API集成

本节介绍如何将Spring Security与Servlet API集成。所述servletapi-XML示例应用程序演示每一种方法的使用。

10.3.1 Servlet 2.5+集成

HttpServletRequest.getRemoteUser()

所述HttpServletRequest.getRemoteUser()将返回的SecurityContextHolder.getContext().getAuthentication().getName()的结果通常是当前用户名。如果要在应用程序中显示当前用户名,这可能很有用。此外,检查this是否为null可用于指示用户是否已经过身份验证或是匿名的。知道用户是否被认证可以用于确定是否应该显示某些UI元素(即,仅当用户被认证时才应该显示注销链接)。

HttpServletRequest.getUserPrincipal()

所述HttpServletRequest.getUserPrincipal()将返回的SecurityContextHolder.getContext().getAuthentication()的结果。这意味着它是Authentication,当使用基于用户名和密码的身份验证时,它通常是UsernamePasswordAuthenticationToken的实例。如果您需要有关用户的其他信息,这可能很有用。例如,您可能创建了一个自定义UserDetailsService,它返回一个自定义UserDetails,其中包含您的用户的名字和姓氏。您可以通过以下方式获取此信息:

Authentication auth = httpServletRequest.getUserPrincipal();
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
MyCustomUserDetails userDetails = (MyCustomUserDetails) auth.getPrincipal();
String firstName = userDetails.getFirstName();
String lastName = userDetails.getLastName();
[注意]注意

应该注意的是,在整个应用程序中执行如此多的逻辑通常是不好的做法。相反,应该集中它以减少Spring Security和Servlet API的任何耦合。

HttpServletRequest.isUserInRole(字符串)

所述HttpServletRequest.isUserInRole(字符串)将确定是否SecurityContextHolder.getContext().getAuthentication().getAuthorities()包含GrantedAuthority与通入isUserInRole(String)的作用。通常,用户不应将“ROLE_”前缀传递给此方法,因为它会自动添加。例如,如果要确定当前用户是否具有“ROLE_ADMIN”权限,则可以使用以下命令:

boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");

这可能有助于确定是否应显示某些UI组件。例如,仅当当前用户是管理员时,才可以显示管理员链接。

10.3.2 Servlet 3+集成

以下部分描述了Spring Security与之集成的Servlet 3方法。

HttpServletRequest.authenticate(HttpServletRequest的,HttpServletResponse的)

所述HttpServletRequest.authenticate(HttpServletRequest的,HttpServletResponse的)方法可用于确保用户被认证。如果未经过身份验证,配置的AuthenticationEntryPoint将用于请求用户进行身份验证(即重定向到登录页面)。

HttpServletRequest.login(字符串,字符串)

所述HttpServletRequest.login(字符串,字符串)方法可用于与当前AuthenticationManager对用户进行认证。例如,以下内容将尝试使用用户名“user”和密码“password”进行身份验证:

try {
httpServletRequest.login("user","password");
} catch(ServletException e) {
// fail to authenticate
}
[注意]注意

如果您希望Spring Security处理失败的身份验证尝试,则无需捕获ServletException。

HttpServletRequest.logout()

所述HttpServletRequest.logout()方法可用于出登录当前用户。

通常这意味着SecurityContextHolder将被清除,HttpSession将被无效,任何“记住我”身份验证将被清除,等等。但是,配置的LogoutHandler实现将根据您的Spring Security配置而有所不同。重要的是要注意,在调用HttpServletRequest.logout()之后,您仍然负责编写响应。通常,这将涉及重定向到欢迎页面。

AsyncContext.start(可运行)

该AsynchContext.start(Runnable接口),以确保您的凭据方法将传播到新的线程。使用Spring Security的并发支持,Spring Security会覆盖AsyncContext.start(Runnable)以确保在处理Runnable时使用当前的SecurityContext。例如,以下内容将输出当前用户的身份验证:

final AsyncContext async = httpServletRequest.startAsync();
async.start(new Runnable() {public void run() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();try {final HttpServletResponse asyncResponse = (HttpServletResponse) async.getResponse();asyncResponse.setStatus(HttpServletResponse.SC_OK);asyncResponse.getWriter().write(String.valueOf(authentication));async.complete();} catch(Exception e) {throw new RuntimeException(e);}}
});

异步Servlet支持

如果您使用的是基于Java的配置,那么您就可以开始使用了。如果您使用的是XML配置,则需要进行一些更新。第一步是确保您已更新web。xml以至少使用3.0架构,如下所示:


接下来,您需要确保设置springSecurityFilterChain以处理异步请求。


springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy

true


springSecurityFilterChain
/*
REQUEST
ASYNC

而已!现在Spring Security将确保您的SecurityContext也在异步请求上传播。

那么它是怎样工作的?如果您对此不感兴趣,请随意跳过本节的其余部分,否则请继续阅读。其中大部分内容都包含在Servlet规范中,但有一些调整Spring Security可以确保正确处理异步请求。在Spring Security 3.2之前,一旦HttpServletResponse被提交,SecurityContextHolder中的SecurityContext就会自动保存。这可能会导致Async环境中出现问题。例如,请考虑以下事项:

httpServletRequest.startAsync();
new Thread("AsyncThread") {@Overridepublic void run() {try {// Do workTimeUnit.SECONDS.sleep(1);// Write to and commit the httpServletResponsehttpServletResponse.getOutputStream().flush();} catch (Exception e) {e.printStackTrace();}}
}.start();

问题是Spring Security不知道此线程,因此SecurityContext不会传播给它。这意味着当我们提交HttpServletResponse时,没有SecuriytContext。当Spring Security在提交HttpServletResponse时自动保存SecurityContext时,它将丢失我们的登录用户。

从版本3.2开始,Spring Security非常智能,一旦HttpServletRequest.startAsync()被调用,就不再自动保存SecurityContext以提交HttpServletResponse。

10.3.3 Servlet 3.1+集成

以下部分描述了Spring Security与之集成的Servlet 3.1方法。

HttpServletRequest的#changeSessionId()

所述HttpServletRequest.changeSessionId()是用于防止的默认方法会话固定在Servlet的3.1和更高的攻击。

10.4基本和摘要式身份验证

基本身份验证和摘要身份验证是web应用程序中常用的备用身份验证机制。基本身份验证通常与无状态客户端一起使用,后者在每个请求上传递其凭据。将它与基于表单的身份验证结合使用是很常见的,其中应用程序通过基于浏览器的用户界面和web - 服务来使用。但是,基本身份验证将密码作为纯文本传输,因此只能在加密传输层(如HTTPS)上使用。

10.4.1 BasicAuthenticationFilter

BasicAuthenticationFilter负责处理HTTP标头中显示的基本身份验证凭据。这可以用于验证Spring远程协议(例如Hessian和Burlap)以及普通浏览器用户代理(例如Firefox和Internet Explorer)所做的调用。管理HTTP基本身份验证的标准由RFC 1945第11节定义,BasicAuthenticationFilter符合此RFC。基本身份验证是一种极具吸引力的身份验证方法,因为它在用户代理中得到了广泛的部署,并且实现非常简单(它只是用户名的Base64编码:密码,在HTTP标头中指定)。

组态

要实现HTTP基本身份验证,您需要向过滤器链添加BasicAuthenticationFilter。应用程序上下文应包含BasicAuthenticationFilter及其所需的协作者:







配置的AuthenticationManager处理每个身份验证请求。如果身份验证失败,配置的AuthenticationEntryPoint将用于重试身份验证过程。通常,您将结合使用过滤器BasicAuthenticationEntryPoint,它返回带有合适标头的401响应,以重试HTTP基本身份验证。如果身份验证成功,生成的Authentication对象将照常放入SecurityContextHolder

如果身份验证事件成功,或者未尝试进行身份验证,因为HTTP标头不包含受支持的身份验证请求,则过滤器链将正常继续。过滤器链中断的唯一时间是验证失败并调用AuthenticationEntryPoint

10.4.2 DigestAuthenticationFilter

DigestAuthenticationFilter能够处理HTTP标头中显示的摘要式身份验证凭据。摘要式身份验证尝试解决基本身份验证的许多弱点,特别是通过确保永远不会通过网络以明文形式发送凭据。许多用户代理支持摘要式身份验证,包括Mozilla Firefox和Internet Explorer。管理HTTP摘要式身份验证的标准由RFC 2617定义,它更新RFC 2069规定的摘要式身份验证标准的早期版本。大多数用户代理实现RFC 2617. Spring Security的DigestAuthenticationFilter与“身份验证”兼容“RFC 2617规定的保护质量(qop),它还提供与RFC 2069的向后兼容性。如果您需要使用未加密的HTTP(即没有TLS / HTTPS)并希望最大化安全性,摘要式身份验证是一个更具吸引力的选项验证过程。事实上,摘要式身份验证是WebDAV协议的强制性要求,如RFC 2518第17.1节所述。

[注意]注意

您不应该在现代应用程序中使用Digest,因为它不被认为是安全的。最明显的问题是您必须以明文,加密或MD5格式存储密码。所有这些存储格式都被认为是不安全的。相反,您应该使用单向自适应密码哈希(即bCrypt,PBKDF2,SCrypt等)。

摘要式身份验证的核心是“随机数”。这是服务器生成的值。Spring Security的现时采用以下格式:

base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime:   The date and time when the nonce expires, expressed in milliseconds
key:              A private key to prevent modification of the nonce token

DigestAuthenticatonEntryPoint具有指定用于生成随机数令牌的key的属性,以及用于确定到期时间的nonceValiditySeconds属性(默认值300,等于五分钟)。Whist永远是nonce有效,摘要是通过连接各种字符串计算的,包括用户名,密码,nonce,请求的URI,客户端生成的nonce(只是用户代理生成每个请求的随机值),领域名称等,然后执行MD5哈希。服务器和用户代理都执行此摘要计算,如果它们在包含的值(例如密码)上不一致,则会产生不同的哈希码。在Spring Security实现中,如果服务器生成的nonce仅过期(但摘要在其他方面有效),DigestAuthenticationEntryPoint将发送"stale=true"标头。这告诉用户代理不需要打扰用户(因为密码和用户名等是正确的),而只是使用新的nonce重试。

DigestAuthenticationEntryPoint nonceValiditySeconds参数的适当值取决于您的应用程序。极其安全的应用程序应注意,截获的身份验证标头可用于模拟主体,直到达到nonce中包含的expirationTime为止。这是选择适当设置时的关键原则,但对于极其安全的应用程序而言,在第一个实例中不能通过TLS / HTTPS运行是不常见的。

由于摘要式身份验证的实现更复杂,因此通常会出现用户代理问题。例如,Internet Explorer无法在同一会话中的后续请求中显示“不透明”标记。因此,Spring Security过滤器将所有状态信息封装到“随机数”令牌中。在我们的测试中,Spring Security的实现与Mozilla Firefox和Internet Explorer可靠地工作,正确处理nonce超时等。

组态

现在我们已经回顾了这个理论,让我们看看如何使用它。要实现HTTP摘要式身份验证,必须在过滤器链中定义DigestAuthenticationFilter。应用程序上下文需要定义DigestAuthenticationFilter及其所需的协作者:










配置的UserDetailsService是必需的,因为DigestAuthenticationFilter必须能够直接访问用户的明文密码。如果您在DAO [15]中使用编码密码,则摘要式身份验证将不起作用。DAO协作者以及UserCache通常直接与DaoAuthenticationProvider共享。authenticationEntryPoint属性必须为DigestAuthenticationEntryPoint,以便DigestAuthenticationFilter可以获得正确的realmNamekey进行摘要计算。

BasicAuthenticationFilter一样,如果身份验证成功,Authentication请求令牌将被放入SecurityContextHolder。如果身份验证事件成功,或者由于HTTP标头未包含摘要式身份验证请求而未尝试身份验证,则过滤器链将正常继续。过滤器链中断的唯一时间是验证失败并调用AuthenticationEntryPoint,如前一段所述。

摘要式身份验证的RFC提供了一系列附加功能,可进一步提高安全性。例如,可以在每个请求上更改随机数。尽管如此,Spring Security实现的目的是最大限度地降低实现的复杂性(以及无疑会出现的用户代理不兼容性),并避免需要存储服务器端状态。如果您希望更详细地探索这些功能,请邀请您查看RFC 2617。据我们所知,Spring Security的实现确实符合本RFC的最低标准。

10.5记住我的身份验证

10.5.1概述

记住我或持久登录认证是指web站点能够记住会话之间的主体身份。这通常通过向浏览器发送cookie来实现,在将来的会话期间检测到cookie并导致自动登录。Spring Security为这些操作提供了必要的钩子,并且有两个具体的记住我实现。一个使用散列来保护基于cookie的令牌的安全性,另一个使用数据库或其他持久存储机制来存储生成的令牌。

请注意,这两种实现都需要UserDetailsService。如果您使用的身份验证提供程序不使用UserDetailsService(例如,LDAP提供程序),那么除非您的应用程序上下文中还有一个UserDetailsService bean,否则它将无法运行。

10.5.2简单的基于哈希的令牌方法

这种方法使用散列来实现有用的记住策略。本质上,在成功进行交互式身份验证后,cookie将被发送到浏览器,其中cookie的组成如下:

base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))username:          As identifiable to the UserDetailsService
password:          That matches the one in the retrieved UserDetails
expirationTime:    The date and time when the remember-me token expires, expressed in milliseconds
key:               A private key to prevent modification of the remember-me token

因此,remember-me令牌仅在指定的时间段内有效,并且前提是用户名,密码和密钥不会更改。值得注意的是,这具有潜在的安全性问题,因为捕获的记住我令牌将可以从任何用户代理使用,直到令牌到期为止。这与摘要式身份验证的问题相同。如果委托人知道已经捕获了令牌,他们可以轻松更改其密码并立即使所有记住我的令牌无效。如果需要更重要的安全性,则应使用下一节中描述的方法。或者,记住我的服务根本就不应该被使用。

如果您熟悉命名空间配置一章中讨论的主题,则只需添加元素即可启用remember-me身份验证:


...


通常会自动选择UserDetailsService。如果您的应用程序上下文中有多个,则需要指定哪个应该与user-service-ref属性一起使用,其中值是UserDetailsService bean的名称。

10.5.3持久令牌方法

这种方法基于文章http://jaspan.com/improved_persistent_login_cookie_best_practice并进行了一些小的修改[16]。要在命名空间配置中使用此方法,您将提供数据源引用:


...


数据库应包含使用以下SQL(或等效的)创建的persistent_logins表:

create table persistent_logins (username varchar(64) not null,series varchar(64) primary key,token varchar(64) not null,last_used timestamp not null)

10.5.4记住我的接口和实现

记住我与UsernamePasswordAuthenticationFilter一起使用,并通过AbstractAuthenticationProcessingFilter超类中的钩子实现。它也在BasicAuthenticationFilter内使用。钩子将在适当的时间调用具体的RememberMeServices。界面如下所示:

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);void loginFail(HttpServletRequest request, HttpServletResponse response);void loginSuccess(HttpServletRequest request, HttpServletResponse response,Authentication successfulAuthentication);

请参考Javadoc以更全面地讨论这些方法的作用,尽管在此阶段注意AbstractAuthenticationProcessingFilter仅调用loginFail()loginSuccess()方法。只要SecurityContextHolder不包含AuthenticationautoLogin()就会调用autoLogin()方法。因此,该接口为基础记忆实现提供了与认证相关的事件的充分通知,并且只要候选web请求可能包含cookie并希望被记住,就委托给实现。这种设计允许任何数量的记住我实施策略。我们在上面已经看到Spring Security提供了两种实现。我们依次看看这些。

TokenBasedRememberMeServices

此实现支持第10.5.2节“基于简单哈希的令牌方法”中描述的更简单的方法。TokenBasedRememberMeServices生成RememberMeAuthenticationToken,由RememberMeAuthenticationProvider处理。此身份验证提供程序与TokenBasedRememberMeServices之间共享key。此外,TokenBasedRememberMeServices需要一个UserDetailsS​​ervice,它可以从中检索用户名和密码以进行签名比较,并生成RememberMeAuthenticationToken以包含正确的GrantedAuthority。应用程序应提供某种logout命令,如果用户请求,则会使cookie无效。TokenBasedRememberMeServices还实现了Spring Security的LogoutHandler接口,因此可以与LogoutFilter一起使用以自动清除cookie。

应用程序上下文中启用remember-me服务所需的bean如下所示:










不要忘记将RememberMeServices实施添加到您的UsernamePasswordAuthenticationFilter.setRememberMeServices()媒体资源中,在AuthenticationManager.setProviders()列表中添加RememberMeAuthenticationProvider,并将RememberMeAuthenticationFilter添加到您的FilterChainProxy中(通常会在你的UsernamePasswordAuthenticationFilter)。

对PersistentTokenBasedRememberMeServices

此类可以与TokenBasedRememberMeServices相同的方式使用,但它还需要配置PersistentTokenRepository来存储令牌。有两种标准实现。

  • InMemoryTokenRepositoryImpl仅用于测试。
  • JdbcTokenRepositoryImpl将令牌存储在数据库中。

上面的第10.5.3节“持久令牌方法”中描述了数据库模式。

10.6跨站请求伪造(CSRF)

本节讨论Spring Security的跨站请求伪造(CSRF)支持。

10.6.1 CSRF攻击

在我们讨论Spring Security如何保护应用程序免受CSRF攻击之前,我们将解释CSRF攻击是什么。让我们看一个具体的例子来更好地理解。

假设您的银行网站提供的表单允许将当前登录用户的资金转移到另一个银行帐户。例如,HTTP请求可能如下所示:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencodedamount=100.00&routingNumber=1234&account=9876

现在假装你在银行的网站上进行身份验证,然后在没有退出的情况下访问一个邪恶的网站。邪恶的网站包含一个HTML页面,其格式如下:

你想赢钱,所以你点击提交按钮。在此过程中,您无意中将100美元转移给了恶意用户。发生这种情况是因为,虽然恶意网站无法看到您的Cookie,但与您的银行相关联的Cookie仍会随请求一起发送。

最糟糕的是,整个过程可以使用JavaScript自动完成。这意味着您甚至不需要单击按钮。那么我们如何保护自己免受此类攻击呢?

10.6.2同步器令牌模式

问题是来自银行网站的HTTP请求和来自邪恶网站的请求完全相同。这意味着无法拒绝来自恶意网站的请求并允许来自银行网站的请求。为了防止CSRF攻击,我们需要确保请求中存在恶意网站无法提供的内容。

一种解决方案是使用同步器令牌模式。此解决方案是为了确保每个请求除了我们的会话cookie之外还需要随机生成的令牌作为HTTP参数。提交请求时,服务器必须查找参数的预期值,并将其与请求中的实际值进行比较。如果值不匹配,请求将失败。

我们可以放松期望,只需要为每个更新状态的HTTP请求提供令牌。这可以安全地完成,因为相同的原始策略确保邪恶站点无法读取响应。此外,我们不希望在HTTP GET中包含随机令牌,因为这可能导致令牌泄露。

让我们来看看我们的例子将如何改变。假设随机生成的令牌存在于名为_csrf的HTTP参数中。例如,转账的请求如下:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencodedamount=100.00&routingNumber=1234&account=9876&_csrf=

您会注意到我们添加了带有随机值的_csrf参数。现在邪恶的网站将无法猜测_csrf参数的正确值(必须在恶意网站上明确提供),并且当服务器将实际令牌与预期令牌进行比较时,传输将失败。

10.6.3何时使用CSRF保护

什么时候应该使用CSRF保护?我们的建议是对普通用户可以由浏览器处理的任何请求使用CSRF保护。如果您只创建非浏览器客户端使用的服务,则可能需要禁用CSRF保护。

CSRF保护和JSON

一个常见的问题是“我是否需要保护javascript发出的JSON请求?” 简而言之,这取决于。但是,您必须非常小心,因为存在可能影响JSON请求的CSRF漏洞。例如,恶意用户可以使用以下格式使用JSON创建CSRF:

这将产生以下JSON结构

{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}

如果应用程序未验证Content-Type,那么它将暴露给此漏洞。根据设置,通过更新URL后缀以“.json”结尾,仍然可以利用验证Content-Type的Spring MVC应用程序,如下所示:

CSRF和无状态浏览器应用程序

如果我的申请是无国籍的怎么办?这并不一定意味着你受到保护。实际上,如果用户不需要在web浏览器中针对给定请求执行任何操作,则他们可能仍然容易受到CSRF攻击。

例如,假设应用程序使用包含其中所有状态的自定义cookie进行身份验证而不是JSESSIONID。当进行CSRF攻击时,自定义cookie将与请求一起发送,其方式与我们上一个示例中发送的JSESSIONID cookie相同。

使用基本身份验证的用户也容易受到CSRF攻击,因为浏览器会自动在任何请求中包含用户名密码,其方式与我们上一个示例中发送的JSESSIONID cookie相同。

10.6.4使用Spring Security CSRF保护

那么使用Spring Security保护我们网站免受CSRF攻击的必要步骤是什么?使用Spring Security CSRF保护的步骤概述如下:

  • 使用正确的HTTP谓词
  • 配置CSRF保护
  • 包括CSRF令牌

使用正确的HTTP谓词

防止CSRF攻击的第一步是确保您的网站使用正确的HTTP谓词。具体来说,在Spring Security的CSRF支持可以使用之前,您需要确定您的应用程序正在使用PATCH,POST,PUT和/或DELETE来修改状态。

这不是Spring Security支持的限制,而是对正确的CSRF预防的一般要求。原因是在HTTP GET中包含私人信息会导致信息泄露。有关使用POST而不是GET获取敏感信息的一般指导,请参阅RFC 2616第15.1.3节“在URI中编码敏感信息”。

配置CSRF保护

下一步是在您的应用程序中包含Spring Security的CSRF保护。一些框架通过使用户的会话无效来处理无效的CSRF令牌,但这会导致其自身的问题。相反,默认情况下Spring Security的CSRF保护将产生HTTP 403访问被拒绝。这可以通过配置AccessDeniedHandler以不同方式处理InvalidCsrfTokenException来自定义。

从Spring Security 4.0开始,默认情况下使用XML配置启用CSRF保护。如果要禁用CSRF保护,可以在下面看到相应的XML配置。



默认情况下,Java Configuration会启用CSRF保护。如果要禁用CSRF,可以在下面看到相应的Java配置。有关如何配置CSRF保护的其他自定义,请参阅csrf()的Javadoc。

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable();}
}

包括CSRF令牌

表格提交

最后一步是确保在所有PATCH,POST,PUT和DELETE方法中包含CSRF令牌。解决此问题的一种方法是使用_csrf请求属性来获取当前CsrfToken。使用JSP执行此操作的示例如下所示:


更简单的方法是使用Spring Security JSP标记库中的csrfInput标记。

[注意]注意

如果您使用Spring MVC 标签或Thymeleaf 2.1+并使用@EnableWebSecurity,则会自动为您添加CsrfToken(使用CsrfRequestDataValueProcessor)。

Ajax和JSON请求

如果您使用的是JSON,则无法在HTTP参数中提交CSRF令牌。相反,您可以在HTTP标头中提交令牌。典型的模式是在元标记中包含CSRF标记。JSP的示例如下所示:





您可以使用Spring Security JSP标记库中更简单的csrfMetaTags标记,而不是手动创建元标记。

然后,您可以在所有Ajax请求中包含令牌。如果您使用的是jQuery,可以使用以下命令完成:

$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {xhr.setRequestHeader(header, token);
});
});

作为jQuery的替代方案,我们建议使用cujoJS的 rest.js. 该rest.js模块提供了在REST风格方式的HTTP请求和响应工作先进支持。核心功能是通过将拦截器链接到客户端来根据需要对HTTP客户端添加行为进行上下文化的能力。

var client = rest.chain(csrf, {
token: $("meta[name='_csrf']").attr("content"),
name: $("meta[name='_csrf_header']").attr("content")
});

配置的客户端可以与需要向CSRF保护资源发出请求的应用程序的任何组件共享。rest.js和jQuery之间的一个显着区别是,只有使用配置的客户端发出的请求才会包含CSRF令牌,而jQuery中的所有请求都将包含令牌。对接收令牌的请求进行范围调整的能力有助于防止CSRF令牌泄露给第三方。有关rest.js的更多信息,请参阅rest.js参考文档。

CookieCsrfTokenRepository

可能存在用户希望将CsrfToken保留在cookie中的情况。默认情况下,CookieCsrfTokenRepository将写入名为XSRF-TOKEN的cookie,并从名为X-XSRF-TOKEN的标头或HTTP参数_csrf中读取它。这些默认值来自AngularJS

您可以使用以下命令在XML中配置CookieCsrfTokenRepository




[注意]注意

该示例明确设置cookieHttpOnly=false。这是允许JavaScript(即AngularJS)读取它所必需的。如果您不需要直接使用JavaScript读取cookie,建议省略cookieHttpOnly=false以提高安全性。

您可以使用以下命令在Java配置中配置CookieCsrfTokenRepository

@EnableWebSecurity
public class WebSecurityConfig extendsWebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());}
}
[注意]注意

该示例明确设置cookieHttpOnly=false。这是允许JavaScript(即AngularJS)读取它所必需的。如果您不需要直接使用JavaScript读取cookie,建议省略cookieHttpOnly=false(使用new CookieCsrfTokenRepository()代替)以提高安全性。

10.6.5 CSRF警告

实施CSRF时有一些注意事项。

超时

一个问题是预期的CSRF令牌存储在HttpSession中,因此只要HttpSession到期,您配置的AccessDeniedHandler就会收到InvalidCsrfTokenException。如果您使用默认的AccessDeniedHandler,浏览器将获得HTTP 403并显示错误的错误消息。

[注意]注意

有人可能会问为什么预期的CsrfToken默认情况下不存储在cookie中。这是因为已知的漏洞可以由另一个域设置标头(即指定cookie)。这就是当存在标题X-Requested-With时, Ruby on Rails 不再跳过CSRF检查的原因。有关如何执行漏洞利用的详细信息,请参阅此webappsec.org线程。另一个缺点是,通过删除状态(即超时),如果令牌被破坏,您将失去强制终止令牌的能力。

缓解活动用户遇到超时的一种简单方法是使用一些JavaScript让用户知道他们的会话即将过期。用户可以单击按钮继续并刷新会话。

或者,指定自定义AccessDeniedHandler可让您以任何方式处理InvalidCsrfTokenException。有关如何自定义AccessDeniedHandler的示例,请参阅xml和Java配置的提供链接。

最后,可以将应用程序配置为使用不会过期的CookieCsrfTokenRepository。如前所述,这不如使用会话安全,但在许多情况下可以足够好。

在登录

为了防止伪造登录请求,还应该保护登录表单免受CSRF攻击。由于CsrfToken存储在HttpSession中,这意味着只要访问CsrfToken令牌属性就会创建HttpSession。虽然这在RESTful /无状态架构中听起来很糟糕但实际情况是状态是实现实际安全性所必需的。没有状态,如果令牌被泄露,我们无能为力。实际上,CSRF令牌的规模非常小,对我们的架构的影响可以忽略不计。

保护登录表单的常用技术是使用JavaScript函数在表单提交之前获取有效的CSRF令牌。通过执行此操作,无需考虑会话超时(在上一节中讨论),因为会话是在表单提交之前创建的(假设未配置CookieCsrfTokenRepository),因此用户可以保留在登录页面上并在需要时提交用户名/密码。为了实现这一点,您可以利用Spring Security提供的CsrfTokenArgumentResolver并公开此处描述的端点。

注销

添加CSRF会将LogoutFilter更新为仅使用HTTP POST。这可确保注销需要CSRF令牌,并且恶意用户无法强制注销您的用户。

一种方法是使用表单进行注销。如果您真的想要一个链接,您可以使用JavaScript让链接执行POST(即可能在隐藏的表单上)。对于禁用了JavaScript的浏览器,您可以选择让链接将用户带到将执行POST的注销确认页面。

如果你真的想在退出时使用HTTP GET,你可以这样做,但是请记住这通常不推荐。例如,以下Java配置将执行注销,并使用任何HTTP方法请求URL / logout:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"));}
}

Multipart(文件上传)

将CSRF保护与multipart / form-data一起使用有两种选择。每个选项都有其权衡。

  • 在Spring Security之前放置MultipartFilter
  • 包含CSRF令牌
[注意]注意

在将Spring Security的CSRF保护与多部分文件上载集成之前,请确保您可以在没有CSRF保护的情况下进行上载。有关使用Spring的多部分表单的更多信息可以在Spring引用和MultipartFilter javadoc的17.10 Spring的多部分(文件上载)支持部分中找到。

在Spring Security之前放置MultipartFilter

第一个选项是确保在Spring Security过滤器之前指定MultipartFilter。在Spring Security过滤器之前指定MultipartFilter意味着没有授权调用MultipartFilter,这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序处理的文件。通常,这是推荐的方法,因为临时文件上载应该对大多数服务器产生可忽略的影响。

为了确保在使用java配置的Spring Security过滤器之前指定MultipartFilter,用户可以覆盖beforeSpringSecurityFilterChain,如下所示:

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {@Overrideprotected void beforeSpringSecurityFilterChain(ServletContext servletContext) {insertFilters(servletContext, new MultipartFilter());}
}

为确保在使用XML配置的Spring Security过滤器之前指定MultipartFilter,用户可以确保MultipartFilter元素放在web。xml中的springSecurityFilterChain之前,如图所示下面:

MultipartFilterorg.springframework.web.multipart.support.MultipartFilter

springSecurityFilterChainorg.springframework.web.filter.DelegatingFilterProxy

MultipartFilter/*

springSecurityFilterChain/*

包含CSRF令牌

如果允许未经授权的用户上传临时文件是不可接受的,另一种方法是在Spring Security过滤器之后放置MultipartFilter并将CSRF作为查询参数包含在表单的action属性中。jsp的示例如下所示

这种方法的缺点是可能泄漏查询参数。更常见的是,将敏感数据放入正文或标题中以确保其不会泄露被认为是最佳做法。其他信息可以在RFC 2616第15.1.3节“在URI中编码敏感信息”中找到。

HiddenHttpMethodFilter

HiddenHttpMethodFilter应放在Spring Security过滤器之前。一般来说,这是事实,但在防范CSRF攻击时可能会产生额外的影响。

请注意,HiddenHttpMethodFilter仅覆盖POST上的HTTP方法,因此实际上不太可能导致任何实际问题。但是,最佳做法仍然是确保它放在Spring Security过滤器之前。

10.6.6覆盖默认值

Spring Security的目标是提供保护用户免受攻击的默认设置。这并不意味着您被迫接受所有默认值。

例如,您可以提供自定义CsrfTokenRepository来覆盖CsrfToken的存储方式。

您还可以指定自定义RequestMatcher来确定哪些请求受CSRF保护(即,您可能不关心是否利用了注销)。简而言之,如果Spring Security的CSRF保护行为不完全符合您的要求,您就可以自定义行为。参考the section called “” 有关如何使用XML进行这些自定义的详细信息的文档,以及有关如何在使用Java配置时进行这些自定义的详细信息的CsrfConfigurer javadoc。

10.7 CORS

Spring Framework 为CORS提供一流的支持。CORS必须在Spring Security之前处理,因为飞行前请求不包含任何cookie(即JSESSIONID)。如果请求不包含任何cookie并且Spring Security是第一个,则该请求将确定用户未经过身份验证(因为请求中没有cookie)并拒绝它。

确保首先处理CORS的最简单方法是使用CorsFilter。用户可以使用以下内容提供CorsConfigurationSource,将CorsFilter与Spring Security集成:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http// by default uses a Bean by the name of corsConfigurationSource.cors().and()...}@BeanCorsConfigurationSource corsConfigurationSource() {CorsConfiguration configuration = new CorsConfiguration();configuration.setAllowedOrigins(Arrays.asList("https://example.com"));configuration.setAllowedMethods(Arrays.asList("GET","POST"));UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", configuration);return source;}
}

或者用XML

...

...

如果您使用Spring MVC的CORS支持,则可以省略指定CorsConfigurationSource和Spring Security将利用提供给Spring MVC的CORS配置。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http// if Spring MVC is on classpath and no CorsConfigurationSource is provided,// Spring Security will use CORS configuration provided to Spring MVC.cors().and()...}
}

或者用XML

...

10.8安全HTTP响应标头

本节讨论Spring Security对向响应添加各种安全标头的支持。

10.8.1默认安全标头

Spring Security允许用户轻松注入默认安全标头以帮助保护其应用程序。Spring Security的默认值包括以下标头:

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: 1; mode=block
[注意]注意

仅在HTTPS请求中添加严格传输安全性

有关每个标头的其他详细信息,请参阅相应的部分:

  • 缓存控制
  • 内容类型选项
  • HTTP严格传输安全性
  • X-框架,选项
  • X-XSS-保护

虽然这些标头中的每一个都被认为是最佳实践,但应注意并非所有客户端都使用标头,因此鼓励进行额外的测试。

您可以自定义特定标头。例如,假设您希望HTTP响应标头如下所示:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block

具体来说,您希望所有默认标头都具有以下自定义项:

  • X-Frame-Options允许来自同一域的任何请求
  • HTTP严格传输安全性(HSTS)不会添加到响应中

您可以使用以下Java配置轻松完成此操作:

@EnableWebSecurity
public class WebSecurityConfig extendsWebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http// ....headers().frameOptions().sameOrigin().httpStrictTransportSecurity().disable();}
}

或者,如果您使用的是Spring Security XML配置,则可以使用以下命令:



如果您不希望添加默认值并希望明确控制应使用的内容,则可以禁用默认值。下面提供了基于Java和XML的配置的示例:

如果您使用的是Spring Security的Java配置,则以下内容仅添加缓存控制。

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers()// do not use any default headers unless explicitly listed.defaultsDisabled().cacheControl();
}
}

以下XML仅添加缓存控制。



如有必要,您可以使用以下Java配置禁用所有HTTP安全响应标头:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().disable();
}
}

如有必要,您可以使用以下XML配置禁用所有HTTP安全响应标头:



缓存控制

过去Spring Security要求您为web应用程序提供自己的缓存控制。这在当时似乎是合理的,但浏览器缓存已经发展为包括用于安全连接的缓存。这意味着用户可以查看经过身份验证的页面,注销,然后恶意用户可以使用浏览器历史记录来查看缓存页面。为了帮助缓解这种情况,Spring Security添加了缓存控制支持,它将在您的响应中插入以下标头。

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0

简单地添加没有子元素的元素将自动添加Cache Control和其他一些保护。但是,如果您只想要缓存控制,则可以使用Spring Security的XML命名空间和元素以及headers @ defaults-disabled属性启用此功能。



同样,您可以使用以下命令在Java配置中仅启用缓存控制:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().defaultsDisabled().cacheControl();
}
}

如果您确实想要缓存特定响应,那么您的应用程序可以有选择地调用HttpServletResponse.setHeader(String,String)来覆盖由Spring Security设置的标头。这有助于确保正确缓存CSS,JavaScript和图像等内容。

使用Spring Web MVC时,通常在您的配置中完成。例如,以下配置将确保为所有资源设置缓存标头:

@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/resources/**").addResourceLocations("/resources/").setCachePeriod(31556926);}// ...
}

内容类型选项

历史上,浏览器(包括Internet Explorer)会尝试使用内容嗅探来猜测请求的内容类型。这允许浏览器通过猜测未指定内容类型的资源上的内容类型来改善用户体验。例如,如果浏览器遇到未指定内容类型的JavaScript文件,则可以猜测内容类型然后执行它。

[注意]注意

在允许上传内容时,还应该做许多其他事情(即仅在不同的域中显示文档,确保设置Content-Type标题,清理文档等)。但是,这些措施超出了Spring Security提供的范围。指出禁用内容嗅探时,必须指定内容类型以使事情正常工作,这一点也很重要。

内容嗅探的问题在于,这允许恶意用户使用多字符(即,作为多种内容类型有效的文件)来执行XSS攻击。例如,某些站点可能允许用户向网站提交有效的postscript文档并进行查看。恶意用户可能会创建一个postscript文档,该文档也是一个有效的JavaScript文件,并使用它执行XSS攻击。

可以通过在响应中添加以下标头来禁用内容嗅探:

X-Content-Type-Options: nosniff

与缓存控制元素一样,在使用没有子元素的元素时,默认情况下会添加nosniff指令。但是,如果您想要更多地控制添加哪些标头,可以使用元素和headers @ defaults-disabled属性,如下所示:



默认情况下,使用Spring Security Java配置添加X-Content-Type-Options标头。如果您想要更多地控制标题,可以使用以下内容显式指定内容类型选项:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().defaultsDisabled().contentTypeOptions();
}
}

HTTP严格传输安全(HSTS)

当您在银行的网站上输入内容时,是否输入mybank.example.com或输入https://mybank.example.com?如果省略https协议,则可能容易受到中间人攻击。即使网站执行重定向到https://mybank.example.com,恶意用户也可以拦截初始HTTP请求并操纵响应(即重定向到https://mibank.example.com并窃取其凭据)。

许多用户省略了https协议,这就是创建HTTP严格传输安全(HSTS)的原因。将mybank.example.com添加为HSTS主机后,浏览器可以提前知道对mybank.example.com的任何请求都应解释为https://mybank.example.com。这大大降低了中间人攻击发生的可能性。

[注意]注意

根据RFC6797,HSTS标头仅注入HTTPS响应。为了使浏览器确认标头,浏览器必须首先信任签署用于建立连接的SSL证书的CA(而不仅仅是SSL证书)。

将站点标记为HSTS主机的一种方法是将主机预加载到浏览器中。另一种方法是在响应中添加“Strict-Transport-Security”标头。例如,以下内容将指示浏览器将域视为一年的HSTS主机(一年中大约有31536000秒):

Strict-Transport-Security: max-age=31536000 ; includeSubDomains

可选的includeSubDomains指令指示Spring Security子域(即secure.mybank.example.com)也应被视为HSTS域。

与其他标头一样,Spring Security默认添加HSTS。您可以使用元素自定义HSTS标头,如下所示:



同样,您只能通过Java配置启用HSTS标头:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().httpStrictTransportSecurity().includeSubdomains(true).maxAgeSeconds(31536000);
}
}

HTTP公钥固定(HPKP)

HTTP公钥锁定(HPKP)是一种安全功能,它告诉web客户端将特定加密公钥与某个web服务器相关联,以防止伪造证书的中间人(MITM)攻击。

为了确保TLS会话中使用的服务器公钥的真实性,此公钥将包装到X.509证书中,该证书通常由证书颁发机构(CA)签名。诸如浏览器之类的Web客户端信任许多这些CA,它们都可以为任意域名创建证书。如果攻击者能够破坏单个CA,则他们可以对各种TLS连接执行MITM攻击。HPKP可以通过告诉客户端哪个公钥属于某个web服务器来规避HTTPS协议的这种威胁。HPKP是首次使用信任(TOFU)技术。web服务器第一次通过特殊的HTTP头告诉客户端哪些公钥属于它,客户端会在给定的时间段内存储此信息。当客户端再次访问服务器时,它需要一个包含公钥的证书,该公钥的指纹已通过HPKP获知。如果服务器提供未知的公钥,则客户端应向用户发出警告。

[注意]注意

由于用户代理需要针对SSL证书链验证引脚,因此HPKP标头仅注入HTTPS响应。

为您的站点启用此功能非常简单,只需在通过HTTPS访问站点时返回Public-Key-Pins HTTP标头即可。例如,以下内容将指示用户代理仅针对2个引脚向给定URI(通过report-uri指令)报告引脚验证失败:

Public-Key-Pins-Report-Only: max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=" ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="http://example.net/pkp-report" ; includeSubDomains

销验证失败报告是一个标准的JSON结构可被捕获或者由web应用程序自己的API或通过公托管HPKP报告服务,诸如,REPORT-URI

可选的includeSubDomains指令指示浏览器还使用给定的引脚验证子域。

与其他标题相反,Spring Security默认情况下不添加HPKP。您可以使用元素自定义HPKP标头,如下所示:

d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=

同样,您可以使用Java配置启用HPKP标头:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http// ....headers().httpPublicKeyPinning().includeSubdomains(true).reportUri("http://example.net/pkp-report").addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";}
}

X-框架,选项

允许将您的网站添加到框架可能是一个安全问题。例如,使用聪明的CSS样式用户可能会被欺骗点击他们不想要的东西(视频演示)。例如,登录到其银行的用户可能会单击授予其他用户访问权限的按钮。这种攻击称为Clickjacking。

[注意]注意

另一种处理点击劫持的现代方法是使用“内容安全策略(CSP)”一节。

有许多方法可以缓解点击劫持攻击。例如,为了保护旧版浏览器免受点击劫持攻击,您可以使用破帧代码。虽然不完美,但破帧代码是您可以为旧版浏览器做的最好的代码。

解决点击劫持的更现代的方法是使用X-Frame-Options标头:

X-Frame-Options: DENY

X-Frame-Options响应头指示浏览器阻止响应中具有此标头的任何站点在帧内呈现。默认情况下,Spring Security禁用iframe中的呈现。

您可以使用frame-options元素自定义X-Frame-Options 。例如,以下内容将指示Spring Security使用“X-Frame-Options:SAMEORIGIN”,它允许同一域内的iframe:



同样,您可以使用以下内容自定义框架选项以在Java配置中使用相同的源:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().frameOptions().sameOrigin();
}
}

X-XSS-保护

一些浏览器内置支持过滤掉反射的XSS攻击。这绝不是万无一失的,但确实有助于XSS保护。

默认情况下,通常会启用过滤,因此添加标头通常只会确保它已启用,并指示浏览器在检测到XSS攻击时要执行的操作。例如,过滤器可能会尝试以最少侵入性的方式更改内容以仍然呈现所有内容。有时,这种类型的替换本身可能成为XSS漏洞。相反,最好阻止内容而不是尝试修复它。为此,我们可以添加以下标头:

X-XSS-Protection: 1; mode=block

默认情况下包含此标头。但是,如果需要,我们可以自定义它。例如:



同样,您可以使用以下命令在Java Configuration中自定义XSS保护:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().xssProtection().block(false);
}
}

内容安全政策(CSP)

内容安全策略(CSP)是web应用程序可以利用的机制,用于缓解内容注入漏洞,例如跨站点脚本(XSS)。CSP是一种声明性策略,为web应用程序作者提供了一种工具,用于声明并最终通知客户端(用户代理)有关web应用程序期望加载资源的源。

[注意]注意

内容安全策略不是为了解决所有内容注入漏洞。相反,可以利用CSP来帮助减少内容注入攻击造成的伤害。作为第一道防线,web应用程序作者应验证其输入并对其输出进行编码。

web应用程序可以通过在响应中包含以下HTTP标头之一来使用CSP:

  • 内容安全,策略
  • 内容安全,策略报告,只有

这些标头中的每一个都用作向客户端提供安全策略的机制。安全策略包含一组安全策略指令(例如,script-srcobject-src),每个指令负责声明对特定资源表示的限制。

例如,web应用程序可以声明它希望通过在响应中包含以下标头来加载来自特定可信源的脚本:

Content-Security-Policy: script-src https://trustedscripts.example.com

尝试从除script-src指令中声明的内容之外的其他源加载脚本将被用户代理阻止。此外,如果在安全策略中声明了report-uri指令,则用户代理会将违规报告给声明的URL。

例如,如果web应用程序违反了声明的安全策略,则以下响应标头将指示用户代理将违规报告发送到策略的report-uri指令中指定的URL 。

Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/

违规报告是标准的JSON结构,可以由web应用程序自己的API或公共托管的CSP违规报告服务(如 REPORT-URI)捕获。

内容安全,策略报告,仅头部为web应用程序的作者和管理员监控安全策略,而不是强制他们的能力。此标头通常在试验和/或开发站点的安全策略时使用。当策略被认为有效时,可以通过使用Content-Security-Policy头字段来强制执行该策略

给定以下响应头,策略声明可以从两个可能的源之一加载脚本。

Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/

如果站点违反此策略,则尝试从evil.com加载脚本时,用户代理将向report-uri指令指定的声明URL发送违规报告,但仍然允许加载违规资源。

配置内容安全策略

请注意,Spring Security 默认情况下不会添加内容安全策略。web应用程序作者必须声明安全策略以强制和/或监视受保护资源。

例如,给定以下安全策略:

script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/

您可以使用带有元素的XML配置启用CSP标头,如下所示:



要启用CSP “仅报告”标头,请按如下方式配置元素:



同样,您可以使用Java配置启用CSP标头,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/");
}
}

要启用CSP的“仅报告”标头,请提供以下Java配置:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/").reportOnly();
}
}
其他资源

将内容安全策略应用于web应用程序通常是一件非常重要的事情。以下资源可为您的站点制定有效的安全策略提供进一步的帮助。

内容安全策略简介

CSP指南 - Mozilla开发人员网络

W3C候选人推荐

推荐人政策

引用者策略是web应用程序可以利用来管理引用者字段的机制,该字段包含用户所在的最后一页。

Spring Security的方法是使用Referrer Policy标头,它提供不同的策略:

Referrer-Policy: same-origin

Referrer-Policy响应标头指示浏览器让目标知道用户之前的源。

配置推荐人策略

Spring Security 默认情况下不会添加 Referrer Policy标头。

您可以使用带有元素的XML配置启用Referrer-Policy标头,如下所示:



同样,您可以使用Java配置启用Referrer Policy标头,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().referrerPolicy(ReferrerPolicy.SAME_ORIGIN);
}
}

功能政策

功能策略是一种允许web开发人员有选择地启用,禁用和修改浏览器中某些API和web功能的行为的机制。

Feature-Policy: geolocation 'self'

借助功能策略,开发人员可以选择加入一组“策略”,以便浏览器强制执行您网站中使用的特定功能。这些策略限制站点可以访问的API或修改浏览器对某些功能的默认行为。

配置功能策略

Spring Security 默认情况下不添加功能策略标头。

您可以使用带有元素的XML配置启用Feature-Policy标头,如下所示:



同样,您可以使用Java配置启用功能策略标头,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().featurePolicy("geolocation 'self'");
}
}

10.8.2自定义标题

Spring Security具有一些机制,可以方便地将更常见的安全标头添加到您的应用程序中。但是,它还提供了挂钩以启用添加自定义标头。

静态标题

有时您可能希望将自定义安全标头注入到您的应用程序中,并且不支持开箱即用。例如,给定以下自定义安全标头:

X-Custom-Security-Header: header-value

使用XML命名空间时,可以使用

元素将这些标头添加到响应中,如下所示:

同样,可以使用Java Configuration将标头添加到响应中,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value"));
}
}

标题作家

当命名空间或Java配置不支持所需的标头时,您可以创建自定义HeadersWriter实例,甚至可以提供HeadersWriter的自定义实现。

让我们看一下使用XFrameOptionsHeaderWriter的自定义实例的示例。也许您希望允许为同一来源构建内容。通过将policy属性设置为“SAMEORIGIN” 可以轻松支持这一点,但让我们看一下使用ref属性的更明确的示例。

我们还可以使用Java配置将内容框架限制为相同的来源:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {http// ....headers().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN));
}
}

DelegatingRequestMatcherHeaderWriter

有时您可能只想为某些请求编写标头。例如,您可能只希望保护您的登录页面不受框架限制。您可以使用DelegatingRequestMatcherHeaderWriter来执行此操作。使用XML命名空间配置时,可以使用以下命令完成此操作:

我们还可以使用java配置阻止将内容框架到登录页面:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {RequestMatcher matcher = new AntPathRequestMatcher("/login");DelegatingRequestMatcherHeaderWriter headerWriter =new DelegatingRequestMatcherHeaderWriter(matcher,new XFrameOptionsHeaderWriter());http// ....headers().frameOptions().disabled().addHeaderWriter(headerWriter);
}
}

10.9会话管理

与HTTP会话相关的功能由过滤器委托的SessionManagementFilterSessionAuthenticationStrategy接口的组合处理。典型用法包括会话固定保护攻击防范,会话超时检测以及经过身份验证的用户可能同时打开的会话数限制。

10.9.1 SessionManagementFilter

SessionManagementFilter检查SecurityContextRepository的内容与SecurityContextHolder的当前内容,以确定用户是否在当前请求期间已经过身份验证,通常是通过非交互式身份验证机制,例如认证或记住我[17]。如果存储库包含安全上下文,则过滤器不执行任何操作。如果没有,并且线程局部SecurityContext包含(非匿名)Authentication对象,则过滤器假定它们已由堆栈中的先前过滤器进行了身份验证。然后它将调用配置的SessionAuthenticationStrategy

如果用户当前未经过身份验证,则过滤器将检查是否已请求无效会话ID(例如,由于超时),并将调用已配置的InvalidSessionStrategy(如果已设置)。最常见的行为是重定向到固定的URL,这封装在标准实现SimpleRedirectInvalidSessionStrategy中。如前所述,在通过命名空间配置无效会话URL时也会使用后者。

10.9.2 SessionAuthenticationStrategy

SessionManagementFilterAbstractAuthenticationProcessingFilter都使用SessionAuthenticationStrategy,因此,如果您使用自定义的表单登录类,则需要将其注入这两个类。在这种情况下,组合命名空间和自定义bean的典型配置可能如下所示:




...

请注意,如果您在实现HttpSessionBindingListener的会话中存储bean(包括Spring会话范围的bean),则使用默认值SessionFixationProtectionStrategy可能会导致问题。有关更多信息,请参阅此类的Javadoc。

10.9.3并发控制

Spring Security能够防止主体同时对同一个应用程序进行超过指定次数的身份验证。许多ISV利用此功能来强制执行许可,而网络管理员喜欢此功能,因为它有助于防止人们共享登录名。例如,您可以阻止用户“Batman”从两个不同的会话登录web应用程序。您可以使之前的登录过期,也可以在尝试再次登录时报告错误,从而阻止第二次登录。请注意,如果您使用的是第二种方法,那么未明确注销的用户(例如,刚刚关闭浏览器的用户)将无法再次登录,直到原始会话到期为止。

命名空间支持并发控制,因此请查看较早的命名空间章节以获取最简单的配置。有时你需要自定义东西。

该实现使用SessionAuthenticationStrategy的专用版本,称为ConcurrentSessionControlAuthenticationStrategy

[注意]注意

以前,ProviderManager进行并发身份验证检查,可以注入ConcurrentSessionController。后者将检查用户是否试图超过允许的会话数。但是,这种方法需要提前创建HTTP会话,这是不希望的。在Spring Security 3中,用户首先由AuthenticationManager进行身份验证,一旦成功通过身份验证,就会创建会话并检查是否允许他们打开另一个会话。

要使用并发会话支持,您需要将以下内容添加到web.xml

org.springframework.security.web.session.HttpSessionEventPublisher

此外,您需要将ConcurrentSessionFilter添加到FilterChainProxyConcurrentSessionFilter需要两个构造函数参数sessionRegistry,它们通常指向SessionRegistryImplsessionInformationExpiredStrategy的实例,它定义了会话到期时应用的策略。使用命名空间创建FilterChainProxy和其他默认bean的配置可能如下所示:
















每次HttpSession开始或终止时,将侦听器添加到web.xml会导致ApplicationEvent发布到Spring ApplicationContext。这很关键,因为它允许在会话结束时通知SessionRegistryImpl。没有它,一旦用户超过他们的会话津贴,即使他们退出另一个会话或超时,用户也永远无法再次登录。

查询当前经过身份验证的用户及其会话的SessionRegistry

通过命名空间或使用普通bean设置并发控制有一个有用的副作用,即为您提供可在应用程序中直接使用的SessionRegistry的引用,因此即使您不想限制用户可能拥有的会话数量,无论如何都可能值得设置基础架构。您可以将maximumSession属性设置为-1以允许无限制的会话。如果您正在使用命名空间,则可以使用session-registry-alias属性为内部创建的SessionRegistry设置别名,从而提供可以注入您自己的bean的引用。

getAllPrincipals()方法为您提供当前已验证用户的列表。您可以通过调用getAllSessions(Object principal, boolean includeExpiredSessions)方法列出用户的会话,该方法返回SessionInformation对象的列表。您还可以通过在SessionInformation实例上调用expireNow()来使用户的会话到期。当用户返回应用程序时,将阻止他们继续进行。例如,您可能会发现这些方法在管理应用程序中很有用。有关更多信息,请查看Javadoc。

10.10匿名身份验证

10.10.1概述

通常认为采用“默认拒绝”是一种良好的安全措施,您可以明确指定允许的内容并禁止其他所有内容。定义未经身份验证的用户可以访问的内容也是类似的情况,特别是对于web应用程序。许多站点要求必须对用户进行身份验证,除了几个URL(例如主页和登录页面)。在这种情况下,最简单的方法是为这些特定URL定义访问配置属性,而不是为每个安全资源定义。换句话说,有时很高兴默认情况下需要ROLE_SOMETHING,并且只允许此规则的某些例外,例如应用程序的登录,注销和主页。您也可以完全从过滤器链中省略这些页面,从而绕过访问控制检查,但由于其他原因,这可能是不合需要的,特别是如果页面对经过身份验证的用户的行为不同。

这就是匿名身份验证的含义。请注意,“匿名身份验证”的用户与未经身份验证的用户之间没有真正的概念差异。Spring Security的匿名身份验证只是为您提供了一种更方便的方法来配置访问控制属性。例如,调用servlet API调用(例如getCallerPrincipal)仍将返回null,即使SecurityContextHolder中实际存在匿名身份验证对象。

在其他情况下,匿名身份验证很有用,例如审计拦截器查询SecurityContextHolder以确定哪个主体负责给定操作。如果类知道SecurityContextHolder总是包含Authentication对象,而且从不null,则可以更健壮地创建类。

10.10.2配置

使用HTTP配置Spring Security 3.0时会自动提供匿名身份验证支持,并且可以使用元素自定义(或禁用)。除非使用传统的bean配置,否则不需要配置此处描述的bean。

三个类一起提供匿名身份验证功能。AnonymousAuthenticationTokenAuthentication的实现,并存储适用于匿名主体的GrantedAuthority。有一个相应的AnonymousAuthenticationProvider,它被链接到ProviderManager,以便接受AnonymousAuthenticationToken。最后,有一个AnonymousAuthenticationFilter,它在正常的身份验证机制之后被链接,如果那里没有Authentication,则自动将AnonymousAuthenticationToken添加到SecurityContextHolder。过滤器和身份验证提供程序的定义如下所示:







key在过滤器和身份验证提供程序之间共享,因此前者创建的令牌被后者接受[18]userAttributeusernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]的形式表示。这与InMemoryDaoImpluserMap属性的等号后使用的语法相同。

如前所述,匿名身份验证的好处是所有URI模式都可以应用安全性。例如:




" +


10.10.3 AuthenticationTrustResolver

完整的匿名身份验证讨论是AuthenticationTrustResolver接口,其对应的AuthenticationTrustResolverImpl实现。此接口提供isAnonymous(Authentication)方法,允许感兴趣的类考虑这种特殊类型的身份验证状态。ExceptionTranslationFilter在处理AccessDeniedException时使用此接口。如果抛出AccessDeniedException,并且身份验证是匿名类型,而不是抛出403(禁止)响应,则过滤器将启动AuthenticationEntryPoint,以便主体可以正确进行身份验证。这是必要的区别,否则主体将始终被视为“经过身份验证”,并且永远不会有机会通过表单,基本,摘要或其他一些正常的身份验证机制进行登录。

您经常会看到上面的拦截器配置中的ROLE_ANONYMOUS属性被IS_AUTHENTICATED_ANONYMOUSLY替换,这在定义访问控制时实际上是相同的。这是使用AuthenticatedVoter的一个例子,我们将在授权章节中看到。它使用AuthenticationTrustResolver来处理此特定配置属性并授予匿名用户访问权限。AuthenticatedVoter方法更强大,因为它允许您区分匿名,记住我和完全认证的用户。如果您不需要此功能,那么您可以使用ROLE_ANONYMOUS,这将由Spring Security的标准RoleVoter处理。

10.11 WebSocket安全性

Spring Security 4增加了对保护Spring的WebSocket支持的支持。本节介绍如何使用Spring Security的WebSocket支持。

[注意]注意

您可以在https://github.com/spring-projects/spring-session/tree/master/samples/boot/websocket上找到WebSocket安全性的完整工作示例。

直接JSR-356支持

Spring Security不提供直接的JSR-356支持,因为这样做几乎没有价值。这是因为格式未知,因此很少有Spring可以保护未知格式。此外,JSR-356没有提供拦截消息的方法,因此安全性会相当具有侵略性。

10.11.1 WebSocket配置

Spring Security 4.0通过Spring消息传递抽象引入了对WebSockets的授权支持。要使用Java Configuration配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。例如:

@Configuration
public class WebSocketSecurityConfigextends AbstractSecurityWebSocketMessageBrokerConfigurer { 1 2protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {messages.simpDestMatchers("/user/*").authenticated() 3}
}

这将确保:

1

任何入站CONNECT消息都需要有效的CSRF令牌来强制实施同源策略

2

对于任何入站请求,SecurityContextHolder将在simpUser头属性中填充用户。

3

我们的消息需要适当的授权。具体来说,任何以“/ user /”开头的入站邮件都需要ROLE_USER。有关授权的更多详细信息,请参见第10.11.3节“WebSocket授权”

Spring Security还提供XML Namespace支持以保护WebSockets。基于XML的可比配置如下所示:

 1 23

这将确保:

1

任何入站CONNECT消息都需要有效的CSRF令牌来强制实施同源策略

2

对于任何入站请求,SecurityContextHolder将在simpUser头属性中填充用户。

3

我们的消息需要适当的授权。具体来说,任何以“/ user /”开头的入站邮件都需要ROLE_USER。有关授权的更多详细信息,请参见第10.11.3节“WebSocket授权”

10.11.2 WebSocket认证

WebSockets重用与WebSocket连接时在HTTP请求中找到的相同身份验证信息。这意味着HttpServletRequest上的Principal将被移交给WebSockets。如果您使用Spring Security,则会自动覆盖HttpServletRequest上的Principal

更具体地说,为了确保用户已经对您的WebSocket应用程序进行了身份验证,所有必要的是确保您设置Spring Security来验证基于HTTP的web应用程序。

10.11.3 WebSocket授权

Spring Security 4.0通过Spring消息传递抽象引入了对WebSockets的授权支持。要使用Java Configuration配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。例如:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {@Overrideprotected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {messages.nullDestMatcher().authenticated() 1.simpSubscribeDestMatchers("/user/queue/errors").permitAll() 2.simpDestMatchers("/app/**").hasRole("USER") 3.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") 4.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() 5.anyMessage().denyAll(); 6}
}

这将确保:

1

任何没有目的地的消息(即消息类型为MESSAGE或SUBSCRIBE以外的任何消息)都需要用户进行身份验证

2

任何人都可以订阅/ user / queue / errors

3

任何目标以“/ app /”开头的消息都要求用户具有角色ROLE_USER

4

任何以SUBSCRIBE类型的“/ user /”或“/ topic / friends /”开头的消息都需要ROLE_USER

五

MESSAGE或SUBSCRIBE类型的任何其他消息都将被拒绝。由于6我们不需要这一步,但它说明了如何匹配特定的消息类型。

6

任何其他消息都被拒绝。这是一个好主意,以确保您不会错过任何消息。

Spring Security还提供XML Namespace支持以保护WebSockets。基于XML的可比配置如下所示:

1 2      345 6

这将确保:

1

任何类型为CONNECT,UNSUBSCRIBE或DISCONNECT的消息都需要对用户进行身份验证

2

任何人都可以订阅/ user / queue / errors

3

任何目标以“/ app /”开头的消息都要求用户具有角色ROLE_USER

4

任何以SUBSCRIBE类型的“/ user /”或“/ topic / friends /”开头的消息都需要ROLE_USER

五

MESSAGE或SUBSCRIBE类型的任何其他消息都将被拒绝。由于6我们不需要这一步,但它说明了如何匹配特定的消息类型。

6

具有目的地的任何其他消息都被拒绝。这是一个好主意,以确保您不会错过任何消息。

WebSocket授权说明

为了正确保护您的应用程序,了解Spring的WebSocket支持非常重要。

消息类型的WebSocket授权

重要的是要理解SUBSCRIBE和MESSAGE类型的消息之间的区别以及它在Spring中的工作方式。

考虑聊天应用程序。

  • 系统可以通过“/ topic / system / notifications”目的地向所有用户发送通知MESSAGE
  • 客户端可以通过SUBSCRIBE接收“/ topic / system / notifications”的通知。

虽然我们希望客户端能够SUBSCRIBE到“/ topic / system / notifications”,但我们不希望它们能够将MESSAGE发送到该目的地。如果我们允许向“/ topic / system / notifications”发送MESSAGE,则客户端可以直接向该端点发送消息并模拟系统。

通常,应用程序通常拒绝发送到以代理前缀开头的消息(即“/ topic /”或“/ queue /”)的任何MESSAGE 。

目的地的WebSocket授权

了解目的地如何转变也很重要。

考虑聊天应用程序。

  • 用户可以通过向目的地“/ app / chat”发送消息来向特定用户发送消息。
  • 应用程序看到消息,确保将“from”属性指定为当前用户(我们不能信任客户端)。
  • 然后,应用程序使用SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)将消息发送给收件人。
  • 消息将变为“/ queue / user / messages- ”的目标

通过上面的应用程序,我们希望允许我们的客户端监听转换为“/ queue / user / messages- ”的“/ user / queue”。但是,我们不希望客户端能够侦听“/ queue / *”,因为这样可以让客户端看到每个用户的消息。

通常,应用程序通常拒绝发送到以代理前缀开头的消息(即“/ topic /”或“/ queue /”)的任何SUBSCRIBE 。当然,我们可能会提供例外来解释类似的事情

出站邮件

Spring包含一个标题为消息流的部分,描述消息如何流经系统。值得注意的是,Spring Security只能保证clientInboundChannel。Spring Security并未试图获得clientOutboundChannel

最重要的原因是性能。对于每一条消息,通常会有更多消息传出去。我们鼓励保护订阅端点,而不是保护出站邮件。

10.11.4实施同源策略

重要的是要强调浏览器不会为WebSocket连接强制实施同源策略。这是一个非常重要的考虑因素。

为什么同源?

请考虑以下情形。用户访问bank.com并对其帐户进行身份验证。同一个用户在浏览器中打开另一个选项卡并访问evil.com。同源策略确保evil.com无法读取或写入bank.com的数据。

使用WebSockets时,同源策略不适用。事实上,除非bank.com明确禁止,否则evil.com可以代表用户读写数据。这意味着用户可以通过webSocket执行任何操作(即转账),evil.com可以代表该用户进行操作。

由于SockJS试图模拟WebSockets,它也绕过了同源策略。这意味着开发人员在使用SockJS时需要明确保护其应用程序免受外部域的影响。

Spring WebSocket允许来源

幸运的是,由于Spring 4.1.5 Spring的WebSocket和SockJS支持限制了对当前域的访问。Spring Security增加了额外的保护层,以提供深度防御。

将CSRF添加到Stomp Headers

默认情况下,Spring Security需要任何CONNECT消息类型中的CSRF令牌。这可确保只有可以访问CSRF令牌的站点才能连接。由于只有Same Origin可以访问CSRF令牌,因此不允许外部域建立连接。

通常,我们需要在HTTP标头或HTTP参数中包含CSRF令牌。但是,SockJS不允许这些选项。相反,我们必须在Stomp标头中包含令牌

应用程序可以通过访问名为_csrf的请求属性来获取CSRF令牌。例如,以下内容将允许访问JSP中的CsrfToken

var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";

如果您使用的是静态HTML,则可以在REST端点上公开CsrfToken。例如,以下内容将公开URL / csrf上的CsrfToken

@RestController
public class CsrfController {@RequestMapping("/csrf")public CsrfToken csrf(CsrfToken token) {return token;}
}

JavaScript可以对端点进行REST调用,并使用响应来填充headerName和令牌。

我们现在可以在Stomp客户端中包含令牌。例如:

...
var headers = {};
headers[headerName] = token;
stompClient.connect(headers, function(frame) {...}

在WebSockets中禁用CSRF

如果您想允许其他域访问您的网站,您可以禁用Spring Security的保护。例如,在Java Configuration中,您可以使用以下命令:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {...@Overrideprotected boolean sameOriginDisabled() {return true;}
}

10.11.5使用SockJS

SockJS提供后备传输以支持旧版浏览器。使用后备选项时,我们需要放宽一些安全约束,以允许SockJS与Spring Security一起使用。

SockJS和框架选项

SockJS可以使用利用iframe的传输。默认情况下,Spring Security将拒绝该站点被阻止以防止Clickjacking攻击。为了允许基于SockJS帧的传输工作,我们需要配置Spring Security以允许相同的源来构建内容。

您可以使用frame-options元素自定义X-Frame-Options 。例如,以下内容将指示Spring Security使用“X-Frame-Options:SAMEORIGIN”,它允许同一域内的iframe:



同样,您可以使用以下内容自定义框架选项以在Java配置中使用相同的源:

@EnableWebSecurity
public class WebSecurityConfig extendsWebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http// ....headers().frameOptions().sameOrigin();}
}

SockJS和放松CSRF

对于任何基于HTTP的传输,SockJS在CONNECT消息上使用POST。通常,我们需要在HTTP标头或HTTP参数中包含CSRF令牌。但是,SockJS不允许这些选项。相反,我们必须在Stomp标头中包含令牌,如“将CSRF添加到Stomp Headers”一节中所述。

这也意味着我们需要使用web层来放松我们的CSRF保护。具体来说,我们要为连接URL禁用CSRF保护。我们不想为每个URL禁用CSRF保护。否则我们的网站将容易受到CSRF攻击。

我们可以通过提供CSRF RequestMatcher轻松实现这一目标。我们的Java配置使这非常简单。例如,如果我们的stomp端点是“/ chat”,我们可以使用以下配置仅对以“/ chat /”开头的URL禁用CSRF保护:

@Configuration
@EnableWebSecurity
public class WebSecurityConfigextends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf()// ignore our stomp endpoints since they are protected using Stomp headers.ignoringAntMatchers("/chat/**").and().headers()// allow same origin to frame our site to support iframe SockJS.frameOptions().sameOrigin().and().authorizeRequests()...

如果我们使用基于XML的配置,我们可以使用csrf @ request-matcher-ref。例如:

...



[6]请注意,您需要在应用程序上下文XML文件中包含安全命名空间才能使用此语法。仍然支持使用filter-chain-map的旧语法,但不赞成使用构造函数参数注入。

[7] request-matcher-ref属性可用于指定RequestMatcher实例以实现更强大的匹配,而不是路径模式

[8]当浏览器不支持cookie并且在分号后将jsessionid参数附加到URL时,您可能已经看到了这一点。但是,RFC允许在URL的任何路径段中存在这些参数

[9]一旦请求离开FilterChainProxy,将返回原始值,因此仍可供应用程序使用。

[10]因此,例如,原始请求路径/secure;hack=1/somefile.html;hack=2将作为/secure/somefile.html返回。

[11]我们使用forward,因此SecurityContextHolder仍然包含主体的详细信息,这可能对显示给用户很有用。在Spring Security的旧版本中,我们依靠servlet容器来处理403错误消息,该消息缺少这种有用的上下文信息。

[12]在Spring Security 2.0及更早版本中,此过滤器被称为HttpSessionContextIntegrationFilter并且执行了存储上下文的所有工作都是由过滤器本身执行的。如果您熟悉此类,那么现在可以在HttpSessionSecurityContextRepository上找到大多数可用的配置选项。

[13]由于历史原因,在Spring Security 3.0之前,此过滤器被称为AuthenticationProcessingFilter,入口点被称为AuthenticationProcessingFilterEntryPoint。由于框架现在支持许多不同形式的身份验证,因此它们在3.0中都被赋予了更具体的名称。

[14]在3.0之前的版本中,此时的应用程序流程已演变为一个阶段,由此类和策略插件的混合属性控制。决定3.0重构代码以使这两个策略完全负责。

[15]如果DigestAuthenticationFilter.passwordAlreadyEncoded设置为true,则可以以HEX(MD5(用户名:领域:密码))格式对密码进行编码。但是,其他密码编码不适用于摘要式身份验证。

[16]基本上,用户名不包含在cookie中,以防止不必要地暴露有效的登录名。在本文的评论部分对此进行了讨论。

[17] SessionManagementFilter将不会检测通过身份验证后执行重定向的机制(例如表单登录)进行身份验证,因为在身份验证请求期间不会调用过滤器。在这些情况下,必须单独处理会话管理功能。

[18] key财产的使用不应被视为在此提供任何真正的安全。这只是一本簿记练习。如果您在身份验证客户端可以构造Authentication对象(例如使用RMI调用)的情况下共享ProviderManager,其中包含AnonymousAuthenticationProvider,则恶意客户端可以提交它创建的AnonymousAuthenticationToken(选择了用户名和权限列表)。如果key是可猜测的或可以找到,那么匿名提供者将接受该令牌。这不是正常使用的问题,但如果您使用的是RMI,最好使用自定义的ProviderManager,它会省略匿名提供程序,而不是共享您用于HTTP身份验证机制的提供程序。

11.授权

Spring Security内的高级授权功能是其受欢迎程度最引人注目的原因之一。无论您选择如何进行身份验证 - 无论是使用Spring Security - 提供的机制和提供程序,还是与容器或其他非Spring Security身份验证机构集成 - 您都会发现授权服务可以在您的应用程序中使用一致而简单的方式。

在这一部分中,我们将探讨第一部分中介绍的不同AbstractSecurityInterceptor实现。然后我们继续探讨如何通过使用域访问控制列表来微调授权。

11.1授权体系结构

11.1.1当局

正如我们在技术概述中看到的,所有Authentication实现都存储了GrantedAuthority对象的列表。这些代表已授予委托人的当局。GrantedAuthority对象由AuthenticationManager插入Authentication对象,稍后由AccessDecisionManager在做出授权决定时读取。

GrantedAuthority是一个只有一个方法的接口:

String getAuthority();

此方法允许AccessDecisionManager s获得GrantedAuthority的精确String表示。通过将表示作为String返回,GrantedAuthority可以很容易地“读取”GrantedAuthority。如果GrantedAuthority不能精确地表示为String,则GrantedAuthority被视为“复杂”,getAuthority()必须返回null

“复杂”GrantedAuthority的一个示例是存储适用于不同客户帐号的操作和权限阈值列表的实现。将此复合体GrantedAuthority表示为String将非常困难,因此getAuthority()方法应返回null。这将向任何AccessDecisionManager表明它需要专门支持GrantedAuthority实施以了解其内容。

Spring Security包括一个具体的GrantedAuthority实施,SimpleGrantedAuthority。这允许任何用户指定的String转换为GrantedAuthority。安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority填充Authentication对象。

11.1.2预调用处理

正如我们在技术概述章节中看到的那样,Spring Security提供了拦截器来控制对安全对象的访问,例如方法调用或web请求。AccessDecisionManager是否允许调用是否允许进行调用。

AccessDecisionManager

AccessDecisionManagerAbstractSecurityInterceptor调用,负责做出最终的访问控制决策。AccessDecisionManager接口包含三种方法:

void decide(Authentication authentication, Object secureObject,Collection attrs) throws AccessDeniedException;boolean supports(ConfigAttribute attribute);boolean supports(Class clazz);

AccessDecisionManagerdecide方法传递了它所需的所有相关信息,以便做出授权决定。特别是,传递secure Object可以检查实际安全对象调用中包含的那些参数。例如,假设安全对象是MethodInvocation。查询MethodInvocation任何Customer参数很容易,然后在AccessDecisionManager中实现某种安全逻辑,以确保允许委托人对该客户进行操作。如果访问被拒绝,预计实现将抛出AccessDeniedException

AbstractSecurityInterceptor在启动时调用supports(ConfigAttribute)方法来确定AccessDecisionManager是否可以处理传递的ConfigAttribute。安全拦截器实现调用supports(Class)方法以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全对象的类型。

基于投票的AccessDecisionManager实现

虽然用户可以实现自己的AccessDecisionManager来控制授权的所有方面,但Spring Security包括几个基于投票的AccessDecisionManager实现。 图11.1“投票决策管理器”说明了相关的类。

图11.1。投票决策经理

进入决策投票

使用此方法,将对授权决策轮询一系列AccessDecisionVoter实现。然后AccessDecisionManager根据对选票的评估决定是否投出AccessDeniedException

AccessDecisionVoter接口有三种方法:

int vote(Authentication authentication, Object object, Collection attrs);boolean supports(ConfigAttribute attribute);boolean supports(Class clazz);

具体实现返回int,可能的值反映在AccessDecisionVoter静态字段ACCESS_ABSTAINACCESS_DENIEDACCESS_GRANTED中。如果投票实施对授权决定没有意见,则返回ACCESS_ABSTAIN。如果确实有意见,则必须返回ACCESS_DENIEDACCESS_GRANTED

提供Spring Security的三个具体AccessDecisionManager与计票结果相符。ConsensusBased实施将基于非弃权投票的共识授予或拒绝访问。提供Properties来控制投票相等或所有投票弃权的行为。如果收到一个或多个ACCESS_GRANTED票,则AffirmativeBased实施将授予访问权(即如果至少有一个授权投票,则拒绝投票将被忽略)。与ConsensusBased实施一样,如果所有选民弃权,都有一个控制行为的参数。UnanimousBased提供者期望获得一致ACCESS_GRANTED票,以便授予访问权限,而忽略弃权。如果有任何ACCESS_DENIED投票,它将拒绝访问。与其他实现一样,如果所有选民弃权,则有一个参数可以控制行为。

可以实现以不同方式计算选票的自定义AccessDecisionManager。例如,来自特定AccessDecisionVoter的投票可能会获得额外的权重,而来自特定选民的拒绝投票则可能具有否决权。

的RoleVoter

Spring Security提供的最常用的AccessDecisionVoter是简单的RoleVoter,它将配置属性视为简单的角色名称,并在用户被分配了该角色时授予访问权限。

如果任何ConfigAttribute以前缀ROLE_开头,它将投票。如果有GrantedAuthority返回String表示(通过getAuthority()方法)完全等于从前缀ROLE_开始的一个或多个ConfigAttributes,它将投票授予访问权限。如果与ROLE_开头的任何ConfigAttribute没有完全匹配,则RoleVoter将投票拒绝访问。如果没有ConfigAttributeROLE_开头,选民将弃权。

AuthenticatedVoter使用

我们隐含看到的另一个选民是AuthenticatedVoter,它可用于区分匿名,完全身份验证和记住身份验证的用户。许多站点允许在remember-me身份验证下进行某些有限访问,但需要用户通过登录进行完全访问来确认其身份。

当我们使用属性IS_AUTHENTICATED_ANONYMOUSLY授予匿名访问权限时,AuthenticatedVoter正在处理此属性。有关更多信息,请参阅此类的Javadoc。

自定义选民

显然,您还可以实现自定义AccessDecisionVoter,并且您可以将所需的任何访问控制逻辑放在其中。它可能特定于您的应用程序(与业务逻辑相关),也可能实现某些安全管理逻辑。例如,您可以在Spring web网站上找到一篇博客文章,其中介绍了如何使用投票人实时拒绝帐户被暂停的用户访问。

11.1.3调用处理后

虽然在继续安全对象调用之前AbstractSecurityInterceptor调用AccessDecisionManager,但某些应用程序需要一种修改安全对象调用实际返回的对象的方法。虽然您可以轻松实现自己的AOP关注来实现这一点,但Spring Security提供了一个方便的钩子,它具有几个与其ACL功能集成的具体实现。

图11.2“调用实现后”说明了Spring Security的AfterInvocationManager及其具体实现。

图11.2。在调用实现之后

在调用之后

与Spring Security的许多其他部分一样,AfterInvocationManager具有单个具体实现AfterInvocationProviderManager,其轮询AfterInvocationProvider的列表。允许每个AfterInvocationProvider修改返回对象或抛出AccessDeniedException。实际上,多个提供者可以修改对象,因为先前提供者的结果被传递到列表中的下一个提供者。

请注意,如果您使用的是AfterInvocationManager,则仍需要允许MethodSecurityInterceptorAccessDecisionManager允许操作的配置属性。如果您使用典型的Spring Security包含的AccessDecisionManager实现,则没有为特定安全方法调用定义配置属性将导致每个AccessDecisionVoter放弃投票。反过来,如果AccessDecisionManager属性“allowIfAllAbstainDecisions”为false,则会抛出AccessDeniedException。您可以通过(i)将“allowIfAllAbstainDecisions”设置为true(尽管通常不建议这样做)或(ii)确保至少有一个AccessDecisionVoter投票的配置属性来避免此潜在问题授予访问权限。后一种(推荐)方法通常通过ROLE_USERROLE_AUTHENTICATED配置属性来实现。

11.1.4分层角色

通常要求应用程序中的特定角色应自动“包含”其他角色。例如,在具有“admin”和“user”角色概念的应用程序中,您可能希望管理员能够执行普通用户可以执行的所有操作。为此,您可以确保为所有管理员用户分配“用户”角色。或者,您可以修改每个需要“用户”角色的访问约束,以包含“admin”角色。如果您的应用程序中有许多不同的角色,这可能会变得非常复杂。

通过使用角色层次结构,您可以配置哪些角色(或权限)应包含其他角色。Spring Security的RoleVoter RoleHierarchyVoter 的扩展版本配置了RoleHierarchy,从中获取用户所分配的所有“可达权限”。典型配置可能如下所示:



ROLE_ADMIN > ROLE_STAFFROLE_STAFF > ROLE_USERROLE_USER > ROLE_GUEST

在这里,我们在层次结构ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST中有四个角色。使用ROLE_ADMIN进行身份验证的用户,在使用上述RoleHierarchyVoter配置的AccessDecisionManager评估安全性约束时,其行为就像拥有所有四个角色一样。>符号可以被认为是“包含”。

角色层次结构提供了一种简便的方法,可以简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量。对于更复杂的要求,您可能希望在应用程序所需的特定访问权限和分配给用户的角色之间定义逻辑映射,在加载用户信息时在两者之间进行转换。

11.2安全对象实现

11.2.1 AOP联盟(MethodInvocation)安全拦截器

在Spring Security 2.0之前,保护MethodInvocation需要相当多的锅炉板配置。现在推荐的方法安全性方法是使用命名空间配置。这样,方法安全基础结构bean就会自动为您配置,因此您实际上不需要了解实现类。我们将简要介绍这里涉及的类。

使用MethodSecurityInterceptor强制执行方法安全性,这可以保护MethodInvocation s。根据配置方法,拦截器可能特定于单个bean或在多个bean之间共享。拦截器使用MethodSecurityMetadataSource实例来获取适用于特定方法调用的配置属性。MapBasedMethodSecurityMetadataSource用于存储由方法名称(可以是通配符)键控的配置属性,并且在使用元素在应用程序上下文中定义属性时将在内部使用。其他实现将用于处理基于注释的配置。

显式MethodSecurityInterceptor配置

您当然可以在应用程序上下文中直接配置MethodSecurityIterceptor,以便与Spring AOP的代理机制之一一起使用:








11.2.2 AspectJ(JoinPoint)安全拦截器

AspectJ安全拦截器与上一节中讨论的AOP Alliance安全拦截器非常相似。实际上,我们只讨论本节的不同之处。

AspectJ拦截器名为AspectJSecurityInterceptor。与AOP Alliance安全拦截器不同,后者依赖Spring应用程序上下文通过代理编织安全拦截器,AspectJSecurityInterceptor通过AspectJ编译器编译。在同一应用程序中使用两种类型的安全拦截器并不罕见,AspectJSecurityInterceptor用于域对象实例安全性,AOP Alliance MethodSecurityInterceptor用于服务层安全性。

我们首先考虑如何在Spring应用程序上下文中配置AspectJSecurityInterceptor








如您所见,除了类名,AspectJSecurityInterceptor与AOP Alliance安全拦截器完全相同。实际上,两个拦截器可以共享相同的securityMetadataSource,因为SecurityMetadataSourcejava.lang.reflect.Method一起使用而不是AOP库特定的类。当然,您的访问决策可以访问相关的AOP库特定的调用(即MethodInvocationJoinPoint),因此在进行访问决策时可以考虑一系列添加标准(例如方法参数)。

接下来,您需要定义AspectJ aspect。例如:

package org.springframework.security.samples.aspectj;import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor;
import org.springframework.security.access.intercept.aspectj.AspectJCallback;
import org.springframework.beans.factory.InitializingBean;public aspect DomainObjectInstanceSecurityAspect implements InitializingBean {private AspectJSecurityInterceptor securityInterceptor;pointcut domainObjectInstanceExecution(): target(PersistableEntity)&& execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect);Object around(): domainObjectInstanceExecution() {if (this.securityInterceptor == null) {return proceed();}AspectJCallback callback = new AspectJCallback() {public Object proceedWithObject() {return proceed();}};return this.securityInterceptor.invoke(thisJoinPoint, callback);}public AspectJSecurityInterceptor getSecurityInterceptor() {return securityInterceptor;}public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) {this.securityInterceptor = securityInterceptor;}public void afterPropertiesSet() throws Exception {if (this.securityInterceptor == null)throw new IllegalArgumentException("securityInterceptor required");}}
}

在上面的示例中,安全拦截器将应用于PersistableEntity的每个实例,这是一个未显示的抽象类(您可以使用您喜欢的任何其他类或pointcut表达式)。对于那些好奇的人,需要AspectJCallback,因为proceed();声明仅在around()体内有特殊意义。AspectJSecurityInterceptor在希望目标对象继续时调用此匿名AspectJCallback类。

您需要配置Spring以加载方面并使用AspectJSecurityInterceptor连接它。实现此目的的bean声明如下所示:




而已!现在,您可以使用您认为合适的任何方式(例如new Person();)从应用程序中的任何位置创建bean,并且它们将应用安全拦截器。

11.3基于表达式的访问控制

Spring Security 3.0引入了使用Spring EL表达式作为授权机制的能力,以及之前见过的配置属性和访问决策选民的简单使用。基于表达式的访问控制建立在相同的体系结构上,但允许将复杂的布尔逻辑封装在单个表达式中。

11.3.1概述

Spring Security使用Spring EL表达支持,如果您有兴趣更深入地理解该主题,您应该看看它是如何工作的。表达式使用“根对象”作为评估上下文的一部分进行评估。Spring Security使用web的特定类和方法安全性作为根对象,以便提供内置表达式和对诸如当前主体之类的值的访问。

常见的内置表达式

表达式根对象的基类是SecurityExpressionRoot。这提供了web和方法安全性中可用的一些常用表达式。

表11.1。常见的内置表达式

表达描述

hasRole([role])

如果当前主体具有指定角色,则返回true。默认情况下,如果提供的角色不以“ROLE_”开头,则会添加该角色。这可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix来自定义。

hasAnyRole([role1,role2])

如果当前主体具有任何提供的角色(以逗号分隔的字符串列表给出),则返回true。默认情况下,如果提供的角色不以“ROLE_”开头,则会添加该角色。这可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix来自定义。

hasAuthority([authority])

如果当前主体具有指定的权限,则返回true

hasAnyAuthority([authority1,authority2])

如果当前主体具有任何提供的权限(以逗号分隔的字符串列表给出),则返回true

principal

允许直接访问代表当前用户的主体对象

authentication

允许直接访问从SecurityContext获取的当前Authentication对象

permitAll

始终评估为true

denyAll

始终评估为false

isAnonymous()

如果当前主体是匿名用户,则返回true

isRememberMe()

如果当前主体是remember-me用户,则返回true

isAuthenticated()

如果用户不是匿名用户,则返回true

isFullyAuthenticated()

如果用户不是匿名用户或记住我用户,则返回true

hasPermission(Object target, Object permission)

如果用户有权访问给定权限的提供目标,则返回true。例如,hasPermission(domainObject, 'read')

hasPermission(Object targetId, String targetType, Object permission)

如果用户有权访问给定权限的提供目标,则返回true。例如,hasPermission(1, 'com.example.domain.Message', 'read')

11.3.2 Web安全表达式

要使用表达式来保护单个URL,首先需要将元素中的use-expressions属性设置为true。然后Spring Security将期望元素的access属性包含Spring EL表达式。表达式应该计算为布尔值,定义是否允许访问。例如:

...

在这里,我们定义了应用程序的“admin”区域(由URL模式定义)仅对具有授权权限“admin”且其IP地址与本地子网匹配的用户可用。我们已经在上一节中看到了内置的hasRole表达式。表达式hasIpAddress是一个额外的内置表达式,特定于web安全性。它由WebSecurityExpressionRoot类定义,其实例在评估web - 访问表达式时用作表达式根对象。此对象还直接在名称request下公开HttpServletRequest对象,因此您可以直接在表达式中调用请求。如果正在使用表达式,则WebExpressionVoter将添加到命名空间使用的AccessDecisionManager。因此,如果您不使用命名空间并且想要使用表达式,则必须在配置中添加其中一个。

参考Web安全表达式中的Bean

如果您希望扩展可用的表达式,可以轻松引用您公开的任何Spring Bean。例如,假设您有一个名为webSecurity的Bean,其中包含以下方法签名:

public class WebSecurity {public boolean check(Authentication authentication, HttpServletRequest request) {...}
}

您可以使用以下方法引用该方法:

...

或者在Java配置中

http.authorizeRequests().antMatchers("/user/**").access("@webSecurity.check(authentication,request)")...

Web安全表达式中的路径变量

有时能够引用URL中的路径变量很好。例如,考虑一个RESTful应用程序,它以/user/{userId}格式从URL路径中按ID查找用户。

您可以通过将路径变量放在模式中来轻松引用它。例如,如果您的Bean名称为webSecurity,则包含以下方法签名:

public class WebSecurity {public boolean checkUserId(Authentication authentication, int id) {...}
}

您可以使用以下方法引用该方法:

...

或者在Java配置中

http.authorizeRequests().antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")...

在两种配置中,匹配的URL都会将路径变量(并将其转换为)传递给checkUserId方法。例如,如果网址为/user/123/resource,则传入的ID为123

11.3.3方法安全表达式

方法安全性比简单的允许或拒绝规则稍微复杂一些。Spring Security 3.0引入了一些新的注释,以便全面支持表达式的使用。

@Pre和@Post Annotations

有四个注释支持表达式属性,以允许调用前和调用后授权检查,还支持过滤提交的集合参数或返回值。它们是@PreAuthorize@PreFilter@PostAuthorize@PostFilter。它们的使用是通过global-method-security命名空间元素启用的:


使用@PreAuthorize和@PostAuthorize进行访问控制

最明显有用的注释是@PreAuthorize,它决定了是否可以实际调用方法。例如(来自“Contacts”示例应用程序)

@PreAuthorize("hasRole('USER')")
public void create(Contact contact);

这意味着只有角色为“ROLE_USER”的用户才能访问。显然,使用传统配置和所需角色的简单配置属性可以轻松实现相同的目标。但是关于:

@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);

这里我们实际上使用方法参数作为表达式的一部分来决定当前用户是否具有给定联系人的“admin”权限。内置的hasPermission()表达式通过应用程序上下文链接到Spring Security ACL模块,我们将在下面看到。您可以按名称访问任何方法参数作为表达式变量。

Spring Security可以通过多种方式解析方法参数。Spring Security使用DefaultSecurityParameterNameDiscoverer来发现参数名称。默认情况下,对整个方法尝试以下选项。

  • 如果Spring Security的@P注释出现在方法的单个参数上,则将使用该值。这对于使用JDK 8之前的JDK编译的接口非常有用,它不包含有关参数名称的任何信息。例如:

    import org.springframework.security.access.method.P;...@PreAuthorize("#c.name == authentication.name")
    public void doSomething(@P("c") Contact contact);
    

    在幕后使用AnnotationParameterNameDiscoverer实现了这种用法,可以对其进行自定义以支持任何指定注释的value属性。

  • 如果Spring Data的@Param注释出现在方法的至少一个参数上,则将使用该值。这对于使用JDK 8之前的JDK编译的接口非常有用,它不包含有关参数名称的任何信息。例如:

    import org.springframework.data.repository.query.Param;...@PreAuthorize("#n == authentication.name")
    Contact findContactByName(@Param("n") String name);
    

    在幕后使用AnnotationParameterNameDiscoverer实现了这种用法,可以对其进行自定义以支持任何指定注释的value属性。

  • 如果使用JDK 8来编译带有-parameters参数的源并且正在使用Spring 4+,则使用标准JDK反射API来发现参数名称。这适用于类和接口。
  • 最后,如果使用调试符号编译代码,则将使用调试符号发现参数名称。这对接口不起作用,因为它们没有关于参数名称的调试信息。对于接口,必须使用注释或JDK 8方法。

表达式中提供了任何Spring - EL功能,因此您还可以访问参数的属性。例如,如果您希望某个特定方法仅允许访问其用户名与联系人的用户名匹配的用户,则可以编写

@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);

这里我们访问另一个内置表达式authentication,它是存储在安全上下文中的Authentication。您还可以使用表达式principal直接访问其“principal”属性。该值通常为UserDetails实例,因此您可以使用principal.usernameprincipal.enabled之类的表达式。

不太常见的是,您可能希望在调用方法后执行访问控制检查。这可以使用@PostAuthorize注释来实现。要从方法访问返回值,请在表达式中使用内置名称returnObject

使用@PreFilter和@PostFilter进行过滤

您可能已经意识到,Spring Security支持对集合和数组进行过滤,现在可以使用表达式实现。这通常是在方法的返回值上执行的。例如:

@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List getAll();

使用@PostFilter注释时,Spring Security遍历返回的集合并删除所提供的表达式为false的所有元素。名称filterObject指的是集合中的当前对象。您也可以在方法调用之前使用@PreFilter进行过滤,尽管这是一个不太常见的要求。语法是一样的,但是如果有多个参数是集合类型,那么您必须使用此批注的filterTarget属性按名称选择一个。

请注意,过滤显然不能替代调整数据检索查询。如果您要过滤大型集合并删除许多条目,那么这可能效率低下。

内置表达式

有一些特定于方法安全性的内置表达式,我们已经在上面使用过了。filterTargetreturnValue值很简单,但使用hasPermission()表达式需要仔细研究。

PermissionEvaluator接口

hasPermission()表达式被委托给PermissionEvaluator的实例。它旨在桥接表达式系统和Spring Security的ACL系统,允许您根据抽象权限指定域对象的授权约束。它没有对ACL模块的明确依赖性,因此如果需要,您可以将其交换为替代实现。界面有两种方法:

boolean hasPermission(Authentication authentication, Object targetDomainObject,Object permission);boolean hasPermission(Authentication authentication, Serializable targetId,String targetType, Object permission);

它直接映射到表达式的可用版本,但不提供第一个参数(Authentication对象)。第一种用于已经加载了对其进行访问的域对象的情况。然后,如果当前用户具有该对象的给定权限,则表达式将返回true。第二个版本用于未加载对象但其标识符已知的情况。还需要域对象的抽象“类型”说明符,允许加载正确的ACL权限。传统上这是对象的Java类,但不一定只要与权限的加载方式一致。

要使用hasPermission()表达式,您必须在应用程序上下文中显式配置PermissionEvaluator。这看起来像这样:





其中myPermissionEvaluator是实现PermissionEvaluator的bean。通常这将是ACL模块的实现,称为AclPermissionEvaluator。有关详细信息,请参阅“联系人”示例应用程序配置。

方法安全元注释

您可以将元注释用于方法安全性,以使您的代码更具可读性。如果您发现在整个代码库中重复使用相同的复杂表达式,这将非常方便。例如,请考虑以下事项:

@PreAuthorize("#contact.name == authentication.name")

我们可以创建一个可以替代使用的元注释,而不是在任何地方重复这一点。

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}

元注释可用于任何Spring Security方法安全注释。为了保持符合规范,JSR-250注释不支持元注释。

12.其他主题

在本部分中,我们将介绍需要了解前面章节的功能以及框架中一些更高级和不太常用的功能。

12.1域对象安全性(ACL)

12.1.1概述

复杂的应用程序通常会发现需要定义访问权限,而不仅仅是在web请求或方法调用级别。相反,安全决策需要包括谁(Authentication),哪里(MethodInvocation)和什么(SomeDomainObject)。换句话说,授权决策还需要考虑方法调用的实际域对象实例主题。

想象一下,您正在为宠物诊所设计应用程序。基于Spring的应用程序将有两个主要用户组:宠物诊所的工作人员以及宠物诊所的客户。员工可以访问所有数据,而您的客户只能看到自己的客户记录。为了使它更有趣,您的客户可以允许其他用户查看他们的客户记录,例如他们的“幼儿学前”导师或他们当地的“小马俱乐部”的总裁。使用Spring Security作为基础,您可以使用以下几种方法:

  • 编写业务方法以强制执行安全性。您可以查询Customer域对象实例中的集合,以确定哪些用户可以访问。通过使用SecurityContextHolder.getContext().getAuthentication(),您将能够访问Authentication对象。
  • 编写AccessDecisionVoter以强制执行Authentication对象中存储的GrantedAuthority[]安全性。这意味着您的AuthenticationManager需要填充Authentication,其中自定义GrantedAuthority[]代表委托人可以访问的每个Customer域对象实例。
  • 编写AccessDecisionVoter以强制执行安全性并直接打开目标Customer域对象。这意味着您的选民需要访问允许其检索Customer对象的DAO。然后,它将访问Customer对象的已批准用户集合并做出适当的决定。

这些方法中的每一种都是完全合法的。但是,第一个将您的授权检查与您的业务代码相结合。这方面的主要问题包括增加了单元测试的难度,以及在其他地方重用Customer授权逻辑会更加困难。从Authentication对象获取GrantedAuthority[] s也没问题,但不会扩展到大量的Customer s。如果一个用户可能能够访问5,000 Customer s(在这种情况下不太可能,但想象一下它是一个大型小马俱乐部的流行兽医!)消耗的内存量和构建Authentication所需的时间对象是不受欢迎的。直接从外部代码打开Customer的最终方法可能是三者中最好的。它实现了关注点的分离,并且不会滥用内存或CPU周期,但它仍然是低效的,因为AccessDecisionVoter和最终的业务方法本身都会执行对负责检索Customer的DAO的调用宾语。每个方法调用两次访问显然是不可取的。此外,列出的每种方法都需要从头开始编写自己的访问控制列表(ACL)持久性和业务逻辑。

幸运的是,还有另一种选择,我们将在下面讨论。

12.1.2关键概念

Spring Security的ACL服务在spring-security-acl-xxx.jar中发布。您需要将此JAR添加到类路径中以使用Spring Security的域对象实例安全功能。

Spring Security的域对象实例安全功能以访问控制列表(ACL)的概念为中心。系统中的每个域对象实例都有自己的ACL,ACL会记录谁可以和不能使用该域对象的详细信息。考虑到这一点,Spring Security为您的应用程序提供了三个与ACL相关的主要功能:

  • 一种有效检索所有域对象的ACL条目(以及修改这些ACL)的方法
  • 在调用方法之前,允许确保给定主体使用对象的方法
  • 在调用方法之后,允许确保给定主体的方法与您的对象(或它们返回的东西)一起工作

如第一个要点所示,Spring Security ACL模块的主要功能之一是提供检索ACL的高性能方法。此ACL存储库功能非常重要,因为系统中的每个域对象实例可能具有多个访问控制条目,并且每个ACL可能从树状结构中的其他ACL继承(这通过{支持开箱即用) 1 /},并且非常常用)。Spring Security的ACL功能经过精心设计,可提供ACL的高性能检索,以及可插拔缓存,最小化数据库更新死锁,独立于ORM框架(我们直接使用JDBC),正确封装和透明数据库更新。

鉴于数据库是ACL模块操作的核心,让我们探讨在实现中默认使用的四个主表。下表按典型Spring Security ACL部署中的大小顺序列出,最后列出的行数最多:

  • ACL_SID允许我们唯一地标识系统中的任何主体或权限(“SID”代表“安全身份”)。唯一的列是ID,SID的文本表示,以及指示文本表示是指主体名称还是GrantedAuthority的标志。因此,每个唯一主体或GrantedAuthority都有一行。当在接收许可的上下文中使用时,SID通常被称为“接收者”。
  • ACL_CLASS允许我们唯一地标识系统中的任何域对象类。唯一的列是ID和Java类名。因此,对于我们希望存储ACL权限的每个唯一类,只有一行。
  • ACL_OBJECT_IDENTITY存储系统中每个唯一域对象实例的信息。列包括ID,ACL_CLASS表的外键,唯一标识符,因此我们知道我们为哪个ACL_CLASS实例提供信息,父,ACL_SID表的外键表示域对象实例的所有者,以及是否允许ACL条目从任何父ACL继承。对于我们存储ACL权限的每个域对象实例,我们只有一行。
  • 最后,ACL_ENTRY存储分配给每个收件人的各个权限。列包括ACL_OBJECT_IDENTITY的外键,收件人(即ACL_SID的外键),是否要审核,以及表示授予或拒绝实际权限的整数位掩码。对于每个收到使用域对象的权限的收件人,我们都有一行。

如上一段所述,ACL系统使用整数位屏蔽。不用担心,你不需要知道使用ACL系统的位移的更好点,但足以说我们有32位可以打开或关闭。这些位中的每一个都代表一个权限,默认情况下,读取权限(位0),写入(位1),创建(位2),删除(位3)和管理(位4)。如果您希望使用其他权限,则很容易实现您自己的Permission实例,并且ACL框架的其余部分将在不知道您的扩展的情况下运行。

重要的是要理解系统中域对象的数量与我们选择使用整数位屏蔽的事实完全没有关系。虽然您有32位可用于权限,但您可能拥有数十亿个域对象实例(这意味着ACL_OBJECT_IDENTITY中的数十亿行,很可能是ACL_ENTRY)。我们提出这一点是因为我们发现有时人们错误地认为他们需要为每个潜在的域对象提供一点点,但事实并非如此。

现在我们已经提供了ACL系统的基本概述,以及它在表结构中的样子,让我们来探索关键接口。关键接口是:

  • Acl:每个域对象都有一个且只有一个Acl对象,它在内部保存AccessControlEntry并且知道Acl的所有者。Acl不直接引用域对象,而是引用ObjectIdentityAcl存储在ACL_OBJECT_IDENTITY表中。
  • AccessControlEntryAcl拥有多个AccessControlEntry,在框架中通常缩写为ACE。每个ACE指的是PermissionSidAcl的特定元组。ACE还可以授予或不授予并包含审核设置。ACE存储在ACL_ENTRY表中。
  • Permission:权限表示特定的不可变位掩码,并为位屏蔽和输出信息提供便利功能。上面给出的基本权限(位0到4)包含在BasePermission类中。
  • Sid:ACL模块需要引用主体和GrantedAuthority[]Sid接口提供了间接级别,它是“安全标识”的缩写。常用类包括PrincipalSid(代表Authentication对象内的主体)和GrantedAuthoritySid。安全标识信息存储在ACL_SID表中​​。
  • ObjectIdentity:每个域对象在ACL模块内部由ObjectIdentity表示。默认实现称为ObjectIdentityImpl
  • AclService:检索适用于给定ObjectIdentityAcl。在包含的实现(JdbcAclService)中,检索操作被委托给LookupStrategyLookupStrategy提供了一种高度优化的策略,用于检索ACL信息,使用批量检索(BasicLookupStrategy)并支持利用物化视图,分层查询和类似的以性能为中心的非ANSI SQL功能的自定义实现。
  • MutableAclService:允许修改后的Acl表示持久性。如果您不希望,则不必使用此界面。

请注意,我们开箱即用的AclService和相关数据库类都使用ANSI SQL。因此,这应该适用于所有主要数据库。在撰写本文时,系统已使用Hypersonic SQL,PostgreSQL,Microsoft SQL Server和Oracle成功进行了测试。

两个样本附带Spring Security,用于演示ACL模块。第一个是Contacts Sample,另一个是Document Management System(DMS)Sample。我们建议您查看这些示例。

12.1.3入门

要开始使用Spring Security的ACL功能,您需要将ACL信息存储在某处。这需要使用Spring实例化DataSource。然后将DataSource注入JdbcMutableAclServiceBasicLookupStrategy实例。后者提供高性能的ACL检索功能,前者提供了mutator功能。有关示例配置,请参阅Spring Security附带的其中一个示例。您还需要使用上一节中列出的四个特定于ACL的表填充数据库(请参阅相应SQL语句的ACL示例)。

一旦您创建了所需的模式并实例化JdbcMutableAclService,您接下来需要确保您的域模型支持与Spring Security ACL包的互操作性。希望ObjectIdentityImpl证明是足够的,因为它提供了许多可以使用它的方法。大多数人都会拥有包含public Serializable getId()方法的域对象。如果返回类型很长或与long兼容(例如int),您将发现无需进一步考虑ObjectIdentity问题。ACL模块的许多部分都依赖于长标识符。如果你没有使用long(或int,byte等),你很有可能需要重新实现一些类。我们不打算在Spring Security的ACL模块中支持非长标识符,因为long已经与所有数据库序列(最常见的标识符数据类型)兼容,并且足够长以适应所有常见的使用场景。

以下代码片段显示了如何创建Acl或修改现有的Acl

// 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);

在上面的示例中,我们检索与标识号为44的“Foo”域对象关联的ACL。然后我们添加一个ACE,以便名为“Samantha”的主体可以“管理”该对象。除了insertAce方法之外,代码片段相对不言自明。insertAce方法的第一个参数是确定将在Acl中的哪个位置插入新条目。在上面的示例中,我们只是将新ACE放在现有ACE的末尾。最后一个参数是一个布尔值,表示ACE是授予还是拒绝。大部分时间它将授予(true),但如果它拒绝(false),则权限被有效阻止。

Spring Security没有提供任何特殊集成来自动创建,更新或删除ACL作为DAO或存储库操作的一部分。相反,您需要为您的各个域对象编写如上所示的代码。值得考虑在服务层使用AOP来自动将ACL信息与服务层操作集成。我们在过去发现了这种非常有效的方法。

一旦您使用上述技术在数据库中存储一些ACL信息,下一步就是实际使用ACL信息作为授权决策逻辑的一部分。你在这里有很多选择。您可以编写自己的AccessDecisionVoterAfterInvocationProvider,分别在方法调用之前或之后触发。这些类将使用AclService来检索相关的ACL,然后调用Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode)来决定是授予还是拒绝许可。或者,您可以使用我们的AclEntryVoterAclEntryAfterInvocationProviderAclEntryAfterInvocationCollectionFilteringProvider类。所有这些类都提供了一种基于声明的方法,用于在运行时评估ACL信息,从而使您无需编写任何代码。请参阅示例应用程序以了解如何使用这些类。

12.2预认证方案

在某些情况下,您希望使用Spring Security进行授权,但在访问应用程序之前,某些外部系统已经对用户进行了可靠的身份验证。我们将这些情况称为“预先验证”的情景。示例包括X.509,Siteminder以及运行应用程序的Java EE容器的身份验证。使用预身份验证时,Spring Security必须使用

  • 确定发出请求的用户。
  • 获取用户的权限。

细节将取决于外部认证机制。在X.509的情况下,可以通过其证书信息来标识用户,或者在Siteminder的情况下通过HTTP请求标头来标识用户。如果依赖于容器身份验证,则将通过在传入HTTP请求上调用getUserPrincipal()方法来标识用户。在某些情况下,外部机制可以为用户提供角色/权限信息,但在其他情况下,必须从单独的源(例如UserDetailsService)获取权限。

12.2.1预认证框架类

由于大多数预身份验证机制遵循相同的模式,Spring Security具有一组类,这些类为实现预先验证的身份验证提供程序提供了内部框架。这消除了重复,并允许以结构化方式添加新实现,而无需从头开始编写所有内容。如果您想使用X.509身份验证之类的东西,则无需了解这些类,因为它已经具有更易于使用和开始使用的命名空间配置选项。如果您需要使用显式bean配置或计划编写自己的实现,那么了解所提供的实现如何工作将是有用的。您将在org.springframework.security.web.authentication.preauth下找到课程。我们只是在这里提供一个大纲,所以你应该在适当的时候咨询Javadoc和来源。

AbstractPreAuthenticatedProcessingFilter

此类将检查安全上下文的当前内容,如果为空,它将尝试从HTTP请求中提取用户信息并将其提交到AuthenticationManager。子类重写以下方法以获取此信息:

protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);

调用这些后,过滤器将创建一个包含返回数据的PreAuthenticatedAuthenticationToken并提交以进行身份​​验证。通过这里的“身份验证”,我们实际上只是意味着可能会加载用户权限的进一步处理,但遵循标准的Spring Security身份验证体系结构。

与其他Spring Security身份验证过滤器一样,预身份验证过滤器具有authenticationDetailsSource属性,默认情况下会创建一个WebAuthenticationDetails对象来存储其他信息,例如{4中的会话标识符和原始IP地址/ Authentication对象的属性。在可以从预认证机制获得用户角色信息的情况下,数据也存储在该属性中,其中详细信息实现了GrantedAuthoritiesContainer接口。这使身份验证提供程序能够读取外部分配给用户的权限。接下来我们将看一个具体的例子。

J2eeBasedPreAuthenticatedWebAuthenticationDetailsS​​ource

如果过滤器配置了authenticationDetailsSource(此类的实例),则通过为每个预定义的“可映射角色”调用isUserInRole(String role)方法来获取权限信息。该类从配置的MappableAttributesRetriever中获取这些内容。可能的实现包括在应用程序上下文中对列表进行硬编码,以及从web.xml文件中的信息中读取角色信息。预认证示例应用程序使用后一种方法。

还有一个额外阶段,使用配置的Attributes2GrantedAuthoritiesMapper将角色(或属性)映射到Spring Security GrantedAuthority对象。默认情况下,只会在名称中添加通常的ROLE_前缀,但它可以让您完全控制行为。

PreAuthenticatedAuthenticationProvider

预先认证的提供程序除了为用户加载UserDetails对象之外,还有更多工作要做。它通过委托给AuthenticationUserDetailsService来做到这一点。后者类似于标准UserDetailsService,但是采用Authentication对象而不仅仅是用户名:

public interface AuthenticationUserDetailsService {UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}

此接口可能还有其他用途,但通过预身份验证,它允许访问打包在Authentication对象中的权限,正如我们在上一节中看到的那样。PreAuthenticatedGrantedAuthoritiesUserDetailsService类就是这样做的。或者,它可以通过UserDetailsByNameServiceWrapper实施委托给标准UserDetailsService

Http403ForbiddenEntryPoint

AuthenticationEntryPoint在技术概述章节中进行了讨论。通常,它负责启动未经身份验证的用户的身份验证过程(当他们尝试访问受保护的资源时),但在预先验证的情况下,这不适用。如果您没有将预身份验证与其他身份验证机制结合使用,则只能使用此类的实例配置ExceptionTranslationFilter。如果用户被AbstractPreAuthenticatedProcessingFilter拒绝,则会调用它,从而导致空认证。如果被调用,它总是返回403 - 禁止的响应代码。

12.2.2具体实施

X.509身份验证在其自己的章节中介绍。在这里,我们将介绍一些为其他预先验证的方案提供支持的类。

请求标头身份验证(Siteminder)

外部认证系统可以通过在HTTP请求上设置特定报头来向应用程序提供信息。一个众所周知的例子是Siteminder,它在名为SM_USER的标题中传递用户名。类RequestHeaderAuthenticationFilter支持此机制,它只是从标头中提取用户名。它默认使用名称SM_USER作为标题名称。有关更多详细信息,请参阅Javadoc。

[小费]小费

请注意,在使用这样的系统时,框架根本不执行任何身份验证检查,外部系统配置正确并保护对应用程序的所有访问权限非常重要。如果攻击者能够在未检测到原始请求的情况下伪造标头,那么他们可能会选择他们希望的任何用户名。

Siteminder示例配置

使用此过滤器的典型配置如下所示:













我们假设安全命名空间用于配置。还假设您已在配置中添加了UserDetailsService(称为“userDetailsS​​ervice”)以加载用户的角色。

Java EE容器身份验证

J2eePreAuthenticatedProcessingFilter将从HttpServletRequestuserPrincipal属性中提取用户名。使用此过滤器通常与Java EE角色的使用相结合,如上文“J2eeBasedPreAuthenticatedWebAuthenticationDetailsS​​ource”一节中所述。

代码库中有一个使用这种方法的示例应用程序,因此如果您感兴趣,请从github获取代码并查看应用程序上下文文件。代码位于samples/xml/preauth目录中。

12.3 LDAP认证

12.3.1概述

组织经常将LDAP用作用户信息和身份验证服务的中央存储库。它还可用于存储应用程序用户的角色信息。

关于如何配置LDAP服务器有许多不同的方案,因此Spring Security的LDAP提供程序是完全可配置的。它使用单独的策略接口进行身份验证和角色检索,并提供可配置为处理各种情况的默认实现。

在尝试将其与Spring Security一起使用之前,您应该熟悉LDAP。以下链接提供了对所涉及概念的良好介绍,以及使用免费LDAP服务器OpenLDAP设置目录的指南:http://www.zytrax.com/books/ldap/。熟悉用于从Java访问LDAP的JNDI API也可能很有用。我们不在LDAP提供程序中使用任何第三方LDAP库(Mozilla,JLDAP等),但是大量使用Spring LDAP,因此如果您计划添加您的项目,那么熟悉该项目可能会很有用自己的自定义。

使用LDAP身份验证时,务必确保正确配置LDAP连接池。如果您不熟悉如何执行此操作,可以参考Java LDAP文档。

12.3.2使用LDAP和Spring Security

Spring Security中的LDAP身份验证大致可分为以下几个阶段。

  • 从登录名中获取唯一的LDAP“专有名称”或DN。这通常意味着在目录中执行搜索,除非事先知道用户名到DN的确切映射。因此,用户在登录时可能会输入名称“joe”,但用于向LDAP进行身份验证的实际名称将是完整的DN,例如uid=joe,ou=users,dc=spring,dc=io
  • 通过“绑定”该用户或通过对DN的目录条目中的密码属性执行用户密码的远程“比较”操作来验证用户。
  • 加载用户的权限列表。

例外情况是,LDAP目录仅用于检索用户信息并在本地对其进行身份验证。这可能是不可能的,因为目录通常设置为对用户密码等属性的读访问权限有限。

我们将在下面介绍一些配置方案。有关可用配置选项的完整信息,请参阅安全命名空间架构(XML编辑器中应提供的信息)。

12.3.3配置LDAP服务器

您需要做的第一件事是配置应该进行身份验证的服务器。这是使用安全命名空间中的元素完成的。可以使用url属性将其配置为指向外部LDAP服务器:


使用嵌入式测试服务器

元素也可用于创建嵌入式服务器,这对测试和演示非常有用。在这种情况下,您使用它而不使用url属性:


这里我们已经指定目录的根DIT应该是“dc = springframework,dc = org”,这是默认值。使用这种方式,命名空间解析器将创建一个嵌入式Apache Directory服务器,并扫描类路径中的任何LDIF文件,它将尝试加载到服务器中。您可以使用ldif属性自定义此行为,该属性定义要加载的LDIF资源:


这使得启动和运行LDAP变得更加容易,因为使用外部服务器一直工作会很不方便。它还使用户免受连接Apache Directory服务器所需的复杂bean配置的影响。使用普通Spring Bean,配置会更加混乱。您必须具有可供应用程序使用的必需Apache Directory依赖项jar。这些可以从LDAP示例应用程序获得。

使用绑定身份验证

这是最常见的LDAP身份验证方案。


这个简单的示例将通过替换提供的模式中的用户登录名并尝试使用登录密码绑定该用户来获取用户的DN。如果所有用户都存储在目录中的单个节点下,则可以。如果您希望配置LDAP搜索过滤器以找到用户,则可以使用以下内容:


如果与上面的服务器定义一起使用,这将使用user-search-filter属性的值作为过滤器在DN ou=people,dc=springframework,dc=org下执行搜索。同样,用户登录名将替换过滤器名称中的参数,因此它将搜索uid属性等于用户名的条目。如果未提供user-search-base,则将从根执行搜索。

加载机构

如何从LDAP目录中的组加载权限由以下属性控制。

  • group-search-base.定义应在其下执行组搜索的目录树的一部分。
  • group-role-attribute.包含组条目定义的权限名称的属性。默认为cn
  • group-search-filter.用于搜索组成员身份的过滤器。默认值为uniqueMember={0},对应于groupOfUniqueNames LDAP类[19]。在这种情况下,替换参数是用户的完整可分辨名称。如果要筛选登录名,可以使用参数{1}

所以如果我们使用以下配置


并且成功验证为用户“ben”,随后的权限加载将在目录条目ou=groups,dc=springframework,dc=org下执行搜索,查找包含值为uid=ben,ou=people,dc=springframework,dc=org的属性uniqueMember的条目。默认情况下,权限名称前缀为ROLE_。您可以使用role-prefix属性更改此设置。如果您不想要任何前缀,请使用role-prefix="none"。有关加载权限的更多信息,请参阅DefaultLdapAuthoritiesPopulator类的Javadoc。

12.3.4实现类

我们上面使用的命名空间配置选项易于使用,并且比明确使用Spring bean更简洁。在某些情况下,您可能需要知道如何在应用程序上下文中直接配置Spring Security LDAP。例如,您可能希望自定义某些类的行为。如果您对使用命名空间配置感到满意,那么您可以跳过本节和下一节。

主要的LDAP提供程序类LdapAuthenticationProvider实际上并没有做太多的事情,而是将工作委托给另外两个bean,LdapAuthenticatorLdapAuthoritiesPopulator,它们负责验证用户身份并检索用户的集合分别为GrantedAuthority s。

LdapAuthenticator实现

验证者还负责检索任何所需的用户属性。这是因为属性的权限可能取决于所使用的身份验证的类型。例如,如果以用户身份进行绑定,则可能需要使用用户自己的权限来读取它们。

目前有Spring Security提供的两种身份验证策略:

  • 直接向LDAP服务器进行身份验证(“绑定”身份验证)。
  • 密码比较,其中将用户提供的密码与存储库中存储的密码进行比较。这可以通过检索密码属性的值并在本地检查它或通过执行LDAP“比较”操作来完成,其中提供的密码被传递到服务器以进行比较,并且永远不会检索真实密码值。
共同功能

在可以对用户进行身份验证之前(通过任一策略),必须从提供给应用程序的登录名获取可分辨名称(DN)。这可以通过简单的模式匹配(通过设置setUserDnPatterns数组属性)或通过设置userSearch属性来完成。对于DN模式匹配方法,使用标准Java模式格式,并且登录名将替换参数{0}。该模式应该相对于配置的SpringSecurityContextSource将绑定到的DN(有关此信息的更多信息,请参阅有关连接到LDAP服务器的部分)。例如,如果您使用的URL服务器的URL为ldap://monkeymachine.co.uk/dc=springframework,dc=org,模式为uid={0},ou=greatapes,那么登录名“gorilla”将映射到DN uid=gorilla,ou=greatapes,dc=springframework,dc=org。将依次尝试每个配置的DN模式,直到找到匹配项。有关使用搜索的信息,请参阅下面的搜索对象部分。也可以使用这两种方法的组合 - 首先检查模式,如果没有找到匹配的DN,将使用搜索。

认证者

org.springframework.security.ldap.authentication中的类BindAuthenticator实现了绑定认证策略。它只是尝试以用户身份绑定。

PasswordComparisonAuthenticator

PasswordComparisonAuthenticator实现密码比较身份验证策略。

连接到LDAP服务器

上面讨论的bean必须能够连接到服务器。它们都必须提供SpringSecurityContextSource,这是Spring LDAP的ContextSource的扩展。除非您有特殊要求,否则通常会配置一个DefaultSpringSecurityContextSource bean,可以使用LDAP服务器的URL配置,也可以配置“manager”用户的用户名和密码,默认情况下绑定到服务器(而不是匿名绑定)。有关更多信息,请阅读此类的Javadoc和Spring LDAP的AbstractContextSource

LDAP搜索对象

通常需要比简单DN匹配更复杂的策略来在目录中定位用户条目。这可以封装在LdapUserSearch实例中,该实例可以提供给验证器实现,例如,允许它们定位用户。提供的实现是FilterBasedLdapUserSearch

FilterBasedLdapUserSearch中

此bean使用LDAP过滤器来匹配目录中的用户对象。在Javadoc中解释了JDK DirContext类上相应搜索方法的过程。如那里所解释的,可以向搜索过滤器提供参数。对于这个类,唯一有效的参数是{0},它将被用户的登录名替换。

LdapAuthoritiesPopulator在

在成功验证用户之后,LdapAuthenticationProvider将尝试通过调用配置的LdapAuthoritiesPopulator bean为用户加载一组权限。DefaultLdapAuthoritiesPopulator是一个实现,它将通过在目录中搜索用户所属的组来加载权限(通常这些将是目录中的groupOfNamesgroupOfUniqueNames条目)。有关其工作原理的更多详细信息,请参阅此类的Javadoc。

如果您只想使用LDAP进行身份验证,但是从差异源(例如数据库)加载权限,那么您可以提供自己的此接口实现并注入该接口。

Spring Bean配置

使用我们在这里讨论的一些bean的典型配置可能如下所示:







uid={0},ou=people







这将设置提供程序以访问URL为ldap://monkeymachine:389/dc=springframework,dc=org的LDAP服务器。将通过尝试绑定DN uid=,ou=people,dc=springframework,dc=org来执行身份验证。身份验证成功后,将通过使用默认过滤器(member=)在DN ou=groups,dc=springframework,dc=org下搜索,将角色分配给用户。角色名称将取自每场比赛的“ou”属性。

要配置用户搜索对象,使用过滤器(uid=)代替DN模式(或除此之外),您将配置以下bean






并通过设置BindAuthenticator bean的userSearch属性来使用它。然后,在尝试以此用户身份绑定之前,身份验证者将调用搜索对象以获取正确的用户DN。

LDAP属性和自定义用户详细信息

使用LdapAuthenticationProvider进行身份验证的最终结果与使用标准UserDetailsService接口的正常Spring Security身份验证相同。创建UserDetails对象并将其存储在返回的Authentication对象中。与使用UserDetailsService一样,常见的要求是能够自定义此实现并添加额外的属性。使用LDAP时,这些通常是来自用户条目的属性。UserDetails对象的创建由提供者的UserDetailsContextMapper策略控制,该策略负责将用户对象映射到LDAP上下文数据和从LDAP上下文数据映射:

public interface UserDetailsContextMapper {UserDetails mapUserFromContext(DirContextOperations ctx, String username,Collection authorities);void mapUserToContext(UserDetails user, DirContextAdapter ctx);
}

只有第一种方法与身份验证相关。如果您提供此接口的实现并将其注入LdapAuthenticationProvider,则可以精确控制UserDetails对象的创建方式。第一个参数是Spring LDAP的DirContextOperations实例,它允许您访问在身份验证期间加载的LDAP属性。username参数是用于进行身份验证的名称,最后一个参数是由配置的LdapAuthoritiesPopulator为用户加载的权限集合。

加载上下文数据的方式略有不同,具体取决于您使用的身份验证类型。使用BindAuthenticator,绑定操作返回的上下文将用于读取属性,否则将使用从配置的ContextSource获取的标准上下文读取数据(当搜索配置为定位用户时) ,这将是搜索对象返回的数据)。

12.3.5 Active Directory身份验证

Active Directory支持自己的非标准身份验证选项,并且正常使用模式与标准LdapAuthenticationProvider不太合适。通常,使用域用户名(格式为user@domain)执行身份验证,而不是使用LDAP专有名称。为了简化这一过程,Spring Security 3.1有一个身份验证提供程序,可以为典型的Active Directory设置进行自定义。

ActiveDirectoryLdapAuthenticationProvider

配置ActiveDirectoryLdapAuthenticationProvider非常简单。您只需提供域名和提供服务器地址的LDAP URL [20]。然后,示例配置如下所示:



}

请注意,为了定义服务器位置,不需要指定单独的ContextSource - bean完全是自包含的。例如,名为“Sharon”的用户可以通过输入用户名sharon或完整的Active Directory userPrincipalName(即sharon@mydomain.com)进行身份验证。然后将定位用户的目录条目,并返回属性以用于自定义创建的UserDetails对象(可以为此目的注入UserDetailsContextMapper,如上所述)。与目录的所有交互都以用户自己的身份进行。没有“经理”用户的概念。

默认情况下,用户权限是从用户条目的memberOf属性值获取的。分配给用户的权限可以再次使用UserDetailsContextMapper进行自定义。您还可以将GrantedAuthoritiesMapper注入提供程序实例以控制最终位于Authentication对象中的权限。

Active Directory错误代码

默认情况下,失败的结果将导致标准Spring Security BadCredentialsException。如果将属性convertSubErrorCodesToExceptions设置为true,则将解析异常消息以尝试提取特定于Active Directory的错误代码并引发更具体的异常。检查类Javadoc以获取更多信息。

12.4 OAuth 2.0登录 - 高级配置

HttpSecurity.oauth2Login()提供了许多用于自定义OAuth 2.0登录的配置选项。主要配置选项分组到其协议端点对应项中。

例如,oauth2Login().authorizationEndpoint()允许配置授权端点,而oauth2Login().tokenEndpoint()允许配置令牌端点

以下代码显示了一个示例:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().authorizationEndpoint()....redirectionEndpoint()....tokenEndpoint()....userInfoEndpoint()...}
}

oauth2Login() DSL的主要目标是与规范中定义的命名密切配合。

OAuth 2.0授权框架定义协议端点,如下所示:

授权过程使用两个授权服务器端点(HTTP资源):

  • 授权端点:客户端用于通过用户代理重定向从资源所有者获取授权。
  • 令牌端点:客户端用于交换访问令牌的授权授权,通常使用客户端身份验证。

以及一个客户端端点:

  • 重定向端点:由授权服务器用于通过资源所有者用户代理将包含授权凭据的响应返回给客户端。

OpenID Connect Core 1.0规范定义了UserInfo端点,如下所示:

UserInfo端点是OAuth 2.0受保护资源,它返回有关经过身份验证的最终用户的声明。为了获得有关最终用户的请求声明,客户端使用通过OpenID Connect身份验证获得的访问令牌向UserInfo端点发出请求。这些声明通常由JSON对象表示,该对象包含声明的名称 - 值对的集合。

以下代码显示了oauth2Login() DSL可用的完整配置选项:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().clientRegistrationRepository(this.clientRegistrationRepository()).authorizedClientRepository(this.authorizedClientRepository()).authorizedClientService(this.authorizedClientService()).loginPage("/login").authorizationEndpoint().baseUri(this.authorizationRequestBaseUri()).authorizationRequestRepository(this.authorizationRequestRepository()).authorizationRequestResolver(this.authorizationRequestResolver()).and().redirectionEndpoint().baseUri(this.authorizationResponseBaseUri()).and().tokenEndpoint().accessTokenResponseClient(this.accessTokenResponseClient()).and().userInfoEndpoint().userAuthoritiesMapper(this.userAuthoritiesMapper()).userService(this.oauth2UserService()).oidcUserService(this.oidcUserService()).customUserType(GitHubOAuth2User.class, "github");}
}

以下部分详细介绍了每种可用的配置选项:

  • 第12.4.1节“OAuth 2.0登录页面”
  • 第12.4.2节“重定向端点”
  • 第12.4.3节“UserInfo端点”

12.4.1 OAuth 2.0登录页面

默认情况下,OAuth 2.0登录页面由DefaultLoginPageGeneratingFilter自动生成。默认登录页面显示每个已配置的OAuth客户端及其ClientRegistration.clientName作为链接,该链接能够启动授权请求(或OAuth 2.0登录)。

每个OAuth客户端的链接目标默认为以下内容:

OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI +“/ {registrationId}”

以下行显示了一个示例:

Google

要覆盖默认登录页面,请配置oauth2Login().loginPage()和(可选)oauth2Login().authorizationEndpoint().baseUri()

以下清单显示了一个示例:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().loginPage("/login/oauth2")....authorizationEndpoint().baseUri("/login/oauth2/authorization")....}
}
[重要]重要

您需要提供一个@Controller,其@RequestMapping("/login/oauth2")能够呈现自定义登录页面。

[小费]小费

如前所述,配置oauth2Login().authorizationEndpoint().baseUri()是可选的。但是,如果您选择自定义它,请确保指向每个OAuth客户端的链接与authorizationEndpoint().baseUri()匹配。

以下行显示了一个示例:

Google

12.4.2重定向端点

授权服务器使用重定向端点通过资源所有者用户代理将授权响应(包含授权凭据)返回给客户端。

[小费]小费

OAuth 2.0 Login利用授权代码授权。因此,授权凭证是授权代码。

默认授权响应baseUri(重定向端点)为/login/oauth2/code/*,在OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI中定义。

如果要自定义授权响应baseUri,请按以下示例所示进行配置:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().redirectionEndpoint().baseUri("/login/oauth2/callback/*")....}
}
[重要]重要

您还需要确保ClientRegistration.redirectUriTemplate与自定义授权响应baseUri匹配。

以下清单显示了一个示例:

return CommonOAuth2Provider.GOOGLE.getBuilder("google").clientId("google-client-id").clientSecret("google-client-secret").redirectUriTemplate("{baseUrl}/login/oauth2/callback/{registrationId}").build();

12.4.3 UserInfo端点

UserInfo端点包括许多配置选项,如以下子部分所述:

  • 映射用户权限
  • 配置自定义OAuth2User
  • OAuth 2.0 UserService
  • OpenID Connect 1.0 UserService

映射用户权限

用户成功通过OAuth 2.0提供程序进行身份验证后,OAuth2User.getAuthorities()(或OidcUser.getAuthorities())可能会映射到一组新的GrantedAuthority实例,这些实例将在完成身份验证时提供给OAuth2AuthenticationToken

[小费]小费

OAuth2AuthenticationToken.getAuthorities()用于授权请求,例如hasRole('USER')hasRole('ADMIN')

映射用户权限时,有几个选项可供选择:

  • 使用GrantedAuthoritiesMapper
  • 使用OAuth2UserService的基于委派的策略
使用GrantedAuthoritiesMapper

提供GrantedAuthoritiesMapper的实现并对其进行配置,如以下示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(this.userAuthoritiesMapper())...}private GrantedAuthoritiesMapper userAuthoritiesMapper() {return (authorities) -> {Set mappedAuthorities = new HashSet<>();authorities.forEach(authority -> {if (OidcUserAuthority.class.isInstance(authority)) {OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;OidcIdToken idToken = oidcUserAuthority.getIdToken();OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();// Map the claims found in idToken and/or userInfo// to one or more GrantedAuthority's and add it to mappedAuthorities} else if (OAuth2UserAuthority.class.isInstance(authority)) {OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;Map userAttributes = oauth2UserAuthority.getAttributes();// Map the attributes found in userAttributes// to one or more GrantedAuthority's and add it to mappedAuthorities}});return mappedAuthorities;};}
}

或者,您可以注册GrantedAuthoritiesMapper @Bean以使其自动应用于配置,如以下示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login();}@Beanpublic GrantedAuthoritiesMapper userAuthoritiesMapper() {...}
}
使用OAuth2UserService的基于委派的策略

与使用GrantedAuthoritiesMapper相比,此策略更先进,但它也更灵活,因为它可让您访问OAuth2UserRequestOAuth2User(使用OAuth 2.0 UserService时)或OidcUserRequestOidcUser(使用OpenID Connect 1.0 UserService时)。

OAuth2UserRequest(和OidcUserRequest)为您提供对相关OAuth2AccessToken的访问权限,这在委托人需要从受保护资源获取权限信息才能映射自定义权限的情况下非常有用。用户。

以下示例显示如何使用OpenID Connect 1.0 UserService实现和配置基于委派的策略:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().userInfoEndpoint().oidcUserService(this.oidcUserService())...}private OAuth2UserService oidcUserService() {final OidcUserService delegate = new OidcUserService();return (userRequest) -> {// Delegate to the default implementation for loading a userOidcUser oidcUser = delegate.loadUser(userRequest);OAuth2AccessToken accessToken = userRequest.getAccessToken();Set mappedAuthorities = new HashSet<>();// TODO// 1) Fetch the authority information from the protected resource using accessToken// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities// 3) Create a copy of oidcUser but use the mappedAuthorities insteadoidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());return oidcUser;};}
}

配置自定义OAuth2User

CustomUserTypesOAuth2UserServiceOAuth2UserService的实现,为自定义OAuth2User类型提供支持。

如果默认实现(DefaultOAuth2User)不符合您的需求,您可以定义自己的OAuth2User实现。

以下代码演示了如何为GitHub注册自定义OAuth2User类型:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().userInfoEndpoint().customUserType(GitHubOAuth2User.class, "github")...}
}

以下代码显示了GitHub的自定义OAuth2User类型的示例:

public class GitHubOAuth2User implements OAuth2User {private List authorities =AuthorityUtils.createAuthorityList("ROLE_USER");private Map attributes;private String id;private String name;private String login;private String email;@Overridepublic Collection getAuthorities() {return this.authorities;}@Overridepublic Map getAttributes() {if (this.attributes == null) {this.attributes = new HashMap<>();this.attributes.put("id", this.getId());this.attributes.put("name", this.getName());this.attributes.put("login", this.getLogin());this.attributes.put("email", this.getEmail());}return attributes;}public String getId() {return this.id;}public void setId(String id) {this.id = id;}@Overridepublic String getName() {return this.name;}public void setName(String name) {this.name = name;}public String getLogin() {return this.login;}public void setLogin(String login) {this.login = login;}public String getEmail() {return this.email;}public void setEmail(String email) {this.email = email;}
}
[小费]小费

idnameloginemail是GitHub的UserInfo响应中返回的属性。有关从UserInfo端点返回的详细信息,请参阅“获取经过身份验证的用户”的API文档。

OAuth 2.0 UserService

DefaultOAuth2UserService是支持标准OAuth 2.0提供程序的OAuth2UserService的实现。

[注意]注意

OAuth2UserService从UserInfo端点获取最终用户(资源所有者)的用户属性(通过使用在授权流程中授予客户端的访问令牌)并以{的形式返回AuthenticatedPrincipal 3 /}。

在UserInfo端点请求用户属性时,DefaultOAuth2UserService使用RestOperations

如果您需要自定义UserInfo请求的预处理,则可以为DefaultOAuth2UserService.setRequestEntityConverter()提供自定义Converter>。默认实现OAuth2UserRequestEntityConverter构建UserInfo请求的RequestEntity表示,默认情况下在Authorization标头中设置OAuth2AccessToken

另一方面,如果您需要自定义UserInfo响应的后处理,则需要为DefaultOAuth2UserService.setRestOperations()提供自定义配置的RestOperations。默认RestOperations配置如下:

RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

OAuth2ErrorResponseErrorHandlerResponseErrorHandler,可以处理OAuth 2.0错误(400错误请求)。它使用OAuth2ErrorHttpMessageConverter将OAuth 2.0 Error参数转换为OAuth2Error

无论您是自定义DefaultOAuth2UserService还是提供自己的OAuth2UserService实现,都需要对其进行配置,如以下示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().userInfoEndpoint().userService(this.oauth2UserService())...}private OAuth2UserService oauth2UserService() {...}
}

OpenID Connect 1.0 UserService

OidcUserService是支持OpenID Connect 1.0 Provider的OAuth2UserService的实现。

在UserInfo端点请求用户属性时,OidcUserService利用DefaultOAuth2UserService

如果您需要自定义UserInfo请求的预处理和/或UserInfo响应的后处理,则需要为OidcUserService.setOauth2UserService()提供自定义配置的DefaultOAuth2UserService

无论您是自定义OidcUserService还是为OpenID Connect 1.0 Provider提供自己的OAuth2UserService实现,您都需要对其进行配置,如以下示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Login().userInfoEndpoint().oidcUserService(this.oidcUserService())...}private OAuth2UserService oidcUserService() {...}
}

[19]请注意,这与使用member={0}的基础DefaultLdapAuthoritiesPopulator的默认配置不同。

[20]也可以使用DNS查找获取服务器的IP地址。目前不支持此功能,但希望将来会在未来版本中使用。

13.用于Servlet环境的WebClient

[注意]注意

以下文档适用于Servlet环境。对于所有其他环境,请参阅WebClient for Reactive环境。

Spring Framework已经内置支持设置Bearer令牌。

webClient.get().headers(h -> h.setBearerAuth(token))...

Spring Security建立在这种支持的基础上,以提供额外的好处:

  • Spring Security将自动刷新过期的令牌(如果存在刷新令牌)
  • 如果请求访问令牌但不存在,Spring Security将自动请求访问令牌。

    • 对于authorization_code,这涉及执行重定向,然后重放原始请求
    • 对于client_credentials,只需请求并保存令牌
  • 支持透明地包含当前OAuth令牌或明确选择应使用哪个令牌的能力。

13.1 WebClient OAuth2设置

第一步是确保正确设置WebClient。可以在下面找到在servlet环境中设置WebClient的示例:

@Bean
WebClient webClient(ClientRegistrationRepository clientRegistrations,OAuth2AuthorizedClientRepository authorizedClients) {ServletOAuth2AuthorizedClientExchangeFilterFunction oauth =new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);// (optional) explicitly opt into using the oauth2Login to provide an access token implicitly// oauth.setDefaultOAuth2AuthorizedClient(true);// (optional) set a default ClientRegistration.registrationId// oauth.setDefaultClientRegistrationId("client-registration-id");return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
}

13.2隐式OAuth2AuthorizedClient

如果我们在设置中将defaultOAuth2AuthorizedClient设置为true并且使用oauth2Login(即OIDC)对用户进行身份验证,则使用当前身份验证自动提供访问令牌。或者,如果我们将defaultClientRegistrationId设置为有效的ClientRegistration id,则该注册用于提供访问令牌。这很方便,但在并非所有端点都应获取访问令牌的环境中,这很危险(您可能会向端点提供错误的访问令牌)。

Mono body = this.webClient.get().uri(this.uri).retrieve().bodyToMono(String.class);

13.3显式OAuth2AuthorizedClient

可以通过在请求属性上设置OAuth2AuthorizedClient来明确提供。在下面的示例中,我们使用Spring WebFlux或Spring MVC参数解析器支持来解析OAuth2AuthorizedClient。但是,OAuth2AuthorizedClient如何解决并不重要。

@GetMapping("/explicit")
Mono explicit(@RegisteredOAuth2AuthorizedClient("client-id") OAuth2AuthorizedClient authorizedClient) {return this.webClient.get().uri(this.uri).attributes(oauth2AuthorizedClient(authorizedClient)).retrieve().bodyToMono(String.class);
}

13.4 clientRegistrationId

或者,可以在请求属性上指定clientRegistrationIdWebClient将尝试查找OAuth2AuthorizedClient。如果未找到,将自动获取一个。

Mono body = this.webClient.get().uri(this.uri).attributes(clientRegistrationId("client-id")).retrieve().bodyToMono(String.class);

13.5 JSP标记库

Spring Security有自己的taglib,它为访问安全信息和在JSP中应用安全约束提供基本支持。

13.5.1声明Taglib

要使用任何标记,必须在JSP中声明安全性标记库:

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

13.5.2授权标签

此标记用于确定是否应评估其内容。在Spring Security 3.0中,它可以以两种方式使用[21]。第一种方法使用web - 安全表达式,在标记的access属性中指定。表达式评估将委托给应用程序上下文中定义的SecurityExpressionHandler(您应该在命名空间配置中启用web表达式以确保此服务可用)。所以,例如,你可能有

This content will only be visible to users who have the "supervisor" authority in their list of GrantedAuthoritys.

与Spring Security的PermissionEvaluator结合使用时,该标记也可用于检查权限。例如:

This content will only be visible to users who have read or write permission to the Object found as a request attribute named "domain".

如果实际允许用户单击该链接,则通常的要求是仅显示特定链接。我们如何提前确定是否允许某些事情?此标记还可以在备用模式下运行,该模式允许您将特定URL定义为属性。如果允许用户调用该URL,则将评估标记正文,否则将跳过该标记正文。所以你可能有类似的东西

This content will only be visible to users who are authorized to send requests to the "/admin" URL.

要使用此标记,您的应用程序上下文中还必须有一个WebInvocationPrivilegeEvaluator的实例。如果您使用命名空间,将自动注册。这是DefaultWebInvocationPrivilegeEvaluator的一个实例,它为提供的URL创建一个虚拟web请求,并调用安全拦截器以查看请求是成功还是失败。这允许您委托使用命名空间配置中的intercept-url声明定义的访问控制设置,并节省必须复制JSP中的信息(例如所需角色)。此方法还可以与method属性结合使用,提供HTTP方法,以实现更具体的匹配。

通过将var属性设置为变量名称,可以将评估标记的布尔结果(无论是授予还是拒绝访问)存储在页面上下文范围变量中,从而无需复制和重新评估其他条件。页面中的点。

禁用用于测试的标记授权

在未经授权的用户的页面中隐藏链接并不会阻止他们访问该URL。例如,他们可以直接在浏览器中输入它。作为测试过程的一部分,您可能希望揭示隐藏区域,以便检查链接是否真的在后端得到保护。如果将系统属性spring.security.disableUISecurity设置为true,则authorize标记仍将运行,但不会隐藏其内容。默认情况下,它还会使用…​标记包围内容。这允许您显示具有特定CSS样式的“隐藏”内容,例如不同的背景颜色。例如,尝试在启用此属性的情况下运行“tutorial”示例应用程序。

您还可以设置属性spring.security.securedUIPrefixspring.security.securedUISuffix如果要更改默认span标签周围文本(或使用空字符串来彻底删除)。

13.5.3认证标签

此标记允许访问存储在安全上下文中的当前Authentication对象。它直接在JSP中呈现对象的属性。因此,例如,如果Authenticationprincipal属性是Spring Security的UserDetails对象的实例,则使用将呈现当前用户的名称。

当然,没有必要为这种事情使用JSP标记,有些人更喜欢在视图中保持尽可能少的逻辑。您可以访问MVC控制器中的Authentication对象(通过调用SecurityContextHolder.getContext().getAuthentication())并将数据直接添加到模型中以供视图呈现。

13.5.4 accesscontrollist标签

此标记仅在与Spring Security的ACL模块一起使用时有效。它检查指定域对象的逗号分隔的所需权限列表。如果当前用户具有所有这些权限,则将评估标记正文。如果他们不这样做,它将被跳过。一个例子可能是

[警告]警告

通常,此标记应被视为已弃用。而是使用第13.5.2节“授权标签”。

This will be shown if the user has all of the permissions represented by the values "1" or "2" on the given object.

权限被传递给应用程序上下文中定义的PermissionFactory,将它们转换为ACL Permission实例,因此它们可以是工厂支持的任何格式 - 它们不必是整数,它们可以是READWRITE之类的字符串。如果未找到PermissionFactory,将使用DefaultPermissionFactory的实例。应用程序上下文中的AclService将用于加载所提供对象的Acl实例。将使用所需权限调用Acl以检查是否已授予所有权限。

此标记还支持var属性,与authorize标记的方式相同。

13.5.5 csrfInput标记

如果启用了CSRF保护,则此标记将插入一个隐藏的表单字段,其中包含CSRF保护令牌的正确名称和值。如果未启用CSRF保护,则此标记不会输出任何内容。

通常Spring Security会自动为您使用的任何标签插入CSRF表单字段,但如果由于某种原因您无法使用,则csrfInput是一个方便的替代品。

您应该将此标记放在HTML 块中,您通常会在其中放置其他输入字段。请勿将此标记放在Spring 块中。Spring Security自动处理Spring表格。

Name:
...

13.5.6 csrfMetaTags标记

如果启用了CSRF保护,则此标记将插入包含CSRF保护令牌表单字段和标头名称以及CSRF保护令牌值的元标记。这些元标记对于在应用程序中的JavaScript中使用CSRF保护非常有用。

您应该将csrfMetaTags放在HTML 块中,您通常会放置其他元标记。使用此标记后,您可以使用JavaScript轻松访问表单字段名称,标题名称和标记值。在此示例中使用JQuery来简化任务。


CSRF Protected JavaScript Page