employee-service调用department-service,如果要按OAUTH2.0流程,只需要提供client-id和client-secrect即可。在KEYCLOAK中引入service-account,即配置该employee-service时,取消standard-flow,同时激活service-account。
employee-service的application.yaml文件,其中的public-key要从KEYCLOAK中取
server:
port: 8090
# Can be set to false to disable security during local development
rest:
security:
enabled: true
#issuer-uri: http://localhost:8080/auth/realms/dev
api-matcher: /api/**
cors:
allowed-origins: '*'
allowed-headers: '*'
allowed-methods: GET,POST,PUT,PATCH,DELETE,OPTIONS
max-age: 3600
security:
oauth2:
resource:
filter-order: 3
id: test-employee-service
token-info-uri: ${rest.security.issuer-uri}/protocol/openid-connect/token/introspect
user-info-uri: ${rest.security.issuer-uri}/protocol/openid-connect/userinfo
jwt:
key-value: |
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB
-----END PUBLIC KEY-----
# To access another secured micro-service
client:
client-id: test-employee-service
#client-secret: 25c33006-e1b9-4fc2-a6b9-c43dbc41ecd0
user-authorization-uri: ${rest.security.issuer-uri}/protocol/openid-connect/auth
access-token-uri: ${rest.security.issuer-uri}/protocol/openid-connect/token
scope: openid
grant-type: client_credentials
is-client-only: true
#Logging Configuration
logging:
level:
org.springframework.boot.autoconfigure.logging: INFO
org.springframework.security: DEBUG
org.arun: DEBUG
root: INFO
application-dev.yaml
rest:
security:
issuer-uri: http://10.80.27.69:8180/auth/realms/quickstart
department-service:
url: http://10.80.27.69:8095/api/departments/1
security:
oauth2:
client:
client-secret: db25cdbd-605b-429d-bd92-96705bdf1474
department-service的application.yaml
server:
port: 8095
# Can be set to false to disable security during local development
rest:
security:
enabled: true
#issuer-uri: http://localhost:8080/auth/realms/dev
api-matcher: /api/**
cors:
allowed-origins: '*'
allowed-headers: '*'
allowed-methods: GET,POST,PUT,PATCH,DELETE,OPTIONS
max-age: 3600
security:
oauth2:
resource:
filter-order: 3
id: test-department-service
token-info-uri: ${rest.security.issuer-uri}/protocol/openid-connect/token/introspect
user-info-uri: ${rest.security.issuer-uri}/protocol/openid-connect/userinfo
jwt:
key-value: |
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB
-----END PUBLIC KEY-----
#Logging Configuration
logging:
level:
org.springframework.boot.autoconfigure.logging: INFO
org.springframework.security: DEBUG
org.arun: DEBUG
root: INFO
application-dev.yaml
rest:
security:
issuer-uri: http://10.80.27.69:8180/auth/realms/quickstart
employee-service的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 http://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.1.18.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.arun.springoauth</groupId>
<artifactId>spring-oauth2-employee-service</artifactId>
<version>1.0.0</version>
<name>spring-oauth2-employee-service</name>
<description>Employee Service</description>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.1.18.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<!-- <version>2.1.18.RELEASE</version> -->
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-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>
将jwt格式的access_token转成Authentication的类JwtAccessTokenCustomizer
package org.arun.springoauth.employee.config;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.resource.JwtAccessTokenConverterConfigurer;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
@Configuration
public class JwtAccessTokenCustomizer extends DefaultAccessTokenConverter implements JwtAccessTokenConverterConfigurer {
private static final Logger LOG = LoggerFactory.getLogger(JwtAccessTokenCustomizer.class);
private static final String CLIENT_NAME_ELEMENT_IN_JWT = "resource_access";
private static final String ROLE_ELEMENT_IN_JWT = "roles";
private ObjectMapper mapper;
@Autowired
public JwtAccessTokenCustomizer(ObjectMapper mapper) {
this.mapper = mapper;
LOG.info("Initialized {}", JwtAccessTokenCustomizer.class.getSimpleName());
}
@Override
public void configure(JwtAccessTokenConverter converter) {
converter.setAccessTokenConverter(this);
LOG.info("Configured {}", JwtAccessTokenConverter.class.getSimpleName());
}
/**
* Spring oauth2 expects roles under authorities element in tokenMap, but
* keycloak provides it under resource_access. Hence extractAuthentication
* method is overriden to extract roles from resource_access.
*
* @return OAuth2Authentication with authorities for given application
*/
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> tokenMap) {
LOG.debug("Begin extractAuthentication: tokenMap = {}", tokenMap);
JsonNode token = mapper.convertValue(tokenMap, JsonNode.class);
Set<String> audienceList = extractClients(token); // extracting client names
List<GrantedAuthority> authorities = extractRoles(token); // extracting client roles
OAuth2Authentication authentication = super.extractAuthentication(tokenMap);
OAuth2Request oAuth2Request = authentication.getOAuth2Request();
OAuth2Request request = new OAuth2Request(oAuth2Request.getRequestParameters(), oAuth2Request.getClientId(),
authorities, true, oAuth2Request.getScope(), audienceList, null, null, null);
Authentication usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(
authentication.getPrincipal(), "N/A", authorities);
LOG.debug("End extractAuthentication");
return new OAuth2Authentication(request, usernamePasswordAuthentication);
}
private List<GrantedAuthority> extractRoles(JsonNode jwt) {
LOG.debug("Begin extractRoles: jwt = {}", jwt);
Set<String> rolesWithPrefix = new HashSet<>();
jwt.path(CLIENT_NAME_ELEMENT_IN_JWT).elements().forEachRemaining(e -> e.path(ROLE_ELEMENT_IN_JWT).elements()
.forEachRemaining(r -> rolesWithPrefix.add("ROLE_" + r.asText())));
final List<GrantedAuthority> authorityList = AuthorityUtils
.createAuthorityList(rolesWithPrefix.toArray(new String[0]));
LOG.debug("End extractRoles: roles = {}", authorityList);
return authorityList;
}
private Set<String> extractClients(JsonNode jwt) {
LOG.debug("Begin extractClients: jwt = {}", jwt);
if (jwt.has(CLIENT_NAME_ELEMENT_IN_JWT)) {
JsonNode resourceAccessJsonNode = jwt.path(CLIENT_NAME_ELEMENT_IN_JWT);
final Set<String> clientNames = new HashSet<>();
resourceAccessJsonNode.fieldNames().forEachRemaining(clientNames::add);
LOG.debug("End extractClients: clients = {}", clientNames);
return clientNames;
} else {
throw new IllegalArgumentException(
"Expected element " + CLIENT_NAME_ELEMENT_IN_JWT + " not found in token");
}
}
}
Reference
https://medium.com/@bcarunmail/securing-rest-api-using-keycloak-and-spring-oauth2-6ddf3a1efcc2
KEYCLOAK是一套用户、WEB API登录管理,授权管理的WEB应用。
如果要访问受KEYCLOAK保护的REST API服务,则需要夹带一个ACCESS_TOKEN。
前端页面:
- 前端页面一般是给用户使用的,则需要用户输入在KEYCLOAK中有效的用户名和密码,并提供CALL BAK的URL,提交给KEYCLOAK
http://10.80.27.69:8180/auth/realms/quickstart/protocol/openid-connect/auth?client_id=app-springboot-confidential&redirect_uri=http://10.80.27.69:8183/&response_type=code&scope=openid
- 如果KEYCLOAK验证通过,则通知页面重导向回调的URL,并附上code=xxx,此code则是AUTHORIZATION_CODE
http://10.80.27.69:8183/?session_state=2ad9ab98-6c39-43a8-872f-2112c27b74df&code=3f48ce19-58f9-45d9-8c09-30d492bf4b24.2ad9ab98-6c39-43a8-872f-2112c27b74df.bd7526ef-b1bf-447f-baef-b7dfd6f0df93
- 回调的URL对应的SERVELET,取得AUTHORIZATION_CODE,并加上client_id和client_secrect,调用KEYLOAK的取ACCESS_TOKEN的HTTP API,取得ACCESS_TOKEN,返回给页面
http://10.80.27.69:8180/auth/realms/quickstart/protocol/openid-connect/token
client_id=app-springboot-confidential&client_secret=3acf7692-49cb-4c45-9943-6f3dba512dae&redirect_uri=http://10.80.27.69:8183/&grant_type=authorization_code&code=cc7ac566-90f9-404e-b88e-fa28037b07d1.591311e1-5380-46a2-9363-834f17337922.bd7526ef-b1bf-447f-baef-b7dfd6f0df93
- 页面保存此ACCESS_TOKEN,就可以调用后台的各种API获取数据
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGSjg2R2NGM2pUYk5MT2NvNE52WmtVQ0lVbWZZQ3FvcXRPUWVNZmJoTmxFIn0.eyJleHAiOjE2MzQwMjA4ODksImlhdCI6MTYzNDAyMDU4OSwianRpIjoiNDAwOTQ4ZmQtMGU0MS00YWRjLTlhY2MtMzczZWM2NDVhNzM5IiwiaXNzIjoiaHR0cDovLzEwLjgwLjI3LjY5OjgxODAvYXV0aC9yZWFsbXMvcXVpY2tzdGFydCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJkZGVkMDA2YS0xY2QxLTRjODUtOTQ1MS0wMjFlZmY3OTFiMmUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhcHAtc3ByaW5nYm9vdC1jb25maWRlbnRpYWwiLCJzZXNzaW9uX3N0YXRlIjoiYzRlN2QzYTgtMDg2My00OTAwLTkxZmEtMGExYmFmYmRlNGU3IiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYXBwLXNwcmluZ2Jvb3QtY29uZmlkZW50aWFsIjp7InJvbGVzIjpbInVtYV9wcm90ZWN0aW9uIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNsaWVudElkIjoiYXBwLXNwcmluZ2Jvb3QtY29uZmlkZW50aWFsIiwiY2xpZW50SG9zdCI6IjEwLjEwLjIwLjU3IiwidXNlcl9uYW1lIjoic2VydmljZS1hY2NvdW50LWFwcC1zcHJpbmdib290LWNvbmZpZGVudGlhbCIsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1hcHAtc3ByaW5nYm9vdC1jb25maWRlbnRpYWwiLCJjbGllbnRBZGRyZXNzIjoiMTAuMTAuMjAuNTcifQ.Ut6aZ6E1d4Esz0gRv2ubxdvrxmGvZLHHZepD5pnGxlqb_yZ4Q82TdGTG0iL4JJn2NH3QAU501dhzzuv6-OT9BUBKP-4ufyKv2DxSvt3GgdN30au5JsATHFyOWuuZGRBd3iWcynf9u3OJnSkHEnrIwRYatgndLzy8dy3AeqF12CI",
"expires_in": 300,
"refresh_expires_in": 600,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2MTlhMmJjOS0yMWIwLTRmNGMtODI4OC1kNTJmMjA3OWEzY2EifQ.eyJleHAiOjE2MzQwMjExODksImlhdCI6MTYzNDAyMDU4OSwianRpIjoiYTM0NTQ1MTYtMzc3NC00YmRlLTgzOTMtN2QyMTdkZjdkZmJkIiwiaXNzIjoiaHR0cDovLzEwLjgwLjI3LjY5OjgxODAvYXV0aC9yZWFsbXMvcXVpY2tzdGFydCIsImF1ZCI6Imh0dHA6Ly8xMC44MC4yNy42OTo4MTgwL2F1dGgvcmVhbG1zL3F1aWNrc3RhcnQiLCJzdWIiOiJkZGVkMDA2YS0xY2QxLTRjODUtOTQ1MS0wMjFlZmY3OTFiMmUiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiYXBwLXNwcmluZ2Jvb3QtY29uZmlkZW50aWFsIiwic2Vzc2lvbl9zdGF0ZSI6ImM0ZTdkM2E4LTA4NjMtNDkwMC05MWZhLTBhMWJhZmJkZTRlNyIsInNjb3BlIjoicHJvZmlsZSBlbWFpbCJ9.QhjkJBGz5UvwBF7xHM7_V_yjfF0lrA_EWzAVdFf-BRI",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "c4e7d3a8-0863-4900-91fa-0a1bafbde4e7",
"scope": "profile email"
}
- 这就是authorization_code流程
后端服务:
验证Access Token和获取Token元信息:
刷新Token:
http://10.80.27.69:8180/auth/realms/quickstart/protocol/openid-connect/token
client_id=app-springboot-confidential&client_secret=3acf7692-49cb-4c45-9943-6f3dba512dae&grant_type=refresh_token&refresh_token=asdfasd
- 返回
{
"access_token": "eyJhbGciOiJSUzI1NiIsIn",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOi",
"token_type": "Bearer",
"not-before-policy": 1610728470,
"session_state": "c1273eb5-f922-420c-b23a-854be9735c1d",
"scope": "profile email"
}
Reference:
https://blog.csdn.net/nklinsirui/article/details/112706006https://www.baeldung.com/?s=keycloakhttps://www.doag.org/formes/pubfiles/11143470/2019-NN-Sebastien_Blanc-Easily_Secure_your_Microservices_with_Keycloak-Praesentation.pdf
enrich时可以发起一个子流程,取得结果后再设置回当前的对象中。
package org.springframework.integration.stackoverflow.enricher;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Transformers;
import org.springframework.integration.http.dsl.Http;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class SpringIntegrationEnricherApplication {
public static void main(String[] args) {
SpringApplication.run(SpringIntegrationEnricherApplication.class, args);
}
@Bean
public IntegrationFlow jsonEnricherFlow(RestTemplate restTemplate) {
return IntegrationFlows.from(Function.class)
.transform(Transformers.fromJson(Map.class))
.enrich((enricher) -> enricher
.<Map<String, ?>>requestPayload((message) ->
((List<?>) message.getPayload().get("attributeIds"))
.stream()
.map(Object::toString)
.collect(Collectors.joining(",")))
.requestSubFlow((subFlow) ->
subFlow.handle(
Http.outboundGateway("/attributes?id={ids}", restTemplate)
.httpMethod(HttpMethod.GET)
.expectedResponseType(Map.class)
.uriVariable("ids", "payload")))
.propertyExpression("attributes", "payload.attributes"))
.<Map<String, ?>, Map<String, ?>>transform(
(payload) -> {
payload.remove("attributeIds");
return payload;
})
.transform(Transformers.toJson())
.get();
}
}
https://stackoverflow.com/questions/58205432/spring-integration-enrich-transform-message-using-rest-callhttps://www.tabnine.com/web/assistant/code/rs/5c781b6ae70f87000197ab9f#L312
Java9之HttpClientAPI实战详解
https://blog.csdn.net/u014042066/article/details/78153653
Java 9 揭秘(14. HTTP/2 Client API)
https://www.cnblogs.com/IcanFixIt/p/7229611.html
Java JDK11(Java11)中设置HttpClient允许不安全的HTTPS连接
https://www.cjavapy.com/article/84/