Xem bài viết gốc tại đây

Bạn có thể sử dụng bài giảng này để hiểu Spring Security là gì và các tính năng cốt lõi như authentication, authorization hay bảo vệ khai thác chung hoạt động như thế nào. Ngoài ra, còn có các câu hỏi thường hay gặp mà bao hàm toàn bộ nội dung.

Lưu ý của người biên tập : Với khoảng 6500 từ, bạn có thể sẽ không muốn đọc trên thiết bị di động. Hãy đánh dấu trang và quay lại sau

 

Giới thiệu

Không sớm thì muộn, mọi người đều cần phải thêm bảo mật cho dự án của mình và trong hệ sinh thái Spring bạn có thể thực hiện việc đó với sự trợ giúp của thư viện Spring Security .

Vì thế khi bạn mới bắt đầu thêm Spring Security vào dự án Spring Boot của bạn và đột nhiên…

  • … Bạn có các trang login được tạo tự động.
  • … Bạn không thể thực hiện các method POST được nữa.
  • … Toàn bộ ứng dụng của bạn bị đóng và nó yêu cầu bạn nhập username và password.

Sau khi sống sót qua khỏi sự suy sụp tinh thần đó, bạn mới bắt đầu quan tâm tìm hiểu cách những thứ này hoạt động ra sao.

Spring Security là gì và nó hoạt động như thế nào?

Câu trả lời ngắn gọn :

Về cốt lõi, Spring Security thực sự chỉ là một loạt các bộ lọc servlet giúp bạn thêm authentication và authorization vào ứng dụng web của mình.

Nó cũng tích hợp tốt với các framework như Spring Web MVC (hay còn gọi là Spring Boot ), cũng như với các tiêu chuẩn như OAuth2 hoặc SAML. Và nó tự động tạo các trang login / logout và bảo vệ chống lại các hành vi khai thác thông tin như CSRF.

Đến đây vẫn chưa giúp ích gì cho bạn phải không?

May mắn thay, cũng có một câu trả lời dài:

Chính là phần còn lại của bài viết này.

 

Bảo mật ứng dụng web: 101

Trước khi trở thành bậc thầy Spring Security, bạn cần hiểu ba khái niệm quan trọng:

  1. Authentication
  2. Authorization
  3. Bộ lọc Servlet

Lời khuyên: Đừng bỏ qua phần này, vì nó là nền tảng cho mọi thứ mà Spring Security thực hiện. Ngoài ra, tôi sẽ làm cho thú vị nhất có thể.

1. Authentication (xác thực)

Trước hết, nếu bạn đang chạy một ứng dụng (web) điển hình, bạn cần user của mình authenticate. Điều đó có nghĩa là ứng dụng của bạn cần xác minh xem user có phải là người mà anh ta thừa nhận đúng là anh ta hay không, thường được thực hiện bằng kiểm tra username và password.

User : "Tôi là tổng thống Hoa Kỳ. username của tôi là: potus!"

Ứng dụng web của bạn : "Chắc chắn rồi, thế còn password thì sao, thưa ngài Tổng thống?"

User : "Pass của tôi là: th3don4ld".

Ứng dụng web của bạn : "Đúng. Chào mừng, thưa ngài!"

 

2. Authorization (Phân quyền)

Trong các ứng dụng đơn giản hơn, chỉ cần authenticate là đủ: Ngay sau khi người dùng authenticate, họ có thể truy cập từng phần của ứng dụng.

Nhưng hầu hết các ứng dụng đều có khái niệm về quyền (hoặc vai trò). Hãy tưởng tượng: khách hàng có quyền truy cập vào giao diện công khai của webshop của bạn và quản trị viên có quyền truy cập vào khu vực quản trị riêng biệt.

Cả hai loại user đều cần đăng nhập, nhưng thực tế authentication đơn thuần không nói lên gì về việc họ được phép làm gì trong hệ thống của bạn. Do đó, bạn cũng cần phải kiểm tra các quyền của user đã được authenticate, tức là bạn cần cấp quyền cho người dùng đó.

User : "Hãy để tôi chơi với quả bóng hạt nhân đó…."

Ứng dụng web của bạn : "Đợi một giây, tôi cần kiểm tra lại permission của ngài trước… Dạ thưa Chủ tịch, ngài có đủ điều kiện. Hãy cứ tận hưởng”

