快人一步
VMware 提供培训和认证,助您加速进步。
了解更多我代表社区,很高兴宣布 Spring Security 5.0.0 M4 发布。此版本包含错误修复、新功能,并基于 Spring Framework 5.0.0 RC4。您可以在变更日志中找到完整详情。此版本的亮点包括
对 HttpSecurity.oauth2Login()
DSL 进行了多项改进。
您现在可以使用 AuthorizationGrantTokenExchanger
或 SecurityTokenRepository<AccessToken>
的自定义实现来配置令牌端点,如下所示
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.tokenEndpoint()
.authorizationCodeTokenExchanger(this.authorizationCodeTokenExchanger())
.accessTokenRepository(this.accessTokenRepository());
}
我们还添加了自定义授权端点和重定向端点请求路径的功能
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.authorizationEndpoint()
.requestMatcher(new AntPathRequestMatcher("/custom-path/{clientAlias}"))
.and()
.redirectionEndpoint()
.requestMatcher(new AntPathRequestMatcher("/custom-path/callback/{clientAlias}"));
}
与 Spring Security 中的所有 AbstractAuthenticationProcessingFilter
一样,您还可以设置自定义的 AuthenticationSuccessHandler
和 AuthenticationFailureHandler
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.successHandler(this.customAuthenticationSuccessHandler())
.failureHandler(this.customAuthenticationFailureHandler());
}
我们引入了 SecurityTokenRepository<T extends SecurityToken>
抽象,它负责 SecurityToken
的持久化。
初步实现 InMemoryAccessTokenRepository
提供了 AccessToken
的持久化。在即将发布的版本中,我们还将提供一个支持刷新令牌持久化的实现。
对 IdToken
进行了一些小的改进,并对 JwtClaimAccessor
、StandardClaimAccessor
和 IdTokenClaimAccessor
进行了最终实现细节的处理,它们提供了方便的访问,以便在其关联的构造(例如 Jwt
、IdToken
、UserInfo
)中访问 claims
。
我们为 AuthorizationRequestRepository
添加了将授权请求持久化到 Cookie
的功能。当前的默认实现是持久化到 HttpSession
中,但是,可以提供自定义实现来代替持久化到 Cookie
。
还添加了对在 AuthorizationCodeRequestRedirectFilter
的 redirect-uri
中配置 URI
变量的支持。
用于配置 OAuth 2.0 客户端的属性进行了一些小的更新。下面的配置概述了当前的结构。您会注意到支持配置多个客户端,例如 google、github、okta 等。
security:
oauth2:
client:
google:
client-id: your-app-client-id
client-secret: your-app-client-secret
client-authentication-method: basic
authorization-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{contextPath}/oauth2/authorize/code/{clientAlias}"
scope: openid, profile, email, address, phone
authorization-uri: "https://#/o/oauth2/v2/auth"
token-uri: "https://www.googleapis.com/oauth2/v4/token"
user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
user-name-attribute-name: "sub"
jwk-set-uri: "https://www.googleapis.com/oauth2/v3/certs"
client-name: Google
client-alias: google
github:
...
okta:
...
可以在 Spring Security 示例中的 oauth2login 找到使用新的 Spring Security OAuth 2.0 / OpenID Connect 1.0 登录功能的完整示例。该指南将引导您完成设置示例应用程序的步骤,以便使用外部 OAuth 2.0 或 OpenID Connect 1.0 提供程序进行 OAuth 2.0 登录。
Spring Security 的响应式支持现在通过利用 Reactor 的 Context 包括了方法安全。亮点如下,您可以在 samples/javaconfig/hellowebflux-method 中找到它的完整示例。
第一步是使用 @EnableReactiveMethodSecurity
来启用对 @PreAuthorize
和 @PostAuthorize
注解的支持。这一步确保对象被正确代理。
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
下一步是创建一个使用 @PreAuthorize
或 @PostAuthorize
注解的服务。例如
@PreAuthorize("hasRole('ADMIN')")
public Mono<String> findMessage() {
Spring Security 的 WebFlux 支持将确保 Reactor Context 填充当前用户,用于确定是否授予或拒绝访问。
Spring Security 标准的 @WithMockUser
和相关注解已更新,以支持响应式方法安全。例如
@RunWith(SpringRunner.class) // ... public class HelloWorldMessageServiceTests { @Autowired HelloWorldMessageService messages;
@Test public void messagesWhenNotAuthenticatedThenDenied() { StepVerifier.create(this.messages.findMessage()) .expectError(AccessDeniedException.class) .verify(); }
@Test @WithMockUser public void messagesWhenUserThenDenied() { StepVerifier.create(this.messages.findMessage()) .expectError(AccessDeniedException.class) .verify(); }
@Test @WithMockUser(roles = "ADMIN") public void messagesWhenAdminThenOk() { StepVerifier.create(this.messages.findMessage()) .expectNext("Hello World!") .verifyComplete(); } }
测试支持也与 TestWebClient
很好地配合使用。例如
@RunWith(SpringRunner.class) // ... public class HelloWebfluxMethodApplicationTests { @Autowired ApplicationContext context;
WebTestClient rest;
@Before public void setup() { this.rest = WebTestClient .bindToApplicationContext(this.context) // Setup Spring Security Test Support .apply(springSecurity()) .configureClient() .filter(basicAuthentication()) .build(); }
@Test public void messageWhenNotAuthenticated() throws Exception { this.rest .get() .uri("/message") .exchange() .expectStatus().isUnauthorized(); }
// --- authenticate with HTTP Basic ---
@Test public void messageWhenUserThenForbidden() throws Exception { this.rest .get() .uri("/message") .attributes(robsCredentials()) .exchange() .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) .expectBody().isEmpty(); }
@Test public void messageWhenAdminThenOk() throws Exception { this.rest .get() .uri("/message") .attributes(adminCredentials()) .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello World!"); }
// --- Use @WithMockUser ---
@Test @WithMockUser public void messageWhenWithMockUserThenForbidden() throws Exception { this.rest .get() .uri("/message") .exchange() .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) .expectBody().isEmpty(); }
@Test @WithMockUser(roles = "ADMIN") public void messageWhenWithMockAdminThenOk() throws Exception { this.rest .get() .uri("/message") .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello World!"); }
// --- Use mutateWith ---
@Test public void messageWhenMockUserThenForbidden() throws Exception { this.rest .mutateWith(mockUser()) .get() .uri("/message") .exchange() .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) .expectBody().isEmpty(); }
@Test public void messageWhenMockAdminThenOk() throws Exception { this.rest .mutateWith(mockUser().roles("ADMIN")) .get() .uri("/message") .exchange() .expectStatus().isOk() .expectBody(String.class).isEqualTo("Hello World!"); }
// ...
}
WebFlux 安全现在支持基于表单的登录,并提供了一个默认登录页面以便于入门。例如,samples/javaconfig/hellowebflux[samples/javaconfig/hellowebflux 允许用户使用基于表单的登录和默认登录页面进行身份验证。
@EnableWebFluxSecurity public class HelloWebfluxSecurityConfig {
@Bean public MapUserDetailsRepository userDetailsRepository() { UserDetails user = User.withUsername("user") .password("user") .roles("USER") .build(); return new MapUserDetailsRepository(user); } }
我们决定让默认登录页面链接到一个外部 CSS 文件,使其看起来更好看,而无需捆绑 CSS。您怎么看?
如果您没有连接到互联网,登录页面将回退到没有样式的页面。当然,您也可以提供自己的自定义登录页面。
与 servlet 世界类似,我们也为 WebFlux 安全添加了内容协商支持。例如,当在未认证的情况下请求受保护资源时,我们来自WebFlux 表单登录的最小示例将在 Web 浏览器中产生一个登录页面,并从命令行产生一个 WWW-Authenticate
响应。
---
HTTP/1.1 401 Unauthorized
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Expires: 0
Pragma: no-cache
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
content-length: 0
我们改进了 WebFlux 身份验证和会话管理的工作方式,提供了比 servlet 对应部分更大的灵活性。例如,我们来自WebFlux 表单登录的最小示例在使用基于表单的登录进行身份验证时将产生以下结果
POST /login HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 27
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:8080
username=user&password=user
HTTP/1.1 302 Found Cache-Control: no-cache, no-store, max-age=0, must-revalidate Expires: 0 Location: / Pragma: no-cache X-Content-Type-Options: nosniff X-Frame-Options: DENY X-XSS-Protection: 1 ; mode=block content-length: 0 set-cookie: SESSION=1e04aa3c-5a15-42ed-9e25-933fd0e44b2a; HTTPOnly
然而,完全相同的代码将对 HTTP Basic 身份验证产生以下响应
GET / HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Authorization: Basic dXNlcjp1c2Vy Connection: keep-alive Host: localhost:8080
HTTP/1.1 200 OK Cache-Control: no-cache, no-store, max-age=0, must-revalidate Content-Type: application/json;charset=UTF-8 Expires: 0 Pragma: no-cache X-Content-Type-Options: nosniff X-Frame-Options: DENY X-XSS-Protection: 1 ; mode=block transfer-encoding: chunked
{ "message": "Hello user!" }
请注意,基于表单的登录在响应中包含一个 SESSION cookie,而 HTTP Basic 没有。这通过单个 HttpSecurity
配置完成(我们将应用程序拆分为多个部分)。
如果您对此版本有反馈,我鼓励您通过 StackOverflow、GitHub Issues 或评论区联系。您也可以在 Twitter 上 @ 我 @rob_winch 或 @ Joe @joe_grandja。
当然,最好的反馈形式是贡献。