Securing Spring Boot with JWT - Part 2 - Xác thực người dung dựa trên dữ liệu trong cơ sở dữ liệu

Trước tiên mình xin được gửi lời cảm ơn của mình tới tất cả các bạn đã dành thời gian theo dõi bài viết của mình. Sau khi bài viết Securing Spring Boot with JWT được public, mình đã nhận được khá nhiều câu hỏi liên quan. Chính vì thế bài viết hôm nay, mình muốn chia sẻ thêm 1 chút về cách việc bảo mật ứng dụng web dựa trên JWT trong Spring Boot.

Trong bài viết này, mình sẽ chia sẻ cách kết hợp sử dụng việc login dựa vào dữ liệu trong database thực sự, thay vì dự liệu fake sử dụng in-memory như bài trước Source code tại đây https://github.com/nhs3108/SpringJWT

1. Chuẩn bị database

Ta sẽ chỉ cần một database đơn giản là có thể đủ để demo cho bài viết này rồi. Để thực hiện việc xác thực, dĩ nhiên rồi, ta không thể không có bảng lưu thông tin user rồi. Và mình cũng chỉ cần nó là đủ rồi.

CREATE TABLE "user"
(
    user_id INTEGER DEFAULT nextval('user_user_id_seq'::regclass) PRIMARY KEY NOT NULL,
    username VARCHAR(64) NOT NULL,
    password TEXT NOT NULL,
    role VARCHAR(32) DEFAULT 'user'::character varying NOT NULL,
    enabled BOOLEAN DEFAULT true NOT NULL
);
CREATE UNIQUE INDEX user_username_uindex ON "user" (username);

Với

  • username: Tên định danh (tên đăng nhập)
  • password: mật khẩu (đã được mã hóa)
  • role: role của user đối với hệ thống. role có thể là user(người dùng bình thường) hay admin (quản trị viên)

Mình đã đặt file dump.sql tại đây

2. Kết hợp xác thực với dữ liệu có trong database

Nếu các bạn có theo dõi bài viết trước đó của mình (mà mình đã đề cập ở phía bên trên), thì chắc hẳn bạn sẽ không lạ với đoạn code này trong file WebSecurityConfig

package com.nhs3108.configs;

import com.nhs3108.filtes.JWTAuthenticationFilter;
import com.nhs3108.filtes.JWTLoginFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.web.authentication.UsernamePasswordAuthenticationFilter;

/**
* Created by nhs3108 on 29/03/2017.
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.csrf().disable().authorizeRequests()
               .antMatchers("/").permitAll() // Có nghĩa là request "/" ko cần phải đc xác thực
               .antMatchers(HttpMethod.POST, "/login").permitAll() // Request dạng POST tới "/login" luôn được phép truy cập dù là đã authenticated hay chưa
               .anyRequest().authenticated() // Các request còn lại đều cần được authenticated
               .and()
               // Add các filter vào ứng dụng của chúng ta, thứ mà sẽ hứng các request để xử lý trước khi tới các xử lý trong controllers.
               // Về thứ tự của các filter, các bạn tham khảo thêm tại http://docs.spring.io/spring-security/site/docs/3.0.x/reference/security-filter-chain.html mục 7.3 Filter Ordering
               .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class) 
               .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
   }

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.inMemoryAuthentication().withUser("admin").password("password").roles("ADMIN");
       /*
       // Mình comment phần dưới này vì chúng ta ko sử dụng DB nhé. Nếu các bạn sử dụng, bỏ comment và config query sao cho phù hợp. Các bạn có thể GG để tìm hiểu thêm
       auth.jdbcAuthentication().dataSource(dataSource)
               .usersByUsernameQuery("select username,password, enabled from users where username=?")
               .authoritiesByUsernameQuery("select username, role from user_roles where username=?");
       */
   }
}

Chú ý đoạn

