Bộ sưu tập

Xác thực cơ bản và phân quyền bằng Spring Security cho ứng dụng Webflux


https://codersontrang.com/2017/10/30/xac-thuc-co-ban-va-phan-quyen-bang-spring-security-cho-ung-dung-webflux/
Trong bài viết trước, chúng ta đã cùng tìm hiểu về Spring Webflux là một framework mới cho các lập trình viên Java Web với mục tiêu là hướng đến các xử lý Non-Blocking. Trong bài viết này chúng ta sẽ tiếp tục tìm hiểu về Webflux và cách tích hợp ứng dụng Webflux cùng với Spring Security là một framwork giúp xác thực và ủy quyền cho người sử dụng ứng dụng.

Trong loạt bài viết về Spring Security trước đây, chúng ta chưa nói rõ về kiến trúc cũng như các thành phần chính của framework này. Vì thế, nhân tiện bài viết này, chúng ta sẽ cùng nhìn lại tổng quan về cách thức Spring Security xác thực và ủy quyền cho người dùng theo như hình vẽ dưới đây:

Nhìn vào hình vẽ trên thì ta thấy có hai thành phần chính cần phải kể đến trong Spring Security là

  • Authentication Manager: là thành phần đảm nhận nhiệm vụ xác thực người dùng. Tức là với một tài khoản bao gồm username và mật khẩu được người dùng nhập vào hệ thống, thành phần này sẽ đảm nhận vai trò xác minh rằng tài khoản trên có đích thị là một tài khoản hợp lệ đối với hệ thống hay không. Hay nói cách khác, nó giúp trả lời cho câu hỏi “Có đúng là anh không ?
  • Authorization Manager: là thành phần đảm nhận nhiệm vụ ủy quyền (hay thường gọi là phân quyền) cho người dùng. Sau khi tài khoản của người sử dụng đã được xác minh là hợp lệ bởi Authentication Manager ở trên, thì nó sẽ được tiếp tục kiểm tra để xem người dùng có đủ quyền hạn để truy cập đến một tài nguyên nào đó trong hệ thống hay không. Hay nói cách khác, nó giúp trả lời cho câu hỏi “Có đúng là anh có thể làm điều này không ?

Dựa vào hình vẽ trên, ta sẽ đi qua từng bước một để miêu tả cách mà một người dùng truy cập vào một tài nguyên nào đó trong hệ thống như sau:

  • Đầu tiên, người dùng muốn truy cập vào hệ thống, hệ thống sẽ chưa biết người dùng là ai, vì vậy đòi hỏi người dùng phải nhập thông tin xác minh của mình bao gồm username và mật khẩu qua một trang Login.
    Sau khi người dùng nhập hai thông tin này và bấm vào nút Login thì username cũng như mật khẩu sẽ được gói vào trong một đối tượng gọi là Authentication.
  • Tiếp đó đối tượng Authentication sẽ được gửi đến Authentication Manager để thành phần này xác minh xem tài khoản của người dùng có đúng hay không. Ở đây diễn ra quá trình đối sánh thông tin giữa cái mà người dùng nhập vào với cái thông tin được được lưu trong hệ thống, tất cả dựa trên một thông tin chung là username. Cụ thể như sau, từ username trên, Authentication Manager sẽ gọi đến UserDetailsRepository để lấy ra thông tin đang lưu trong hệ thống tương ứng với username đó, thông tin trả ra sẽ kèm theo thông tin về mật khẩu (password) cũng như các quyền hạn mà username đang được ghi nhận trong hệ thống (authorities). Những thông tin này sẽ được gói trong một đối tượng là UserDetails. Sau đó thông tin trong đối tượng UserDetails (cụ thể là mật khẩu) này sẽ được so sánh với các thông tin ở trong đối tượng Authentication mà ta đã nói ở trên quan hàm authenticate() của Authentication Manager. Nếu chúng không trùng khớp thì quá trình xác thực sẽ dừng lại ở đây, người dùng sẽ quay trở về bước đầu tiên nhất để nhập lại thông tin cho đúng. Còn trong trường hợp chúng trùng khớp nhau, thì một đối tượng Authentication khác sẽ được tạo ra và chứa không chỉ các thông tin xác thực (username, password) do người dùng nhập lúc đầu mà còn chứa các thông tin về quyền hạn (authorities) lấy tương ứng từ trong hệ thống ra nữa.
  • Đối tượng Authenticaton được sinh ra sau khi xác minh thành công sẽ được chuyển tới Authorization Manager để tiến hành kiểm tra quyền hạn ở đây. Việc kiểm tra quyền hạn sẽ dựa trên sự đối sánh giữa quyền hạn thực tế của tài khoản đang dùng và quyền hạn bắt buộc được yêu cầu từ phía tài nguyên cần truy cập. Quyền hạn thực tế của tài khoản đang dùng chính là nằm trong đối tượng Authentication ở trên, còn quyền hạn bắt buộc được yêu cầu từ phía tài nguyên cần truy cập sẽ được khai báo trong hệ thống khi ta cấu hình các tài nguyên cho việc bảo mật qua API authorizeExchange(). Sự đối sánh về quyền hạn sẽ được Authorization Manager thực hiện qua hàm verify()
  • Cuối cùng, nêu như việc kiểm tra quyền hạn cho biết rằng tài khoản của người dùng có đủ quyền hạn để truy cập vào tài nguyên, thì hệ thống sẽ đưa ra cho người dùng tài nguyên tương ứng (ví dụ ở đây là trang Home), còn không thì người dùng sẽ nhận được lời nhắn báo rằng không đủ quyền để truy cập tài nguyên này (access denined)