User : "Lại cái nút đỏ đó là gì vậy... ??"

 

3. Bộ lọc Servlet

Cuối cùng nhưng không kém phần quan trọng, chúng ta hãy xem xét Bộ lọc Servlet. Chúng liên quan gì đến authentication và authorization?

Tại sao sử dụng Bộ lọc Servlet?

Hãy nhớ lại bài viết khác của tôi , nơi chúng tôi phát hiện ra rằng về cơ bản bất kỳ ứng dụng web Spring nào cũng chỉ là một servlet: DispatcherServlet cũ của Spring, giúp chuyển hướng các yêu cầu HTTP đến (ví dụ từ trình duyệt) đến @Controllers hoặc @RestControllers của bạn.

Vấn đề là: Không có mật mã bảo mật nào được mã hóa trong DispatcherServlet đó và bạn cũng rất có thể không muốn mò mẫm với header HTTP Basic Auth thô sơ trong @Controllers của mình. Để cho tối ưu, việc authentication và authorization nên được thực hiện trước khi một request truy cập vào @Controllers của bạn.

May mắn thay, có một cách để thực hiện chính xác điều này trong thế giới web Java: bạn có thể đặt bộ lọc lên trước các servlet, tức là bạn có thể viết SecurityFilter và cấu hình nó trong Tomcat (servlet container/ application server) của bạn để lọc mọi request HTTP trước khi nó truy cập vào servlet của bạn.

Một SecurityFilter đơn giản

Một SecurityFilter có khoảng 4 tác vụ và một cách triển khai đơn giản quá mức có thể trông như thế này:

import javax.servlet.*;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class SecurityServletFilter extends HttpFilter {

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

        UsernamePasswordToken token = extractUsernameAndPasswordFrom(request);  // (1)

        if (notAuthenticated(token)) {  // (2)
            // either no or wrong username/password
            // unfortunately the HTTP status code is called "unauthorized", instead of "unauthenticated"
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 401.
            return;
        }

        if (notAuthorized(token, request)) { // (3)
            // you are logged in, but don't have the proper rights
            response.setStatus(HttpServletResponse.SC_FORBIDDEN); // HTTP 403
            return;
        }

        // allow the HttpRequest to go to Spring's DispatcherServlet
        // and @RestControllers/@Controllers.
        chain.doFilter(request, response); // (4)
    }

    private UsernamePasswordToken extractUsernameAndPasswordFrom(HttpServletRequest request) {
        // Either try and read in a Basic Auth HTTP Header, which comes in the form of user:password
        // Or try and find form login request parameters or POST bodies, i.e. "username=me" & "password="myPass"
        return checkVariousLoginOptions(request);
    }


    private boolean notAuthenticated(UsernamePasswordToken token) {
        // compare the token with what you have in your database...or in-memory...or in LDAP...
        return false;
    }

    private boolean notAuthorized(UsernamePasswordToken token, HttpServletRequest request) {
       // check if currently authenticated user has the permission/role to access this request's /URI
       // e.g. /admin needs a ROLE_ADMIN , /callcenter needs ROLE_CALLCENTER, etc.
       return false;
    }
}
  1. Đầu tiên, bộ lọc cần extract username/password từ request. Nó có thể thông qua Basic Auth Http Header, hoặc các field trong form, hoặc cookie, v.v.

  2. Sau đó, bộ lọc cần xác thực bằng cách đối chiếu tổ hợp username/password đó với một thứ gì đó , chẳng hạn như database.

  3. Sau khi authenticate thành công, bộ lọc cần kiểm tra user có được phép truy cập requested URI hay không.

  4. Nếu request vẫn tồn tại sau tất cả các lần kiểm tra này, thì bộ lọc có thể cho phép request chuyển đến DispatcherServlet của bạn, tức là @Controllers của bạn.

FilterChains

Xác minh trong thực tế: Trong khi code trên có thể hoạt động khi biên dịch, nó sớm hay muộn cũng dẫn đến một bộ lọc chất đầy hàng tấn code cho các cơ chế authentication và authorization khác nhau.

Trong thế giới thực, bạn sẽ chia bộ lọc này thành nhiều bộ lọc, sau đó bạn chain với nhau.

