下面只介绍了Servlet应用程序的,响应式的未写。由于是翻译的参考文档,有些地方应该词不达意。
Spring Security是一个强大且高度可定制的认证和访问控制框架。它是基于Spring的应用程序的事实上的安全标准。
主要特性:
支持全面和可扩展的身份验证和授权;
防止各种攻击,如会话固定攻击,点击劫持,跨网站请求伪造等;
集成Servlet API;
与SpringMvc的可选集成。
应用程序安全性的两个主要方面是"authentication"和"authorization"(或"access-control")。这是Spring Security的两大主要领域。"Authentication"是建立主体的过程("principal"通常指可在应用程序中执行操作的用户,设备或其它系统)。"Authorization"是指是否允许主体在应用程序内执行操作的过程。为了授权,主体的身份已经由认证过程确定。
在认证级别,Spring Security支持多种认证模式。这些认证模式大多由第三方提供,或者由相关标准组织开发。另外,Spring Security还提供了自己的一套认证功能。
无论Authentication机制如何,Spring Security都提供了一套深层次的授权功能。有三个主要的领域:授权Web请求,授权是否可以调用方法,并授权访问单个域对象实例。为理解这些差异,请分别考虑Servlet规范的Web模式安全性,EJB容器托管安全性和文件系统安全性中的授权功能。Spring Security在所有这些重要领域提供了深入的功能,在本参考指南的后面部分探讨这些功能。
在Spring Security 3.0中,代码库被细分为独立的jar,这些jar更清楚地区分了不同的功能区域和第三方依赖关系。
包含核心认证和访问控制类和接口,远程处理支持和基本配置API。由使用Spring Security的任何应用程序所要求。支持独立应用程序,远程客户端,方法(服务层)安全性和JDBC用户配置。包含顶级包:
org.springframework.security.core
org.springframework.security.access
org.springframework.security.authentication
org.springframework.security.provisioning
提供与Spring Remoting的集成。除非正在编写一个使用Spring Remoting的远程客户端,否则不需要这个。主包是org.springframework.security.remoting。
包含过滤器和相关的网络安全基础架构代码。任何具有servlet API依赖性的东西。如果需要Spring Security Web认证服务和基于URL的访问控制,将需要它。主包是org.springframework.security.web。
包含安全命名空间解析代码和Java代码。如果使用Spring Security XML命名空间进行配置或Spring Security的Java代码支持,则需要它。主包是org.springframework.security.config。这些类别都不能直接用于应用程序。
LDAP认证和供应代码。如果需要使用LDAPAuthentication或管理LDAP用户条目,则为必需。顶级包装是org.springframework.security.ldap。
spring-security-oauth2-core.jar包含为OAuth 2.0授权框架和OpenID Connect Core 1.0提供支持的核心类和接口。这是使用OAuth 2.0或OpenID Connect Core 1.0的应用程序所必需的,例如客户端,资源服务器和授权服务器。顶级包装是org.springframework.security.oauth2.core。
spring-security-oauth2-client.jar是Spring Security对OAuth 2.0授权框架和OpenID Connect Core 1.0的客户端支持。由利用OAuth 2.0 Login和/或OAuth客户端支持的应用程序所需。顶级包装是org.springframework.security.oauth2.client。
spring-security-oauth2-jose.jar包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。JOSE框架旨在提供一种安全地在各方之间传输声明的方法。它由一系列规范构建而成:
JSON Web令牌(JWT)
JSON Web签名(JWS)
JSON Web加密(JWE)
JSON Web密钥(JWK)
它包含顶级软件包:
org.springframework.security.oauth2.jwt
org.springframework.security.oauth2.jose
专门的域对象ACL实现。用于将安全性应用于应用程序内的特定域对象实例。顶级包装是org.springframework.security.acls。
Spring Security的CAS客户端集成。如果想使用CAS单点登录服务器的Spring Security Web认证。顶级包是org.springframework.security.cas。
OpenID Web认证支持。用于对外部OpenID服务器进行Authentication。org.springframework.security.openid,需要OpenID4Java。
支持使用Spring Security进行测试。
在Spring 3.1中为Spring Framework添加了对Java代码的一般支持。自Spring Security 3.2以来,已经有了Java代码的支持,使用户无需使用任何XML即可轻松配置Spring Security。
第一步,是创建Spring Security Java代码。该配置会创建一个名为springSecurityFilterChain的Servlet过滤器,它负责应用程序中的所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。可以在下面找到Spring Security Java Configuration的最基本的例子:
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 {
@Bean
public UserDetailsService userDetailsService() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
return manager;
}
}
这个配置有以下功能摘要:
下一步是在war中注册springSecurityFilterChain。可通过在支持Servlet 3.0+的环境中使用Spring的WebApplicationInitializer完成java代码。Spring Security提供了一个基类AbstractSecurityWebApplicationInitializer,它将确保springSecurityFilterChain注册。
如果没有使用Spring或Spring MVC,则需要将WebSecurityConfig传递给超类,以确保获取到配置。例子:
import org.springframework.security.web.context.*;
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
public SecurityWebApplicationInitializer() {
super(WebSecurityConfig.class);
}
}
SecurityWebApplicationInitializer将执行以下操作:
1、为应用程序中的每个URL自动注册springSecurityFilterChain过滤器;
2、增加一个载入WebSecurityConfig的ContextLoaderListener。
如果在应用程序中使用Spring,可能已经有一个WebApplicationInitializer。应该使用现有的ApplicationContext注册Spring Security。例如,使用Spring MVC,SecurityWebApplicationInitializer将如下所示:
import org.springframework.security.web.context.*;
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
}
这只会为应用程序中的每个URL注册springSecurityFilterChain过滤器。之后,将确保WebSecurityConfig在现有的ApplicationInitializer中加载。例如,如果使用Spring MVC,它将被添加到getRootConfigClasses()。
public class MvcWebApplicationInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class>[] getRootConfigClasses() {
return new Class[] { WebSecurityConfig.class };
}
// ... other overrides ...
}
迄今为止,WebSecurityConfig仅包含如何验证用户的信息。Spring Security如何知道我们想要所有用户进行Authentication?Spring Security如何知道我们想要支持基于表单的Authentication?原来WebSecurityConfigurerAdapter在configure(HttpSecurity http)方法中提供了一个默认配置,如下所示:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
上面的默认配置:
1、确保对应用程序的任何请求都做用户身份认证;
2、允许用户通过登录表单做身份认证;
3、允许用户使用HTTP基本认证做身份认证。
这个配置与XML命名空间配置非常相似:
与闭合的XML标签等价的Java代码是使用and()方法表示的,它允许继续配置父级元素。
由于没有提及任何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>,更新后的配置指定了登录页面的位置(/login)。
<2>,必须授权所有用户(即未经身份认证的用户)访问登录页面。formLogin().permitAll()授权所有用户可访问与基于表单登录关联的URL。
下面是一个使用JSP实现的用于当前配置的登录页面示例。
注意:下面的登录页面代表我们当前的配置。如果某些默认设置不能满足需求,可以更新配置。
<1>,通过URL地址/login以POST方式发起请求,以对用户做身份认证;
<2>,如果检查到参数中存在错误,则认证会失败;
<3>,如果检查到参数中有logout,则认为用户已成功注销;
<4>,用户名必须以username的HTTP参数名提供;
<5>,密码必须以password的HTTP参数名提供;
<6>,有一个隐含域是包含CSRF令牌的。
示例只需要用户做身份认证,并已为应用程序中的每个URL完成此操作。可以通过向http.authorizeRequests()方法中添加多个子项来为URL指定自定义要求。例如:
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()方法有多个子项方法,每个matcher按照它们声明的顺序执行。
<2>,指定了任何用户都可以访问的多个URL模式。如果URL以"/resources/",或"/signup",或"/about"开头,那么任何用户都可以访问。
<3>,任何以"/admin/"开头的URL将仅限于拥有"ROLE_ADMIN"角色的用户访问。由于调用了hasRole()方法,因此不需要指定"ROLE_"前缀。
<4>,任何以"/db/"开头的URL都需要用户同时拥有"ROLE_ADMIN"和"ROLE_DBA"角色。由于使用的是hasRole表达式,因此无需指定"ROLE_"前缀。
<5>,任何尚未匹配的URL只需要对用户做身份认证。
使用WebSecurityConfigurerAdapter时,会自动应用注销功能。默认情况下,访问URL /logout将通过以下方式注销用户:
1,使HTTP session失效;
2,清除任何已配置的RememberMe认证信息;
3,清除SecurityContextHolder;
4,重定向到/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模式。
<3>,发生注销后重定向到的URL。默认是/login?logout。
<4>,指定一个自定义LogoutSuccessHandler。如果指定,则logoutSuccessUrl()被忽略。
<5>,指定在注销时是否使HttpSession无效,默认true。可配置内部实现的SecurityContextLogoutHandler。
<6>,添加一个LogoutHandler。SecurityContextLogoutHandler默认作为最后一个LogoutHandler。
<7>,允许指定在注销成功时删除的cookies名称。这是显式添加CookieClearingLogoutHandler的快捷方式。
注销也可以使用XML命名空间表示法进行配置。
通常,为了自定义注销功能,可以添加LogoutHandler和(或)LogoutSuccessHandler实现。
通常,LogoutHandler实现是指那些能够参与注销处理的类,期望调用它们来执行必要的清理工作,因此,它们不应该抛出异常。实现如下:
[PersistentTokenBasedRememberMeServices]
[TokenBasedRememberMeServices]
[CookieClearingLogoutHandler]
[CsrfLogoutHandler]
[SecurityContextLogoutHandler]
与直接提供LogoutHandler实现不同,fluent API还提供些快捷方式,它们在内部提供了各自的LogoutHandler实现。例如,deleteCookies()允许指定在注销成功时删除一个或多个Cookie的名称。与添加CookieClearingLogoutHandler相比,这是一条捷径。
LogoutFilter在成功注销后调用LogoutSuccessHandler,以处理重定向或期望的转发目标。请注意,interface与LogoutHandler几乎相同,但可能会引发异常。
提供以下实现:
[SimpleUrlLogoutSuccessHandler]
[HttpStatusReturningLogoutSuccessHandler]
如上所述,不需要直接指定SimpleUrlLogoutSuccessHandler。fluent API通过设置logoutSuccessUrl()提供了一种快捷方式,这是由SimpleUrlLogoutSuccessHandler在内部设置的。在注销发生后将重定向到提供的URL上,默认是/login?logout。
在REST API类型的场景中,HttpStatusReturningLogoutSuccessHandler可能很有趣。与在成功注销时重定向到URL不同,这个LogoutSuccessHandler允许提供一个要返回的简单HTTP状态码。如果未配置,默认状态码返回200。
OAuth 2.0 Login特性为应用程序提供了一种功能,可以让用户通过使用他们在OAuth 2.0 Provider(例如GitHub)或OpenID Connect 1.0 Provider(例如谷歌)的现有帐户登录到应用程序。OAuth 2.0 Login实现了这样的功能:"Login with Google"或"Login with GitHub"。
注意:OAuth 2.0 Login是通过Authorization Code Grant实现的。
ClientRegistration是向OAuth 2.0或OpenID Connect 1.0 Provider注册的客户端的展现。
一个客户端注册会包含很多信息,如client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI和其它信息。
ClientRegistration及其属性定义如下:
public final class ClientRegistration {
private String registrationId; <1>
private String clientId; <2>
private String clientSecret; <3>
private ClientAuthenticationMethod clientAuthenticationMethod; <4>
private AuthorizationGrantType authorizationGrantType; <5>
private String redirectUriTemplate; <6>
private Set
private ProviderDetails providerDetails;
private String clientName; <8>
public class ProviderDetails {
private String authorizationUri; <9>
private String tokenUri; <10>
private UserInfoEndpoint userInfoEndpoint;
private String jwkSetUri; <11>
public class UserInfoEndpoint {
private String uri; <12>
private String userNameAttributeName; <13>
}
}
}
<1>,registrationId:唯一标识`ClientRegistration`的ID。
<2>,clientId:客户端标识符。
<3>,clientSecret:客户端密码。
<4>,clientAuthenticationMethod:用于向Provider验证客户端的方法。支持的值有basic和post。
<5>,authorizationGrantType:OAuth 2.0授权框架定义了4个[授权]类型。支持的值有authorization_code和implicity。
<6>,redirectUriTemplate:在最终用户通过身份认证和授权可访问客户端之后,授权服务器将最终用户的用户代理重定向到客户端被注册的重定向URI上。默认的重定向URI模板是`{baseUrl}/login/oauth2/code/{registrationId}`,它支持URI模板变量。
<7>,scopes:客户端在授权请求流程中请求的范围,例如openid,email或profile。
<8>,clientName:客户端使用的描述性名称。该名称可用于某些特定场景,例如当在自动生成的登录页面中显示客户端名称的时候。
<9>,authorizationUri:用于授权服务器的授权端点URI。
<10>,tokenUri:授权服务器的令牌端点URI。
<11>,jwkSetUri:该URI用于从授权服务器获取[JSON Web Key(JWK)]集合,其中包含用于验证ID令牌的[JSON Web Signature(JWS)]以及可选的UserInfo响应的加密密钥。
<12>,(userInfoEndpoint)uri:UserInfo端点URI,用于访问经过身份认证的最终用户的声明/属性。
<13>,userNameAttributeName:在UserInfo响应中返回的属性名称,该名称引用最终用户的名称或标识符。
下表概述了Spring Boot 2.0 OAuth客户端属性到ClientRegistration属性的映射。
Spring Boot 2.0 |
ClientRegistration |
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-template |
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].userNameAttribute |
providerDetails.userInfoEndpoint.userNameAttributeName |
ClientRegistrationRepository充当OAuth 2.0 / OpenID Connect 1.0 ClientRegistration(s)的存储库。
注意:客户端注册信息最终由关联的授权服务器存储和拥有。该存储库提供了获取主客户端注册信息的子集的能力。
Spring Boot 2.0自动配置将spring.security.oauth2.client.registration.[registrationId]下的每个属性绑定到ClientRegistration的实例,然后在ClientRegistrationRepository内组合每个ClientRegistration实例。
注意:ClientRegistrationRepository的默认实现是InMemoryClientRegistrationRepository。
自动配置还把ClientRegistrationRepository作为@Bean在ApplicationContext中注册,以便应用程序需要时,可用于依赖注入。
示例:
@Controller
public class OAuth2LoginController {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@RequestMapping("/")
public String index() {
ClientRegistration googleRegistration =
this.clientRegistrationRepository.findByRegistrationId("google");
...
return "index";
}
}
CommonOAuth2Provider预先定义了一组默认客户端属性,适用于众多知名Provider:Google,GitHub,Facebook和Okta。
例如,authorization-uri,token-uri和user-info-uri不会经常更改Provider。因此,提供默认值以减少所需的配置是有意义的。
如前所述,当配置Google client时,只需要client-id和client-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()中设置的客户端属性的自动默认设置。
有些OAuth 2.0提供者支持多租户,这会为每个租户(或子域)生成不同的协议端点。
例如,向Okta注册的OAuth客户端被分配到特定的子域,并拥有自己的协议端点。
对于这些情况,Spring Boot 2.0提供了以下基本属性以配置自定义提供者属性:spring.security.oauth2.client.provider.[providerId]。
spring:
security:
oauth2:
client:
registration:
okta: <1>
client-id: okta-client-id
client-secret: okta-client-secret
provider:
okta:
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)允许自定义配置协议端点的位置。
用于OAuth客户端支持的Spring Boot 2.0自动配置类为OAuth2ClientAutoConfiguration。
它执行以下任务:
如果需要根据特定要求覆盖自动配置,可以通过以下方式进行:
下例显示如何注册ClientRegistrationRepository(用@Bean注解):
@Configuration
public class OAuth2LoginConfig {
@Bean
public 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,并通过httpSecurity.oauth2Login()启用OAuth 2.0登录:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login();
}
}
下例显示如何通过注册ClientRegistrationRepository(用@Bean注解),并提供WebSecurityConfigurerAdapter完全覆盖自动配置。
@Configuration
public class OAuth2LoginConfig {
@EnableWebSecurity
public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login();
}
}
@Bean
public 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();
}
}
如果不使用Spring Boot 2.0,并且想在CommonOAuth2Provider(例如google)中配置一个预定义的Provider,请应用以下配置:
@Configuration
public class OAuth2LoginConfig {
@EnableWebSecurity
public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login();
}
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
}
@Bean
public OAuth2AuthorizedClientService authorizedClientService() {
return new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository());
}
private ClientRegistration googleClientRegistration() {
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.build();
}
}
OAuth2AuthorizedClient是授权客户端的展现。当最终用户(Resource Owner)授权客户端可访问其受保护的资源时,该客户端就被认为是已授权的。
OAuth2AuthorizedClient用于将OAuth2AccessToken关联到ClientRegistration(客户端)和资源所有者,该资源所有者是主要的被授权最终用户。
OAuth2AuthorizedClientService的主要作用是管理OAuth2AuthorizedClient实例。从开发人员的角度看,它提供了查找与客户端关联的OAuth2AccessToken的功能,以便可以使用它向资源服务器发起请求。
注意,Spring Boot 2.0自动配置在`ApplicationContext`中注册`OAuth2AuthorizedClientService`(用@Bean注解)。
开发人员还可以在ApplicationContext(覆盖Spring Boot 2.0自动配置)中注册OAuth2AuthorizedClientService@Bean,以便能够查找与特定相关的OAuth2AccessToken ClientRegistration(客户端)。示例:
@Controller
public class OAuth2LoginController {
@Autowired
private OAuth2AuthorizedClientService authorizedClientService;
@RequestMapping("/userinfo")
public String userinfo(OAuth2AuthenticationToken authentication) {
// authentication.getAuthorizedClientRegistrationId() returns the
// registrationId of the Client that was authorized during the Login flow
OAuth2AuthorizedClient authorizedClient =
this.authorizedClientService.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(),
authentication.getName());
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "userinfo";
}
}
到目前为止,只看了最基本的身份验证配置。下面看一下配置身份验证的稍微高级一些的选项。
以下是配置多个用户的示例:
@Bean
public UserDetailsService userDetailsService() throws Exception {
// ensure the passwords are encoded properly
UserBuilder 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;
}
以下示例假定,已经在应用程序中定义了DataSource。
@Autowired
private DataSource dataSource;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// ensure the passwords are encoded properly
UserBuilder 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"));
}
基于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: groups
dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people
dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password
dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password
dn: 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=org
dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
通过将自定义的AuthenticationProvider类公开为bean来定义自定义的认证类。例如,假设SpringAuthenticationProvider类实现了AuthenticationProvider接口,那么下面的代码就是自定义的身份认证:
注:仅当AuthenticationManagerBuilder尚未填充时才会使用。
@Bean
public SpringAuthenticationProvider springAuthenticationProvider() {
return new SpringAuthenticationProvider();
}
通过将自定义UserDetailsService类公开为bean来定义自定义认证类。例如,假设SpringDataUserDetailsService类实现了UserDetailsService接口,那么,下面的代码就是自定义的身份认证:
注意:仅当AuthenticationManagerBuilder尚未填充且未定义AuthenticationProviderBean时才会使用此选项。
@Bean
public SpringDataUserDetailsService springDataUserDetailsService() {
return new SpringDataUserDetailsService();
}
还可以通过将PasswordEncoder类公开为bean来定制密码的编码方式。例如,如果使用bcrypt,则可以添加如下所示的一个bean定义:
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
可以配置多个HttpSecurity实例,就像可以有多个
@EnableWebSecurity
public class MultiHttpSecurityConfig {
@Bean <1>
public UserDetailsService userDetailsService() throws Exception {
// ensure the passwords are encoded properly
UserBuilder 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) <2>
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**") <3>
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}
}
@Configuration <4>
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
}
<1>,正常配置身份认证;
<2>,创建包含@Order的WebSecurityConfigurerAdapter实例,以指定首先使用哪个WebSecurityConfigurerAdapter。
<3>,http.antMatcher()说明此HttpSecurity仅适用于以/api/开头的URL。
<4>,创建WebSecurityConfigurerAdapter的另一个实例,如果URL不以/api/开头,则会使用此配置。这个配置会在ApiWebSecurityConfigurationAdapter之后执行,因为Api..Adapter用@Order(1)注解,而没有@Order注解的默认最后执行。
从2.0版本开始,Spring Security已经大大改善了对服务层方法的安全性支持。它既提供了框架级的@Secured注解,也支持对JSR-250安全性的注解。从3.0开始,还可以使用新的基于表达式的注解。对单个bean应用安全性时,需要使用拦截方法的元素来修饰bean声明,也可以使用AspectJ样式的切入点在整个服务层中横切多个bean。
可以在任何@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 {
// ...
}
这些都是基于JSR标准的,允许应用简单的基于角色的约束,但是没有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);
}
有时可能需要执行比@EnableGlobalMethodSecurity注解所允许的更复杂的操作。对于这些实例来说,可以通过继承GlobalMethodSecurityConfiguration,来确保@EnableGlobalMethodSecurity注解能用于子类。例如,如果想提供一个自定义MethodSecurityExpressionHandler,则可以使用以下配置:
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
// ... create and return custom MethodSecurityExpressionHandler ...
return expressionHandler;
}
}
Spring Security的Java代码不会公开它配置的每个对象的所有属性。这简化了大多数用户的配置操作。毕竟,如果每个属性都暴露出来,用户就可以使用标准的bean配置。
尽管有很好的理由不会直接暴露所有属性,但用户可能仍然需要更高级的配置选项。为了解决这个问题,Spring Security引入了ObjectPostProcessor的概念,它可以用来修改或替换由Java代码创建的许多对象实例。例如,如果想在FilterSecurityInterceptor上配置filterSecurityPublishAuthorizationSuccess属性,则可以:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor
public
O fsi) {
fsi.setPublishAuthorizationSuccess(true);
return fsi;
}
});
}
可以在Spring Security中提供定制DSL。例如:
public class MyCustomDsl extends AbstractHttpConfigurer
private boolean flag;
@Override
public void init(H http) throws Exception {
// any method that adds another configurer
// must be done in the init method
http.csrf().disable();
}
@Override
public 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 {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.apply(customDsl())
.flag(true)
.and()
...;
}
}
代码按以下顺序执行:
1、Config的configure方法被调用;
2、MyCustomDsl的init方法被调用;
3、MyCustomDsl的configure方法被调用。
如果需要,可以让WebSecurityConfiguerAdapter通过使用SpringFactories来默认添加MyCustomDsl。例如,在类路径上创建一个名为META-INF/spring.factories的文件,内容如下:
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyCustomDsl
希望禁用默认设置的用户可以明确地这样做。
@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.apply(customDsl()).disable()
...;
}
}
命名空间配置自Spring Framework 2.0就开始使用。它允许使用来自其它XML Schema的元素来补充传统的Spring bean应用程序上下文语法。命名空间元素可以简单地用于配置单个Bean,或者定义一种与问题域更接近的配置语法,并向用户隐藏潜在的复杂性。一个简单的元素背后可能会有多个bean配置和处理步骤。例如,将以下元素从安全性命名空间添加到应用程序上下文将启动一个嵌入式LDAP服务器,以在应用程序中测试使用情况:
这比连接相应的Apache Directory Server bean简单得多。最常见的备选配置需求由ldap-server元素上的属性支持,用户不必担心需要创建哪些bean以及那些bean的属性名称是什么。
要开始在应用程序上下文中使用安全性命名空间,需要在类路径中包含spring-security-config.jar。然后,就是将模式声明添加到应用程序上下文文件中:
xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> ...
在示例中,会将"security"用作默认命名空间而不是"beans",这意味着,可以忽略所有安全命名空间元素上的前缀,使内容更易于阅读。如果将应用程序上下文划分为单独的文件,并在其中一个文件中包含大部分安全配置,则可能还需要执行此操作。安全应用程序上下文文件将会像这样开始。
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> ...
命名空间旨在抓住框架的最常见用途,并提供简化和简洁的语法来在应用程序中启用它们。该设计基于框架内的大规模依赖关系,可分为以下几个方面:
在本节中,将看看如何构建一个命名空间配置,来使用框架的一些主要功能。可将身份认证支持和访问控制添加到现有的Web应用程序,并带有一些测试登录名。然后,研究如何根据数据库或其它安全存储库切换到身份验证。在后面的章节中,将介绍更高级的命名空间配置选项。
首先是将以下过滤器声明添加到web.xml文件中:
这种配置提供了一个进入Spring Security Web基础设施的钩子。DelegatingFilterProxy是Spring框架的类,它执行一个过滤器实现。在这里,该bean是"springSecurityFilterChain",它是由命名空间创建的用于处理Web安全性的内部bean。Web安全服务使用
要启用web安全性,首先添加以下内容到web.xml。
以上配置说明应用程序中的所有URL都受到保护,只有拥有【ROLE_USER】角色的用户才能访问,要使用带有用户名和密码的表单登录应用程序,并且要注册一个注销的URL,以允许用户正常退出。
注意,可以使用多个
要添加一些用户,可以直接在命名空间中定义一组测试数据:
这是存储相同密码的安全方式的一个例子。密码前缀为{bcrypt},以表明DelegatingPasswordEncoder支持任何经BCrypt加密方式对密码进行Hash处理的PasswordEncoder:
authorities="ROLE_USER, ROLE_ADMIN" /> authorities="ROLE_USER" /> 如果熟悉框架的pre-namespace版本,可能已经大概猜出这里发生了什么。 上面的配置定义了两个用户、他们的密码和他们在应用程序中的角色(将用于访问控制)。也可以使用 此时,可以启动应用程序,并且需要登录才能继续执行。 由于没有明确设置登录页面的URL,因此Spring Security会根据已启用的功能和使用的标准值自动生成一个登录界面,以处理提交的登录请求,用户登录后将被发送至默认的URL处。不过,命名空间提供了大量支持以定制这些选项。如果想提供自己的登录页面,则可以使用: 另注意,我们添加了一个额外的 从Spring Security 3.1开始,可以使用多个 重要的是要意识到,这些不安全的请求将完全忽略任何与Spring Security web相关的配置或诸如["requires-channel"]之类的附加属性。因此,在请求期间,将无法访问当前用户的信息或调用安全的方法。如果仍想应用安全过滤器链,那么可以使用[access='IS_AUTHENTICATED_ANONYMOUSLY']作为替代方案。 如果想使用基本身份认证而不是表单登录,配置如下。 如果尝试访问受保护资源时未提示表单登录,则["default-target-url"]就会发挥作用。这是成功登录后用户将访问的URL,默认为"/"。通过将["always-use-default-target"]设置为"true",以使用户始终停留在此页面(无论登录名是"on-demand",还是明确选择登录)。这有助于应用程序总是要求用户从"home"页开始,例如: always-use-default-target='true' /> 为了更好地控制目标,可以使用["authentication-success-handler-ref"]作为["default-target-url"]的替代选项。被引用的bean应该是AuthenticationSuccessHandler的一个实例。 在实践中,需要一个更具可扩展性的用户信息来源,而不是简单地将几个名称添加到应用程序上下文文件中,如将用户信息存储在数据库或LDAP服务器中。如果在应用程序上下文中有一个名为"myUserDetailsService"的UserDetailsService实现,那么,可以用以下方式做身份认证。 如果使用数据库,那么可以使用。 "securityDataSource"是数据源的bean名,指向包含标准Spring Security用户表的数据库。或者,可以配置Spring Security的JdbcDaoImpl类并让["user-service-ref"]指向它: class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"> 还可以使用标准的身份认证提供者bean,如下。 其中myAuthenticationProvider是应用程序上下文中实现认证提供者的bean名。可以使用多个 应该始终使用满足设计目标的安全哈希算法对密码编码(不是像SHA或MD5这样的标准算法), class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/> authorities="ROLE_USER, ROLE_ADMIN" /> authorities="ROLE_USER" /> 在大多数情况下,用[bcrypt]作为编码算法都是个不错的选择。如果仅使用简单哈希,或者直接存储纯文本密码,那么就应该考虑一个更安全的方案,如使用[bcrypt]。 如果应用程序同时支持HTTP和HTTPS,并且要求只能通过HTTPS访问特定的URL,则可以使用 ... 使用此配置后,如果用户尝试以HTTP模式访问任何匹配【"/secure/**"】的地址时,就会首先被重定向到HTTPS模式的URL上。可用参数包括"http","https"或"any"。其中,"any"表示可以使用HTTP或HTTPS。 如果应用程序使用HTTP或HTTPS的非标准端口,则可以按如下方式指定端口映射列表: ... 请注意,为了确保安全,应用程序不应该使用HTTP或在HTTP和HTTPS之间切换。应该从HTTPS开始(用户输入HTTPS的URL),并始终使用安全连接以避免任何可能的中间攻击。 可以配置Spring Security来检测提交的无效会话ID,并将用户重定向到适当的URL上。通过 ... 注意,如果使用此机制来检测会话超时,那么在用户没有关闭浏览器的情况下,直接注销,然后又登录回来,则可能会引起错误报告。这是因为当会话失效时,其cookie不会被清除,即使用户注销,其cookie也会保存下来。这可以通过在注销时显式地删除["JSESSIONID"]的cookie来避免以上错误,例如,可在注销处理程序中使用以下语法: 不幸的是,这不能保证对每个servlet容器都有效,需要在实际环境中测试验证。 如果要限制单个用户登录应用程序的能力,可通过简单配置实现。首先,将以下listener添加到web.xml,以使Spring Security可更新会话生命周期事件: org.springframework.security.web.session.HttpSessionEventPublisher 然后,添加下面的内容到web.xml: ... 防止用户多次登录--第二次登录会导致第一次登录失效。通常情况下,应该阻止第二次登录,如下: ... 第二次登录将被拒绝,"拒绝"的意思是,如果使用基于表单的登录,则用户地第二次登录将被转到[authentication-failure-url]上。如果通过另一种非交互机制(如"remember-me")进行第二次身份验证,则会向客户端发送一个"unauthorized"(401)错误。如果要使用错误页面,则可以在 如果使用自定义的基于表单登录的认证过滤器,则必须明确配置并发会话控制支持。 会话固定攻击是一个潜在的风险,因为恶意攻击者可能通过访问网站来创建会话,然后让访问该网站的其他用户使用同一会话登录(通过将会话标识符作为一个链接的参数发送给他们)。Spring Security通过创建新会话或在用户登录时更改会话ID来自动防止该风险。如果不需要该项保护,或与其它配置冲突,则可以使用 当发生会话固定保护时,会产生SessionFixationProtectionEvent事件。如果使用的是"changeSessionId",会导致任何javax.servlet.http.HttpSessionIdListener的监听器都会被通知到,如果代码中正在监听这两个事件,就要谨慎使用。 命名空间普通的基于表单的登录外,还支持支持OpenID的登录,例如: 然后,应该向OpenID提供者(例如myopenid.com)注册,并将用户信息添加到 应该可以使用myopenid.com网站进行Authentication。通过设置openid-login元素上的user-service-ref属性,还可以选择特定的UserDetailsService bean来使用OpenID。请注意,从上述用户配置中省略了密码属性,因为这组用户数据仅用于为用户加载权限。随机密码将在内部生成,从而防止在配置中的其它位置意外地将此用户数据用作Authentication源。 支持OpenID属性交换。例如,以下配置将尝试从OpenID提供程序中检索电子邮件和全名,供应用程序使用: 每个OpenID属性的"type"是一个由特定模式确定的URI,在这种情况下为http://axschema.org/。如果必须为成功认证检索属性,则可以设置required属性。支持的确切架构和属性取决于OpenIDProvider。属性值作为Authentication过程的一部分返回,并可以使用以下代码进行访问: OpenIDAuthenticationToken token = (OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication(); List OpenIDAttribute包含属性类型和检索值(或多值属性中的值)。如果希望使用多个身份提供程序,则还支持多个属性交换配置。可以使用每个identifier-matcher属性提供多个attribute-exchange个元素。这包含一个正则表达式,它将与用户提供的OpenID标识符相匹配。 SpringSecurity框架维护了一个过滤器链,如果想将自己的过滤器添加到过滤器链的特定位置,或者在没有命名空间配置的地方使用Spring Security的过滤器(如CAS)。又或者想要使用标准命名空间过滤器的自定义版本,例如,由 在使用命名空间时,过滤器始终按顺序严格执行。在创建应用程序上下文时,过滤器bean按命名空间的代码名称排序,标准的Spring Security过滤器在命名空间中都有一个别名和一个固定的位置。 在以前的版本中,在应用程序上下文的后处理期间,创建过滤器实例之后进行排序。在版本3.0+中,现在在bean实例化之前,在bean元数据级完成排序。这会影响如何将自己的过滤器添加到堆栈,因为在解析 下表显示了过滤器,别名和命名空间元素/属性。按照它们在过滤器链中出现的顺序列出。 别名 过滤器类 命名空间元素或属性 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子类 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 要使用定制过滤器,可通过指定 如果想将过滤器放在另一个过滤器的前后,可以使用["after"]或["before"]属性。["position"]属性的"FIRST"或"LAST"值,指定过滤器放在整个过滤器链的最前或最后。 提示:避免过滤器位置冲突 如果插入的自定义过滤器与命名空间创建的一个标准过滤器位置相同,那么重要的是不要错误地包含命名空间版本。 注意,无法替换使用 如果要替换需要身份认证入口点的命名空间过滤器(例如,未经身份验证的用户试图访问安全资源而触发身份验证过程),则还需要添加一个自定义入口点Bean。 如果没有通过命名空间使用表单登录,OpenID或基本认证,那么可能需要使用传统的bean语法定义认证过滤器和入口点,并将它们链接到命名空间中,就像刚刚看到的那样。可以使用 当使用命名空间配置时,会自动为程序注册一个默认的AccessDecisionManager实例,并将根据你在 系统的默认策略是使用带有RoleVoter和AuthenticatedVoter的AccessDecisionManager。 如果需要使用更复杂的访问控制策略,则很容易为方法和网络安全设置替代方案。 对于方法安全性,可以通过将 ... 在 ... 在Spring Security中提供认证服务的主界面是AuthenticationManager。它通常是ProviderManager类的实例,该实例是使用 可能要向ProviderManager注册其它AuthenticationProvider的Bean,这可以通过使用带有["ref"]属性的 class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> ... 其它常见需求是另一个bean可能要引用AuthenticationManager。可以为AuthenticationManager注册别名,并在应用程序上下文的其它地方使用此名称。 ... class="com.somecompany.security.web.CustomFormLoginFilter"> ... 在熟悉设置和运行一些基于命名空间配置的应用程序后,要更深入地了解框架在命名空间表象背后的工作原理。像大多数软件一样,Spring Security也有一些核心接口,类和概念抽象,这些通常在整个框架中使用。本部分将看看它们如何协同工作来支持Spring Security中的认证和访问控制。 在Spring Security 3.0中,spring-security-core.jar的内容被精简到最低限度。它不再包含任何与Web应用程序安全性、LDAP或命名空间配置相关的代码。这里可以查看在核心模块中使用的一些Java类型。它们代表了框架中的构建块(building blocks),因此,如果需要超越简单的命名空间配置,那么理解它们是什么是非常重要的,即使实际上不需要直接与它们交互。 最基本的对象是SecurityContextHolder,存储应用程序当前安全上下文详细信息,其中包括当前使用该应用程序的主体的详细信息。默认情况下,SecurityContextHolder使用ThreadLocal来存储这些细节,这意味着安全上下文始终可用于同一执行线程中的方法,即使安全上下文没有作为参数显式地传递给那些方法。以请求被处理后清楚线程的方式使用ThreadLocal是非常安全的,Spring Security会自动处理。 有些应用程序并不完全适合使用ThreadLocal,因为它们使用线程的方式不同。例如,Swing客户端可能希望Java虚拟机中的所有线程使用相同的安全上下文。可以在启动时使用策略配置SecurityContextHolder,以指定如何存储上下文。对于独立应用程序,可以使用SecurityContextHolder.MODE_GLOBAL策略。其它应用程序可能希望安全线程产生的线程也具有相同的安全标识,这可以通过SecurityContextHolder.MODE_INHERITABLETHREADLOCAL实现。有两种从默认的SecurityContextHolder.MODE_THREADLOCAL更改模式的方法。第一个是设置系统属性,第二个是在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接口的一个实例。这是保存在线程本地存储中的对象。Security中的大多数认证机制均返回一个UserDetails实例作为主体。 上述代码说明可从Authentication对象获得principal,principal只是一个Object。大多数情况下,可以转换为UserDetails对象。UserDetails是Spring Security的核心接口。它以一种可扩展的和特定于应用程序的方式,代表一个主体。可将UserDetails看作是自己的用户数据库与SecurityContextHolder中SpringSecurity所需数据之间的适配器。作为来自用户数据库中某些内容的表示,通常会将UserDetails转换为应用程序提供的原始对象,这样,就可以调用特定的业务方法(如getEmail(),getEmployeeNumber()等)。 那么,何时提供UserDetails对象,又该怎么做呢?这里有一个称为UserDetailsService的特殊接口,它的唯一方法接受String型的username参数,并返回UserDetails: UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 这是在Spring Security中加载用户信息的最常见方法,当需要用户信息时,会看到它在整个框架中使用。 对于成功的身份验证,UserDetails用于构建存储在SecurityContextHolder中的Authentication对象。好消息是,我们提供了许多UserDetailsService实现,其中一个使用内存映射(InMemoryDaoImpl),另一个使用JDBC(JdbcDaoImpl)。不过,大多数用户倾向于自己实现,它们通常基于已存在的DAO实现。记住,无论UserDetailsService返回什么,都可以使用上面的代码片段从SecurityContextHolder中获得。 注意,关于UserDetailsService经常有一些混淆。它纯粹是用于用户数据的DAO,除了向框架内的其它组件提供该数据之外,它不执行其它功能。特别是,不会对用户进行身份认证,这是由AuthenticationManager完成的。在许多情况下,如果需要自定义身份认证过程,直接实现AuthenticationProvider更有意义。 除了principal之外,Authentication提供的另一个重要方法是getAuthorities(),此方法提供了一个GrantedAuthority对象数组,GrantedAuthority是授予principal的权限,这些权限通常是"roles",如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。这些角色稍后将配置为Web授权,方法授权和域对象授权。Spring Security的其它部分有解析这些权限的能力,GrantedAuthority对象通常由UserDetailsService加载。 通常,GrantedAuthority对象是应用程序范围的权限。它们不是特定于给定领域对象的。因此,不太可能有一个GrantedAuthority对象代表对编号为54的Employee对象的授权,因为如果有成千上万个此类权限,会很快耗尽内存(至少,导致应用程序花很长时间来验证用户)。当然,Spring Security是专门设计来处理这种常见的需求,除非你想使用项目的领域对象来实现安全性目标。 到目前为止,Spring Security的主要类是: SecurityContextHolder,提供对SecurityContext的访问。 SecurityContext,保存Authentication对象和特定请求的安全信息。 Authentication,在Spring Security中以特定方式表示principal。 GrantedAuthority,反映授予principal的应用程序范围的权限。 UserDetails,提供从应用程序的DAOs或其它安全数据源构建Authentication对象所需的信息。 UserDetailsService,根据传递的username参数创建UserDetails对象。 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 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对象时,就会对用户进行身份验证。 Spring Security并不关心如何将Authentication对象放入SecurityContextHolder中。唯一的关键要求是SecurityContextHolder包含包含一个Authentication对象,它在AbstractSecurityInterceptor需要授权用户操作之前代表一个主体。 可以编写自己的过滤器或MVC控制器,以便为不基于Spring Security的认证系统提供互操作性。例如,可能使用容器管理的认证,它使当前用户可以从ThreadLocal或JNDI位置访问。或者,也可能为拥有旧的专有认证系统的公司工作,该系统是你无法控制的公司"标准"。在这种情况下,Spring Security很容易运行,并且仍然提供授权功能。所需做的就是写一个过滤器(或等效过滤器),它从一个位置读取第三方用户信息,构建一个Spring Security特定的Authentication对象,并将其放入SecurityContextHolder中。这时,还要考虑通常由内置认证基础程序自动处理的事情。例如,在向客户端脚注写入响应之前,可能需要预先创建一个HTTP会话来缓存请求之间的上下文[在响应提交后无法创建会话]。 现在来探讨一下在Web应用程序中使用Spring Security的情况(未启用web.xml安全性)。用户如何进行Authentication并建立安全上下文? 考虑一个典型的Web应用程序的认证过程: 1、访问主页,然后点击链接。 2、向服务器发送请求,服务器检查请求的是否为受保护的资源。 3、由于还没有做身份认证,服务器会返回一个响应,指出必须进行身份认证。响应可能是HTTP响应代码,也可能是重定向到特定页面。 4、根据验证机制的不同,浏览器将重定向到特定的网页,以便填写表单,或者浏览器以某种方式获取你的身份标识符(通过基本身份认证对话框,cookie或X.509证书等)。 5、浏览器将向服务器发送一个响应。这要么是包含已填写的表单内容的HTTP POST,要么是包含身份验证详细信息的HTTP头。 6、接下来,服务器将确定提交的凭证是否有效。如果有效,下一步就会发生。如果无效,通常浏览器会显示重新填写信息(因此,回到上面的第2步)。 7、触发身份认证过程所提交的原始请求要重新提交,希望你已经通过了足够的授权权限来访问受保护的资源。如果有足够的访问权限,请求将会成功。否则,将收到代码403的HTTP错误,意思是"forbidden"。 Spring Security有不同的类,负责上述大多数步骤。主要参与者(按其使用顺序)是ExceptionTranslationFilter,一个AuthenticationEntryPoint和一个"身份认证机制",它负责调用前面的AuthenticationManager。 ExceptionTranslationFilter是一个Spring Security过滤器,负责检测抛出的任何Spring Security异常。此类异常通常由AbstractSecurityInterceptor抛出,它是授权服务的主要提供者。下一节讨论AbstractSecurityInterceptor,现在只需要知道它会产生Java异常,并且不关心HTTP,也不去认证主体。相反,ExceptionTranslationFilter提供此服务,具体负责返回错误代码403(如果主体已通过认证,并只是缺乏足够的访问权限--按上述步骤7),或启动AuthenticationEntryPoint(如果主体未经认证,需要开始第3步)。 AuthenticationEntryPoint负责上面的第3步。每个Web应用程序都会有一个默认的认证策略(可以像配置Spring Security中的其它任何东西一样,但现在要保持简单)。每个主要的认证系统都有自己的AuthenticationEntryPoint实现,通常执行步骤3中描述的操作之一。 一旦浏览器提交了你的身份认证凭据(以HTTP表单POST或HTTP header的形式),服务器上就需要有某种东西"收集"这些身份验证细节。目前已经走到上面列表中的第6步了。在Spring Security中,从用户代理(通常是web浏览器)收集身份验证细节的功能有一个特殊的名称,称为"身份验证机制"。示例是基于表单的登录和基本身份认证。一旦从用户代理收集了身份认证详细信息,就会构建一个Authentication的"request"对象,然后将其交给AuthenticationManager。 在认证机制接收到完全填充的Authentication对象后,它认为请求有效,就会将Authentication对象放入SecurityContextHolder中,并要求重试原始请求(上面的步骤7)。另一方面,如果AuthenticationManager拒绝了请求,认证机制将要求用户代理重试(上面的第2步)。 根据应用程序的类型,可能需要适当的策略来存储用户操作之间的安全上下文。在典型的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()总是返回一个新的上下文实例。 在SpringSecurity中,负责制定访问控制决策的主要接口是AccessDecisionManager。它有一个decide方法,该方法接受一个表示请求访问的主体的Authentication对象、一个"安全对象"(见下文)和一个应用于该对象的安全元数据属性列表(例如授予访问权限所需的角色列表)。 如果你熟悉AOP,那么应该知道有不同类型的Advice可供选择:before,after,throws和around。AOP的around增强非常有用,因为一个增强可以选择是否继续方法调用,是否修改响应以及是否抛出异常。Spring Security为方法调用和Web请求提供了一个环绕增强,可使用Spring的标准AOP支持为方法调用实现环绕增强,使用标准过滤器实现对Web请求的环绕增强。 对于那些不熟悉AOP的人来说,关键是理解Spring Security可以保护方法调用以及Web请求。大多数人都对保护其服务层上的方法调用感兴趣,是因为服务层是大多数业务逻辑所在。如果只需要在服务层保证方法调用的安全,Spring的标准AOP就足够了。如果需要直接保护领域对象,可能AspectJ更值得考虑。 可以选择使用AspectJ或Spring AOP执行方法授权,也可以选择使用过滤器执行Web请求授权。可以一起使用0,1,2,3个这些方法。主流使用模式是执行一些Web请求授权,再加上服务层上的一些Spring AOP方法调用授权。 那么什么是"安全对象"呢?Spring Security使用这个术语来指代应用于它的任何具有安全性的对象(比如授权决策)。最常见的例子是方法调用和web请求。 每个受支持的安全对象类型都有自己的拦截器类,它是AbstractSecurityInterceptor的子类。重要的是,当调用AbstractSecurityInterceptor时,如果主体已经通过身份验证,则SecurityContextHolder将包含一个有效的Authentication对象。 AbstractSecurityInterceptor为处理安全对象请求提供了一致的工作流程,通常为: 1、查找与当前请求相关的"配置属性"; 2、将安全对象,当前认证对象和配置属性提交给AccessDecisionManager以获得授权决定。 3、可选地在调用发生时变更认证对象。 4、允许继续调用安全对象(假设授予访问权限)。 5、如果配置了AfterInvocationManager,则在调用返回后调用它。如果调用引发异常,就不会调用AfterInvocationManager。 可以将"配置属性"看作是对AbstractSecurityInterceptor使用的类具有特殊含义的字符串,它们由框架中的ConfigAttribute接口表示。它们可能是简单的角色名称,或者具有更复杂的含义,具体取决于AccessDecisionManager实现的复杂程度。AbstractSecurityInterceptor使用SecurityMetadataSource配置,它使用这个源来查找安全对象的属性,通常这个配置对用户是隐藏的。配置属性将作为安全方法的注释或安全URL上的访问属性输入。例如,当在命名空间中看到类似 假设AccessDecisionManager决定允许请求,则AbstractSecurityInterceptor通常只会继续处理请求。话虽如此,在极少数情况下,用户可能希望用不同的身份验证对象替换SecurityContext中的身份验证对象,这是由AccessDecisionManager调用RunAsManager处理的。这在相当不寻常的情况下可能很有用,例如服务层方法需要调用远程系统并呈现不同的标识。由于Spring Security自动将安全标识符从一台服务器传播到另一台服务器(假设使用正确配置的RMI或HttpInvoker远程协议客户端),所以这可能很有用。 在继续调用安全对象之后,返回--这可能意味着完成了方法调用或过滤器链正在处理--AbstractSecurityInterceptor有最后一次处理调用的机会。在这个阶段,AbstractSecurityInterceptor可能会修改返回的对象。我们可能希望这样做,因为不能在"进入"安全对象调用的过程中做出授权决策。由于是高度可插拔的,AbstractSecurityInterceptor会将控制权交给AfterInvocationManager,以根据需要修改对象。这个类甚至可以完全替换这个对象,或者抛出一个异常,或者不按其选择的任何方式更改它。只有调用成功时才会执行调用后检查。如果发生异常,将跳过额外的检查。 安全拦截器和"安全对象"模型。 只有考虑采用全新方式拦截和授权请求的开发人员才需要直接使用安全对象。例如,可以构建一个新的安全对象来保护对消息系统的调用。任何需要安全性并提供拦截调用的方法(比如环绕通知语义的AOP)的东西都能够被转换为安全对象。大多数Spring应用程序将完全透明地使用目前支持的三种安全对象类型(AOPAliance MethodInvocation,AspectJ JoinPoint和Web请求FilterInvocation)。 Spring Security支持终端用户可能看到的异常消息的本地化。如果应用程序是为讲英语的用户设计的,则默认情况下,不需要执行任何操作,所有安全信息都是英文的。 所有异常消息都可以本地化,包括与认证失败和访问被拒绝有关的消息(授权失败)。异常和日志消息(包括不正确的属性,接口约束违规,使用不正确的构造函数,启动时间验证,调试级别日志记录)未做本地化,而是在Spring Security代码中以英文硬编码。 在spring-security-core-xx.jar中发送邮件,会有一个org.springframework.security包,其中包含messages.properties文件,以及一些常用语言的本地化版本。这可被ApplicationContext引用,因为Spring Security类实现了Spring的MessageSourceAware接口,并期望消息解析器在应用程序上下文启动时被依赖注入。通常,只需在应用程序上下文中注册一个bean来引用这些消息。如下: class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> messages.properties按照标准资源包进行命名,并表示Spring Security消息支持的默认语言,这个默认文件是英文的。 如果想自定义messages.properties文件或支持其他语言,则应复制该文件并相应地对其重命名,并在上面的bean定义中注册它。此文件中没有大量的消息密钥,因此本地化不应被视为主要举措。 Spring Security依靠Spring的本地化支持来实际查找适当的消息。为了达到此目的,必须确保传入请求中的语言环境存储在Spring的org.springframework.context.i18n.LocaleContextHolder中。Spring MVC的DispatcherServlet会自动为应用程序执行此操作,但由于在此之前调用了Spring Security的过滤器,因此需要在调用过滤器之前将LocaleContextHolder设置为包含正确的Locale。可以自己在过滤器中执行此操作(它必须位于web.xml中的Spring Security过滤器之前),也可以使用Spring的RequestContextFilter。 现在对Spring Security体系结构及其核心类进行高级概述,仔细研究一个或两个核心接口及其实现,特别是AuthenticationManager,UserDetailsService和AccessDecisionManager。了解它们如何配置以及如何操作非常重要。 AuthenticationManager只是一个接口,因此实现可以是我们选择的任何东西,但是它在实践中是如何工作的呢?如果需要检查多个身份验证数据库或不同身份验证服务(如数据库和LDAP服务器)的组合,该怎么办? Spring Security中的默认实现被称为ProviderManager,它将委托给一个已配置的AuthenticationProvider列表,而不是处理身份验证请求本身,依次查询每个列表,以查看是否可以执行身份验证。每个Provider要么抛出一个异常,要么返回一个完全填充的Authentication对象。还记得UserDetails和UserDetailsService吗?验证一个认证请求的最常见方法是加载相应的UserDetails,并根据用户输入的密码检查已加载的密码。这是DaoAuthenticationProvider使用的方法(见下文)。加载的UserDetails对象--特别是其包含的GrantedAuthority对象--将用于构建完整填充的Authentication对象,该对象从成功的认证中返回并存储在SecurityContext中。 如果使用命名空间,将在内部创建和维护ProviderManager实例,并通过使用名称空间身份验证提供程序元素向其添加提供者。在这种情况下,不应该在应用程序上下文中声明一个ProviderManager bean。但是,如果不使用命名空间,可以这样声明: class="org.springframework.security.authentication.ProviderManager"> 在上面的例子中,有3个Provider。它们按所示顺序(使用List暗示了这一点)进行尝试,每个提供者都可以尝试身份认证,或者通过简单地返回null跳过认证。如果所有实现都返回null,则ProviderManager将抛出ProviderNotFoundException。 身份认证机制(例如Web表单登录处理过滤器)注入了对ProviderManager的引用,并将调用它来处理它们的身份验证请求。需要的提供者有时可以与身份验证机制互换,而在其它时候,它们将依赖于特定的身份验证机制。例如,DaoAuthenticationProvider和LdapAuthenticationProvider与任何提交简单的用户名/密码认证请求的机制兼容,因此可以使用基于表单的登录或HTTP基本身份验证。另一方面,一些身份验证机制创建的身份验证请求对象只能由一种类型的AuthenticationProvider解释。例如,JA-SIG CAS使用服务票据的概念,因此只能通过CasAuthenticationProvider进行认证。不必太在意这一点,如果忘记注册合适的提供者,在尝试进行认证时,会收到ProviderNotFoundException。 默认情况下(从Spring Security 3.1开始),ProviderManager将从Authentication对象中清除任何敏感的凭据信息,该对象由成功的认证请求返回。这可以防止象密码之类的敏感信息保留时间过长。 例如,在使用用户对象缓存以提高无状态应用程序的性能时,这可能会导致问题。如果Authentication对象包含对缓存中对象的引用(例如UserDetails实例),并且该对象的凭据被删除,那么就不再能够根据缓存的值进行身份验证。如果使用缓存,就要考虑这一点。一个明显的解决方案是,首先在缓存实现中或在创建返回的Authentication对象的AuthenticationProvider中复制对象。或者禁用ProviderManager上的eraseCredentialsAfterAuthentication属性。 由Spring Security实现的最简单的AuthenticationProvider类是DaoAuthenticationProvider类,它也是该框架最早支持的类型之一。它利用UserDetailsService(作为DAO)来查找用户名,密码和GrantedAuthority。它通过比较UsernamePasswordAuthenticationToken中提交的密码与UserDetailsService加载的密码来验证用户。配置Provider非常简单: class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> PasswordEncoder是可选的。PasswordEncoder对UserDetails对象中的密码做编码和解码,该对象是从注入的UserDetailsService返回的。 大多数认证提供者利用UserDetails和UserDetailsService接口。UserDetailsService的接口是单一方法: UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 返回的UserDetails是一个有getter的接口,用于保证认证信息非空值的提供,如用户名,密码,授予的权限以及用户帐户是启用还是禁用。即使用户名和密码实际上未用作认证的一部分,大多数认证提供者也会使用UserDetailsService。它们可能会将UserDetails对象仅用于其GrantedAuthority信息,因为其它系统(如LDAP或X.509或CAS等)承担了实际验证凭据的责任。 鉴于UserDetailsService的实现非常简单,用户应该很容易使用自己选择的持久性策略来获取认证信息。话虽如此,Spring Security确实包含了一些有用的基础实现,下面介绍。 创建易用的自定义UserDetailsService实现很简单,它可以从选择的持久性引擎提取信息,但许多应用程序不需要这种复杂性。如果你正在构建原型应用程序,或者刚开始集成Spring Security,尤其当你不想花时间配置数据库或编写UserDetailsService实现时,情况尤其如此。对这种情况,一个简单选择是使用security命名空间的user-service元素: 也支持使用外部属性文件: 属性文件应该包含表单中的条目 username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] 例如 jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled bob=bobspassword,ROLE_USER,enabled Spring Security还包含一个可以从JDBC数据源获取认证信息的UserDetailsService。内部使用了Spring JDBC,因此避免了ORM的复杂性,只用于存储用户详细信息。如果应用程序使用了ORM工具,那么可能更愿意编写一个自定义UserDetailsService来重用已经创建的映射文件。回到JdbcDaoImpl,示例配置如下: class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"> 可以通过修改上面显示的DriverManagerDataSource来使用不同的关系数据库管理系统。也可以像使用其它Spring配置一样,使用从JNDI获取的全局数据源。 默认情况下,JdbcDaoImpl加载单个用户的权限,这里假设权限直接映射到用户。另一种方法是将权限分组,并将组分配给用户。有些人更喜欢用这种方法作为管理用户权限的手段。 Spring Security的PasswordEncoder接口用于执行密码的单向转换,以便安全地存储密码。现有的PasswordEncoder是单向转换,当密码转换需要双向(如存储用于向数据库验证的凭据)时,它并不适用。通常PasswordEncoder用于存储密码,该密码是做身份验证时,与用户提供的密码作比较。 在Spring Security 5.0之前,默认的PasswordEncoder是NoOpPasswordEncoder,它需要纯文本密码。你可能希望现在默认的PasswordEncoder与BCryptPasswordEncoder类似。但是,这忽略了3个现实世界的问题: 1、许多使用旧密码编码的应用程序无法轻松迁移; 2、密码存储的最佳实践将再次发生变化; 3、作为一个框架,Spring Security不能经常发生重大更改。 作为替代,Spring Security引入了DelegatingPasswordEncoder,它解决了所有的问题: 1、确保使用当前密码存储建议对密码编码; 2、允许以现代和传统格式验证密码; 3、允许将来升级编码。 可以使用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编码的密码列表,所有原始密码均为"password"。 {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>,第二个密码的PasswordEncoder 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。 <5>,最终密码的PasswordEncoder ID为sha256,encodedPassword为97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。匹配时,它将委托给StandardPasswordEncoder。 一些用户可能会担心存储格式是为潜在的黑客提供的。这不是问题,因为密码的存储不依赖于算法是一个秘密。此外,大多数格式在没有前缀的情况下很容易被攻击者识别。例如,BCrypt密码通常以$2a$开头。 传递给构造函数的idForEncode将决定使用哪个PasswordEncoder来对密码编码。在上面构建的DelegatingPasswordEncoder中,这意味着编码password的结果将委派给BCryptPasswordEncoder并以{bcrypt}为前缀。最终如下: {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 匹配基于构造函数中提供的{id}以及id到PasswordEncoder的映射。默认情况下,使用密码和未映射的id调用matches(CharSequence, String)的结果(包括空id)将导致IllegalArgumentException。这种行为可以使用DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)进行自定义。 通过使用id,可以匹配任何密码编码方式(password encoding),但要使用最现代的密码编码方式(password encoding)对密码编码(encode passwords)。这很重要,因为与加密不同,密码哈希的设计使得没有简单的方法可以恢复明文。由于无法恢复明文,因此难以迁移密码。尽管用户迁移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(); 这确实散列了存储的密码,但密码仍在内存和编译后的源代码中公开。因此,对于生产环境来说,它仍然不被认为是安全的。对于生产,应该在外部散列密码。 存储的其中一个密码没有标识时会发生以下错误。 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。 注意,NoOpPasswordEncoder被认为是不安全的。应使用DelegatingPasswordEncoder来支持安全的密码编码。 @Bean public static NoOpPasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } 如果使用的是XML配置,则可以公开一个ID为passwordEncoder的PasswordEncoder: class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/> 或者,可以使用正确的ID为所有密码加上前缀,并继续使用DelegatingPasswordEncoder。例如,使用的是BCrypt,则可以将密码从类似下面的: $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 转换至: {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 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实现使用[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实现使用[scrypt]算法对密码进行哈希处理。scrypt是一个故意运行缓慢的算法以抵御定制硬件上的密码破解,该算法需要大量内存。与其它自适应单向函数一样,它应该被优化到用大约1秒来验证系统的密码。 // Create an encoder with all the defaults SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result)); 有大量其它PasswordEncoder实现完全为了向后兼容而存在。其它都被弃用,表明不再被认为是安全的。但是,由于难以迁移现有的遗留系统,因此没有计划将其删除。 Spring Security已经增加了Jackson Support来支持Spring Security相关的类。这可以提高在处理分布式会话(即会话复制,Spring会话等)时序列化Spring Security相关类的性能。 要使用它,请将JacksonJacksonModules.getModules(ClassLoader)注册为Jackson Modules。 ObjectMapper mapper = new ObjectMapper(); ClassLoader loader = getClass().getClassLoader(); List mapper.registerModules(modules); // ... use ObjectMapper as normally ... SecurityContext context = new SecurityContextImpl(); // ... String json = mapper.writeValueAsString(context); 大多数SpringSecurity用户将在使用HTTP和Servlet API的应用程序中使用该框架。本部分,将了解Spring Security如何为应用程序的web层提供身份验证和访问控制功能。我们将查看名称空间的facade,并查看实际组装了哪些类和接口来提供web层安全性。在某些情况下,必须使用传统的bean配置来提供对配置的完全控制,因此我们还将看到如何在没有名称空间的情况下直接配置这些类。 Spring Security的Web基础架构完全基于标准的servlet过滤器。它内部不使用servlet或任何其它基于servlet的框架(如Spring MVC),因此它没有任何特定Web技术的强链接。它处理HttpServletRequest和HttpServletResponse,并不关心请求是来自浏览器,Web服务客户端,HttpInvoker还是AJAX应用程序。 Spring Security在内部维护一个过滤器链,其中每个过滤器都有特定的职责,并根据所需服务来从配置中添加或删除过滤器。过滤器的排序很重要,因为它们之间存在依赖关系。如果一直在使用命名空间配置,那么过滤器会自动为你配置,而且你不必显式定义任何Spring bean,但有时你可能想要完全控制安全过滤器链,可能因为你使用的是命名空间不支持的特性,或者使用的是自定义的类。 使用servlet过滤器时,显然需要在web.xml中声明它们,否则它们将被servlet容器忽略。在Spring Security中,过滤器类也是在应用程序上下文中定义的Spring bean,因此可以利用Spring的依赖注入工具和生命周期接口。Spring的DelegatingFilterProxy提供了web.xml和应用程序上下文之间的链接。 使用DelegatingFilterProxy时,在web.xml文件中会看到类似这样的内容: 请注意,过滤器实际上是DelegatingFilterProxy,而不是实际实现过滤器逻辑的类。DelegatingFilterProxy将Filter的方法委托给从Spring应用程序上下文获得的bean。这使得bean可以从Spring Web应用程序上下文生命周期支持和配置灵活性中受益。该bean必须实现javax.servlet.Filter,并且它必须具有与filter-name元素中相同的名称。 Spring Security的Web基础架构只能通过委派给FilterChainProxy的实例来使用,安全过滤器不应该被它们自己使用。理论上,可以在应用程序上下文文件中声明需要的每个Spring Security过滤器bean,并为每个过滤器添加一个DelegatingFilterProxy条目到web.xml,需要确保它们的顺序是正确的,如果有很多过滤器的话,会很麻烦,而且很快就将web.xml文件弄乱。FilterChainProxy让我们为web.xml添加单个条目,并完全处理应用程序上下文文件以管理Web安全性Bean。它使用DelegatingFilterProxy进行连线,就像上面的示例一样,但将filter-name的bean名称设置为"filterChainProxy"。然后,过滤器链在应用程序上下文中用相同的bean名称声明。 basicAuthenticationFilter, exceptionTranslationFilter, filterSecurityInterceptor" /> formLoginFilter, exceptionTranslationFilter, filterSecurityInterceptor" /> 命名空间元素filter-chain用于方便地设置应用程序中所需的安全筛选器链。它将特定的URL模式映射到根据filters元素中指定的bean名称构建的过滤器列表,并将它们组合到SecurityFilterChain类型的bean中。pattern属性采用Ant路径,应首先放最具体的URI。在运行时,FilterChainProxy将找到与当前Web请求匹配的第一个URI模式,并将filters属性指定的筛选器列表应用于该请求。过滤器将按照它们定义的顺序调用,因此可以完全控制应用于特定URL的过滤器链。 可能注意到已经在过滤器链中声明了两个SecurityContextPersistenceFilter(ASC是allowSessionCreation的缩写,属性为SecurityContextPersistenceFilter)。由于Web服务永远不会在将来的请求中显示jsessionid,因此为这些用户代理创建HttpSession将会是浪费。如果你有一个需要最大可伸缩性的高容量应用程序,我们建议你使用上述方法。对于较小的应用程序,使用单个SecurityContextPersistenceFilter(默认allowSessionCreation为true)可能就足够了。 请注意,FilterChainProxy不会在其配置的过滤器上调用标准过滤器生命周期方法。我们建议你使用Spring的应用程序上下文生命周期接口作为替代方案,就像你对其他任何Spring bean一样。 当我们查看如何使用命名空间配置设置网络安全时,我们使用了名称为"springSecurityFilterChain"的DelegatingFilterProxy。你现在应该能够看到这是由命名空间创建的FilterChainProxy的名称。 可以使用属性filters = "none"作为提供过滤器bean列表的替代方法。这将完全忽略来自安全过滤器链的请求模式。请注意,与此路径相匹配的任何内容都不会应用认证或授权服务,并且可以自由访问。如果你想在请求期间使用SecurityContext内容的内容,则它必须通过安全筛选器链。否则,SecurityContextHolder将不会被填充,并且内容将为空。 过滤器在链中定义的顺序非常重要。无论你实际使用哪些过滤器,顺序应如下所示: 1、ChannelProcessingFilter,因为它可能需要重定向到不同的协议。 2、SecurityContextPersistenceFilter,因此可以在Web请求的开头SecurityContextHolder中设置SecurityContext,并且可以复制对SecurityContext所做的任何更改到Web请求结束时的HttpSession(准备好用于下一个Web请求)。 3、ConcurrentSessionFilter,因为它使用SecurityContextHolder功能,需要更新SessionRegistry以反映来自主体的持续请求。 4、认证处理机制--UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等--以便可以修改SecurityContextHolder以包含有效的Authentication请求令牌。 5、SecurityContextHolderAwareRequestFilter,如果你使用它将感知Spring安全的HttpServletRequestWrapper安装到你的servlet容器中。 6、JaasApiIntegrationFilter,如果JaasAuthenticationToken在SecurityContextHolder中,则会将FilterChain处理为JaasAuthenticationToken中的Subject。 7、RememberMeAuthenticationFilter,这样如果没有更早的认证处理机制更新SecurityContextHolder,并且请求提供了一个允许RememberMe服务的cookie,则会记住一个合适的Authentication对象将放在那里。 8、AnonymousAuthenticationFilter,这样如果没有更早的认证处理机制更新SecurityContextHolder,则会在其中放置匿名Authentication对象。 9、ExceptionTranslationFilter,以捕获任何Spring安全性异常,以便可以返回HTTP错误响应或者启动适当的AuthenticationEntryPoint。 10、FilterSecurityInterceptor,以保护网络URI并在访问被拒绝时引发异常。 Spring Security定义了几个不同的模式处理过程,以确定如何测试并处理传入的请求URL。当FilterChainProxy决定使用哪个过滤链处理请求时,或当FilterSecurityInterceptor决定应用哪个安全约束处理请求时,就会使用这些模式过程。理解模式匹配的机制以及在针对定义的模式进行测试时应该使用什么URL值是很重要的。 Servlet规范定义了HttpServletRequest的几个属性,这些属性可以通过getter方法访问,我们可能希望与之匹配。这些属性包括contextPath,servletPath,pathInfo和queryString。Spring Security只关心应用程序中的路径安全,因此忽略了contextPath。不幸的是,servlet规范没有确切定义servletPath和pathInfo的值将包含哪些特定请求URI。例如,URL中的每个路径段都可能包含参数,如RFC 2396中所定义。规范没有明确说明这些值是否应该包含在servletPath和pathInfo中,并且不同的servlet容器之间的行为会有所不同。当应用程序部署在未从这些值中剥离路径参数的容器中时,攻击者可能会将它们添加到请求的URL中,从而导致模式匹配意外成功或失败。传入URL中的其它变体也是可能的。例如,它可能包含路径遍历序列(如/../)或多个正斜杠(//),这也可能导致模式匹配失败。有些容器在执行servlet映射之前将这些规范化,但另一些则不会。为防止出现类似问题,FilterChainProxy使用HttpFirewall策略检查并包装请求。未规范化的请求默认会自动被拒绝,路径参数和重复的斜杠会被删除以达到匹配的目的。因此,必须使用FilterChainProxy来管理安全过滤器链。请注意,容器对servletPath和pathInfo值进行解码,因此应用程序不应该有任何包含【;】的有效路径,因为这些部分将被删除以达到匹配目的。 如上所述,默认策略是使用Ant风格路径进行匹配,这对于大多数用户来说可能是最佳选择。该策略在AntPathRequestMatcher类中实现,该类使用Spring的AntPathMatcher对模式与连接的servletPath和pathInfo执行不区分大小写的模式匹配,而忽略queryString。 如果由于某种原因,你需要更强大的匹配策略,则可以使用正则表达式。策略实现是RegexRequestMatcher。 实际上,建议在服务层使用方法安全性,以控制对应用程序的访问,而不应完全依赖于在Web应用程序级别定义的安全约束。url会改变,并且很难考虑应用程序可能支持的所有url以及如何操作请求。应该尽量使用一些简单易懂的ant样式的路径,并始终尝试使用"deny-by-default"方法,在这种方法中,拥有一个定义为last且拒绝访问的所有通配符(/or)。 在服务层定义的安全性更健壮,更难绕过,所以应该总是利用Spring Security的方法安全选项。 HttpFirewall还通过拒绝HTTP响应报头中的新行字符来防止HTTP响应分裂。 默认情况下使用StrictHttpFirewall,此实现拒绝看似恶意的请求。如果觉得它太严格,那么可以定制拒绝哪些类型的请求。但是,重要的是要知道这样做会使应用程序容易受到攻击。例如,如果想利用Spring MVC的Matrix变量,则在XML中配置: class="org.springframework.security.web.firewall.StrictHttpFirewall" p:allowSemicolon="true"/> 通过公开StrictHttpFirewall实例,可以实现同样的效果。 @Bean public StrictHttpFirewall httpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowSemicolon(true); return firewall; } StrictHttpFirewall提供了一个有效HTTP方法的白名单,该白名单允许保护[跨站点跟踪(XST)]和[HTTP谓词篡改(Verb Tampering)]。默认的有效方法是"DELETE","GET","HEAD","OPTIONS","PATCH","POST"和"PUT"。如果应用程序需要修改有效的方法,可以配置一个自定义的StrictHttpFirewall实例。例如,以下仅允许使用HTTP的"GET"和"POST"方法: class="org.springframework.security.web.firewall.StrictHttpFirewall" p:allowedHttpMethods="GET,HEAD"/> 通过公开StrictHttpFirewall实例,可以实现同样的效果。 @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", "")来指定方法参数。 如果必须允许任何HTTP方法都可用(不推荐),则可以使用StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true),这会完全禁用HTTP方法验证。 如果使用的其它框架也是基于过滤器的,一定要确保Spring Security过滤器放在最开始的位置,这就能及时填充SecurityContextHolder以供其它过滤器使用。如使用SiteMesh来装饰网页,或像Wicket这样的网络框架,它使用过滤器来处理它的请求。 正如前面在命名空间部分中看到的,可以使用多个 在使用Spring Security的Web应用程序中总是会使用一些关键过滤器,因此,可先看看这些过滤器及其支持的类和接口。 在访问控制的地方,已经简单接触过FilterSecurityInterceptor,并且将其用于 class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> FilterSecurityInterceptor负责处理HTTP资源的安全性。它需要引用AuthenticationManager和AccessDecisionManager。它还提供了适用于不同HTTP的URL请求的配置属性。 可以通过两种方式配置FilterSecurityInterceptor。第一个(如上)使用 注意,FilterSecurityInterceptor.setSecurityMetadataSource()方法实际上返回一个FilterInvocationSecurityMetadataSource的实例。这是一个标记接口,它是SecurityMetadataSource的子类,这仅表示SecurityMetadataSource与FilterInvocation之间有关联。为简单起见,继续将FilterInvocationSecurityMetadataSource称为SecurityMetadataSource,因为它们之间的区别与大多数用户的关系不大。 SecurityMetadataSource通过将请求URL与pattern属性匹配来获取特定FilterInvocation的配置,这与命名空间配置的方式相同。默认情况下,所有表达式都将将视为Ant路径样式,正则表达式也可以支持更复杂的情况。["request-matcher"]属性用于指定使用的模式类型,在同一个定义中不能混合使用ant路径和正则表达式。例如,前面的配置可使用正则表达式来替换Ant路径: class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> 所有模式都会按照它们定义的顺序进行估算。因此,在属性列表中定义更具体的模式比定义通用的模式更重要。如上面的示例,其中更具体的[/secure/super/]模式比不太具体的[/secure/]模式更靠前。如果相反,则始终先匹配上[/secure/]模式,而永远不会匹配到[/secure/super/]了。 在安全筛选器堆栈中,ExceptionTranslationFilter位于FilterSecurityInterceptor的上方,它本身并不做任何实际的安全性校验,只是处理安全拦截器抛出的异常并提供合适的HTTP响应。 class="org.springframework.security.web.access.ExceptionTranslationFilter"> class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> class="org.springframework.security.web.access.AccessDeniedHandlerImpl"> 如果未经过身份认证的用户请求安全的HTTP资源,则会调用AuthenticationEntryPoint。安全拦截器抛出一个适当的AuthenticationException或AccessDeniedException异常,并进一步向下调用堆栈,触发入口点上的commence()方法。这就完成了向用户显示适当响应的工作,以便开始进行身份验证。这里使用了一个LoginUrlAuthenticationEntryPoint,它将请求重定向到不同的URL(通常是登录页面)。实际的实现取决于在应用程序中使用的认证机制。 如果用户已经通过身份验证,并且试图访问受保护的资源,会发生什么情况?在正常情况下,这不应该发生,因为应用程序工作流应只为用户提供有权访问的操作。例如,对没有管理员角色的用户隐藏管理页面的HTML链接。但不能依赖隐藏链接的安全性,因为用户可能直接输入URL以试图绕过这些限制,或他们可以修改一个RESTful的URL改变一些参数值。应用程序必须受到保护以免出现这些情况,否则它肯定会变得不安全。通常可使用简单的Web层安全性将约束应用于基本URL,并在服务层接口上使用更具体的基于方法的安全性来确定允许的内容。 如果用户已经提供认证但还是抛出了AccessDeniedException异常,那么这意味着用户正在调用他们没有足够权限的操作。在这种情况下,ExceptionTranslationFilter将调用第二个策略,即AccessDeniedHandler。默认情况下,系统会使用AccessDeniedHandlerImpl类,仅向客户端发送403(禁止)响应。或者,也可以显式配置一个实例(如上例),并设置一个错误页面URL,它将把请求转发给这个URL。这个URL可以导向简单的"拒绝访问"页面(如JSP),也可以是更复杂的处理程序(如MVC控制器)。当然,还可以定义接口并使用自己的实现。 在使用命名空间配置时,也可以提供自定的AccessDeniedHandler。 ExceptionTranslationFilter的另一个功能是在调用AuthenticationEntryPoint之前保存当前请求,以允许用户通过身份认证后恢复请求。一个典型的例子是用户使用表单登录,然后通过默认SavedRequestAwareAuthenticationSuccessHandler重定向到原始URL。 RequestCache封装了存储和获取HttpServletRequest实例所需的功能。默认情况下使用HttpSessionRequestCache将请求存储在HttpSession中。当用户被重定向到原始URL时,RequestCacheFilter可以恢复缓存中保存的请求。 正常情况下,不需要修改任何此功能,但是保存请求处理是一种"最佳工作"方法,并且可能存在默认配置无法处理的情况。这些接口的使用使它成为完全可插拔式的。 在技术概览一章中已介绍此过滤器的用途,先看看如何将它配置为与FilterChainProxy一起使用,基本配置只需要bean本身。 class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/> 如前所见,这个过滤器有两个主要任务。它负责在HTTP请求之间存储SecurityContext内容,并在请求完成时清除SecurityContextHolder。清除ThreadLocal中存储的上下文信息是非常重要的,否则该线程可能会被放入servlet容器的线程池中,而特定用户的安全上下文仍然是附加在该线程上的。此线程可能会在稍后阶段使用,并使用错误的凭据执行操作。 从Spring Security 3.0开始,加载和存储安全上下文的工作就委托给一个单独的策略接口: public interface SecurityContextRepository { SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder); void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response); } HttpRequestResponseHolder只是传入请求和响应对象的容器,允许实现类用包装类替换它们,返回的内容将被传递给过滤器链。 默认实现是HttpSessionSecurityContextRepository,它将安全性上下文作为HttpSession属性存储。该实现最重要的配置参数是allowSessionCreation属性,默认为true,意思是允许该类创建会话以存储经过认证的用户的安全上下文(如果用户未经认证,并且安全上下文的内容也未改变,则不会创建会话)。如果不想创建会话,则可将属性设置为false: class="org.springframework.security.web.context.SecurityContextPersistenceFilter"> 或者,可以提供一个NullSecurityContextRepository实例,这是一个null对象的实现,即使在请求期间已经创建了一个会话它也不会存储安全上下文。 现已经看到了Spring Security Web配置中总是存在3种主要的过滤器。这是由 1、使用登录页面的URL配置LoginUrlAuthenticationEntryPoint,并在ExceptionTranslationFilter上设置它。 2、实现登录页面(使用JSP或MVC控制器)。 3、在应用程序上下文中配置UsernamePasswordAuthenticationFilter的实例。 4、将过滤器bean添加到过滤器链代理中(注意顺序)。 登录表单只包含username和password输入字段,并发布到由过滤器监控的URL(默认情况下为/login)。基本的过滤器配置如下所示: "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> 过滤器会调用配置的AuthenticationManager处理每个认证请求。认证成功或失败后的目的地址分别由AuthenticationSuccessHandler和AuthenticationFailureHandler策略接口控制。该过滤器允许设置这些属性,以便可以完全自定义行为。现在提供了一些标准实现,如SimpleUrlAuthenticationSuccessHandler,SavedRequestAwareAuthenticationSuccessHandler,SimpleUrlAuthenticationFailureHandler,ExceptionMappingAuthenticationFailureHandler和DelegatingAuthenticationFailureHandler。 如果认证成功,则生成的Authentication对象将被放置到SecurityContextHolder中。然后,调用配置的AuthenticationSuccessHandler将用户重定向或转发到适当的目的地址。默认情况下使用SavedRequestAwareAuthenticationSuccessHandler,这意味着用户在被要求登录之前将被重定向到他们请求的原始地址处。 注意,ExceptionTranslationFilter将缓存用户发出的原始请求,当用户进行身份验证时,请求处理程序使用这个缓存的请求来获取原始URL并重定向到它,然后重新构建原始请求并将其用作替代URL。 如果认证失败,则会调用配置的AuthenticationFailureHandler。 本节介绍Spring Security如何与Servlet API集成。servletapi-xml示例应用程序演示了每种方法的用法。 以下部分描述Spring Security集成的Servlet 3方法。 HttpServletRequest.authenticate(HttpServletRequest的,HttpServletResponse的)方法可用于确保用户通过身份认证。如果它们未通过认证,则配置的AuthenticationEntryPoint将用于请求用户进行认证(即重定向到登录页面)。 HttpServletRequest.login(String,String)方法可用于使用当前AuthenticationManager对用户进行身份认证。例如,以下内容会尝试使用用户名"user"和密码"password"进行认证: try { httpServletRequest.login("user","password"); } catch(ServletException e) { // fail to authenticate } 可以使用HttpServletRequest.logout()方法记录当前用户。 通常这意味着SecurityContextHolder将被清除,HttpSession将失效,任何"Remember Me"认证将被清除等。但是,配置的LogoutHandler实现将根据Spring Security配置而有所不同。请注意,在调用HttpServletRequest.logout()之后,仍需编写响应。通常这会涉及重定向到欢迎页面。 确保凭据将传播到新线程的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); } } }); 如果正在使用基于Java的配置,则可以开始使用了。如果使用的是XML配置,则需要一些更新。第一步是确保更新web.xml以至少使用3.0架构,如下所示: xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> 接下来,需要确保springSecurityFilterChain已设置为处理异步请求。 org.springframework.web.filter.DelegatingFilterProxy 现在,Spring Security将确保SecurityContext也在异步请求上传播。 其中大部分都是内置到Servlet规范中的,但是有一点调整,Spring Security确实能够正确地处理异步请求。在Spring Security 3.2之前,只要提交HttpServletResponse,SecurityContextHolder中的SecurityContext就会自动保存。这可能会导致异步环境中的问题。例如,请考虑以下几点: httpServletRequest.startAsync(); new Thread("AsyncThread") { @Override public void run() { try { // Do work TimeUnit.SECONDS.sleep(1); // Write to and commit the httpServletResponse httpServletResponse.getOutputStream().flush(); } catch (Exception e) { e.printStackTrace(); } } }.start(); 问题是这个Thread对于Spring Security来说是未知的,所以SecurityContext不会传播给它。这意味着当提交HttpServletResponse时,没有SecuriytContext。当Spring Security在提交HttpServletResponse自动保存SecurityContext时,它会丢失登录用户。 从3.2版本开始,只要HttpServletRequest.startAsync()被调用,就不会再自动保存SecurityContext来提交HttpServletResponse。 以下部分描述Spring Security集成的Servlet 3.1方法。 HttpServletRequest.changeSessionId()是在Servlet 3.1及更高版本中防止Session Fixation攻击的默认方法。 基本身份验证和摘要身份验证是在Web应用程序中流行的备用认证机制。基本身份验证通常用于无状态客户端,这些客户端在每次请求时都会传递凭证。将其与基于表单的身份验证结合使用非常常见,其中应用程序通过基于浏览器的用户界面和web服务使用。但是,基本身份验证将密码以纯文本的形式传输,因此它应该仅在加密传输层(如HTTPS)上使用。 BasicAuthenticationFilter负责处理HTTP头中的基本认证凭证。这可用于验证Spring Remoting协议(例如Hessian和Burlap)以及普通浏览器用户代理(如FF和IE)发出的调用。HTTP基本身份验证的标准由RFC 1945第11节定义,BasicAuthenticationFilter符合这个RFC。基本身份验证是一种很有吸引力的身份验证方法,因为它广泛部署在用户代理中,实现非常简单(它只是HTTP头中指定的username:password的Base64编码)。 要实现HTTP基本身份验证,需要向过滤器链添加一个BasicAuthenticationFilter。应用程序上下文应包含BasicAuthenticationFilter及其所需的协作者: class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter"> class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint"> 配置的AuthenticationManager处理每个认证请求,如果认证失败,将使用配置的AuthenticationEntryPoint来重试身份验证过程。通常需要结合BasicAuthenticationEntryPoint使用过滤器,该EP返回带有适当标头的401响应,以重试HTTP基本身份验证。如果认证成功,则像往常一样将生成的Authentication对象放入SecurityContextHolder。 如果认证事件成功,或者由于HTTP标头不包含支持的认证请求而未尝试认证,则过滤器链将继续正常运行。只有当身份验证失败并调用AuthenticationEntryPoint时,过滤器链才会中断。 DigestAuthenticationFilter能够处理HTTP头中提供的摘要式认证凭证。摘要式认证试图解决基本认证的许多缺点,特别是确保凭证不会以明文的形式在网络上传播。许多用户代理支持摘要式认证,包括FF和IE。管理HTTP摘要身份验证的标准由RFC 2617定义,RFC 2617更新了RFC 2069规定的摘要身份验证标准的早期版本。大多数用户代理实现了RFC 2617。 Spring Security的DigestAuthenticationFilter与RFC 2617规定的"auth"保护质量(qop)兼容,这也提供了与RFC 2069的向后兼容性。如果要使用未加密的HTTP(即无TLS/HTTPS)并希望最大限度地提高认证过程的安全性,则摘要式认证是一种更具吸引力的选择。事实上,如RFC 2518第17.1节所述,摘要式认证是WebDAV协议的强制性要求。 注意,在现代应用程序中不应该使用摘要,因为它被认为是不安全的。最明显的问题是必须以明文、加密或MD5格式存储密码。所有这些存储格式都被认为是不安全的。应该使用单向自适应密码散列替代(即bCrypt、PBKDF2、SCrypt等)。 摘要身份验证的中心是"nonce",这是服务器生成的值。Spring Security的"nonce"采用以下格式: 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有一个属性,指定用于生成nonce令牌的key,以及一个用于确定过期时间的nonceValiditySeconds(默认值300,等于5分钟)。尽管nonce是有效的,但要通过连接各种字符串,包括用户名,密码,nonce,正在请求的URI,客户端生成的nonce(只是用户代理生成每个请求的随机值),领域名等,然后执行MD5哈希来计算摘要。服务器和用户代理都会执行此摘要计算,如果它们所包含的值不同(例如密码),则会生成不同的哈希码。在Spring Security实现中,如果服务器生成的nonce仅仅过期了(但该摘要在其他方面是有效的),DigestAuthenticationEntryPoint将发送一个"stale=true"标头,告诉用户代理不需要打扰用户(因为密码和用户名等是正确的),只需使用新的nonce重试即可。 DigestAuthenticationEntryPoint的nonceValiditySeconds参数的适当值取决于应用程序。非常安全的应用程序应该注意到,拦截的身份验证头可以用于模拟主体,直到nonce中包含的expirationTime到期为止。这是选择恰当设置的关键原则,但对于非常安全的应用程序而言,不首先在TLS/HTTPS上运行实例是异常的。 由于摘要身份验证的实现比较复杂,因此用户代理经常会出问题。例如,IE未能对同一会话中的后续请求显示"opaque"令牌。因此Spring Security过滤器将所有状态信息封装到"nonce"令牌中。 要实现HTTP摘要认证,有必要在过滤器链中定义DigestAuthenticationFilter。应用程序上下文将需要定义DigestAuthenticationFilter及其所需的协作者: "org.springframework.security.web.authentication.www.DigestAuthenticationFilter"> "org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint"> 需要配置UserDetailsService,因为DigestAuthenticationFilter必须能够直接访问用户的明文密码。如果在DAO中使用了编码过的密码,那么摘要身份验证将失效。DAO协作者和UserCache通常直接与DaoAuthenticationProvider共享。authenticationEntryPoint属性必须是DigestAuthenticationEntryPoint,以便DigestAuthenticationFilter可以获取正确的realmName和key以进行摘要计算。 与BasicAuthenticationFilter类似,如果认证成功,认证请求令牌将被放入SecurityContextHolder中。如果认证事件成功,或者由于HTTP头未包含摘要认证请求而未尝试认证,则过滤器链将继续正常运行。只有当身份验证失败并调用AuthenticationEntryPoint时,过滤器链才会中断,如前段所述。 摘要认证的RFC提供了一系列附加特性以进一步提高安全性。例如,每个请求都可以更改nonce。尽管如此,Spring Security实现的目的还是最小化实现的复杂性(以及可能出现的用户代理不兼容),并避免需要存储服务器端状态。 基于安全性考虑,不建议使用此功能。 RememberMe或持久登录身份验证是指web站点能够在会话之间记住主体的身份。这通常通过向浏览器发送cookie来完成,在以后的会话中检测到cookie并实现自动登录。Spring Security为这些操作提供了必要的hook,并且有两个具体的RememberMe的实现。一个使用hash来保存基于cookie的令牌的安全性,另一个使用数据库或其它持久存储机制来存储生成的令牌。 请注意,这两种实现都需要UserDetailsService。如果不使用UserDetailsService的身份验证提供者(例如,LDAP提供者),那么除非应用程序上下文中还有UserDetailsService的bean,否则它将无法工作。 这种方法使用散列来实现有用的RememberMe策略。在交互验证成功后,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 因此,RememberMe仅在指定的时间段内有效,并且用户名,密码和密钥不变。值得注意的是,这会存在潜在的安全问题,因为捕获的remember-me令牌在令牌过期之前,任何用户代理都可以使用它,这与摘要身份验证的问题相同。如果有人知道令牌已被捕获,他们可以轻松更改其密码,并立即使所有已发布的remember-me令牌失效。如果需要更强的安全性,则应使用下一节中介绍的方法。或者,根本就不用RememberMe服务。 可通过添加 ... 通常会自动选择UserDetailsService。如果应用程序环境中有多个应用,则需要指定哪一个应与user-service-ref属性一起使用,其中的值是UserDetailsService的名称。 此方法基于文章http://jaspan.com/improved_persistent_login_cookie_best_practice进行了一些小修改。要在命名空间配置中使用这种方法,你需要提供一个数据源参考: ... 数据库应该包含使用以下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) 本节讨论Spring Security的跨站点请求伪造(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-urlencoded amount=100.00&routingNumber=1234&account=9876 现在假设你在银行的网站上进行身份验证,然后在没有注销的情况下访问一个恶意网站,该恶意网站包含一个HTML页面,其格式如下:
name="amount" value="100.00"/>
name="routingNumber" value="evilsRoutingNumber"/>
name="account" value="evilsAccountNumber"/>
value="Win Money!"/> 由于你喜欢赢钱,所以你点击提交按钮。在这个过程中,你无意间将100美元转让给恶意用户。发生这种情况的原因是,虽然恶意网站无法看到你的Cookie,但与你的银行相关的Cookie仍会与请求一起发送。 最糟糕的是,整个过程可以使用JavaScript自动完成。这意味着你甚至不需要点击按钮。那么我们如何保护自己免受此类攻击呢? 问题是来自银行网站的HTTP请求和来自恶意网站的请求完全相同。这意味着无法拒绝来自恶意网站的请求,而只允许来自银行网站的请求。为了防止CSRF攻击,需要确保在请求中有恶意站点无法提供的内容。 一种解决方案是使用[Synchronizer Token Pattern]。该方案要求除了会话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-urlencoded amount=100.00&routingNumber=1234&account=9876&_csrf= 注意,我们添加了带有随机值的_csrf参数。现在,恶意网站将无法猜测_csrf参数的正确值(必须在恶意网站上显式提供),并且当服务器比较实际令牌与预期令牌时,传输将失败。 什么时候应该使用CSRF保护?建议是针对普通用户可以通过浏览器访问的任何请求都使用CSRF保护。如果你只创建非浏览器客户端使用的服务,则可能希望禁用CSRF保护。 一个常见的问题是"我需要保护javascript发出的JSON请求吗?",简单的回答是,视情况而定。但是,必须非常小心,因为有CSRF漏洞可能会影响JSON请求。例如,恶意用户可以用以下表单使JSON创建CSRF:
value="Win Money!"/> 这会生成以下JSON结构。 { "amount": 100, "routingNumber": "evilsRoutingNumber", "account": "evilsAccountNumber", "ignore_me": "=test" } 如果应用程序未验证Content-Type,那么它将暴露于此漏洞利用。根据设置不同,仍可通过更新URL后缀以".json"结尾来利用验证Content-Type的Spring MVC应用程序,如下所示:
value="Win Money!"/> 如果应用程序是无状态的呢?这也不一定意味着程序受到了保护。事实上,如果用户不需要在Web浏览器中对给定的请求执行任何操作,那么他们仍然容易受到CSRF攻击。 例如,假设应用程序使用一个自定义cookie,其中包含身份验证的所有状态,而不是JSESSIONID。在进行CSRF攻击时,定制cookie将以与前面示例中发送JSESSIONID cookie相同的方式与请求一起发送。 使用基本身份验证的用户也很容易受到CSRF攻击,因为浏览器会自动在任何请求中包含用户名和密码,就像前面示例中发送JSESSIONID cookie一样。 那么,使用Spring Security来保护站点免受CSRF攻击需要采取哪些步骤?下面概述了使用Spring Security的CSRF保护的步骤: 1、Use proper HTTP verbs 2、Configure CSRF Protection 3、Include the CSRF Token 防止CSRF攻击的第一步是确保网站使用正确的HTTP动词。具体来说,在Spring Security的CSRF支持可用之前,要确保应用程序正在使用PATCH,POST,PUT或DELETE来修改状态。 这不是Spring Security支持的限制,而是正确的CSRF预防的一般要求。原因是在HTTP的GET中包含私有信息会导致信息泄露。 下一步是在应用程序中包含Spring Security的CSRF保护。有些框架通过让用户的会话失效来处理无效的CSRF令牌,但这会导致它自己的问题。相反,默认情况下,Spring Security的CSRF保护将产生HTTP的403错误而拒绝访问。这可以通过配置AccessDeniedHandler以不同的方式处理InvalidCsrfTokenException来定制。 从Spring Security 4.0开始,默认情况下使用XML配置启用CSRF保护。如果想禁用CSRF保护,则可以使用下面的XML配置。 默认情况下,CSRF保护是通过Java代码启用的。如果想禁用CSRF,可以使用下面的Java代码。 @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable(); } } 最后一步是确保在所有PATCH,POST,PUT和DELETE方法中包含CSRF令牌。一种方法是使用_csrf请求属性来获得当前的CsrfToken。下面是用JSP做的例子: 更简单的方法是使用Spring Security JSP标签库中的csrfInput标签。 如果使用的是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,它为RESTful方式处理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,其中all请求将包含该令牌。限定哪些请求接收令牌的能力有助于防止将CSRF令牌泄露给第三方。 可能会有用户想要将CsrfToken保存在Cookie中。默认情况下,CookieCsrfTokenRepository将写入名为XSRF-TOKEN的Cookie,并从名为X-XSRF-TOKEN的标头或HTTP参数_csrf中读取。这些默认值来自[AngularJS]。 可以使用以下方式在XML中配置CookieCsrfTokenRepository: class="org.springframework.security.web.csrf.CookieCsrfTokenRepository" p:cookieHttpOnly="false"/> 可以使用以下方法配置CookieCsrfTokenRepository: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } } 注意,示例显式设置cookieHttpOnly=false。这是允许JavaScript(即AngularJS)读取它的必要条件。如果不需要直接使用JavaScript读取cookie,则省略cookieHttpOnly=false(改为使用new CookieCsrfTokenRepository())以提高安全性。 实现CSRF时有一些注意事项。 一个问题是预期的CSRF令牌存储在HttpSession中,因此一旦HttpSession过期,配置的AccessDeniedHandler将收到InvalidCsrfTokenException异常。如果使用默认的AccessDeniedHandler,浏览器将获得HTTP的403状态码并显示错误消息。 减轻活动用户超时的一种简单方法是使用一些JavaScript,让用户知道他们的会话即将到期。用户可以单击按钮继续刷新会话。 另外,可指定一个定制的AccessDeniedHandler,以用任何方式处理InvalidCsrfTokenException异常。 最后,可以将应用程序配置为使用不会过期的CookieCsrfTokenRepository。这并不像使用会话那样安全,但在许多情况下已经足够了。 为了防止伪造登录请求,登录表单也应该受到保护,以防止CSRF攻击。由于CsrfToken存储在HttpSession中,这意味着一旦访问CsrfToken令牌,就会创建一个HttpSession。虽然这在RESTful/stateless的架构中听起来很糟糕,但实际上,状态是实现实际安全性所必需的。如果没有状态,如果令牌失效,我们就无能为力了。实际上CSRF令牌的大小相当小,对架构的影响微不足道。 保护登录表单的常用技术是在提交表单之前使用JavaScript函数获取有效的CSRF令牌。这样做,就不需要考虑会话超时了,因为会话是在提交表单之前创建的(假设没有配置CookieCsrfTokenRepository),因此用户可以停留在登录页面上,并在需要时提交用户名/密码。为了实现这一点,可以利用Spring Security提供的CsrfTokenArgumentResolver,并像这里描述的那样公开一个端点。 添加CSRF将更新LogoutFilter以仅使用HTTP的POST,这可确保注销需要一个CSRF令牌,并让恶意用户无法强制注销用户。 一种方法是使用表单注销。如果想要一个链接,可以使用JavaScript来让链接执行一个POST(可能是在一个隐藏的表单上)。对于禁用JavaScript的浏览器,可以选择让链接将用户带到注销确认页面,该页面执行POST操作。 如果确实想在注销时使用HTTP的GET方式,也可以这样做,但不推荐。例如,以下代码执行注销,并使用任何HTTP方法请求/logout: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")); } } 使用Multipart/form-data的CSRF保护有两种选择,每个选项都有它的权衡。 第一种是确保在SpringSecurity过滤器之前指定MultipartFilter,这意味着没有授权调用MultipartFilter,那么任何人都可以往服务器上放临时文件。但是,只有授权用户才能提交由应用程序处理的文件。一般来说,这是推荐的方法,因为上传临时文件对大多数服务器来说无影响。 在代码中为确保在Spring Security过滤器之前指定MultipartFilter,可以覆盖beforeSpringSecurityFilterChain,如下所示: public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer { @Override protected void beforeSpringSecurityFilterChain(ServletContext servletContext) { insertFilters(servletContext, new MultipartFilter()); } } 为确保在XML中Spring Security过滤器之前指定MultipartFilter,可以使MultipartFilter的 如果不允许未经授权的用户上传临时文件,另一种选择是将MultipartFilter放在Spring Security过滤器之后,并将CSRF作为查询参数包含在表单的action属性中。 这种方法的缺点是查询参数可能被泄漏,最好将敏感数据放在正文或标头中,以确保不会泄漏。 HiddenHttpMethodFilter应放在Spring Security过滤器之前。一般来说,这是正确的,但在防范CSRF攻击时可能会产生额外的影响。 注意,HiddenHttpMethodFilter只覆盖POST上的HTTP方法,所以这实际上不会导致任何问题。但是,确保在Spring Security过滤器之前放置它仍然是最佳实践。 Spring Security的目标是提供保护用户免受攻击的默认设置。这并不意味着必须接受它的所有默认值。 例如,可以自定义一个CsrfTokenRepository来覆盖CsrfToken的存储方式。 还可以指定一个自定义的RequestMatcher来确定哪些请求受到CSRF保护(例如,可能不关心注销是否被利用)。简而言之,如果Spring Security的CSRF保护行为不符合要求,就可以自定义。 Spring框架为CORS提供了一流支持。由于pre-flight请求不会包含任何Cookie(即JSESSIONID),所以必须在Spring Security之前处理CORS。如果请求中未包含任何cookie并且把Spring Security放在了最开始,那么用户请求就被认为是未认证的(因为请求中没有cookie)并会遭到拒绝。 确保首先处理CORS的最简单方法是使用CorsFilter。用户可以通过使用以下命令提供CorsConfigurationSource来将CorsFilter与Spring Security集成在一起: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // by default uses a Bean by the name of corsConfigurationSource .cors().and() ... } @Bean CorsConfigurationSource 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 { @Override protected 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格式 ... 本节讨论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请求中添加Strict-Transport-Security。 可以定制特定的标头。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 具体来说,你希望所有的默认标头都具有以下定制信息: 1、X-Frame-Options允许来自同一个域中的任何请求; 2、HTTP Strict Transport Security(HSTS)不会被添加到响应中。 可用以下代码完成此操作: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .frameOptions().sameOrigin() .httpStrictTransportSecurity().disable(); } } 或者,XML配置: 如果不想添加默认设置,并希望对应该使用的内容进行显式控制,则可以禁用默认设置。 代码,以下内容仅会添加Cache Control。 @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,仅会添加Cache Control。 如有必要,可以使用代码禁用所有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 简单添加没有子元素的 同样,可以通过代码仅启用缓存控制: @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 { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry .addResourceHandler("/resources/**") .addResourceLocations("/resources/") .setCachePeriod(31556926); } // ... } 过去,包括IE在内的浏览器会使用内容嗅探来猜测请求的内容类型。这允许浏览器通过猜测没有指定内容类型的资源的内容类型来改善用户体验。例如,如果浏览器遇到没有指定内容类型的JavaScript文件,它将能够猜测内容类型并执行它。 内容嗅探的问题在于,它允许恶意用户使用polyglots(即该文件作为多种内容类型都是有效的)来执行XSS攻击。例如,某些网站可能允许用户向网站提交有效的postscript文档并查看它。恶意用户可能会创建postscript文件,同时也是一个有效的JavaScript文件,并使用它执行XSS攻击。 可以通过向响应添加以下标头来禁用内容嗅探: X-Content-Type-Options: nosniff 就像缓存控制元素一样,在没有子元素的情况下使用 默认情况下,代码中会添加X-Content-Type-Options标头。如果想更好地控制标头,可以显式指定内容类型: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .defaultsDisabled() .contentTypeOptions(); } } 当输入银行网站时,是输入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。这就大大降低了中间人发动攻击的可能性。 将站点标记为HSTS主机的一种方法是将主机预加载到浏览器中。另一种方法是将"Strict-Transport-Security"标头添加到响应中。例如,以下内容将指示浏览器将域名视为一年的HSTS主机(一年大约有31536000秒): Strict-Transport-Security: max-age=31536000 ; includeSubDomains 可选的includeSubDomains指令指示Spring Security将子域(即secure.mybank.example.com)也视为HSTS域。 与其它标头一样,Spring Security默认添加HSTS。可以使用 include-subdomains="true" max-age-seconds="31536000" /> 同样,代码启用HSTS标头: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .httpStrictTransportSecurity() .includeSubdomains(true) .maxAgeSeconds(31536000); } } HTTP Public Key Pinning(HPKP)是一项安全功能,它告诉Web客户端将特定的加密公钥与特定的Web服务器相关联,以防止中间人(MITM)用伪造证书攻击。 为确保在TLS会话中使用的服务器公钥的真实性,该公钥被封装到通常由证书颁发机构(CA)签署的X.509证书中。Web客户端(如浏览器)信任很多这样的CA,它们都可以为任意域名创建证书。如果攻击者能够损害单个CA,他们就可以对各种TLS连接执行MITM攻击。HPKP可以通过告诉客户端哪个公钥属于某个Web服务器来规避HTTPS协议的这种威胁。HPKP是一种首次使用信任(TOFU)的技术。当Web服务器第一次通过特殊的HTTP头告诉客户端哪些公钥属于它时,客户端在给定的一段时间内存储这些信息。当客户端再次访问服务器时,它需要一个包含公钥的证书,该公钥的指纹已经通过HPKP获知了。如果服务器提供的是未知的公钥,则客户端应向用户发出警告。 为站点启用此功能非常简单,只需在通过HTTPS访问站点时返回[Public-Key-Pins]的HTTP标头即可。例如,以下内容将指示用户代理仅向指定的URI报告pin验证失败(通过[report-uri]指令),有2个pin: 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 pin验证失败报告是标准的JSON结构,可由Web应用程序自己的API或公开托管的HPKP报告服务来捕获,如[REPORT-URI]。 可选的includeSubDomains指令指示浏览器也使用给定pin验证子域。 与其他标头相反,Spring Security默认不添加HPKP,可以使用 include-subdomains="true" report-uri="http://example.net/pkp-report"> 同样,可以使用代码启用HPKP标头: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .httpPublicKeyPinning() .includeSubdomains(true) .reportUri("http://example.net/pkp-report") .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="; } } 将网站加到frame中可能是一个安全问题。例如,使用诱人的CSS样式,用户可能会被欺骗去点击他们不打算点击的东西(视频)。又如,登录银行的用户可能会点击一个按钮,授予对其他用户的访问权限。这种攻击被称为[点击劫持]。 有多种方法可以减少点击劫持攻击。例如,为了保护传统浏览器免受点击劫持攻击,可以使用断章取义的代码(Frame Breaking Code)。虽不完美,但对于传统浏览器来说,这是最好的选择。 一个更现代的解决点击劫持的方法是使用X-Frame-Options报头: X-Frame-Options: DENY X-Frame-Options响应头会使浏览器禁止在frame中显示那些响应中带此标头的任何站点。默认情况下,Spring Security会禁用iframe中的显示功能。 可以使用 同样,可以使用代码设置: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .frameOptions() .sameOrigin(); } } 一些浏览器内置了对过滤反射XSS攻击的支持,这绝不是万无一失的,但确实有助于XSS的保护。 默认情况下,过滤通常处于启用状态,因此添加标头通常会确保启用它并指示浏览器在检测到XSS攻击时该执行什么操作。例如,该过滤器可能会尝试以最小侵入方式更改内容,以继续呈现所有内容。有时候,这种类型的替换本身就可能成为XSS漏洞。相反,最好是阻塞内容而不是尝试修复它。为此,可以添加以下标头: X-XSS-Protection: 1; mode=block 默认包含该标头,可以根据需要定制,例如: 同样,可用代码自定义XSS保护: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .xssProtection() .block(false); } } 内容安全策略(CSP)是一种机制,web应用程序可以利用它来减轻内容注入漏洞,例如跨站点脚本编制(XSS)。CSP是一种声明性策略,它为web应用程序作者提供了一种工具,用于声明并最终通知客户端(用户代理)web应用程序希望从中加载资源的源。 Web应用程序可以通过在响应中包含以下HTTP标头之一来使用CSP: Content-Security-Policy Content-Security-Policy-Report-Only 每个标头都用作向客户端交付安全策略的机制。安全策略包含一组安全策略指令(例如script-src和object-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/ Violation reports是标准的JSON结构,可以通过Web应用程序自己的API或公开托管的CSP违规报告服务来捕获,如[REPORT-URI]。 Content-Security-Policy-Report-Only标头为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/ 可以使用带 policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" /> 要启用CSP的"report-only"标头,按如下配置: policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" report-only="true" /> 同样,可用代码启用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的'report-only'标头: @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(); } } Referrer策略是Web应用程序可以利用的机制以管理Referrer字段,该字段包含用户所在的最后一个页面。 Spring Security的方法是使用Referrer策略标头,它提供了不同的策略: Referrer-Policy: same-origin [Referrer-Policy]响应头指示浏览器让目的地知道用户以前所在的源。 Spring Security默认不会添加Referrer策略标头。 可以使用带 同样,可用代码启用Referrer策略标头: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .referrerPolicy(ReferrerPolicy.SAME_ORIGIN); } } Feature策略是一种允许web开发人员在浏览器中选择性地启用、禁用和修改某些api和web特性行为的机制。 Feature-Policy: geolocation 'self' 通过该策略,开发人员可以选择一组"策略",让浏览器强制执行在整个站点中使用的特定特性。这些策略限制了站点可以访问或修改浏览器某些特性的默认行为的api。 默认情况下,Spring Security不会添加特性策略标头。 可以使用XML配置以启用,并使用 也可通过代码启用: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .featurePolicy("geolocation 'self'"); } } Spring Security具有一些机制,可以方便地将更常见的安全性标题添加到应用程序中。不过,它也提供hook来启用添加自定义标头。 有时可能想把自定义安全标头注入应用程序,但这并不支持开箱即用。例如: X-Custom-Security-Header: header-value 使用XML配置时,可用 也可用代码: @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")); } } 当xml或代码不支持配置所需的标头时,可以创建自定义的HeadersWriter实例,甚至可以自定义HeadersWriter的实现。 来看一个使用XFrameOptionsHeaderWriter定制实例的示例。如果想允许相同来源的内容框架。通过将policy属性设置为"SAMEORIGIN"即可。 class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter" c:frameOptionsMode="SAMEORIGIN"/> 还可用代码将内容的框架限制在相同的来源上: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN)); } } 有时可能只想为某些请求编写标头。例如,只想保护登录页面不被frame化。则可使用DelegatingRequestMatcherHeaderWriter来执行此操作,XML配置: class="org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter"> c:pattern="/login"/> class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"/> 代码防止登录页面的frame化: @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); } } HTTP会话相关功能由过滤器委派的SessionManagementFilter和SessionAuthenticationStrategy接口组合处理。典型用法包括会话固定保护攻击预防,会话超时检测以及限制已验证用户同时打开的会话数。 SessionManagementFilter根据SecurityContextHolder的当前内容检查SecurityContextRepository的内容,以确定用户在当前请求期间是否已通过认证,通常是通过非交互式认证机制作为预认证或RememberMe的。如果存储库包含安全上下文,则该过滤器不执行任何操作。如果没有,并且线程本地SecurityContext包含(非匿名)Authentication对象,则筛选器会假定它们已由堆栈中的前一个筛选器进行了认证。然后它将调用配置的SessionAuthenticationStrategy。 如果用户当前未通过认证,则筛选器将检查是否请求了无效的会话ID(例如由于超时),并且将调用配置的InvalidSessionStrategy(如果已设置)。最常见的行为就是重定向到一个固定的URL,并将其封装在标准实现SimpleRedirectInvalidSessionStrategy中。通过名称空间配置无效的会话URL时,也会使用后者。 SessionManagementFilter和AbstractAuthenticationProcessingFilter都使用SessionAuthenticationStrategy,所以如果使用的是自定义的表单登录类,则需要将其注入到这两者中。在这种情况下,组合命名空间和自定义Bean的典型配置可能如下所示: "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> ... "org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" /> 请注意,如果要在实现HttpSessionBindingListener的会话中存储Bean,包括Spring会话范围的bean,则使用默认值SessionFixationProtectionStrategy可能会导致问题。 SpringSecurity能够防止主体并发地对同一应用程序进行身份验证的次数超过指定次数。许多isv利用这一点来执行许可,而网络管理员喜欢这个特性,因为它有助于防止人们共享登录名。例如,可以阻止用户"Batman"从两个不同的会话登录到web应用程序。可以终止他们之前的登录,也可以在他们试图再次登录时报告错误,从而阻止第二次登录。注意,如果使用的是第二种方法,那么没有显式注销(例如,刚刚关闭浏览器)的用户将无法再次登录,直到其原始会话过期。 该实现使用SessionAuthenticationStrategy的专用版本,称为ConcurrentSessionControlAuthenticationStrategy。 要使用并发会话支持,需要添加到web.xml: org.springframework.security.web.session.HttpSessionEventPublisher 另外,还需要将ConcurrentSessionFilter添加到FilterChainProxy。ConcurrentSessionFilter需要两个构造函数参数,sessionRegistry(通常指向SessionRegistryImpl的实例),和sessionInformationExpiredStrategy(定义了在会话过期时应用的策略)。FilterChainProxy和其它默认bean的配置可能如下所示: class="org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy"> class="org.springframework.security.web.session.ConcurrentSessionFilter"> "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> class="org.springframework.security.core.session.SessionRegistryImpl" /> 每当HttpSession开始或结束时,将侦听器添加到web.xml,都会将ApplicationEvent发布到ApplicationContext。这很重要,因为它允许在会话结束时通知SessionRegistryImpl。如果没有它,即使用户退出其它会话或超时,用户也永远无法再次重新登录。 通过命名空间或使用普通bean设置并发控制具有有用的副作用,即提供可在应用程序中直接使用的SessionRegistry的引用,因此即使不想限制用户可能拥有的会话数量,但建立基础架构可能都是值得的。可以将maximumSession属性设置为-1以允许无限会话。如果使用xml配置,则可用session-registry-alias属性为内部创建的SessionRegistry设置别名,从而为bean注入引用。 getAllPrincipals()方法提供当前已通过认证的用户列表。可以通过调用getAllSessions(Object principal, boolean includeExpiredSessions)方法列出用户的会话,该方法返回SessionInformation对象列表。也可以通过调用SessionInformation实例上的expireNow()来让用户会话过期。当用户返回到应用程序时,将阻止他们继续进行。例如,可以在管理应用程序中找到这些方法。 通常认为采用"deny-by-default"的安全做法非常好,你可以在其中明确指定允许的内容并拒绝其他所有内容。定义未经认证的用户可访问的内容也是类似的情况,特别是对于Web应用程序。 许多网站要求用户必须通过除少数网址以外的其他任何验证(例如,家庭和登录页面)。 在这种情况下,为这些特定的URL定义访问配置属性是最容易的,而不是针对每个安全资源。 换句话说,有时很高兴地说默认情况下ROLE_SOMETHING是必需的,并且只允许某些例外情况,例如登录,注销和应用程序的主页。 你也可以完全忽略过滤器链中的这些页面,从而绕过访问控制检查,但这可能因其他原因而不受欢迎,特别是如果页面对经过认证的用户的行为不同。 这就是我们所说的匿名认证。请注意,"匿名认证"的用户与未经认证的用户之间没有真正的概念区别。Spring Security的匿名认证只是提供一种更方便的配置访问控制属性的方式。例如,调用诸如getCallerPrincipal的servlet API调用,即使实际上存在SecurityContextHolder中的匿名认证对象,仍将返回null。 在其他情况下,匿名认证很有用,例如审计拦截器查询SecurityContextHolder以确定哪个主体负责给定操作。如果他们知道SecurityContextHolder始终包含Authentication对象,并且从不会是null,则可以更强健地编写类。 当使用HTTP配置Spring Security 3.0时,会自动提供匿名认证支持,并且可以使用 有3个类提供了匿名认证功能。AnonymousAuthenticationToken是Authentication的实现,并存储适用于匿名主体的GrantedAuthority。有一个对应的AnonymousAuthenticationProvider,链接到ProviderManager,以便接受AnonymousAuthenticationToken。最后,还有一个AnonymousAuthenticationFilter,它在普通认证机制之后被链接,并且如果没有现有的Authentication对象,则会自动向SecurityContextHolder添加AnonymousAuthenticationToken。筛选器和认证提供者的定义如下: class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter"> class="org.springframework.security.authentication.AnonymousAuthenticationProvider"> key在过滤器和认证提供者之间共享,以便前者创建的令牌可以被后面的接受。userAttribute以usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]的形式表示。这与在InMemoryDaoImpl的userMap属性的等号之后使用的语法相同。 如前所述,匿名认证的好处是所有的URI模式都可以应用于它们。例如: class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> " + 舍弃匿名认证讨论的是AuthenticationTrustResolver接口及其相应的AuthenticationTrustResolverImpl实现。该接口提供了isAnonymous(Authentication)方法,它允许感兴趣的类考虑这种特殊类型的Authentication状态。ExceptionTranslationFilter在处理AccessDeniedException时使用此接口。如果AccessDeniedException被抛出,并且Authentication是匿名类型,而不是抛出403(禁止)响应,则过滤器将改为开始AuthenticationEntryPoint,以便主体可以正确进行认证。这是一个必要的区别,否则主体将始终被视为"已认证",永远不会有机会通过表单,基本,摘要或其他正常的认证机制进行登录。 你经常会看到上述拦截器配置中的ROLE_ANONYMOUS属性被IS_AUTHENTICATED_ANONYMOUSLY替代,这在定义访问控制时实际上是同样的事情。这是使用我们将在认证章节中看到的AuthenticatedVoter的一个示例。它使用AuthenticationTrustResolver来处理这个特定的配置属性并授予匿名用户访问权限。AuthenticatedVoter方法更强大,因为它允许区分匿名,RememberMe和完全认证的用户。如果不需要此功能,那么可以坚持使用ROLE_ANONYMOUS,这将由Spring Security的标准RoleVoter处理。 Spring Security 4增加了对保护 Spring的WebSocket支持的支持。本节介绍如何使用Spring Security的WebSocket支持。 Spring Security 4.0通过Spring消息抽象为WebSockets引入了授权支持。要使用Java代码配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。 例如: @Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { <1> <2> protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages .simpDestMatchers("/user/*").authenticated() <3> } } 这将确保: <1>,任何入站CONNECT消息都需要一个有效的CSRF令牌来执行Same Origin Policy <2>,SecurityContextHolder由simpUser标头属性中的用户填充,用于任何入站请求。 <3>,邮件需要获得适当的授权。具体来说,任何以"/user/"开头的入站邮件都需要ROLE_USER。 Spring Security还为保护WebSocket提供XML Namespace支持。 可比的基于XML的配置如下所示: 这将确保: <1>,任何入站CONNECT消息都需要一个有效的CSRF令牌来执行Same Origin Policy <2>,SecurityContextHolder由simpUser标头属性中的用户填充,用于任何入站请求。 <3>,邮件需要获得适当的授权。具体来说,任何以"/user/"开头的入站邮件都需要ROLE_USER。 当WebSocket连接建立时,WebSocket重复使用与HTTP请求中相同的认证信息。 这意味着HttpServletRequest上的Principal将被移交给WebSockets。 如果你使用的是Spring Security,则HttpServletRequest上的Principal会自动覆盖。 更具体地说,为了确保用户已经对WebSocket应用程序进行了Authentication,所有必需的是确保你设置Spring Security来验证你的基于HTTP的Web应用程序。 Spring Security 4.0通过Spring消息抽象为WebSockets引入了授权支持。 要使用Java代码配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。 例如: @Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { @Override protected 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以外的任何消息)都需要用户进行Authentication。 <2>,任何人都可以订阅/user/queue/errors。 <3>,具有以"/app/"开头的目标的任何消息都将要求用户拥有角色ROLE_USER。 <4>,任何类型为SUBSCRIBE的以"/user/"或"/topic/friends/"开头的消息都需要ROLE_USER。 <5>,任何类型为MESSAGE或SUBSCRIBE的消息都会被拒绝。由于6,我们不需要这一步,但它说明了如何匹配特定的消息类型。 <6>,其他消息被拒绝。这是确保你不会错过任何消息的好主意。 Spring Security还为保护WebSocket提供XML支持。可比的基于XML的配置如下所示: <1> <4> <5> 这将确保: <1>,类型为CONNECT,UNSUBSCRIBE或DISCONNECT的任何消息都需要用户进行Authentication。 <2>,任何人都可以订阅/user/queue/errors。 <3>,具有以"/app/"开头的目标的任何消息都将要求用户拥有角色ROLE_USER。 <4>,任何类型为SUBSCRIBE的以"/user/"或"/topic/friends/"开头的消息都需要ROLE_USER。 <5>,任何类型为MESSAGE或SUBSCRIBE的消息都会被拒绝。由于6,我们不需要这一步,但它说明了如何匹配特定的消息类型。 <6>,任何其他带有目的地的邮件都会被拒绝。这是确保你不会错过任何消息的好主意。 为了正确保护你的应用程序,理解Spring的WebSocket支持是很重要的。 了解SUBSCRIBE和MESSAGE消息类型之间的区别以及它在Spring中的工作方式非常重要。 考虑一个聊天应用程序。 1、系统可以通过"/topic/system/notifications"的目的地向所有用户发送通知MESSAGE。 2、客户可以通过SUBSCRIBE向"/topic/system/notifications"收到通知。 虽然我们希望客户能够订阅"/topic/system/notifications",但我们不希望让他们向该目的地发送MESSAGE。 如果我们允许发送MESSAGE到"/topic/system/notifications",则客户端可以直接向该端点发送消息并模拟系统。 一般而言,应用程序通常会拒绝任何发送到以broker前缀开头的消息(即"/topic/"或"/queue/")的消息。 了解目标如何转变也很重要。 考虑一个聊天应用程序。 1、用户可以通过向"/app/chat"的目的地发送消息来向特定用户发送消息。 2、应用程序看到该消息,确保将"from"属性指定为当前用户(我们不能信任客户端)。 3、然后,应用程序使用SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)将消息发送给收件人。 4、邮件转到"/queue/user/messages- 通过上面的应用程序,我们希望允许客户端监听从"/user/queue"转换为"/queue/user/messages- 一般来说,应用程序通常拒绝发送给以broker前缀开头的消息(即"/topic/"或"/queue/")的任何SUBSCRIBE。 当然,我们可能会提供例外来解释诸如此类的事情 Spring包含标题为消息流的部分,它描述了消息如何流经系统。值得注意的是,Spring Security只保护clientInboundChannel。Spring Security不会尝试保护clientOutboundChannel。 最重要的原因是性能。对于每一条消息,通常会有更多消息传出。我们鼓励确保订阅端点,而不是保护出站消息。 强调浏览器不强制WebSocket连接的同源策略是非常重要的。这是一个非常重要的考虑因素。 考虑以下情况。用户访问bank.com并向其帐户进行认证。同一用户在其浏览器中打开另一个选项卡并访问evil.com。同源策略确保evil.com无法读取或写入bank.com数据。 使用WebSockets相同的来源策略不适用。 事实上,除非bank.com明确禁止它,否则evil.com可以代表用户读取和写入数据。 这意味着用户可以通过webSocket进行任何操作(即转账资金),evil.com可以代表该用户进行操作。 由于SockJS试图模拟WebSockets,它也绕过了同源策略。 这意味着开发人员在使用SockJS时需要明确地保护他们的应用程序免受外部域的攻击 幸运的是,自Spring 4.1.5以来,Spring的WebSocket和SockJS支持限制了对 当前域的访问。 Spring Security增加了额外的保护层来提供深度保护。 默认情况下,Spring Security需要任何CONNECT消息类型中的CSRF令牌。这确保只有可访问CSRF令牌的站点才能连接。由于只有同源的可以访问CSRF令牌,因此不允许外部域进行连接。 通常,我们需要将CSRF令牌包含在HTTP标头或HTTP参数中。但是,SockJS不允许使用这些选项。相反,我们必须在Stomp标头中包含令牌。 通过访问名为_csrf的请求属性,应用程序可以获得一个CSRF令牌。 例如,以下将允许访问JSP中的CsrfToken: var headerName = "${_csrf.headerName}"; var token = "${_csrf.token}"; 如果你使用静态HTML,则可以在REST端点上公开CsrfToken。 例如,以下内容将显示/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) { ... } 如果想允许其他域访问网站,可以禁用Spring Security的保护。例如,代码: @Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { ... @Override protected boolean sameOriginDisabled() { return true; } } SockJS提供后备传输以支持旧版浏览器。在使用后备选项时,需要放松一些安全约束,以允许SockJS与Spring Security合作。 SockJS可以使用[transport that leverages an iframe]。默认情况下,Spring Security将deny站点框起来以防止点击劫持攻击。为了允许基于SockJS框架的传输工作,需要配置Spring Security以允许相同的源来构造内容。 可以使用frame-options元素自定义X-Frame-Options。例如,以下内容将指示Spring Security使用允许同一域内的iframe的"X-Frame-Options: SAMEORIGIN": policy="SAMEORIGIN" /> 同样,可以使用以下方法自定义frame以在代码中使用相同的源: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .frameOptions() .sameOrigin(); } } 对于任何基于HTTP的传输,SockJS在CONNECT消息上使用POST。 通常,我们需要将CSRF令牌包含在HTTP标头或HTTP参数中。但是,SockJS不允许使用这些选项。相反,必须按照[将CSRF添加到Stomp标头]中所述将标记包含在Stomp标头中。 这也意味着需要通过Web层来放松CSRF保护。具体而言,希望为连接网址禁用CSRF保护。不希望为每个网址禁用CSRF保护。否则网站将容易受到CSRF攻击。 我们可以通过提供CSRF RequestMatcher轻松实现此目的。例如,如果端点是"/chat",我们可以仅使用以下配置为仅以"/chat/"开头的URL禁用CSRF保护: @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected 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配置可以使用 ... class="AndRequestMatcher"> Spring Security中的高级授权功能是其受欢迎最引人注目的原因之一。无论选择如何进行认证--无论是使用Spring Security提供的机制和提供程序,还是与容器或其它非Spring Security认证机构集成--都会发现授权服务可以在应用程序中以一致和简单的方式使用授权服务。 在本部分中,将介绍不同的AbstractSecurityInterceptor实现。然后,继续探讨如何通过使用域访问控制列表来微调授权。 所有身份验证实现都存储一个GrantedAuthority列表。这个列表代表了主体被授予的权限。AuthenticationManager将GrantedAuthority对象插入到Authentication对象中,并在做出授权决策时由AccessDecisionManager读取。 GrantedAuthority接口只有一个方法: String getAuthority(); 该方法允许AccessDecisionManager获得GrantedAuthority的精确字符串表示。通过返回表示权限的字符串,大多数AccessDecisionManager都可以轻松读取GrantedAuthority。如果GrantedAuthority无法精确地表示为字符串,则认为它是"复杂的",方法getAuthority()必须返回null。 一个复杂的GrantedAuthority的示例将是一个存储操作列表和权限阈值的实现,这些操作和权限阈值适用于不同的客户帐号。将这个复杂的GrantedAuthority表示为字符串很困难,因此getAuthority()方法应返回null。这将向任何AccessDecisionManager表明,为了解析GrantedAuthority,它需要专门的实现。 Spring Security包含一个具体的GrantedAuthority实现,SimpleGrantedAuthority。这允许将任何用户指定的字符串转换为GrantedAuthority。安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority来填充Authentication对象。 正如在技术概述一章中看到的,Spring Security提供了拦截器,它控制对安全对象(如方法调用或web请求)的访问。AccessDecisionManager对是否允许继续进行调用作出预调用决策。 AccessDecisionManager由AbstractSecurityInterceptor调用,并负责做出最终的访问控制决策。AccessDecisionManager接口包含3个方法: void decide(Authentication authentication, Object secureObject, Collection boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); AccessDecisionManager的decide方法传递所需的所有相关信息,以作出授权决定。特别是,传递安全对象可以检查实际安全对象调用中包含的那些参数。例如,假设安全对象是MethodInvocation,很容易为Customer参数查询MethodInvocation,然后在AccessDecisionManager中实施某种安全逻辑,以确保允许主体对该客户进行操作。如果访问被拒绝,实现预计会抛出AccessDeniedException异常。 AbstractSecurityInterceptor在启动时调用supports(ConfigAttribute)方法,以确定AccessDecisionManager是否能够处理传递的ConfigAttribute。supports(Class)方法由安全拦截器的实现调用,以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全对象的类型。 虽然用户可以实现自己的AccessDecisionManager来控制授权的所有方面,但Spring Security还是包括了一些基于投票的AccessDecisionManager实现。 使用这种方法,授权决策将轮询一系列AccessDecisionVoter实现。然后AccessDecisionManager根据对投票的评估决定是否抛出AccessDeniedException异常。 AccessDecisionVoter接口有3个方法: int vote(Authentication authentication, Object object, Collection boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); 具体实现返回一个int,可能的值反映在AccessDecisionVoter静态字段ACCESS_ABSTAIN,ACCESS_DENIED和ACCESS_GRANTED中。如果Voter实现对授权决定没有意见,则Voter实现将返回ACCESS_ABSTAIN。如果确实有意见,则必须返回ACCESS_DENIED或ACCESS_GRANTED。 Spring Security提供了3个具体的AccessDecisionManager来计票。ConsensusBased实现将根据非弃权票的结果授权或拒绝访问。提供的属性是为了处理票数相同或弃权的情况。如果收到一个或多个ACCESS_GRANTED票(即如果至少有一次赞成票,就忽略拒绝投票),则AffirmativeBased实现将授予访问权限。与ConsensusBased实现一样,如果所有Voter都弃权,也一样会有参数指定处理方式。UnanimousBased提供者期望获得一致的ACCESS_GRANTED票,以便授予访问权限,而忽略弃权。如果有任何ACCESS_DENIED投票,它将拒绝访问。与其它实现一样,如果所有Voter都弃权,也会有一个参数指定处理方式。 可以实现一个自定义的AccessDecisionManager来区别投票。例如,来自特定AccessDecisionVoter的投票可能会获得额外的权重,而来自特定Voter的拒绝投票可能会产生否决权。 与SpringSecurity一起提供的最常用的AccessDecisionVoter是简单的RoleVoter,它将配置属性视为简单的角色名,并在用户被分配角色时投票授予访问权。 如果有以前缀ROLE_开头的ConfigAttribute,它将进行投票。如果存在GrantedAuthority(通过getAuthority()方法返回的字符串表示)恰好等于一个或多个以ConfigAttributes开头的ROLE_,则它将投票授予访问权限。如果以ROLE_开头的ConfigAttribute没有精确匹配上,则RoleVoter将投票拒绝访问。如果没有以ROLE_开头的ConfigAttribute,则Voter将弃权。 看不到的另一个Voter是AuthenticatedVoter,它可以用来区分不同的匿名用户,完全认证的用户和RememberMe认证的用户。在remember-me认证下,许多站点允许某些有限的访问,但要完全访问就需要用户登录来确认其身份。 当使用IS_AUTHENTICATED_ANONYMOUSLY授予用户匿名访问权时,AuthenticatedVoter将会处理这个属性。 显然,你也可以实现一个自定义的AccessDecisionVoter,可以把任何想要的访问控制逻辑放在里面。它可能特定于应用程序(与业务逻辑相关),也可能实现一些安全管理逻辑。 在继续进行安全对象调用之前,AccessDecisionManager由AbstractSecurityInterceptor调用,但某些应用程序需要修改安全对象调用实际返回的对象的方法。虽然可以轻松实现自己的AOP关注点来实现此目的,但Spring Security提供了一个方便的hook,它具有几个与ACL功能集成的具体实现。 AfterInvocationManager有一个具体实现AfterInvocationProviderManager,用于轮询AfterInvocationProvider的列表。每个AfterInvocationProvider都可以修改返回对象或抛出AccessDeniedException。事实上,多个Provider可以修改该对象,因为前一个Provider的结果被传递给列表中的下一个。 请注意,如果使用的是AfterInvocationManager,则仍然需要允许MethodSecurityInterceptor的AccessDecisionManager去执行操作。如果使用的是AccessDecisionManager实现,而没有为特定的安全方法调用定义配置属性的话,就会导致每个AccessDecisionVoter放弃投票。反过来,如果AccessDecisionManager属性"allowIfAllAbstainDecisions"为false,则会抛出AccessDeniedException。可以通过以下两种方式避免潜在问题:(i)将"allowIfAllAbstainDecisions"设置为true(不建议),或(ii)确保至少有一个配置属性让AccessDecisionVoter投票授予访问权限。后者(推荐)通常通过ROLE_USER或ROLE_AUTHENTICATED配置属性来实现。 应用程序中的特定角色应该自动"包括"其它角色,这是一个常见的要求。例如,在一个具有"admin"和"user"角色概念的应用程序中,可能希望管理员能够完成普通用户所能做的一切。要做到这一点,需要所有管理员用户都被分配了"user"角色。或者,修改每个需要"user"角色的访问约束,以包含"admin"角色。如果应用程序中有许多不同的角色,这可能会变得非常复杂。 应用角色层次结构以允许配置哪些角色(或权限)应该包括其它角色。Spring Security的RoleHierarchyVoter是RoleVoter的扩展版,使用一个RoleHierarchy配置,从而获得用户分配的所有"可访问权限"。典型配置: class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl"> ROLE_ADMIN > ROLE_STAFF ROLE_STAFF > ROLE_USER ROLE_USER > ROLE_GUEST 这个层次结构有4个角色,ROLE_ADMIN-->ROLE_STAFF-->ROLE_USER-->ROLE_GUEST。使用ROLE_ADMIN做过身份验证的用户,在针对使用上述RoleHierarchyVoter配置的AccessDecisionManager进行安全约束评估时,表现得好像具有全部4种角色一样。【>】符号可以被认为是"包含"含义。 角色层次结构为简化应用程序的访问控制配置数据,和减少需要分配给用户的权限数量提供了一种便捷方式。对于更复杂的需求,可能希望定义应用程序需要的特定访问权限与分配给用户的角色之间的逻辑映射,并在加载用户信息时在两者之间进行转换。 在Spring Security 2.0之前,安全MethodInvocation需要大量的样板代码配置。现在推荐的方法安全性是使用名称空间配置。通过这种方式,可以自动配置方法安全基础结构bean,而不需要了解实现类,这里只简单介绍一下涉及的类。 使用MethodSecurityInterceptor执行的方法安全性,可以确保MethodInvocation的安全。根据配置方法的不同,拦截器可以特定于单个bean,也可以在多个bean之间共享。拦截器使用MethodSecurityMetadataSource实例来获取应用于特定方法调用的配置属性。MapBasedMethodSecurityMetadataSource用于存储由方法名称(可是通配符)键入的配置属性,并且在使用 可以在应用程序上下文中直接配置MethodSecurityIterceptor,以便与Spring AOP的代理机制一起使用: "org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor"> AspectJ安全拦截器与上一节讨论的AOP联盟安全拦截器非常相似。本节只讨论它们的不同之处。 AspectJ拦截器被命名为AspectJSecurityInterceptor。与依靠Spring应用程序上下文通过代理织入安全拦截器的AOP联盟安全拦截器不同,AspectJSecurityInterceptor通过AspectJ编译器织入。在同一个应用程序中使用两种类型的安全拦截器并不少见,AspectJSecurityInterceptor用于域对象实例安全性,而AOP联盟的MethodSecurityInterceptor用于服务层安全性。 首先考虑如何在Spring应用程序上下文中配置AspectJSecurityInterceptor: "org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor"> 除了类名外,AspectJSecurityInterceptor与AOP联盟安全拦截器完全相同。事实上,两个拦截器可以共享securityMetadataSource,因为SecurityMetadataSource与java.lang.reflect.Method协同工作,而不是AOP库的特定类。当然,访问决策可以访问相关的AOP库的特定方法(例如MethodInvocation或JoinPoint),因此在做出访问决策时,可以考虑一系列的附加标准(如方法参数)。 接下来,需要定义一个AspectJ切面。例如: 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连接起来。例如: class="security.samples.aspectj.DomainObjectInstanceSecurityAspect" factory-method="aspectOf"> 现在,在应用程序中的任何地方创建bean,使用任何合适的方法(例如new Person();),然后应用安全拦截器。 Spring Security 3.0引入了使用Spring EL表达式作为授权机制的能力,以及简单使用以前见过的配置属性和访问决策选项。基于表达式的访问控制建立在相同的体系结构上,但允许将复杂的布尔逻辑封装在单个表达式中。 Spring Security使用Spring EL进行表达式支持。表达式使用"root对象"作为评估上下文的一部分进行评估。Spring Security使用Web和方法安全性的特定类作为根对象,以便提供内置表达式和对当前主体等值内容的访问。 表达式根对象的基类是SecurityExpressionRoot。提供了可用于Web和方法安全性的一些常用表达式。 常见的内置表达式 表达式 描述 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() 如果当前主体是记事本用户,则返回true isAuthenticated() 如果用户不是匿名的,则返回true isFullyAuthenticated() 如果用户不是匿名用户或RememberMe用户,则返回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') 要使用表达式保护单个URL,首先需要将 access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/> ... 这里已经定义的应用程序的"admin"区域(由URL模式定义)应该只对具有"admin"权限,并且其IP地址与本地子网匹配的用户可用。在上一节中有内置的hasRole表达式。hasIpAddress是特定于Web安全性的附加内置表达式,它由WebSecurityExpressionRoot类定义,这个类的实例在估算Web访问表达式时用作表达式根对象。该对象还公开了名为request的HttpServletRequest对象,以便可以直接在表达式中调用请求。如果正在使用表达式,则WebExpressionVoter将被添加到命名空间所使用的AccessDecisionManager。因此,如果没有使用名称空间,并且想要使用表达式,那么必须将其中之一添加到配置中。 如果想扩展可用的表达式,可以很容易地引用公开的任何Spring Bean。例如,假设名称为webSecurity的Bean包含以下方法签名: public class WebSecurity { public boolean check(Authentication authentication, HttpServletRequest request) { ... } } 可以引用以下的方法: access="@webSecurity.check(authentication,request)"/> ... 或者代码 http .authorizeRequests() .antMatchers("/user/**").access("@webSecurity.check(authentication,request)") ... 有时能够在URL中引用路径变量是很好的。例如,考虑一个RESTful应用程序,它从URL路径以格式/user/{userId}查找用户id。 通过将路径变量放在模式中,可以很容易地引用它。例如,如果有名为webSecurity的Bean包含以下方法签名: public class WebSecurity { public boolean checkUserId(Authentication authentication, int id) { ... } } 引用使用的方法: access="@webSecurity.checkUserId(authentication,#userId)"/> ... 或者代码。 http .authorizeRequests() .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)") ... 在这两种配置中,匹配的URL将传入路径变量(并将其转换为checkUserId方法)。例如,如果网址为/user/123/resource,则传入的ID为123。 方法安全性比简单的允许或拒绝规则复杂一点。Spring Security 3.0引入了一些新的注解,以便全面支持使用表达式。 有四个注解支持表达式属性,以支持方法调用前后的授权检查,还支持对提交的集合参数或返回值进行过滤。它们是@PreAuthorize,@PreFilter,@PostAuthorize和@PostFilter。可通过 明显最有用的注释是@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); 这里使用方法参数作为表达式的一部分来决定当前用户是否具有给定Contact的"admin"权限。内置的hasPermission()表达式通过应用程序上下文链接到Spring Security ACL模块中,如下所示。可通过表达式变量的name存取任何方法参数。 Spring Security可以通过多种方式来解决方法参数。Spring Security使用DefaultSecurityParameterNameDiscoverer来发现参数名称。默认情况下,对于整个方法尝试以下选项。 1、如果Spring Security的@P注解出现在该方法的单个参数上,则会使用这个参数值。这对在JDK8之前编译的接口非常有用,它不包含有关参数名称的任何信息。例如: import org.springframework.security.access.method.P; ... @PreAuthorize("#c.name == authentication.name") public void doSomething(@P("c") Contact contact); 背后使用AnnotationParameterNameDiscoverer实现的这种使用方式,可以被自定义为支持任何指定注解的value属性。 2、如果Spring Data的@Param至少注解了该方法的一个参数,就使用该值。这对在JDK8之前编译的接口非常有用,它不包含有关参数名称的任何信息。例如: import org.springframework.data.repository.query.Param; ... @PreAuthorize("#n == authentication.name") Contact findContactByName(@Param("n") String name); 背后使用AnnotationParameterNameDiscoverer实现的这种使用方式,可以被自定义为支持任何指定注解的value属性。 3、如果使用JDK8带-parameters参数编译源文件,并且使用Spring 4+,然后用JDK的反射API来发现参数名称,这适用于类和接口。 4、如果使用带调试符号的参数编译源文件,则参数名称将使用调试符号发现。这对接口不起作用,因为它们没有关于参数名称的调试信息。对于接口,必须使用注解或JDK8的方式。 表达式中可以使用任何Spring-EL功能,因此也可以访问参数上的属性。如果希望特定的方法只访问用户名在名单中的用户,那么可以写如下代码: @PreAuthorize("#contact.name == authentication.name") public void doSomething(Contact contact); 这里访问的另一个内置表达式--身份验证,它是存储在安全上下文中的Authentication对象。还可以使用表达式principal直接访问它的"principal"属性,该值通常是UserDetails实例,因此可以使用principal.username或principal.enabled之类的表达式。 不太常见的情况是,可能想在调用方法之后执行访问控制检查,这可以通过@PostAuthorize注解实现。要访问方法的返回值,就需要在注解中使用内置的returnObject。 Spring Security支持对集合和数组进行过滤,可以通过注解表达式实现。通常是作用于方法返回值的。例如: @PreAuthorize("hasRole('USER')") @PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')") public List 当使用@PostFilter注释时,Spring Security遍历返回的集合并删除那些表达式结果为false的元素。filterObject这个名称指向的是集合的当前对象。也可以在方法调用之前使用@PreFilter过滤,尽管这不太常见。如果有多个参数是集合类型,使用语法也是一样的,但必须使用此注解的filterTarget属性通过名称来选择一个参数。 注意,过滤不能替代数据检索查询调优,如果要过滤大量的集合并删除很多条目,那么这种过滤的方法很低效。 有一些特定于方法安全的内置表达式,filterTarget和returnValue的值非常简单,但使用hasPermission()表达式就需要小心谨慎。 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对象)。第1个方法在域对象(已被控制访问的对象)已经加载的情况下使用。如果当前用户对该对象拥有指定的权限,表达式将返回true。第2个方法用于对象未加载但其标识符已知的情况,还需要域对象的抽象"type"描述符,以允许加载正确的ACL权限。这通常是对象的Java类,但只要它与权限的加载方式一致,就不必如此。 要使用hasPermission()表达式,就必须在应用程序上下文中显式配置一个PermissionEvaluator。 "org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler"> 其中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注解不支持元注解。 仅保留Oauth2。 HttpSecurity.oauth2login()提供了许多配置选项,用于定制OAuth 2.0登录。主要配置选项被分组到协议端点对应项中。 例如,oauth2Login().authorizationEndpoint()允许配置授权端点,而oauth2Login().tokenEndpoint()允许配置令牌端点。 @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { //**333+99 @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .authorizationEndpoint() ... .redirectionEndpoint() ... .tokenEndpoint() ... .userInfoEndpoint() ... } } oauth2Login() DSL的主要目标是与规范中定义的命名保持一致。OAuth 2.0授权框架定义协议端点如下。 授权过程使用两个授权服务器端点(HTTP资源): 1、授权端点:客户端使用的授权是通过用户代理重定向从资源所有者获得的。 2、令牌端点:客户端用于交换访问令牌的授权权限,通常与客户端身份验证一起使用。 以及一个客户端端点: 1、重定向端点:授权服务器用于通过资源所有者的用户代理向客户端返回包含授权凭据的响应。 OpenID Connect Core 1.0规范定义了UserInfo端点,如下所示: UserInfo端点是一个OAuth 2.0保护资源,用于返回经过认证的最终用户声明。为了获得最终用户的请求声明,客户端使用经OpenID Connect验证获得的访问令牌向UserInfo端点发出请求。这些声明通常由JSON对象表示,该对象包含声明的名称-值对集合。 以下代码显示了可用于oauth2Login() DSL的完整配置选项: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .clientRegistrationRepository(this.clientRegistrationRepository()) .authorizedClientService(this.authorizedClientService()) .loginPage("/login") .authorizationEndpoint() .baseUri(this.authorizationRequestBaseUri()) .authorizationRequestRepository(this.authorizationRequestRepository()) .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"); } } 默认情况下,OAuth 2.0登录页面由DefaultLoginPageGeneratingFilter自动生成。默认登录页面显示每个已配置的OAuth客户端的ClientRegistration.clientName作为链接,该链接能够发起授权请求(或OAuth 2.0登录)。 每个OAuth客户端的链接目标缺省为以下内容: OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}" 示例: 要覆盖默认的登录页面,配置oauth2Login().loginPage()和(可选)oauth2Login().authorizationEndpoint().baseUri()。 示例: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .loginPage("/login/oauth2") ... .authorizationEndpoint() .baseUri("/login/oauth2/authorization") .... } } 重要,需要提供带有@RequestMapping("/login/oauth2")的@Controller,以便呈现自定义登录页面。 提示,如前所述,配置oauth2Login().authorizationEndpoint().baseUri()是可选的。但是,若想定制,确保每个OAuth客户端的链接都与authorizationEndpoint().baseUri()匹配。 示例: AuthorizationRequestRepository负责OAuth2AuthorizationRequest从发起授权请求到接收到授权响应(回调)的持久化。 提示,OAuth2AuthorizationRequest用于关联和验证授权响应。 AuthorizationRequestRepository的默认实现是HttpSessionOAuth2AuthorizationRequestRepository,它将OAuth2AuthorizationRequest存储在HttpSession中。 如果想提供AuthorizationRequestRepository的定制实现,该实现将OAuth2AuthorizationRequest的属性存储在Cookie中,按下例配置: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .authorizationEndpoint() .authorizationRequestRepository(this.cookieAuthorizationRequestRepository()) ... } private AuthorizationRequestRepository return new HttpCookieOAuth2AuthorizationRequestRepository(); } } 授权服务器使用重定向端点通过资源所有者用户代理将授权响应(包含授权凭证)返回给客户端。 提示,OAuth 2.0登录利用授权码授权。因此,授权凭证就是授权码。 默认的授权响应baseUri(重定向端点)为/login/oauth2/code/*,它在OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI中定义。 若想自定义授权响应baseUri,按下例配置: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected 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(); OAuth2AccessTokenResponseClient负责在授权服务器的令牌端点为访问令牌凭证交换授权权限凭证。 OAuth2AccessTokenResponseClient的默认实现是NimbusAuthorizationCodeTokenResponseClient,它在令牌端点为访问令牌交换授权代码。 注意,Nimbusauthorizationcodetokenresponsecent内部使用的是Nimbus OAuth 2.0 SDK。 若想提供OAuth2AccessTokenResponseClient的定制实现,它使用Spring Framework 5的响应式WebClient来发起对令牌端点的请求,按下例配置: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .tokenEndpoint() .accessTokenResponseClient(this.accessTokenResponseClient()) ... } private OAuth2AccessTokenResponseClient return new SpringWebClientAuthorizationCodeTokenResponseClient(); } } UserInfo端点包含许多配置选项,如下所述。 在用户成功通过OAuth 2.0的Provider进行认证后,OAuth2User.getAuthorities()(或OidcUser.getAuthorities())可能被映射到一组新GrantedAuthority实例上,这些实例在完成认证时会提供给OAuth2AuthenticationToken。 提示,OAuth2AuthenticationToken.getAuthorities()用于授权请求,如hasRole('USER')或hasRole('ADMIN')。 在映射用户权限时,有以下几个选项可供选择。 提供GrantedAuthoritiesMapper的实现,并按下例配置: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .userInfoEndpoint() .userAuthoritiesMapper(this.userAuthoritiesMapper()) ... } private GrantedAuthoritiesMapper userAuthoritiesMapper() { return (authorities) -> { Set 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 // 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 { @Override protected void configure(HttpSecurity http) throws Exception { http.oauth2Login(); } @Bean public GrantedAuthoritiesMapper userAuthoritiesMapper() { ... } } 该策略与使用GrantedAuthoritiesMapper相比更先进,也更灵活,因为它允许访问OAuth2UserRequest和OAuth2User(使用OAuth 2.0 UserService时)或OidcUserRequest和OidcUser(使用OpenID Connect 1.0 UserService时)。 OAuth2UserRequest(和OidcUserRequest)可允许访问关联的OAuth2AccessToken,这在delegator需要从受保护的资源中获取授权信息,然后才能为用户映射自定义权限的情况下非常有用。 下例展示如何使用OpenID Connect 1.0 UserService实现和配置基于委托的策略: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .userInfoEndpoint() .oidcUserService(this.oidcUserService()) ... } private OAuth2UserService final OidcUserService delegate = new OidcUserService(); return (userRequest) -> { // Delegate to the default implementation for loading a user OidcUser oidcUser = delegate.loadUser(userRequest); OAuth2AccessToken accessToken = userRequest.getAccessToken(); Set // 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 instead oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo()); return oidcUser; }; } } CustomUserTypesOAuth2UserService是OAuth2UserService的实现,它为自定义OAuth2User类型提供支持。 如果默认实现(DefaultOAuth2User)不符合需求,可以定制OAuth2User实现。 以下代码演示如何为GitHub注册自定义OAuth2User类型: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .userInfoEndpoint() .customUserType(GitHubOAuth2User.class, "github") ... } } 以下代码显示GitHub的自定义OAuth2User类型的示例: public class GitHubOAuth2User implements OAuth2User { private List AuthorityUtils.createAuthorityList("ROLE_USER"); private Map private String id; private String name; private String login; private String email; @Override public Collection extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public Map 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; } @Override public 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; } } 提示,id,name,login和email是在GitHub的UserInfo响应中返回的属性。 DefaultOAuth2UserService是OAuth2UserService的实现,支持标准的OAuth 2.0 Provider。 注意,OAuth2UserService从UserInfo端点(在授权流程执行期间通过访问令牌授权给客户端权限)获得最终用户(资源所有者)的用户属性,并以OAuth2User的形式返回一个AuthenticatedPrincipal。 如果默认实现不适合,可以为标准的OAuth 2.0 Provider定制OAuth2UserService实现。 以下配置演示如何配置定制的OAuth2UserService: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .userInfoEndpoint() .userService(this.oauth2UserService()) ... } private OAuth2UserService return new CustomOAuth2UserService(); } } OidcUserService是OAuth2UserService的实现,它支持OpenID Connect 1.0 Provider。 注意,OAuth2UserService从UserInfo端点(在授权流程执行期间通过访问令牌授权给客户端权限)获得最终用户(资源所有者)的用户属性,并以OidcUser的形式返回一个AuthenticatedPrincipal。 如果默认实现不适合,可以为OpenID Connect 1.0 Provider定制OAuth2UserService实现。 以下配置演示如何配置定制的OpenID Connect 1.0 OAuth2UserService: @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .userInfoEndpoint() .oidcUserService(this.oidcUserService()) ... } private OAuth2UserService return new CustomOidcUserService(); } } 本框架可使用各种数据库模型,默认用HSQLDB数据库提供了DDL语句。 UserDetailsService(JdbcDaoImpl)的标准JDBC实现要求表为用户提供密码,帐户状态(启用或禁用)以及权限列表(角色)。 create table users( username varchar_ignorecase(50) not null primary key, password varchar_ignorecase(50) not null, enabled boolean not null ); create table authorities ( username varchar_ignorecase(50) not null, authority varchar_ignorecase(50) not null, constraint fk_authorities_users foreign key(username) references users(username) ); create unique index ix_auth_username on authorities (username,authority); Spring Security 2.0在JdbcDaoImpl中引入了对组权限的支持。如果启用,则表结构如下。 create table groups ( id bigint generated by default as identity(start with 0) primary key, group_name varchar_ignorecase(50) not null ); create table group_authorities ( group_id bigint not null, authority varchar(50) not null, constraint fk_group_authorities_group foreign key(group_id) references groups(id) ); create table group_members ( id bigint generated by default as identity(start with 0) primary key, username varchar(50) not null, group_id bigint not null, constraint fk_group_members_group foreign key(group_id) references groups(id) ); 记住,只有在使用框架已提供的JDBC UserDetailsService实现时,才需要这些表。如果自己编写实现或不基于UserDetailsService实现AuthenticationProvider,那么只要满足接口约束,就完全可以自由地存储数据。 此表用于存储更安全的持久化令牌RememberMe实现所使用的数据。如果直接或通过命名空间使用JdbcTokenRepositoryImpl实现,那么需要此表。 create table persistent_logins ( username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null ); Spring Security ACL实现使用了4个表。 1、acl_sid,存储由ACL系统识别的安全标识符,这些标识符可以是一些独一无二的主体,也可以是用于多个主体的一批权限。 2、acl_class,定义用于ACL的域对象类型。["class"]列中存储的是对象的Java类名称。 3、acl_object_identity,存储特定域对象中的对象标识定义。 4、acl_entry,存储适用于特定对象标识和安全标识的ACL权限。 假设数据库自动为每个标识生成主键。JdbcMutableAclService必须能在acl_sid或acl_class表中创建新行时检索这些数据。它有两个属性classIdentityQuery和sidIdentityQuery,用于定义获取这些值所需的SQL,它们默认都调用identity()。 ACL的JAR包中有在HyperSQL(HSQLDB),PostgreSQL,MySQL/MariaDB,MSSQL和Oracle数据库中创建ACL模型的文件。 默认模型与框架中单元测试使用的嵌入式HSQLDB数据库一起使用。 create table acl_sid( id bigint generated by default as identity(start with 100) not null primary key, principal boolean not null, sid varchar_ignorecase(100) not null, constraint unique_uk_1 unique(sid,principal) ); create table acl_class( id bigint generated by default as identity(start with 100) not null primary key, class varchar_ignorecase(100) not null, constraint unique_uk_2 unique(class) ); create table acl_object_identity( id bigint generated by default as identity(start with 100) not null primary key, object_id_class bigint not null, object_id_identity varchar_ignorecase(36) not null, parent_object bigint, owner_sid bigint, entries_inheriting boolean not null, constraint unique_uk_3 unique(object_id_class,object_id_identity), constraint foreign_fk_1 foreign key(parent_object)references acl_object_identity(id), constraint foreign_fk_2 foreign key(object_id_class)references acl_class(id), constraint foreign_fk_3 foreign key(owner_sid)references acl_sid(id) ); create table acl_entry( id bigint generated by default as identity(start with 100) not null primary key, acl_object_identity bigint not null, ace_order int not null, sid bigint not null, mask integer not null, granting boolean not null, audit_success boolean not null, audit_failure boolean not null, constraint unique_uk_4 unique(acl_object_identity,ace_order), constraint foreign_fk_4 foreign key(acl_object_identity) references acl_object_identity(id), constraint foreign_fk_5 foreign key(sid) references acl_sid(id) ); CREATE TABLE acl_sid ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, principal BOOLEAN NOT NULL, sid VARCHAR(100) NOT NULL, UNIQUE KEY unique_acl_sid (sid, principal) ) ENGINE=InnoDB; CREATE TABLE acl_class ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, class VARCHAR(100) NOT NULL, UNIQUE KEY uk_acl_class (class) ) ENGINE=InnoDB; CREATE TABLE acl_object_identity ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, object_id_class BIGINT UNSIGNED NOT NULL, object_id_identity VARCHAR(36) NOT NULL, parent_object BIGINT UNSIGNED, owner_sid BIGINT UNSIGNED, entries_inheriting BOOLEAN NOT NULL, UNIQUE KEY uk_acl_object_identity (object_id_class, object_id_identity), CONSTRAINT fk_acl_object_identity_parent FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id), CONSTRAINT fk_acl_object_identity_class FOREIGN KEY (object_id_class) REFERENCES acl_class (id), CONSTRAINT fk_acl_object_identity_owner FOREIGN KEY (owner_sid) REFERENCES acl_sid (id) ) ENGINE=InnoDB; CREATE TABLE acl_entry ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, acl_object_identity BIGINT UNSIGNED NOT NULL, ace_order INTEGER NOT NULL, sid BIGINT UNSIGNED NOT NULL, mask INTEGER UNSIGNED NOT NULL, granting BOOLEAN NOT NULL, audit_success BOOLEAN NOT NULL, audit_failure BOOLEAN NOT NULL, UNIQUE KEY unique_acl_entry (acl_object_identity, ace_order), CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id), CONSTRAINT fk_acl_entry_acl FOREIGN KEY (sid) REFERENCES acl_sid (id) ) ENGINE=InnoDB;
4.3、 高级Web特性
4.4、 默认AccessDecisionManager
4.5、 认证管理器和命名空间
5、体系结构和实现
5.1、 技术概述
5.2、 核心服务
6、Web应用程序安全性
6.1、 安全过滤器链(Security Filter Chain)
6.2 核心安全筛选器
6.3、 Servlet API集成
6.4、 基本身份验证和摘要身份验证
6.5、 RememberMe的认证
6.6、 跨网站请求伪造(CSRF)
6.7、 CORS
6.8、 安全的HTTP响应头
6.9、会话管理
6.10、 匿名认证
6.11、 WebSocket安全性
7、授权(Authorization)
8、其它主题
9、附录