根据OAUTH2协议,如果需要用户协助的,则使用authorization_code流程,此时需要用户登录页面、CLIENT SERVER、RESOURCE SERVER和AUTHORIZATION SERVER,其中CLIENT SERVER是通过http调用RESOURCE SERVER的api,AUTHORIZATION SERVER使用现成的KEYCLOAK。
如果不需要用户协助的,即SERVER对SERVER的,则适用client_credentials流程,此时需要CLIENT SERVER、RESOURCE SERVER和AUTHORIZATION SERVER。
通常HTTP请求会在HEADER中夹带ACCESS_TOKEN,格式为JWT。
RESOURCE SERVER是如何知道该次的HTTP请求是合法的呢,只需用AUTHORIZATION SERVER提供的PUBLIC KEY能正常解密ACCESS_TOKEN,且不过期,则是合法的请求。
如果需要做授权认证,则检查JWT中的ROLE字符是否是事先定义的字符,如是则认为有权访问,否则拒绝。
RESOURCE SERVER
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.paul</groupId>
<artifactId>test-oauth2-department-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test-oauth2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>ZIP</layout>
<excludes>
<exclude>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclude>
</excludes>
<includes>
<include>
<groupId>com.paul</groupId>
</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yaml
server:
port: 8281
spring:
security:
oauth2:
#check OAuth2ResourceServerProperties
resourceserver:
jwt:
#jwk-set-uri: "${rest.security.issuer-uri}/protocol/openid-connect/certs"
issuer-uri: "${rest.security.issuer-uri}"
#public-key-location: "classpath:/key.pub"
#Logging Configuration
logging:
level:
org.springframework.boot.autoconfigure.logging: INFO
org.springframework.security: DEBUG
com.paul: DEBUG
root: INFO
java配置文件
package com.paul.testoauth2.config;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.paul.testoauth2.oauth2.converter.KeycloakRealmRoleConverter;
import com.paul.testoauth2.oauth2.converter.UsernameSubClaimAdapter;
@Configuration
public class SecurityConfigurer extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(
a -> a.antMatchers("/", "/error", "/webjars/**")
.permitAll()
.antMatchers(HttpMethod.GET, "/protected/**").hasRole("READ_DEPARTMENT")
.anyRequest()
.authenticated()
)
.exceptionHandling(
e -> e//.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
// .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
)
.oauth2ResourceServer(
o -> o.jwt(
j -> j.jwtAuthenticationConverter(jwtAuthenticationConverter(null))
.decoder(jwtDecoder(null))
)
)
// .decoder(jwtDecoder(null))
;
// @formatter:on
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter(ObjectMapper objectMapper) {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter(objectMapper));
return jwtConverter;
}
//below is auto configured at OAuth2ResourceServerJwtConfiguration
@Bean
public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
String issuerUri = properties.getJwt().getIssuerUri();
// Use preferred_username from claims as authentication name, instead of UUID subject
NimbusJwtDecoder jwtDecoder =
JwtDecoders.<NimbusJwtDecoder>fromIssuerLocation(issuerUri);
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
}
CLIENT SERVER
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.paul</groupId>
<artifactId>test-oauth2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test-oauth2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>ZIP</layout>
<excludes>
<exclude>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclude>
</excludes>
<includes>
<include>
<groupId>com.paul</groupId>
</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yaml
server:
port: 8280
spring:
security:
oauth2:
#check OAuth2ResourceServerProperties
resourceserver:
jwt:
#jwk-set-uri: "${rest.security.issuer-uri}/protocol/openid-connect/certs"
issuer-uri: "${rest.security.issuer-uri}"
#public-key-location: "classpath:/key.pub"
#check OAuth2ClientProperties
client:
registration:
my-client-1:
client-id: "test-employee-service"
client-secret: ${client-secret.my-client-1}
client-name: "test-employee-service"
provider: "keycloak-provider"
scope: "openid"
#redirect-uri: "https://my-redirect-uri.com"
client-authentication-method: "basic"
authorization-grant-type: "client_credentials"
app-springboot-confidential:
client-id: "app-springboot-confidential"
client-secret: ${client-secret.app-springboot-confidential}
client-name: "app-springboot-confidential"
provider: "keycloak-provider"
scope: "openid"
redirect-uri: "https://my-redirect-uri.com"
client-authentication-method: "basic"
authorization-grant-type: "authorization_code"
provider:
keycloak-provider:
issuer-uri : "${rest.security.issuer-uri}"
user-name-attribute: "preferred_username"
#Logging Configuration
logging:
level:
org.springframework.boot.autoconfigure.logging: INFO
org.springframework.security: DEBUG
com.paul: DEBUG
root: INFO
SecurityConfigurer.java
package com.paul.testoauth2.config;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.paul.testoauth2.oauth2.converter.KeycloakRealmRoleConverter;
import com.paul.testoauth2.oauth2.converter.UsernameSubClaimAdapter;
@Configuration
//@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfigurer extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(
a -> a
.antMatchers("/", "/error", "/webjars/**")
.permitAll()
.antMatchers(HttpMethod.GET, "/protected/**").hasRole("USER")
.antMatchers(HttpMethod.GET, "/api/employees/**").hasRole("READ_EMPLOYEE")
.anyRequest()
.authenticated()
)
.exceptionHandling(
e -> e//.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
// .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
)
.oauth2Client(
c -> c.authorizedClientService(
authorizedClientService(null, null)
)
)
.oauth2ResourceServer()
.jwt(
c -> c.jwtAuthenticationConverter(
jwtAuthenticationConverter(null)
)
.decoder(jwtDecoder(null))
)
// .decoder(jwtDecoder(null))
;
// @formatter:on
}
@Bean
public OAuth2AuthorizedClientService authorizedClientService(
JdbcOperations jdbcOperations, ClientRegistrationRepository clientRegistrationRepository
) {
return new JdbcOAuth2AuthorizedClientService(jdbcOperations, clientRegistrationRepository);
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter(ObjectMapper objectMapper) {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setPrincipalClaimName("preferred_username");
jwtConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter(objectMapper));
return jwtConverter;
}
//below is auto configured at OAuth2ResourceServerJwtConfiguration
@Bean
public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
String issuerUri = properties.getJwt().getIssuerUri();
// Use preferred_username from claims as authentication name, instead of UUID subject
NimbusJwtDecoder jwtDecoder =
JwtDecoders.<NimbusJwtDecoder>fromIssuerLocation(issuerUri);
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
}
WebClientConfig.java
package com.paul.testoauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(ClientRegistrationRepository clientRegistrations,
OAuth2AuthorizedClientRepository authorizedClients) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
oauth.setDefaultClientRegistrationId("my-client-1");
return WebClient.builder().filter(oauth).build();
}
}
DepartmentRestClient.java
package com.paul.testoauth2.employee.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
@Component
public class DepartmentRestClient {
// @NotNull
@Value("${department-service.url}")
private String endpoint;
@Autowired
private WebClient webClient;
public String getDepartmentName() {
return webClient.get()
.uri(endpoint + "/protected")
.retrieve()
.bodyToMono(String.class)
.block()
;
}
}
Reference:
https://docs.spring.io/spring-security/site/docs/5.5.x/reference/html5/#oauth2Sample Project:
https://github.com/spring-projects/spring-security-samples/tree/5.5.x!相当不错的教程
https://wstutorial.com/index.htmlhttps://wstutorial.com/rest/spring-security-oauth2-keycloak.htmlhttps://wstutorial.com/rest/spring-security-oauth2-keycloak-roles.html