Ví dụ: một request HTTP sẽ…

  1. Đầu tiên, đi qua LoginMethodFilter…

  2. Sau đó, đi qua AuthenticationFilter…

  3. Sau đó, chuyển qua AuthorizationFilter…

  4. Cuối cùng, chạm vào servlet của bạn.

Khái niệm này được gọi là FilterChain và câu lệnh cuối cùng trong method ở bộ lọc phía trên của bạn thực sự đang authorize cho chính chain đó:

chain.doFilter(request, response);

Với một bộ lọc (chain) như vậy thì về cơ bản, bạn có thể xử lý mọi vấn đề authentication hay authorization có trong ứng dụng của mình mà không cần thay đổi việc triển khai ứng dụng thực tế của bạn (như là @RestControllers / @Controllers của bạn).

Với những kiến ​​thức đó, hãy cùng tìm hiểu Spring Security sử dụng phép thuật bộ lọc này như thế nào nhé.

 

FilterChain & Security Configuration DSL

Chúng ta sẽ bắt đầu đề cập đến Spring Security một chút khác thường, bằng cách đi theo hướng ngược lại từ chương trước, bắt đầu với FilterChain của Spring Security.

Spring's DefaultSecurityFilterChain

Giả sử bạn thiết lập Spring Security đúng cách và sau đó khởi động ứng dụng web của mình. Bạn sẽ thấy thông báo sau:

2020-02-25 10:24:27.875  INFO 11116 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@46320c9a, org.springframework.security.web.context.SecurityContextPersistenceFilter@4d98e41b, org.springframework.security.web.header.HeaderWriterFilter@52bd9a27, org.springframework.security.web.csrf.CsrfFilter@51c65a43, org.springframework.security.web.authentication.logout.LogoutFilter@124d26ba, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@61e86192, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@10980560, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32256e68, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52d0f583, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5696c927, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5f025000, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e7abaf7, org.springframework.security.web.session.SessionManagementFilter@681c0ae6, org.springframework.security.web.access.ExceptionTranslationFilter@15639d09, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4f7be6c8]|

Nếu bạn mở rộng một dòng đó thành một list, nó sẽ giống như Spring Security không chỉ cài đặt một bộ lọc, thay vào đó, nó cài đặt toàn bộ một filterchain bao gồm 15 (!) bộ lọc khác nhau.

Vì vậy, khi một HTTPRequest đến, nó sẽ đi qua tất cả 15 bộ lọc này, trước khi request của bạn cuối cùng truy cập vào @RestControllers của bạn. Thứ tự cũng quan trọng, bắt đầu từ trên cùng list đó và đi xuống đáy.

Phân tích FilterChain của Spring

Sẽ mất rất lâu để có một cái nhìn chi tiết về mọi bộ lọc của chain này, nhưng đây là lời giải thích cho một số bộ lọc đó. Vui lòng xem mã nguồn của Spring Security để hiểu các bộ lọc khác.

  • BasicAuthenticationFilter : Cố gắng tìm Basic Auth HTTP Header theo request và nếu tìm thấy, cố gắng xác thực người dùng bằng username và password của header.

  • UsernamePasswordAuthenticationFilter : Cố gắng tìm tham số request username/password hay POST body và nếu được tìm thấy, cố gắng authenticate user bằng các giá trị đó.

  • DefaultLoginPageGeneratingFilter : Tạo trang login cho bạn, nếu bạn không disable tính năng đó. Bộ lọc NÀY là lý do tại sao bạn nhận được trang đăng nhập mặc định khi bật Spring Security.

  • DefaultLogoutPageGeneratingFilter : Tạo trang logout cho bạn, nếu bạn không disable tính năng đó.

  • FilterSecurityInterceptor : Thực hiện authorization của bạn.

Vì vậy, với một số bộ lọc này, Spring Security cung cấp cho bạn trang login / logout, cũng như khả năng login bằng Basic Auth hoặc Form Logins, cũng như một số tính năng bổ sung như CsrfFilter, mà chúng ta sẽ tìm hiểu sau.

