📤Logout trong ứng dụng sử dụng bảo mật Oauth

Tổng quan

Trong hướng dẫn nhanh này, chúng ta sẽ thấy cách thêm chức năng đăng xuất vào ứng dụng Spring Security sử dụng OAuth.

Chúng ta sẽ xem một số cách để làm điều này. Đầu tiên, chúng ta sẽ thấy cách đăng xuất người dùng Keycloak khỏi ứng dụng OAuth như đã mô tả trong hướng dẫn "Tạo một REST API với OAuth2", và sau đó, sử dụng proxy Zuul mà chúng ta đã thấy trước đó.

Chúng ta sẽ sử dụng ngăn xếp OAuth trong Spring Security 5. Nếu bạn muốn sử dụng ngăn xếp OAuth cũ của Spring Security, hãy xem bài viết trước đây này: "Đăng xuất trong một ứng dụng được bảo mật bằng OAuth (sử dụng ngăn xếp cũ)".

Logout Using Front-End Application

Vì Access Tokens được quản lý bởi Authorization Server, chúng cần được vô hiệu hóa ở mức đó. Các bước chính xác để làm điều này sẽ khác nhau một chút tùy thuộc vào Authorization Server bạn đang sử dụng.

Trong ví dụ của chúng ta, theo tài liệu Keycloak, để đăng xuất trực tiếp từ ứng dụng trình duyệt, chúng ta có thể chuyển hướng trình duyệt đến http://auth-server/auth/realms/{realm-name}/protocol/openid-connect/logout?redirect_uri=encodedRedirectUri.

Bên cạnh việc gửi URI chuyển hướng, chúng ta cũng cần truyền id_token_hint cho API đăng xuất của Keycloak. Điều này nên chứa giá trị id_token đã được mã hóa.

Hãy nhớ lại cách chúng ta đã lưu trữ access_token, chúng ta cũng sẽ lưu trữ id_token tương tự:

saveToken(token) {
  var expireDate = new Date().getTime() + (1000 * token.expires_in);
  Cookie.set("access_token", token.access_token, expireDate);
  Cookie.set("id_token", token.id_token, expireDate);
  this._router.navigate(['/']);
}

Quan trọng nhất là, để nhận được ID Token trong phần tử payload của Authorization Server, chúng ta nên bao gồm openid trong tham số scope.

Bây giờ hãy xem quá trình đăng xuất hoạt động.

Chúng ta sẽ chỉnh sửa hàm logout trong App Service:

logout() {
  let token = Cookie.get('id_token');
  Cookie.delete('access_token');
  Cookie.delete('id_token');
  let logoutURL = "http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/logout?
    id_token_hint=" + token + "&post_logout_redirect_uri=" + this.redirectUri;

  window.location.href = logoutURL;
}

Ngoài việc chuyển hướng, chúng ta cũng cần loại bỏ Access và ID Tokens mà chúng ta đã nhận từ Authorization Server.

Vì vậy, trong đoạn mã trên, trước tiên chúng ta đã xóa các token, sau đó mới chuyển hướng trình duyệt đến API đăng xuất của Keycloak.

Đáng chú ý, chúng ta đã truyền vào redirect URI là http://localhost:8089/ ↗ - URI mà chúng ta sử dụng trong toàn bộ ứng dụng - vì vậy chúng ta sẽ đến trang đích sau khi đăng xuất.

Việc xóa Access, ID, và Refresh Tokens tương ứng với phiên làm việc hiện tại được thực hiện tại phía Authorization Server. Trong trường hợp này, ứng dụng trình duyệt của chúng ta không lưu trữ Refresh Token.

Logout Using Zuul Proxy

Trong một bài viết trước về Xử lý Refresh Token, chúng ta đã thiết lập ứng dụng để có thể làm mới Access Token bằng cách sử dụng Refresh Token. Hiện thực này sử dụng một proxy Zuul với các bộ lọc tùy chỉnh.

Ở đây, chúng ta sẽ xem cách thêm chức năng đăng xuất vào đoạn mã trên.

