Chào các bạn đã quay trở lại!

Ở phần trước mình đã liên thiên về API, JSON, JWT và cách tích hợp JWT vào API Java.

[Java] Phần 18 – Xác thực và phân quyền API bằng JWT và Spring Security [2/2]

Phần này mình sẽ tiếp tục liên thiên về Spring Security và cách tích hợp vào ứng dụng Java 😀

Spring cung cấp sẵn cho chúng ta Security ở mức tĩnh, nghĩa là ta phải cấu hình một số lượng hữu hạn user hoặc role để phục vụ phân quyền. Ví dụ như ta có thể cấu hình quyền truy cập API cho role ADMIN, MOD, REPORTER, GUEST ngay trong code. Nhưng khi nghiệp vụ phát sinh thêm role WRITER chẳng hạn, thì lại phải sửa code và khối lượng code thì tùy thuộc cách tổ chức project. Nếu sử dụng cách này thì sẽ không có ý nghĩa với các hệ thống lớn, vậy nên ta cần phải customize lại một phần để Spring Security có thể xử lý kiểm tra quyền này động hơn. Và thông thường các quyền này sẽ được cấu hình trên một hệ thống Quản trị phân quyền khác sau đó lưu trữ trong CSDL, nên phần tiếp theo của bài viết này mình sẽ hướng dẫn các bạn cách customize Spring Security để kiểm tra quyền từ CSDL.

Mục đích chúng ta cần xây dựng là khả năng kiểm tra phân quyền đến từng method của API.

Ví dụ bài toán: Có 1 API SinhVien, chứa các method phục vụ cho xử lý dữ liệu Sinh viên như Thêm Sinh viên (INSERT), Cập nhật thông tin Sinh viên (UPDATE), Xóa Sinh viên (DELETE), Tìm kiếm xem thông tin Sinh viên (VIEW). Tương tự cũng có 1 API GiaoVien gồm các method như vậy.

Cần kiểm tra phân quyền theo user:

ADMIN: Role Admin có thể thao tác được cả 4 action trên (INSERT, UPDATE, DELETE, VIEW), và cả 2 API SinhVien, API GiaoVien

SV: Role Sinh viên có thể xem thông tin (VIEW) và sửa thông tin của mình (UPDATE) của API SinhVien

GV: Role Giáo viên có thể xem và sửa thông tin (VIEW, UPDATE) của API GiaoVien, và INSERT/UPDATE/VIEW API SinhVien

Hình dung theo bảng dưới đây cho dễ.

Điều kiện để làm được việc này là trước đó phải có hệ thống quản lý phân quyền cho user, user nào có quyền ADMIN, user nào có quyền SV, GV,… Sau đó phân quyền theo nhóm ADMIN/SV/GV có quyền gì trên các API nào. Và để xây dựng được hệ thống Quản trị phân quyền như vậy cần rất nhiều kỹ thuật và vấn đề, bạn nào muốn hiểu sâu hơn thì comment, mình sẽ nói thêm. Còn trong phạm vi bài viết này thì mình coi như phần đó đã có sẵn 😀

Liên thiên đủ rồi, bắt tay vào luôn nào.

Bước 1: Add dependency Spring Security vào project

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

Bước 2: Tạo Service Interface kiểm tra quyền

package net.tunghuynh.service;

import org.springframework.security.core.Authentication;
/**
 * net.tunghuynh.service.AppAuthorizer.java
 * TungHuynh
 * Date 12/10/2019 10:20 AM
 */
public interface AppAuthorizer {
    boolean authorize(Authentication authentication, String action, Object callerObj);
}

Phần này các bạn cần nắm được rằng, để kiểm tra phân quyền như đề bài, thì cần các thông tin:

