Written By: Hoàng Mạnh Cường (Java 10)
Gmail: manhcuong200997@gmail.com
Bài viết gốc: https://www.baeldung.com/spring-security-exceptionhandler

Lưu ý: Trong bài viết gốc có một số cụm từ khi dịch sang Tiếng Việt có thể không được sát nghĩa hoặc không truyền tải hết được thông điệp của tác giả nên mình sẽ chú thích kèm trong cặp dấu “(…)”.

1. Tổng quan

Spring Security cho phép tùy chỉnh bảo mật HTTP cho các tính năng như là phân quyền cho các điểm kết thúc (endpoints authorization) hoặc cấu hình trình quản lý xác thực bằng cách kế thừa class WebSecurityConfigurerAdapter. Tuy nhiên, trong những phiên bản gần đây, Spring không còn sử dụng cách này và khuyến khích cấu hình bảo mật dựa trên thành phần.

Trong bài viết này, chúng ta sẽ xem ví dụ làm thế nào để thay thế việc ngừng sử dụng nó trong ứng dụng Spring Boot và chạy một số kiểm thử MVC.

2. Spring Security không sử dụng WebSecurityConfigurerAdapter

Chúng ta thường thấy các lớp cấu hình bảo mật Spring HTTP kế thừa class WebSecurityConfigureAdapter.

Tuy nhiên, từ phiên bản 5.7.0-M2, Spring không còn sử dụng WebSecurityConfigureAdapter và đề xuất tạo ứng dụng mà không có nó.

Chúng ta sẽ tạo một ví dụ về ứng dụng Spring Boot sử dụng xác thực trong bộ nhớ (in-memory authentication) để hiển thị kiểu cấu hình mới này.

Đầu tiên, hãy định nghĩa lớp dùng để cấu hình:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {

    // config

}

Chúng ta đang thêm chú thích bảo mật phương thức (method security annotations) để cho phép xử lý dựa trên các vai trò khác nhau.

2.1. Cấu hình xác thực (Configure Authentication)

Với WebSecurityConfigureAdapter, chúng ta sử dụng AuthenticationManagerBuilder để cài đặt bối cảnh xác thực (authentication context).

Bây giờ, nếu muốn tránh không sử dụng tới, chúng ta có thể định nghĩa thành phần UserDetailsManager hoặc UserDetailsService:

@Bean
public UserDetailsService userDetailsService(BCryptPasswordEncoder bCryptPasswordEncoder) {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("user")
      .password(bCryptPasswordEncoder.encode("userPass"))
      .roles("USER")
      .build());
    manager.createUser(User.withUsername("admin")
      .password(bCryptPasswordEncoder.encode("adminPass"))
      .roles("USER", "ADMIN")
      .build());
    return manager;
}

Hoặc đưa cho chúng ta UserDetailService, chúng ta thậm chí có thể cài đặt AuthenticationManager:

@Bean
public AuthenticationManager authManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService) 
  throws Exception {
    return http.getSharedObject(AuthenticationManagerBuilder.class)
      .userDetailsService(userDetailsService)
      .passwordEncoder(bCryptPasswordEncoder)
      .and()
      .build();
}

Tương tự, điều này sẽ hoạt động nếu chúng ta sử dụng xác thực JDBC hoặc LDAP.

2.2. Cấu hình bảo mật HTTP

Điều quan trọng hơn là nếu muốn không sử dụng nó cho bảo mật HTTP, chúng ta có thể tạo một bean SecurityFilterChain.

Ví dụ, giả sử chúng ta muốn bảo mật những điểm cuối cùng (endpoints) tùy thuộc vào các vai trò và để lại một điểm nhập ẩn danh chỉ để đăng nhập. Các yêu cầu xóa cũng đang được hạn chế đối với vai trò quản trị viên. Chúng ta sẽ sử dụng Basic Authentication:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf()
      .disable()
      .authorizeRequests()
      .antMatchers(HttpMethod.DELETE)
      .hasRole("ADMIN")
      .antMatchers("/admin/**")
      .hasAnyRole("ADMIN")
      .antMatchers("/user/**")
      .hasAnyRole("USER", "ADMIN")
      .antMatchers("/login/**")
      .anonymous()
      .anyRequest()
      .authenticated()
      .and()
      .httpBasic()
      .and()
      .sessionManagement()
      .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    return http.build();
}

Bảo mật HTTP sẽ xây dựng một đối tượng DefaultSecurityFilterChain để tải các yêu cầu so sánh và bộ lọc.

2.3. Cấu hình Web Security

Cũng như trên, đối với bảo mật Web, chúng ta có thể sử dụng callback interface WebSecurityCustomizer.

Hãy thêm một số mức độ lỗi và bỏ qua một số đường dẫn, như là ảnh hoặc các tập lệnh (scripts):

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    return (web) -> web.debug(securityDebug)
      .ignoring()
      .antMatchers("/css/**", "/js/**", "/img/**", "/lib/**", "/favicon.ico");
}

