应用程序的目的
所以我正在开发一个非常基本的应用程序,使用 Kotlin 语言的 Spring Boot,我想集成一个基于Keycloak身份验证管理服务的身份验证系统。
我的应用程序现在包含一个许多页面,当您想首次访问您的个人资料时,该页面要求您通过将您发送到Keycloak登录表单来登录。然后,在身份验证成功后,Keycloak 会自动管理对受保护内容的所有请求。
最后,当您在个人资料中时,您可以选择注销,一旦您这样做,您将被重定向到一个注销页面,该页面宣布您成功注销并返回主页。
我的应用程序配置
首先,以下是我设置应用程序的方法。
这是我的控制器:
@Controller
class MainController {
@GetMapping("/profile")
fun getProfile() = "profile"
@GetMapping("/logout")
@Throws(Exception::class)
fun logout(request: HttpServletRequest): String? {
request.logout()
return "logout"
}
}
这是我的安全配置:
@Configuration
@EnableWebSecurity
class SecurityConfig(keycloakLogoutHandler: KeycloakLogoutHandler?) {
private final val keycloakLogoutHandler: KeycloakLogoutHandler = keycloakLogoutHandler!!
@Bean
@Throws(Exception::class)
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.authorizeHttpRequests()
.requestMatchers("/profile").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login()
.and()
.logout()
.addLogoutHandler(keycloakLogoutHandler)
.logoutSuccessUrl("/logout")
.and()
.csrf().disable()
return http.build()
}
}
这是我自定义的Keycloak配置的注销处理程序:
@Component
class KeycloakLogoutHandler : LogoutHandler {
var logger: Logger = LoggerFactory.getLogger(KeycloakLogoutHandler::class.java)
private final val restTemplate: RestTemplate = RestTemplate()
override fun logout(
request: HttpServletRequest?,
response: HttpServletResponse?,
auth: Authentication?
) {
val user = auth?.principal as OidcUser
val endSessionEndpoint = user.issuer.toString() + "/protocol/openid-connect/logout"
val builder = UriComponentsBuilder
.fromUriString(endSessionEndpoint)
.queryParam("id_token_hint", user.idToken?.tokenValue)
val logoutResponse = restTemplate.getForEntity(
builder.toUriString(), String::class.java
)
if (logoutResponse.statusCode.is2xxSuccessful) {
logger.info("Successfully logged out from Keycloak")
} else {
logger.error("Could not propagate logout to Keycloak")
}
}
}
这是我的应用程序.yml :
server:
port: 8081
keycloak:
realm: http://localhost:8082/realms/StroffosRealm
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${keycloak.realm}
client:
registration:
keycloak:
client-id: authenticator-app
authorization-grant-type: authorization_code
scope: openid
provider:
keycloak:
issuer-uri: ${keycloak.realm}
user-name-attribute: preferred_username
问题所在
该应用程序在大多数情况下都能完美运行,直到您尝试在此配置文件页面中注销:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your Profile</title>
</head>
<body>
<h1>Hello, User!</h1>
<p>This is your profile</p>
<a href="/logout"><button>Logout</button></a>
</body>
</html>
实际的注销请求从正确注销当前会话的KeycloakLogoutHandler
正确发送,并且在我再次重新登录之前不允许我访问配置文件页面。
但是我要显示的注销确认页面显示内部服务器错误 500,因为KeycloakLogoutHandler
的logout()
函数中的auth
参数给了我错误:
java.lang.NullPointerException: null cannot be cast to non-null type org.springframework.security.oauth2.core.oidc.user.OidcUser
但即使我试图通过auth
简单地通过放置
if (auth == null) return
控制作为logout()
函数的第一条指令,但每当 I 或 T注销按钮尝试重定向到注销 HTML 页面时,结果是重定向过多错误
我认为是主要问题
每当我单击Logout
按钮时,都会将我发送到localhost:8081/logout
URL,并且@Controller
中的映射首次执行request.logout()
指令,使我的当前会话无效并清空身份验证信息。 一旦这样做,它应该返回注销HTML页面,但它没有,因为一旦注销成功,就像它在SecurityConfig中编写的那样,重定向URL也会被localost:8081/logout
,因此它将第二次执行request.logout()
指令,但这次找到一个空的auth
参数。
我尝试过什么
从SecurityConfig中删除.logoutSuccessUrl("/logout")
是我的第一个想法,但是一旦执行注销,它就会将我重定向到默认的Spring安全注销页面
我显然不希望它发生。
更新
我已经清理了我的@Controller
和我的安全配置:
@Controller
class MainController {
@GetMapping("/profile")
fun getProfile() = "profile"
@GetMapping("/logout")
fun logoutPage() = "logout"
}
@Configuration
@EnableWebSecurity
class SecurityConfig(keycloakLogoutHandler: KeycloakLogoutHandler?) {
private final val keycloakLogoutHandler: KeycloakLogoutHandler = keycloakLogoutHandler!!
@Bean
@Throws(Exception::class)
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeHttpRequests()
.requestMatchers("/profile").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login()
.and()
.logout()
.logoutRequestMatcher(AntPathRequestMatcher("/logoutRequest"))
.addLogoutHandler(keycloakLogoutHandler)
.logoutSuccessUrl("/logout")
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer<HttpSecurity>::jwt)
http.csrf().disable()
return http.build()
}
}
现在,每当在/logoutRequest
URL发送请求时,它都会被威胁为注销请求,并会正确地将我重定向到注销页面。
不再发生ERR_TOO_MANY_REDIRECTS
,但现在它显示的东西而不是我的自定义注销确认页面是 Spring 安全性提供的默认页面,我不想要。
不确定,但尝试在.logout() 之后的链中添加 .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
编辑:
引用文档:
使用 HttpServletRequest.logout() 选项时,适配器执行 针对 Keycloak 服务器的反向通道 POST 呼叫通过 刷新令牌。如果该方法从未受保护的页面 (a 不检查有效令牌的页面)刷新令牌可以是 不可用,在这种情况下,适配器将跳过调用。为此 原因,使用受保护的页面执行 HttpServletRequest.logout() 建议始终考虑当前令牌 如果需要,可以执行与Keycloak服务器的交互。
似乎您必须将控制器的映射更改为 POST,并使 HTML 页面中的按钮看起来有点
<form th:action="@{/logout}" method="post">
<input type="submit" th:value="LogOut" class="btn btn-warning"/>
* 在这个例子中使用了 Bootstrap CSS 和 Thymeleaf您可以尝试将处理注销逻辑的端点和显示成功注销的端点分开。
例如,您可以有一个/logout-success
端点,该端点返回所需的 html,告诉用户注销成功,并在一段时间后将用户重定向到主页。
您需要将.logoutSuccessUrl("/logout-success")
配置为在成功注销后重定向用户。