Để minh họa cho tất cả những gì nó ở trên ta sẽ tiếp tục bổ sung thêm các thành phần vào ví dụ của bài viết trước. Cụ thể là sẽ thêm một package có tên là springwebfluxdemo.security với các file *.java như hình vẽ dưới đây.

Trong bài viết này ta sẽ chỉ xem qua các file mã nguồn có sự thay đổi trong nội dung, còn lại nội dung của các file khác không có gì thay đổi thì các bạn có thể tìm chúng trong bài viết trước nhé.

pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.codersontrang</groupId>
    <artifactId>spring-webflux-security</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <repositories>
        <repository>
            <id>spring-milestone</id>
            <name>spring milestone</name>
            <url>http://repo.spring.io/milestone/</url>
        </repository>
        <repository>
            <id>maven-central</id>
            <name>maven central</name>
            <url>http://central.maven.org/maven2/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
            <version>2.0.0.M4</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.0.0.M4</version>
        </dependency>

        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>3.0.8.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
            <version>5.0.0.M5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-webflux</artifactId>
            <version>5.0.0.M5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>5.0.0.M5</version>
        </dependency>

    </dependencies>
</project>

Để có thể áp dụng được Spring Security vào trong ứng dụng, ta cần chỉnh sửa lại file pom.xml bằng việc thêm vào các dependencies là spring-security-core, spring-security-webfluxspring-security-config. Tất cả những dependencies này sẽ đều được dùng tại phiên bản mới nhất tính đến thời điểm của bài viết này ra đời là 5.0.0.M5.

ReactiveWebSecurityConfiguration.java


package springwebfluxdemo.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.HttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration
@EnableWebFluxSecurity
public class ReactiveWebSecurityConfiguration{
    @Autowired
    private ReactiveAuthenticationManager authenticationManager;

    @Bean
    SecurityWebFilterChain springWebFilterChain(HttpSecurity http) throws Exception {
        return http
                .formLogin().disable()
                .httpBasic()
                .and()
                .authenticationManager(authenticationManager)
                .authorizeExchange()
                .pathMatchers(HttpMethod.POST, "/submitStudentInfo").hasAuthority("SUPER_ADMIN")
                .anyExchange().authenticated()
                .and()
                .build();
    }
}

File ReactiveWebSecurityConfiguration.java dùng để khai báo cấu hình cho việc sử dụng Spring Security trong ứng dụng. Ta có một số lưu ý sau:

  • Lưu ý trước nhất là chúng ta cần phải có annotation là @EnableWebFluxSecurity.
  • Tiếp sau đó vì trong bài viết này ta sử dụng phương pháp xác thực cơ bản nên ta sẽ gọi đến hàm httpBasic().
  • Về phần authorizeExchange() như đã nói ở trên là dùng để khai báo quyền hạn bắt buộc phải có để tài khoản có thể truy cập các tài nguyên. Trong ứng dụng của chúng ta ta sẽ có hai tài nguyên tương ứng với hai URL là /home/submitStudentInfo. Đối với tài nguyên /submitStudentInfo ta bắt buộc tài khoản đăng nhập phải có quyền là SUPER_ADMIN để có thể truy cập vào, còn lại đối với các tài nguyên khác (bao gồm /home), tài khoản chỉ cần được xác minh là đúng là có thể truy cập vào được.

AuthenticationManager.java


package springwebfluxdemo.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UserDetailsRepositoryAuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsRepository;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationManager extends UserDetailsRepositoryAuthenticationManager{
    public AuthenticationManager(
            @Autowired
            UserDetailsRepository userDetailsRepository) {
        super(userDetailsRepository);
    }
}

AuthenticationManager như đã nói ở trên là dùng để xác minh tài khoản nhập vào từ người dùng có đúng hay không. Và ở trong này nó có chưa một đối tượng UserDetailsRepository để có thể gọi từ đây vào lấy ra thông tin về người dùng tương ứng trong hệ thống.

