Printemps de sécurité. Comment déconnecter l'utilisateur (révoquer oauth2 jeton)
quand je veux me déconnecter, j'invoque ce code:
request.getSession().invalidate();
SecurityContextHolder.getContext().setAuthentication(null);
mais après cela (dans la requête suivante en utilisant le vieux token oauth) j'invoque
SecurityContextHolder.getContext().getAuthentication();
comment le réparer?
8 réponses
@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);
}
}
}
Pour tester:
curl -X GET -H "Authorization: Bearer $TOKEN" http://localhost:8080/backend/oauth/revoke-token
La réponse donnée par camposer peut être amélioré en utilisant L'API fournie par Spring OAuth. En fait, il n'est pas nécessaire d'accéder directement aux en-têtes HTTP, mais la méthode REST qui supprime le token d'accès peut être implémentée comme suit:
@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;
}
j'ai aussi ajouté une redirection vers le point final du filtre de sécurité de Spring, de sorte que la session est invalidée et que le client doit fournir des justificatifs d'identité à nouveau afin d'accéder au point final /oauth/authorizate.
Cela dépend de l'implémentation de votre Token Store.
Si vous utilisez JDBC coup de jeton alors vous avez juste besoin de le retirer de la table... Quoi qu'il en soit, vous devez ajouter /logout endpoint manuellement puis appeler ceci :
@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);
}
}
Cela dépend du type de oauth2 'type de subvention que vous utilisez.
Le plus commun, si vous avez utilisé du printemps @EnableOAuth2Sso
dans votre application client est 'Code D'autorisation'. Dans ce cas, Spring security redirige la demande de connexion vers le 'serveur D'autorisation' et crée une session dans votre application client avec les données reçues de 'serveur D'autorisation'.
vous pouvez facilement détruire votre session à l'application client appelant /logout
point de terminaison, mais ensuite l'application client envoie de nouveau à 'serveur d'autorisation' et renvoie à nouveau connecté.
je propose de créer un mécanisme pour intercepter la demande de déconnexion à l'application client et à partir de ce code serveur, appeler "serveur d'autorisation" pour invalider le token.
le premier changement dont nous avons besoin est de créer un endpoint au niveau du serveur d'autorisation, en utilisant le code proposé par Claudio Tasso, pour invalider l'utilisateur 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;
}
}
Puis à l'application client, créer un 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);
}
}
}
}
Et de faire enregistrer 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
}
Une REMARQUE: Si vous utilisez des tokens web JWT, vous ne pouvez pas les invalider, car le token n'est pas géré par le serveur d'autorisation.
ajouter la ligne suivante dans <http></http>
balise.
<logout invalidate-session="true" logout-url="/logout" delete-cookies="JSESSIONID" />
ceci supprimera jsessionid et invalidera la session. Et le lien vers le bouton de déconnexion ou l'étiquette serait quelque chose comme:
<a href="${pageContext.request.contextPath}/logout">Logout</a>
modifier: Vous voulez invalider session du code java. Je suppose que vous devez effectuer une tâche juste avant de déconnecter l'utilisateur, puis invalider la session. Si c'est le cas, vous devez utiliser custome déconnexion des gestionnaires. Visitez le site site pour plus d'informations.
cela fonctionne pour Keycloak Confidential Client logout. Je n'ai aucune idée pourquoi les gens de keycloak n'ont pas de docs plus robustes sur les clients java non-web et leurs points de fin en général, je suppose que c'est la nature de la bête avec libs open source. J'ai dû passer un peu de temps dans leur code:
//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 );
}
Solution fournie par l'utilisateur éditeur parfaitement fonctionné pour moi. J'ai fait quelques petites modifications au code comme suit,
@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);
}
}
j'ai fait cela parce que si vous essayez d'invalider à nouveau le même token d'accès, il lance L'exception du pointeur Null.
par programmation, vous pouvez vous connecter de cette façon:
public void logout(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
new SecurityContextLogoutHandler().logout(request, response, auth);
}
SecurityContextHolder.getContext().setAuthentication(null);
}