userId (lấy trong authentication: Để xác định user đó thuộc role nào (ADMIN, SV, GV)

menuCode (lấy trong callerObj): Để xác định API cần kiểm tra là API gì (SinhVien, GiaoVien)

action: Để xác định action cần kiểm tra trong API đó là gì (INSERT, UPDATE, DELETE, VIEW)

Bước 3: Implement nghiệp vụ kiểm tra quyền từ CSDL. Nhớ có annotation @Service thì Spring nó mới hiểu đó là Bean nhé.

package net.tunghuynh.service.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ResolvableType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import net.tunghuynh.service.AppAuthorizer;

import java.lang.annotation.Annotation;
import java.util.*;
/**
 * net.tunghuynh.service.impl.AppAuthorizerImpl.java
 * TungHuynh
 * Date 12/10/2019 10:20 AM
 */
@Service("appAuthorizer")
public class AppAuthorizerImpl implements AppAuthorizer {

    private final Logger logger = LoggerFactory.getLogger(AppAuthorizerImpl.class);

    @Override
    public boolean authorize(Authentication authentication, String action, Object callerObj) {
        String securedPath = extractSecuredPath(callerObj);
        if (securedPath==null || "".equals(securedPath.trim())) {//login, logout
            return true;
        }
        String menuCode = securedPath.substring(1);//Bỏ dấu "/" ở đầu Path
        boolean isAllow = false;
        try {
            UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) authentication;
            if (user==null){
                return isAllow;
            }
            String userId = (String)user.getPrincipal();
            if (userId==null || "".equals(userId.trim())) {
                return isAllow;
            }
            //Truy vấn vào CSDL theo userId + menuCode + action
            //Nếu có quyền thì
            {
                isAllow = true;
            }
        } catch (Exception e) {
            logger.error(e.toString(), e);
            throw e;
        }
        return isAllow;
    }

    // Lay ra securedPath duoc Annotate RequestMapping trong Controller
    private String extractSecuredPath(Object callerObj) {
        Class<?> clazz = ResolvableType.forClass(callerObj.getClass()).getRawClass();
        Optional<Annotation> annotation = Arrays.asList(clazz.getAnnotations()).stream().filter((ann) -> {
            return ann instanceof RequestMapping;
        }).findFirst();
        logger.debug("FOUND CALLER CLASS: {}", ResolvableType.forClass(callerObj.getClass()).getType().getTypeName());
        if (annotation.isPresent()) {
            return ((RequestMapping) annotation.get()).value()[0];
        }
        return null;
    }
}

Xong phần code phức tạp nhất 😀

Bước 4: Dùng Spring Security, khai báo phần kiểm tra phân quyền vào từng method API bằng annotation @PreAuthorize

package net.tunghuynh.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.util.Date;
import java.util.Map;
/**
 * net.tunghuynh.controller.SinhVienController.java
 * TungHuynh
 * Date 12/10/2019 10:41 AM
 */
@RestController
@CrossOrigin(origins = "*")
@RequestMapping("/SinhVien")
public class SinhVienController {
    Logger logger = LoggerFactory.getLogger(getClass());

    @RequestMapping(value = {"/get-data"}, method = RequestMethod.POST)
    @PreAuthorize("@appAuthorizer.authorize(authentication, 'VIEW', this)")
    public @ResponseBody ResponseEntity<?> getData(@RequestBody Map map) {
        //Xử lý nghiệp vụ tìm kiếm thông tin
    }

    @RequestMapping(value = {"/insert"}, method = RequestMethod.POST)
    @PreAuthorize("{@appAuthorizer.authorize(authentication, 'INSERT', this)}")
    public @ResponseBody
    ResponseEntity<?> insert(@RequestBody SinhVien sinhVien) {
        //Xử lý nghiệp vụ Insert
    }

    @RequestMapping(value = {"/update"}, method = RequestMethod.POST)
    @PreAuthorize("{@appAuthorizer.authorize(authentication, 'UPDATE', this)}")
    public @ResponseBody
    ResponseEntity<?> update(@RequestBody SinhVien sinhVien) {
        //Xử lý nghiệp vụ Update
    }

    @RequestMapping(value = {"/delete"}, method = RequestMethod.POST)
    @PreAuthorize("{@appAuthorizer.authorize(authentication, 'DELETE', this)}")
    public @ResponseBody
    ResponseEntity<?> delete(@RequestBody SinhVien sinhVien) {
        //Xử lý nghiệp vụ Delete
    }
}