3. Endpoints Controller

Hãy định nghĩa một class REST Controller đơn giản cho ứng dụng:

@RestController
public class ResourceController {
    @GetMapping("/login")
    public String loginEndpoint() {
        return "Login!";
    }

    @GetMapping("/admin")
    public String adminEndpoint() {
        return "Admin!";
    }

    @GetMapping("/user")
    public String userEndpoint() {
        return "User!";
    }

    @GetMapping("/all")
    public String allRolesEndpoint() {
        return "All Roles!";
    }

    @DeleteMapping("/delete")
    public String deleteEndpoint(@RequestBody String s) {
        return "I am deleting " + s;
    }
}

Như đã đề cập trước đó khi định nghĩa bảo mật HTTP, chúng ta sẽ thêm một điểm cuối đăng nhập chung là /login mà ai cũng có thể truy cập được, những điểm cuối cụ thể cho quản trị viên và người dùng, và một điểm cuối là /all không được bảo mật theo một vai trò nào nhưng vẫn yêu cầu xác thực.

4. Kiểm thử Endpoints

Hãy thêm một cấu hình mới cho Spring Boot sử dụng mô hình MVC để kiểm thử.

4.1. Kiểm thử người dùng vô danh (Anonymous Users)

Anonymous users có thể truy cập /login. Nếu cố gắng truy cập vào điểm khác sẽ không được xác thực (401)

@Test
@WithAnonymousUser
public void whenAnonymousAccessLogin_thenOk() throws Exception {
    mvc.perform(get("/login"))
      .andExpect(status().isOk());
}

@Test
@WithAnonymousUser
public void whenAnonymousAccessRestrictedEndpoint_thenIsUnauthorized() throws Exception {
    mvc.perform(get("/all"))
      .andExpect(status().isUnauthorized());
}

Hơn thế nữa, đối với mọi điểm kết thúc ngoại trừ /login, chúng ta luôn yêu cầu xác thực, giống như điểm /all.

4.2. Kiểm thử vai trò của người dùng (User Role)

Một vai trò của người dùng (User role) có thể được quyền truy cập các điểm cuối chung (generic endpoints) và tất cả đường dẫn chúng ta đã cấp phép cho nó:

@Test
@WithUserDetails()
public void whenUserAccessUserSecuredEndpoint_thenOk() throws Exception {
    mvc.perform(get("/user"))
      .andExpect(status().isOk());
}

@Test
@WithUserDetails()
public void whenUserAccessRestrictedEndpoint_thenOk() throws Exception {
    mvc.perform(get("/all"))
      .andExpect(status().isOk());
}

@Test
@WithUserDetails()
public void whenUserAccessAdminSecuredEndpoint_thenIsForbidden() throws Exception {
    mvc.perform(get("/admin"))
      .andExpect(status().isForbidden());
}

@Test
@WithUserDetails()
public void whenUserAccessDeleteSecuredEndpoint_thenIsForbidden() throws Exception {
    mvc.perform(delete("/delete"))
      .andExpect(status().isForbidden());
}

Điều đáng chú ý là nếu một người dùng có vai trò khác (a user role) cố gắng truy cập vào một điểm cuối được bảo mật bởi quản trị viên, người dùng sẽ gặp lỗi “forbidden” (403).

Thay vào đó, ai đó không có thông tin đăng nhập, như là ẩn danh trong ví dụ trước sẽ gặp lỗi “unauthorized” (401).

4.3. Kiểm thử vai trò quản trị (Admin Role)

Có thể thấy một số người với vai trò là quản trị viên có thể truy cập bất kì điểm cuối nào:

@Test
@WithUserDetails(value = "admin")
public void whenAdminAccessUserEndpoint_thenOk() throws Exception {
    mvc.perform(get("/user"))
      .andExpect(status().isOk());
}

@Test
@WithUserDetails(value = "admin")
public void whenAdminAccessAdminSecuredEndpoint_thenIsOk() throws Exception {
    mvc.perform(get("/admin"))
      .andExpect(status().isOk());
}

@Test
@WithUserDetails(value = "admin")
public void whenAdminAccessDeleteSecuredEndpoint_thenIsOk() throws Exception {
    mvc.perform(delete("/delete").content("{}"))
      .andExpect(status().isOk());
}

5. Kết luận

Trong bài viết này, chúng ta đã theo dõi làm thế nào để tạo ứng dụng Spring Security không sử dụng WebSecurityConfigureAdapter và thay thế nó khi khởi tạo các thành phần cho xác thực, bảo mật HTTP và bảo mật Web.

Như mọi khi, chúng ta có thể tìm hiểu thêm về code mẫu tại đây.

Bài viết của mình tới đây là kết thúc. Hi vọng những thông tin trong bài viết này có thể hỗ trợ tốt cho các bạn trong công việc hiện tại và trong tương lai.

Thanks for watching!