Nghỉ giữa hiệp : Những bộ lọc đó, phần lớn, chính  Spring Security. Không hơn, không kém. Chúng làm tất cả công việc. Những gì còn lại cho bạn là cấu hình cách chúng hoạt động, tức URL nào cần bảo vệ, URL nào cần bỏ qua và bảng cơ sở dữ liệu nào sẽ được sử dụng để authenticate.

Do đó, bước tiếp theo chúng ta cần xem xét cách cấu hình Spring Security.

Cách cấu hình Spring Security: WebSecurityConfigurerAdapter

Với các phiên bản Spring Security và/hoặc Spring Boot mới nhất, cách để cấu hình Spring Security là có một class:

  1. Được chú thích bằng @EnableWebSecurity.

  2. Extends WebSecurityConfigurer, về cơ bản cung cấp cho bạn một DSL/methods cấu hình. Với những phương pháp đó, bạn có thể chỉ định những URI nào trong ứng dụng của mình để bảo vệ hoặc những biện pháp bảo vệ nào cần bật/tắt.

Đây là giao diện của một WebSecurityConfigurerAdapter điển hình:

@Configuration
@EnableWebSecurity // (1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // (1)

  @Override
  protected void configure(HttpSecurity http) throws Exception {  // (2)
      http
        .authorizeRequests()
          .antMatchers("/", "/home").permitAll() // (3)
          .anyRequest().authenticated() // (4)
          .and()
       .formLogin() // (5)
         .loginPage("/login") // (5)
         .permitAll()
         .and()
      .logout() // (6)
        .permitAll()
        .and()
      .httpBasic(); // (7)
  }
}
  1. Một Spring @Configuration bình thường với chú thích @EnableWebSecurity, extends từ WebSecurityConfigurerAdapter.

  2. Bằng cách override phương thức cấu hình (HttpSecurity) của adapter, bạn sẽ có được một DSL nhỏ xinh để bạn có thể cấu hình FilterChain của mình.

  3. Tất cả các request sẽ đến và /home được cấp phép - người dùng không phải authenticate nữa. Bạn đang sử dụng antMatcher , có nghĩa là bạn cũng có thể sử dụng ký tự đại diện (*, \ * \ *,?) Trong chuỗi.

  4. Mọi request khác đều cần người dùng authenticate trước , tức là user cần login.

  5. Bạn đang để form login (username/password dưới dạng form), với loginPage tùy chỉnh ( /login ,tức là không phải trang được tạo tự động của Spring Security). Bất kỳ ai cũng có thể truy cập trang login mà không cần phải đăng nhập trước (permitAll; nếu không, chúng tôi sẽ có Catch-22!).

  6. Cũng tương tự như (5) nhưng là với trang logout

  7. Trên hết, bạn cũng đang cho phép Basic Auth, nghĩa là gửi một HTTP Basic Auth Header để authenticate.

Cách sử dụng DSL cấu hình của Spring Security

Phải mất một thời gian để làm quen với DSL đó, nhưng bạn sẽ tìm thấy nhiều ví dụ hơn trong phần Câu hỏi thường gặp: AntMatchers: Những ví dụ phổ biến .

Điều quan trọng bây giờ đó là configure method này là nơi bạn chỉ định:

  1. URL nào cần bảo vệ (đã authenticate()) và URL nào được cấp phép (allowAll ()).

  2. Những method authenticate nào được cấp phép (formLogin (), httpBasic ()) và cách chúng được cấu hình.

  3. Nói vắn tắt: cấu hình security hoàn chỉnh của ứng dụng của bạn.

Lưu ý : Bạn sẽ không cần phải override ngay lập tức configure method của adapter, vì nó mặc định đi kèm với một implementation. Cụ thể như dưới đây:

public abstract class WebSecurityConfigurerAdapter implements
                WebSecurityConfigurer<WebSecurity> {

    protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().authenticated()  // (1)
                    .and()
                .formLogin().and()   // (2)
                .httpBasic();  // (3)
        }
}
  1. Để truy cập bất kỳ URI ( anyRequest()) nào trên ứng dụng của bạn, bạn cần authenticate (authenticated()).

  2. Form login ( formLogin()) với cài đặt mặc định được bật.

  3. Cũng như Basic HTTP authentication(httpBasic()).

Cấu hình mặc định này là lý do tại sao ứng dụng của bạn bị khóa ngay sau khi bạn thêm Spring Security vào nó. Đơn giản phải không?