auth.inMemoryAuthentication().withUser("admin").password("password").roles("ADMIN");
  • Như thế, với code như vậy, ta đang sử dụng in-memory authentication và sẽ chỉ thực hiện được việc login với dữ liệu cứng mà ta fix ở trên (đó là username-password tương ứng là "admin"-"password". Câu hỏi thực tế đặt ra là Làm sao nếu chúng ta muốn xác thực với dữ liệu trên database thực sự?.
  • Đoạn comment của mình trong phuơng thức configure(AuthenticationManagerBuilder auth) có hướng dẫn làm sao để làm điều đó. Tuy nhiên, câu truy vấn cho mỗi phần còn phụ thuộc vào cơ sở dữ liệu của bạn. Mình sẽ chỉ cho các bạn ngay sau đây. Nhưng trước tiên, ta cần cấu hình báo với thằng Spring rằng chúng ta thực hiện việc xác thực thông qua DB như sau
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin").password("password").roles("ADMIN");
        auth.jdbcAuthentication().dataSource(dataSource)
                .usersByUsernameQuery("select username,password, enabled from \"user\" where  username=?")
                .authoritiesByUsernameQuery("select username, role from \"user\" where username=?");
    }
}

với dataSource được autowired từ bean được cấu hình như sau:

package com.nhs3108.configs;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

/**
 * Created by nhs3108 on 07/11/2017.
 */
@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
        dataSourceBuilder.url("jdbc:postgresql://localhost:5432/springjwt");
        dataSourceBuilder.driverClassName("org.postgresql.Driver");
        dataSourceBuilder.username("postgres");
        dataSourceBuilder.password("postgres");
        return dataSourceBuilder.build();
    }
}
  • 2 phuơng thức JdbcUserDetailsManagerConfigurer<B> usersByUsernameQuery(String query)authoritiesByUsernameQuery(String query) của lớp org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer cho phép bạn chỉ định query lần lượt lấy thông tin user và role của nó. Nếu có thời gian, bạn có thể ngó qua class org.springframework.security.core.userdetails.jdb.JdbcDaoImpl bạn sẽ thấy query mặc địnhn mà Spring cung cấp.
public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled "
			+ "from users " + "where username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority "
			+ "from authorities " + "where username = ?";

Tuy nhiên, với DB của mình, mình đã làm như sau

auth.jdbcAuthentication().dataSource(dataSource)
                .usersByUsernameQuery("select username,password, enabled from \"user\" where  username=?")
                .authoritiesByUsernameQuery("select username, role from \"user\" where username=?");

Còn nếu với DB của các bạn trong dự án thực tế, bạn chỉ cần chú ý là

  • parameter của usersByUsernameQuery là query để lấy 3 thuộc tính username, password và enable với enable được hiểu là user đó là active hay inactive (kiểu như là user bị xóa, bị band nick hay chưa ấy).
  • parameter của authoritiesByUsernameQuery là query để lấy 2 thuộc tính username, role (cái này được sử dụng cho việc phân quyền sau này. Sau này có thời gian mình sẽ chỉ cho các bạn sau)

OK. Tất cả chỉ có vậy. Bạn hãy clone project mình đặt trên Github về và chạy thử. Có cả file dump DB của mình luôn nhé.

À còn nữa, vì trong thực tế password mình không bao giờ để raw (nguyên bản) như vậy khu lưu trữ trong DB, nên bạn cần config thêm như sau

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin").password("password").roles("ADMIN");
        auth.jdbcAuthentication().dataSource(dataSource)
                .usersByUsernameQuery("select username,password, true as enabled from \"user\" where  username=?")
                .authoritiesByUsernameQuery("select username, role from \"user\" where username=?")
                .passwordEncoder(new BCryptPasswordEncoder(64));
    }

Mình đã chỉ định 1 instance của Interface PasswordEncoder đó là thực thể của lớp BCryptPasswordEncoder. Bạn có thể dùng 1 thằng PasswordEncoder nào khác tùy bạn.Mình sẽ k đề cập nhiều ở đây vì mình nghĩ tới đây là đủ cho các bạn có cơ sở để tìm hiểu thêm rồi.

Xem bài Securing Spring Boot with JWT để biết cách test nhé các bạn!

Thế thôi. Một bài chia sẻ nho nhỏ hi vọng có thể giúp ích cho các bạn phần nào. Mình định viết nhiều hơn, giải thích cho các bạn luồng đi của thằng này cơ, nhưng mà ngại quá 😦 Vậy nếu các bạn có câu hỏi gì, vui lòng comment phía dưới để mình trả lời và cho các bạn khác đọc nữa nhé. Cảm ơn các bạn