Làm tương tự như vậy với API GiaoVien

Bước 5: Enabled Spring Security bằng 2 annotation dưới, khai báo trong file Config của project

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)

package net.tunghuynh.config;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 
/**
 * net.tunghuynh.config.WebSecurityConfig.java
 * TungHuynh
 * Date 12/10/2019 9:47 AM
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //TODO .......
    }
}

All Done! Như vậy là project của bạn đã được tích hợp kiểm tra phân quyền động từ CSDL cho API bằng cách customize Spring Security. Giờ nếu truy cập vào API mà không có quyền thì bạn sẽ nhận được response có HTTP status 403 😀

Chúc các bạn thành công!

14 Replies to “[Java] Phần 18 – Xác thực và phân quyền API bằng JWT và Spring Security [2/2]”

  1. Bạn có thể nói thêm về hệ thống Quản trị phân quyền kết hợp với bài này được k? Mình rất muốn tìm nguồn để tìm hiểu rõ cái này.

    1. Nếu chỉ dừng ở mức độ đủ dùng thì hệ thống Quản trị phân quyền này không khó, cơ bản thì chỉ cần Quản lý người dùng; Nhóm người dùng; Quản lý chức năng; Chức năng phân quyền.
      Nhưng có rất nhiều hướng để mở rộng tùy theo nhu cầu quản lý mà phức tạp dần lên, ví dụ như quản lý log truy cập, log thay đổi, quản lý đặc quyền, quản lý whitelist, quản lý IP truy cập,…
      Không biết bạn đã nắm được đến đâu và muốn tìm hiểu đến mức độ nào?

      1. Ví dụ mình muốn một hệ thống mà phân biệt rõ ràng giữa client và admin vậy nên mình chia làm 2 bảng trong CSDL khác nhau. Vậy câu hỏi là làm sao để xác thực được người dùng nào là admin, người nào là client. Giả sử khi đăng nhập mình có cho họ chọn đăng nhập với vai trò gì.
        Trên stack có câu trả lời rồi: https://stackoverflow.com/a/49391537/10597062
        Nhưng mình chưa tìm được best practice cho nó. Mong bạn giải đáp hoặc cho mình tư liệu để tham khảo.
        Cảm ơn.

  2. bác có thể nói thêm về Quản lý người dùng; Nhóm người dùng; Quản lý chức năng; Chức năng phân quyền. được không, cơ sở dữ liệu thiết kế như nào ạ.

  3. Chỉ cần 1 bảng user duy nhất cho mọi account đăng nhập. Để tránh việc phải mất công phân quyền cho từng user thì ta gom các user chung quyền vào 1 nhóm và chỉ cần phân quyền cho nhóm là mọi user thuộc nhóm có quyền như nhau. Ví dụ nhóm Admin, Client, …
    Ảnh: https://wp.me/a9vGwl-Cn
    Khi đã có khung dữ liệu như trên, để check quyền thì ta sẽ truy vấn lần lượt: từ Username login -> tìm ra User ID trong bảng USER, từ User ID -> tìm ra thuộc Group ID nào trong bảng USER_GROUP, từ Group ID -> tìm ra được có quyền vào các chức năng nào (Menu ID) trong bảng PERMISSION.
    Trên đây là cấu trúc cơ bản đơn giản nhất của 1 hệ thống phân quyền.

  4. Em muốn truyền vào trong hàm authorize chỗ String action là một list thì khi truyền vào trong @PreAuthorize(“@appAuthorizer.authorize(authentication, ‘VIEW’, this)”) thì truyền như thế nào ạ. Mong anh giải đáp ạ

  5. phương thức extra…Path when check
    callerObj :net.codejava.ProductController@5d767218?
    clazz :class net.codejava.ProductController
    annotation :Optional.empty

    ko lấy đc path để so sánh vs CSDL

  6. Anh ơi! anh có làm youtube không ạ. Nếu được a có thể làm về phần này được không ạ. Cảm ơn a vì bài viết rất bổ ích

Bình luận