Lần này, chúng ta sẽ sử dụng một API Keycloak khác để đăng xuất người dùng. Chúng ta sẽ gọi POST lên đích đến đăng xuất để đăng xuất một phiên thông qua việc gọi không thông qua trình duyệt, thay vì sử dụng chuyển hướng URL chúng ta đã sử dụng trong phần trước.

Define Route for Logout

Để bắt đầu, hãy thêm một đường dẫn khác vào proxy trong file application.yml của chúng ta:

zuul:
  routes:
    //...
    auth/refresh/revoke:
      path: /auth/refresh/revoke/**
      sensitiveHeaders:
      url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/logout
    
    //auth/refresh route

Thực tế, chúng ta đã thêm một đường dẫn con vào đường dẫn auth/refresh đã tồn tại. Quan trọng là chúng ta phải thêm đường dẫn con trước đường dẫn chính, nếu không, Zuul sẽ luôn ánh xạ URL của đường dẫn chính.

Chúng ta đã thêm một đường dẫn con thay vì một đường dẫn chính để có quyền truy cập vào cookie refreshToken chỉ bằng HTTP, được thiết lập với một đường dẫn rất hạn chế là /auth/refresh (và các đường dẫn con của nó). Chúng ta sẽ thấy tại sao chúng ta cần cookie trong phần tiếp theo.

POST to Authorization Server's /logout

Bây giờ, hãy tăng cường việc thực hiện của CustomPreZuulFilter để chặn URL /auth/refresh/revoke và thêm thông tin cần thiết để truyền cho Authorization Server.

Các tham số form yêu cầu cho việc đăng xuất tương tự như yêu cầu Refresh Token, trừ việc không có grant_type:

@Component 
public class CustomPostZuulFilter extends ZuulFilter { 
    //... 
    @Override 
    public Object run() { 
        //...
        if (requestURI.contains("auth/refresh/revoke")) {
            String cookieValue = extractCookie(req, "refreshToken");
            String formParams = String.format("client_id=%s&client_secret=%s&refresh_token=%s", 
              CLIENT_ID, CLIENT_SECRET, cookieValue);
            bytes = formParams.getBytes("UTF-8");
        }
        //...
    }
}

Ở đây, chúng ta đơn giản là trích xuất cookie refreshToken và gửi nó trong required formParams.

Remove the Refresh Token

Khi vô hiệu hóa Access Token bằng cách sử dụng chuyển hướng đăng xuất như chúng ta đã thấy trước đó, Refresh Token liên quan đến nó cũng bị vô hiệu hóa bởi Authorization Server.

Tuy nhiên, trong trường hợp này, cookie httpOnly vẫn được đặt trên Client. Vì chúng ta không thể xóa nó bằng JavaScript, chúng ta cần xóa nó từ phía máy chủ.

Vì vậy, hãy thêm vào việc thực hiện của CustomPostZuulFilter để chặn URL /auth/refresh/revoke để nó sẽ xóa cookie refreshToken khi gặp URL này:

@Component
public class CustomPostZuulFilter extends ZuulFilter {
    //...
    @Override
    public Object run() {
        //...
        String requestMethod = ctx.getRequest().getMethod();
        if (requestURI.contains("auth/refresh/revoke")) {
            Cookie cookie = new Cookie("refreshToken", "");
            cookie.setMaxAge(0);
            ctx.getResponse().addCookie(cookie);
        }
        //...
    }
}

Remove the Access Token from the Angular Client

Ngoài việc vô hiệu hóa Refresh Token, cookie access_token cũng cần được xóa khỏi phía máy khách.

Hãy thêm một phương thức vào Angular controller của chúng ta để xóa cookie access_token và gọi đến /auth/refresh/revoke POST mapping:

logout() {
  let headers = new HttpHeaders({
    'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'});
  
  this._http.post('auth/refresh/revoke', {}, { headers: headers })
    .subscribe(
      data => {
        Cookie.delete('access_token');
        window.location.href = 'http://localhost:8089/';
        },
      err => alert('Could not logout')
    );
}

Hàm này sẽ được gọi khi nhấp vào nút Đăng xuất (Logout):

<a class="btn btn-default pull-right"(click)="logout()" href="#">Logout</a>

Last updated