Spring Boot新版本已经不在支持Spring Security OAuth,而是将资源服务和客户端集成到Spring Security 5.2.x版本中,认证服务单独成一个项目为Spring Authorization Server,版本迁移说明详情可以参阅OAuth 2.0 Migration Guide
Spring Authorization Server目前最新版本为0.3.1,依赖Java8或更高版本
0.3.0版本依赖Java11或更高版本
0.3.0之前版本依赖Java8或更高版本
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-oauth2-authorization-serverartifactId>
<version>0.3.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.nimbusdsgroupId>
<artifactId>nimbus-jose-jwtartifactId>
<version>9.25.1version>
dependency>
<dependency>
<groupId>com.h2databasegroupId>
<artifactId>h2artifactId>
<scope>runtimescope>
dependency>
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")));
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8802/login/oauth2/code/messaging-client-oidc")
.redirectUri("http://127.0.0.1:8802/authorized")
.scope(OidcScopes.OPENID)
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
// Save registered client in db as if in-memory
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(KeyPair keyPair) {
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://localhost:8800").build();
}
@Bean
public EmbeddedDatabase embeddedDatabase() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.setScriptEncoding("UTF-8")
.addScript("sql/oauth2-authorization-schema.sql")
.addScript("sql/oauth2-authorization-consent-schema.sql")
.addScript("sql/oauth2-registered-client-schema.sql")
.build();
}
}
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest()
.authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
UserDetailsService users() {
UserDetails userDetails = User.withUsername("user")
.password("{bcrypt}$2a$10$9uIg5tH8gvyk1Uet8h.Jp.i5i16OTxMhbjb4nbQIQUH.D3m2L2Way")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
server:
port: 8800
spring:
application:
name: cloud-oauth2-authorization-server
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
org.springframework.security.oauth2: INFO
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-oauth2-resource-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
server:
port: 8801
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
org.springframework.security.oauth2: INFO
spring:
application:
name: cloud-oauth2-resource-server
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8800
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.mvcMatcher("/messages/**")
.authorizeRequests()
.mvcMatchers("/messages/**")
.access("hasAuthority('SCOPE_message.read')")
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
}
@RestController
public class ResourceServerController {
@GetMapping("/messages")
public String[] getMessages() {
return new String[] {"Message 1", "Message 2", "Message 3"};
}
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-oauth2-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webfluxartifactId>
dependency>
<dependency>
<groupId>io.projectreactor.nettygroupId>
<artifactId>reactor-nettyartifactId>
dependency>
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>webjars-locator-coreartifactId>
dependency>
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>bootstrapartifactId>
<version>5.2.0version>
dependency>
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>jqueryartifactId>
<version>3.6.1version>
dependency>
server:
port: 8802
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
org.springframework.security.oauth2: INFO
spring:
thymeleaf:
cache: false
application:
name: cloud-oauth2-client
security:
oauth2:
client:
registration:
messaging-client-oidc:
provider: spring
client-id: messaging-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:8802/login/oauth2/code/{registrationId}"
scope: openid
client-name: messaging-client-oidc
messaging-client-authorization-code:
provider: spring
client-id: messaging-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:8802/authorized"
scope: message.read,message.write
client-name: messaging-client-authorization-code
messaging-client-client-credentials:
provider: spring
client-id: messaging-client
client-secret: secret
authorization-grant-type: client_credentials
scope: message.read,message.write
client-name: messaging-client-client-credentials
provider:
spring:
issuer-uri: http://localhost:8800
messages:
base-uri: http://127.0.0.1:8801/messages
@Configuration
public class WebClientConfig {
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
@EnableWebSecurity
public class SecurityConfig {
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/webjars/**");
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.oauth2Login(oauth2Login -> oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc"))
.oauth2Client(withDefaults());
return http.build();
}
}
@Controller
public class AuthorizationController {
private final WebClient webClient;
private final String messagesBaseUri;
public AuthorizationController(WebClient webClient, @Value("${messages.base-uri}") String messagesBaseUri) {
this.webClient = webClient;
this.messagesBaseUri = messagesBaseUri;
}
@GetMapping(value = "/authorize", params = "grant_type=authorization_code")
public String authorizationCodeGrant(Model model, @RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code") OAuth2AuthorizedClient authorizedClient) {
String[] messages = this.webClient
.get()
.uri(this.messagesBaseUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String[].class)
.block();
model.addAttribute("messages", messages);
return "index";
}
@GetMapping(value = "/authorized", params = OAuth2ParameterNames.ERROR)
public String authorizationFailed(Model model, HttpServletRequest request) {
String errorCode = request.getParameter(OAuth2ParameterNames.ERROR);
if (StringUtils.hasText(errorCode)) {
model.addAttribute("error",
new OAuth2Error(errorCode,
request.getParameter(OAuth2ParameterNames.ERROR_DESCRIPTION),
request.getParameter(OAuth2ParameterNames.ERROR_URI))
);
}
return "index";
}
@GetMapping(value = "/authorize", params = "grant_type=client_credentials")
public String clientCredentialsGrant(Model model) {
String[] messages = this.webClient
.get()
.uri(this.messagesBaseUri)
.attributes(clientRegistrationId("messaging-client-client-credentials"))
.retrieve()
.bodyToMono(String[].class)
.block();
model.addAttribute("messages", messages);
return "index";
}
}
DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<title>Spring Security OAuth 2.0 Sampletitle>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css" th:href="@{/webjars/bootstrap/css/bootstrap.css}" />
head>
<body>
<div th:fragment="header">
<nav class="navbar navbar-default">
<div class="container">
<div class="container-fluid">
<div class="navbar-collapse collapse" id="navbar">
div>
div>
div>
nav>
div>
<div class="container">
<div th:if="${error}" class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×span>button>
<h4 th:text="${error}" class="text-center">h4>
div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Authorize the client using <span style="font-family:monospace">grant_typespan>:h3>
div>
<ul class="list-group">
<li class="list-group-item">
<a href="/authorize?grant_type=authorization_code" th:href="@{/authorize?grant_type=authorization_code}"><span style="font-size:medium">Authorization Codespan> <small class="text-muted">(Login to Spring Authorization Server using: user1/password)small>a>
li>
<li class="list-group-item">
<a href="/authorize?grant_type=client_credentials" th:href="@{/authorize?grant_type=client_credentials}"><span style="font-size:medium">Client Credentialsspan>a>
li>
ul>
<div th:if="${messages}" class="panel-footer">
<h4>Messages:h4>
<table class="table table-condensed">
<tbody>
<tr class="row" th:each="message : ${messages}">
<td th:text="${message}">messagetd>
tr>
tbody>
table>
div>
div>
div>
<script src="/webjars/jquery/jquery.min.js" th:src="@{/webjars/jquery/jquery.min.js}">script>
<script src="/webjars/bootstrap/js/bootstrap.min.js" th:src="@{/webjars/bootstrap/js/bootstrap.min.js}">script>
body>
html>
依次启动cloud-oauth2-authorization-server、cloud-oauth2-resource-server和cloud-oauth2-client三个微服务,在浏览器地址栏输入地址http://127.0.0.1:8802/,输入用户名和密码user/123456,成功后会跳转到如下界面
点击Authorization Code会进入权限确认界面
勾选权限(message.read/message.write),点击Submit Consent后会跳转到资源页面
更多关于SpringSecurity使用的案例,可以参阅Baeldung
有需要了解老方案的小伙伴,请参阅SpringCloud搭建微服务之OAuth2认证和授权