ログアウトしたいときは、次のコードを呼び出します。
_request.getSession().invalidate();
SecurityContextHolder.getContext().setAuthentication(null);
_
しかし、その後(古いoauthトークン)を使用した次のリクエストで)呼び出します
SecurityContextHolder.getContext().getAuthentication();
そこに古いユーザーが表示されます。
修正方法
これが私の実装です(Spring OAuth2):
@Controller
public class OAuthController {
@Autowired
private TokenStore tokenStore;
@RequestMapping(value = "/oauth/revoke-token", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
public void logout(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null) {
String tokenValue = authHeader.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
}
}
}
検査用の:
curl -X GET -H "Authorization: Bearer $TOKEN" http://localhost:8080/backend/oauth/revoke-token
camposer による応答は、Spring OAuthが提供するAPIを使用して改善できます。実際、HTTPヘッダーに直接アクセスする必要はありませんが、アクセストークンを削除するRESTメソッドは次のように実装できます。
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired
private ConsumerTokenServices consumerTokenServices;
@RequestMapping("/uaa/logout")
public void logout(Principal principal, HttpServletRequest request, HttpServletResponse response) throws IOException {
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal;
OAuth2AccessToken accessToken = authorizationServerTokenServices.getAccessToken(oAuth2Authentication);
consumerTokenServices.revokeToken(accessToken.getValue());
String redirectUrl = getLocalContextPathUrl(request)+"/logout?myRedirect="+getRefererUrl(request);
log.debug("Redirect URL: {}",redirectUrl);
response.sendRedirect(redirectUrl);
return;
}
また、Spring Securityログアウトフィルターのエンドポイントにリダイレクトを追加したため、セッションが無効になり、クライアントは/ oauth/authorizeエンドポイントにアクセスするために資格情報を再度提供する必要があります。
使用しているoauth2 'grant type'のタイプによって異なります。
クライアントアプリでスプリングの@EnableOAuth2Sso
を使用している場合、最も一般的なのは「認証コード」です。この場合、Springセキュリティはログイン要求を「承認サーバー」にリダイレクトし、「承認サーバー」から受信したデータを使用してクライアントアプリにセッションを作成します。
/logout
エンドポイントを呼び出しているクライアントアプリでセッションを簡単に破棄できますが、クライアントアプリはユーザーを再度「承認サーバー」に送信し、再度ログを返します。
クライアントアプリでログアウトリクエストをインターセプトし、このサーバーコードから「承認サーバー」を呼び出してトークンを無効にするメカニズムを作成することを提案します。
最初に必要な変更は、承認サーバーでエンドポイントを1つ作成し、 Claudio Tasso によって提案されたコードを使用して、ユーザーのaccess_tokenを無効にすることです。
@Controller
@Slf4j
public class InvalidateTokenController {
@Autowired
private ConsumerTokenServices consumerTokenServices;
@RequestMapping(value="/invalidateToken", method= RequestMethod.POST)
@ResponseBody
public Map<String, String> logout(@RequestParam(name = "access_token") String accessToken) {
LOGGER.debug("Invalidating token {}", accessToken);
consumerTokenServices.revokeToken(accessToken);
Map<String, String> ret = new HashMap<>();
ret.put("access_token", accessToken);
return ret;
}
}
次に、クライアントアプリでLogoutHandler
を作成します。
@Slf4j
@Component
@Qualifier("mySsoLogoutHandler")
public class MySsoLogoutHandler implements LogoutHandler {
@Value("${my.oauth.server.schema}://${my.oauth.server.Host}:${my.oauth.server.port}/oauth2AuthorizationServer/invalidateToken")
String logoutUrl;
@Override
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
LOGGER.debug("executing MySsoLogoutHandler.logout");
Object details = authentication.getDetails();
if (details.getClass().isAssignableFrom(OAuth2AuthenticationDetails.class)) {
String accessToken = ((OAuth2AuthenticationDetails)details).getTokenValue();
LOGGER.debug("token: {}",accessToken);
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("access_token", accessToken);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "bearer " + accessToken);
HttpEntity<String> request = new HttpEntity(params, headers);
HttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter();
HttpMessageConverter stringHttpMessageConverternew = new StringHttpMessageConverter();
restTemplate.setMessageConverters(Arrays.asList(new HttpMessageConverter[]{formHttpMessageConverter, stringHttpMessageConverternew}));
try {
ResponseEntity<String> response = restTemplate.exchange(logoutUrl, HttpMethod.POST, request, String.class);
} catch(HttpClientErrorException e) {
LOGGER.error("HttpClientErrorException invalidating token with SSO authorization server. response.status code: {}, server URL: {}", e.getStatusCode(), logoutUrl);
}
}
}
}
そして、それをWebSecurityConfigurerAdapter
で登録します:
@Autowired
MySsoLogoutHandler mySsoLogoutHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.logout()
.logoutSuccessUrl("/")
// using this antmatcher allows /logout from GET without csrf as indicated in
// https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#csrf-logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
// this LogoutHandler invalidate user token from SSO
.addLogoutHandler(mySsoLogoutHandler)
.and()
...
// @formatter:on
}
注:JWT Webトークンを使用している場合、トークンは許可サーバーによって管理されていないため、JWT Webトークンを無効にすることはできません。
トークンストアの実装次第です。
[〜#〜] jdbc [〜#〜]トークンストロークを使用する場合は、テーブルから削除する必要があります...とにかく/ logoutエンドポイントを手動で追加して、これを呼び出す必要があります。
@RequestMapping(value = "/logmeout", method = RequestMethod.GET)
@ResponseBody
public void logmeout(HttpServletRequest request) {
String token = request.getHeader("bearer ");
if (token != null && token.startsWith("authorization")) {
OAuth2AccessToken oAuth2AccessToken = okenStore.readAccessToken(token.split(" ")[1]);
if (oAuth2AccessToken != null) {
tokenStore.removeAccessToken(oAuth2AccessToken);
}
}
<http></http>
タグに次の行を追加します。
<logout invalidate-session="true" logout-url="/logout" delete-cookies="JSESSIONID" />
これにより、JSESSIONIDが削除され、セッションが無効になります。ログアウトボタンまたはラベルへのリンクは次のようになります。
<a href="${pageContext.request.contextPath}/logout">Logout</a>
編集:Javaコードからセッションを無効にします。ユーザーをログアウトする直前に何らかのタスクを実行し、セッションを無効にする必要があると仮定します。カスタムログアウトハンドラー詳細については、 this サイトにアクセスしてください。
これは、Keycloak Confidential Clientログアウトに対して機能します。 keycloakの人々がJava非Webクライアントとそれらのエンドポイント一般に関するより堅牢なドキュメントを持たない理由がわからない。 。私は彼らのコードで少し時間を費やさなければなりませんでした:
//requires a Keycloak Client to be setup with Access Type of Confidential, then using the client secret
public void executeLogout(String url){
HttpHeaders requestHeaders = new HttpHeaders();
//not required but recommended for all components as this will help w/t'shooting and logging
requestHeaders.set( "User-Agent", "Keycloak Thick Client Test App Using Spring Security OAuth2 Framework");
//not required by undertow, but might be for Tomcat, always set this header!
requestHeaders.set( "Accept", "application/x-www-form-urlencoded" );
//the keycloak logout endpoint uses standard OAuth2 Basic Authentication that inclues the
//Base64-encoded keycloak Client ID and keycloak Client Secret as the value for the Authorization header
createBasicAuthHeaders(requestHeaders);
//we need the keycloak refresh token in the body of the request, it can be had from the access token we got when we logged in:
MultiValueMap<String, String> postParams = new LinkedMultiValueMap<String, String>();
postParams.set( OAuth2Constants.REFRESH_TOKEN, accessToken.getRefreshToken().getValue() );
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(postParams, requestHeaders);
RestTemplate restTemplate = new RestTemplate();
try {
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
System.out.println(response.toString());
} catch (HttpClientErrorException e) {
System.out.println("We should get a 204 No Content - did we?\n" + e.getMessage());
}
}
//has a hard-coded client ID and secret, adjust accordingly
void createBasicAuthHeaders(HttpHeaders requestHeaders){
String auth = keycloakClientId + ":" + keycloakClientSecret;
byte[] encodedAuth = Base64.encodeBase64(
auth.getBytes(Charset.forName("US-ASCII")) );
String authHeaderValue = "Basic " + new String( encodedAuth );
requestHeaders.set( "Authorization", authHeaderValue );
}
ユーザーが提供するソリューションcomposer完璧に機能しました。次のようにコードにいくつかの小さな変更を加えました。
@Controller
public class RevokeTokenController {
@Autowired
private TokenStore tokenStore;
@RequestMapping(value = "/revoke-token", method = RequestMethod.GET)
public @ResponseBody ResponseEntity<HttpStatus> logout(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null) {
try {
String tokenValue = authHeader.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
} catch (Exception e) {
return new ResponseEntity<HttpStatus>(HttpStatus.NOT_FOUND);
}
}
return new ResponseEntity<HttpStatus>(HttpStatus.OK);
}
}
同じアクセストークンを再度無効にしようとすると、Null Pointer例外がスローされるため、これを行いました。