UserRepository.java


package springwebfluxdemo.security;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsRepository;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;

@Repository
public class UserRepository implements UserDetailsRepository {

	public Mono<UserDetails> findByUsername(String username) throws UsernameNotFoundException {
		if(StringUtils.isEmpty(username)) return Mono.empty();
		if(username.equals("sa")){
			LoginUserDetail sa = new LoginUserDetail("sa", "password");
			sa.addAuthority(new SimpleGrantedAuthority("SUPER_ADMIN"));
			return Mono.just(sa);
		}else if (username.equals("user")){
			LoginUserDetail user = new LoginUserDetail("user", "password");
			user.addAuthority(new SimpleGrantedAuthority("NORMAL_USER"));
			return Mono.just(user);
		}else{
			return Mono.empty();
		}
	}
}

UserRepository được dùng để lấy thông tin tài khoản tương ứng trong hệ thống ra qua hàm findByUsername(), thông tin tài khoản sẽ được trả về là một đối tượng LoginUsserDetail. Ta có thể nhìn thấy đối tượng này được bọc trong một đối đối tượng khác có kiểu là Mono, thì Mono là một kiểu chuẩn được định nghĩa bởi Reactor (một framwork tuân theo chuẩn Reactive Streams) mang ý nghĩa thể hiện một đối tượng đơn lẻ của một kiểu nào đó. Nó tương tự như kiểu Single trong ReactiveX vậy.

Trong ví dụ này UserRepository được hard-code để trả về một số giá trị có sẵn, ở đây ta có hai tài khoản có thể truy cập vào hệ thống như sau

username password authority
Tài khoản 1 sa password SUPER_ADMIN
Tài khoản 2 user password NORMAL_USER

Trên thực tế thì các thông tin này sẽ có thể lấy ra từ database, web service … hay bất kì một nguồn dữ liệu nào khác. Điều này cũng có nghĩa là ta sẽ phải thay đổi code logic ở lớp UserRepository này để gọi đến database, web service … thay vì hard-code như trong ví dụ.

LoginUserDetail.java


package springwebfluxdemo.security;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class LoginUserDetail implements UserDetails{
	private static final long serialVersionUID = -2893826667451683737L;
	private Set<GrantedAuthority> authorities = new HashSet<>();
	private String password;
	private String username;

	public LoginUserDetail(String username, String password){
		this.username = username;
		this.password = password;
	}

	public void addAuthority(GrantedAuthority auth){
		authorities.add(auth);
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return authorities;
	}

	@Override
	public String getPassword() {
		return password;
	}

	@Override
	public String getUsername() {
		return username;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}
}

Và cuối cùng lớp LoginUserDetail sẽ chứa tất cả các thông tin mô tả cho một tài khoản đăng nhập vào hệ thống như username, password, authorities

Bây giờ tiến hành chạy ứng dụng từ lớp Spring5WebfluxDemoApp có chưa phương thức main(). Đầu tiên ta thử truy cập vào trang /home. Ta sẽ thấy một popup hiện lên yêu cầu người dùng phải nhập vào username và password. Ta dùng tài khoản là sa/password để nhập vào như hình dưới đây:

Sau khi nhập xong ta có thể truy cập đến trang /home, cũng có nghĩa là ứng dụng đã xác thực tài khoản thành công.

Bấm vào nút Submit, ta sẽ truy cập vào /submitStudentInfo một cách bình thường, bời vì tài khoản sa/password có quyền là SUPER_ADMIN, nên nó hoàn toàn thỏa mãn về quyền hạn để truy cập vào tài nguyên này.

Thực hiện lại các bước ở trên nhưng lần này ta sẽ dùng tài khoản là user/password

Ta thấy rằng ta vẫn có thể truy cập vào trang /home một cách bình thường.

Tuy nhiên khi bấm vào nút Submit, ta sẽ thấy ta nhận được một trang lỗi 403 (Access Denined). Đó là vì tài khoản user/password có quyền là NORMAL_USER, trong khi đó tài nguyên /submitStudentInfo lại cần một tài khoản có quyền hạn là SUPER_ADMIN để có thể truy cập vào nó.

Như vậy bài viết này đã cung cấp cho các bạn một ví dụ về cách sử dụng Spring Security để tích hợp cơ chế xác thực cơ bản và phân quyền vào một ứng dụng Webflux dựa trên Spring Boot. Hi vọng qua bài viết này, bạn đọc nắm được các thành phần cơ bản cũng như cách thức hoạt động của Spring Security, và một lần nữa các bạn lại có cơ hội để tiếp xúc nhiều hơn với Webflux một web framework mới mẻ trẻ trung nhất trong hệ sinh thái của Spring.

Good luck!

Advertisements

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s