Tổng quát: Cấu hình DSL của WebSecurityConfigurerAdapter

Chúng ta đã biết rằng Spring Security bao gồm một số bộ lọc mà bạn cấu hình với class WebSecurityConfigurerAdapter @Configuration.

Nhưng còn thiếu một phần quan trọng. Hãy lấy BasicAuthFilter của Spring làm ví dụ. Nó có thể extract username / password từ một HTTP Basic Auth header, nhưng nó authenticate những thông tin này dựa trên điều gì?

Đến đây tự nhiên dẫn chúng ta tới câu hỏi về cách authentication hoạt động với Spring Security.

 

Authentication với Spring Security

Khi nói đến authentication và Spring Security, bạn có ba kịch bản sau:

  1. Mặc định : Bạn có thể truy cập (hashed) password của user, bởi vì bạn có thông tin chi tiết của mình (username, password) được lưu chẳng hạn trong một bảng database.

  2. Ít phổ biến hơn : Bạn không thể truy cập password (hashed) của user. Đây là trường hợp nếu user và password của bạn được lưu trữ ở một nơi khác, chẳng hạn như trong một sản phẩm quản lý danh tính của bên thứ ba cung cấp dịch vụ REST cho authentication. Hãy thử tìm hiểu: Atlassian Crowd.

  3. Cũng phổ biến : Bạn muốn sử dụng OAuth2 hoặc "Đăng nhập bằng Google / Twitter / v.v." (OpenID), khả năng kết hợp với JWT. Sau đó, không có điều nào có thể áp dụng thì bạn nên chuyển thẳng đến phần OAuth2.

Lưu ý : Tùy thuộc vào bạn rơi vào kịch bản nào, bạn cần chỉ định các @Beans khác nhau để Spring Security hoạt động, nếu không bạn sẽ nhận được các exception khá khó hiểu (như NullPointerException nếu bạn quên chỉ định PasswordEncoder). Hãy ghi nhớ nó trong tâm trí.

Chúng ta hãy xem xét hai kịch bản đầu.

1. UserDetailsService: Có quyền truy cập vào password của user

Hãy tưởng tượng bạn có một bảng database nơi bạn lưu trữ user của mình. Nó có một vài cột, nhưng quan trọng nhất là nó có cột username và password, nơi bạn lưu trữ hashed password của user.

create table users (id int auto_increment primary key, username varchar(255), password varchar(255));

Trong trường hợp này, Spring Security cần bạn xác định hai bean để thiết lập và chạy authentication

  1. Một UserDetailsService.

  2. Một PasswordEncoder

Chỉ định một UserDetailsService đơn giản như sau:

@Bean
public UserDetailsService userDetailsService() {
    return new MyDatabaseUserDetailsService(); // (1)
}
  1. MyDatabaseUserDetailsService implements UserDetailsService - một interface rất đơn giản bao gồm một method trả về một object UserDetails:
public class MyDatabaseUserDetailsService implements UserDetailsService {

        UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // (1)
         // 1. Load the user from the users table by username. If not found, throw UsernameNotFoundException.
         // 2. Convert/wrap the user to a UserDetails object and return it.
        return someUserDetails;
    }
}

public interface UserDetails extends Serializable { // (2)

    String getUsername();

    String getPassword();

    // <3> more methods:
    // isAccountNonExpired,isAccountNonLocked,
    // isCredentialsNonExpired,isEnabled
}
  1. Một UserDetailsService tải (load) UserDetails qua username của user. Lưu ý rằng method chỉ nhận một tham số: username (không phải password).

  2. Interface UserDetails có các method để lấy (hashed) password và một method để lấy username.

  3. UserDetails thậm chí còn có nhiều method hơn, chẳng hạn như account đang hoạt động hay bị chặn, thông tin đăng nhập đã hết hạn hay user được cấp phép gì - nhưng chúng ta sẽ không đề cập đến ở đây.

Vì vậy, bạn có thể tự implement các interface này, giống như chúng ta đã làm ở trên, hoặc là sử dụng các interface hiện có mà Spring Security cung cấp.

Các Implementation sẵn có

