복붙노트

[SPRING] 봄 보안. 사용자를 로그 아웃하는 방법 (oauth2 토큰 취소)

SPRING

봄 보안. 사용자를 로그 아웃하는 방법 (oauth2 토큰 취소)

로그 아웃하려면 다음 코드를 호출하십시오.

request.getSession().invalidate();
SecurityContextHolder.getContext().setAuthentication(null);

하지만 (오래된 oauth 토큰을 사용하는 다음 요청에서) 호출 한 후

SecurityContextHolder.getContext (). getAuthentication ();

거기에 내 옛날 사람이 보인다.

그것을 고치는 방법?

해결법

  1. ==============================

    1.여기 내 구현 (봄 OAuth2) :

    여기 내 구현 (봄 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
    
  2. ==============================

    2.camposer의 응답은 Spring OAuth에서 제공하는 API를 사용하여 향상시킬 수 있습니다. 실제로 HTTP 헤더에 직접 액세스 할 필요는 없지만 액세스 토큰을 제거하는 REST 메서드는 다음과 같이 구현할 수 있습니다.

    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;
    }
    

    또한 스프링 보안 로그 아웃 필터의 끝점에 리디렉션을 추가하여 세션이 무효화되고 클라이언트가 / oauth / authorize 끝점에 액세스하기 위해 자격 증명을 다시 제공해야합니다.

  3. ==============================

    3.그것은 당신이 사용하고있는 oauth2 'grant type'의 타입에 달려 있습니다.

    그것은 당신이 사용하고있는 oauth2 'grant type'의 타입에 달려 있습니다.

    귀하의 클라이언트 응용 프로그램에서 Spring의 @ EnableOAuth2Sso를 사용했다면 가장 일반적인 것이 'Authorization Code'입니다. 이 경우 Spring 보안은 로그인 요청을 'Authorization Server'로 리디렉션하고 'Authorization Server'에서 수신 한 데이터로 클라이언트 애플리케이션에 세션을 생성합니다.

    클라이언트 응용 프로그램 호출 / 로그 아웃 끝점에서 세션을 쉽게 파괴 할 수 있지만 클라이언트 응용 프로그램은 사용자를 다시 '인증 서버'로 보내고 다시 기록합니다.

    클라이언트 응용 프로그램에서 로그 아웃 요청을 가로채는 메커니즘을 만들고이 서버 코드에서 토큰을 무효화하기 위해 "인증 서버"를 호출하는 방법을 제안합니다.

    우리가 필요로하는 첫 번째 변경 사항은 사용자의 access_token을 무효화하기 위해 Claudio Tasso가 제안한 코드를 사용하여 권한 서버에서 하나의 엔드 포인트를 작성하는 것입니다.

    @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 웹 토큰을 사용하는 경우 토큰이 권한 서버에 의해 관리되지 않으므로 JWT 웹 토큰을 사용할 수 없습니다.

  4. ==============================

    4.토큰 스토어 구현까지.

    토큰 스토어 구현까지.

    JDBC 토큰 스트로크를 사용하는 경우 테이블에서 제거해야합니다. 어쨌든 수동으로 엔드 포인트를 추가 / 로그 아웃 한 후 다음을 호출해야합니다.

    @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);
            }
    }
    
  5. ==============================

    5. 태그에 다음 행을 추가하십시오.

    태그에 다음 행을 추가하십시오.

    <logout invalidate-session="true" logout-url="/logout" delete-cookies="JSESSIONID" />
    

    그러면 JSESSIONID가 삭제되고 세션이 무효화됩니다. 로그 아웃 버튼이나 라벨에 대한 링크는 다음과 같습니다.

    <a href="${pageContext.request.contextPath}/logout">Logout</a>
    

    편집하다: 자바 코드에서 세션을 무효화하려고합니다. 사용자를 로그 아웃하기 전에 작업을 수행하고 세션을 무효화해야한다고 가정합니다. 이것이 유스 케이스라면 custome logout handler를 사용해야한다. 자세한 내용은이 사이트를 방문하십시오.

  6. ==============================

    6.이 작업은 Keycloak Confidential Client 로그 아웃에서 작동합니다. kecloak에있는 사람들이 자바가 아닌 웹 클라이언트와 그 종점에 대해 더 강력한 문서를 가지고 있지 않은 이유를 모르겠다. 오픈 소스 라이브러리가있는 짐승의 본질이라고 생각한다. 나는 그들의 코드에서 약간의 시간을 보냈다.

    이 작업은 Keycloak Confidential Client 로그 아웃에서 작동합니다. kecloak에있는 사람들이 자바가 아닌 웹 클라이언트와 그 종점에 대해 더 강력한 문서를 가지고 있지 않은 이유를 모르겠다. 오픈 소스 라이브러리가있는 짐승의 본질이라고 생각한다. 나는 그들의 코드에서 약간의 시간을 보냈다.

        //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 );
    }
    
  7. ==============================

    7.사용자 작곡가가 제공하는 솔루션이 나를 위해 완벽하게 작동했습니다. 다음과 같이 코드를 약간 변경했습니다.

    사용자 작곡가가 제공하는 솔루션이 나를 위해 완벽하게 작동했습니다. 다음과 같이 코드를 약간 변경했습니다.

    @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 포인터 예외가 발생하므로이 작업을 수행했습니다.

  8. ==============================

    8.프로그래밍 방식으로 다음과 같이 로그 아웃 할 수 있습니다.

    프로그래밍 방식으로 다음과 같이 로그 아웃 할 수 있습니다.

    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);
    }
    
  9. from https://stackoverflow.com/questions/21987589/spring-security-how-to-log-out-user-revoke-oauth2-token by cc-by-sa and MIT license