通过 Keycloak REST API 注销用户不起作用
Logout user via Keycloak REST API doesn't work
我在从(移动)应用程序调用 Keycloak 的 logout 端点时遇到问题。
如 its documentation 所述支持此方案:
/realms/{realm-name}/protocol/openid-connect/logout
The logout endpoint logs out the authenticated user.
The user agent can be redirected to the endpoint, in which case the active user session is logged out. Afterward the user agent is redirected back to the application.
The endpoint can also be invoked directly by the application. To invoke this endpoint directly the refresh token needs to be included as well as the credentials required to authenticate the client.
我的请求格式如下:
POST http://localhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout
Authorization: Bearer <access_token>
Content-Type: application/x-www-form-urlencoded
refresh_token=<refresh_token>
但总是出现此错误:
HTTP/1.1 400 Bad Request
Connection: keep-alive
X-Powered-By: Undertow/1
Server: WildFly/10
Content-Type: application/json
Content-Length: 123
Date: Wed, 11 Oct 2017 12:47:08 GMT
{
"error": "unauthorized_client",
"error_description": "UNKNOWN_CLIENT: Client was not identified by any client authenticator"
}
如果我提供了 access_token,Keycloak 似乎无法检测到当前客户端的身份事件。我使用相同的 access_token 访问其他 Keycloak 的 API 没有任何问题,比如 userinfo
(/auth/realms//protocol/openid-connect/userinfo)。
我的请求是基于此 Keycloak's issue。该问题的作者成功了,但我的情况并非如此。
我正在使用 Keycloak 3.2.1.Final。
你有同样的问题吗?您知道如何解决吗?
最后,我通过查看 Keycloak 的源代码找到了解决方案:https://github.com/keycloak/keycloak/blob/9cbc335b68718443704854b1e758f8335b06c242/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L169。它说:
If the client is a public client, then you must include a "client_id" form parameter.
所以我缺少的是 client_id 表单参数。我的要求应该是:
POST http://localhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout
Authorization: Bearer <access_token>
Content-Type: application/x-www-form-urlencoded
client_id=<my_client_id>&refresh_token=<refresh_token>
会话应该被正确销毁。
仅供参考:OIDC 规范和 Google 的实施具有 token revocation endpoint
它已在 Keycloak 10 中实现。有关详细信息,请参阅Keycloak JIRA
在版本 3.4 中,您需要作为 x-www-form-urlencoded
主体键 client_id、client_secret
和 refresh_token.
我用 Keycloak 4.4.0.Final 和 4.6.0.Final 试过了。我检查了 keycloak 服务器日志,在控制台输出中看到以下警告消息。
10:33:22,882 WARN [org.keycloak.events] (default task-1) type=REFRESH_TOKEN_ERROR, realmId=master, clientId=security-admin-console, userId=null, ipAddress=127.0.0.1, error=invalid_token, grant_type=refresh_token, client_auth_method=client-secret
10:40:41,376 WARN [org.keycloak.events] (default task-5) type=LOGOUT_ERROR, realmId=demo, clientId=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJqYTBjX18xMHJXZi1KTEpYSGNqNEdSNWViczRmQlpGS3NpSHItbDlud2F3In0.eyJqdGkiOiI1ZTdhYzQ4Zi1mYjkyLTRkZTYtYjcxNC01MTRlMTZiMmJiNDYiLCJleHAiOjE1NDM0MDE2MDksIm5iZiI6MCwiaWF0IjoxNTQzNDAxMzA5LCJpc3MiOiJodHRwOi8vMTI3Lj, userId=null, ipAddress=127.0.0.1, error=invalid_client_credentials
那么HTTP请求是如何构建的呢?首先,我从 HttpSession 中检索了用户主体并转换为内部 Keycloak 实例类型:
KeycloakAuthenticationToken keycloakAuthenticationToken = (KeycloakAuthenticationToken) request.getUserPrincipal();
final KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal)keycloakAuthenticationToken.getPrincipal();
final RefreshableKeycloakSecurityContext context = (RefreshableKeycloakSecurityContext) keycloakPrincipal.getKeycloakSecurityContext();
final AccessToken accessToken = context.getToken();
final IDToken idToken = context.getIdToken();
其次,我创建了注销 URL,如顶部堆栈溢出答案(见上文):
final String logoutURI = idToken.getIssuer() +"/protocol/openid-connect/logout?"+
"redirect_uri="+response.encodeRedirectURL(url.toString());
现在我会像这样构建其余的 HTTP 请求:
KeycloakRestTemplate keycloakRestTemplate = new KeycloakRestTemplate(keycloakClientRequestFactory);
HttpHeaders headers = new HttpHeaders();
headers.put("Authorization", Collections.singletonList("Bearer "+idToken.getId()));
headers.put("Content-Type", Collections.singletonList("application/x-www-form-urlencoded"));
同时构建正文内容字符串:
StringBuilder bodyContent = new StringBuilder();
bodyContent.append("client_id=").append(context.getTokenString())
.append("&")
.append("client_secret=").append(keycloakCredentialsSecret)
.append("&")
.append("user_name=").append(keycloakPrincipal.getName())
.append("&")
.append("user_id=").append(idToken.getId())
.append("&")
.append("refresh_token=").append(context.getRefreshToken())
.append("&")
.append("token=").append(accessToken.getId());
HttpEntity<String> entity = new HttpEntity<>(bodyContent.toString(), headers);
// ...
ResponseEntity<String> forEntity = keycloakRestTemplate.exchange(logoutURI, HttpMethod.POST, entity, String.class); // *FAILURE*
如您所见,我尝试了多种主题变体,但我一直收到无效的用户身份验证。
哦耶。我使用 @Value
将来自 application.properties
的 keycloak 凭据秘密注入到对象实例字段中
@Value("${keycloak.credentials.secret}")
private String keycloakCredentialsSecret;
Java Spring 有安全经验的工程师有什么想法吗?
附录
我在 KC 中创建了一个名为 'demo' 的领域和一个名为 'web-portal' 的客户端
使用以下参数:
Client Protocol: openid-connect
Access Type: public
Standard Flow Enabled: On
Implicit Flow Enabled: Off
Direct Access Grants Enabled: On
Authorization Enabled: Off
这里是重建重定向 URI 的代码,我忘了把它包含在这里。
final String scheme = request.getScheme(); // http
final String serverName = request.getServerName(); // hostname.com
final int serverPort = request.getServerPort(); // 80
final String contextPath = request.getContextPath(); // /mywebapp
// Reconstruct original requesting URL
StringBuilder url = new StringBuilder();
url.append(scheme).append("://").append(serverName);
if (serverPort != 80 && serverPort != 443) {
url.append(":").append(serverPort);
}
url.append(contextPath).append("/offline-page.html");
就这些了
适用于 Keycloak 6.0。
为了清楚起见:我们确实使 refreshToken 过期,但 accessToken 在“访问令牌生命周期”时间内仍然有效。下次用户尝试通过刷新令牌更新访问令牌时,Keycloak returns 400 错误请求,应该捕获并作为 401 未经授权的响应发送。
public void logout(String refreshToken) {
try {
MultiValueMap<String, String> requestParams = new LinkedMultiValueMap<>();
requestParams.add("client_id", "my-client-id");
requestParams.add("client_secret", "my-client-id-secret");
requestParams.add("refresh_token", refreshToken);
logoutUserSession(requestParams);
} catch (Exception e) {
log.info(e.getMessage(), e);
throw e;
}
}
private void logoutUserSession(MultiValueMap<String, String> requestParams) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(requestParams, headers);
String realmName = "your-realm-name";
String url = "/auth/realms/" + realmName + "/protocol/openid-connect/logout";
restTemplate.postForEntity(url, request, Object.class);
// got response 204, no content
}
这就是我的 SpringBoot FX 应用程序的工作方式
获取 http://localhost:8080/auth/realms//protocol/openid-connect/logout?post_redirect_uri=your_encodedRedirectUri&id_token_hint =id_token
在 JWT 中你有“session_state”
{
"exp": 1616268254,
"iat": 1616267954,
....
"session_state": "c0e2cd7a-11ed-4537-b6a5-182db68eb00f",
...
}
之后
public void testDeconnexion() {
String serverUrl = "http://localhost:8080/auth";
String realm = "master";
String clientId = "admin-cli";
String clientSecret = "1d911233-bfb3-452b-8186-ebb7cceb426c";
String sessionState = "c0e2cd7a-11ed-4537-b6a5-182db68eb00f";
Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(realm)
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
.clientId(clientId)
.clientSecret(clientSecret)
.build();
String realmApp = "MeineSuperApp";
RealmResource realmResource = keycloak.realm(realmApp);
realmResource.deleteSession(sessionState);
}
此方法不需要任何手动端点触发器。它依赖于 LogoutSuccessHandler
尤其是 OidcClientInitiatedLogoutSuccessHandler
来检查 end_session_endpoint
是否存在于 ClientRegistration
bean 上。
在某些情况下,当与 Spring 安全性配对时,大多数身份验证提供商(Okta 除外)默认不使用 end_session_endpoint
,我们只能手动将其注入 ClientRegistration
.最简单的方法是将其放在 InMemoryClientRegistrationRepository
初始化之前,就在 application.properties
或 application.yaml
加载之后。
package com.tb.ws.cscommon.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Configuration
public class ClientRegistrationConfig {
@Bean
@ConditionalOnMissingBean({ClientRegistrationRepository.class})
InMemoryClientRegistrationRepository clientRegistrationRepository(
OAuth2ClientProperties properties) {
List<ClientRegistration> registrations =
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties)
.values()
.stream()
.map(
o ->
ClientRegistration.withClientRegistration(o)
.providerConfigurationMetadata(
Map.of(
"end_session_endpoint",
"http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/logout"))
.build())
.collect(Collectors.toList());
return new InMemoryClientRegistrationRepository(registrations);
}
}
并且在 WebSecurity
中:
package com.tb.ws.cscommon.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
@Slf4j
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private final InMemoryClientRegistrationRepository registrationRepository;
public WebSecurity(InMemoryClientRegistrationRepository registrationRepository) {
this.registrationRepository = registrationRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
String[] permitAccess = new String[] {"/", "/styles/**"};
http.authorizeRequests()
.antMatchers(permitAccess)
.permitAll()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.and()
.logout(
logout -> {
logout.logoutSuccessHandler(logoutSuccessHandler());
logout.invalidateHttpSession(true);
logout.clearAuthentication(true);
logout.deleteCookies("JSESSIONID");
});
}
private LogoutSuccessHandler logoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler handler =
new OidcClientInitiatedLogoutSuccessHandler(registrationRepository);
handler.setPostLogoutRedirectUri("http://127.0.0.1:8005/");
return handler;
}
}
默认情况下,Spring 安全性将查询参数 id_token_hint
和 post_logout_redirect_uri
附加到 end_session_endpoint
上。这可以用 OidcClientInitiatedLogoutSuccessHandler handler
改变。这可以与社交提供者一起使用。只需为每个提供商提供相关的 end_session_endpoint
。
本例中使用的属性文件application.yaml
:
spring:
application:
name: cs-common
main:
banner-mode: off
security:
oauth2:
client:
registration:
cs-common-1:
client_id: cs-common
client-secret: 03e2f8e1-f150-449c-853d-4d8f51f66a29
scope: openid, profile, roles
authorization-grant-type: authorization_code
redirect_uri: http://127.0.0.1:8005/login/oauth2/code/cs-common-1
provider:
cs-common-1:
authorization-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/auth
token-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/token
jwk-set-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/certs
user-info-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/userinfo
user-name-attribute: preferred_username
server:
port: 8005
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8004/eureka
instance:
instance-id: ${spring.application.name}:${instanceId:${random.value}}
为了测试我们只是从 UI.
踢 Spring 安全的默认 GET /logout
端点
杂项:
- Spring 启动 2.5
- Spring云2020.0.3
- Java11
- Keycloak 服务器 13.0.1
客户端设置:
- 启用标准流程
- 隐式流已禁用
- 已启用直接访问授权
某人,某处可能会觉得有用。
P.S。该应用程序及其属性文件供学习使用
终于。它对我有用。我进行了如下所示的 REST 调用:
Headers:
{
"Authorization" : "Bearer <access_token>",
"Content-Type" : "application/x-www-form-urlencoded"
}
请求Body:
{
"client_id" : "<client_id>",
"client_secret" : "<client_secret>",
"refresh_token" : "<refresh_token>"
}
方法:
POST
URL:
<scheme>://<host>:<port>/auth/realms/<realmName>/protocol/openid-connect/logout
我收到了 200 作为响应...如果您做错任何事,您将收到 401 或 400 错误。调试这个问题非常困难。顺便说一句,我的 keycloak 版本是 12.0.4
如果 post 不清楚或者您需要更多信息,请告诉我。
我在从(移动)应用程序调用 Keycloak 的 logout 端点时遇到问题。
如 its documentation 所述支持此方案:
/realms/{realm-name}/protocol/openid-connect/logout
The logout endpoint logs out the authenticated user.
The user agent can be redirected to the endpoint, in which case the active user session is logged out. Afterward the user agent is redirected back to the application.
The endpoint can also be invoked directly by the application. To invoke this endpoint directly the refresh token needs to be included as well as the credentials required to authenticate the client.
我的请求格式如下:
POST http://localhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout
Authorization: Bearer <access_token>
Content-Type: application/x-www-form-urlencoded
refresh_token=<refresh_token>
但总是出现此错误:
HTTP/1.1 400 Bad Request
Connection: keep-alive
X-Powered-By: Undertow/1
Server: WildFly/10
Content-Type: application/json
Content-Length: 123
Date: Wed, 11 Oct 2017 12:47:08 GMT
{
"error": "unauthorized_client",
"error_description": "UNKNOWN_CLIENT: Client was not identified by any client authenticator"
}
如果我提供了 access_token,Keycloak 似乎无法检测到当前客户端的身份事件。我使用相同的 access_token 访问其他 Keycloak 的 API 没有任何问题,比如 userinfo (/auth/realms//protocol/openid-connect/userinfo)。
我的请求是基于此 Keycloak's issue。该问题的作者成功了,但我的情况并非如此。
我正在使用 Keycloak 3.2.1.Final。
你有同样的问题吗?您知道如何解决吗?
最后,我通过查看 Keycloak 的源代码找到了解决方案:https://github.com/keycloak/keycloak/blob/9cbc335b68718443704854b1e758f8335b06c242/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L169。它说:
If the client is a public client, then you must include a "client_id" form parameter.
所以我缺少的是 client_id 表单参数。我的要求应该是:
POST http://localhost:8080/auth/realms/<my_realm>/protocol/openid-connect/logout
Authorization: Bearer <access_token>
Content-Type: application/x-www-form-urlencoded
client_id=<my_client_id>&refresh_token=<refresh_token>
会话应该被正确销毁。
仅供参考:OIDC 规范和 Google 的实施具有 token revocation endpoint
它已在 Keycloak 10 中实现。有关详细信息,请参阅Keycloak JIRA
在版本 3.4 中,您需要作为 x-www-form-urlencoded
主体键 client_id、client_secret
和 refresh_token.
我用 Keycloak 4.4.0.Final 和 4.6.0.Final 试过了。我检查了 keycloak 服务器日志,在控制台输出中看到以下警告消息。
10:33:22,882 WARN [org.keycloak.events] (default task-1) type=REFRESH_TOKEN_ERROR, realmId=master, clientId=security-admin-console, userId=null, ipAddress=127.0.0.1, error=invalid_token, grant_type=refresh_token, client_auth_method=client-secret
10:40:41,376 WARN [org.keycloak.events] (default task-5) type=LOGOUT_ERROR, realmId=demo, clientId=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJqYTBjX18xMHJXZi1KTEpYSGNqNEdSNWViczRmQlpGS3NpSHItbDlud2F3In0.eyJqdGkiOiI1ZTdhYzQ4Zi1mYjkyLTRkZTYtYjcxNC01MTRlMTZiMmJiNDYiLCJleHAiOjE1NDM0MDE2MDksIm5iZiI6MCwiaWF0IjoxNTQzNDAxMzA5LCJpc3MiOiJodHRwOi8vMTI3Lj, userId=null, ipAddress=127.0.0.1, error=invalid_client_credentials
那么HTTP请求是如何构建的呢?首先,我从 HttpSession 中检索了用户主体并转换为内部 Keycloak 实例类型:
KeycloakAuthenticationToken keycloakAuthenticationToken = (KeycloakAuthenticationToken) request.getUserPrincipal();
final KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal)keycloakAuthenticationToken.getPrincipal();
final RefreshableKeycloakSecurityContext context = (RefreshableKeycloakSecurityContext) keycloakPrincipal.getKeycloakSecurityContext();
final AccessToken accessToken = context.getToken();
final IDToken idToken = context.getIdToken();
其次,我创建了注销 URL,如顶部堆栈溢出答案(见上文):
final String logoutURI = idToken.getIssuer() +"/protocol/openid-connect/logout?"+
"redirect_uri="+response.encodeRedirectURL(url.toString());
现在我会像这样构建其余的 HTTP 请求:
KeycloakRestTemplate keycloakRestTemplate = new KeycloakRestTemplate(keycloakClientRequestFactory);
HttpHeaders headers = new HttpHeaders();
headers.put("Authorization", Collections.singletonList("Bearer "+idToken.getId()));
headers.put("Content-Type", Collections.singletonList("application/x-www-form-urlencoded"));
同时构建正文内容字符串:
StringBuilder bodyContent = new StringBuilder();
bodyContent.append("client_id=").append(context.getTokenString())
.append("&")
.append("client_secret=").append(keycloakCredentialsSecret)
.append("&")
.append("user_name=").append(keycloakPrincipal.getName())
.append("&")
.append("user_id=").append(idToken.getId())
.append("&")
.append("refresh_token=").append(context.getRefreshToken())
.append("&")
.append("token=").append(accessToken.getId());
HttpEntity<String> entity = new HttpEntity<>(bodyContent.toString(), headers);
// ...
ResponseEntity<String> forEntity = keycloakRestTemplate.exchange(logoutURI, HttpMethod.POST, entity, String.class); // *FAILURE*
如您所见,我尝试了多种主题变体,但我一直收到无效的用户身份验证。 哦耶。我使用 @Value
将来自application.properties
的 keycloak 凭据秘密注入到对象实例字段中
@Value("${keycloak.credentials.secret}")
private String keycloakCredentialsSecret;
Java Spring 有安全经验的工程师有什么想法吗?
附录 我在 KC 中创建了一个名为 'demo' 的领域和一个名为 'web-portal' 的客户端 使用以下参数:
Client Protocol: openid-connect
Access Type: public
Standard Flow Enabled: On
Implicit Flow Enabled: Off
Direct Access Grants Enabled: On
Authorization Enabled: Off
这里是重建重定向 URI 的代码,我忘了把它包含在这里。
final String scheme = request.getScheme(); // http
final String serverName = request.getServerName(); // hostname.com
final int serverPort = request.getServerPort(); // 80
final String contextPath = request.getContextPath(); // /mywebapp
// Reconstruct original requesting URL
StringBuilder url = new StringBuilder();
url.append(scheme).append("://").append(serverName);
if (serverPort != 80 && serverPort != 443) {
url.append(":").append(serverPort);
}
url.append(contextPath).append("/offline-page.html");
就这些了
适用于 Keycloak 6.0。
为了清楚起见:我们确实使 refreshToken 过期,但 accessToken 在“访问令牌生命周期”时间内仍然有效。下次用户尝试通过刷新令牌更新访问令牌时,Keycloak returns 400 错误请求,应该捕获并作为 401 未经授权的响应发送。
public void logout(String refreshToken) {
try {
MultiValueMap<String, String> requestParams = new LinkedMultiValueMap<>();
requestParams.add("client_id", "my-client-id");
requestParams.add("client_secret", "my-client-id-secret");
requestParams.add("refresh_token", refreshToken);
logoutUserSession(requestParams);
} catch (Exception e) {
log.info(e.getMessage(), e);
throw e;
}
}
private void logoutUserSession(MultiValueMap<String, String> requestParams) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(requestParams, headers);
String realmName = "your-realm-name";
String url = "/auth/realms/" + realmName + "/protocol/openid-connect/logout";
restTemplate.postForEntity(url, request, Object.class);
// got response 204, no content
}
这就是我的 SpringBoot FX 应用程序的工作方式
获取 http://localhost:8080/auth/realms/
在 JWT 中你有“session_state”
{
"exp": 1616268254,
"iat": 1616267954,
....
"session_state": "c0e2cd7a-11ed-4537-b6a5-182db68eb00f",
...
}
之后
public void testDeconnexion() {
String serverUrl = "http://localhost:8080/auth";
String realm = "master";
String clientId = "admin-cli";
String clientSecret = "1d911233-bfb3-452b-8186-ebb7cceb426c";
String sessionState = "c0e2cd7a-11ed-4537-b6a5-182db68eb00f";
Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(realm)
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
.clientId(clientId)
.clientSecret(clientSecret)
.build();
String realmApp = "MeineSuperApp";
RealmResource realmResource = keycloak.realm(realmApp);
realmResource.deleteSession(sessionState);
}
此方法不需要任何手动端点触发器。它依赖于 LogoutSuccessHandler
尤其是 OidcClientInitiatedLogoutSuccessHandler
来检查 end_session_endpoint
是否存在于 ClientRegistration
bean 上。
在某些情况下,当与 Spring 安全性配对时,大多数身份验证提供商(Okta 除外)默认不使用 end_session_endpoint
,我们只能手动将其注入 ClientRegistration
.最简单的方法是将其放在 InMemoryClientRegistrationRepository
初始化之前,就在 application.properties
或 application.yaml
加载之后。
package com.tb.ws.cscommon.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Configuration
public class ClientRegistrationConfig {
@Bean
@ConditionalOnMissingBean({ClientRegistrationRepository.class})
InMemoryClientRegistrationRepository clientRegistrationRepository(
OAuth2ClientProperties properties) {
List<ClientRegistration> registrations =
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties)
.values()
.stream()
.map(
o ->
ClientRegistration.withClientRegistration(o)
.providerConfigurationMetadata(
Map.of(
"end_session_endpoint",
"http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/logout"))
.build())
.collect(Collectors.toList());
return new InMemoryClientRegistrationRepository(registrations);
}
}
并且在 WebSecurity
中:
package com.tb.ws.cscommon.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
@Slf4j
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
private final InMemoryClientRegistrationRepository registrationRepository;
public WebSecurity(InMemoryClientRegistrationRepository registrationRepository) {
this.registrationRepository = registrationRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
String[] permitAccess = new String[] {"/", "/styles/**"};
http.authorizeRequests()
.antMatchers(permitAccess)
.permitAll()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.and()
.logout(
logout -> {
logout.logoutSuccessHandler(logoutSuccessHandler());
logout.invalidateHttpSession(true);
logout.clearAuthentication(true);
logout.deleteCookies("JSESSIONID");
});
}
private LogoutSuccessHandler logoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler handler =
new OidcClientInitiatedLogoutSuccessHandler(registrationRepository);
handler.setPostLogoutRedirectUri("http://127.0.0.1:8005/");
return handler;
}
}
默认情况下,Spring 安全性将查询参数 id_token_hint
和 post_logout_redirect_uri
附加到 end_session_endpoint
上。这可以用 OidcClientInitiatedLogoutSuccessHandler handler
改变。这可以与社交提供者一起使用。只需为每个提供商提供相关的 end_session_endpoint
。
本例中使用的属性文件application.yaml
:
spring:
application:
name: cs-common
main:
banner-mode: off
security:
oauth2:
client:
registration:
cs-common-1:
client_id: cs-common
client-secret: 03e2f8e1-f150-449c-853d-4d8f51f66a29
scope: openid, profile, roles
authorization-grant-type: authorization_code
redirect_uri: http://127.0.0.1:8005/login/oauth2/code/cs-common-1
provider:
cs-common-1:
authorization-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/auth
token-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/token
jwk-set-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/certs
user-info-uri: http://127.0.0.1:8080/auth/realms/OAuth2/protocol/openid-connect/userinfo
user-name-attribute: preferred_username
server:
port: 8005
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8004/eureka
instance:
instance-id: ${spring.application.name}:${instanceId:${random.value}}
为了测试我们只是从 UI.
踢 Spring 安全的默认GET /logout
端点
杂项:
- Spring 启动 2.5
- Spring云2020.0.3
- Java11
- Keycloak 服务器 13.0.1
客户端设置:
- 启用标准流程
- 隐式流已禁用
- 已启用直接访问授权
某人,某处可能会觉得有用。
P.S。该应用程序及其属性文件供学习使用
终于。它对我有用。我进行了如下所示的 REST 调用:
Headers:
{
"Authorization" : "Bearer <access_token>",
"Content-Type" : "application/x-www-form-urlencoded"
}
请求Body:
{
"client_id" : "<client_id>",
"client_secret" : "<client_secret>",
"refresh_token" : "<refresh_token>"
}
方法:
POST
URL:
<scheme>://<host>:<port>/auth/realms/<realmName>/protocol/openid-connect/logout
我收到了 200 作为响应...如果您做错任何事,您将收到 401 或 400 错误。调试这个问题非常困难。顺便说一句,我的 keycloak 版本是 12.0.4
如果 post 不清楚或者您需要更多信息,请告诉我。