Một lưu ý nhỏ: Bạn luôn có thể tự mình triển khai các interface UserDetailsService và UserDetails.

Tuy nhiên, bạn cũng có thể thay thế bằng các implementations có sẵn của Spring Security mà bạn có thể sử dụng/configure/extend/override.

  1. JdbcUserDetailsManager, là một UserDetailsService dựa trên JDBC (database). Bạn có thể cấu hình nó để khớp với cấu trúc bảng/cột user của mình .

  2. InMemoryUserDetailsManager , giữ tất cả các chi tiết user in-memory và rất tốt cho việc test.

  3. org.springframework.security.core.userdetail.User, là một implementation UserDetails mặc định, hợp lý mà bạn có thể sử dụng. Điều đó có nghĩa là có khả năng ánh xạ/sao chép giữa các entity/ bảng database của bạn và class User này. Ngoài ra, bạn có thể chỉ cần làm cho các entity của mình implement interface UserDetails.

Quy trình làm việc đầy đủ của UserDetails: HTTP Basic Authentication

Bây giờ, hãy quay trở lại HTTP Basic Authentication của bạn, bạn đang bảo mật ứng dụng của mình bằng Spring Security và Basic Auth. Đây là những gì sẽ diễn ra khi bạn chỉ định một UserDetailsService và cố gắng login:

  1. Extract tổ hợp username/password từ HTTP Basic Authentication header trong một bộ lọc. Bạn không phải làm bất cứ điều gì cả, nó sẽ tự diễn ra đằng sau tấm màn che.

  2. Gọi MyDatabaseUserDetailsService của bạn để tải user tương ứng từ database, được bao bọc dưới dạng đối tượng UserDetails, làm lộ hashed password của user.

  3. Lấy password được extract từ HTTP Basic Auth header, tự động băm nó và so sánh nó với hashed password từ object UserDetails của bạn. Nếu cả hai khớp, user sẽ được authenticate thành công.

Đó là tất cả gì cần để authenticate. Nhưng đợi đã, làm cách nào Spring Security băm được password từ phía client (bước 3)? Và với thuật toán nào?

PasswordEncoders

Spring Security không thể đoán một cách kỳ diệu thuật toán băm password ưa thích của bạn. Đó là lý do tại sao bạn cần chỉ định một @Bean khác, một PasswordEncoder. Giả sử nếu bạn muốn sử dụng chức năng hashed password kiểu BCrypt (mặc định của Spring Security) cho tất cả các password của mình , bạn sẽ chỉ định @Bean này trong SecurityConfig.

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder();
}

Điều gì sẽ xảy ra nếu bạn có nhiều thuật toán băm password, vì bạn có một số user cũ có password được lưu trữ bằng MD5 (đừng làm điều này) và những user mới hơn với Bcrypt hoặc thậm chí là thuật toán thứ ba như SHA-256? Vậy thì bạn sẽ sử dụng bộ mã hóa sau:

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

Bộ mã hóa này hoạt động như thế nào? Nó sẽ xem xét hashed password của UserDetail (đến từ bảng database của bạn chẳng hạn), bây giờ phải bắt đầu với một {prefix}Prefix (tiền tố) đó, chính là phương pháp hash của bạn! Bảng database của bạn sau đó sẽ trông như thế này:

username

password

 

john@doe.com

{bcrypt}$2y$12$6t86Rpr3llMANhCUt26oUen2WhvXr/A89Xo9zJion8W7gWgZ/zA0C

 

my@user.com

{sha256}5ffa39f5757a0dad5dfada519d02c6b71b61ab1df51b4ed1f3beed6abe0ff5f6

Spring Security sẽ:

  1. Đọc các password đó và loại bỏ tiền tố ({bcrypt} hoặc {sha256}).

  2. Tùy thuộc vào giá trị tiền tố, hãy sử dụng PasswordEncoder cho chính xác (tức là BCryptEncoder hoặc SHA256Encoder)

  3. Băm password thô với PasswordEncoder đó và so sánh với password đã lưu trữ.

Đó là tất cả những gì có được với PasswordEncoders.

Tóm tắt: quyền truy cập vào password của user

