5.1.2.RELEASE
版权所有©2004-2017
目录
本节讨论Spring Security的后勤问题。
欢迎来到Spring Security社区!本节讨论如何充分利用我们庞大的社区。
如果您需要Spring Security的帮助,我们随时为您提供帮助。以下是获得帮助的一些最佳步骤:
spring-security
在https://stackoverflow.com上
提问我们欢迎您参与Spring Security项目。有很多贡献方式,包括回答StackOverflow上的问题,编写新代码,改进现有代码,协助编写文档,开发示例或教程,报告错误或简单地提出建议。
Spring Security的源代码可以在GitHub上找到https://github.com/spring-projects/spring-security/
Spring Security是在Apache 2.0许可下发布的开源软件。
您可以在Twitter上关注@SpringSecurity和Spring Security团队以及时了解最新消息。您还可以关注@SpringCentral以了解整个Spring投资组合的最新信息。
Spring Security 5.1提供了许多新功能。以下是该版本的亮点。
authorization_code
给予支持
client_credentials
给予支持
RequestMatcher
选择AccessDeniedHandler
添加了OAuth2支持
@WithUserDetails
现在适用于ReactiveUserDetailsService
添加了对以下HTTP标头的支持
errorOnInvalidType
本节讨论了获取Spring Security二进制文件时需要了解的所有信息。有关如何获取源代码,请参见第1.3节“源代码”。
Spring Security版本的格式为MAJOR.MINOR.PATCH
像大多数开源项目一样,Spring Security将其依赖项部署为Maven工件。以下部分提供了有关如何在使用Maven时使用Spring Security的详细信息。
Spring Boot提供了一个spring-boot-starter-security启动程序,它将Spring Security相关的依赖项聚合在一起。利用启动器的最简单和首选方法是使用IDE集成(Eclipse,IntelliJ,NetBeans)或通过https://start.spring.io使用Spring Initializr。
或者,可以手动添加启动器:
pom.xml中。
<dependencies> <!-- ... other dependency elements ... --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
由于Spring Boot提供Maven BOM来管理依赖版本,因此无需指定版本。如果您希望覆盖Spring Security版本,可以通过提供Maven属性来实现:
pom.xml中。
<properties> <!-- ... --> <spring-security.version>5.1.2.RELEASE</spring-security.version> </dependencies>
由于Spring Security仅在主要版本中进行重大更改,因此使用Spring Security和Spring Boot的较新版本是安全的。但是,有时可能还需要更新Spring Framework的版本。这可以通过添加Maven属性轻松完成:
pom.xml中。
<properties> <!-- ... --> <spring.version>5.1.3.RELEASE</spring.version> </dependencies>
如果您正在使用LDAP,OpenID等其他功能,则还需要包含相应的第4章“ 项目模块”。
使用不带Spring Boot的Spring Security时,首选方法是利用Spring Security的BOM来确保在整个项目中使用Spring Security的一致版本。
pom.xml中。
<dependencyManagement> <dependencies> <!-- ... other dependency elements ... --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-bom</artifactId> <version>5.1.2.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
最小Spring Security Maven依赖项集通常如下所示:
pom.xml中。
<dependencies> <!-- ... other dependency elements ... --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> </dependencies>
如果您正在使用LDAP,OpenID等其他功能,则还需要包含相应的第4章“ 项目模块”。
Spring Security建立在Spring Framework 5.1.3.RELEASE之上,但通常应该适用于任何较新版本的Spring Framework 5.x许多用户将遇到的问题是Spring Security的传递依赖性解决了Spring Framework 5.1.3.RELEASE可能导致奇怪的类路径问题。解决此问题的最简单方法是使用pom.xml
的<dependencyManagement>
部分中的spring-framework-bom
,如下所示:
pom.xml中。
<dependencyManagement> <dependencies> <!-- ... other dependency elements ... --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>5.1.3.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
这将确保Spring Security的所有传递依赖性使用Spring 5.1.3.RELEASE模块。
![]() | 注意 |
---|---|
这种方法使用Maven的“物料清单”(BOM)概念,仅适用于Maven 2.0.9+。有关如何解析依赖关系的其他详细信息,请参阅Maven的依赖关系机制简介文档。 |
所有GA版本(即以.RELEASE结尾的版本)都部署到Maven Central,因此不需要在您的pom中声明其他Maven存储库。
如果您使用的是SNAPSHOT版本,则需要确保定义了Spring快照存储库,如下所示:
pom.xml中。
<repositories> <!-- ... possibly other repository elements ... --> <repository> <id>spring-snapshot</id> <name>Spring Snapshot Repository</name> <url>https://repo.spring.io/snapshot</url> </repository> </repositories>
如果您使用里程碑或候选发布版本,则需要确保定义了Spring Milestone存储库,如下所示:
pom.xml中。
<repositories> <!-- ... possibly other repository elements ... --> <repository> <id>spring-milestone</id> <name>Spring Milestone Repository</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories>
像大多数开源项目一样,Spring Security将其依赖关系部署为Maven工件,允许获得一流的Gradle支持。以下部分提供有关如何在使用Gradle时使用Spring Security的详细信息。
Spring Boot提供了一个spring-boot-starter-security启动程序,它将Spring Security相关的依赖项聚合在一起。利用启动器的最简单和首选方法是使用IDE集成(Eclipse,IntelliJ,NetBeans)或通过https://start.spring.io使用Spring Initializr。
或者,可以手动添加启动器:
的build.gradle。
dependencies {
compile "org.springframework.boot:spring-boot-starter-security"
}
由于Spring Boot提供Maven BOM来管理依赖版本,因此无需指定版本。如果您希望覆盖Spring Security版本,可以通过提供Gradle属性来执行此操作:
的build.gradle。
ext['spring-security.version']='5.1.2.RELEASE'
由于Spring Security仅在主要版本中进行重大更改,因此使用Spring Security和Spring Boot的较新版本是安全的。但是,有时可能还需要更新Spring Framework的版本。这也可以通过添加Gradle属性轻松完成:
的build.gradle。
ext['spring.version']='5.1.3.RELEASE'
如果您正在使用LDAP,OpenID等其他功能,则还需要包含相应的第4章“ 项目模块”。
使用不带Spring Boot的Spring Security时,首选方法是利用Spring Security的BOM来确保在整个项目中使用Spring Security的一致版本。这可以通过使用Dependency Management Plugin来完成。
的build.gradle。
plugins { id "io.spring.dependency-management" version "1.0.6.RELEASE" } dependencyManagement { imports { mavenBom 'org.springframework.security:spring-security-bom:5.1.2.RELEASE' } }
最小Spring Security Maven依赖项集通常如下所示:
的build.gradle。
dependencies { compile "org.springframework.security:spring-security-web" compile "org.springframework.security:spring-security-config" }
如果您正在使用LDAP,OpenID等其他功能,则还需要包含相应的第4章“ 项目模块”。
Spring Security建立在Spring Framework 5.1.3.RELEASE之上,但通常应该适用于任何较新版本的Spring Framework 5.x许多用户将遇到的问题是Spring Security的传递依赖性解决了Spring Framework 5.1.3.RELEASE可能导致奇怪的类路径问题。解决此问题的最简单方法是在pom.xml
的<dependencyManagement>
部分中使用spring-framework-bom
,如下所示:这可以通过使用依赖关系管理插件来完成。
的build.gradle。
plugins { id "io.spring.dependency-management" version "1.0.6.RELEASE" } dependencyManagement { imports { mavenBom 'org.springframework:spring-framework-bom:5.1.3.RELEASE' } }
这将确保Spring Security的所有传递依赖性使用Spring 5.1.3.RELEASE模块。
所有GA版本(即以.RELEASE结尾的版本)都部署到Maven Central,因此使用mavenCentral()存储库足以支持GA版本。
的build.gradle。
repositories { mavenCentral() }
如果您使用的是SNAPSHOT版本,则需要确保定义了Spring快照存储库,如下所示:
的build.gradle。
repositories {
maven { url 'https://repo.spring.io/snapshot' }
}
如果您使用里程碑或候选发布版本,则需要确保定义了Spring Milestone存储库,如下所示:
的build.gradle。
repositories {
maven { url 'https://repo.spring.io/milestone' }
}
在Spring Security 3.0中,代码库被细分为单独的jar,它们更清楚地区分不同的功能区域和第三方依赖项。如果您使用Maven构建项目,那么这些是您将添加到pom.xml
的模块。即使您没有使用Maven,我们也建议您查阅pom.xml
文件以了解第三方依赖项和版本。或者,一个好主意是检查示例应用程序中包含的库。
包含核心身份验证和access-contol类和接口,远程支持和基本配置API。任何使用Spring Security的应用程序都需要。支持独立应用程序,远程客户端,方法(服务层)安全性和JDBC用户配置。包含顶级包:
org.springframework.security.core
org.springframework.security.access
org.springframework.security.authentication
org.springframework.security.provisioning
提供与Spring Remoting的整合。除非您正在编写使用Spring远程处理的远程客户端,否则您不需要此操作。主要包是org.springframework.security.remoting
。
包含过滤器和相关的web - 安全基础结构代码。任何具有servlet API依赖性的东西。如果您需要Spring Security web身份验证服务和基于URL的访问控制,则需要它。主要包是org.springframework.security.web
。
包含安全名称空间解析代码和Java配置代码。如果您使用Spring Security XML命名空间进行配置或Spring Security的Java配置支持,则需要它。主要包是org.springframework.security.config
。这些类都不打算直接用于应用程序。
LDAP身份验证和配置代码。如果需要使用LDAP身份验证或管理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登录和/或OAuth客户端支持。顶级包是org.springframework.security.oauth2.client
。
spring-security-oauth2-jose.jar
包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。的圣何塞框架旨在提供安全地传输双方之间的权利要求的方法。它由一系列规范构建:
它包含顶级包:
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服务器对用户进行身份验证。org.springframework.security.openid
.需要OpenID4Java。
项目中有几个示例web应用程序。为避免过大的下载,分发zip文件中仅包含“教程”和“联系人”示例。其他可以直接从您可以获得的源构建,如介绍中所述。自己构建项目很容易,有关项目web站点http://spring.io/spring-security/的更多信息。本章中提到的所有路径都与项目源目录相关。
教程示例是一个很好的基本示例,可帮助您入门。它始终使用简单的命名空间配置 已编译的应用程序包含在分发zip文件中,可以部署到web容器(spring-security-samples-tutorial-3.1.x.war
)中。基于表单的身份验证机制与常用的记住我身份验证提供程序结合使用,以使用cookie自动记住登录。
我们建议您从教程示例开始,因为XML很小且易于遵循。最重要的是,您可以轻松地将这一个XML文件(及其相应的web.xml
条目)添加到现有应用程序中。只有在实现此基本集成时,我们才建议您尝试添加方法授权或域对象安全性。
Contacts Sample是一个高级示例,它说明了除基本应用程序安全性之外的域对象访问控制列表(ACL)的更强大功能。该应用程序提供了一个界面,用户可以使用该界面管理简单的联系人数据库(域对象)。
要进行部署,只需将WAR文件从Spring Security发行版复制到容器的webapps
目录中。战争应该被称为spring-security-samples-contacts-3.1.x.war
(附加的版本号将根据您使用的版本而有所不同)。
启动容器后,检查应用程序是否可以加载。访问http:// localhost:8080 / contacts(或适用于您的web容器和您部署的WAR的URL)。
接下来,单击“调试”。系统将提示您进行身份验证,并在该页面上建议一系列用户名和密码。只需使用其中任何一个进行身份验证即可查看生成的页面。它应包含类似于以下内容的成功消息:
Security Debug Information Authentication object is of type: org.springframework.security.authentication.UsernamePasswordAuthenticationToken Authentication object as a String: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@1f127853: Principal: org.springframework.security.core.userdetails.User@b07ed00: Username: rod; \ Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; \ Granted Authorities: ROLE_SUPERVISOR, ROLE_USER; \ Password: [PROTECTED]; Authenticated: true; \ Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0: \ RemoteIpAddress: 127.0.0.1; SessionId: 8fkp8t83ohar; \ Granted Authorities: ROLE_SUPERVISOR, ROLE_USER Authentication object holds the following granted authorities: ROLE_SUPERVISOR (getAuthority(): ROLE_SUPERVISOR) ROLE_USER (getAuthority(): ROLE_USER) Success! Your web filters appear to be properly configured!
成功收到上述消息后,返回示例应用程序的主页并单击“管理”。然后,您可以试用该应用程序。请注意,仅显示当前登录用户可用的联系人,并且只有ROLE_SUPERVISOR
的用户才有权删除其联系人。在幕后,MethodSecurityInterceptor
正在保护业务对象。
该应用程序允许您修改与不同联系人关联的访问控制列表。请务必通过查看应用程序上下文XML文件来尝试并了解其工作原理。
OpenID示例演示了如何使用命名空间配置OpenID以及如何为Google,Yahoo和MyOpenID身份提供程序设置属性交换配置(如果愿意,可以尝试添加其他配置)。它使用基于JQuery的openid-selector项目来提供用户友好的登录页面,允许用户轻松选择提供者,而不是键入完整的OpenID标识符。
该应用程序与普通身份验证方案的不同之处在于,它允许任何用户访问该站点(前提是他们的OpenID身份验证成功)。第一次登录时,您将收到“欢迎[您的姓名]”消息。如果您注销并重新登录(具有相同的OpenID身份),则应更改为“欢迎回来”。这是通过使用custom UserDetailsService
,它为任何用户分配一个标准角色,并在内部将身份存储在一个地图中。显然,真正的应用程序会使用数据库。请查看源表单的更多信息。这个类还考虑了这个事实可以从不同的提供程序返回不同的属性,并构建用于相应地向用户发出的名称。
CAS示例要求您同时运行CAS服务器和CAS客户端。它不包含在发布包里,你的描述应该检查项目代码的介绍。您将在sample/cas
目录下找到相关文件。还有一个Readme.txt
文件,解释了如何直接从源代码树运行服务器和客户端,完成SSL支持。
JAAS示例是如何将JAAS LoginModule与Spring Security一起使用的非常简单的示例。如果用户名等于密码,则提供的LoginModule将成功验证用户,否则抛出LoginException。本示例中使用的AuthorityGranter始终授予角色ROLE_USER。示例应用程序还演示了如何通过将jaas-api-provision设置为“true” 来作为LoginModule返回的JAAS主题运行。
在Spring 3.1中,Spring Framework添加了对Java配置的一般支持。自Spring Security 3.2以来,Spring Security Java配置支持使用户无需使用任何XML即可轻松配置Spring Security。
如果您熟悉第7章安全命名空间配置,那么您应该发现它与安全Java配置支持之间有很多相似之处。
第一步是创建我们的Spring Security Java配置。该配置创建一个称为springSecurityFilterChain
的Servlet过滤器,它负责应用程序中的所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。您可以在下面找到Spring Security Java配置的最基本示例:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.*; import org.springframework.security.config.annotation.authentication.builders.*; import org.springframework.security.config.annotation.web.configuration.*; @EnableWebSecurity public class WebSecurityConfig implements WebMvcConfigurer { @Bean public UserDetailsService userDetailsService() throws Exception { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build()); return manager; } }
这种配置确实没什么用,但它做了很多。您可以在下面找到以下功能的摘要:
安全标头集成
与以下Servlet API方法集成
下一步是在战争中注册springSecurityFilterChain
。这可以在Java配置中使用Spring在Servlet 3.0+环境中的WebApplicationInitializer支持来完成。毫不奇怪,Spring Security提供了一个基类AbstractSecurityWebApplicationInitializer
,可确保springSecurityFilterChain
为您注册。我们使用AbstractSecurityWebApplicationInitializer
的方式取决于我们是否已经使用Spring或者Spring Security是我们应用程序中唯一的Spring组件。
如果您不使用Spring或Spring MVC,则需要将WebSecurityConfig
传入超类以确保获取配置。你可以在下面找到一个例子:
import org.springframework.security.web.context.*; public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { public SecurityWebApplicationInitializer() { super(WebSecurityConfig.class); } }
SecurityWebApplicationInitializer
将执行以下操作:
如果我们在我们的应用程序的其他地方使用Spring,我们可能已经有WebApplicationInitializer
正在加载我们的Spring配置。如果我们使用以前的配置,我们会收到错误。相反,我们应该使用现有的ApplicationContext
注册Spring Security。例如,如果我们使用Spring MVC,我们的SecurityWebApplicationInitializer
将如下所示:
import org.springframework.security.web.context.*; public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { }
这只会为应用程序中的每个URL注册springSecurityFilterChain过滤器。之后,我们将确保在我们现有的ApplicationInitializer中加载WebSecurityConfig
。例如,如果我们使用Spring MVC,它将被添加到getRootConfigClasses()
中
public class MvcWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class[] { WebSecurityConfig.class }; } // ... other overrides ... }
到目前为止,我们的WebSecurityConfig仅包含有关如何验证用户身份的信息。Spring Security如何知道我们要求所有用户都经过身份验证?Spring Security如何知道我们想要支持基于表单的身份验证?原因是WebSecurityConfigurerAdapter
在configure(HttpSecurity http)
方法中提供了一个默认配置,如下所示:
protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .httpBasic(); }
上面的默认配置:
您会注意到此配置与XML命名空间配置非常相似:
<http> <intercept-url pattern="/**" access="authenticated"/> <form-login /> <http-basic /> </http>
使用and()
方法表示关闭XML标记的Java配置,它允许我们继续配置父标记。如果您阅读代码,它也是有道理的。我想配置授权请求并配置表单登录并配置HTTP基本身份验证。
当您被提示登录时,您可能想知道登录表单的来源,因为我们没有提及任何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").permitAll();
}
更新的配置指定登录页面的位置。 | |
我们必须授予所有用户(即未经身份验证的用户)访问我们的登录页面的权限。 |
使用JSP为我们当前配置实现的示例登录页面如下所示:
![]() | 注意 |
---|---|
下面的登录页面代表我们当前的配置。如果某些默认设置不符合我们的需求,我们可以轻松更新配置。 |
<c:url value="/login" var="loginUrl"/> <form action="${loginUrl}" method="post"><c:if test="${param.error != null}">
<p> Invalid username and password. </p> </c:if> <c:if test="${param.logout != null}">
<p> You have been logged out. </p> </c:if> <p> <label for="username">Username</label> <input type="text" id="username" name="username"/>
</p> <p> <label for="password">Password</label> <input type="password" id="password" name="password"/>
</p> <input type="hidden"
name="${_csrf.parameterName}" value="${_csrf.token}"/> <button type="submit" class="btn">Log in</button> </form>
对 | |
如果查询参数 | |
如果查询参数 | |
用户名必须作为名为username的HTTP参数出现 | |
密码必须作为名为password的HTTP参数出现 | |
我们必须在“包含CSRF令牌”一节中了解更多信息,请参阅第10.6节“跨站点请求伪造(CSRF)”部分的参考 |
我们的示例仅要求用户进行身份验证,并且已针对应用程序中的每个URL进行了身份验证。我们可以通过向http.authorizeRequests()
方法添加多个子项来指定网址的自定义要求。例如:
protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests().antMatchers("/resources/**", "/signup", "/about").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().authenticated()
.and() // ... .formLogin(); }
| |
我们指定了任何用户都可以访问的多种URL模式。具体来说,如果URL以“/ resources /”开头,等于“/ signup”或等于“/ about”,则任何用户都可以访问请求。 | |
任何以“/ admin /”开头的URL都将仅限于具有“ROLE_ADMIN”角色的用户。您会注意到,由于我们正在调用 | |
任何以“/ db /”开头的URL都要求用户同时拥有“ROLE_ADMIN”和“ROLE_DBA”。您会注意到,由于我们使用的是 | |
任何尚未匹配的URL只需要对用户进行身份验证 |
使用WebSecurityConfigurerAdapter
时,会自动应用注销功能。默认情况下,访问URL /logout
将通过以下方式记录用户:
SecurityContextHolder
/login?logout
但是,与配置登录功能类似,您还可以使用各种选项来进一步自定义注销要求:
protected void configure(HttpSecurity http) throws Exception { http .logout().logoutUrl("/my/logout")
.logoutSuccessUrl("/my/index")
.logoutSuccessHandler(logoutSuccessHandler)
.invalidateHttpSession(true)
.addLogoutHandler(logoutHandler)
.deleteCookies(cookieNamesToClear)
.and() ... }
提供注销支持。使用 | |
触发注销的URL(默认为 | |
注销后重定向到的URL。默认值为 | |
我们指定一个自定义 | |
指定在注销时是否使 | |
添加 | |
允许指定在注销成功时删除的cookie的名称。这是显式添加 |
![]() | 注意 |
---|---|
===当然也可以使用XML Namespace表示法配置注销。有关更多详细信息,请参阅Spring Security XML命名空间部分中的logout元素的文档。=== |
通常,为了自定义注销功能,您可以添加LogoutHandler
和/或LogoutSuccessHandler
实现。对于许多常见场景,这些处理程序在使用流畅的API时应用于幕后。
通常,LogoutHandler
实现表示能够参与注销处理的类。预计将调用它们以进行必要的清理。因此,他们不应该抛出异常。提供了各种实现:
有关详细信息,请参见第10.5.4节“记住我的接口和实现”。
而不是直接提供LogoutHandler
实现,流畅的API还提供了快捷方式,提供了各自的LogoutHandler
实现。例如,deleteCookies()
允许指定在注销成功时删除的一个或多个cookie的名称。与添加CookieClearingLogoutHandler
相比,这是一种捷径。
在LogoutFilter
成功注销后调用LogoutSuccessHandler
,以处理例如重定向或转发到适当的目的地。请注意,该接口几乎与LogoutHandler
相同,但可能引发异常。
提供以下实现:
如上所述,您无需直接指定SimpleUrlLogoutSuccessHandler
。相反,fluent API通过设置logoutSuccessUrl()
来提供快捷方式。这将设置SimpleUrlLogoutSuccessHandler
。发生注销后,提供的URL将重定向到。默认值为/login?logout
。
在REST API类型场景中,HttpStatusReturningLogoutSuccessHandler
可能很有趣。成功注销后,LogoutSuccessHandler
允许您提供要返回的纯HTTP状态代码,而不是在成功注销时重定向到URL。如果未配置,则默认返回状态代码200。
OAuth 2.0客户端功能为OAuth 2.0授权框架中定义的客户端角色提供支持。
可以使用以下主要功能:
WebClient
扩展(用于发出受保护的资源请求)
HttpSecurity.oauth2Client()
提供了许多用于自定义OAuth 2.0 Client的配置选项。以下代码显示了oauth2Client()
DSL可用的完整配置选项:
@EnableWebSecurity public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Client() .clientRegistrationRepository(this.clientRegistrationRepository()) .authorizedClientRepository(this.authorizedClientRepository()) .authorizedClientService(this.authorizedClientService()) .authorizationCodeGrant() .authorizationRequestRepository(this.authorizationRequestRepository()) .authorizationRequestResolver(this.authorizationRequestResolver()) .accessTokenResponseClient(this.accessTokenResponseClient()); } }
以下部分详细介绍了每种可用的配置选项:
ClientRegistration
表示在OAuth 2.0或OpenID Connect 1.0提供程序中注册的客户端。
客户端注册保存信息,例如客户端ID,客户端密钥,授权授权类型,重定向URI,范围,授权URI,令牌URI和其他详细信息。
ClientRegistration
及其属性定义如下:
public final class ClientRegistration { private String registrationId;private String clientId;
private String clientSecret;
private ClientAuthenticationMethod clientAuthenticationMethod;
private AuthorizationGrantType authorizationGrantType;
private String redirectUriTemplate;
private Set<String> scopes;
private ProviderDetails providerDetails; private String clientName;
public class ProviderDetails { private String authorizationUri;
private String tokenUri;
private UserInfoEndpoint userInfoEndpoint; private String jwkSetUri;
private Map<String, Object> configurationMetadata;
public class UserInfoEndpoint { private String uri;
private AuthenticationMethod authenticationMethod;
private String userNameAttributeName;
} } }
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
ClientRegistrationRepository
充当OAuth 2.0 / OpenID Connect 1.0 ClientRegistration
的存储库。
![]() | 注意 |
---|---|
客户端注册信息最终由关联的授权服务器存储和拥有。此存储库提供检索主客户端注册信息的子集的功能,该子集与授权服务器一起存储。 |
Spring Boot 2.x auto-configuration将spring.security.oauth2.client.registration.[registrationId]
下的每个属性绑定到ClientRegistration
的实例,然后在ClientRegistrationRepository
内组成每个ClientRegistration
个实例。
![]() | 注意 |
---|---|
|
自动配置还在ApplicationContext
中将ClientRegistrationRepository
注册为@Bean
,以便在应用程序需要时可用于依赖注入。
以下清单显示了一个示例:
@Controller public class OAuth2ClientController { @Autowired private ClientRegistrationRepository clientRegistrationRepository; @RequestMapping("/") public String index() { ClientRegistration googleRegistration = this.clientRegistrationRepository.findByRegistrationId("google"); ... return "index"; } }
OAuth2AuthorizedClient
是授权客户的代表。当最终用户(资源所有者)已授权客户端访问其受保护资源时,将认为客户端已获得授权。
OAuth2AuthorizedClient
用于将OAuth2AccessToken
(和可选的OAuth2RefreshToken
)与ClientRegistration
(客户端)和资源所有者相关联,后者是授予授权的Principal
最终用户。
OAuth2AuthorizedClientRepository
负责在web个请求之间保持OAuth2AuthorizedClient
个。鉴于OAuth2AuthorizedClientService
的主要作用是在应用程序级别管理OAuth2AuthorizedClient
(s)。
从开发人员的角度来看,OAuth2AuthorizedClientRepository
或OAuth2AuthorizedClientService
提供了查找与客户端关联的OAuth2AccessToken
的功能,以便可以使用它来启动受保护的资源请求。
![]() | 注意 |
---|---|
Spring Boot 2.x自动配置在 |
开发人员还可以在ApplicationContext
中注册OAuth2AuthorizedClientRepository
或OAuth2AuthorizedClientService
@Bean
(覆盖Spring Boot 2.x自动配置),以便能够查找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 oauth2Login() flow OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient( authentication.getAuthorizedClientRegistrationId(), authentication.getName()); OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); ... return "userinfo"; } }
@RegisteredOAuth2AuthorizedClient
注释提供了将方法参数解析为类型OAuth2AuthorizedClient
的参数值的功能。与通过OAuth2AuthorizedClientService
查找OAuth2AuthorizedClient
相比,这是一个方便的选择。
@Controller public class OAuth2LoginController { @RequestMapping("/userinfo") public String userinfo(@RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient) { OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); ... return "userinfo"; } }
@RegisteredOAuth2AuthorizedClient
注释由OAuth2AuthorizedClientArgumentResolver
处理,并提供以下功能:
如果客户端尚未获得授权,将自动请求OAuth2AccessToken
。
authorization_code
,这涉及触发授权请求重定向以启动流
client_credentials
,使用DefaultClientCredentialsTokenResponseClient
直接从令牌端点获取访问令牌
从授权请求启动到收到授权响应时(回调),AuthorizationRequestRepository
负责OAuth2AuthorizationRequest
的持久性。
![]() | 小费 |
---|---|
|
AuthorizationRequestRepository
的默认实现是HttpSessionOAuth2AuthorizationRequestRepository
,它将OAuth2AuthorizationRequest
存储在HttpSession
中。
如果您想提供AuthorizationRequestRepository
的自定义实现,将OAuth2AuthorizationRequest
的属性存储在Cookie
中,您可以按以下示例所示进行配置:
@EnableWebSecurity public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Client() .authorizationCodeGrant() .authorizationRequestRepository(this.cookieAuthorizationRequestRepository()) ... } private AuthorizationRequestRepository<OAuth2AuthorizationRequest> cookieAuthorizationRequestRepository() { return new HttpCookieOAuth2AuthorizationRequestRepository(); } }
OAuth2AuthorizationRequestResolver
的主要作用是从提供的web请求中解析OAuth2AuthorizationRequest
。默认实现DefaultOAuth2AuthorizationRequestResolver
匹配(默认)路径/oauth2/authorization/{registrationId}
,提取registrationId
并使用它来为关联的ClientRegistration
构建OAuth2AuthorizationRequest
。
OAuth2AuthorizationRequestResolver
可以实现的主要用例之一是能够使用超出OAuth 2.0授权框架中定义的标准参数的附加参数来定制授权请求。
例如,OpenID Connect为授权代码流定义了额外的OAuth 2.0请求参数,这些参数扩展自OAuth 2.0授权框架中定义的标准参数。其中一个扩展参数是prompt
参数。
![]() | 注意 |
---|---|
可选的。空格分隔,区分大小写的ASCII字符串值列表,指定授权服务器是否提示最终用户进行重新认证和同意。定义的值为:none,login,consent,select_account |
以下示例显示如何通过包含请求参数prompt=consent
来实现OAuth2AuthorizationRequestResolver
来自定义oauth2Login()
的授权请求。
@EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private ClientRegistrationRepository clientRegistrationRepository; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login() .authorizationEndpoint() .authorizationRequestResolver( new CustomAuthorizationRequestResolver( this.clientRegistrationRepository));} } public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver; public CustomAuthorizationRequestResolver( ClientRegistrationRepository clientRegistrationRepository) { this.defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( clientRegistrationRepository, "/oauth2/authorization"); } @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { OAuth2AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request);
return authorizationRequest != null ?
customAuthorizationRequest(authorizationRequest) : null; } @Override public OAuth2AuthorizationRequest resolve( HttpServletRequest request, String clientRegistrationId) { OAuth2AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve( request, clientRegistrationId);
return authorizationRequest != null ?
customAuthorizationRequest(authorizationRequest) : null; } private OAuth2AuthorizationRequest customAuthorizationRequest( OAuth2AuthorizationRequest authorizationRequest) { Map<String, Object> additionalParameters = new LinkedHashMap<>(authorizationRequest.getAdditionalParameters()); additionalParameters.put("prompt", "consent");
return OAuth2AuthorizationRequest.from(authorizationRequest)
.additionalParameters(additionalParameters)
.build(); } }
配置自定义 | |
尝试使用 | |
如果解决了 | |
将自定义参数添加到现有 | |
创建默认 | |
覆盖默认 |
![]() | 小费 |
---|---|
|
上面的示例显示了在标准参数之上添加自定义参数的常见用例。但是,如果您需要删除或更改标准参数或者您的要求更高级,则可以通过简单地覆盖OAuth2AuthorizationRequest.authorizationRequestUri
属性来完全控制构建授权请求URI。
以下示例显示了前一个示例中customAuthorizationRequest()
方法的变体,而是覆盖了OAuth2AuthorizationRequest.authorizationRequestUri
属性。
private OAuth2AuthorizationRequest customAuthorizationRequest( OAuth2AuthorizationRequest authorizationRequest) { String customAuthorizationRequestUri = UriComponentsBuilder .fromUriString(authorizationRequest.getAuthorizationRequestUri()) .queryParam("prompt", "consent") .build(true) .toUriString(); return OAuth2AuthorizationRequest.from(authorizationRequest) .authorizationRequestUri(customAuthorizationRequestUri) .build(); }
OAuth2AccessTokenResponseClient
的主要作用是在授权服务器的令牌端点处为访问令牌凭证交换授权授予凭证。
authorization_code
授权的OAuth2AccessTokenResponseClient
的默认实现是DefaultAuthorizationCodeTokenResponseClient
,它使用RestOperations
在令牌端点交换访问令牌的授权码。
DefaultAuthorizationCodeTokenResponseClient
非常灵活,因为它允许您自定义令牌请求的预处理和/或令牌响应的后处理。
如果您需要自定义令牌请求的预处理,则可以为DefaultAuthorizationCodeTokenResponseClient.setRequestEntityConverter()
提供自定义Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>>
。默认实现OAuth2AuthorizationCodeGrantRequestEntityConverter
构建标准OAuth 2.0访问令牌请求的RequestEntity
表示。但是,提供自定义Converter
将允许您扩展标准令牌请求并添加自定义参数。
![]() | 重要 |
---|---|
自定义 |
另一方面,如果您需要自定义令牌响应的后处理,则需要为DefaultAuthorizationCodeTokenResponseClient.setRestOperations()
提供自定义配置的RestOperations
。默认RestOperations
配置如下:
RestTemplate restTemplate = new RestTemplate(Arrays.asList( new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter())); restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
![]() | 小费 |
---|---|
发送OAuth 2.0访问令牌请求时使用Spring MVC |
对于OAuth 2.0访问令牌响应,OAuth2AccessTokenResponseHttpMessageConverter
是HttpMessageConverter
。您可以为OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
提供自定义Converter<Map<String, String>, OAuth2AccessTokenResponse>
,用于将OAuth 2.0访问令牌响应参数转换为OAuth2AccessTokenResponse
。
OAuth2ErrorResponseErrorHandler
是ResponseErrorHandler
,可以处理OAuth 2.0错误(400错误请求)。它使用OAuth2ErrorHttpMessageConverter
将OAuth 2.0 Error参数转换为OAuth2Error
。
无论您是自定义DefaultAuthorizationCodeTokenResponseClient
还是提供自己的OAuth2AccessTokenResponseClient
实现,都需要对其进行配置,如以下示例所示:
@EnableWebSecurity public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Client() .authorizationCodeGrant() .accessTokenResponseClient(this.customAccessTokenResponseClient()) ... } private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> customAccessTokenResponseClient() { ... } }
OAuth 2.0登录功能为应用程序提供了使用OAuth 2.0提供程序(例如GitHub)或OpenID Connect 1.0提供程序(例如Google)上的现有帐户登录应用程序的功能。OAuth 2.0 Login实现了用例:“使用Google登录”或“使用GitHub登录”。
![]() | 注意 |
---|---|
OAuth 2.0登录是使用授权代码授予实现的,如OAuth 2.0授权框架和OpenID Connect Core 1.0中所指定。 |
Spring Boot 2.x为OAuth 2.0登录带来了完整的自动配置功能。
本节介绍如何使用Google作为身份验证提供程序配置OAuth 2.0登录示例,并介绍以下主题:
要使用Google的OAuth 2.0身份验证系统进行登录,您必须在Google API控制台中设置项目以获取OAuth 2.0凭据。
![]() | 注意 |
---|---|
Google的OAuth 2.0身份验证实施符合OpenID Connect 1.0规范,并通过OpenID认证。 |
按照OpenID Connect页面上的说明操作,从“设置OAuth 2.0”部分开始。
完成“获取OAuth 2.0凭据”说明后,您应该拥有一个新的OAuth客户端,其中包含客户端ID和客户端密钥的凭据。
重定向URI是应用程序中的路径,最终用户的用户代理在使用Google进行身份验证并在“同意”页面上授予了对OAuth客户端(在上一步中创建)的访问权限后重定向回的路径。
在“设置重定向URI”子部分中,确保将“ 授权重定向URI”字段设置为http://localhost:8080/login/oauth2/code/google
。
![]() | 小费 |
---|---|
默认重定向URI模板为 |
现在您已经有了一个新的OAuth客户端与Google,您需要配置应用程序以使用OAuth客户端进行身份验证流程。为此:
转到application.yml
并设置以下配置:
spring: security: oauth2: client: registration:google:
client-id: google-client-id client-secret: google-client-secret
例6.1。OAuth客户端属性
| |
基本属性前缀后面是ClientRegistration的ID ,例如google。 |
client-id
和client-secret
属性中的值。
启动Spring Boot 2.x示例并转到http://localhost:8080
。然后,您将被重定向到默认的自动生成的登录页面,该页面显示Google的链接。
点击Google链接,然后您将重定向到Google进行身份验证。
使用您的Google帐户凭据进行身份验证后,显示给您的下一页是“同意”屏幕。“同意”屏幕会要求您允许或拒绝访问您之前创建的OAuth客户端。单击“ 允许”以授权OAuth客户端访问您的电子邮件地址和基本配置文件信息。
此时,OAuth客户端从UserInfo端点检索您的电子邮件地址和基本配置文件信息,并建立经过身份验证的会话。
下表概述了Spring Boot 2.x OAuth客户端属性到ClientRegistration属性的映射。
Spring Boot 2.x | ClientRegistration |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CommonOAuth2Provider
为众多知名提供商预定义了一组默认客户端属性:Google,GitHub,Facebook和Okta。
例如,authorization-uri
,token-uri
和user-info-uri
对于提供商而言不会经常更改。因此,提供默认值以减少所需的配置是有意义的。
如前所述,当我们配置Google客户端时,只需要client-id
和client-secret
属性。
以下清单显示了一个示例:
spring: security: oauth2: client: registration: google: client-id: google-client-id client-secret: google-client-secret
![]() | 小费 |
---|---|
客户端属性的自动默认无缝地在这里工作,因为 |
对于您可能希望指定其他registrationId
的情况,例如google-login
,您仍然可以通过配置provider
属性来利用客户端属性的自动默认。
以下清单显示了一个示例:
spring: security: oauth2: client: registration: google-login:provider: google
client-id: google-client-id client-secret: google-client-secret
有些OAuth 2.0提供程序支持多租户,这会为每个租户(或子域)产生不同的协议端点。
例如,向Okta注册的OAuth客户端被分配给特定的子域并拥有自己的协议端点。
对于这些情况,Spring Boot 2.x提供了以下用于配置自定义提供程序属性的基本属性:spring.security.oauth2.client.provider.[providerId]
。
以下清单显示了一个示例:
spring: security: oauth2: client: registration: okta: client-id: okta-client-id client-secret: okta-client-secret provider: okta: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
OAuth客户端支持的Spring Boot 2.x自动配置类为OAuth2ClientAutoConfiguration
。
它执行以下任务:
ClientRegistration
组成的ClientRegistrationRepository
@Bean
。
WebSecurityConfigurerAdapter
@Configuration
并通过httpSecurity.oauth2Login()
启用OAuth 2.0登录。
如果您需要根据具体要求覆盖自动配置,可以通过以下方式执行此操作:
以下示例显示如何注册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(); } }
以下示例说明如何使用@EnableWebSecurity
提供WebSecurityConfigurerAdapter
并通过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.x并希望在CommonOAuth2Provider
(例如Google)中配置其中一个预定义的提供商,请应用以下配置:
@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( ClientRegistrationRepository clientRegistrationRepository) { return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); } @Bean public OAuth2AuthorizedClientRepository authorizedClientRepository( OAuth2AuthorizedClientService authorizedClientService) { return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); } private ClientRegistration googleClientRegistration() { return CommonOAuth2Provider.GOOGLE.getBuilder("google") .clientId("google-client-id") .clientSecret("google-client-secret") .build(); } }
以下附加资源描述了高级配置选项:
Spring Security支持使用JWT编码的OAuth 2.0 承载令牌保护端点。
在应用程序将其权限管理联合到授权服务器(例如,Okta或Ping Identity)的情况下,这很方便。资源服务器可以查询此授权服务器,以在提供请求时验证权限。
![]() | 注意 |
---|---|
可以在OAuth 2.0 Resource Server Servlet示例中找到完整的工作示例。 |
大多数资源服务器支持都收集到spring-security-oauth2-resource-server
。但是,对解码和验证JWT的支持是spring-security-oauth2-jose
,这意味着两者都是必要的,以便拥有一个支持JWT编码的承载令牌的工作资源服务器。
使用Spring Boot时,将应用程序配置为资源服务器包含两个基本步骤。首先,包括所需的依赖项,然后指出授权服务器的位置。
要指定要使用的授权服务器,只需执行以下操作:
security: oauth2: resourceserver: jwt: issuer-uri: https://idp.example.com
其中https://idp.example.com
是授权服务器将发出的iss
JWT令牌声明中包含的值。资源服务器将使用此属性进一步自我配置,发现授权服务器的公钥,并随后验证传入的JWT。
![]() | 注意 |
---|---|
要使用 |
就是这样!
使用此属性和这些依赖项时,Resource Server将自动配置自身以验证JWT编码的承载令牌。
它通过确定性的启动过程实现了这一点:
https://idp.example.com/.well-known/openid-configuration
,处理jwks_url
属性的响应
jwks_url
以获取有效的公钥
https://idp.example.com
验证每个JWT iss
声明。
此过程的结果是授权服务器必须启动并接收请求才能使Resource Server成功启动。
![]() | 注意 |
---|---|
如果授权服务器在资源服务器查询时关闭(给定适当的超时),则启动将失败。 |
启动应用程序后,Resource Server将尝试处理包含Authorization: Bearer
标头的任何请求:
GET / HTTP/1.1 Authorization: Bearer some-token-value # Resource Server will process this
只要指示此方案,资源服务器将尝试根据承载令牌规范处理请求。
给定格式良好的JWT令牌,资源服务器将
jwks_url
端点获取的公钥验证其签名,并与JWTs头匹配
exp
和nbf
时间戳以及JWT iss
声明,以及
SCOPE_
的权限。
![]() | 注意 |
---|---|
当授权服务器提供新密钥时,Spring Security将自动旋转用于验证JWT令牌的密钥。 |
结果Authentication#getPrincipal
默认为Spring Security Jwt
对象,Authentication#getName
映射到JWT的sub
属性(如果存在)。
从这里开始,考虑跳转到:
如果授权服务器不支持Provider Configuration端点,或者Resource Server必须能够独立于授权服务器启动,则可以将issuer-uri
交换为jwk-set-uri
:
security: oauth2: resourceserver: jwt: jwk-set-uri: https://idp.example.com/.well-known/jwks.json
![]() | 注意 |
---|---|
JWK Set uri不是标准化的,但通常可以在授权服务器的文档中找到 |
因此,资源服务器不会在启动时ping授权服务器。但是,它也将不再验证JWT中的iss
声明(因为资源服务器不再知道发行者值应该是什么)。
![]() | 注意 |
---|---|
此属性也可以直接在DSL上提供。 |
Spring Boot代表资源服务器生成了@Bean
个@Bean
。
第一个是WebSecurityConfigurerAdapter
,它将应用程序配置为资源服务器:
protected void configure(HttpSecurity http) { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2ResourceServer() .jwt(); }
如果应用程序没有公开WebSecurityConfigurerAdapter
bean,那么Spring Boot将公开上面的默认值。
替换它就像在应用程序中公开bean一样简单:
@EnableWebSecurity public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeRequests() .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyRequest().authenticated() .and() .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(myConverter()); } }
对于以/messages/
开头的任何网址,上述内容要求message:read
的范围。
oauth2ResourceServer
DSL上的方法也将覆盖或替换自动配置。
例如,第二个@Bean
Spring Boot创建的是JwtDecoder
,它将String
令牌解码为经过验证的Jwt
实例:
@Bean public JwtDecoder jwtDecoder() { return JwtDecoders.fromOidcIssuerLocation(issuerUri); }
如果应用程序没有公开JwtDecoder
bean,那么Spring Boot将公开上面的默认值。
并且可以使用jwkSetUri()
覆盖其配置或使用decoder()
替换它的配置。
授权服务器的JWK Set Uri可以配置为配置属性,也可以在DSL中提供:
@EnableWebSecurity public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2ResourceServer() .jwt() .jwkSetUri("https://idp.example.com/.well-known/jwks.json"); } }
使用jwkSetUri()
优先于任何配置属性。
比jwkSetUri()
更强大的是decoder()
,它将完全取代JwtDecoder
的任何Boot自动配置:
@EnableWebSecurity public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2ResourceServer() .jwt() .decoder(myCustomDecoder()); } }
从OAuth 2.0授权服务器发出的JWT通常具有scope
或scp
属性,表示已授予其范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,资源服务器将尝试将这些范围强制转换为已授权的权限列表,并在每个范围前添加字符串“SCOPE_”。
这意味着要使用从JWT派生的作用域保护端点或方法,相应的表达式应包含此前缀:
@EnableWebSecurity public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeRequests() .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyRequest().authenticated() .and() .oauth2ResourceServer() .jwt(); } }
或者类似于方法安全性:
@PreAuthorize("hasAuthority('SCOPE_messages')") public List<Message> getMessages(...) {}
但是,在许多情况下,此默认值不足。例如,某些授权服务器不使用scope
属性,而是拥有自己的自定义属性。或者,在其他时候,资源服务器可能需要使属性或属性的组合适应内部化的权限。
为此,DSL公开了jwtAuthenticationConverter()
:
@EnableWebSecurity public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(grantedAuthoritiesExtractor()); } } Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() { return new GrantedAuthoritiesExtractor(); }
负责将Jwt
转换为Authentication
。
我们可以简单地重写这一点,以改变授予权限的方式:
static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter { protected Collection<GrantedAuthorities> extractAuthorities(Jwt jwt) { Collection<String> authorities = (Collection<String>) jwt.getClaims().get("mycustomclaim"); return authorities.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } }
为了获得更大的灵活性,DSL支持完全用任何实现Converter<Jwt, AbstractAuthenticationToken>
的类替换转换器:
static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { public AbstractAuthenticationToken convert(Jwt jwt) { return new CustomAuthenticationToken(jwt); } }
使用最小Spring Boot配置,指示授权服务器的颁发者uri,资源服务器将默认验证iss
声明以及exp
和nbf
时间戳声明。
在需要自定义验证的情况下,Resource Server附带两个标准验证器,并且还接受自定义OAuth2TokenValidator
实例。
JWT通常有一个有效窗口,nbf
声明中指示的窗口的开头和exp
声明中指出的结尾。
但是,每个服务器都可能遇到时钟漂移,这可能导致令牌过期到一个服务器,但不会到另一个服务器。随着协作服务器数量在分布式系统中的增加,这可能会导致一些实施灼伤。
资源服务器使用JwtTimestampValidator
验证令牌的有效性窗口,并且可以使用clockSkew
配置它以缓解上述问题:
@Bean JwtDecoder jwtDecoder() { NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport) JwtDecoders.withOidcIssuerLocation(issuerUri); OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>( new JwtTimestampValidator(Duration.ofSeconds(60)), new IssuerValidator(issuerUri)); jwtDecoder.setJwtValidator(withClockSkew); return jwtDecoder; }
![]() | 注意 |
---|---|
默认情况下,资源服务器配置30秒的时钟偏差。 |
使用OAuth2TokenValidator
API添加对aud
声明的检查很简单:
public class AudienceValidator implements OAuth2TokenValidator<Jwt> { OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null); public OAuth2TokenValidatorResult validate(Jwt jwt) { if (jwt.getAudience().contains("messaging")) { return OAuth2TokenValidatorResult.success(); } else { return OAuth2TokenValidatorResult.failure(error); } } }
然后,要添加到资源服务器,需要指定JwtDecoder
实例:
@Bean JwtDecoder jwtDecoder() { NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport) JwtDecoders.withOidcIssuerLocation(issuerUri); OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(); OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); jwtDecoder.setJwtValidator(withAudience); return jwtDecoder; }
Spring Security使用Nimbus库解析JWT并验证其签名。因此,Spring Security受Nimbus对每个字段值的解释以及如何将每个字段强制转换为Java类型。
例如,因为Nimbus与Java 7兼容,所以它不使用Instant
来表示时间戳字段。
并且完全可以使用不同的库或JWT处理,这可能会使自己的强制决策需要调整。
或者,很简单,资源服务器可能希望根据特定域的原因添加或删除JWT中的声明。
出于这些目的,Resource Server支持使用MappedJwtClaimSetConverter
映射JWT声明集。
默认情况下,MappedJwtClaimSetConverter
会尝试将声明强制转换为以下类型:
Claim | Java Type |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可以使用MappedJwtClaimSetConverter.withDefaults
配置个人声明的转化策略:
@Bean JwtDecoder jwtDecoder() { NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri); MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub)); jwtDecoder.setJwtClaimSetConverter(converter); return jwtDecoder; }
这将保留所有默认值,但它将覆盖sub
的默认声明转换器。
MappedJwtClaimSetConverter
也可用于添加自定义声明,例如,以适应现有系统:
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
使用相同的API删除声明也很简单:
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
在更复杂的场景中,例如一次咨询多个声明或重命名声明,Resource Server接受任何实现Converter<Map<String, Object>, Map<String,Object>>
的类:
public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> { private final MappedJwtClaimSetConverter delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); public Map<String, Object> convert(Map<String, Object> claims) { Map<String, Object> convertedClaims = this.delegate.convert(claims); String username = (String) convertedClaims.get("user_name"); convertedClaims.put("sub", username); return convertedClaims; } }
然后,可以像平常一样提供实例:
@Bean JwtDecoder jwtDecoder() { NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri); jwtDecoder.setJwtClaimSetConverter(new UsernameSubClaimAdapter()); return jwtDecoder; }
默认情况下,Resource Server使用每个30秒的连接和套接字超时来协调授权服务器。
在某些情况下,这可能太短。此外,它没有考虑更复杂的模式,如退避和发现。
要调整Resource Server连接到授权服务器的方式,NimbusJwtDecoderJwkSupport
接受RestOperations
的实例:
@Bean public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { RestOperations rest = builder .setConnectionTimeout(60000) .setReadTimeout(60000) .build(); NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri); jwtDecoder.setRestOperations(rest); return jwtDecoder; }
到目前为止,我们只看了最基本的身份验证配置。我们来看一些稍微更高级的配置身份验证选项。
我们已经看到了为单个用户配置内存中身份验证的示例。以下是配置多个用户的示例:
@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; }
您可以找到支持基于JDBC的身份验证的更新。以下示例假定您已在应用程序中定义了DataSource
。该JDBC-javaconfig样品提供了使用基于JDBC认证的一个完整的示例。
@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的身份验证的更新。的LDAP的javaconfig样品提供了使用基于LDAP的认证的完整的例子。
@Autowired private DataSource dataSource; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .ldapAuthentication() .userDnPatterns("uid={0},ou=people") .groupSearchBase("ou=groups"); }
上面的示例使用以下LDIF和嵌入式Apache DS LDAP实例。
users.ldif。
dn: ou=groups,dc=springframework,dc=org objectclass: top objectclass: organizationalUnit ou: 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
,以下将自定义身份验证:
![]() | 注意 |
---|---|
仅在未填充 |
@Bean public SpringAuthenticationProvider springAuthenticationProvider() { return new SpringAuthenticationProvider(); }
您可以通过将自定义UserDetailsService
公开为bean来定义自定义身份验证。例如,假设SpringDataUserDetailsService
实现UserDetailsService
,以下将自定义身份验证:
![]() | 注意 |
---|---|
仅在未填充 |
@Bean public SpringDataUserDetailsService springDataUserDetailsService() { return new SpringDataUserDetailsService(); }
您还可以通过将PasswordEncoder
暴露为bean来自定义密码的编码方式。例如,如果使用bcrypt,则可以添加bean定义,如下所示:
@Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
我们可以配置多个HttpSecurity实例,就像我们可以有多个<http>
块一样。关键是多次延长WebSecurityConfigurationAdapter
。例如,以下是具有以/api/
开头的URL的不同配置的示例。
@EnableWebSecurity public class MultiHttpSecurityConfig { @Beanpublic 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)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http .antMatcher("/api/**")
.authorizeRequests() .anyRequest().hasRole("ADMIN") .and() .httpBasic(); } } @Configuration
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin(); } } }
从版本2.0开始,Spring Security大大提高了对服务层方法的安全性的支持。它支持JSR-250注释安全性以及框架的原始@Secured
注释。从3.0开始,您还可以使用基于表达式的新注释。您可以将安全性应用于单个bean,使用intercept-methods
元素来装饰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 { // ... }
这些是基于标准的,允许应用简单的基于角色的约束,但没有强大的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; } }
有关可以覆盖的方法的其他信息,请参阅GlobalMethodSecurityConfiguration
Javadoc。
Spring Security的Java配置不会公开它配置的每个对象的每个属性。这简化了大多数用户的配置。毕竟,如果每个属性都被暴露,用户可以使用标准bean配置。
虽然有充分的理由不直接公开每个属性,但用户可能仍需要更高级的配置选项。为了解决这个问题,Spring Security引入了ObjectPostProcessor
的概念,可以用来修改或替换Java配置创建的许多Object实例。例如,如果要在FilterSecurityInterceptor
上配置filterSecurityPublishAuthorizationSuccess
属性,可以使用以下命令:
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { public <O extends FilterSecurityInterceptor> O postProcess( O fsi) { fsi.setPublishAuthorizationSuccess(true); return fsi; } }); }
您可以在Spring Security中提供自己的自定义DSL。例如,您可能看起来像这样:
public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> { 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(); } }
![]() | 注意 |
---|---|
这实际上是如何实现像 |
然后可以像这样使用自定义DSL:
@EnableWebSecurity public class Config extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .apply(customDsl()) .flag(true) .and() ...; } }
代码按以下顺序调用:
如果需要,您可以WebSecurityConfiguerAdapter
默认使用SpringFactories
添加MyCustomDsl
。例如,您将在名为META-INF/spring.factories
的类路径上创建一个资源,其中包含以下内容:
META-INF / spring.factories。
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyCustomDsl
希望禁用默认值的用户可以明确地这样做。
@EnableWebSecurity public class Config extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .apply(customDsl()).disable() ...; } }
自Spring Framework版本2.0起,命名空间配置已可用。它允许您使用其他XML模式中的元素来补充传统的Spring bean应用程序上下文语法。您可以在Spring 参考文档中找到更多信息。命名空间元素可以简单地用于允许更简洁的方式来配置单个bean,或者更有力地用于定义替代配置语法,该语法更紧密地匹配问题域并且隐藏用户的底层复杂性。一个简单的元素可能会隐藏多个bean和处理步骤被添加到应用程序上下文的事实。例如,将以下元素从安全名称空间添加到应用程序上下文将启动嵌入式LDAP服务器,以便在应用程序中测试使用:
<security:ldap-server />
这比连接等效的Apache Directory Server bean简单得多。ldap-server
元素上的属性支持最常见的替代配置要求,并且用户不必担心他们需要创建哪些bean以及bean属性名称是什么。
[1]。在编辑应用程序上下文文件时使用良好的XML编辑器应该提供有关可用属性和元素的信息。我们建议您试用Spring工具套件,因为它具有处理标准Spring命名空间的特殊功能。
要在应用程序上下文中开始使用安全命名空间,您需要在类路径上使用spring-security-config
jar。然后,您需要做的就是将架构声明添加到应用程序上下文文件中:
<beans xmlns="http://www.springframework.org/schema/beans" 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"> ... </beans>
在您将看到的许多示例中(以及示例应用程序中),我们经常使用“security”作为默认命名空间而不是“beans”,这意味着我们可以在所有安全命名空间元素上省略前缀,从而制作内容更容易阅读。如果您将应用程序上下文划分为单独的文件并在其中一个文件中包含大部分安全配置,则可能还需要执行此操作。然后,您的安全应用程序上下文文件将如下所示
<beans:beans xmlns="http://www.springframework.org/schema/security" 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"> ... </beans:beans>
我们假设从现在开始在本章中使用了这种语法。
命名空间旨在捕获框架的最常见用法,并提供简化和简洁的语法,以便在应用程序中启用它们。该设计基于框架内的大规模依赖性,可分为以下几个方面:
我们将在以下部分中看到如何配置它们。
在本节中,我们将介绍如何构建命名空间配置以使用框架的一些主要功能。假设您最初希望尽快启动并运行,并将认证支持和访问控制添加到现有的web应用程序,并进行一些测试登录。然后,我们将了解如何更改以对数据库或其他安全存储库进行身份验证。在后面的部分中,我们将介绍更高级的命名空间配置选项。
您需要做的第一件事是将以下过滤器声明添加到您的web.xml
文件中:
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
这为Spring Security web基础设施提供了一个钩子。DelegatingFilterProxy
是一个Spring Framework类,它委托给一个过滤器实现,在你的应用程序上下文中定义为一个Spring bean。在这种情况下,bean被命名为“springSecurityFilterChain”,它是由命名空间创建的内部基础结构bean,用于处理web安全性。请注意,您不应自己使用此bean名称。将此添加到web.xml
后,您就可以开始编辑应用程序上下文文件了。使用<http>
元素配置Web安全服务。
启用web安全性所需的全部内容是
<http> <intercept-url pattern="/**" access="hasRole('USER')" /> <form-login /> <logout /> </http>
这表示我们希望应用程序中的所有URL都是安全的,需要角色ROLE_USER
来访问它们,我们希望使用带有用户名和密码的表单登录到应用程序,并且我们希望注册的注销URL将允许我们退出应用程序。<http>
元素是所有与web相关的命名空间功能的父元素。<intercept-url>
元素定义了pattern
,它使用ant路径样式语法[2]与传入请求的URL进行匹配。您还可以使用正则表达式匹配作为替代方法(有关详细信息,请参阅命名空间附录)。access
属性定义与给定模式匹配的请求的访问要求。使用默认配置时,这通常是以逗号分隔的角色列表,其中一个角色必须允许用户发出请求。前缀“ROLE_”是一个标记,表示应该与用户的权限进行简单比较。换句话说,应该使用正常的基于角色的检查。Spring Security中的访问控制不仅限于使用简单角色(因此使用前缀来区分不同类型的安全属性)。稍后我们将看到解释如何变化脚注:[access
属性中逗号分隔值的解释取决于所使用的-1-的实现。在Spring Security 3.0中,该属性也可以用-2-填充。
![]() | 注意 |
---|---|
=== |
您可以使用多个<intercept-url>
元素为不同的URL集定义不同的访问要求,但它们将按列出的顺序进行评估,并将使用第一个匹配项。所以你必须把最具体的比赛放在最上面。您还可以添加method
属性以限制与特定HTTP方法(GET
,POST
,PUT
等)的匹配。
===
要添加一些用户,您可以直接在命名空间中定义一组测试数据:
<authentication-manager> <authentication-provider> <user-service> <!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that NoOpPasswordEncoder should be used. This is not safe for production, but makes reading in samples easier. Normally passwords should be hashed using BCrypt --> <user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" /> <user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" /> </user-service> </authentication-provider> </authentication-manager>
这是存储相同密码的安全方式的示例。密码以{bcrypt}
为前缀,指示DelegatingPasswordEncoder
,它支持任何配置的PasswordEncoder
匹配,密码使用BCrypt进行哈希处理:
<authentication-manager> <authentication-provider> <user-service> <user name="jimi" password="{bcrypt}$2a$10$ddEWZUl8aU0GdZPPpy7wbu82dvEw/pBpbRvDQRqA41y6mK1CoH00m" authorities="ROLE_USER, ROLE_ADMIN" /> <user name="bob" password="{bcrypt}$2a$10$/elFpMBnAYYig6KRR5bvOOYeZr1ie1hSogJryg9qDlhza4oCw1Qka" authorities="ROLE_USER" /> <user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" /> <user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" /> </user-service> </authentication-provider> </authentication-manager>
上面的配置定义了两个用户,他们的密码和他们在应用程序中的角色(将用于访问控制)。也可以使用user-service
上的properties
属性从标准属性文件加载用户信息。有关文件格式的更多详细信息,请参阅内存中身份验证部分。使用<authentication-provider>
元素意味着身份验证管理器将使用用户信息来处理身份验证请求。您可以使用多个<authentication-provider>
元素来定义不同的身份验证源,并依次查阅每个身份验证源。
此时,您应该可以启动应用程序,并且您将需要登录才能继续。尝试一下,或尝试尝试项目附带的“教程”示例应用程序。
当您被提示登录时,您可能想知道登录表单的来源,因为我们没有提及任何HTML文件或JSP。事实上,由于我们没有明确设置登录页面的URL,Spring Security会根据启用的功能自动生成一个URL,并使用处理提交的登录的URL的标准值,默认目标URL用户将在登录后发送,等等。但是,命名空间提供了大量支持,允许您自定义这些选项。例如,如果要提供自己的登录页面,可以使用:
<http> <intercept-url pattern="/login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/**" access="ROLE_USER" /> <form-login login-page='/login.jsp'/> </http>
另请注意,我们添加了一个额外的intercept-url
元素,表示任何对登录页面的请求都应该可供匿名用户[3]以及AuthenticatedVoter类使用,以获取有关如何处理值IS_AUTHENTICATED_ANONYMOUSLY
的更多详细信息]。否则,请求将与模式/ **匹配,并且无法访问登录页面本身!这是一个常见的配置错误,将导致应用程序中出现无限循环。如果您的登录页面看起来是安全的,Spring Security将在日志中发出警告。通过为模式定义单独的http
元素,也可以使所有与特定模式匹配的请求完全绕过安全过滤器链:
<http pattern="/css/**" security="none"/> <http pattern="/login.jsp*" security="none"/> <http use-expressions="false"> <intercept-url pattern="/**" access="ROLE_USER" /> <form-login login-page='/login.jsp'/> </http>
从Spring Security 3.1开始,现在可以使用多个http
元素为不同的请求模式定义单独的安全过滤器链配置。如果http
元素中省略了pattern
属性,则它匹配所有请求。创建不安全模式是此语法的一个简单示例,其中模式映射到空过滤器链[4]。我们将在有关安全过滤器链的章节中更详细地介绍这种新语法。
重要的是要意识到这些不安全的请求将完全忽略任何Spring Security web相关配置或requires-channel
等附加属性,因此您将无法访问有关当前用户或呼叫的信息请求期间的安全方法。如果您仍希望应用安全过滤器链,请使用access='IS_AUTHENTICATED_ANONYMOUSLY'
作为替代。
如果要使用基本身份验证而不是表单登录,请将配置更改为
<http use-expressions="false"> <intercept-url pattern="/**" access="ROLE_USER" /> <http-basic /> </http>
然后,基本身份验证将优先,并将用于在用户尝试访问受保护资源时提示登录。如果您希望使用表单登录,则仍可在此配置中使用表单登录,例如通过嵌入在另一个web页面中的登录表单。
如果尝试访问受保护资源时未提示表单登录,则default-target-url
选项将起作用。这是用户成功登录后将使用的URL,默认为“/”。您还可以通过将always-use-default-target
属性设置为“true”来配置事物,以便用户始终在此页面结束(无论登录是“按需”还是明确选择登录)。如果您的应用程序始终要求用户在“主页”页面启动,则此选项非常有用,例如:
<http pattern="/login.htm*" security="none"/> <http use-expressions="false"> <intercept-url pattern='/**' access='ROLE_USER' /> <form-login login-page='/login.htm' default-target-url='/home.htm' always-use-default-target='true' /> </http>
要进一步控制目标,可以使用authentication-success-handler-ref
属性作为default-target-url
的替代。引用的bean应该是AuthenticationSuccessHandler
的实例。您可以在Core Filters一章以及命名空间附录中找到更多相关信息,以及有关如何在身份验证失败时自定义流的信息。
logout
元素通过导航到特定URL添加了对注销的支持。默认的注销URL为/logout
,但您可以使用logout-url
属性将其设置为其他内容。有关其他可用属性的更多信息,请参见命名空间附录。
实际上,除了添加到应用程序上下文文件中的一些名称之外,您还需要一个更具伸缩性的用户信息源。您很可能希望将用户信息存储在数据库或LDAP服务器中。LDAP命名空间配置在LDAP章节中处理,因此我们不在此处介绍它。如果您的应用程序上下文中有Spring Security的UserDetailsService
自定义实现,名为“myUserDetailsService”,那么您可以使用以下方法对此进行身份验证
<authentication-manager> <authentication-provider user-service-ref='myUserDetailsService'/> </authentication-manager>
如果要使用数据库,则可以使用
<authentication-manager> <authentication-provider> <jdbc-user-service data-source-ref="securityDataSource"/> </authentication-provider> </authentication-manager>
其中“securityDataSource”是应用程序上下文中DataSource
bean的名称,指向包含标准Spring Security 用户数据表的数据库。或者,您可以使用user-service-ref
属性配置Spring Security JdbcDaoImpl
bean并指向该bean:
<authentication-manager> <authentication-provider user-service-ref='myUserDetailsService'/> </authentication-manager> <beans:bean id="myUserDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"> <beans:property name="dataSource" ref="dataSource"/> </beans:bean>
您还可以使用标准AuthenticationProvider
bean,如下所示
<authentication-manager> <authentication-provider ref='myAuthenticationProvider'/> </authentication-manager>
其中myAuthenticationProvider
是应用程序上下文中实现AuthenticationProvider
的bean的名称。您可以使用多个authentication-provider
元素,在这种情况下,将按照声明的顺序查询提供程序。有关如何使用命名空间配置Spring Security AuthenticationManager
的更多信息,请参见第7.6节“身份验证管理器和命名空间”。
应始终使用为此目的设计的安全散列算法对密码进行编码(不是像SHA或MD5这样的标准算法)。这是<password-encoder>
元素支持的。使用bcrypt编码的密码,原始身份验证提供程序配置如下所示:
<beans:bean name="bcryptEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/> <authentication-manager> <authentication-provider> <password-encoder ref="bcryptEncoder"/> <user-service> <user name="jimi" password="$2a$10$ddEWZUl8aU0GdZPPpy7wbu82dvEw/pBpbRvDQRqA41y6mK1CoH00m" authorities="ROLE_USER, ROLE_ADMIN" /> <user name="bob" password="$2a$10$/elFpMBnAYYig6KRR5bvOOYeZr1ie1hSogJryg9qDlhza4oCw1Qka" authorities="ROLE_USER" /> </user-service> </authentication-provider> </authentication-manager>
对于大多数情况,bcrypt是一个不错的选择,除非你有一个强制你使用不同算法的遗留系统。如果您使用简单的散列算法,或者更糟糕的是,存储纯文本密码,那么您应该考虑迁移到更安全的选项,如bcrypt。
有关remember-me命名空间配置的信息,请参阅单独的Remember-Me章节。
如果您的应用程序同时支持HTTP和HTTPS,并且您要求只能通过HTTPS访问特定URL,则使用<intercept-url>
上的requires-channel
属性直接支持此功能:
<http> <intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/> <intercept-url pattern="/**" access="ROLE_USER" requires-channel="any"/> ... </http>
有了这种配置,如果用户尝试使用HTTP访问与“/ secure / **”模式匹配的任何内容,它们将首先被重定向到HTTPS URL [5]。可用选项为“http”,“https”或“any”。使用值“any”表示可以使用HTTP或HTTPS。
如果您的应用程序使用HTTP和/或HTTPS的非标准端口,则可以指定端口映射列表,如下所示:
<http> ... <port-mappings> <port-mapping http="9080" https="9443"/> </port-mappings> </http>
请注意,为了确保安全,应用程序根本不应使用HTTP或在HTTP和HTTPS之间切换。它应该以HTTPS(用户输入HTTPS URL)开始,并始终使用安全连接,以避免任何中间人攻击的可能性。
您可以配置Spring Security以检测无效会话ID的提交,并将用户重定向到适当的URL。这是通过session-management
元素实现的:
<http> ... <session-management invalid-session-url="/invalidSession.htm" /> </http>
请注意,如果您使用此机制来检测会话超时,则可能会错误地报告错误,如果用户注销,然后在不关闭浏览器的情况下重新登录。这是因为当您使会话无效时,会话cookie不会被清除,即使用户已注销,也会重新提交。您可以在注销时显式删除JSESSIONID cookie,例如在注销处理程序中使用以下语法:
<http> <logout delete-cookies="JSESSIONID" /> </http>
不幸的是,这不能保证与每个servlet容器一起使用,因此您需要在您的环境中对其进行测试
![]() | 注意 |
---|---|
===如果您在代理后面运行应用程序,您也可以通过配置代理服务器来删除会话cookie。例如,使用Apache HTTPD的mod_headers,以下指令将通过在对注销请求的响应中使其失效来删除 |
<LocationMatch "/tutorial/logout"> Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT" </LocationMatch>
===
如果您希望对单个用户登录应用程序的能力施加约束,Spring Security通过以下简单添加支持此开箱即用。首先,您需要将以下侦听器添加到web.xml
文件中,以使Spring Security更新有关会话生命周期事件的信息:
<listener> <listener-class> org.springframework.security.web.session.HttpSessionEventPublisher </listener-class> </listener>
然后将以下行添加到应用程序上下文中:
<http> ... <session-management> <concurrency-control max-sessions="1" /> </session-management> </http>
这将阻止用户多次登录 - 第二次登录将导致第一次失效。通常您更愿意阻止第二次登录,在这种情况下您可以使用
<http> ... <session-management> <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" /> </session-management> </http>
然后将拒绝第二次登录。“被拒绝”是指如果使用基于表单的登录,用户将被发送到authentication-failure-url
。如果第二次认证是通过另一种非交互机制发生的,例如“记住我”,则会向客户端发送“未授权”(401)错误。如果您想要使用错误页面,则可以将属性session-authentication-error-url
添加到session-management
元素。
如果您使用自定义身份验证筛选器进行基于表单的登录,则必须明确配置并发会话控制支持。更多详细信息可在“ 会话管理”一章中找到。
会话固定攻击是潜在的风险,恶意攻击者可能通过访问站点来创建会话,然后说服其他用户使用相同的会话登录(通过向他们发送包含会话标识符作为参数的链接,例)。Spring Security通过在用户登录时创建新会话或以其他方式更改会话ID来自动防止这种情况。如果您不需要此保护,或者它与某些其他要求冲突,您可以使用{来控制行为<session-management>
上的2 /}属性,有四个选项
none
- 不要做任何事。原始会话将保留。
newSession
- 创建一个新的“干净”会话,而不复制现有会话数据(Spring Security - 相关属性仍将被复制)。
migrateSession
- 创建新会话并将所有现有会话属性复制到新会话。这是Servlet 3.0或旧容器中的默认设置。
changeSessionId
- 不要创建新会话。而是使用Servlet容器(HttpServletRequest#changeSessionId()
)提供的会话固定保护。此选项仅适用于Servlet 3.1(Java EE 7)和更新的容器。在旧容器中指定它将导致异常。这是Servlet 3.1和更新容器中的默认设置。
发生会话固定保护时,会导致在应用程序上下文中发布SessionFixationProtectionEvent
。如果您使用changeSessionId
,这种保护会也导致任何javax.servlet.http.HttpSessionIdListener
S是通知,所以如果你的代码侦听这两个事件谨慎使用。有关其他信息,请参阅会话管理章节。
除了普通的基于表单的登录之外,命名空间支持OpenID登录,只需进行简单的更改:
<http> <intercept-url pattern="/**" access="ROLE_USER" /> <openid-login /> </http>
然后,您应该使用OpenID提供程序(例如myopenid.com)注册自己,并将用户信息添加到内存中<user-service>
:
<user name="http://jimi.hendrix.myopenid.com/" authorities="ROLE_USER" />
您应该能够使用myopenid.com
站点进行身份验证登录。通过在openid-login
元素上设置user-service-ref
属性,也可以选择特定的UserDetailsService
bean来使用OpenID。有关更多信息,请参阅上一节有关身份验证提供程 请注意,我们已从上述用户配置中省略了password属性,因为此组用户数据仅用于加载用户的权限。将在内部生成随机密码,以防止您意外地将此用户数据用作配置中其他位置的身份验证源。
支持OpenID 属性交换。例如,以下配置将尝试从OpenID提供程序检索电子邮件和全名,以供应用程序使用:
<openid-login> <attribute-exchange> <openid-attribute name="email" type="http://axschema.org/contact/email" required="true"/> <openid-attribute name="name" type="http://axschema.org/namePerson"/> </attribute-exchange> </openid-login>
每个OpenID属性的“类型”是由特定模式确定的URI,在本例中为http://axschema.org/。如果必须检索属性以进行成功验证,则可以设置required
属性。支持的确切架构和属性取决于您的OpenID提供程序。属性值作为身份验证过程的一部分返回,之后可以使用以下代码进行访问:
OpenIDAuthenticationToken token = (OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication(); List<OpenIDAttribute> attributes = token.getAttributes();
OpenIDAttribute
包含属性类型和检索到的值(或多值属性的值)。在我们查看技术概述章节中的核心Spring Security组件时,我们将看到更多关于如何使用SecurityContextHolder
类的信息。如果您希望使用多个身份提供程序,也支持多个属性交换配置。您可以提供多个attribute-exchange
元素,每个元素使用identifier-matcher
属性。它包含一个正则表达式,该表达式将与用户提供的OpenID标识符进行匹配。请参阅代码库中的OpenID示例应用程序以获取示例配置,为Google,Yahoo和MyOpenID提供程序提供不同的属性列表。
有关如何自定义headers元素的其他信息,请参阅参考的第10.8节“安全HTTP响应标头”部分。
如果您以前使用过Spring Security,那么您将知道该框架维护了一系列过滤器以便应用其服务。您可能希望将自己的过滤器添加到特定位置的堆栈,或者使用当前没有命名空间配置选项的Spring Security过滤器(例如,CAS)。或者您可能希望使用标准命名空间过滤器的自定义版本,例如由<form-login>
元素创建的UsernamePasswordAuthenticationFilter
,利用明确使用bean可用的一些额外配置选项。如何通过名称空间配置来完成此操作,因为过滤器链未直接公开?
使用命名空间时,始终严格执行过滤器的顺序。在创建应用程序上下文时,过滤器bean按名称空间处理代码进行排序,标准Spring Security过滤器在名称空间中都有一个别名和一个众所周知的位置。
![]() | 注意 |
---|---|
===在以前的版本中,排序发生在创建过滤器实例之后,在应用程序上下文的后处理期间。在版本3.0+中,现在在实例化类之前,在bean元数据级别完成排序。这对于如何将自己的过滤器添加到堆栈有影响,因为在解析 |
表7.1“标准过滤器别名和排序”中显示了创建过滤器的过滤器,别名和名称空间元素/属性。过滤器按它们在过滤器链中出现的顺序列出。
表7.1。标准过滤器别名和订购
别号 | 过滤类 | 命名空间元素或属性 |
---|---|---|
CHANNEL_FILTER |
|
|
SECURITY_CONTEXT_FILTER |
|
|
CONCURRENT_SESSION_FILTER |
|
|
HEADERS_FILTER |
|
|
CSRF_FILTER |
|
|
LOGOUT_FILTER |
|
|
X509_FILTER |
|
|
PRE_AUTH_FILTER |
| N/A |
CAS_FILTER |
| N/A |
FORM_LOGIN_FILTER |
|
|
BASIC_AUTH_FILTER |
|
|
SERVLET_API_SUPPORT_FILTER |
|
|
JAAS_API_SUPPORT_FILTER |
|
|
REMEMBER_ME_FILTER |
|
|
ANONYMOUS_FILTER |
|
|
SESSION_MANAGEMENT_FILTER |
|
|
EXCEPTION_TRANSLATION_FILTER |
|
|
FILTER_SECURITY_INTERCEPTOR |
|
|
SWITCH_USER_FILTER |
| N/A |
您可以使用custom-filter
元素和其中一个名称将自己的过滤器添加到堆栈,以指定过滤器应显示的位置:
<http> <custom-filter position="FORM_LOGIN_FILTER" ref="myFilter" /> </http> <beans:bean id="myFilter" class="com.mycompany.MySpecialAuthenticationFilter"/>
如果希望在堆栈中的另一个过滤器之前或之后插入过滤器,也可以使用after
或before
属性。名称“FIRST”和“LAST”可与position
属性一起使用,以指示您希望过滤器分别出现在整个堆栈之前或之后。
![]() | 避免过滤器位置冲突 |
---|---|
=== |
如果要插入的自定义过滤器可能与命名空间创建的标准过滤器之一占据相同的位置,则重要的是不要错误地包含命名空间版本。删除任何创建要替换其功能的过滤器的元素。
请注意,您无法替换使用<http>
元素本身创建的过滤器 - SecurityContextPersistenceFilter
,ExceptionTranslationFilter
或FilterSecurityInterceptor
。默认情况下会添加其他一些过滤器,但您可以禁用它们。默认情况下会添加AnonymousAuthenticationFilter
,除非您禁用会话固定保护,否则还会在过滤器链中添加SessionManagementFilter
。
===
如果要替换需要身份验证入口点的命名空间过滤器(即,未经身份验证的用户尝试访问安全资源而触发身份验证过程),则还需要添加自定义入口点bean。
如果您没有使用表单登录,OpenID或通过命名空间进行基本身份验证,您可能需要使用传统的bean语法定义身份验证过滤器和入口点,并将它们链接到命名空间,如我们刚才所见。可以使用<http>
元素上的entry-point-ref
属性设置相应的AuthenticationEntryPoint
。
CAS示例应用程序是使用带有命名空间的自定义bean的一个很好的示例,包括此语法。如果您不熟悉身份验证入口点,则会在技术概述一章中讨论它们。
从版本2.0开始,Spring Security大大提高了对服务层方法的安全性的支持。它为JSR-250注释安全性以及框架的原始@Secured
注释提供支持。从3.0开始,您还可以使用基于表达式的新注释。您可以将安全性应用于单个bean,使用intercept-methods
元素来装饰bean声明,或者可以使用AspectJ样式切入点在整个服务层中保护多个bean。
此元素用于在应用程序中启用基于注释的安全性(通过在元素上设置适当的属性),还可以将安全性切入点声明组合在一起,这些声明将应用于整个应用程序上下文。您应该只声明一个<global-method-security>
元素。以下声明将支持Spring Security的@Secured
:
<global-method-security secured-annotations="enabled" />
然后,在方法(类或接口)上添加注释会相应地限制对该方法的访问。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注释
<global-method-security jsr250-annotations="enabled" />
这些是基于标准的,允许应用简单的基于角色的约束,但没有强大的Spring Security本机注释。要使用新的基于表达式的语法,您可以使用
<global-method-security pre-post-annotations="enabled" />
和等效的Java代码
public interface BankService { @PreAuthorize("isAnonymous()") public Account readAccount(Long id); @PreAuthorize("isAnonymous()") public Account[] findAccounts(); @PreAuthorize("hasAuthority('ROLE_TELLER')") public Account post(Account account, double amount); }
如果您需要定义简单的规则,而不是根据用户的权限列表检查角色名称,那么基于表达式的注释是一个不错的选择。
![]() | 注意 |
---|---|
===仅对定义为Spring bean的实例(在启用了method-security的同一应用程序上下文中)保护带注释的方法。如果要保护不是由Spring创建的实例(例如,使用 |
![]() | 注意 |
---|---|
===您可以在同一个应用程序中启用多种类型的注释,但是任何接口或类只应使用一种类型,否则行为将无法明确定义。如果找到适用于特定方法的两个注释,则只应用其中一个注释。=== |
protect-pointcut
的使用特别强大,因为它允许您只使用简单的声明将安全性应用于许多bean。请考虑以下示例:
<global-method-security> <protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="ROLE_USER"/> </global-method-security>
这将保护在应用程序上下文中声明的bean上的所有方法,这些bean的类在com.mycompany
包中,其类名以“Service”结尾。只有具有ROLE_USER
角色的用户才能调用这些方法。与URL匹配一样,最具体的匹配必须首先出现在切入点列表中,因为将使用第一个匹配表达式。安全注释优先于切入点。
本节假设您已了解Spring Security内访问控制的基础架构。如果不这样做,您可以跳过它并稍后再回过头来看,因为本节仅对需要进行一些自定义以便使用简单基于角色的安全性的人员非常重要。
当您使用命名空间配置时,将自动为您注册AccessDecisionManager
的默认实例,并将根据您在{3中指定的访问属性,用于为方法调用和web URL访问做出访问决策。 /}和protect-pointcut
声明(如果使用注释安全方法,则在注释中)。
默认策略是使用带有RoleVoter
和AuthenticatedVoter
的AffirmativeBased
AccessDecisionManager
。您可以在授权章节中找到有关这些内容的更多信息。
如果您需要使用更复杂的访问控制策略,那么很容易为方法和web安全性设置替代方案。
对于方法安全性,可以通过将global-method-security
上的access-decision-manager-ref
属性设置为应用程序上下文中相应AccessDecisionManager
bean的id
来执行此操作:
<global-method-security access-decision-manager-ref="myAccessDecisionManagerBean"> ... </global-method-security>
web安全性的语法是相同的,但在http
元素上:
<http access-decision-manager-ref="myAccessDecisionManagerBean"> ... </http>
在Spring Security中提供身份验证服务的主要接口是AuthenticationManager
。这通常是Spring Security的ProviderManager
类的一个实例,如果您之前使用过该框架,那么您可能已经熟悉了它。如果没有,稍后将在技术概述章节中介绍。使用authentication-manager
命名空间元素注册bean实例。如果您通过命名空间使用HTTP或方法安全性,则不能使用自定义AuthenticationManager
,但这不应该是一个问题,因为您可以完全控制所使用的AuthenticationProvider
。
您可能希望使用ProviderManager
注册其他AuthenticationProvider
bean,并且可以使用带有ref
属性的<authentication-provider>
元素来执行此操作,其中属性的值是提供者bean的名称你想要添加。例如:
<authentication-manager> <authentication-provider ref="casAuthenticationProvider"/> </authentication-manager> <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> ... </bean>
另一个常见的要求是上下文中的另一个bean可能需要引用AuthenticationManager
。您可以轻松注册AuthenticationManager
的别名,并在应用程序上下文的其他位置使用此名称。
<security:authentication-manager alias="authenticationManager"> ... </security:authentication-manager> <bean id="customizedFormLoginFilter" class="com.somecompany.security.web.CustomFormLoginFilter"> <property name="authenticationManager" ref="authenticationManager"/> ... </bean>
[1]您可以在第12.3节“LDAP身份验证”一章中找到有关使用ldap-server
元素的更多信息。
[2]有关如何实际执行匹配的更多详细信息,请参阅Web应用程序基础结构一章中第10.1.4节“请求匹配和HttpFirewall”一节。
[4]使用多个<http>
元素是一个重要特性,例如,允许命名空间同时支持同一应用程序中的有状态和无状态路径。在intercept-url
元素上使用属性filters="none"
的先前语法与此更改不兼容,并且在3.1中不再受支持。
[5]有关如何实现通道处理的更多详细信息,请参阅ChannelProcessingFilter
的Javadoc和相关类。
熟悉设置和运行某些基于命名空间配置的应用程序之后,您可能希望更多地了解框架在命名空间外观背后的实际工作方式。与大多数软件一样,Spring Security具有某些中心接口,类和概念抽象,这些都是整个框架中常用的。在参考指南的这一部分中,我们将查看其中的一些内容,并了解它们如何协同工作以支持Spring Security内的身份验证和访问控制。
Spring Security 3.0需要Java 5.0 Runtime Environment或更高版本。由于Spring Security旨在以自包含方式运行,因此无需将任何特殊配置文件放入Java运行时环境中。特别是,无需配置特殊的Java身份验证和授权服务(JAAS)策略文件,也不需要将Spring Security放入常见的类路径位置。
同样,如果您使用的是EJB容器或Servlet容器,则无需在任何地方放置任何特殊配置文件,也不需要在服务器类加载器中包含Spring Security。所有必需的文件都将包含在您的应用程序中。
这种设计提供了最大的部署时间灵活性,因为您可以简单地将目标工件(无论是JAR,WAR还是EAR)从一个系统复制到另一个系统,它将立即起作用。
在Spring Security 3.0中,spring-security-core
jar的内容被剥离到最低限度。它不再包含与web相关的任何代码 - 应用程序安全性,LDAP或命名空间配置。我们将在这里看一下您在核心模块中可以找到的一些Java类型。它们代表了框架的构建块,因此如果您需要超越简单的命名空间配置,那么即使您实际上不需要直接与它们进行交互,您也必须了解它们是什么。
最基本的对象是SecurityContextHolder
。这是我们存储应用程序当前安全上下文的详细信息的地方,其中包括当前使用该应用程序的主体的详细信息。默认情况下,SecurityContextHolder
使用ThreadLocal
来存储这些详细信息,这意味着安全上下文始终可用于同一执行线程中的方法,即使安全上下文未作为参数显式传递那些方法。如果在处理当前委托人的请求之后小心地清除线程,以这种方式使用ThreadLocal
是非常安全的。当然,Spring Security会自动为您解决这个问题,因此无需担心。
某些应用程序并不完全适合使用ThreadLocal
,因为它们使用线程的特定方式。例如,Swing客户端可能希望Java虚拟机中的所有线程都使用相同的安全上下文。SecurityContextHolder
可以在启动时配置策略,以指定您希望如何存储上下文。对于独立应用程序,您将使用SecurityContextHolder.MODE_GLOBAL
策略。其他应用程序可能希望安全线程生成的线程也采用相同的安全标识。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
来实现的。您可以通过两种方式从默认SecurityContextHolder.MODE_THREADLOCAL
更改模式。第一个是设置系统属性,第二个是在SecurityContextHolder
上调用静态方法。大多数应用程序不需要更改默认值,但如果这样做,请查看JavaDoc for SecurityContextHolder
以了解更多信息。
在SecurityContextHolder
内,我们存储了当前与应用程序交互的主体的详细信息。Spring Security使用Authentication
对象来表示此信息。您通常不需要自己创建Authentication
对象,但用户查询Authentication
对象是相当常见的。您可以使用以下代码块(从应用程序的任何位置)获取当前经过身份验证的用户的名称,例如:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
调用getContext()
返回的对象是SecurityContext
接口的实例。这是保存在线程本地存储中的对象。正如我们将在下面看到的,Spring Security中的大多数认证机制都返回UserDetails
的实例作为主体。
上面代码片段中需要注意的另一个问题是,您可以从Authentication
对象获取主体。校长只是Object
。大多数情况下,这可以转换为UserDetails
对象。UserDetails
是Spring Security中的核心界面。它代表一个主体,但是以可扩展和特定于应用程序的方式。可以将UserDetails
视为您自己的用户数据库与SecurityContextHolder
内Spring Security所需的适配器之间的适配器。作为来自您自己的用户数据库的东西的表示,您经常会将UserDetails
转换为您的应用程序提供的原始对象,因此您可以调用特定于业务的方法(如getEmail()
,getEmployeeNumber()
和等等)。
到现在为止你可能想知道,所以我什么时候提供UserDetails
对象?我怎么做?我以为你说这个东西是声明性的,我不需要编写任何Java代码 - 是什么给出的?简短的回答是有一个名为UserDetailsService
的特殊界面。此接口上唯一的方法接受基于String
的用户名参数并返回UserDetails
:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
这是在Spring Security内为用户加载信息的最常用方法,只要需要有关用户的信息,您就会看到它在整个框架中使用。
上成功的认证,UserDetails
被用来建立存储在SecurityContextHolder
(关于这一点的Authentication
对象下面)。好消息是我们提供了许多UserDetailsService
实现,包括一个使用内存映射(InMemoryDaoImpl
)和另一个使用JDBC(JdbcDaoImpl
)的实现。但是,大多数用户倾向于自己编写,他们的实现通常只是位于代表其员工,客户或应用程序其他用户的现有数据访问对象(DAO)之上。记住使用上面的代码片段始终可以从SecurityContextHolder
获得UserDetailsService
返回的优点。
![]() | 注意 |
---|---|
|
除了校长之外,Authentication
提供的另一个重要方法是getAuthorities()
。此方法提供GrantedAuthority
个对象的数组。毫不奇怪,GrantedAuthority
是授予校长的权力。这些权力通常是“角色”,例如ROLE_ADMINISTRATOR
或ROLE_HR_SUPERVISOR
。稍后将为web授权,方法授权和域对象授权配置这些角色。Spring Security的其他部分能够解释这些权威,并期望它们存在。GrantedAuthority
对象通常由UserDetailsService
加载。
通常GrantedAuthority
对象是应用程序范围的权限。它们不是特定于给定的域对象。因此,你不可能有一个GrantedAuthority
代表Employee
对象编号54的权限,因为如果有数千个这样的权限,你很快就会耗尽内存(或者,至少,因为应用程序需要很长时间来验证用户身份)。当然,Spring Security专门用于处理这个常见要求,但您可以使用项目的域对象安全功能来实现此目的。
回顾一下,到目前为止我们看到的Spring Security的主要构建块是:
SecurityContextHolder
,提供SecurityContext
的访问权限。
SecurityContext
,保存Authentication
和可能的特定于请求的安全信息。
Authentication
,以特定于Spring Security的方式代表校长。
GrantedAuthority
,以反映授予主体的应用程序范围的权限。
UserDetails
,提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息。
UserDetailsService
,在基于String
的用户名(或证书ID等)中传递时创建UserDetails
。
既然您已经了解了这些重复使用的组件,那么让我们仔细看看身份验证过程。
Spring Security可以参与许多不同的身份验证环境。虽然我们建议人们使用Spring Security进行身份验证,而不是与现有的容器管理身份验证集成,但它仍然受到支持 - 与您自己的专有身份验证系统集成。
让我们考虑一个每个人都熟悉的标准身份验证方案。
前三项构成了身份验证过程,因此我们将在Spring Security内查看这些过程是如何发生的。
UsernamePasswordAuthenticationToken
的实例中(我们之前看到的Authentication
接口的实例)。
AuthenticationManager
的实例以进行验证。
AuthenticationManager
在成功验证后返回完全填充的Authentication
实例。
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<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>(); 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会话来缓存请求之间的上下文:[一旦提交响应,就无法创建会话。
如果您想知道如何在现实世界的例子中实现AuthenticationManager
,我们将在核心服务章节中看一下。
现在让我们来探讨在web应用程序中使用Spring Security的情况(未启用web.xml
安全性)。如何对用户进行身份验证并建立安全上下文?
考虑典型的web应用程序的身份验证过程:
Spring Security有不同的类负责上述大多数步骤。主要参与者(按照他们使用的顺序)是ExceptionTranslationFilter
,AuthenticationEntryPoint
和“认证机制”,它负责调用我们在上一节中看到的AuthenticationManager
。
ExceptionTranslationFilter
是一个Spring Security过滤器,负责检测抛出的任何Spring Security异常。AbstractSecurityInterceptor
通常会抛出此类异常,这是授权服务的主要提供者。我们将在下一节讨论AbstractSecurityInterceptor
,但是现在我们只需要知道它产生Java异常并且对HTTP一无所知或者如何对主体进行身份验证。相反,ExceptionTranslationFilter
提供此服务,特别负责返回错误代码403(如果主体已经过身份验证,因此根本没有足够的访问权限 - 按照上面的步骤7),或者启动AuthenticationEntryPoint
(如果校长尚未通过认证,因此我们需要开始第三步)。
AuthenticationEntryPoint
负责上面列表中的第三步。可以想象,每个web应用程序都有一个默认的身份验证策略(好吧,这可以像Spring Security中的几乎所有其他配置一样配置,但现在让我们保持简单)。每个主要身份验证系统都有自己的AuthenticationEntryPoint
实现,通常执行步骤3中描述的操作之一。
一旦您的浏览器提交您的身份验证凭据(作为HTTP表单帖子或HTTP标头),服务器上就需要“收集”这些身份验证详细信息。到目前为止,我们已经在上面的列表中的第六步了。在Spring Security中,我们有一个特殊的名称,用于从用户代理(通常是web浏览器)收集身份验证详细信息,并将其称为“身份验证机制”。例如基于表单的登录和基本身份验证。一旦从用户代理收集了身份验证详细信息,就会构建一个Authentication
“请求”对象,然后将其呈现给AuthenticationManager
。
在认证机制收到完全填充的Authentication
对象后,它将认为请求有效,将Authentication
放入SecurityContextHolder
,并使原始请求重试(上面的步骤7)。另一方面,如果AuthenticationManager
拒绝了请求,则认证机制将要求用户代理重试(上面的步骤2)。
根据应用程序的类型,可能需要采用策略来在用户操作之间存储安全上下文。在典型的web应用程序中,用户登录一次,然后由其会话ID标识。服务器缓存持续时间会话的主体信息。在Spring Security中,在请求之间存储SecurityContext
的责任落在SecurityContextPersistenceFilter
上,默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession
属性。它会为每个请求恢复上下文SecurityContextHolder
,并且最重要的是,在请求完成时清除SecurityContextHolder
。出于安全考虑,您不应直接与HttpSession
进行交互。没有理由这样做 - 总是使用SecurityContextHolder
。
许多其他类型的应用程序(例如,无状态RESTful web服务)不使用HTTP会话,并将在每个请求上重新进行身份验证。但是,链中包含SecurityContextPersistenceFilter
以确保在每次请求后清除SecurityContextHolder
仍然很重要。
![]() | 注意 |
---|---|
在一个会话中接收并发请求的应用程序中,将在线程之间共享相同的 |
负责在Spring Security中做出访问控制决策的主界面是AccessDecisionManager
。它有一个decide
方法,它接受一个代表请求访问的主体的Authentication
对象,一个“安全对象”(见下文)和一个适用于该对象的安全元数据属性列表(例如角色列表)这是获得访问所必需的。
如果您熟悉AOP,您会发现有不同类型的建议可用:之前,之后,投掷和周围。around建议非常有用,因为顾问可以选择是否继续进行方法调用,是否修改响应,以及是否抛出异常。Spring Security提供了方法调用和web请求的周围建议。我们使用Spring的标准AOP支持为方法调用提供了一个周围的建议,我们使用标准过滤器为web请求提供了建议。
对于那些不熟悉AOP的人来说,理解的关键点是Spring Security可以帮助您保护方法调用以及web请求。大多数人都对在服务层上保护方法调用感兴趣。这是因为服务层是大多数业务逻辑驻留在当前一代Java EE应用程序中的地方。如果您只需要在服务层中保护方法调用,那么Spring的标准AOP就足够了。如果您需要直接保护域对象,您可能会发现AspectJ值得考虑。
您可以选择使用AspectJ或Spring AOP执行方法授权,也可以选择使用过滤器执行web请求授权。您可以将这些方法中的零个,一个,两个或三个一起使用。主流使用模式是执行一些web请求授权,以及服务层上的一些Spring AOP方法调用授权。
那么什么是 “安全对象”呢?Spring Security使用该术语来指代可以对其应用安全性(例如授权决策)的任何对象。最常见的示例是方法调用和web请求。
每个受支持的安全对象类型都有自己的拦截器类,它是AbstractSecurityInterceptor
的子类。重要的是,在调用AbstractSecurityInterceptor
时,如果主体已经过身份验证,则SecurityContextHolder
将包含有效的Authentication
。
AbstractSecurityInterceptor
为处理安全对象请求提供了一致的工作流程,通常:
Authentication
和配置属性提交到AccessDecisionManager
以进行授权决策
Authentication
AfterInvocationManager
(如果已配置)。如果调用引发异常,则不会调用AfterInvocationManager
。
“配置属性”可以被认为是对AbstractSecurityInterceptor
使用的类具有特殊含义的String。它们由框架内的接口ConfigAttribute
表示。它们可能是简单的角色名称或具有更复杂的含义,具体取决于AccessDecisionManager
实现的复杂程度。AbstractSecurityInterceptor
配置了SecurityMetadataSource
,用于查找安全对象的属性。通常,此配置将对用户隐藏。配置属性将作为安全方法的注释输入,或作为安全URL的访问属性输入。例如,当我们在命名空间简介中看到类似<intercept-url pattern='/secure/**' access='ROLE_A,ROLE_B'/>
的内容时,这就是说配置属性ROLE_A
和ROLE_B
适用于匹配给定模式的web请求。实际上,使用默认的AccessDecisionManager
配置,这意味着任何具有GrantedAuthority
匹配这两个属性之一的人都将被允许访问。严格地说,它们只是属性,解释依赖于AccessDecisionManager
实现。前缀ROLE_
的使用是一个标记,表示这些属性是角色,应由Spring Security的RoleVoter
使用。这仅在使用基于选民的AccessDecisionManager
时才有意义。我们将在授权章节中看到AccessDecisionManager
的实现方式。
假设AccessDecisionManager
决定允许请求,AbstractSecurityInterceptor
通常只会继续请求。话虽如此,在极少数情况下,用户可能希望将SecurityContext
内的Authentication
替换为Authentication
,这由AccessDecisionManager
调用RunAsManager
处理。这在合理的异常情况下可能很有用,例如服务层方法需要调用远程系统并呈现不同的身份。因为Spring Security会自动将安全身份从一个服务器传播到另一个服务器(假设您正在使用正确配置的RMI或HttpInvoker远程协议客户端),这可能很有用。
在安全对象调用继续进行然后返回 - 这可能意味着方法调用完成或过滤器链继续进行 - AbstractSecurityInterceptor
获得最后一次机会来处理调用。在这个阶段,AbstractSecurityInterceptor
对可能修改返回对象感兴趣。我们可能希望这种情况发生,因为无法在安全对象调用的“途中”进行授权决策。作为高度可插拔的,AbstractSecurityInterceptor
会将控制传递给AfterInvocationManager
以在需要时实际修改对象。这个类甚至可以完全替换对象,或抛出异常,或者不以任何方式更改它。只有在调用成功时才会执行调用后检查。如果发生异常,将跳过其他检查。
AbstractSecurityInterceptor
及其相关对象如图8.1所示,“安全拦截器和”安全对象“模型”
Spring Security支持最终用户可能会看到的异常消息的本地化。如果您的应用程序是为讲英语的用户设计的,则无需执行任何操作,因为默认情况下所有安全消息均为英语。如果您需要支持其他语言环境,则需要了解的所有内容都包含在本节中。
可以对所有异常消息进行本地化,包括与身份验证失败和访问被拒绝相关的消息(授权失败)。专注于开发人员或系统部署人员的异常和日志消息(包括错误的属性,接口合同违规,使用错误的构造函数,启动时间验证,调试级别日志记录)不是本地化的,而是在Spring Security内用英语进行硬编码的代码。
在spring-security-core-xx.jar
中运送你会发现一个org.springframework.security
包,其中包含一个messages.properties
文件,以及一些常用语言的本地化版本。这应该由您的ApplicationContext
引用,因为Spring Security类实现Spring的MessageSourceAware
接口,并期望消息解析器在应用程序上下文启动时被依赖注入。通常,您需要做的就是在应用程序上下文中注册bean以引用消息。一个例子如下所示:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basename" value="classpath:org/springframework/security/messages"/> </bean>
messages.properties
根据标准资源包命名,表示Spring Security消息支持的默认语言。此默认文件为英文。
如果您希望自定义messages.properties
文件或支持其他语言,您应该复制该文件,相应地重命名,并在上面的bean定义中注册它。此文件中没有大量的消息密钥,因此本地化不应被视为主要的主动。如果您确实执行了此文件的本地化,请考虑通过记录JIRA任务并附加适当命名的本地化版本messages.properties
来与社区共享您的工作。
Spring Security依赖于Spring的本地化支持,以便实际查找相应的消息。为了使其工作,您必须确保传入请求中的区域设置存储在Spring的org.springframework.context.i18n.LocaleContextHolder
中。Spring MVC的DispatcherServlet
会自动为您的应用程序执行此操作,但由于在此之前调用了Spring Security的过滤器,因此需要将LocaleContextHolder
设置为包含正确的Locale
过滤器被调用。您可以自己在过滤器中执行此操作(必须在web.xml
中的Spring Security过滤器之前),或者您可以使用Spring的RequestContextFilter
。有关使用Spring本地化的更多详细信息,请参阅Spring Framework文档。
“contacts”示例应用程序设置为使用本地化消息。
现在我们对Spring Security架构及其核心类进行了高级概述,让我们仔细研究一个或两个核心接口及其实现,特别是AuthenticationManager
,UserDetailsService
和AccessDecisionManager
。这些文件会在本文档的其余部分定期出现,因此了解它们的配置方式以及它们的运行方式非常重要。
AuthenticationManager
只是一个界面,所以实现可以是我们选择的任何东西,但它在实践中如何运作?如果我们需要检查多个身份验证数据库或不同身份验证服务(如数据库和LDAP服务器)的组合,该怎么办?
Spring Security中的默认实现称为ProviderManager
,而不是处理身份验证请求本身,它会委托给已配置的AuthenticationProvider
列表,每个列表都会被查询以查看它是否可以执行认证。每个提供程序将抛出异常或返回完全填充的Authentication
对象。还记得我们的好朋友UserDetails
和UserDetailsService
吗?如果没有,请回到上一章并刷新记忆。验证身份验证请求的最常用方法是加载相应的UserDetails
并检查加载的密码与用户输入的密码。这是DaoAuthenticationProvider
使用的方法(见下文)。加载的UserDetails
对象 - 特别是它包含的GrantedAuthority
- 将在构建完全填充的Authentication
对象时使用,该对象从成功的身份验证返回并存储在SecurityContext
中。
如果您正在使用命名空间,则会在内部创建和维护ProviderManager
的实例,并使用命名空间身份验证提供程序元素向其添加提供程序(请参阅命名空间章节)。在这种情况下,您不应在应用程序上下文中声明ProviderManager
bean。但是,如果您没有使用命名空间,那么您将声明它如下:
<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager"> <constructor-arg> <list> <ref local="daoAuthenticationProvider"/> <ref local="anonymousAuthenticationProvider"/> <ref local="ldapAuthenticationProvider"/> </list> </constructor-arg> </bean>
在上面的例子中,我们有三个提供者。它们按照显示的顺序进行尝试(使用List
表示),每个提供程序都可以尝试进行身份验证,或者只需返回null
即可跳过身份验证。如果所有实现都返回null,则ProviderManager
将抛出ProviderNotFoundException
。如果您有兴趣了解有关链接提供程序的更多信息,请参阅ProviderManager
Javadoc。
身份验证机制(例如web表单登录处理过滤器)将注入ProviderManager
的引用,并将调用它来处理其身份验证请求。您需要的提供程序有时可以与身份验证机制互换,而在其他时候,它们将依赖于特定的身份验证机制。例如,DaoAuthenticationProvider
和LdapAuthenticationProvider
与提交简单用户名/密码身份验证请求的任何机制兼容,因此可以使用基于表单的登录或HTTP基本身份验证。另一方面,一些认证机制创建一个认证请求对象,该对象只能由单一类型的AuthenticationProvider
解释。一个例子是JA-SIG CAS,它使用服务票据的概念,因此只能通过CasAuthenticationProvider
进行身份验证。您不必过于担心这一点,因为如果您忘记注册合适的提供商,那么在尝试进行身份验证时,您只需收到ProviderNotFoundException
。
默认情况下(从Spring Security 3.1开始)ProviderManager
将尝试清除成功身份验证请求返回的Authentication
对象中的任何敏感凭据信息。这可以防止密码保留的时间超过必要的时间。
当您使用用户对象的缓存时,这可能会导致问题,例如,提高无状态应用程序的性能。如果Authentication
包含对缓存中对象的引用(例如UserDetails
实例)并且其凭据已删除,则将无法再对缓存的值进行身份验证。如果使用缓存,则需要考虑这一点。一个显而易见的解决方案是首先在缓存实现中或在创建返回的Authentication
对象的AuthenticationProvider
中创建对象的副本。或者,您可以在ProviderManager
上禁用eraseCredentialsAfterAuthentication
属性。有关更多信息,请参阅Javadoc。
Spring Security实现的最简单的AuthenticationProvider
是DaoAuthenticationProvider
,这也是该框架最早支持的之一。它利用UserDetailsService
(作为DAO)来查找用户名,密码和GrantedAuthority
。它只需将UsernamePasswordAuthenticationToken
中提交的密码与UserDetailsService
加载的密码进行比较,即可对用户进行身份验证。配置提供程序非常简单:
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="inMemoryDaoImpl"/> <property name="passwordEncoder" ref="passwordEncoder"/> </bean>
PasswordEncoder
是可选的。PasswordEncoder
提供从配置的UserDetailsService
返回的UserDetails
对象中显示的密码的编码和解码。这将在下面更详细地讨论。
如本参考指南前面所述,大多数身份验证提供程序都利用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
实现,则尤其如此。对于这种情况,一个简单的选择是使用安全命名空间中的user-service
元素:
<user-service id="userDetailsService"> <!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that NoOpPasswordEncoder should be used. This is not safe for production, but makes reading in samples easier. Normally passwords should be hashed using BCrypt --> <user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" /> <user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" /> </user-service>
这也支持使用外部属性文件:
<user-service id="userDetailsService" properties="users.properties"/>
属性文件应包含表单中的条目
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
,示例配置如下所示:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/> <property name="username" value="sa"/> <property name="password" value=""/> </bean> <bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"> <property name="dataSource" ref="dataSource"/> </bean>
您可以通过修改上面显示的DriverManagerDataSource
来使用不同的关系数据库管理系统。您还可以使用从JNDI获取的全局数据源,与任何其他Spring配置一样。
默认情况下,JdbcDaoImpl
会为单个用户加载权限,并假设权限直接映射到用户(请参阅数据库架构附录)。另一种方法是将权限划分为组并将组分配给用户。有些人更喜欢这种方法来管理用户权利。有关如何启用组权限的更多信息,请参阅JdbcDaoImpl
Javadoc。组架构也包含在附录中。
Spring Security的PasswordEncoder
界面用于执行密码的单向转换,以允许密码安全存储。鉴于PasswordEncoder
是单向转换,当密码转换需要双向(即存储用于向数据库进行身份验证的凭证)时,并不打算这样做。通常,PasswordEncoder
用于存储在验证时需要与用户提供的密码进行比较的密码。
多年来,存储密码的标准机制已经发展。在开始时,密码以纯文本格式存储。假设密码是安全的,因为数据存储密码保存在所需的凭据中以访问它。但是,恶意用户能够找到使用SQL注入等攻击获取用户名和密码的大量“数据转储”的方法。随着越来越多的用户凭证成为公共安全专家意识到我们需要做更多的工作来保护用户密码。
然后鼓励开发人员在通过单向散列(如SHA-256)运行密码后存储密码。当用户尝试进行身份验证时,散列密码将与他们键入的密码的哈希值进行比较。这意味着系统只需要存储密码的单向散列。如果发生了破坏,则只暴露密码的单向哈希。由于哈希是一种方式,并且在计算上难以猜测给定哈希的密码,因此在系统中找出每个密码是不值得的。为了打败这个新系统,恶意用户决定创建名为Rainbow Tables的查找表。他们不是每次都在猜测每个密码,而是计算密码一次并将其存储在查找表中。
为了降低Rainbow Tables的有效性,鼓励开发人员使用salted密码。不是仅使用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为盐)。salt和用户的密码将通过哈希函数运行,该哈希函数产生唯一的哈希值。盐将以明文形式存储在用户密码旁边。然后,当用户尝试进行身份验证时,散列密码将与存储的salt的哈希值和他们键入的密码进行比较。独特的盐意味着Rainbow Tables不再有效,因为每个盐和密码组合的哈希值都不同。
在现代,我们意识到加密哈希(如SHA-256)不再安全。原因是,使用现代硬件,我们可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地单独破解每个密码。
现在鼓励开发人员利用自适应单向函数来存储密码。使用自适应单向函数验证密码是故意的资源(即CPU,内存等)密集型。自适应单向函数允许配置“工作因子”,随着硬件变得越来越好。建议将“工作因素”调整为大约1秒钟以验证系统上的密码。这种折衷是为了让攻击者难以破解密码,但不是那么昂贵,这给你自己的系统带来了过重的负担。Spring Security试图为“工作因素”提供一个良好的起点,但鼓励用户为自己的系统定制“工作因素”,因为不同系统的性能会有很大差异。应该使用的自适应单向函数的示例包括 bcrypt, PBKDF2, scrypt和Argon2。
由于自适应单向函数是有意为资源密集型的,因此为每个请求验证用户名和密码会显着降低应用程序的性能。没有任何Spring Security(或任何其他库)可以加快密码验证,因为通过使验证资源密集来获得安全性。鼓励用户交换短期凭证(即会话,OAuth令牌等)的长期凭证(即用户名和密码)。短期凭证可以快速验证,而不会有任何安全损失。
在Spring Security 5.0之前,默认PasswordEncoder
为NoOpPasswordEncoder
,需要纯文本密码。根据密码历史记录部分,您可能希望默认PasswordEncoder
现在类似于BCryptPasswordEncoder
。但是,这忽略了三个现实世界的问题:
而Spring Security引入了DelegatingPasswordEncoder
,通过以下方式解决了所有问题:
您可以使用PasswordEncoderFactories
轻松构造DelegatingPasswordEncoder
的实例。
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
或者,您可以创建自己的自定义实例。例如:
String idForEncode = "bcrypt"; Map encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("sha256", new StandardPasswordEncoder()); PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
密码的一般格式是:
{id}encodedPassword
这样id
是用于查找应该使用PasswordEncoder
的标识符,encodedPassword
是所选PasswordEncoder
的原始编码密码。id
必须位于密码的开头,以{
开头,以}
结尾。如果找不到id
,则id
将为空。例如,以下可能是使用不同id
编码的密码列表。所有原始密码都是“密码”。
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
第一个密码的 | |
第二个密码的 | |
第三个密码的 | |
第四个密码的 | |
最终密码的 |
![]() | 注意 |
---|---|
一些用户可能担心为潜在的黑客提供存储格式。这不是问题,因为密码的存储不依赖于算法是秘密的。此外,大多数格式很容易让攻击者在没有前缀的情况下弄清楚。例如,BCrypt密码通常以 |
传递给构造函数的idForEncode
确定将使用哪个PasswordEncoder
来编码密码。在我们上面构造的DelegatingPasswordEncoder
中,这意味着编码password
的结果将被委托给BCryptPasswordEncoder
并以{bcrypt}
作为前缀。最终结果如下:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
匹配是基于{id}
以及构造函数中提供的id
到PasswordEncoder
的映射完成的。我们在“密码存储格式”一节中的示例提供了如何完成此操作的工作示例。默认情况下,使用密码调用matches(CharSequence, String)
和未映射的id
(包括空id)的结果将导致IllegalArgumentException
。可以使用DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
自定义此行为。
通过使用id
,我们可以匹配任何密码编码,但使用最现代的密码编码对密码进行编码。这很重要,因为与加密不同,密码哈希的设计使得没有简单的方法来恢复明文。由于无法恢复明文,因此难以迁移密码。虽然用户很容易迁移NoOpPasswordEncoder
,但我们默认选择将其包含在内,以便简化入门体验。
如果您正在整理演示或示例,那么花些时间来散列用户密码有点麻烦。有一些便利机制可以使这更容易,但这仍然不适合生产。
User user = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("user") .build(); System.out.println(user.getPassword()); // {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
如果要创建多个用户,还可以重用该构建器。
UserBuilder users = User.withDefaultPasswordEncoder(); User user = users .username("user") .password("password") .roles("USER") .build(); User admin = users .username("admin") .password("password") .roles("USER","ADMIN") .build();
这会对存储的密码进行哈希处理,但密码仍会在内存和已编译的源代码中公开。因此,对于生产环境而言仍然不被认为是安全的。对于生产,您应该在外部散列密码。
如其中一个密码没有id,如“密码存储格式”一节中所述,会发生以下错误。
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233) at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
解决错误的最简单方法是切换到显式提供密码编码的PasswordEncoder
。解决问题的最简单方法是弄清楚当前如何存储密码并明确提供正确的PasswordEncoder
。如果从Spring Security 4.2.x迁移,则可以通过公开NoOpPasswordEncoder
bean恢复到先前的行为。例如,如果您使用的是Java配置,则可以创建如下配置:
![]() | 警告 |
---|---|
恢复到 |
@Bean public static NoOpPasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
如果您使用的是XML配置,则可以使用id passwordEncoder
公开PasswordEncoder
:
<b:bean id="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
有关映射的完整列表,请参阅PasswordEncoderFactories上的Javadoc 。
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));
Spring Security增加了Jackson对持续Spring Security相关课程的支持。在使用分布式会话(即会话复制,Spring Session等)时,这可以提高序列化Spring Security相关类的性能。
要使用它,请将SecurityJackson2Modules.getModules(ClassLoader)
注册为Jackson模块。
ObjectMapper mapper = new ObjectMapper(); ClassLoader loader = getClass().getClassLoader(); List<Module> modules = SecurityJackson2Modules.getModules(loader); mapper.registerModules(modules); // ... use ObjectMapper as normally ... SecurityContext context = new SecurityContextImpl(); // ... String json = mapper.writeValueAsString(context);
本节介绍Spring Security提供的测试支持。
![]() | 小费 |
---|---|
要使用Spring Security测试支持,必须包含 |
本节演示如何使用Spring Security的测试支持来测试基于安全性的方法。我们首先介绍一个MessageService
,要求用户进行身份验证才能访问它。
public class HelloMessageService implements MessageService { @PreAuthorize("authenticated") public String getMessage() { Authentication authentication = SecurityContextHolder.getContext() .getAuthentication(); return "Hello " + authentication; } }
getMessage
的结果是对当前Spring Security Authentication
说“Hello”的字符串。输出的示例如下所示。
Hello org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ca25360: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
在我们使用Spring Security测试支持之前,我们必须执行一些设置。下面是一个例子:
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration
public class WithMockUserTests {
这是如何设置Spring Security测试的基本示例。亮点是:
| |
|
![]() | 注意 |
---|---|
Spring Security使用 |
请记住,我们在HelloMessageService
中添加了@PreAuthorize
注释,因此需要经过身份验证的用户才能调用它。如果我们运行以下测试,我们希望以下测试通过:
@Test(expected = AuthenticationCredentialsNotFoundException.class) public void getMessageUnauthenticated() { messageService.getMessage(); }
问题是“作为特定用户,我们怎样才能最轻松地运行测试?” 答案是使用@WithMockUser
。以下测试将以用户名“user”,密码“password”和角色“ROLE_USER”的用户身份运行。
@Test @WithMockUser public void getMessageWithMockUser() { String message = messageService.getMessage(); ... }
具体如下:
SecurityContext
中填充的Authentication
类型为UsernamePasswordAuthenticationToken
Authentication
上的校长是Spring Security的User
对象
User
将具有“user”的用户名,密码“password”,并且使用名为“ROLE_USER”的单个GrantedAuthority
。
我们的例子很好,因为我们能够利用很多默认值。如果我们想用不同的用户名运行测试怎么办?以下测试将使用用户名“customUser”运行。同样,用户不需要实际存在。
@Test @WithMockUser("customUsername") public void getMessageWithMockUserCustomUsername() { String message = messageService.getMessage(); ... }
我们还可以轻松自定义角色。例如,将使用用户名“admin”和角色“ROLE_USER”和“ROLE_ADMIN”调用此测试。
@Test @WithMockUser(username="admin",roles={"USER","ADMIN"}) public void getMessageWithMockUserCustomUser() { String message = messageService.getMessage(); ... }
如果我们不希望值自动以ROLE_为前缀,我们可以利用authority属性。例如,将使用用户名“admin”和权限“USER”和“ADMIN”调用此测试。
@Test @WithMockUser(username = "admin", authorities = { "ADMIN", "USER" }) public void getMessageWithMockUserCustomAuthorities() { String message = messageService.getMessage(); ... }
当然,在每个测试方法上放置注释可能有点单调乏味。相反,我们可以将注释放在类级别,每个测试都将使用指定的用户。例如,以下内容将使用用户名为“admin”,密码为“password”以及角色“ROLE_USER”和“ROLE_ADMIN”的用户运行每个测试。
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @WithMockUser(username="admin",roles={"USER","ADMIN"}) public class WithMockUserTests {
默认情况下,SecurityContext
在TestExecutionListener.beforeTestMethod
事件期间设置。这相当于在JUnit的@Before
之前发生的事情。您可以将此更改发生在JUnit的@Before
之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution
事件期间。
@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
使用@WithAnonymousUser
允许以匿名用户身份运行。当您希望与特定用户运行大多数测试但希望以匿名用户身份运行一些测试时,这尤其方便。例如,以下将使用@WithMockUser和匿名用户匿名用户运行withMockUser1和withMockUser2 。
@RunWith(SpringJUnit4ClassRunner.class) @WithMockUser public class WithUserClassLevelAuthenticationTests { @Test public void withMockUser1() { } @Test public void withMockUser2() { } @Test @WithAnonymousUser public void anonymous() throws Exception { // override default to run as anonymous user } }
默认情况下,SecurityContext
在TestExecutionListener.beforeTestMethod
事件期间设置。这相当于在JUnit的@Before
之前发生的事情。您可以将此更改发生在JUnit的@Before
之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution
事件期间。
@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
虽然@WithMockUser
是一种非常方便的入门方式,但它可能并不适用于所有情况。例如,应用程序通常期望Authentication
主体属于特定类型。这样做是为了使应用程序可以将主体引用为自定义类型并减少Spring Security上的耦合。
自定义主体通常由自定义UserDetailsService
返回,返回实现UserDetails
和自定义类型的对象。对于这种情况,使用自定义UserDetailsService
创建测试用户很有用。这正是@WithUserDetails
所做的。
假设我们将一个UserDetailsService
暴露为bean,将使用类型为UsernamePasswordAuthenticationToken
的Authentication
调用以及使用用户名“user”从UserDetailsService
返回的主体调用以下测试。
@Test @WithUserDetails public void getMessageWithUserDetails() { String message = messageService.getMessage(); ... }
我们还可以自定义用于从UserDetailsService
查找用户的用户名。例如,此测试将使用从UserDetailsService
以“customUsername”用户名返回的主体执行。
@Test @WithUserDetails("customUsername") public void getMessageWithUserDetailsCustomUsername() { String message = messageService.getMessage(); ... }
我们还可以提供一个显式的bean名称来查找UserDetailsService
。例如,此测试将使用带有bean名称“myUserDetailsService”的UserDetailsService
查找“customUsername”的用户名。
@Test @WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService") public void getMessageWithUserDetailsServiceBeanName() { String message = messageService.getMessage(); ... }
与@WithMockUser
一样,我们也可以将我们的注释放在类级别,以便每个测试都使用相同的用户。但是,与@WithMockUser
不同,@WithUserDetails
要求用户存在。
默认情况下,SecurityContext
在TestExecutionListener.beforeTestMethod
事件期间设置。这相当于在JUnit的@Before
之前发生的事情。您可以将此更改发生在JUnit的@Before
之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution
事件期间。
@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
我们已经看到@WithMockUser
是一个很好的选择,如果我们不使用自定义Authentication
校长。接下来我们发现@WithUserDetails
允许我们使用自定义UserDetailsService
创建我们的Authentication
主体,但要求用户存在。我们现在将看到一个允许最大灵活性的选项。
我们可以创建自己的注释,使用@WithSecurityContext
创建我们想要的任何SecurityContext
。例如,我们可能会创建一个名为@WithMockCustomUser
的注释,如下所示:
@Retention(RetentionPolicy.RUNTIME) @WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) public @interface WithMockCustomUser { String username() default "rob"; String name() default "Rob Winch"; }
您可以看到@WithMockCustomUser
使用@WithSecurityContext
注释进行注释。这是Spring Security测试支持的信号,我们打算为测试创建一个SecurityContext
。@WithSecurityContext
注释要求我们指定一个SecurityContextFactory
,它将根据我们的@WithMockCustomUser
注释创建一个新的SecurityContext
。您可以在下面找到我们的WithMockCustomUserSecurityContextFactory
实施:
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> { @Override public SecurityContext createSecurityContext(WithMockCustomUser customUser) { SecurityContext context = SecurityContextHolder.createEmptyContext(); CustomUserDetails principal = new CustomUserDetails(customUser.name(), customUser.username()); Authentication auth = new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities()); context.setAuthentication(auth); return context; } }
我们现在可以使用我们的新注释来注释测试类或测试方法,Spring Security的WithSecurityContextTestExecutionListener
将确保我们的SecurityContext
被适当填充。
在创建自己的WithSecurityContextFactory
实现时,很高兴知道它们可以使用标准Spring注释进行注释。例如,WithUserDetailsSecurityContextFactory
使用@Autowired
注释来获取UserDetailsService
:
final class WithUserDetailsSecurityContextFactory implements WithSecurityContextFactory<WithUserDetails> { private UserDetailsService userDetailsService; @Autowired public WithUserDetailsSecurityContextFactory(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } public SecurityContext createSecurityContext(WithUserDetails withUser) { String username = withUser.value(); Assert.hasLength(username, "value() must be non-empty String"); UserDetails principal = userDetailsService.loadUserByUsername(username); Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities()); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authentication); return context; } }
默认情况下,SecurityContext
在TestExecutionListener.beforeTestMethod
事件期间设置。这相当于在JUnit的@Before
之前发生的事情。您可以将此更改发生在JUnit的@Before
之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution
事件期间。
@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)
如果经常在测试中重复使用同一个用户,则不必重复指定属性。例如,如果有许多与管理用户相关的测试,用户名为“admin”,角色为ROLE_USER
和ROLE_ADMIN
,则必须编写:
@WithMockUser(username="admin",roles={"USER","ADMIN"})
我们可以使用元注释,而不是在任何地方重复这一点。例如,我们可以创建一个名为WithMockAdmin
的元注释:
@Retention(RetentionPolicy.RUNTIME) @WithMockUser(value="rob",roles="ADMIN") public @interface WithMockAdmin { }
现在我们可以使用@WithMockAdmin
与更详细@WithMockUser
相同的方式。
元注释适用于上述任何测试注释。例如,这意味着我们也可以为@WithUserDetails("admin")
创建元注释。
Spring Security提供与Spring MVC测试的全面整合
要将Spring Security与Spring MVC测试一起使用,必须将Spring Security FilterChainProxy
添加为Filter
。还需要添加Spring Security的TestSecurityContextHolderPostProcessor
以支持在带有注释的Spring MVC测试中以用户身份运行。这可以使用Spring Security的SecurityMockMvcConfigurers.springSecurity()
来完成。例如:
![]() | 注意 |
---|---|
Spring Security的测试支持需要spring-test-4.1.3.RELEASE或更高版本。 |
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @WebAppConfiguration public class CsrfShowcaseTests { @Autowired private WebApplicationContext context; private MockMvc mvc; @Before public void setup() { mvc = MockMvcBuilders .webAppContextSetup(context) .apply(springSecurity()).build(); } ...
Spring MVC Test提供了一个名为RequestPostProcessor
的便捷界面,可用于修改请求。Spring Security提供了许多RequestPostProcessor
实现,使测试更容易。为了使用Spring Security的RequestPostProcessor
实现,请确保使用以下静态导入:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
在测试任何非安全HTTP方法并使用Spring Security的CSRF保护时,您必须确保在请求中包含有效的CSRF令牌。要使用以下命令将有效的CSRF令牌指定为请求参数:
mvc
.perform(post("/").with(csrf()))
如果您愿意,可以在标题中包含CSRF令牌:
mvc
.perform(post("/").with(csrf().asHeader()))
您还可以使用以下方法测试提供无效的CSRF令牌:
mvc
.perform(post("/").with(csrf().useInvalidToken()))
通常希望将测试作为特定用户运行。填充用户有两种简单的方法:
有许多选项可用于将用户与当前HttpServletRequest
相关联。例如,以下将以用户(不需要存在)的形式运行,用户名为“user”,密码为“password”,角色为“ROLE_USER”:
![]() | 注意 |
---|---|
支持通过将用户与
|
mvc .perform(get("/").with(user("user")))
您可以轻松进行自定义。例如,以下将以用户名(admin,不需要存在)运行,用户名为“admin”,密码为“pass”,角色为“ROLE_USER”和“ROLE_ADMIN”。
mvc .perform(get("/admin").with(user("admin").password("pass").roles("USER","ADMIN")))
如果您有自己想要使用的自定义UserDetails
,也可以轻松指定。例如,以下将使用指定的UserDetails
(不需要存在)与具有指定UserDetails
的主体的UsernamePasswordAuthenticationToken
一起运行:
mvc
.perform(get("/").with(user(userDetails)))
您可以使用以下方式以匿名用户身份运行:
mvc
.perform(get("/").with(anonymous()))
如果您使用默认用户运行并希望以匿名用户身份执行一些请求,则此功能尤其有用。
如果您需要自定义Authentication
(不需要存在),可以使用以下命令执行此操作:
mvc
.perform(get("/").with(authentication(authentication)))
您甚至可以使用以下内容自定义SecurityContext
:
mvc
.perform(get("/").with(securityContext(securityContext)))
我们还可以使用MockMvcBuilders
的默认请求确保以每个请求的特定用户身份运行。例如,以下将以用户名(admin,不需要存在)运行,用户名为“admin”,密码为“password”,角色为“ROLE_ADMIN”:
mvc = MockMvcBuilders .webAppContextSetup(context) .defaultRequest(get("/").with(user("user").roles("ADMIN"))) .apply(springSecurity()) .build();
如果您发现在许多测试中使用的是同一个用户,建议将用户移动到某个方法。例如,您可以在自己的名为CustomSecurityMockMvcRequestPostProcessors
的类中指定以下内容:
public static RequestPostProcessor rob() { return user("rob").roles("ADMIN"); }
现在,您可以在SecurityMockMvcRequestPostProcessors
上执行静态导入,并在测试中使用它:
import static sample.CustomSecurityMockMvcRequestPostProcessors.*; ... mvc .perform(get("/").with(rob()))
作为使用RequestPostProcessor
创建用户的替代方法,您可以使用第9.1节“测试方法安全性”中所述的注释。例如,以下将使用用户名“user”,密码“password”和角色“ROLE_USER”运行测试:
@Test @WithMockUser public void requestProtectedUrlWithUser() throws Exception { mvc .perform(get("/")) ... }
或者,以下将使用用户名“user”,密码“password”和角色“ROLE_ADMIN”运行测试:
@Test @WithMockUser(roles="ADMIN") public void requestProtectedUrlWithUser() throws Exception { mvc .perform(get("/")) ... }
Spring MVC测试还提供了一个RequestBuilder
接口,可用于创建测试中使用的MockHttpServletRequest
。Spring Security提供了一些RequestBuilder
实现,可用于简化测试。为了使用Spring Security的RequestBuilder
实现,请确保使用以下静态导入:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;
您可以使用Spring Security的测试支持轻松创建测试基于表单的身份验证的请求。例如,以下内容将使用用户名“user”,密码“password”和有效的CSRF令牌向“/ login”提交POST:
mvc .perform(formLogin())
可以轻松自定义请求。例如,以下内容将使用用户名“admin”,密码“pass”和有效的CSRF令牌向“/ auth”提交POST:
mvc .perform(formLogin("/auth").user("admin").password("pass"))
我们还可以自定义包含用户名和密码的参数名称。例如,这是上面的请求被修改为包括HTTP参数“u”上的用户名和HTTP参数“p”上的密码。
mvc .perform(formLogin("/auth").user("u","admin").password("p","pass"))
有时希望对请求做出各种与安全相关的断言。为了满足这种需求,Spring Security测试支持实现了Spring MVC Test的ResultMatcher
接口。为了使用Spring Security的ResultMatcher
实现,请确保使用以下静态导入:
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;
有时,断言没有与MockMvc
调用的结果相关联的经过身份验证的用户可能很有价值。例如,您可能希望测试提交无效的用户名和密码,并验证没有用户通过身份验证。使用Spring Security的测试支持可以使用以下内容轻松完成此操作:
mvc
.perform(formLogin().password("invalid"))
.andExpect(unauthenticated());
通常我们必须断言经过身份验证的用户存在。例如,我们可能想验证我们是否已成功验证。我们可以使用以下代码片段验证基于表单的登录是否成功:
mvc .perform(formLogin()) .andExpect(authenticated());
如果我们想要断言用户的角色,我们可以优化我们之前的代码,如下所示:
mvc .perform(formLogin().user("admin")) .andExpect(authenticated().withRoles("USER","ADMIN"));
或者,我们可以验证用户名:
mvc .perform(formLogin().user("admin")) .andExpect(authenticated().withUsername("admin"));
我们也可以结合断言:
mvc .perform(formLogin().user("admin").roles("USER","ADMIN")) .andExpect(authenticated().withUsername("admin"));
我们还可以对身份验证进行任意断言
mvc
.perform(formLogin())
.andExpect(authenticated().withAuthentication(auth ->
assertThat(auth).isInstanceOf(UsernamePasswordAuthenticationToken.class)));
大多数Spring Security用户将在使用HTTP和Servlet API的应用程序中使用该框架。在本部分中,我们将了解Spring Security如何为应用程序的web层提供身份验证和访问控制功能。我们将查看命名空间的外观,并查看实际组装的类和接口,以提供web - 层安全性。在某些情况下,有必要使用传统的bean配置来提供对配置的完全控制,因此我们还将看到如何在没有命名空间的情况下直接配置这些类。
Spring Security的web基础结构完全基于标准的servlet过滤器。它不在内部使用servlet或任何其他基于servlet的框架(例如Spring MVC),因此它与任何特定的web技术没有强大的链接。它处理HttpServletRequest
和HttpServletResponse
s并不关心请求是来自浏览器,web服务客户端,HttpInvoker
还是AJAX应用程序。
Spring Security在内部维护一个过滤器链,其中每个过滤器都有特定的责任,并根据所需的服务在配置中添加或删除过滤器。过滤器的顺序很重要,因为它们之间存在依赖关系。如果您一直在使用命名空间配置,那么将自动为您配置过滤器,您不必明确定义任何Spring bean,但这可能是您希望完全控制安全过滤器链的时候,因为您正在使用命名空间中不支持的功能,或者您正在使用自己的自定义版本的类。
使用servlet过滤器时,显然需要在web.xml
中声明它们,否则servlet容器将忽略它们。在Spring Security中,过滤器类也是在应用程序上下文中定义的Spring bean,因此能够利用Spring丰富的依赖注入工具和生命周期接口。Spring的DelegatingFilterProxy
提供了web.xml
与应用程序上下文之间的链接。
使用DelegatingFilterProxy
时,您会在web.xml
文件中看到类似的内容:
<filter> <filter-name>myFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>myFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
请注意,过滤器实际上是DelegatingFilterProxy
,而不是实际实现过滤器逻辑的类。DelegatingFilterProxy
的作用是将Filter
的方法委托给从Spring应用程序上下文中获取的bean。这使bean能够受益于Spring web应用程序上下文生命周期支持和配置灵活性。bean必须实现javax.servlet.Filter
,并且它必须与filter-name
元素中的名称相同。有关更多信息,请阅读DelegatingFilterProxy
的Javadoc
Spring Security的web基础设施只能通过委托给FilterChainProxy
的实例来使用。安全过滤器本身不应使用。从理论上讲,您可以在应用程序上下文文件中声明所需的每个Spring Security过滤器bean,并为每个过滤器添加相应的DelegatingFilterProxy
条目到web.xml
,确保它们的排序正确,但这将是如果你有很多过滤器,那么很麻烦并且会很快弄乱web.xml
文件。FilterChainProxy
允许我们向web.xml
添加一个条目,并完全处理应用程序上下文文件以管理我们的web安全bean。它使用DelegatingFilterProxy
进行连线,就像上面的示例一样,但是将filter-name
设置为bean名称“filterChainProxy”。然后,在应用程序上下文中使用相同的bean名称声明过滤器链。这是一个例子:
<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy"> <constructor-arg> <list> <sec:filter-chain pattern="/restful/**" filters=" securityContextPersistenceFilterWithASCFalse, basicAuthenticationFilter, exceptionTranslationFilter, filterSecurityInterceptor" /> <sec:filter-chain pattern="/**" filters=" securityContextPersistenceFilterWithASCTrue, formLoginFilter, exceptionTranslationFilter, filterSecurityInterceptor" /> </list> </constructor-arg> </bean>
命名空间元素filter-chain
用于方便地设置应用程序中所需的安全过滤器链。
[6]。它将特定的URL模式映射到根据filters
元素中指定的bean名称构建的过滤器列表,并将它们组合在SecurityFilterChain
类型的bean中。pattern
属性采用Ant路径,最具体的URI应首先出现[7]。在运行时,FilterChainProxy
将找到与当前web请求匹配的第一个URI模式,并且filters
属性指定的过滤器bean列表将应用于该请求。过滤器将按照定义的顺序调用,因此您可以完全控制应用于特定URL的过滤器链。
您可能已经注意到我们在过滤器链中声明了两个SecurityContextPersistenceFilter
s(ASC
是allowSessionCreation
的缩写,属性为SecurityContextPersistenceFilter
)。由于web服务永远不会在未来的请求中出现jsessionid
,因此为这样的用户代理创建HttpSession
将是浪费的。如果您的大批量应用程序需要最大的可扩展性,我们建议您使用上面显示的方法。对于较小的应用程序,使用单个SecurityContextPersistenceFilter
(默认allowSessionCreation
为true
)可能就足够了。
请注意,FilterChainProxy
不会在配置的过滤器上调用标准过滤器生命周期方法。我们建议您使用Spring的应用程序上下文生命周期接口作为替代,就像使用任何其他Spring bean一样。
当我们查看如何使用命名空间配置设置web安全性时,我们使用名为“springSecurityFilterChain”的DelegatingFilterProxy
。您现在应该能够看到这是由命名空间创建的FilterChainProxy
的名称。
过滤器在链中定义的顺序非常重要。无论您实际使用哪种过滤器,订单应如下:
ChannelProcessingFilter
,因为它可能需要重定向到不同的协议
SecurityContextPersistenceFilter
,因此可以在web请求开头的SecurityContextHolder
中设置SecurityContext
,并且SecurityContext
的任何更改都可以复制到HttpSession
当web请求结束时(准备好与下一个web请求一起使用)
ConcurrentSessionFilter
,因为它使用SecurityContextHolder
功能并需要更新SessionRegistry
以反映来自校长的持续请求
UsernamePasswordAuthenticationFilter
,CasAuthenticationFilter
,BasicAuthenticationFilter
等 - 以便SecurityContextHolder
可以修改为包含有效的Authentication
请求令牌
SecurityContextHolderAwareRequestFilter
,如果您使用它将Spring Security识别HttpServletRequestWrapper
安装到您的servlet容器中
JaasApiIntegrationFilter
,如果SecurityContextHolder
位于SecurityContextHolder
,则会将FilterChain
视为JaasAuthenticationToken
中的Subject
RememberMeAuthenticationFilter
,如果没有早期的身份验证处理机制更新SecurityContextHolder
,并且请求提供了一个启用记住我服务的cookie,则会在那里放置一个合适的记忆Authentication
对象
AnonymousAuthenticationFilter
,如果没有早期的身份验证处理机制更新SecurityContextHolder
,那么匿名Authentication
对象将被放置在那里
ExceptionTranslationFilter
,捕获任何Spring Security异常,以便可以返回HTTP错误响应或启动适当的AuthenticationEntryPoint
FilterSecurityInterceptor
,用于保护web URI并在访问被拒绝时引发异常
Spring Security有几个区域,您定义的模式将针对传入请求进行测试,以决定如何处理请求。当FilterChainProxy
决定应该通过哪个过滤器链以及FilterSecurityInterceptor
决定哪个安全约束适用于请求时,会发生这种情况。在针对您定义的模式进行测试时,了解机制是什么以及使用什么URL值非常重要。
Servlet规范定义了HttpServletRequest
的几个属性,这些属性可以通过getter方法访问,我们可能希望与之匹配。这些是contextPath
,servletPath
,pathInfo
和queryString
。Spring Security仅对保护应用程序中的路径感兴趣,因此忽略contextPath
。不幸的是,servlet规范没有准确定义servletPath
和pathInfo
的值将包含特定请求URI的内容。例如,URL的每个路径段可以包含参数,如RFC 2396
[8]中所定义。规范没有明确说明这些是否应该包含在servletPath
和pathInfo
值中,并且不同servlet容器之间的行为也不同。存在这样的危险:当应用程序部署在不从这些值中删除路径参数的容器中时,攻击者可以将它们添加到请求的URL中,以使模式匹配成功或意外失败。
[9]。传入URL的其他变体也是可能的。例如,它可能包含路径遍历序列(如/../
)或多个正斜杠(//
),这也可能导致模式匹配失败。一些容器在执行servlet映射之前将这些规范化,但其他容器则没有。为了防止出现这些问题,FilterChainProxy
使用HttpFirewall
策略检查并包装请求。默认情况下会自动拒绝未规范化的请求,并且会删除路径参数和重复斜杠以进行匹配。
[10]。因此,必须使用FilterChainProxy
来管理安全过滤器链。请注意,servletPath
和pathInfo
值由容器解码,因此您的应用程序不应包含任何包含分号的有效路径,因为这些部分将被删除以用于匹配目的。
如上所述,默认策略是使用Ant样式路径进行匹配,这可能是大多数用户的最佳选择。该策略在类AntPathRequestMatcher
中实现,该类使用Spring的AntPathMatcher
来执行模式与连接的servletPath
和pathInfo
的不区分大小写的匹配,忽略queryString
。
如果由于某种原因,您需要更强大的匹配策略,则可以使用正则表达式。战略实施是RegexRequestMatcher
。有关更多信息,请参阅此类的Javadoc。
实际上,我们建议您在服务层使用方法安全性,以控制对应用程序的访问,而不是完全依赖于web - 应用程序级别定义的安全性约束。URL发生变化,很难考虑应用程序可能支持的所有可能的URL以及如何操作请求。您应该尝试限制自己使用一些简单易懂的简单蚂蚁路径。始终尝试使用“默认拒绝”方法,其中您最后定义了一个全能通配符(/ 或)并拒绝访问。
在服务层定义的安全性更强大,更难以绕过,因此您应该始终利用Spring Security的方法安全选项。
HttpFirewall
还通过拒绝HTTP响应标头中的新行字符来阻止HTTP响应拆分。
默认情况下,使用StrictHttpFirewall
。此实现拒绝看似恶意的请求。如果它对您的需求过于严格,那么您可以自定义拒绝的请求类型。但是,重要的是要知道这可以打开您的应用程序直至攻击。例如,如果您希望利用Spring MVC的矩阵变量,可以在XML中使用以下配置:
<b:bean id="httpFirewall" class="org.springframework.security.web.firewall.StrictHttpFirewall" p:allowSemicolon="true"/> <http-firewall ref="httpFirewall"/>
通过公开StrictHttpFirewall
bean,Java Configuration可以实现同样的目的。
@Bean public StrictHttpFirewall httpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowSemicolon(true); return firewall; }
StrictHttpFirewall
提供了有效HTTP方法的白名单,可以防止跨站点跟踪(XST)和HTTP动词篡改。默认的有效方法是“DELETE”,“GET”,“HEAD”,“OPTIONS”,“PATCH”,“POST”和“PUT”。如果您的应用程序需要修改有效方法,则可以配置自定义StrictHttpFirewall
bean。例如,以下内容仅允许HTTP“GET”和“POST”方法:
<b:bean id="httpFirewall" class="org.springframework.security.web.firewall.StrictHttpFirewall" p:allowedHttpMethods="GET,HEAD"/> <http-firewall ref="httpFirewall"/>
通过公开StrictHttpFirewall
bean,Java Configuration可以实现同样的目的。
@Bean public StrictHttpFirewall httpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST")); return firewall; }
![]() | 小费 |
---|---|
如果您使用的是 |
如果必须允许任何HTTP方法(不推荐),则可以使用StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)
。这将完全禁用HTTP方法的验证。
如果您正在使用其他基于过滤器的框架,那么您需要确保首先使用Spring Security过滤器。这样可以及时填充SecurityContextHolder
以供其他过滤器使用。示例是使用SiteMesh来装饰您的web页面或像Wicket这样的web框架,它使用过滤器来处理其请求。
正如我们之前在命名空间章节中看到的那样,可以使用多个http
元素为不同的URL模式定义不同的安全配置。每个元素在内部FilterChainProxy
内创建一个过滤器链,并在应该映射到它的URL模式中创建。元素将按声明的顺序添加,因此必须首先声明最具体的模式。这是另一个例子,对于与上面类似的情况,应用程序同时支持无状态RESTful API以及用户使用表单登录的普通web应用程序。
<!-- Stateless RESTful service using Basic authentication --> <http pattern="/restful/**" create-session="stateless"> <intercept-url pattern='/**' access="hasRole('REMOTE')" /> <http-basic /> </http> <!-- Empty filter chain for the login page --> <http pattern="/login.htm*" security="none"/> <!-- Additional filter chain for normal users, matching all other requests --> <http> <intercept-url pattern='/**' access="hasRole('USER')" /> <form-login login-page='/login.htm' default-target-url="/home.htm"/> <logout /> </http>
有一些关键过滤器将始终用于使用Spring Security的web应用程序,因此我们将首先查看这些及其支持类和接口。我们不会涵盖所有功能,因此如果您想获得完整的图片,请务必查看Javadoc。
在讨论访问控制时,我们已经简要地看过FilterSecurityInterceptor
,我们已经将它与命名空间一起使用,其中<intercept-url>
元素被组合在内部进行配置。现在我们将看到如何显式配置它以使用FilterChainProxy
及其伴随过滤器ExceptionTranslationFilter
。典型配置示例如下所示:
<bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <property name="authenticationManager" ref="authenticationManager"/> <property name="accessDecisionManager" ref="accessDecisionManager"/> <property name="securityMetadataSource"> <security:filter-security-metadata-source> <security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/> <security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/> </security:filter-security-metadata-source> </property> </bean>
FilterSecurityInterceptor
负责处理HTTP资源的安全性。它需要引用AuthenticationManager
和AccessDecisionManager
。它还提供了适用于不同HTTP URL请求的配置属性。请参阅技术介绍中有关这些内容的原始讨论。
FilterSecurityInterceptor
可以通过两种方式配置配置属性。第一个,如上所示,使用<filter-security-metadata-source>
命名空间元素。这类似于命名空间章节中的<http>
元素,但<intercept-url>
子元素仅使用pattern
和access
属性。逗号用于分隔适用于每个HTTP URL的不同配置属性。第二个选项是编写自己的SecurityMetadataSource
,但这超出了本文档的范围。无论使用何种方法,SecurityMetadataSource
都负责返回包含与单个安全HTTP URL关联的所有配置属性的List<ConfigAttribute>
。
应该注意的是,FilterSecurityInterceptor.setSecurityMetadataSource()
方法实际上需要一个FilterInvocationSecurityMetadataSource
的实例。这是一个子类SecurityMetadataSource
的标记接口。它只是表示SecurityMetadataSource
理解FilterInvocation
s。为了简单起见,我们将继续将FilterInvocationSecurityMetadataSource
称为SecurityMetadataSource
,因为区别与大多数用户无关。
命名空间语法创建的SecurityMetadataSource
通过将请求URL与配置的pattern
属性相匹配来获取特定FilterInvocation
的配置属性。这与命名空间配置的行为方式相同。缺省情况是将所有表达式视为Apache Ant路径,并且对于更复杂的情况也支持正则表达式。request-matcher
属性用于指定正在使用的模式类型。无法在同一定义中混合表达式语法。例如,使用正则表达式而不是Ant路径的先前配置将编写如下:
<bean id="filterInvocationInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <property name="authenticationManager" ref="authenticationManager"/> <property name="accessDecisionManager" ref="accessDecisionManager"/> <property name="runAsManager" ref="runAsManager"/> <property name="securityMetadataSource"> <security:filter-security-metadata-source request-matcher="regex"> <security:intercept-url pattern="\A/secure/super/.*\Z" access="ROLE_WE_DONT_HAVE"/> <security:intercept-url pattern="\A/secure/.*\" access="ROLE_SUPERVISOR,ROLE_TELLER"/> </security:filter-security-metadata-source> </property> </bean>
始终按照定义的顺序评估模式。因此,重要的是在列表中定义的更具体的模式比不太具体的模式更高。这反映在上面的示例中,其中更具体的/secure/super/
模式看起来高于不太具体的/secure/
模式。如果它们被反转,则/secure/
模式将始终匹配,并且永远不会评估/secure/super/
模式。
ExceptionTranslationFilter
位于安全过滤器堆栈中的FilterSecurityInterceptor
之上。它不执行任何实际的安全实施,但处理安全拦截器抛出的异常并提供合适的HTTP响应。
<bean id="exceptionTranslationFilter" class="org.springframework.security.web.access.ExceptionTranslationFilter"> <property name="authenticationEntryPoint" ref="authenticationEntryPoint"/> <property name="accessDeniedHandler" ref="accessDeniedHandler"/> </bean> <bean id="authenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> <property name="loginFormUrl" value="/login.jsp"/> </bean> <bean id="accessDeniedHandler" class="org.springframework.security.web.access.AccessDeniedHandlerImpl"> <property name="errorPage" value="/accessDenied.htm"/> </bean>
如果用户请求安全的HTTP资源但未对其进行身份验证,则将调用AuthenticationEntryPoint
。安全拦截器将在调用堆栈的下方抛出适当的AuthenticationException
或AccessDeniedException
,在入口点触发commence
方法。这样做的目的是向用户提供适当的响应,以便开始身份验证。我们在这里使用的是LoginUrlAuthenticationEntryPoint
,它将请求重定向到不同的URL(通常是登录页面)。使用的实际实现将取决于您希望在应用程序中使用的身份验证机制。
如果用户已经过身份验证并且他们尝试访问受保护资源,会发生什么?在正常使用中,这不应该发生,因为应用程序工作流应限制为用户有权访问的操作。例如,可能会向没有管理员角色的用户隐藏指向管理页面的HTML链接。但是,您不能依赖隐藏链接来保证安全性,因为用户总是有可能直接输入URL以试图绕过限制。或者他们可能会修改RESTful URL以更改某些参数值。您的应用程序必须受到这些情况的保护,否则肯定是不安全的。您通常会使用简单的web层安全性将约束应用于基本URL,并在服务层接口上使用更具体的基于方法的安全性来真正确定允许的内容。
如果抛出AccessDeniedException
并且用户已经过身份验证,那么这意味着已尝试对其没有足够权限的操作。在这种情况下,ExceptionTranslationFilter
将调用第二个策略AccessDeniedHandler
。默认情况下,使用AccessDeniedHandlerImpl
,它只向客户端发送403(禁止)响应。或者,您可以显式配置实例(如上例所示)并设置错误页面URL,它将请求转发到[11]。这可以是简单的“访问被拒绝”页面,例如JSP,或者它可以是更复杂的处理程序,例如MVC控制器。当然,您可以自己实现界面并使用自己的实现。
当您使用命名空间配置应用程序时,也可以提供自定义AccessDeniedHandler
。有关详细信息,请参阅命名空间附录。
ExceptionTranslationFilter
职责的另一个责任是在调用AuthenticationEntryPoint
之前保存当前请求。这允许在用户进行身份验证后恢复请求(请参阅先前的web身份验证概述)。一个典型的例子是用户使用表单登录,然后通过默认值SavedRequestAwareAuthenticationSuccessHandler
重定向到原始URL(见下文)。
RequestCache
封装了存储和检索HttpServletRequest
实例所需的功能。默认情况下,使用HttpSessionRequestCache
,它将请求存储在HttpSession
中。当用户被重定向到原始URL时,RequestCacheFilter
的作用是实际从缓存中恢复已保存的请求。
在正常情况下,您不需要修改任何此功能,但保存请求处理是“尽力而为”的方法,并且可能存在默认配置无法处理的情况。使用这些接口使其从Spring Security 3.0开始完全可插拔。
我们在“ 技术概述”一章中介绍了这个非常重要的过滤器的用途,因此您可能希望在此时重新阅读该部分。我们先来看看如何配置它以便与FilterChainProxy
一起使用。基本配置只需要bean本身
<bean id="securityContextPersistenceFilter" 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
属性[12]。此实现最重要的配置参数是allowSessionCreation
属性,默认为true
,因此如果需要一个会话来为经过身份验证的用户存储安全上下文,则允许该类创建会话(它不会创建一个,除非进行了身份验证并且安全上下文的内容已更改)。如果您不想创建会话,则可以将此属性设置为false
:
<bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter"> <property name='securityContextRepository'> <bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'> <property name='allowSessionCreation' value='false' /> </bean> </property> </bean>
或者,您可以提供NullSecurityContextRepository
的实例,即空对象实现,即使在请求期间已创建会话,也会阻止安全上下文的存储。
我们现在已经看到了三个主要的过滤器,这些过滤器始终存在于Spring Security web配置中。这些也是由命名空间<http>
元素自动创建的三个,不能用替代品替换。现在唯一缺少的是实际的身份验证机制,允许用户进行身份验证。此过滤器是最常用的身份验证过滤器,也是最常定制的过滤器[13]。它还提供了命名空间中<form-login>
元素使用的实现。配置它需要三个阶段。
LoginUrlAuthenticationEntryPoint
,就像我们上面所做的那样,并将其设置在ExceptionTranslationFilter
上。
UsernamePasswordAuthenticationFilter
的实例
登录表单只包含username
和password
输入字段,并发布到过滤器监控的URL(默认情况下为/login
)。基本过滤器配置如下所示:
<bean id="authenticationFilter" class= "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> <property name="authenticationManager" ref="authenticationManager"/> </bean>
筛选器调用已配置的AuthenticationManager
来处理每个身份验证请求。身份验证成功或身份验证失败后的目标分别由AuthenticationSuccessHandler
和AuthenticationFailureHandler
策略接口控制。过滤器具有允许您设置这些属性的属性,因此您可以完全自定义行为[14]。提供了一些标准实现,例如SimpleUrlAuthenticationSuccessHandler
,SavedRequestAwareAuthenticationSuccessHandler
,SimpleUrlAuthenticationFailureHandler
,ExceptionMappingAuthenticationFailureHandler
和DelegatingAuthenticationFailureHandler
。查看这些类的Javadoc以及AbstractAuthenticationProcessingFilter
以了解它们的工作原理和支持的功能。
如果身份验证成功,生成的Authentication
对象将被放入SecurityContextHolder
。然后将调用配置的AuthenticationSuccessHandler
以将用户重定向或转发到适当的目标。默认情况下,使用SavedRequestAwareAuthenticationSuccessHandler
,这意味着在要求用户登录之前,用户将被重定向到他们请求的原始目的地。
![]() | 注意 |
---|---|
|
如果身份验证失败,将调用配置的AuthenticationFailureHandler
。
本节介绍如何将Spring Security与Servlet API集成。所述servletapi-XML示例应用程序演示每一种方法的使用。
所述HttpServletRequest.getRemoteUser()将返回的SecurityContextHolder.getContext().getAuthentication().getName()
的结果通常是当前用户名。如果要在应用程序中显示当前用户名,这可能很有用。此外,检查this是否为null可用于指示用户是否已经过身份验证或是匿名的。知道用户是否被认证可以用于确定是否应该显示某些UI元素(即,仅当用户被认证时才应该显示注销链接)。
所述HttpServletRequest.getUserPrincipal()将返回的SecurityContextHolder.getContext().getAuthentication()
的结果。这意味着它是Authentication
,当使用基于用户名和密码的身份验证时,它通常是UsernamePasswordAuthenticationToken
的实例。如果您需要有关用户的其他信息,这可能很有用。例如,您可能创建了一个自定义UserDetailsService
,它返回一个自定义UserDetails
,其中包含您的用户的名字和姓氏。您可以通过以下方式获取此信息:
Authentication auth = httpServletRequest.getUserPrincipal(); // assume integrated custom UserDetails called MyCustomUserDetails // by default, typically instance of UserDetails MyCustomUserDetails userDetails = (MyCustomUserDetails) auth.getPrincipal(); String firstName = userDetails.getFirstName(); String lastName = userDetails.getLastName();
![]() | 注意 |
---|---|
应该注意的是,在整个应用程序中执行如此多的逻辑通常是不好的做法。相反,应该集中它以减少Spring Security和Servlet API的任何耦合。 |
所述HttpServletRequest.isUserInRole(字符串)将确定是否SecurityContextHolder.getContext().getAuthentication().getAuthorities()
包含GrantedAuthority
与通入isUserInRole(String)
的作用。通常,用户不应将“ROLE_”前缀传递给此方法,因为它会自动添加。例如,如果要确定当前用户是否具有“ROLE_ADMIN”权限,则可以使用以下命令:
boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");
这可能有助于确定是否应显示某些UI组件。例如,仅当当前用户是管理员时,才可以显示管理员链接。
以下部分描述了Spring Security与之集成的Servlet 3方法。
所述HttpServletRequest.authenticate(HttpServletRequest的,HttpServletResponse的)方法可用于确保用户被认证。如果未经过身份验证,配置的AuthenticationEntryPoint将用于请求用户进行身份验证(即重定向到登录页面)。
所述HttpServletRequest.login(字符串,字符串)方法可用于与当前AuthenticationManager
对用户进行认证。例如,以下内容将尝试使用用户名“user”和密码“password”进行身份验证:
try { httpServletRequest.login("user","password"); } catch(ServletException e) { // fail to authenticate }
![]() | 注意 |
---|---|
如果您希望Spring Security处理失败的身份验证尝试,则无需捕获ServletException。 |
所述HttpServletRequest.logout()方法可用于出登录当前用户。
通常这意味着SecurityContextHolder将被清除,HttpSession将被无效,任何“记住我”身份验证将被清除,等等。但是,配置的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架构,如下所示:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" 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"> </web-app>
接下来,您需要确保设置springSecurityFilterChain以处理异步请求。
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> <async-supported>true</async-supported> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>ASYNC</dispatcher> </filter-mapping>
而已!现在Spring Security将确保您的SecurityContext也在异步请求上传播。
那么它是怎样工作的?如果您对此不感兴趣,请随意跳过本节的其余部分,否则请继续阅读。其中大部分内容都包含在Servlet规范中,但有一些调整Spring Security可以确保正确处理异步请求。在Spring Security 3.2之前,一旦HttpServletResponse被提交,SecurityContextHolder中的SecurityContext就会自动保存。这可能会导致Async环境中出现问题。例如,请考虑以下事项:
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();
问题是Spring Security不知道此线程,因此SecurityContext不会传播给它。这意味着当我们提交HttpServletResponse时,没有SecuriytContext。当Spring Security在提交HttpServletResponse时自动保存SecurityContext时,它将丢失我们的登录用户。
从版本3.2开始,Spring Security非常智能,一旦HttpServletRequest.startAsync()被调用,就不再自动保存SecurityContext以提交HttpServletResponse。
以下部分描述了Spring Security与之集成的Servlet 3.1方法。
所述HttpServletRequest.changeSessionId()是用于防止的默认方法会话固定在Servlet的3.1和更高的攻击。
基本身份验证和摘要身份验证是web应用程序中常用的备用身份验证机制。基本身份验证通常与无状态客户端一起使用,后者在每个请求上传递其凭据。将它与基于表单的身份验证结合使用是很常见的,其中应用程序通过基于浏览器的用户界面和web - 服务来使用。但是,基本身份验证将密码作为纯文本传输,因此只能在加密传输层(如HTTPS)上使用。
BasicAuthenticationFilter
负责处理HTTP标头中显示的基本身份验证凭据。这可以用于验证Spring远程协议(例如Hessian和Burlap)以及普通浏览器用户代理(例如Firefox和Internet Explorer)所做的调用。管理HTTP基本身份验证的标准由RFC 1945第11节定义,BasicAuthenticationFilter
符合此RFC。基本身份验证是一种极具吸引力的身份验证方法,因为它在用户代理中得到了广泛的部署,并且实现非常简单(它只是用户名的Base64编码:密码,在HTTP标头中指定)。
要实现HTTP基本身份验证,您需要向过滤器链添加BasicAuthenticationFilter
。应用程序上下文应包含BasicAuthenticationFilter
及其所需的协作者:
<bean id="basicAuthenticationFilter" class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter"> <property name="authenticationManager" ref="authenticationManager"/> <property name="authenticationEntryPoint" ref="authenticationEntryPoint"/> </bean> <bean id="authenticationEntryPoint" class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint"> <property name="realmName" value="Name Of Your Realm"/> </bean>
配置的AuthenticationManager
处理每个身份验证请求。如果身份验证失败,配置的AuthenticationEntryPoint
将用于重试身份验证过程。通常,您将结合使用过滤器BasicAuthenticationEntryPoint
,它返回带有合适标头的401响应,以重试HTTP基本身份验证。如果身份验证成功,生成的Authentication
对象将照常放入SecurityContextHolder
。
如果身份验证事件成功,或者未尝试进行身份验证,因为HTTP标头不包含受支持的身份验证请求,则过滤器链将正常继续。过滤器链中断的唯一时间是验证失败并调用AuthenticationEntryPoint
。
DigestAuthenticationFilter
能够处理HTTP标头中显示的摘要式身份验证凭据。摘要式身份验证尝试解决基本身份验证的许多弱点,特别是通过确保永远不会通过网络以明文形式发送凭据。许多用户代理支持摘要式身份验证,包括Mozilla Firefox和Internet Explorer。管理HTTP摘要式身份验证的标准由RFC 2617定义,它更新RFC 2069规定的摘要式身份验证标准的早期版本。大多数用户代理实现RFC 2617. Spring Security的DigestAuthenticationFilter
与“身份验证”兼容“RFC 2617规定的保护质量(qop
),它还提供与RFC 2069的向后兼容性。如果您需要使用未加密的HTTP(即没有TLS / HTTPS)并希望最大化安全性,摘要式身份验证是一个更具吸引力的选项验证过程。事实上,摘要式身份验证是WebDAV协议的强制性要求,如RFC 2518第17.1节所述。
![]() | 注意 |
---|---|
您不应该在现代应用程序中使用Digest,因为它不被认为是安全的。最明显的问题是您必须以明文,加密或MD5格式存储密码。所有这些存储格式都被认为是不安全的。相反,您应该使用单向自适应密码哈希(即bCrypt,PBKDF2,SCrypt等)。 |
摘要式身份验证的核心是“随机数”。这是服务器生成的值。Spring Security的现时采用以下格式:
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key)) expirationTime: The date and time when the nonce expires, expressed in milliseconds key: A private key to prevent modification of the nonce token
DigestAuthenticatonEntryPoint
具有指定用于生成随机数令牌的key
的属性,以及用于确定到期时间的nonceValiditySeconds
属性(默认值300,等于五分钟)。Whist永远是nonce有效,摘要是通过连接各种字符串计算的,包括用户名,密码,nonce,请求的URI,客户端生成的nonce(只是用户代理生成每个请求的随机值),领域名称等,然后执行MD5哈希。服务器和用户代理都执行此摘要计算,如果它们在包含的值(例如密码)上不一致,则会产生不同的哈希码。在Spring Security实现中,如果服务器生成的nonce仅过期(但摘要在其他方面有效),DigestAuthenticationEntryPoint
将发送"stale=true"
标头。这告诉用户代理不需要打扰用户(因为密码和用户名等是正确的),而只是使用新的nonce重试。
DigestAuthenticationEntryPoint
nonceValiditySeconds
参数的适当值取决于您的应用程序。极其安全的应用程序应注意,截获的身份验证标头可用于模拟主体,直到达到nonce中包含的expirationTime
为止。这是选择适当设置时的关键原则,但对于极其安全的应用程序而言,在第一个实例中不能通过TLS / HTTPS运行是不常见的。
由于摘要式身份验证的实现更复杂,因此通常会出现用户代理问题。例如,Internet Explorer无法在同一会话中的后续请求中显示“不透明”标记。因此,Spring Security过滤器将所有状态信息封装到“随机数”令牌中。在我们的测试中,Spring Security的实现与Mozilla Firefox和Internet Explorer可靠地工作,正确处理nonce超时等。
现在我们已经回顾了这个理论,让我们看看如何使用它。要实现HTTP摘要式身份验证,必须在过滤器链中定义DigestAuthenticationFilter
。应用程序上下文需要定义DigestAuthenticationFilter
及其所需的协作者:
<bean id="digestFilter" class= "org.springframework.security.web.authentication.www.DigestAuthenticationFilter"> <property name="userDetailsService" ref="jdbcDaoImpl"/> <property name="authenticationEntryPoint" ref="digestEntryPoint"/> <property name="userCache" ref="userCache"/> </bean> <bean id="digestEntryPoint" class= "org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint"> <property name="realmName" value="Contacts Realm via Digest Authentication"/> <property name="key" value="acegi"/> <property name="nonceValiditySeconds" value="10"/> </bean>
配置的UserDetailsService
是必需的,因为DigestAuthenticationFilter
必须能够直接访问用户的明文密码。如果您在DAO [15]中使用编码密码,则摘要式身份验证将不起作用。DAO协作者以及UserCache
通常直接与DaoAuthenticationProvider
共享。authenticationEntryPoint
属性必须为DigestAuthenticationEntryPoint
,以便DigestAuthenticationFilter
可以获得正确的realmName
和key
进行摘要计算。
与BasicAuthenticationFilter
一样,如果身份验证成功,Authentication
请求令牌将被放入SecurityContextHolder
。如果身份验证事件成功,或者由于HTTP标头未包含摘要式身份验证请求而未尝试身份验证,则过滤器链将正常继续。过滤器链中断的唯一时间是验证失败并调用AuthenticationEntryPoint
,如前一段所述。
摘要式身份验证的RFC提供了一系列附加功能,可进一步提高安全性。例如,可以在每个请求上更改随机数。尽管如此,Spring Security实现的目的是最大限度地降低实现的复杂性(以及无疑会出现的用户代理不兼容性),并避免需要存储服务器端状态。如果您希望更详细地探索这些功能,请邀请您查看RFC 2617。据我们所知,Spring Security的实现确实符合本RFC的最低标准。
记住我或持久登录认证是指web站点能够记住会话之间的主体身份。这通常通过向浏览器发送cookie来实现,在将来的会话期间检测到cookie并导致自动登录。Spring Security为这些操作提供了必要的钩子,并且有两个具体的记住我实现。一个使用散列来保护基于cookie的令牌的安全性,另一个使用数据库或其他持久存储机制来存储生成的令牌。
请注意,这两种实现都需要UserDetailsService
。如果您使用的身份验证提供程序不使用UserDetailsService
(例如,LDAP提供程序),那么除非您的应用程序上下文中还有一个UserDetailsService
bean,否则它将无法运行。
这种方法使用散列来实现有用的记住策略。本质上,在成功进行交互式身份验证后,cookie将被发送到浏览器,其中cookie的组成如下:
base64(username + ":" + expirationTime + ":" + md5Hex(username + ":" + expirationTime + ":" password + ":" + key)) username: As identifiable to the UserDetailsService password: That matches the one in the retrieved UserDetails expirationTime: The date and time when the remember-me token expires, expressed in milliseconds key: A private key to prevent modification of the remember-me token
因此,remember-me令牌仅在指定的时间段内有效,并且前提是用户名,密码和密钥不会更改。值得注意的是,这具有潜在的安全性问题,因为捕获的记住我令牌将可以从任何用户代理使用,直到令牌到期为止。这与摘要式身份验证的问题相同。如果委托人知道已经捕获了令牌,他们可以轻松更改其密码并立即使所有记住我的令牌无效。如果需要更重要的安全性,则应使用下一节中描述的方法。或者,记住我的服务根本就不应该被使用。
如果您熟悉命名空间配置一章中讨论的主题,则只需添加<remember-me>
元素即可启用remember-me身份验证:
<http> ... <remember-me key="myAppKey"/> </http>
通常会自动选择UserDetailsService
。如果您的应用程序上下文中有多个,则需要指定哪个应该与user-service-ref
属性一起使用,其中值是UserDetailsService
bean的名称。
这种方法基于文章http://jaspan.com/improved_persistent_login_cookie_best_practice并进行了一些小的修改[16]。要在命名空间配置中使用此方法,您将提供数据源引用:
<http> ... <remember-me data-source-ref="someDataSource"/> </http>
数据库应包含使用以下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)
记住我与UsernamePasswordAuthenticationFilter
一起使用,并通过AbstractAuthenticationProcessingFilter
超类中的钩子实现。它也在BasicAuthenticationFilter
内使用。钩子将在适当的时间调用具体的RememberMeServices
。界面如下所示:
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response); void loginFail(HttpServletRequest request, HttpServletResponse response); void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication);
请参考Javadoc以更全面地讨论这些方法的作用,尽管在此阶段注意AbstractAuthenticationProcessingFilter
仅调用loginFail()
和loginSuccess()
方法。只要SecurityContextHolder
不包含Authentication
,autoLogin()
就会调用autoLogin()
方法。因此,该接口为基础记忆实现提供了与认证相关的事件的充分通知,并且只要候选web请求可能包含cookie并希望被记住,就委托给实现。这种设计允许任何数量的记住我实施策略。我们在上面已经看到Spring Security提供了两种实现。我们依次看看这些。
此实现支持第10.5.2节“基于简单哈希的令牌方法”中描述的更简单的方法。TokenBasedRememberMeServices
生成RememberMeAuthenticationToken
,由RememberMeAuthenticationProvider
处理。此身份验证提供程序与TokenBasedRememberMeServices
之间共享key
。此外,TokenBasedRememberMeServices
需要一个UserDetailsService,它可以从中检索用户名和密码以进行签名比较,并生成RememberMeAuthenticationToken
以包含正确的GrantedAuthority
。应用程序应提供某种logout命令,如果用户请求,则会使cookie无效。TokenBasedRememberMeServices
还实现了Spring Security的LogoutHandler
接口,因此可以与LogoutFilter
一起使用以自动清除cookie。
应用程序上下文中启用remember-me服务所需的bean如下所示:
<bean id="rememberMeFilter" class= "org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter"> <property name="rememberMeServices" ref="rememberMeServices"/> <property name="authenticationManager" ref="theAuthenticationManager" /> </bean> <bean id="rememberMeServices" class= "org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices"> <property name="userDetailsService" ref="myUserDetailsService"/> <property name="key" value="springRocks"/> </bean> <bean id="rememberMeAuthenticationProvider" class= "org.springframework.security.authentication.RememberMeAuthenticationProvider"> <property name="key" value="springRocks"/> </bean>
不要忘记将RememberMeServices
实施添加到您的UsernamePasswordAuthenticationFilter.setRememberMeServices()
媒体资源中,在AuthenticationManager.setProviders()
列表中添加RememberMeAuthenticationProvider
,并将RememberMeAuthenticationFilter
添加到您的FilterChainProxy
中(通常会在你的UsernamePasswordAuthenticationFilter
)。
此类可以与TokenBasedRememberMeServices
相同的方式使用,但它还需要配置PersistentTokenRepository
来存储令牌。有两种标准实现。
InMemoryTokenRepositoryImpl
仅用于测试。
JdbcTokenRepositoryImpl
将令牌存储在数据库中。
上面的第10.5.3节“持久令牌方法”中描述了数据库模式。
本节讨论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页面,其格式如下:
<form action="https://bank.example.com/transfer" method="post"> <input type="hidden" name="amount" value="100.00"/> <input type="hidden" name="routingNumber" value="evilsRoutingNumber"/> <input type="hidden" name="account" value="evilsAccountNumber"/> <input type="submit" value="Win Money!"/> </form>
你想赢钱,所以你点击提交按钮。在此过程中,您无意中将100美元转移给了恶意用户。发生这种情况是因为,虽然恶意网站无法看到您的Cookie,但与您的银行相关联的Cookie仍会随请求一起发送。
最糟糕的是,整个过程可以使用JavaScript自动完成。这意味着您甚至不需要单击按钮。那么我们如何保护自己免受此类攻击呢?
问题是来自银行网站的HTTP请求和来自邪恶网站的请求完全相同。这意味着无法拒绝来自恶意网站的请求并允许来自银行网站的请求。为了防止CSRF攻击,我们需要确保请求中存在恶意网站无法提供的内容。
一种解决方案是使用同步器令牌模式。此解决方案是为了确保每个请求除了我们的会话cookie之外还需要随机生成的令牌作为HTTP参数。提交请求时,服务器必须查找参数的预期值,并将其与请求中的实际值进行比较。如果值不匹配,请求将失败。
我们可以放松期望,只需要为每个更新状态的HTTP请求提供令牌。这可以安全地完成,因为相同的原始策略确保邪恶站点无法读取响应。此外,我们不希望在HTTP GET中包含随机令牌,因为这可能导致令牌泄露。
让我们来看看我们的例子将如何改变。假设随机生成的令牌存在于名为_csrf的HTTP参数中。例如,转账的请求如下:
POST /transfer HTTP/1.1 Host: bank.example.com Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly Content-Type: application/x-www-form-urlencoded amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>
您会注意到我们添加了带有随机值的_csrf参数。现在邪恶的网站将无法猜测_csrf参数的正确值(必须在恶意网站上明确提供),并且当服务器将实际令牌与预期令牌进行比较时,传输将失败。
什么时候应该使用CSRF保护?我们的建议是对普通用户可以由浏览器处理的任何请求使用CSRF保护。如果您只创建非浏览器客户端使用的服务,则可能需要禁用CSRF保护。
一个常见的问题是“我是否需要保护javascript发出的JSON请求?” 简而言之,这取决于。但是,您必须非常小心,因为存在可能影响JSON请求的CSRF漏洞。例如,恶意用户可以使用以下格式使用JSON创建CSRF:
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain"> <input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'> <input type="submit" value="Win Money!"/> </form>
这将产生以下JSON结构
{ "amount": 100, "routingNumber": "evilsRoutingNumber", "account": "evilsAccountNumber", "ignore_me": "=test" }
如果应用程序未验证Content-Type,那么它将暴露给此漏洞。根据设置,通过更新URL后缀以“.json”结尾,仍然可以利用验证Content-Type的Spring MVC应用程序,如下所示:
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain"> <input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'> <input type="submit" value="Win Money!"/> </form>
那么使用Spring Security保护我们网站免受CSRF攻击的必要步骤是什么?使用Spring Security CSRF保护的步骤概述如下:
防止CSRF攻击的第一步是确保您的网站使用正确的HTTP谓词。具体来说,在Spring Security的CSRF支持可以使用之前,您需要确定您的应用程序正在使用PATCH,POST,PUT和/或DELETE来修改状态。
这不是Spring Security支持的限制,而是对正确的CSRF预防的一般要求。原因是在HTTP GET中包含私人信息会导致信息泄露。有关使用POST而不是GET获取敏感信息的一般指导,请参阅RFC 2616第15.1.3节“在URI中编码敏感信息”。
下一步是在您的应用程序中包含Spring Security的CSRF保护。一些框架通过使用户的会话无效来处理无效的CSRF令牌,但这会导致其自身的问题。相反,默认情况下Spring Security的CSRF保护将产生HTTP 403访问被拒绝。这可以通过配置AccessDeniedHandler以不同方式处理InvalidCsrfTokenException
来自定义。
从Spring Security 4.0开始,默认情况下使用XML配置启用CSRF保护。如果要禁用CSRF保护,可以在下面看到相应的XML配置。
<http> <!-- ... --> <csrf disabled="true"/> </http>
默认情况下,Java Configuration会启用CSRF保护。如果要禁用CSRF,可以在下面看到相应的Java配置。有关如何配置CSRF保护的其他自定义,请参阅csrf()的Javadoc。
@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执行此操作的示例如下所示:
<c:url var="logoutUrl" value="/logout"/> <form action="${logoutUrl}" method="post"> <input type="submit" value="Log out" /> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> </form>
更简单的方法是使用Spring Security JSP标记库中的csrfInput标记。
![]() | 注意 |
---|---|
如果您使用Spring MVC |
如果您使用的是JSON,则无法在HTTP参数中提交CSRF令牌。相反,您可以在HTTP标头中提交令牌。典型的模式是在元标记中包含CSRF标记。JSP的示例如下所示:
<html> <head> <meta name="_csrf" content="${_csrf.token}"/> <!-- default header name is X-CSRF-TOKEN --> <meta name="_csrf_header" content="${_csrf.headerName}"/> <!-- ... --> </head> <!-- ... -->
您可以使用Spring Security JSP标记库中更简单的csrfMetaTags标记,而不是手动创建元标记。
然后,您可以在所有Ajax请求中包含令牌。如果您使用的是jQuery,可以使用以下命令完成:
$(function () { var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $(document).ajaxSend(function(e, xhr, options) { xhr.setRequestHeader(header, token); }); });
作为jQuery的替代方案,我们建议使用cujoJS的 rest.js. 该rest.js模块提供了在REST风格方式的HTTP请求和响应工作先进支持。核心功能是通过将拦截器链接到客户端来根据需要对HTTP客户端添加行为进行上下文化的能力。
var client = rest.chain(csrf, { token: $("meta[name='_csrf']").attr("content"), name: $("meta[name='_csrf_header']").attr("content") });
配置的客户端可以与需要向CSRF保护资源发出请求的应用程序的任何组件共享。rest.js和jQuery之间的一个显着区别是,只有使用配置的客户端发出的请求才会包含CSRF令牌,而jQuery中的所有请求都将包含令牌。对接收令牌的请求进行范围调整的能力有助于防止CSRF令牌泄露给第三方。有关rest.js的更多信息,请参阅rest.js参考文档。
可能存在用户希望将CsrfToken
保留在cookie中的情况。默认情况下,CookieCsrfTokenRepository
将写入名为XSRF-TOKEN
的cookie,并从名为X-XSRF-TOKEN
的标头或HTTP参数_csrf
中读取它。这些默认值来自AngularJS
您可以使用以下命令在XML中配置CookieCsrfTokenRepository
:
<http> <!-- ... --> <csrf token-repository-ref="tokenRepository"/> </http> <b:bean id="tokenRepository" class="org.springframework.security.web.csrf.CookieCsrfTokenRepository" p:cookieHttpOnly="false"/>
![]() | 注意 |
---|---|
该示例明确设置 |
您可以使用以下命令在Java配置中配置CookieCsrfTokenRepository
:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } }
![]() | 注意 |
---|---|
该示例明确设置 |
实施CSRF时有一些注意事项。
一个问题是预期的CSRF令牌存储在HttpSession中,因此只要HttpSession到期,您配置的AccessDeniedHandler
就会收到InvalidCsrfTokenException。如果您使用默认的AccessDeniedHandler
,浏览器将获得HTTP 403并显示错误的错误消息。
![]() | 注意 |
---|---|
有人可能会问为什么预期的 |
缓解活动用户遇到超时的一种简单方法是使用一些JavaScript让用户知道他们的会话即将过期。用户可以单击按钮继续并刷新会话。
或者,指定自定义AccessDeniedHandler
可让您以任何方式处理InvalidCsrfTokenException
。有关如何自定义AccessDeniedHandler
的示例,请参阅xml和Java配置的提供链接。
最后,可以将应用程序配置为使用不会过期的CookieCsrfTokenRepository。如前所述,这不如使用会话安全,但在许多情况下可以足够好。
为了防止伪造登录请求,还应该保护登录表单免受CSRF攻击。由于CsrfToken
存储在HttpSession中,这意味着只要访问CsrfToken
令牌属性就会创建HttpSession。虽然这在RESTful /无状态架构中听起来很糟糕但实际情况是状态是实现实际安全性所必需的。没有状态,如果令牌被泄露,我们无能为力。实际上,CSRF令牌的规模非常小,对我们的架构的影响可以忽略不计。
保护登录表单的常用技术是使用JavaScript函数在表单提交之前获取有效的CSRF令牌。通过执行此操作,无需考虑会话超时(在上一节中讨论),因为会话是在表单提交之前创建的(假设未配置CookieCsrfTokenRepository),因此用户可以保留在登录页面上并在需要时提交用户名/密码。为了实现这一点,您可以利用Spring Security提供的CsrfTokenArgumentResolver
并公开此处描述的端点。
添加CSRF会将LogoutFilter更新为仅使用HTTP POST。这可确保注销需要CSRF令牌,并且恶意用户无法强制注销您的用户。
一种方法是使用表单进行注销。如果您真的想要一个链接,您可以使用JavaScript让链接执行POST(即可能在隐藏的表单上)。对于禁用了JavaScript的浏览器,您可以选择让链接将用户带到将执行POST的注销确认页面。
如果你真的想在退出时使用HTTP GET,你可以这样做,但是请记住这通常不推荐。例如,以下Java配置将执行注销,并使用任何HTTP方法请求URL / logout:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")); } }
将CSRF保护与multipart / form-data一起使用有两种选择。每个选项都有其权衡。
![]() | 注意 |
---|---|
在将Spring Security的CSRF保护与多部分文件上载集成之前,请确保您可以在没有CSRF保护的情况下进行上载。有关使用Spring的多部分表单的更多信息可以在Spring引用和MultipartFilter javadoc的17.10 Spring的多部分(文件上载)支持部分中找到。 |
第一个选项是确保在Spring Security过滤器之前指定MultipartFilter
。在Spring Security过滤器之前指定MultipartFilter
意味着没有授权调用MultipartFilter
,这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序处理的文件。通常,这是推荐的方法,因为临时文件上载应该对大多数服务器产生可忽略的影响。
为了确保在使用java配置的Spring Security过滤器之前指定MultipartFilter
,用户可以覆盖beforeSpringSecurityFilterChain,如下所示:
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer { @Override protected void beforeSpringSecurityFilterChain(ServletContext servletContext) { insertFilters(servletContext, new MultipartFilter()); } }
为确保在使用XML配置的Spring Security过滤器之前指定MultipartFilter
,用户可以确保MultipartFilter
的<filter-mapping>元素放在web。xml中的springSecurityFilterChain之前,如图所示下面:
<filter> <filter-name>MultipartFilter</filter-name> <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class> </filter> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>MultipartFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
如果允许未经授权的用户上传临时文件是不可接受的,另一种方法是在Spring Security过滤器之后放置MultipartFilter
并将CSRF作为查询参数包含在表单的action属性中。jsp的示例如下所示
<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
这种方法的缺点是可能泄漏查询参数。更常见的是,将敏感数据放入正文或标题中以确保其不会泄露被认为是最佳做法。其他信息可以在RFC 2616第15.1.3节“在URI中编码敏感信息”中找到。
Spring Security的目标是提供保护用户免受攻击的默认设置。这并不意味着您被迫接受所有默认值。
例如,您可以提供自定义CsrfTokenRepository来覆盖CsrfToken
的存储方式。
您还可以指定自定义RequestMatcher来确定哪些请求受CSRF保护(即,您可能不关心是否利用了注销)。简而言之,如果Spring Security的CSRF保护行为不完全符合您的要求,您就可以自定义行为。参考the section called “<csrf>” 有关如何使用XML进行这些自定义的详细信息的文档,以及有关如何在使用Java配置时进行这些自定义的详细信息的CsrfConfigurer
javadoc。
Spring Framework 为CORS提供一流的支持。CORS必须在Spring Security之前处理,因为飞行前请求不包含任何cookie(即JSESSIONID
)。如果请求不包含任何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
<http> <cors configuration-source-ref="corsSource"/> ... </http> <b:bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource"> ... </b:bean>
如果您使用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
<http> <!-- Default to Spring MVC's CORS configuration --> <cors /> ... </http>
本节讨论Spring Security对向响应添加各种安全标头的支持。
Spring Security允许用户轻松注入默认安全标头以帮助保护其应用程序。Spring Security的默认值包括以下标头:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Content-Type-Options: nosniff Strict-Transport-Security: max-age=31536000 ; includeSubDomains X-Frame-Options: DENY X-XSS-Protection: 1; mode=block
![]() | 注意 |
---|---|
仅在HTTPS请求中添加严格传输安全性 |
有关每个标头的其他详细信息,请参阅相应的部分:
虽然这些标头中的每一个都被认为是最佳实践,但应注意并非所有客户端都使用标头,因此鼓励进行额外的测试。
您可以自定义特定标头。例如,假设您希望HTTP响应标头如下所示:
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
具体来说,您希望所有默认标头都具有以下自定义项:
您可以使用以下Java配置轻松完成此操作:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .frameOptions().sameOrigin() .httpStrictTransportSecurity().disable(); } }
或者,如果您使用的是Spring Security XML配置,则可以使用以下命令:
<http> <!-- ... --> <headers> <frame-options policy="SAMEORIGIN" /> <hsts disable="true"/> </headers> </http>
如果您不希望添加默认值并希望明确控制应使用的内容,则可以禁用默认值。下面提供了基于Java和XML的配置的示例:
如果您使用的是Spring Security的Java配置,则以下内容仅添加缓存控制。
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() // do not use any default headers unless explicitly listed .defaultsDisabled() .cacheControl(); } }
以下XML仅添加缓存控制。
<http> <!-- ... --> <headers defaults-disabled="true"> <cache-control/> </headers> </http>
如有必要,您可以使用以下Java配置禁用所有HTTP安全响应标头:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers().disable(); } }
如有必要,您可以使用以下XML配置禁用所有HTTP安全响应标头:
<http> <!-- ... --> <headers disabled="true" /> </http>
过去Spring Security要求您为web应用程序提供自己的缓存控制。这在当时似乎是合理的,但浏览器缓存已经发展为包括用于安全连接的缓存。这意味着用户可以查看经过身份验证的页面,注销,然后恶意用户可以使用浏览器历史记录来查看缓存页面。为了帮助缓解这种情况,Spring Security添加了缓存控制支持,它将在您的响应中插入以下标头。
Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0
简单地添加没有子元素的<headers >元素将自动添加Cache Control和其他一些保护。但是,如果您只想要缓存控制,则可以使用Spring Security的XML命名空间和<cache-control >元素以及headers @ defaults-disabled属性启用此功能。
<http> <!-- ... --> <headers defaults-disable="true"> <cache-control /> </headers> </http>
同样,您可以使用以下命令在Java配置中仅启用缓存控制:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .defaultsDisabled() .cacheControl(); } }
如果您确实想要缓存特定响应,那么您的应用程序可以有选择地调用HttpServletResponse.setHeader(String,String)来覆盖由Spring Security设置的标头。这有助于确保正确缓存CSS,JavaScript和图像等内容。
使用Spring Web MVC时,通常在您的配置中完成。例如,以下配置将确保为所有资源设置缓存标头:
@EnableWebMvc public class WebMvcConfiguration implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry .addResourceHandler("/resources/**") .addResourceLocations("/resources/") .setCachePeriod(31556926); } // ... }
历史上,浏览器(包括Internet Explorer)会尝试使用内容嗅探来猜测请求的内容类型。这允许浏览器通过猜测未指定内容类型的资源上的内容类型来改善用户体验。例如,如果浏览器遇到未指定内容类型的JavaScript文件,则可以猜测内容类型然后执行它。
![]() | 注意 |
---|---|
在允许上传内容时,还应该做许多其他事情(即仅在不同的域中显示文档,确保设置Content-Type标题,清理文档等)。但是,这些措施超出了Spring Security提供的范围。指出禁用内容嗅探时,必须指定内容类型以使事情正常工作,这一点也很重要。 |
内容嗅探的问题在于,这允许恶意用户使用多字符(即,作为多种内容类型有效的文件)来执行XSS攻击。例如,某些站点可能允许用户向网站提交有效的postscript文档并进行查看。恶意用户可能会创建一个postscript文档,该文档也是一个有效的JavaScript文件,并使用它执行XSS攻击。
可以通过在响应中添加以下标头来禁用内容嗅探:
X-Content-Type-Options: nosniff
与缓存控制元素一样,在使用没有子元素的<headers>元素时,默认情况下会添加nosniff指令。但是,如果您想要更多地控制添加哪些标头,可以使用<content-type-options >元素和headers @ defaults-disabled属性,如下所示:
<http> <!-- ... --> <headers defaults-disabled="true"> <content-type-options /> </headers> </http>
默认情况下,使用Spring Security Java配置添加X-Content-Type-Options标头。如果您想要更多地控制标题,可以使用以下内容显式指定内容类型选项:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .defaultsDisabled() .contentTypeOptions(); } }
当您在银行的网站上输入内容时,是否输入mybank.example.com或输入https://mybank.example.com?如果省略https协议,则可能容易受到中间人攻击。即使网站执行重定向到https://mybank.example.com,恶意用户也可以拦截初始HTTP请求并操纵响应(即重定向到https://mibank.example.com并窃取其凭据)。
许多用户省略了https协议,这就是创建HTTP严格传输安全(HSTS)的原因。将mybank.example.com添加为HSTS主机后,浏览器可以提前知道对mybank.example.com的任何请求都应解释为https://mybank.example.com。这大大降低了中间人攻击发生的可能性。
![]() | 注意 |
---|---|
根据RFC6797,HSTS标头仅注入HTTPS响应。为了使浏览器确认标头,浏览器必须首先信任签署用于建立连接的SSL证书的CA(而不仅仅是SSL证书)。 |
将站点标记为HSTS主机的一种方法是将主机预加载到浏览器中。另一种方法是在响应中添加“Strict-Transport-Security”标头。例如,以下内容将指示浏览器将域视为一年的HSTS主机(一年中大约有31536000秒):
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
可选的includeSubDomains指令指示Spring Security子域(即secure.mybank.example.com)也应被视为HSTS域。
与其他标头一样,Spring Security默认添加HSTS。您可以使用<hsts >元素自定义HSTS标头,如下所示:
<http> <!-- ... --> <headers> <hsts include-subdomains="true" max-age-seconds="31536000" /> </headers> </http>
同样,您只能通过Java配置启用HSTS标头:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .httpStrictTransportSecurity() .includeSubdomains(true) .maxAgeSeconds(31536000); } }
HTTP公钥锁定(HPKP)是一种安全功能,它告诉web客户端将特定加密公钥与某个web服务器相关联,以防止伪造证书的中间人(MITM)攻击。
为了确保TLS会话中使用的服务器公钥的真实性,此公钥将包装到X.509证书中,该证书通常由证书颁发机构(CA)签名。诸如浏览器之类的Web客户端信任许多这些CA,它们都可以为任意域名创建证书。如果攻击者能够破坏单个CA,则他们可以对各种TLS连接执行MITM攻击。HPKP可以通过告诉客户端哪个公钥属于某个web服务器来规避HTTPS协议的这种威胁。HPKP是首次使用信任(TOFU)技术。web服务器第一次通过特殊的HTTP头告诉客户端哪些公钥属于它,客户端会在给定的时间段内存储此信息。当客户端再次访问服务器时,它需要一个包含公钥的证书,该公钥的指纹已通过HPKP获知。如果服务器提供未知的公钥,则客户端应向用户发出警告。
![]() | 注意 |
---|---|
由于用户代理需要针对SSL证书链验证引脚,因此HPKP标头仅注入HTTPS响应。 |
为您的站点启用此功能非常简单,只需在通过HTTPS访问站点时返回Public-Key-Pins HTTP标头即可。例如,以下内容将指示用户代理仅针对2个引脚向给定URI(通过report-uri指令)报告引脚验证失败:
Public-Key-Pins-Report-Only: max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=" ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="http://example.net/pkp-report" ; includeSubDomains
甲销验证失败报告是一个标准的JSON结构可被捕获或者由web应用程序自己的API或通过公托管HPKP报告服务,诸如,REPORT-URI。
可选的includeSubDomains指令指示浏览器还使用给定的引脚验证子域。
与其他标题相反,Spring Security默认情况下不添加HPKP。您可以使用<hpkp >元素自定义HPKP标头,如下所示:
<http> <!-- ... --> <headers> <hpkp include-subdomains="true" report-uri="http://example.net/pkp-report"> <pins> <pin algorithm="sha256">d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=</pin> <pin algorithm="sha256">E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=</pin> </pins> </hpkp> </headers> </http>
同样,您可以使用Java配置启用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="; } }
允许将您的网站添加到框架可能是一个安全问题。例如,使用聪明的CSS样式用户可能会被欺骗点击他们不想要的东西(视频演示)。例如,登录到其银行的用户可能会单击授予其他用户访问权限的按钮。这种攻击称为Clickjacking。
![]() | 注意 |
---|---|
另一种处理点击劫持的现代方法是使用“内容安全策略(CSP)”一节。 |
有许多方法可以缓解点击劫持攻击。例如,为了保护旧版浏览器免受点击劫持攻击,您可以使用破帧代码。虽然不完美,但破帧代码是您可以为旧版浏览器做的最好的代码。
解决点击劫持的更现代的方法是使用X-Frame-Options标头:
X-Frame-Options: DENY
X-Frame-Options响应头指示浏览器阻止响应中具有此标头的任何站点在帧内呈现。默认情况下,Spring Security禁用iframe中的呈现。
您可以使用frame-options元素自定义X-Frame-Options 。例如,以下内容将指示Spring Security使用“X-Frame-Options:SAMEORIGIN”,它允许同一域内的iframe:
<http> <!-- ... --> <headers> <frame-options policy="SAMEORIGIN" /> </headers> </http>
同样,您可以使用以下内容自定义框架选项以在Java配置中使用相同的源:
@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
默认情况下包含此标头。但是,如果需要,我们可以自定义它。例如:
<http> <!-- ... --> <headers> <xss-protection block="false"/> </headers> </http>
同样,您可以使用以下命令在Java Configuration中自定义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应用程序期望加载资源的源。
![]() | 注意 |
---|---|
内容安全策略不是为了解决所有内容注入漏洞。相反,可以利用CSP来帮助减少内容注入攻击造成的伤害。作为第一道防线,web应用程序作者应验证其输入并对其输出进行编码。 |
web应用程序可以通过在响应中包含以下HTTP标头之一来使用CSP:
这些标头中的每一个都用作向客户端提供安全策略的机制。安全策略包含一组安全策略指令(例如,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/
违规报告是标准的JSON结构,可以由web应用程序自己的API或公共托管的CSP违规报告服务(如 REPORT-URI)捕获。
在内容安全,策略报告,仅头部为web应用程序的作者和管理员监控安全策略,而不是强制他们的能力。此标头通常在试验和/或开发站点的安全策略时使用。当策略被认为有效时,可以通过使用Content-Security-Policy头字段来强制执行该策略。
给定以下响应头,策略声明可以从两个可能的源之一加载脚本。
Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/
如果站点违反此策略,则尝试从evil.com加载脚本时,用户代理将向report-uri指令指定的声明URL发送违规报告,但仍然允许加载违规资源。
请注意,Spring Security 默认情况下不会添加内容安全策略。web应用程序作者必须声明安全策略以强制和/或监视受保护资源。
例如,给定以下安全策略:
script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/
您可以使用带有<content-security-policy >元素的XML配置启用CSP标头,如下所示:
<http> <!-- ... --> <headers> <content-security-policy policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" /> </headers> </http>
要启用CSP “仅报告”标头,请按如下方式配置元素:
<http> <!-- ... --> <headers> <content-security-policy policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" report-only="true" /> </headers> </http>
同样,您可以使用Java配置启用CSP标头,如下所示:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/"); } }
要启用CSP的“仅报告”标头,请提供以下Java配置:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/") .reportOnly(); } }
将内容安全策略应用于web应用程序通常是一件非常重要的事情。以下资源可为您的站点制定有效的安全策略提供进一步的帮助。
引用者策略是web应用程序可以利用来管理引用者字段的机制,该字段包含用户所在的最后一页。
Spring Security的方法是使用Referrer Policy标头,它提供不同的策略:
Referrer-Policy: same-origin
Referrer-Policy响应标头指示浏览器让目标知道用户之前的源。
Spring Security 默认情况下不会添加 Referrer Policy标头。
您可以使用带有<referrer-policy >元素的XML配置启用Referrer-Policy标头,如下所示:
<http> <!-- ... --> <headers> <referrer-policy policy="same-origin" /> </headers> </http>
同样,您可以使用Java配置启用Referrer Policy标头,如下所示:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .referrerPolicy(ReferrerPolicy.SAME_ORIGIN); } }
功能策略是一种允许web开发人员有选择地启用,禁用和修改浏览器中某些API和web功能的行为的机制。
Feature-Policy: geolocation 'self'
借助功能策略,开发人员可以选择加入一组“策略”,以便浏览器强制执行您网站中使用的特定功能。这些策略限制站点可以访问的API或修改浏览器对某些功能的默认行为。
Spring Security 默认情况下不添加功能策略标头。
您可以使用带有<feature-policy >元素的XML配置启用Feature-Policy标头,如下所示:
<http> <!-- ... --> <headers> <feature-policy policy-directives="geolocation 'self'" /> </headers> </http>
同样,您可以使用Java配置启用功能策略标头,如下所示:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .featurePolicy("geolocation 'self'"); } }
Spring Security具有一些机制,可以方便地将更常见的安全标头添加到您的应用程序中。但是,它还提供了挂钩以启用添加自定义标头。
有时您可能希望将自定义安全标头注入到您的应用程序中,并且不支持开箱即用。例如,给定以下自定义安全标头:
X-Custom-Security-Header: header-value
使用XML命名空间时,可以使用<header >元素将这些标头添加到响应中,如下所示:
<http> <!-- ... --> <headers> <header name="X-Custom-Security-Header" value="header-value"/> </headers> </http>
同样,可以使用Java Configuration将标头添加到响应中,如下所示:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value")); } }
当命名空间或Java配置不支持所需的标头时,您可以创建自定义HeadersWriter
实例,甚至可以提供HeadersWriter
的自定义实现。
让我们看一下使用XFrameOptionsHeaderWriter
的自定义实例的示例。也许您希望允许为同一来源构建内容。通过将policy属性设置为“SAMEORIGIN” 可以轻松支持这一点,但让我们看一下使用ref属性的更明确的示例。
<http> <!-- ... --> <headers> <header ref="frameOptionsWriter"/> </headers> </http> <!-- Requires the c-namespace. See http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-c-namespace --> <beans:bean id="frameOptionsWriter" class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter" c:frameOptionsMode="SAMEORIGIN"/>
我们还可以使用Java配置将内容框架限制为相同的来源:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN)); } }
有时您可能只想为某些请求编写标头。例如,您可能只希望保护您的登录页面不受框架限制。您可以使用DelegatingRequestMatcherHeaderWriter
来执行此操作。使用XML命名空间配置时,可以使用以下命令完成此操作:
<http> <!-- ... --> <headers> <frame-options disabled="true"/> <header ref="headerWriter"/> </headers> </http> <beans:bean id="headerWriter" class="org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter"> <beans:constructor-arg> <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher" c:pattern="/login"/> </beans:constructor-arg> <beans:constructor-arg> <beans:bean class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"/> </beans:constructor-arg> </beans:bean>
我们还可以使用java配置阻止将内容框架到登录页面:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { RequestMatcher matcher = new AntPathRequestMatcher("/login"); DelegatingRequestMatcherHeaderWriter headerWriter = new DelegatingRequestMatcherHeaderWriter(matcher,new XFrameOptionsHeaderWriter()); http // ... .headers() .frameOptions().disabled() .addHeaderWriter(headerWriter); } }
与HTTP会话相关的功能由过滤器委托的SessionManagementFilter
和SessionAuthenticationStrategy
接口的组合处理。典型用法包括会话固定保护攻击防范,会话超时检测以及经过身份验证的用户可能同时打开的会话数限制。
SessionManagementFilter
检查SecurityContextRepository
的内容与SecurityContextHolder
的当前内容,以确定用户是否在当前请求期间已经过身份验证,通常是通过非交互式身份验证机制,例如认证或记住我[17]。如果存储库包含安全上下文,则过滤器不执行任何操作。如果没有,并且线程局部SecurityContext
包含(非匿名)Authentication
对象,则过滤器假定它们已由堆栈中的先前过滤器进行了身份验证。然后它将调用配置的SessionAuthenticationStrategy
。
如果用户当前未经过身份验证,则过滤器将检查是否已请求无效会话ID(例如,由于超时),并将调用已配置的InvalidSessionStrategy
(如果已设置)。最常见的行为是重定向到固定的URL,这封装在标准实现SimpleRedirectInvalidSessionStrategy
中。如前所述,在通过命名空间配置无效会话URL时也会使用后者。
SessionManagementFilter
和AbstractAuthenticationProcessingFilter
都使用SessionAuthenticationStrategy
,因此,如果您使用自定义的表单登录类,则需要将其注入这两个类。在这种情况下,组合命名空间和自定义bean的典型配置可能如下所示:
<http> <custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" /> <session-management session-authentication-strategy-ref="sas"/> </http> <beans:bean id="myAuthFilter" class= "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> <beans:property name="sessionAuthenticationStrategy" ref="sas" /> ... </beans:bean> <beans:bean id="sas" class= "org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
请注意,如果您在实现HttpSessionBindingListener
的会话中存储bean(包括Spring会话范围的bean),则使用默认值SessionFixationProtectionStrategy
可能会导致问题。有关更多信息,请参阅此类的Javadoc。
Spring Security能够防止主体同时对同一个应用程序进行超过指定次数的身份验证。许多ISV利用此功能来强制执行许可,而网络管理员喜欢此功能,因为它有助于防止人们共享登录名。例如,您可以阻止用户“Batman”从两个不同的会话登录web应用程序。您可以使之前的登录过期,也可以在尝试再次登录时报告错误,从而阻止第二次登录。请注意,如果您使用的是第二种方法,那么未明确注销的用户(例如,刚刚关闭浏览器的用户)将无法再次登录,直到原始会话到期为止。
命名空间支持并发控制,因此请查看较早的命名空间章节以获取最简单的配置。有时你需要自定义东西。
该实现使用SessionAuthenticationStrategy
的专用版本,称为ConcurrentSessionControlAuthenticationStrategy
。
![]() | 注意 |
---|---|
以前, |
要使用并发会话支持,您需要将以下内容添加到web.xml
:
<listener> <listener-class> org.springframework.security.web.session.HttpSessionEventPublisher </listener-class> </listener>
此外,您需要将ConcurrentSessionFilter
添加到FilterChainProxy
。ConcurrentSessionFilter
需要两个构造函数参数sessionRegistry
,它们通常指向SessionRegistryImpl
和sessionInformationExpiredStrategy
的实例,它定义了会话到期时应用的策略。使用命名空间创建FilterChainProxy
和其他默认bean的配置可能如下所示:
<http> <custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" /> <custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" /> <session-management session-authentication-strategy-ref="sas"/> </http> <beans:bean id="redirectSessionInformationExpiredStrategy" class="org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy"> <beans:constructor-arg name="invalidSessionUrl" value="/session-expired.htm" /> </beans:bean> <beans:bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter"> <beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" /> <beans:constructor-arg name="sessionInformationExpiredStrategy" ref="redirectSessionInformationExpiredStrategy" /> </beans:bean> <beans:bean id="myAuthFilter" class= "org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> <beans:property name="sessionAuthenticationStrategy" ref="sas" /> <beans:property name="authenticationManager" ref="authenticationManager" /> </beans:bean> <beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy"> <beans:constructor-arg> <beans:list> <beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy"> <beans:constructor-arg ref="sessionRegistry"/> <beans:property name="maximumSessions" value="1" /> <beans:property name="exceptionIfMaximumExceeded" value="true" /> </beans:bean> <beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy"> </beans:bean> <beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy"> <beans:constructor-arg ref="sessionRegistry"/> </beans:bean> </beans:list> </beans:constructor-arg> </beans:bean> <beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />
每次HttpSession
开始或终止时,将侦听器添加到web.xml
会导致ApplicationEvent
发布到Spring ApplicationContext
。这很关键,因为它允许在会话结束时通知SessionRegistryImpl
。没有它,一旦用户超过他们的会话津贴,即使他们退出另一个会话或超时,用户也永远无法再次登录。
通过命名空间或使用普通bean设置并发控制有一个有用的副作用,即为您提供可在应用程序中直接使用的SessionRegistry
的引用,因此即使您不想限制用户可能拥有的会话数量,无论如何都可能值得设置基础架构。您可以将maximumSession
属性设置为-1以允许无限制的会话。如果您正在使用命名空间,则可以使用session-registry-alias
属性为内部创建的SessionRegistry
设置别名,从而提供可以注入您自己的bean的引用。
getAllPrincipals()
方法为您提供当前已验证用户的列表。您可以通过调用getAllSessions(Object principal, boolean includeExpiredSessions)
方法列出用户的会话,该方法返回SessionInformation
对象的列表。您还可以通过在SessionInformation
实例上调用expireNow()
来使用户的会话到期。当用户返回应用程序时,将阻止他们继续进行。例如,您可能会发现这些方法在管理应用程序中很有用。有关更多信息,请查看Javadoc。
通常认为采用“默认拒绝”是一种良好的安全措施,您可以明确指定允许的内容并禁止其他所有内容。定义未经身份验证的用户可以访问的内容也是类似的情况,特别是对于web应用程序。许多站点要求必须对用户进行身份验证,除了几个URL(例如主页和登录页面)。在这种情况下,最简单的方法是为这些特定URL定义访问配置属性,而不是为每个安全资源定义。换句话说,有时很高兴默认情况下需要ROLE_SOMETHING
,并且只允许此规则的某些例外,例如应用程序的登录,注销和主页。您也可以完全从过滤器链中省略这些页面,从而绕过访问控制检查,但由于其他原因,这可能是不合需要的,特别是如果页面对经过身份验证的用户的行为不同。
这就是匿名身份验证的含义。请注意,“匿名身份验证”的用户与未经身份验证的用户之间没有真正的概念差异。Spring Security的匿名身份验证只是为您提供了一种更方便的方法来配置访问控制属性。例如,调用servlet API调用(例如getCallerPrincipal
)仍将返回null,即使SecurityContextHolder
中实际存在匿名身份验证对象。
在其他情况下,匿名身份验证很有用,例如审计拦截器查询SecurityContextHolder
以确定哪个主体负责给定操作。如果类知道SecurityContextHolder
总是包含Authentication
对象,而且从不null
,则可以更健壮地创建类。
使用HTTP配置Spring Security 3.0时会自动提供匿名身份验证支持,并且可以使用<anonymous>
元素自定义(或禁用)。除非使用传统的bean配置,否则不需要配置此处描述的bean。
三个类一起提供匿名身份验证功能。AnonymousAuthenticationToken
是Authentication
的实现,并存储适用于匿名主体的GrantedAuthority
。有一个相应的AnonymousAuthenticationProvider
,它被链接到ProviderManager
,以便接受AnonymousAuthenticationToken
。最后,有一个AnonymousAuthenticationFilter
,它在正常的身份验证机制之后被链接,如果那里没有Authentication
,则自动将AnonymousAuthenticationToken
添加到SecurityContextHolder
。过滤器和身份验证提供程序的定义如下所示:
<bean id="anonymousAuthFilter" class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter"> <property name="key" value="foobar"/> <property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/> </bean> <bean id="anonymousAuthenticationProvider" class="org.springframework.security.authentication.AnonymousAuthenticationProvider"> <property name="key" value="foobar"/> </bean>
key
在过滤器和身份验证提供程序之间共享,因此前者创建的令牌被后者接受[18]。userAttribute
以usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]
的形式表示。这与InMemoryDaoImpl
的userMap
属性的等号后使用的语法相同。
如前所述,匿名身份验证的好处是所有URI模式都可以应用安全性。例如:
<bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <property name="authenticationManager" ref="authenticationManager"/> <property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/> <property name="securityMetadata"> <security:filter-security-metadata-source> <security:intercept-url pattern='/index.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/> <security:intercept-url pattern='/hello.htm' access='ROLE_ANONYMOUS,ROLE_USER'/> <security:intercept-url pattern='/logoff.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/> <security:intercept-url pattern='/login.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/> <security:intercept-url pattern='/**' access='ROLE_USER'/> </security:filter-security-metadata-source>" + </property> </bean>
完整的匿名身份验证讨论是AuthenticationTrustResolver
接口,其对应的AuthenticationTrustResolverImpl
实现。此接口提供isAnonymous(Authentication)
方法,允许感兴趣的类考虑这种特殊类型的身份验证状态。ExceptionTranslationFilter
在处理AccessDeniedException
时使用此接口。如果抛出AccessDeniedException
,并且身份验证是匿名类型,而不是抛出403(禁止)响应,则过滤器将启动AuthenticationEntryPoint
,以便主体可以正确进行身份验证。这是必要的区别,否则主体将始终被视为“经过身份验证”,并且永远不会有机会通过表单,基本,摘要或其他一些正常的身份验证机制进行登录。
您经常会看到上面的拦截器配置中的ROLE_ANONYMOUS
属性被IS_AUTHENTICATED_ANONYMOUSLY
替换,这在定义访问控制时实际上是相同的。这是使用AuthenticatedVoter
的一个例子,我们将在授权章节中看到。它使用AuthenticationTrustResolver
来处理此特定配置属性并授予匿名用户访问权限。AuthenticatedVoter
方法更强大,因为它允许您区分匿名,记住我和完全认证的用户。如果您不需要此功能,那么您可以使用ROLE_ANONYMOUS
,这将由Spring Security的标准RoleVoter
处理。
Spring Security 4增加了对保护Spring的WebSocket支持的支持。本节介绍如何使用Spring Security的WebSocket支持。
![]() | 注意 |
---|---|
您可以在https://github.com/spring-projects/spring-session/tree/master/samples/boot/websocket上找到WebSocket安全性的完整工作示例。 |
Spring Security 4.0通过Spring消息传递抽象引入了对WebSockets的授权支持。要使用Java Configuration配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer
并配置MessageSecurityMetadataSourceRegistry
即可。例如:
@Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {![]()
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages .simpDestMatchers("/user/*").authenticated()
} }
这将确保:
任何入站CONNECT消息都需要有效的CSRF令牌来强制实施同源策略 | |
对于任何入站请求,SecurityContextHolder将在simpUser头属性中填充用户。 | |
我们的消息需要适当的授权。具体来说,任何以“/ user /”开头的入站邮件都需要ROLE_USER。有关授权的更多详细信息,请参见第10.11.3节“WebSocket授权” |
Spring Security还提供XML Namespace支持以保护WebSockets。基于XML的可比配置如下所示:
<websocket-message-broker>![]()
![]()
<intercept-message pattern="/user/**" access="hasRole('USER')" /> </websocket-message-broker>
这将确保:
任何入站CONNECT消息都需要有效的CSRF令牌来强制实施同源策略 | |
对于任何入站请求,SecurityContextHolder将在simpUser头属性中填充用户。 | |
我们的消息需要适当的授权。具体来说,任何以“/ user /”开头的入站邮件都需要ROLE_USER。有关授权的更多详细信息,请参见第10.11.3节“WebSocket授权” |
WebSockets重用与WebSocket连接时在HTTP请求中找到的相同身份验证信息。这意味着HttpServletRequest
上的Principal
将被移交给WebSockets。如果您使用Spring Security,则会自动覆盖HttpServletRequest
上的Principal
。
更具体地说,为了确保用户已经对您的WebSocket应用程序进行了身份验证,所有必要的是确保您设置Spring Security来验证基于HTTP的web应用程序。
Spring Security 4.0通过Spring消息传递抽象引入了对WebSockets的授权支持。要使用Java Configuration配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer
并配置MessageSecurityMetadataSourceRegistry
即可。例如:
@Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { @Override protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages .nullDestMatcher().authenticated().simpSubscribeDestMatchers("/user/queue/errors").permitAll()
.simpDestMatchers("/app/**").hasRole("USER")
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER")
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll()
.anyMessage().denyAll();
} }
这将确保:
任何没有目的地的消息(即消息类型为MESSAGE或SUBSCRIBE以外的任何消息)都需要用户进行身份验证 | |
任何人都可以订阅/ user / queue / errors | |
任何目标以“/ app /”开头的消息都要求用户具有角色ROLE_USER | |
任何以SUBSCRIBE类型的“/ user /”或“/ topic / friends /”开头的消息都需要ROLE_USER | |
MESSAGE或SUBSCRIBE类型的任何其他消息都将被拒绝。由于6我们不需要这一步,但它说明了如何匹配特定的消息类型。 | |
任何其他消息都被拒绝。这是一个好主意,以确保您不会错过任何消息。 |
Spring Security还提供XML Namespace支持以保护WebSockets。基于XML的可比配置如下所示:
<websocket-message-broker><intercept-message type="CONNECT" access="permitAll" /> <intercept-message type="UNSUBSCRIBE" access="permitAll" /> <intercept-message type="DISCONNECT" access="permitAll" /> <intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" />
<intercept-message pattern="/app/**" access="hasRole('USER')" />
![]()
<intercept-message pattern="/user/**" access="hasRole('USER')" /> <intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />
<intercept-message type="MESSAGE" access="denyAll" /> <intercept-message type="SUBSCRIBE" access="denyAll" /> <intercept-message pattern="/**" access="denyAll" />
</websocket-message-broker>
这将确保:
任何类型为CONNECT,UNSUBSCRIBE或DISCONNECT的消息都需要对用户进行身份验证 | |
任何人都可以订阅/ user / queue / errors | |
任何目标以“/ app /”开头的消息都要求用户具有角色ROLE_USER | |
任何以SUBSCRIBE类型的“/ user /”或“/ topic / friends /”开头的消息都需要ROLE_USER | |
MESSAGE或SUBSCRIBE类型的任何其他消息都将被拒绝。由于6我们不需要这一步,但它说明了如何匹配特定的消息类型。 | |
具有目的地的任何其他消息都被拒绝。这是一个好主意,以确保您不会错过任何消息。 |
为了正确保护您的应用程序,了解Spring的WebSocket支持非常重要。
重要的是要理解SUBSCRIBE和MESSAGE类型的消息之间的区别以及它在Spring中的工作方式。
考虑聊天应用程序。
虽然我们希望客户端能够SUBSCRIBE到“/ topic / system / notifications”,但我们不希望它们能够将MESSAGE发送到该目的地。如果我们允许向“/ topic / system / notifications”发送MESSAGE,则客户端可以直接向该端点发送消息并模拟系统。
通常,应用程序通常拒绝发送到以代理前缀开头的消息(即“/ topic /”或“/ queue /”)的任何MESSAGE 。
了解目的地如何转变也很重要。
考虑聊天应用程序。
SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)
将消息发送给收件人。
通过上面的应用程序,我们希望允许我们的客户端监听转换为“/ queue / user / messages- <sessionid>”的“/ user / queue”。但是,我们不希望客户端能够侦听“/ queue / *”,因为这样可以让客户端看到每个用户的消息。
通常,应用程序通常拒绝发送到以代理前缀开头的消息(即“/ 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 Security需要任何CONNECT消息类型中的CSRF令牌。这可确保只有可以访问CSRF令牌的站点才能连接。由于只有Same Origin可以访问CSRF令牌,因此不允许外部域建立连接。
通常,我们需要在HTTP标头或HTTP参数中包含CSRF令牌。但是,SockJS不允许这些选项。相反,我们必须在Stomp标头中包含令牌
应用程序可以通过访问名为_csrf的请求属性来获取CSRF令牌。例如,以下内容将允许访问JSP中的CsrfToken
:
var headerName = "${_csrf.headerName}"; var token = "${_csrf.token}";
如果您使用的是静态HTML,则可以在REST端点上公开CsrfToken
。例如,以下内容将公开URL / csrf上的CsrfToken
@RestController public class CsrfController { @RequestMapping("/csrf") public CsrfToken csrf(CsrfToken token) { return token; } }
JavaScript可以对端点进行REST调用,并使用响应来填充headerName和令牌。
我们现在可以在Stomp客户端中包含令牌。例如:
... var headers = {}; headers[headerName] = token; stompClient.connect(headers, function(frame) { ... }
SockJS提供后备传输以支持旧版浏览器。使用后备选项时,我们需要放宽一些安全约束,以允许SockJS与Spring Security一起使用。
SockJS可以使用利用iframe的传输。默认情况下,Spring Security将拒绝该站点被阻止以防止Clickjacking攻击。为了允许基于SockJS帧的传输工作,我们需要配置Spring Security以允许相同的源来构建内容。
您可以使用frame-options元素自定义X-Frame-Options 。例如,以下内容将指示Spring Security使用“X-Frame-Options:SAMEORIGIN”,它允许同一域内的iframe:
<http> <!-- ... --> <headers> <frame-options policy="SAMEORIGIN" /> </headers> </http>
同样,您可以使用以下内容自定义框架选项以在Java配置中使用相同的源:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .frameOptions() .sameOrigin(); } }
对于任何基于HTTP的传输,SockJS在CONNECT消息上使用POST。通常,我们需要在HTTP标头或HTTP参数中包含CSRF令牌。但是,SockJS不允许这些选项。相反,我们必须在Stomp标头中包含令牌,如“将CSRF添加到Stomp Headers”一节中所述。
这也意味着我们需要使用web层来放松我们的CSRF保护。具体来说,我们要为连接URL禁用CSRF保护。我们不想为每个URL禁用CSRF保护。否则我们的网站将容易受到CSRF攻击。
我们可以通过提供CSRF RequestMatcher轻松实现这一目标。我们的Java配置使这非常简单。例如,如果我们的stomp端点是“/ chat”,我们可以使用以下配置仅对以“/ chat /”开头的URL禁用CSRF保护:
@Configuration @EnableWebSecurity public class 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的配置,我们可以使用csrf @ request-matcher-ref。例如:
<http ...> <csrf request-matcher-ref="csrfMatcher"/> <headers> <frame-options policy="SAMEORIGIN"/> </headers> ... </http> <b:bean id="csrfMatcher" class="AndRequestMatcher"> <b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/> <b:constructor-arg> <b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher"> <b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher"> <b:constructor-arg value="/chat/**"/> </b:bean> </b:bean> </b:constructor-arg> </b:bean>
[6]请注意,您需要在应用程序上下文XML文件中包含安全命名空间才能使用此语法。仍然支持使用filter-chain-map
的旧语法,但不赞成使用构造函数参数注入。
[7] request-matcher-ref
属性可用于指定RequestMatcher
实例以实现更强大的匹配,而不是路径模式
[8]当浏览器不支持cookie并且在分号后将jsessionid
参数附加到URL时,您可能已经看到了这一点。但是,RFC允许在URL的任何路径段中存在这些参数
[9]一旦请求离开FilterChainProxy
,将返回原始值,因此仍可供应用程序使用。
[10]因此,例如,原始请求路径/secure;hack=1/somefile.html;hack=2
将作为/secure/somefile.html
返回。
[11]我们使用forward,因此SecurityContextHolder仍然包含主体的详细信息,这可能对显示给用户很有用。在Spring Security的旧版本中,我们依靠servlet容器来处理403错误消息,该消息缺少这种有用的上下文信息。
[12]在Spring Security 2.0及更早版本中,此过滤器被称为HttpSessionContextIntegrationFilter
并且执行了存储上下文的所有工作都是由过滤器本身执行的。如果您熟悉此类,那么现在可以在HttpSessionSecurityContextRepository
上找到大多数可用的配置选项。
[13]由于历史原因,在Spring Security 3.0之前,此过滤器被称为AuthenticationProcessingFilter
,入口点被称为AuthenticationProcessingFilterEntryPoint
。由于框架现在支持许多不同形式的身份验证,因此它们在3.0中都被赋予了更具体的名称。
[14]在3.0之前的版本中,此时的应用程序流程已演变为一个阶段,由此类和策略插件的混合属性控制。决定3.0重构代码以使这两个策略完全负责。
[15]如果DigestAuthenticationFilter.passwordAlreadyEncoded
设置为true
,则可以以HEX(MD5(用户名:领域:密码))格式对密码进行编码。但是,其他密码编码不适用于摘要式身份验证。
[16]基本上,用户名不包含在cookie中,以防止不必要地暴露有效的登录名。在本文的评论部分对此进行了讨论。
[17] SessionManagementFilter
将不会检测通过身份验证后执行重定向的机制(例如表单登录)进行身份验证,因为在身份验证请求期间不会调用过滤器。在这些情况下,必须单独处理会话管理功能。
[18] key
财产的使用不应被视为在此提供任何真正的安全。这只是一本簿记练习。如果您在身份验证客户端可以构造Authentication
对象(例如使用RMI调用)的情况下共享ProviderManager
,其中包含AnonymousAuthenticationProvider
,则恶意客户端可以提交它创建的AnonymousAuthenticationToken
(选择了用户名和权限列表)。如果key
是可猜测的或可以找到,那么匿名提供者将接受该令牌。这不是正常使用的问题,但如果您使用的是RMI,最好使用自定义的ProviderManager
,它会省略匿名提供程序,而不是共享您用于HTTP身份验证机制的提供程序。
Spring Security内的高级授权功能是其受欢迎程度最引人注目的原因之一。无论您选择如何进行身份验证 - 无论是使用Spring Security - 提供的机制和提供程序,还是与容器或其他非Spring Security身份验证机构集成 - 您都会发现授权服务可以在您的应用程序中使用一致而简单的方式。
在这一部分中,我们将探讨第一部分中介绍的不同AbstractSecurityInterceptor
实现。然后我们继续探讨如何通过使用域访问控制列表来微调授权。
正如我们在技术概述中看到的,所有Authentication
实现都存储了GrantedAuthority
对象的列表。这些代表已授予委托人的当局。GrantedAuthority
对象由AuthenticationManager
插入Authentication
对象,稍后由AccessDecisionManager
在做出授权决定时读取。
GrantedAuthority
是一个只有一个方法的接口:
String getAuthority();
此方法允许AccessDecisionManager
s获得GrantedAuthority
的精确String
表示。通过将表示作为String
返回,GrantedAuthority
可以很容易地“读取”GrantedAuthority
。如果GrantedAuthority
不能精确地表示为String
,则GrantedAuthority
被视为“复杂”,getAuthority()
必须返回null
。
“复杂”GrantedAuthority
的一个示例是存储适用于不同客户帐号的操作和权限阈值列表的实现。将此复合体GrantedAuthority
表示为String
将非常困难,因此getAuthority()
方法应返回null
。这将向任何AccessDecisionManager
表明它需要专门支持GrantedAuthority
实施以了解其内容。
Spring Security包括一个具体的GrantedAuthority
实施,SimpleGrantedAuthority
。这允许任何用户指定的String
转换为GrantedAuthority
。安全体系结构中包含的所有AuthenticationProvider
都使用SimpleGrantedAuthority
填充Authentication
对象。
正如我们在技术概述章节中看到的那样,Spring Security提供了拦截器来控制对安全对象的访问,例如方法调用或web请求。AccessDecisionManager
是否允许调用是否允许进行调用。
AccessDecisionManager
由AbstractSecurityInterceptor
调用,负责做出最终的访问控制决策。AccessDecisionManager
接口包含三种方法:
void decide(Authentication authentication, Object secureObject, Collection<ConfigAttribute> attrs) throws AccessDeniedException; boolean supports(ConfigAttribute attribute); boolean supports(Class clazz);
AccessDecisionManager
的decide
方法传递了它所需的所有相关信息,以便做出授权决定。特别是,传递secure Object
可以检查实际安全对象调用中包含的那些参数。例如,假设安全对象是MethodInvocation
。查询MethodInvocation
任何Customer
参数很容易,然后在AccessDecisionManager
中实现某种安全逻辑,以确保允许委托人对该客户进行操作。如果访问被拒绝,预计实现将抛出AccessDeniedException
。
AbstractSecurityInterceptor
在启动时调用supports(ConfigAttribute)
方法来确定AccessDecisionManager
是否可以处理传递的ConfigAttribute
。安全拦截器实现调用supports(Class)
方法以确保配置的AccessDecisionManager
支持安全拦截器将呈现的安全对象的类型。
虽然用户可以实现自己的AccessDecisionManager
来控制授权的所有方面,但Spring Security包括几个基于投票的AccessDecisionManager
实现。
图11.1“投票决策管理器”说明了相关的类。
使用此方法,将对授权决策轮询一系列AccessDecisionVoter
实现。然后AccessDecisionManager
根据对选票的评估决定是否投出AccessDeniedException
。
AccessDecisionVoter
接口有三种方法:
int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs); boolean supports(ConfigAttribute attribute); boolean supports(Class clazz);
具体实现返回int
,可能的值反映在AccessDecisionVoter
静态字段ACCESS_ABSTAIN
,ACCESS_DENIED
和ACCESS_GRANTED
中。如果投票实施对授权决定没有意见,则返回ACCESS_ABSTAIN
。如果确实有意见,则必须返回ACCESS_DENIED
或ACCESS_GRANTED
。
提供Spring Security的三个具体AccessDecisionManager
与计票结果相符。ConsensusBased
实施将基于非弃权投票的共识授予或拒绝访问。提供Properties来控制投票相等或所有投票弃权的行为。如果收到一个或多个ACCESS_GRANTED
票,则AffirmativeBased
实施将授予访问权(即如果至少有一个授权投票,则拒绝投票将被忽略)。与ConsensusBased
实施一样,如果所有选民弃权,都有一个控制行为的参数。UnanimousBased
提供者期望获得一致ACCESS_GRANTED
票,以便授予访问权限,而忽略弃权。如果有任何ACCESS_DENIED
投票,它将拒绝访问。与其他实现一样,如果所有选民弃权,则有一个参数可以控制行为。
可以实现以不同方式计算选票的自定义AccessDecisionManager
。例如,来自特定AccessDecisionVoter
的投票可能会获得额外的权重,而来自特定选民的拒绝投票则可能具有否决权。
Spring Security提供的最常用的AccessDecisionVoter
是简单的RoleVoter
,它将配置属性视为简单的角色名称,并在用户被分配了该角色时授予访问权限。
如果任何ConfigAttribute
以前缀ROLE_
开头,它将投票。如果有GrantedAuthority
返回String
表示(通过getAuthority()
方法)完全等于从前缀ROLE_
开始的一个或多个ConfigAttributes
,它将投票授予访问权限。如果与ROLE_
开头的任何ConfigAttribute
没有完全匹配,则RoleVoter
将投票拒绝访问。如果没有ConfigAttribute
以ROLE_
开头,选民将弃权。
我们隐含看到的另一个选民是AuthenticatedVoter
,它可用于区分匿名,完全身份验证和记住身份验证的用户。许多站点允许在remember-me身份验证下进行某些有限访问,但需要用户通过登录进行完全访问来确认其身份。
当我们使用属性IS_AUTHENTICATED_ANONYMOUSLY
授予匿名访问权限时,AuthenticatedVoter
正在处理此属性。有关更多信息,请参阅此类的Javadoc。
显然,您还可以实现自定义AccessDecisionVoter
,并且您可以将所需的任何访问控制逻辑放在其中。它可能特定于您的应用程序(与业务逻辑相关),也可能实现某些安全管理逻辑。例如,您可以在Spring web网站上找到一篇博客文章,其中介绍了如何使用投票人实时拒绝帐户被暂停的用户访问。
虽然在继续安全对象调用之前AbstractSecurityInterceptor
调用AccessDecisionManager
,但某些应用程序需要一种修改安全对象调用实际返回的对象的方法。虽然您可以轻松实现自己的AOP关注来实现这一点,但Spring Security提供了一个方便的钩子,它具有几个与其ACL功能集成的具体实现。
图11.2“调用实现后”说明了Spring Security的AfterInvocationManager
及其具体实现。
与Spring Security的许多其他部分一样,AfterInvocationManager
具有单个具体实现AfterInvocationProviderManager
,其轮询AfterInvocationProvider
的列表。允许每个AfterInvocationProvider
修改返回对象或抛出AccessDeniedException
。实际上,多个提供者可以修改对象,因为先前提供者的结果被传递到列表中的下一个提供者。
请注意,如果您使用的是AfterInvocationManager
,则仍需要允许MethodSecurityInterceptor
的AccessDecisionManager
允许操作的配置属性。如果您使用典型的Spring Security包含的AccessDecisionManager
实现,则没有为特定安全方法调用定义配置属性将导致每个AccessDecisionVoter
放弃投票。反过来,如果AccessDecisionManager
属性“allowIfAllAbstainDecisions”为false
,则会抛出AccessDeniedException
。您可以通过(i)将“allowIfAllAbstainDecisions”设置为true
(尽管通常不建议这样做)或(ii)确保至少有一个AccessDecisionVoter
投票的配置属性来避免此潜在问题授予访问权限。后一种(推荐)方法通常通过ROLE_USER
或ROLE_AUTHENTICATED
配置属性来实现。
通常要求应用程序中的特定角色应自动“包含”其他角色。例如,在具有“admin”和“user”角色概念的应用程序中,您可能希望管理员能够执行普通用户可以执行的所有操作。为此,您可以确保为所有管理员用户分配“用户”角色。或者,您可以修改每个需要“用户”角色的访问约束,以包含“admin”角色。如果您的应用程序中有许多不同的角色,这可能会变得非常复杂。
通过使用角色层次结构,您可以配置哪些角色(或权限)应包含其他角色。Spring Security的RoleVoter RoleHierarchyVoter
的扩展版本配置了RoleHierarchy
,从中获取用户所分配的所有“可达权限”。典型配置可能如下所示:
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter"> <constructor-arg ref="roleHierarchy" /> </bean> <bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl"> <property name="hierarchy"> <value> ROLE_ADMIN > ROLE_STAFF ROLE_STAFF > ROLE_USER ROLE_USER > ROLE_GUEST </value> </property> </bean>
在这里,我们在层次结构ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST
中有四个角色。使用ROLE_ADMIN
进行身份验证的用户,在使用上述RoleHierarchyVoter
配置的AccessDecisionManager
评估安全性约束时,其行为就像拥有所有四个角色一样。>
符号可以被认为是“包含”。
角色层次结构提供了一种简便的方法,可以简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量。对于更复杂的要求,您可能希望在应用程序所需的特定访问权限和分配给用户的角色之间定义逻辑映射,在加载用户信息时在两者之间进行转换。
在Spring Security 2.0之前,保护MethodInvocation
需要相当多的锅炉板配置。现在推荐的方法安全性方法是使用命名空间配置。这样,方法安全基础结构bean就会自动为您配置,因此您实际上不需要了解实现类。我们将简要介绍这里涉及的类。
使用MethodSecurityInterceptor
强制执行方法安全性,这可以保护MethodInvocation
s。根据配置方法,拦截器可能特定于单个bean或在多个bean之间共享。拦截器使用MethodSecurityMetadataSource
实例来获取适用于特定方法调用的配置属性。MapBasedMethodSecurityMetadataSource
用于存储由方法名称(可以是通配符)键控的配置属性,并且在使用<intercept-methods>
或<protect-point>
元素在应用程序上下文中定义属性时将在内部使用。其他实现将用于处理基于注释的配置。
您当然可以在应用程序上下文中直接配置MethodSecurityIterceptor
,以便与Spring AOP的代理机制之一一起使用:
<bean id="bankManagerSecurity" class= "org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor"> <property name="authenticationManager" ref="authenticationManager"/> <property name="accessDecisionManager" ref="accessDecisionManager"/> <property name="afterInvocationManager" ref="afterInvocationManager"/> <property name="securityMetadataSource"> <sec:method-security-metadata-source> <sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/> <sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/> </sec:method-security-metadata-source> </property> </bean>
AspectJ安全拦截器与上一节中讨论的AOP Alliance安全拦截器非常相似。实际上,我们只讨论本节的不同之处。
AspectJ拦截器名为AspectJSecurityInterceptor
。与AOP Alliance安全拦截器不同,后者依赖Spring应用程序上下文通过代理编织安全拦截器,AspectJSecurityInterceptor
通过AspectJ编译器编译。在同一应用程序中使用两种类型的安全拦截器并不罕见,AspectJSecurityInterceptor
用于域对象实例安全性,AOP Alliance MethodSecurityInterceptor
用于服务层安全性。
我们首先考虑如何在Spring应用程序上下文中配置AspectJSecurityInterceptor
:
<bean id="bankManagerSecurity" class= "org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor"> <property name="authenticationManager" ref="authenticationManager"/> <property name="accessDecisionManager" ref="accessDecisionManager"/> <property name="afterInvocationManager" ref="afterInvocationManager"/> <property name="securityMetadataSource"> <sec:method-security-metadata-source> <sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/> <sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/> </sec:method-security-metadata-source> </property> </bean>
如您所见,除了类名,AspectJSecurityInterceptor
与AOP Alliance安全拦截器完全相同。实际上,两个拦截器可以共享相同的securityMetadataSource
,因为SecurityMetadataSource
与java.lang.reflect.Method
一起使用而不是AOP库特定的类。当然,您的访问决策可以访问相关的AOP库特定的调用(即MethodInvocation
或JoinPoint
),因此在进行访问决策时可以考虑一系列添加标准(例如方法参数)。
接下来,您需要定义AspectJ aspect
。例如:
package org.springframework.security.samples.aspectj; import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor; import org.springframework.security.access.intercept.aspectj.AspectJCallback; import org.springframework.beans.factory.InitializingBean; public aspect DomainObjectInstanceSecurityAspect implements InitializingBean { private AspectJSecurityInterceptor securityInterceptor; pointcut domainObjectInstanceExecution(): target(PersistableEntity) && execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect); Object around(): domainObjectInstanceExecution() { if (this.securityInterceptor == null) { return proceed(); } AspectJCallback callback = new AspectJCallback() { public Object proceedWithObject() { return proceed(); } }; return this.securityInterceptor.invoke(thisJoinPoint, callback); } public AspectJSecurityInterceptor getSecurityInterceptor() { return securityInterceptor; } public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) { this.securityInterceptor = securityInterceptor; } public void afterPropertiesSet() throws Exception { if (this.securityInterceptor == null) throw new IllegalArgumentException("securityInterceptor required"); } } }
在上面的示例中,安全拦截器将应用于PersistableEntity
的每个实例,这是一个未显示的抽象类(您可以使用您喜欢的任何其他类或pointcut
表达式)。对于那些好奇的人,需要AspectJCallback
,因为proceed();
声明仅在around()
体内有特殊意义。AspectJSecurityInterceptor
在希望目标对象继续时调用此匿名AspectJCallback
类。
您需要配置Spring以加载方面并使用AspectJSecurityInterceptor
连接它。实现此目的的bean声明如下所示:
<bean id="domainObjectInstanceSecurityAspect" class="security.samples.aspectj.DomainObjectInstanceSecurityAspect" factory-method="aspectOf"> <property name="securityInterceptor" ref="bankManagerSecurity"/> </bean>
而已!现在,您可以使用您认为合适的任何方式(例如new Person();
)从应用程序中的任何位置创建bean,并且它们将应用安全拦截器。
Spring Security 3.0引入了使用Spring EL表达式作为授权机制的能力,以及之前见过的配置属性和访问决策选民的简单使用。基于表达式的访问控制建立在相同的体系结构上,但允许将复杂的布尔逻辑封装在单个表达式中。
Spring Security使用Spring EL表达支持,如果您有兴趣更深入地理解该主题,您应该看看它是如何工作的。表达式使用“根对象”作为评估上下文的一部分进行评估。Spring Security使用web的特定类和方法安全性作为根对象,以便提供内置表达式和对诸如当前主体之类的值的访问。
表达式根对象的基类是SecurityExpressionRoot
。这提供了web和方法安全性中可用的一些常用表达式。
表11.1。常见的内置表达式
表达 | 描述 |
---|---|
| 如果当前主体具有指定角色,则返回 |
| 如果当前主体具有任何提供的角色(以逗号分隔的字符串列表给出),则返回 |
| 如果当前主体具有指定的权限,则返回 |
| 如果当前主体具有任何提供的权限(以逗号分隔的字符串列表给出),则返回 |
| 允许直接访问代表当前用户的主体对象 |
| 允许直接访问从 |
| 始终评估为 |
| 始终评估为 |
| 如果当前主体是匿名用户,则返回 |
| 如果当前主体是remember-me用户,则返回 |
| 如果用户不是匿名用户,则返回 |
| 如果用户不是匿名用户或记住我用户,则返回 |
| 如果用户有权访问给定权限的提供目标,则返回 |
| 如果用户有权访问给定权限的提供目标,则返回 |
要使用表达式来保护单个URL,首先需要将<http>
元素中的use-expressions
属性设置为true
。然后Spring Security将期望<intercept-url>
元素的access
属性包含Spring EL表达式。表达式应该计算为布尔值,定义是否允许访问。例如:
<http> <intercept-url pattern="/admin*" access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/> ... </http>
在这里,我们定义了应用程序的“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) { ... } }
您可以使用以下方法引用该方法:
<http> <intercept-url pattern="/user/**" access="@webSecurity.check(authentication,request)"/> ... </http>
或者在Java配置中
http .authorizeRequests() .antMatchers("/user/**").access("@webSecurity.check(authentication,request)") ...
有时能够引用URL中的路径变量很好。例如,考虑一个RESTful应用程序,它以/user/{userId}
格式从URL路径中按ID查找用户。
您可以通过将路径变量放在模式中来轻松引用它。例如,如果您的Bean名称为webSecurity
,则包含以下方法签名:
public class WebSecurity { public boolean checkUserId(Authentication authentication, int id) { ... } }
您可以使用以下方法引用该方法:
<http> <intercept-url pattern="/user/{userId}/**" access="@webSecurity.checkUserId(authentication,#userId)"/> ... </http>
或者在Java配置中
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
。它们的使用是通过global-method-security
命名空间元素启用的:
<global-method-security pre-post-annotations="enabled"/>
最明显有用的注释是@PreAuthorize
,它决定了是否可以实际调用方法。例如(来自“Contacts”示例应用程序)
@PreAuthorize("hasRole('USER')") public void create(Contact contact);
这意味着只有角色为“ROLE_USER”的用户才能访问。显然,使用传统配置和所需角色的简单配置属性可以轻松实现相同的目标。但是关于:
@PreAuthorize("hasPermission(#contact, 'admin')") public void deletePermission(Contact contact, Sid recipient, Permission permission);
这里我们实际上使用方法参数作为表达式的一部分来决定当前用户是否具有给定联系人的“admin”权限。内置的hasPermission()
表达式通过应用程序上下文链接到Spring Security ACL模块,我们将在下面看到。您可以按名称访问任何方法参数作为表达式变量。
Spring Security可以通过多种方式解析方法参数。Spring Security使用DefaultSecurityParameterNameDiscoverer
来发现参数名称。默认情况下,对整个方法尝试以下选项。
如果Spring Security的@P
注释出现在方法的单个参数上,则将使用该值。这对于使用JDK 8之前的JDK编译的接口非常有用,它不包含有关参数名称的任何信息。例如:
import org.springframework.security.access.method.P; ... @PreAuthorize("#c.name == authentication.name") public void doSomething(@P("c") Contact contact);
在幕后使用AnnotationParameterNameDiscoverer
实现了这种用法,可以对其进行自定义以支持任何指定注释的value属性。
如果Spring Data的@Param
注释出现在方法的至少一个参数上,则将使用该值。这对于使用JDK 8之前的JDK编译的接口非常有用,它不包含有关参数名称的任何信息。例如:
import org.springframework.data.repository.query.Param; ... @PreAuthorize("#n == authentication.name") Contact findContactByName(@Param("n") String name);
在幕后使用AnnotationParameterNameDiscoverer
实现了这种用法,可以对其进行自定义以支持任何指定注释的value属性。
表达式中提供了任何Spring - EL功能,因此您还可以访问参数的属性。例如,如果您希望某个特定方法仅允许访问其用户名与联系人的用户名匹配的用户,则可以编写
@PreAuthorize("#contact.name == authentication.name") public void doSomething(Contact contact);
这里我们访问另一个内置表达式authentication
,它是存储在安全上下文中的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<Contact> getAll();
使用@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
对象)。第一种用于已经加载了对其进行访问的域对象的情况。然后,如果当前用户具有该对象的给定权限,则表达式将返回true。第二个版本用于未加载对象但其标识符已知的情况。还需要域对象的抽象“类型”说明符,允许加载正确的ACL权限。传统上这是对象的Java类,但不一定只要与权限的加载方式一致。
要使用hasPermission()
表达式,您必须在应用程序上下文中显式配置PermissionEvaluator
。这看起来像这样:
<security:global-method-security pre-post-annotations="enabled"> <security:expression-handler ref="expressionHandler"/> </security:global-method-security> <bean id="expressionHandler" class= "org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler"> <property name="permissionEvaluator" ref="myPermissionEvaluator"/> </bean>
其中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注释不支持元注释。
在本部分中,我们将介绍需要了解前面章节的功能以及框架中一些更高级和不太常用的功能。
复杂的应用程序通常会发现需要定义访问权限,而不仅仅是在web请求或方法调用级别。相反,安全决策需要包括谁(Authentication
),哪里(MethodInvocation
)和什么(SomeDomainObject
)。换句话说,授权决策还需要考虑方法调用的实际域对象实例主题。
想象一下,您正在为宠物诊所设计应用程序。基于Spring的应用程序将有两个主要用户组:宠物诊所的工作人员以及宠物诊所的客户。员工可以访问所有数据,而您的客户只能看到自己的客户记录。为了使它更有趣,您的客户可以允许其他用户查看他们的客户记录,例如他们的“幼儿学前”导师或他们当地的“小马俱乐部”的总裁。使用Spring Security作为基础,您可以使用以下几种方法:
Customer
域对象实例中的集合,以确定哪些用户可以访问。通过使用SecurityContextHolder.getContext().getAuthentication()
,您将能够访问Authentication
对象。
AccessDecisionVoter
以强制执行Authentication
对象中存储的GrantedAuthority[]
安全性。这意味着您的AuthenticationManager
需要填充Authentication
,其中自定义GrantedAuthority[]
代表委托人可以访问的每个Customer
域对象实例。
AccessDecisionVoter
以强制执行安全性并直接打开目标Customer
域对象。这意味着您的选民需要访问允许其检索Customer
对象的DAO。然后,它将访问Customer
对象的已批准用户集合并做出适当的决定。
这些方法中的每一种都是完全合法的。但是,第一个将您的授权检查与您的业务代码相结合。这方面的主要问题包括增加了单元测试的难度,以及在其他地方重用Customer
授权逻辑会更加困难。从Authentication
对象获取GrantedAuthority[]
s也没问题,但不会扩展到大量的Customer
s。如果一个用户可能能够访问5,000 Customer
s(在这种情况下不太可能,但想象一下它是一个大型小马俱乐部的流行兽医!)消耗的内存量和构建Authentication
所需的时间对象是不受欢迎的。直接从外部代码打开Customer
的最终方法可能是三者中最好的。它实现了关注点的分离,并且不会滥用内存或CPU周期,但它仍然是低效的,因为AccessDecisionVoter
和最终的业务方法本身都会执行对负责检索Customer
的DAO的调用宾语。每个方法调用两次访问显然是不可取的。此外,列出的每种方法都需要从头开始编写自己的访问控制列表(ACL)持久性和业务逻辑。
幸运的是,还有另一种选择,我们将在下面讨论。
Spring Security的ACL服务在spring-security-acl-xxx.jar
中发布。您需要将此JAR添加到类路径中以使用Spring Security的域对象实例安全功能。
Spring Security的域对象实例安全功能以访问控制列表(ACL)的概念为中心。系统中的每个域对象实例都有自己的ACL,ACL会记录谁可以和不能使用该域对象的详细信息。考虑到这一点,Spring Security为您的应用程序提供了三个与ACL相关的主要功能:
如第一个要点所示,Spring Security ACL模块的主要功能之一是提供检索ACL的高性能方法。此ACL存储库功能非常重要,因为系统中的每个域对象实例可能具有多个访问控制条目,并且每个ACL可能从树状结构中的其他ACL继承(这通过{支持开箱即用) 1 /},并且非常常用)。Spring Security的ACL功能经过精心设计,可提供ACL的高性能检索,以及可插拔缓存,最小化数据库更新死锁,独立于ORM框架(我们直接使用JDBC),正确封装和透明数据库更新。
鉴于数据库是ACL模块操作的核心,让我们探讨在实现中默认使用的四个主表。下表按典型Spring Security ACL部署中的大小顺序列出,最后列出的行数最多:
GrantedAuthority
的标志。因此,每个唯一主体或GrantedAuthority
都有一行。当在接收许可的上下文中使用时,SID通常被称为“接收者”。
如上一段所述,ACL系统使用整数位屏蔽。不用担心,你不需要知道使用ACL系统的位移的更好点,但足以说我们有32位可以打开或关闭。这些位中的每一个都代表一个权限,默认情况下,读取权限(位0),写入(位1),创建(位2),删除(位3)和管理(位4)。如果您希望使用其他权限,则很容易实现您自己的Permission
实例,并且ACL框架的其余部分将在不知道您的扩展的情况下运行。
重要的是要理解系统中域对象的数量与我们选择使用整数位屏蔽的事实完全没有关系。虽然您有32位可用于权限,但您可能拥有数十亿个域对象实例(这意味着ACL_OBJECT_IDENTITY中的数十亿行,很可能是ACL_ENTRY)。我们提出这一点是因为我们发现有时人们错误地认为他们需要为每个潜在的域对象提供一点点,但事实并非如此。
现在我们已经提供了ACL系统的基本概述,以及它在表结构中的样子,让我们来探索关键接口。关键接口是:
Acl
:每个域对象都有一个且只有一个Acl
对象,它在内部保存AccessControlEntry
并且知道Acl
的所有者。Acl不直接引用域对象,而是引用ObjectIdentity
。Acl
存储在ACL_OBJECT_IDENTITY表中。
AccessControlEntry
:Acl
拥有多个AccessControlEntry
,在框架中通常缩写为ACE。每个ACE指的是Permission
,Sid
和Acl
的特定元组。ACE还可以授予或不授予并包含审核设置。ACE存储在ACL_ENTRY表中。
Permission
:权限表示特定的不可变位掩码,并为位屏蔽和输出信息提供便利功能。上面给出的基本权限(位0到4)包含在BasePermission
类中。
Sid
:ACL模块需要引用主体和GrantedAuthority[]
。Sid
接口提供了间接级别,它是“安全标识”的缩写。常用类包括PrincipalSid
(代表Authentication
对象内的主体)和GrantedAuthoritySid
。安全标识信息存储在ACL_SID表中。
ObjectIdentity
:每个域对象在ACL模块内部由ObjectIdentity
表示。默认实现称为ObjectIdentityImpl
。
AclService
:检索适用于给定ObjectIdentity
的Acl
。在包含的实现(JdbcAclService
)中,检索操作被委托给LookupStrategy
。LookupStrategy
提供了一种高度优化的策略,用于检索ACL信息,使用批量检索(BasicLookupStrategy
)并支持利用物化视图,分层查询和类似的以性能为中心的非ANSI SQL功能的自定义实现。
MutableAclService
:允许修改后的Acl
表示持久性。如果您不希望,则不必使用此界面。
请注意,我们开箱即用的AclService和相关数据库类都使用ANSI SQL。因此,这应该适用于所有主要数据库。在撰写本文时,系统已使用Hypersonic SQL,PostgreSQL,Microsoft SQL Server和Oracle成功进行了测试。
两个样本附带Spring Security,用于演示ACL模块。第一个是Contacts Sample,另一个是Document Management System(DMS)Sample。我们建议您查看这些示例。
要开始使用Spring Security的ACL功能,您需要将ACL信息存储在某处。这需要使用Spring实例化DataSource
。然后将DataSource
注入JdbcMutableAclService
和BasicLookupStrategy
实例。后者提供高性能的ACL检索功能,前者提供了mutator功能。有关示例配置,请参阅Spring Security附带的其中一个示例。您还需要使用上一节中列出的四个特定于ACL的表填充数据库(请参阅相应SQL语句的ACL示例)。
一旦您创建了所需的模式并实例化JdbcMutableAclService
,您接下来需要确保您的域模型支持与Spring Security ACL包的互操作性。希望ObjectIdentityImpl
证明是足够的,因为它提供了许多可以使用它的方法。大多数人都会拥有包含public Serializable getId()
方法的域对象。如果返回类型很长或与long兼容(例如int),您将发现无需进一步考虑ObjectIdentity
问题。ACL模块的许多部分都依赖于长标识符。如果你没有使用long(或int,byte等),你很有可能需要重新实现一些类。我们不打算在Spring Security的ACL模块中支持非长标识符,因为long已经与所有数据库序列(最常见的标识符数据类型)兼容,并且足够长以适应所有常见的使用场景。
以下代码片段显示了如何创建Acl
或修改现有的Acl
:
// Prepare the information we'd like in our access control entry (ACE) ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44)); Sid sid = new PrincipalSid("Samantha"); Permission p = BasePermission.ADMINISTRATION; // Create or update the relevant ACL MutableAcl acl = null; try { acl = (MutableAcl) aclService.readAclById(oi); } catch (NotFoundException nfe) { acl = aclService.createAcl(oi); } // Now grant some permissions via an access control entry (ACE) acl.insertAce(acl.getEntries().length, p, sid, true); aclService.updateAcl(acl);
在上面的示例中,我们检索与标识号为44的“Foo”域对象关联的ACL。然后我们添加一个ACE,以便名为“Samantha”的主体可以“管理”该对象。除了insertAce方法之外,代码片段相对不言自明。insertAce方法的第一个参数是确定将在Acl中的哪个位置插入新条目。在上面的示例中,我们只是将新ACE放在现有ACE的末尾。最后一个参数是一个布尔值,表示ACE是授予还是拒绝。大部分时间它将授予(true),但如果它拒绝(false),则权限被有效阻止。
Spring Security没有提供任何特殊集成来自动创建,更新或删除ACL作为DAO或存储库操作的一部分。相反,您需要为您的各个域对象编写如上所示的代码。值得考虑在服务层使用AOP来自动将ACL信息与服务层操作集成。我们在过去发现了这种非常有效的方法。
一旦您使用上述技术在数据库中存储一些ACL信息,下一步就是实际使用ACL信息作为授权决策逻辑的一部分。你在这里有很多选择。您可以编写自己的AccessDecisionVoter
或AfterInvocationProvider
,分别在方法调用之前或之后触发。这些类将使用AclService
来检索相关的ACL,然后调用Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode)
来决定是授予还是拒绝许可。或者,您可以使用我们的AclEntryVoter
,AclEntryAfterInvocationProvider
或AclEntryAfterInvocationCollectionFilteringProvider
类。所有这些类都提供了一种基于声明的方法,用于在运行时评估ACL信息,从而使您无需编写任何代码。请参阅示例应用程序以了解如何使用这些类。
在某些情况下,您希望使用Spring Security进行授权,但在访问应用程序之前,某些外部系统已经对用户进行了可靠的身份验证。我们将这些情况称为“预先验证”的情景。示例包括X.509,Siteminder以及运行应用程序的Java EE容器的身份验证。使用预身份验证时,Spring Security必须使用
细节将取决于外部认证机制。在X.509的情况下,可以通过其证书信息来标识用户,或者在Siteminder的情况下通过HTTP请求标头来标识用户。如果依赖于容器身份验证,则将通过在传入HTTP请求上调用getUserPrincipal()
方法来标识用户。在某些情况下,外部机制可以为用户提供角色/权限信息,但在其他情况下,必须从单独的源(例如UserDetailsService
)获取权限。
由于大多数预身份验证机制遵循相同的模式,Spring Security具有一组类,这些类为实现预先验证的身份验证提供程序提供了内部框架。这消除了重复,并允许以结构化方式添加新实现,而无需从头开始编写所有内容。如果您想使用X.509身份验证之类的东西,则无需了解这些类,因为它已经具有更易于使用和开始使用的命名空间配置选项。如果您需要使用显式bean配置或计划编写自己的实现,那么了解所提供的实现如何工作将是有用的。您将在org.springframework.security.web.authentication.preauth
下找到课程。我们只是在这里提供一个大纲,所以你应该在适当的时候咨询Javadoc和来源。
此类将检查安全上下文的当前内容,如果为空,它将尝试从HTTP请求中提取用户信息并将其提交到AuthenticationManager
。子类重写以下方法以获取此信息:
protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request); protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);
调用这些后,过滤器将创建一个包含返回数据的PreAuthenticatedAuthenticationToken
并提交以进行身份验证。通过这里的“身份验证”,我们实际上只是意味着可能会加载用户权限的进一步处理,但遵循标准的Spring Security身份验证体系结构。
与其他Spring Security身份验证过滤器一样,预身份验证过滤器具有authenticationDetailsSource
属性,默认情况下会创建一个WebAuthenticationDetails
对象来存储其他信息,例如{4中的会话标识符和原始IP地址/ Authentication
对象的属性。在可以从预认证机制获得用户角色信息的情况下,数据也存储在该属性中,其中详细信息实现了GrantedAuthoritiesContainer
接口。这使身份验证提供程序能够读取外部分配给用户的权限。接下来我们将看一个具体的例子。
如果过滤器配置了authenticationDetailsSource
(此类的实例),则通过为每个预定义的“可映射角色”调用isUserInRole(String role)
方法来获取权限信息。该类从配置的MappableAttributesRetriever
中获取这些内容。可能的实现包括在应用程序上下文中对列表进行硬编码,以及从web.xml
文件中的<security-role>
信息中读取角色信息。预认证示例应用程序使用后一种方法。
还有一个额外阶段,使用配置的Attributes2GrantedAuthoritiesMapper
将角色(或属性)映射到Spring Security GrantedAuthority
对象。默认情况下,只会在名称中添加通常的ROLE_
前缀,但它可以让您完全控制行为。
预先认证的提供程序除了为用户加载UserDetails
对象之外,还有更多工作要做。它通过委托给AuthenticationUserDetailsService
来做到这一点。后者类似于标准UserDetailsService
,但是采用Authentication
对象而不仅仅是用户名:
public interface AuthenticationUserDetailsService { UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException; }
此接口可能还有其他用途,但通过预身份验证,它允许访问打包在Authentication
对象中的权限,正如我们在上一节中看到的那样。PreAuthenticatedGrantedAuthoritiesUserDetailsService
类就是这样做的。或者,它可以通过UserDetailsByNameServiceWrapper
实施委托给标准UserDetailsService
。
AuthenticationEntryPoint
在技术概述章节中进行了讨论。通常,它负责启动未经身份验证的用户的身份验证过程(当他们尝试访问受保护的资源时),但在预先验证的情况下,这不适用。如果您没有将预身份验证与其他身份验证机制结合使用,则只能使用此类的实例配置ExceptionTranslationFilter
。如果用户被AbstractPreAuthenticatedProcessingFilter
拒绝,则会调用它,从而导致空认证。如果被调用,它总是返回403
- 禁止的响应代码。
X.509身份验证在其自己的章节中介绍。在这里,我们将介绍一些为其他预先验证的方案提供支持的类。
外部认证系统可以通过在HTTP请求上设置特定报头来向应用程序提供信息。一个众所周知的例子是Siteminder,它在名为SM_USER
的标题中传递用户名。类RequestHeaderAuthenticationFilter
支持此机制,它只是从标头中提取用户名。它默认使用名称SM_USER
作为标题名称。有关更多详细信息,请参阅Javadoc。
![]() | 小费 |
---|---|
请注意,在使用这样的系统时,框架根本不执行任何身份验证检查,外部系统配置正确并保护对应用程序的所有访问权限非常重要。如果攻击者能够在未检测到原始请求的情况下伪造标头,那么他们可能会选择他们希望的任何用户名。 |
使用此过滤器的典型配置如下所示:
<security:http> <!-- Additional http configuration omitted --> <security:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" /> </security:http> <bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter"> <property name="principalRequestHeader" value="SM_USER"/> <property name="authenticationManager" ref="authenticationManager" /> </bean> <bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider"> <property name="preAuthenticatedUserDetailsService"> <bean id="userDetailsServiceWrapper" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"> <property name="userDetailsService" ref="userDetailsService"/> </bean> </property> </bean> <security:authentication-manager alias="authenticationManager"> <security:authentication-provider ref="preauthAuthProvider" /> </security:authentication-manager>
我们假设安全命名空间用于配置。还假设您已在配置中添加了UserDetailsService
(称为“userDetailsService”)以加载用户的角色。
类J2eePreAuthenticatedProcessingFilter
将从HttpServletRequest
的userPrincipal
属性中提取用户名。使用此过滤器通常与Java EE角色的使用相结合,如上文“J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource”一节中所述。
代码库中有一个使用这种方法的示例应用程序,因此如果您感兴趣,请从github获取代码并查看应用程序上下文文件。代码位于samples/xml/preauth
目录中。
组织经常将LDAP用作用户信息和身份验证服务的中央存储库。它还可用于存储应用程序用户的角色信息。
关于如何配置LDAP服务器有许多不同的方案,因此Spring Security的LDAP提供程序是完全可配置的。它使用单独的策略接口进行身份验证和角色检索,并提供可配置为处理各种情况的默认实现。
在尝试将其与Spring Security一起使用之前,您应该熟悉LDAP。以下链接提供了对所涉及概念的良好介绍,以及使用免费LDAP服务器OpenLDAP设置目录的指南:http://www.zytrax.com/books/ldap/。熟悉用于从Java访问LDAP的JNDI API也可能很有用。我们不在LDAP提供程序中使用任何第三方LDAP库(Mozilla,JLDAP等),但是大量使用Spring LDAP,因此如果您计划添加您的项目,那么熟悉该项目可能会很有用自己的自定义。
使用LDAP身份验证时,务必确保正确配置LDAP连接池。如果您不熟悉如何执行此操作,可以参考Java LDAP文档。
Spring Security中的LDAP身份验证大致可分为以下几个阶段。
uid=joe,ou=users,dc=spring,dc=io
。
例外情况是,LDAP目录仅用于检索用户信息并在本地对其进行身份验证。这可能是不可能的,因为目录通常设置为对用户密码等属性的读访问权限有限。
我们将在下面介绍一些配置方案。有关可用配置选项的完整信息,请参阅安全命名空间架构(XML编辑器中应提供的信息)。
您需要做的第一件事是配置应该进行身份验证的服务器。这是使用安全命名空间中的<ldap-server>
元素完成的。可以使用url
属性将其配置为指向外部LDAP服务器:
<ldap-server url="ldap://springframework.org:389/dc=springframework,dc=org" />
<ldap-server>
元素也可用于创建嵌入式服务器,这对测试和演示非常有用。在这种情况下,您使用它而不使用url
属性:
<ldap-server root="dc=springframework,dc=org"/>
这里我们已经指定目录的根DIT应该是“dc = springframework,dc = org”,这是默认值。使用这种方式,命名空间解析器将创建一个嵌入式Apache Directory服务器,并扫描类路径中的任何LDIF文件,它将尝试加载到服务器中。您可以使用ldif
属性自定义此行为,该属性定义要加载的LDIF资源:
<ldap-server ldif="classpath:users.ldif" />
这使得启动和运行LDAP变得更加容易,因为使用外部服务器一直工作会很不方便。它还使用户免受连接Apache Directory服务器所需的复杂bean配置的影响。使用普通Spring Bean,配置会更加混乱。您必须具有可供应用程序使用的必需Apache Directory依赖项jar。这些可以从LDAP示例应用程序获得。
这是最常见的LDAP身份验证方案。
<ldap-authentication-provider user-dn-pattern="uid={0},ou=people"/>
这个简单的示例将通过替换提供的模式中的用户登录名并尝试使用登录密码绑定该用户来获取用户的DN。如果所有用户都存储在目录中的单个节点下,则可以。如果您希望配置LDAP搜索过滤器以找到用户,则可以使用以下内容:
<ldap-authentication-provider user-search-filter="(uid={0})" user-search-base="ou=people"/>
如果与上面的服务器定义一起使用,这将使用user-search-filter
属性的值作为过滤器在DN ou=people,dc=springframework,dc=org
下执行搜索。同样,用户登录名将替换过滤器名称中的参数,因此它将搜索uid
属性等于用户名的条目。如果未提供user-search-base
,则将从根执行搜索。
如何从LDAP目录中的组加载权限由以下属性控制。
group-search-base
.定义应在其下执行组搜索的目录树的一部分。
group-role-attribute
.包含组条目定义的权限名称的属性。默认为cn
group-search-filter
.用于搜索组成员身份的过滤器。默认值为uniqueMember={0}
,对应于groupOfUniqueNames
LDAP类[19]。在这种情况下,替换参数是用户的完整可分辨名称。如果要筛选登录名,可以使用参数{1}
。
所以如果我们使用以下配置
<ldap-authentication-provider user-dn-pattern="uid={0},ou=people" group-search-base="ou=groups" />
并且成功验证为用户“ben”,随后的权限加载将在目录条目ou=groups,dc=springframework,dc=org
下执行搜索,查找包含值为uid=ben,ou=people,dc=springframework,dc=org
的属性uniqueMember
的条目。默认情况下,权限名称前缀为ROLE_
。您可以使用role-prefix
属性更改此设置。如果您不想要任何前缀,请使用role-prefix="none"
。有关加载权限的更多信息,请参阅DefaultLdapAuthoritiesPopulator
类的Javadoc。
我们上面使用的命名空间配置选项易于使用,并且比明确使用Spring bean更简洁。在某些情况下,您可能需要知道如何在应用程序上下文中直接配置Spring Security LDAP。例如,您可能希望自定义某些类的行为。如果您对使用命名空间配置感到满意,那么您可以跳过本节和下一节。
主要的LDAP提供程序类LdapAuthenticationProvider
实际上并没有做太多的事情,而是将工作委托给另外两个bean,LdapAuthenticator
和LdapAuthoritiesPopulator
,它们负责验证用户身份并检索用户的集合分别为GrantedAuthority
s。
验证者还负责检索任何所需的用户属性。这是因为属性的权限可能取决于所使用的身份验证的类型。例如,如果以用户身份进行绑定,则可能需要使用用户自己的权限来读取它们。
目前有Spring Security提供的两种身份验证策略:
在可以对用户进行身份验证之前(通过任一策略),必须从提供给应用程序的登录名获取可分辨名称(DN)。这可以通过简单的模式匹配(通过设置setUserDnPatterns
数组属性)或通过设置userSearch
属性来完成。对于DN模式匹配方法,使用标准Java模式格式,并且登录名将替换参数{0}
。该模式应该相对于配置的SpringSecurityContextSource
将绑定到的DN(有关此信息的更多信息,请参阅有关连接到LDAP服务器的部分)。例如,如果您使用的URL服务器的URL为ldap://monkeymachine.co.uk/dc=springframework,dc=org
,模式为uid={0},ou=greatapes
,那么登录名“gorilla”将映射到DN uid=gorilla,ou=greatapes,dc=springframework,dc=org
。将依次尝试每个配置的DN模式,直到找到匹配项。有关使用搜索的信息,请参阅下面的搜索对象部分。也可以使用这两种方法的组合 - 首先检查模式,如果没有找到匹配的DN,将使用搜索。
上面讨论的bean必须能够连接到服务器。它们都必须提供SpringSecurityContextSource
,这是Spring LDAP的ContextSource
的扩展。除非您有特殊要求,否则通常会配置一个DefaultSpringSecurityContextSource
bean,可以使用LDAP服务器的URL配置,也可以配置“manager”用户的用户名和密码,默认情况下绑定到服务器(而不是匿名绑定)。有关更多信息,请阅读此类的Javadoc和Spring LDAP的AbstractContextSource
。
通常需要比简单DN匹配更复杂的策略来在目录中定位用户条目。这可以封装在LdapUserSearch
实例中,该实例可以提供给验证器实现,例如,允许它们定位用户。提供的实现是FilterBasedLdapUserSearch
。
此bean使用LDAP过滤器来匹配目录中的用户对象。在Javadoc中解释了JDK DirContext类上相应搜索方法的过程。如那里所解释的,可以向搜索过滤器提供参数。对于这个类,唯一有效的参数是{0}
,它将被用户的登录名替换。
在成功验证用户之后,LdapAuthenticationProvider
将尝试通过调用配置的LdapAuthoritiesPopulator
bean为用户加载一组权限。DefaultLdapAuthoritiesPopulator
是一个实现,它将通过在目录中搜索用户所属的组来加载权限(通常这些将是目录中的groupOfNames
或groupOfUniqueNames
条目)。有关其工作原理的更多详细信息,请参阅此类的Javadoc。
如果您只想使用LDAP进行身份验证,但是从差异源(例如数据库)加载权限,那么您可以提供自己的此接口实现并注入该接口。
使用我们在这里讨论的一些bean的典型配置可能如下所示:
<bean id="contextSource" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource"> <constructor-arg value="ldap://monkeymachine:389/dc=springframework,dc=org"/> <property name="userDn" value="cn=manager,dc=springframework,dc=org"/> <property name="password" value="password"/> </bean> <bean id="ldapAuthProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider"> <constructor-arg> <bean class="org.springframework.security.ldap.authentication.BindAuthenticator"> <constructor-arg ref="contextSource"/> <property name="userDnPatterns"> <list><value>uid={0},ou=people</value></list> </property> </bean> </constructor-arg> <constructor-arg> <bean class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator"> <constructor-arg ref="contextSource"/> <constructor-arg value="ou=groups"/> <property name="groupRoleAttribute" value="ou"/> </bean> </constructor-arg> </bean>
这将设置提供程序以访问URL为ldap://monkeymachine:389/dc=springframework,dc=org
的LDAP服务器。将通过尝试绑定DN uid=<user-login-name>,ou=people,dc=springframework,dc=org
来执行身份验证。身份验证成功后,将通过使用默认过滤器(member=<user’s-DN>)
在DN ou=groups,dc=springframework,dc=org
下搜索,将角色分配给用户。角色名称将取自每场比赛的“ou”属性。
要配置用户搜索对象,使用过滤器(uid=<user-login-name>)
代替DN模式(或除此之外),您将配置以下bean
<bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch"> <constructor-arg index="0" value=""/> <constructor-arg index="1" value="(uid={0})"/> <constructor-arg index="2" ref="contextSource" /> </bean>
并通过设置BindAuthenticator
bean的userSearch
属性来使用它。然后,在尝试以此用户身份绑定之前,身份验证者将调用搜索对象以获取正确的用户DN。
使用LdapAuthenticationProvider
进行身份验证的最终结果与使用标准UserDetailsService
接口的正常Spring Security身份验证相同。创建UserDetails
对象并将其存储在返回的Authentication
对象中。与使用UserDetailsService
一样,常见的要求是能够自定义此实现并添加额外的属性。使用LDAP时,这些通常是来自用户条目的属性。UserDetails
对象的创建由提供者的UserDetailsContextMapper
策略控制,该策略负责将用户对象映射到LDAP上下文数据和从LDAP上下文数据映射:
public interface UserDetailsContextMapper { UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<GrantedAuthority> authorities); void mapUserToContext(UserDetails user, DirContextAdapter ctx); }
只有第一种方法与身份验证相关。如果您提供此接口的实现并将其注入LdapAuthenticationProvider
,则可以精确控制UserDetails对象的创建方式。第一个参数是Spring LDAP的DirContextOperations
实例,它允许您访问在身份验证期间加载的LDAP属性。username
参数是用于进行身份验证的名称,最后一个参数是由配置的LdapAuthoritiesPopulator
为用户加载的权限集合。
加载上下文数据的方式略有不同,具体取决于您使用的身份验证类型。使用BindAuthenticator
,绑定操作返回的上下文将用于读取属性,否则将使用从配置的ContextSource
获取的标准上下文读取数据(当搜索配置为定位用户时) ,这将是搜索对象返回的数据)。
Active Directory支持自己的非标准身份验证选项,并且正常使用模式与标准LdapAuthenticationProvider
不太合适。通常,使用域用户名(格式为user@domain
)执行身份验证,而不是使用LDAP专有名称。为了简化这一过程,Spring Security 3.1有一个身份验证提供程序,可以为典型的Active Directory设置进行自定义。
配置ActiveDirectoryLdapAuthenticationProvider
非常简单。您只需提供域名和提供服务器地址的LDAP URL [20]。然后,示例配置如下所示:
<bean id="adAuthenticationProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider"> <constructor-arg value="mydomain.com" /> <constructor-arg value="ldap://adserver.mydomain.com/" /> </bean> }
请注意,为了定义服务器位置,不需要指定单独的ContextSource
- bean完全是自包含的。例如,名为“Sharon”的用户可以通过输入用户名sharon
或完整的Active Directory userPrincipalName
(即sharon@mydomain.com
)进行身份验证。然后将定位用户的目录条目,并返回属性以用于自定义创建的UserDetails
对象(可以为此目的注入UserDetailsContextMapper
,如上所述)。与目录的所有交互都以用户自己的身份进行。没有“经理”用户的概念。
默认情况下,用户权限是从用户条目的memberOf
属性值获取的。分配给用户的权限可以再次使用UserDetailsContextMapper
进行自定义。您还可以将GrantedAuthoritiesMapper
注入提供程序实例以控制最终位于Authentication
对象中的权限。
HttpSecurity.oauth2Login()
提供了许多用于自定义OAuth 2.0登录的配置选项。主要配置选项分组到其协议端点对应项中。
例如,oauth2Login().authorizationEndpoint()
允许配置授权端点,而oauth2Login().tokenEndpoint()
允许配置令牌端点。
以下代码显示了一个示例:
@EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .authorizationEndpoint() ... .redirectionEndpoint() ... .tokenEndpoint() ... .userInfoEndpoint() ... } }
oauth2Login()
DSL的主要目标是与规范中定义的命名密切配合。
OAuth 2.0授权框架定义协议端点,如下所示:
授权过程使用两个授权服务器端点(HTTP资源):
以及一个客户端端点:
OpenID Connect Core 1.0规范定义了UserInfo端点,如下所示:
UserInfo端点是OAuth 2.0受保护资源,它返回有关经过身份验证的最终用户的声明。为了获得有关最终用户的请求声明,客户端使用通过OpenID Connect身份验证获得的访问令牌向UserInfo端点发出请求。这些声明通常由JSON对象表示,该对象包含声明的名称 - 值对的集合。
以下代码显示了oauth2Login()
DSL可用的完整配置选项:
@EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .clientRegistrationRepository(this.clientRegistrationRepository()) .authorizedClientRepository(this.authorizedClientRepository()) .authorizedClientService(this.authorizedClientService()) .loginPage("/login") .authorizationEndpoint() .baseUri(this.authorizationRequestBaseUri()) .authorizationRequestRepository(this.authorizationRequestRepository()) .authorizationRequestResolver(this.authorizationRequestResolver()) .and() .redirectionEndpoint() .baseUri(this.authorizationResponseBaseUri()) .and() .tokenEndpoint() .accessTokenResponseClient(this.accessTokenResponseClient()) .and() .userInfoEndpoint() .userAuthoritiesMapper(this.userAuthoritiesMapper()) .userService(this.oauth2UserService()) .oidcUserService(this.oidcUserService()) .customUserType(GitHubOAuth2User.class, "github"); } }
以下部分详细介绍了每种可用的配置选项:
默认情况下,OAuth 2.0登录页面由DefaultLoginPageGeneratingFilter
自动生成。默认登录页面显示每个已配置的OAuth客户端及其ClientRegistration.clientName
作为链接,该链接能够启动授权请求(或OAuth 2.0登录)。
每个OAuth客户端的链接目标默认为以下内容:
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI
+“/ {registrationId}”
以下行显示了一个示例:
<a href="/oauth2/authorization/google">Google</a>
要覆盖默认登录页面,请配置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") .... } }
![]() | 重要 |
---|---|
您需要提供一个 |
![]() | 小费 |
---|---|
如前所述,配置 以下行显示了一个示例: <a href="/login/oauth2/authorization/google">Google</a> |
授权服务器使用重定向端点通过资源所有者用户代理将授权响应(包含授权凭据)返回给客户端。
![]() | 小费 |
---|---|
OAuth 2.0 Login利用授权代码授权。因此,授权凭证是授权代码。 |
默认授权响应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/*") .... } }
![]() | 重要 |
---|---|
您还需要确保 以下清单显示了一个示例: return CommonOAuth2Provider.GOOGLE.getBuilder("google") .clientId("google-client-id") .clientSecret("google-client-secret") .redirectUriTemplate("{baseUrl}/login/oauth2/callback/{registrationId}") .build(); |
UserInfo端点包括许多配置选项,如以下子部分所述:
用户成功通过OAuth 2.0提供程序进行身份验证后,OAuth2User.getAuthorities()
(或OidcUser.getAuthorities()
)可能会映射到一组新的GrantedAuthority
实例,这些实例将在完成身份验证时提供给OAuth2AuthenticationToken
。
![]() | 小费 |
---|---|
|
映射用户权限时,有几个选项可供选择:
提供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<GrantedAuthority> mappedAuthorities = new HashSet<>(); authorities.forEach(authority -> { if (OidcUserAuthority.class.isInstance(authority)) { OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority; OidcIdToken idToken = oidcUserAuthority.getIdToken(); OidcUserInfo userInfo = oidcUserAuthority.getUserInfo(); // Map the claims found in idToken and/or userInfo // to one or more GrantedAuthority's and add it to mappedAuthorities } else if (OAuth2UserAuthority.class.isInstance(authority)) { OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority; Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes(); // Map the attributes found in userAttributes // to one or more GrantedAuthority's and add it to mappedAuthorities } }); return mappedAuthorities; }; } }
或者,您可以注册GrantedAuthoritiesMapper
@Bean
以使其自动应用于配置,如以下示例所示:
@EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @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
的访问权限,这在委托人需要从受保护资源获取权限信息才能映射自定义权限的情况下非常有用。用户。
以下示例显示如何使用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<OidcUserRequest, OidcUser> oidcUserService() { 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<GrantedAuthority> mappedAuthorities = new HashSet<>(); // TODO // 1) Fetch the authority information from the protected resource using accessToken // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities // 3) Create a copy of oidcUser but use the mappedAuthorities 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<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); private Map<String, Object> attributes; private String id; private String name; private String login; private String email; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public Map<String, Object> getAttributes() { if (this.attributes == null) { this.attributes = new HashMap<>(); this.attributes.put("id", this.getId()); this.attributes.put("name", this.getName()); this.attributes.put("login", this.getLogin()); this.attributes.put("email", this.getEmail()); } return attributes; } public String getId() { return this.id; } public void setId(String id) { this.id = id; } @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; } }
![]() | 小费 |
---|---|
|
DefaultOAuth2UserService
是支持标准OAuth 2.0提供程序的OAuth2UserService
的实现。
![]() | 注意 |
---|---|
|
在UserInfo端点请求用户属性时,DefaultOAuth2UserService
使用RestOperations
。
如果您需要自定义UserInfo请求的预处理,则可以为DefaultOAuth2UserService.setRequestEntityConverter()
提供自定义Converter<OAuth2UserRequest, RequestEntity<?>>
。默认实现OAuth2UserRequestEntityConverter
构建UserInfo请求的RequestEntity
表示,默认情况下在Authorization
标头中设置OAuth2AccessToken
。
另一方面,如果您需要自定义UserInfo响应的后处理,则需要为DefaultOAuth2UserService.setRestOperations()
提供自定义配置的RestOperations
。默认RestOperations
配置如下:
RestTemplate restTemplate = new RestTemplate(); restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
OAuth2ErrorResponseErrorHandler
是ResponseErrorHandler
,可以处理OAuth 2.0错误(400错误请求)。它使用OAuth2ErrorHttpMessageConverter
将OAuth 2.0 Error参数转换为OAuth2Error
。
无论您是自定义DefaultOAuth2UserService
还是提供自己的OAuth2UserService
实现,都需要对其进行配置,如以下示例所示:
@EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .userInfoEndpoint() .userService(this.oauth2UserService()) ... } private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() { ... } }
OidcUserService
是支持OpenID Connect 1.0 Provider的OAuth2UserService
的实现。
在UserInfo端点请求用户属性时,OidcUserService
利用DefaultOAuth2UserService
。
如果您需要自定义UserInfo请求的预处理和/或UserInfo响应的后处理,则需要为OidcUserService.setOauth2UserService()
提供自定义配置的DefaultOAuth2UserService
。
无论您是自定义OidcUserService
还是为OpenID Connect 1.0 Provider提供自己的OAuth2UserService
实现,您都需要对其进行配置,如以下示例所示:
@EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .oauth2Login() .userInfoEndpoint() .oidcUserService(this.oidcUserService()) ... } private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() { ... } }
![]() | 注意 |
---|---|
以下文档适用于Servlet环境。对于所有其他环境,请参阅WebClient for Reactive环境。 |
Spring Framework已经内置支持设置Bearer令牌。
webClient.get() .headers(h -> h.setBearerAuth(token)) ...
Spring Security建立在这种支持的基础上,以提供额外的好处:
如果请求访问令牌但不存在,Spring Security将自动请求访问令牌。
第一步是确保正确设置WebClient
。可以在下面找到在servlet环境中设置WebClient
的示例:
@Bean WebClient webClient(ClientRegistrationRepository clientRegistrations, OAuth2AuthorizedClientRepository authorizedClients) { ServletOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients); // (optional) explicitly opt into using the oauth2Login to provide an access token implicitly // oauth.setDefaultOAuth2AuthorizedClient(true); // (optional) set a default ClientRegistration.registrationId // oauth.setDefaultClientRegistrationId("client-registration-id"); return WebClient.builder() .apply(oauth2.oauth2Configuration()) .build(); }
如果我们在设置中将defaultOAuth2AuthorizedClient
设置为true
并且使用oauth2Login(即OIDC)对用户进行身份验证,则使用当前身份验证自动提供访问令牌。或者,如果我们将defaultClientRegistrationId
设置为有效的ClientRegistration
id,则该注册用于提供访问令牌。这很方便,但在并非所有端点都应获取访问令牌的环境中,这很危险(您可能会向端点提供错误的访问令牌)。
Mono<String> body = this.webClient .get() .uri(this.uri) .retrieve() .bodyToMono(String.class);
可以通过在请求属性上设置OAuth2AuthorizedClient
来明确提供。在下面的示例中,我们使用Spring WebFlux或Spring MVC参数解析器支持来解析OAuth2AuthorizedClient
。但是,OAuth2AuthorizedClient
如何解决并不重要。
@GetMapping("/explicit") Mono<String> explicit(@RegisteredOAuth2AuthorizedClient("client-id") OAuth2AuthorizedClient authorizedClient) { return this.webClient .get() .uri(this.uri) .attributes(oauth2AuthorizedClient(authorizedClient)) .retrieve() .bodyToMono(String.class); }
或者,可以在请求属性上指定clientRegistrationId
,WebClient
将尝试查找OAuth2AuthorizedClient
。如果未找到,将自动获取一个。
Mono<String> body = this.webClient .get() .uri(this.uri) .attributes(clientRegistrationId("client-id")) .retrieve() .bodyToMono(String.class);
Spring Security有自己的taglib,它为访问安全信息和在JSP中应用安全约束提供基本支持。
要使用任何标记,必须在JSP中声明安全性标记库:
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
此标记用于确定是否应评估其内容。在Spring Security 3.0中,它可以以两种方式使用[21]。第一种方法使用web - 安全表达式,在标记的access
属性中指定。表达式评估将委托给应用程序上下文中定义的SecurityExpressionHandler<FilterInvocation>
(您应该在<http>
命名空间配置中启用web表达式以确保此服务可用)。所以,例如,你可能有
<sec:authorize access="hasRole('supervisor')"> This content will only be visible to users who have the "supervisor" authority in their list of <tt>GrantedAuthority</tt>s. </sec:authorize>
与Spring Security的PermissionEvaluator结合使用时,该标记也可用于检查权限。例如:
<sec:authorize access="hasPermission(#domain,'read') or hasPermission(#domain,'write')"> This content will only be visible to users who have read or write permission to the Object found as a request attribute named "domain". </sec:authorize>
如果实际允许用户单击该链接,则通常的要求是仅显示特定链接。我们如何提前确定是否允许某些事情?此标记还可以在备用模式下运行,该模式允许您将特定URL定义为属性。如果允许用户调用该URL,则将评估标记正文,否则将跳过该标记正文。所以你可能有类似的东西
<sec:authorize url="/admin"> This content will only be visible to users who are authorized to send requests to the "/admin" URL. </sec:authorize>
要使用此标记,您的应用程序上下文中还必须有一个WebInvocationPrivilegeEvaluator
的实例。如果您使用命名空间,将自动注册。这是DefaultWebInvocationPrivilegeEvaluator
的一个实例,它为提供的URL创建一个虚拟web请求,并调用安全拦截器以查看请求是成功还是失败。这允许您委托使用<http>
命名空间配置中的intercept-url
声明定义的访问控制设置,并节省必须复制JSP中的信息(例如所需角色)。此方法还可以与method
属性结合使用,提供HTTP方法,以实现更具体的匹配。
通过将var
属性设置为变量名称,可以将评估标记的布尔结果(无论是授予还是拒绝访问)存储在页面上下文范围变量中,从而无需复制和重新评估其他条件。页面中的点。
在未经授权的用户的页面中隐藏链接并不会阻止他们访问该URL。例如,他们可以直接在浏览器中输入它。作为测试过程的一部分,您可能希望揭示隐藏区域,以便检查链接是否真的在后端得到保护。如果将系统属性spring.security.disableUISecurity
设置为true
,则authorize
标记仍将运行,但不会隐藏其内容。默认情况下,它还会使用<span class="securityHiddenUI">…</span>
标记包围内容。这允许您显示具有特定CSS样式的“隐藏”内容,例如不同的背景颜色。例如,尝试在启用此属性的情况下运行“tutorial”示例应用程序。
您还可以设置属性spring.security.securedUIPrefix
和spring.security.securedUISuffix
如果要更改默认span
标签周围文本(或使用空字符串来彻底删除)。
此标记允许访问存储在安全上下文中的当前Authentication
对象。它直接在JSP中呈现对象的属性。因此,例如,如果Authentication
的principal
属性是Spring Security的UserDetails
对象的实例,则使用<sec:authentication property="principal.username" />
将呈现当前用户的名称。
当然,没有必要为这种事情使用JSP标记,有些人更喜欢在视图中保持尽可能少的逻辑。您可以访问MVC控制器中的Authentication
对象(通过调用SecurityContextHolder.getContext().getAuthentication()
)并将数据直接添加到模型中以供视图呈现。
此标记仅在与Spring Security的ACL模块一起使用时有效。它检查指定域对象的逗号分隔的所需权限列表。如果当前用户具有所有这些权限,则将评估标记正文。如果他们不这样做,它将被跳过。一个例子可能是
![]() | 警告 |
---|---|
通常,此标记应被视为已弃用。而是使用第13.5.2节“授权标签”。 |
<sec:accesscontrollist hasPermission="1,2" domainObject="${someObject}"> This will be shown if the user has all of the permissions represented by the values "1" or "2" on the given object. </sec:accesscontrollist>
权限被传递给应用程序上下文中定义的PermissionFactory
,将它们转换为ACL Permission
实例,因此它们可以是工厂支持的任何格式 - 它们不必是整数,它们可以是READ
或WRITE
之类的字符串。如果未找到PermissionFactory
,将使用DefaultPermissionFactory
的实例。应用程序上下文中的AclService
将用于加载所提供对象的Acl
实例。将使用所需权限调用Acl
以检查是否已授予所有权限。
此标记还支持var
属性,与authorize
标记的方式相同。
如果启用了CSRF保护,则此标记将插入一个隐藏的表单字段,其中包含CSRF保护令牌的正确名称和值。如果未启用CSRF保护,则此标记不会输出任何内容。
通常Spring Security会自动为您使用的任何<form:form>
标签插入CSRF表单字段,但如果由于某种原因您无法使用<form:form>
,则csrfInput
是一个方便的替代品。
您应该将此标记放在HTML <form></form>
块中,您通常会在其中放置其他输入字段。请勿将此标记放在Spring <form:form></form:form>
块中。Spring Security自动处理Spring表格。
<form method="post" action="/do/something"> <sec:csrfInput /> Name:<br /> <input type="text" name="name" /> ... </form>
如果启用了CSRF保护,则此标记将插入包含CSRF保护令牌表单字段和标头名称以及CSRF保护令牌值的元标记。这些元标记对于在应用程序中的JavaScript中使用CSRF保护非常有用。
您应该将csrfMetaTags
放在HTML <head></head>
块中,您通常会放置其他元标记。使用此标记后,您可以使用JavaScript轻松访问表单字段名称,标题名称和标记值。在此示例中使用JQuery来简化任务。
<!DOCTYPE html> <html> <head> <title>CSRF Protected JavaScript Page</title> <meta name="description" content="This is the description for this page" /> <sec:csrfMetaTags /> <script type="text/javascript" language="javascript"> var csrfParameter = $("meta[name='_csrf_parameter']").attr("content"); var csrfHeader = $("meta[name='_csrf_header']").attr("content"); var csrfToken = $("meta[name='_csrf']").attr("content"); // using XMLHttpRequest directly to send an x-www-form-urlencoded request var ajax = new XMLHttpRequest(); ajax.open("POST", "http://www.example.org/do/something", true); ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded data"); ajax.send(csrfParameter + "=" + csrfToken + "&name=John&..."); // using XMLHttpRequest directly to send a non-x-www-form-urlencoded request var ajax = new XMLHttpRequest(); ajax.open("POST", "http://www.example.org/do/something", true); ajax.setRequestHeader(csrfHeader, csrfToken); ajax.send("..."); // using JQuery to send an x-www-form-urlencoded request var data = {}; data[csrfParameter] = csrfToken; data["name"] = "John"; ... $.ajax({ url: "http://www.example.org/do/something", type: "POST", data: data, ... }); // using JQuery to send a non-x-www-form-urlencoded request var headers = {}; headers[csrfHeader] = csrfToken; $.ajax({ url: "http://www.example.org/do/something", type: "POST", headers: headers, ... }); <script> </head> <body> ... </body> </html>
如果未启用CSRF保护,则csrfMetaTags
不输出任何内容。
AbstractJaasAuthenticationProvider
是所提供的JAAS AuthenticationProvider
实现的基础。子类必须实现一个创建LoginContext
的方法。AbstractJaasAuthenticationProvider
有许多可以注入其中的依赖项,将在下面讨论。
大多数JAAS LoginModule
需要某种回调。这些回调通常用于从用户获取用户名和密码。
在Spring Security部署中,Spring Security负责此用户交互(通过身份验证机制)。因此,在将身份验证请求委托给JAAS时,Spring Security的身份验证机制已经完全填充了一个Authentication
对象,其中包含JAAS LoginModule
所需的所有信息。
因此,Spring Security的JAAS包提供了两个默认的回调处理程序JaasNameCallbackHandler
和JaasPasswordCallbackHandler
。这些回调处理程序中的每一个都实现JaasAuthenticationCallbackHandler
。在大多数情况下,这些回调处理程序可以在不了解内部机制的情况下使用。
对于那些需要完全控制回调行为的人,内部AbstractJaasAuthenticationProvider
用InternalCallback