Điểm nhấn cho phần này là: nếu bạn đang sử dụng Spring Security và có thể truy cập vào password của user, thì:

  1. Chỉ định một UserDetailsService. Có thể implementation tùy chọn hay sử dụng và tự cấu hình một implementation do Spring Security cung cấp.

  2. Chỉ định một PasswordEncoder.

Đó là vắn tắt về authentication trong Spring Security.

2. AuthenticationProvider: Không có quyền truy cập vào password của user

Bây giờ, hãy tưởng tượng rằng bạn đang sử dụng Atlassian Crowd để quản lý danh tính. Điều đó có nghĩa là tất cả user và password của bạn cho tất cả các ứng dụng được lưu trữ trong Atlassian Crowd và không còn trong bảng database của bạn nữa.

Điều này có hai hàm ý:

  1. Bạn không có password user trong ứng dụng của mình nữa, vì bạn không thể yêu cầu Crowd chỉ cung cấp cho bạn những password đó.

  2. Tuy nhiên, bạn có một API REST mà bạn có thể đăng nhập bằng username và password của mình. (Một POST request đến điểm cuối /rest/usermanagement/1/authentication của REST).

Nếu đúng như vậy, bạn không thể sử dụng UserDetailsService nữa, thay vào đó, bạn cần implement và cung cấp AuthenticationProvider @Bean.

    @Bean
    public AuthenticationProvider authenticationProvider() {
        return new AtlassianCrowdAuthenticationProvider();
    }

Một AuthenticationProvider chủ yếu bao gồm một method và một implementation ngắn gọn có thể trông như thế này:

public class AtlassianCrowdAuthenticationProvider implements AuthenticationProvider {

        Authentication authenticate(Authentication authentication)  // (1)
                throws AuthenticationException {
            String username = authentication.getPrincipal().toString(); // (1)
            String password = authentication.getCredentials().toString(); // (1)

            User user = callAtlassianCrowdRestService(username, password); // (2)
            if (user == null) {                                     // (3)
                throw new AuthenticationException("could not login");
            }
            return new UserNamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities()); // (4)
        }
            // other method ignored
}
  1. So với method UserDetails load (), nơi bạn chỉ có quyền truy cập vào username, giờ đây bạn đã có quyền truy cập để authenticate hoàn chỉnh, thường chứa username và password.

  2. Bạn có thể làm bất cứ điều gì bạn muốn để authenticate user, như gọi một REST-service chẳng hạn.

  3. Nếu authenticate không thành công, bạn cần phải throw ra một exception.

  4. Nếu authenticate thành công, bạn cần return một UsernamePasswordAuthenticationToken được khởi tạo đầy đủ. Nó là một implementation của interface Authentication và cần phải đặt trường được authenticate thành true (mà constructor được sử dụng ở trên sẽ tự động đặt). Chúng ta sẽ đề cập đến các authorities trong chương tiếp theo.

Quy trình làm việc AuthenticationProvider đầy đủ: HTTP Basic Authentication

Bây giờ, hãy quay lại HTTP Basic Authentication của bạn, có nghĩa là bạn đang bảo mật ứng dụng của mình bằng Spring Security và Basic Auth. Đây là những gì sẽ xảy ra khi bạn chỉ định AuthenticationProvider và cố gắng login:

  1. Extract tổ hợp username / password từ HTTP Basic Authentication Header trong một bộ lọc. Bạn không phải làm bất cứ điều gì để thực hiện việc đó cả, nó sẽ tự diễn ra đằng sau tấm màn che.

  2. Gọi AuthenticationProvider của bạn (ví dụ: AtlassianCrowdAuthenticationProvider) bằng username và password đó để bạn tự thực hiện authentication (ví dụ: gọi REST).

Không có quá trình hash password hoặc gì đó tương tự đang diễn ra, vì về cơ bản bạn đang ủy quyền cho bên thứ ba thực hiện kiểm tra username / password thực tế. Tóm lại đó là tất cả về AuthenticationProvider!

Tóm tắt: AuthenticationProvider

Bài học kinh nghiệm cho phần này là: nếu bạn đang sử dụng Spring Security và không có quyền truy cập vào password của user, thì hãy implement và cung cấp một @Bean AuthenticationProvider .

*Lời người dịch: Bài viết khá dài nên sẽ chia làm 2 phần. Đầu phần tiếp theo sẽ bàn tiếp về authorization.