Mảng bảo mật luôn là vấn đề quan trọng và không thể bỏ qua đối với bất kỳ website nào, kể cả đó là website bạn xây dựng với mục đích cung cấp dịch vụ công cộng và miễn phí. Bảo mật website tốt trước nhất là bạn sẽ bảo vệ được website của mình trước những tấn công phá hoại, đồng thời website của bạn cũng sẽ được nâng cao uy tín trong mắt người tiêu dùng bởi độ tin cậy và an toàn khi mà người dùng đã tin tưởng giao các thông tin cá nhân của mình.

Giới thiệu

Là một thành phần quan trọng và được sử dụng khá thường xuyên trong gia đình Spring Framework, Spring Security đã được biết đến khá rộng rãi khi vẫn còn đồng hành với Spring MVC với cơ chế bảo vệ mạnh mẽ đồng thời cung cấp cho lập trình viên sự tuỳ biến linh hoạt tuỳ theo nghiệp vụ và yêu cầu của từng Hệ Thống.

Và hôm nay với sự hỗ trợ từ Spring Boot, Spring Security lại tiếp tục chứng tỏ và khẳng định vai trò của mình bằng những bản nâng cấp về mặt chức năng và được tích hợp thêm những interface cấu hình từ Spring Boot, từ đó làm cho việc sử dụng Spring Security trở nên dễ dàng hơn trước, nếu không nói là xoá đi hẳn ấn tượng "Spring Security khó dùng lắm" từ ngày trước.

Bài viết hôm nay sẽ hướng dẫn các bạn cách setup một project Spring Boot sử dụng Spring Security cho các chức năng Authentication và Authorization, kết hợp đọc dữ liệu User từ Database MySQL. Đến đây chắc các bạn đã hiểu được rằng chúng ta sẽ cần kết hợp kiến thức từ bài hướng dẫn kết nối ứng dụng Spring với CSDL MySQL rồi, các bạn có thể tìm đọc lại ở đây.

Các Tools cần chuẩn bị

  • JDK 8 trở lên.
  • IDE tuỳ chọn
  • Maven 3.6
  • Spring Boot 2.1.5.RELEASE
  • Thymeleaf 2.1.5.RELEASE
  • MySQL Workbench
  • XAMPP
  • Tạo sẵn 1 database với tên "testspringsecurity"

Các đặc điểm mới của Spring Security với Spring Boot

Setup đơn giản

Phiên bản Spring Security hiện tại là phiên bản 5 được tích hợp với Spring Boot phiên bản 2.1.x. Nghĩa là bạn không cần phải inject toàn bộ gói jar vào project như ngày trước mà chỉ cần khai báo dependency của Spring Security vào trong thư viện Maven:

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

 Tích hợp nhanh chóng và đơn giản với Front-end

Không chỉ bảo mật được các API cho Server Backend mà ngay trên tầng Front-end (HTML) bạn vẫn có thể dùng Spring Securtiy để xử lý các nghiệp vụ bảo mật. Được tích hợp chặt chẽ với template engine Thymeleaf. Để sử dụng được Spring Security trên Front-end các bạn cần thư viện Maven sau:

	<!-- optional, it brings userful tags to display spring security stuff -->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        </dependency>

Tương tự như ngày trước dùng với JSP, chúng ta embed thêm các thư viện Tag custom để có thể thuận tiên hơn trong việc xử lý những nghiệp vụ trên tầng Front-end, bản thân thư viện <thymeleaf-extras-springsecurity4> cũng hỗ trợ tương tự cho Thymeleaf. Chúng ta sẽ đi chi tiết phần này ở mục hướng dẫn code.

Spring Security không làm nặng ứng dụng

Thời gian trước khi Spring MVC vẫn còn đóng vai trò độc quyền trong Spring Framework các lập trình viên khi tích hợp Spring Security vào project hầu hết đều ca thán và cho rằng Spring Security quá chiếm tài nguyên hệ thống và làm nặng ứng dụng của họ một cách không đáng có, nhưng vì nhiều nguyên nhân khác nhau họ vẫn chấp nhận dùng Spring Security cho Hệ Thống của mình.

Và câu chuyện của ngày hôm nay đã hoàn toàn khác, hầu như không có sự khác biệt rõ ràng nào giữa ứnng dụng có sử dụng Spring Security và ứng dụng không sử dụng, ngoại trừ việc ứng dụng của chúng ta được bảo mật hơn. Website TheCodeRoot hiện đang sử dụng Spring Security nhưng vẫn đáp ứng được tốc độ load trang cần thiết cho độc giả.

 

Hướng dẫn code

Như đã đề cập ở đầu bài, ví dụ hôm nay chúng ta sẽ focus vào việc sử dụng Spring Security với các cấu hình được hỗ trợ từ Spring Boot, đồng thời hướng dẫn các bạn cách dùng Spring Security với Thymeleaf xử lý các nghiệp vụ bảo mật ở tầng Front-end.

Thư viện Maven

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

    <artifactId>spring-boot-web-spring-security</artifactId>
    <packaging>jar</packaging>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <!-- thymeleaf? -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- userful tags for thymeleaf to integrate with spring security -->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

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

        <!--spring data-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.13</version>
        </dependency>

        <!-- enable live reload -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- for bootstrap -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>3.3.7</version>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.8</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <!-- Package as an executable jar/war -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

 

Cấu trúc project

Khai báo User Entity

@Entity
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(nullable = false, unique = true)
    @Email
    private String email;
 
    private String password;

    private String phone;

    private String roleName;
 
    //standard getters and setters
}

Việc đầu tiên chúng ta cần làm là tạo một class entity User chứa các trường cơ bản như username, password. Với trường username các bạn có thể sử dụng bất kỳ dạng dữ liệu text nào các bạn cần, với ví dụ này tôi sẽ sử dụng email của User chính là username đăng nhập vào Hệ Thống.

Và entity User Role dùng để định danh quyền hạn của User có được truy cập vào các tài nguyên nhất định của Hệ Thống hay không.

UserRepository
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

    public User findByEmail(String email);

}

Và để có thể lấy được dữ liệu từ Database dĩ nhiên chúng ta phải cần đến thư viện Spring Data. Khai báo 1 Repository cho User. Chúng ta sẽ cần 1 phương thức để tìm user bằng email dùng để kiểm tra user đó có tồn tại hay không khi user nhập email của họ và thực hiện login.

 

WebSecurityConfigurerAdapter

Bước đầu tiên trước hết chúng  ta phải cần có 1 class extends class WebSecurityConfigurerAdapter để có thể sử dụng các chức năng  của Spring Security:

package thecoderoot.tutorial.springsecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import thecoderoot.tutorial.springsecurity.service.CustomUserDetailsService;

import javax.sql.DataSource;

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/admin/**").hasAnyRole("ADMIN")
                .antMatchers("/user/**").hasAnyRole("USER")
                .anyRequest().authenticated()
                .antMatchers("/login")
                .permitAll()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf()
                .disable();
    }

    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(encoder())
                .and()
                .authenticationProvider(authenticationProvider())
                .jdbcAuthentication()
                .dataSource(dataSource);
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(encoder());
        return authProvider;
    }


    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

}

Class SpringSecurityConfig làm khá nhiều nhiệm vụ

  • configure - antmatchers: khai báo các đường dẫn nào sẽ được truy cập với role nào.
    • permitAll(): cho phép tất cả user được quyền truy cập.
    • hasAnyRole("ROLE_USER"): chỉ cho phép user có quyền user.
    • hasAnyRole("ROLE_ADMIN"):  chỉ cho phép user có quyền Admin.
  •  .authenticationProvider(authenticationProvider())
                .jdbcAuthentication()
                .dataSource(dataSource);

    Khai báo phương thức dùng để Authentication của chúng ta là dùng DaoAuthenticationProvider

  • Và các bạn cần khai báo phương thức mã hoá mật khẩu người dùng. Điều này rất quan trọng. Một quy tắc bất di bất dịch cho lập trình viên không kể công nghệ hay ngôn ngữ lập trình đó là "không bao giờ lưu trữ mật khẩu dưới dạng clean text" - một trong  quy tắc nghề nghiệp cơ bản nhất của lập trình viên.
    ​ @Bean
     public PasswordEncoder encoder() {
         return new BCryptPasswordEncoder();
     }

     

application.properties

server.port=8899

logging.level.org.springframework.security=DEBUG

#database
spring.datasource.url = jdbc:mysql://localhost:3306/testspringsecurity?useUnicode=yes&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

Chúng ta cần khai báo datasource cho Spring Data để kết nối với Database như đã được hướng dẫn trong bài trước. Với setting <spring.jpa.hibernate.ddl-auto=update> nghĩa là chúng ta sẽ yêu cầu Hibernate sẽ tự động tạo bảng nếu chưa có.

DaoAuthenticationProvider 

Spring Security cung cấp nhiều class chức năng để handle chức năng Authentication và Authorization, và trong đó class DaoAuthenticationProvider có chức năng đọc dữ liệu User từ Database và so sánh username-password của người dùng khi Login hoặc kiểm tra request từ User có được quyền truy cập tài nguyên Hệ Thống hay không.

Các bạn khai báo DaoAuthenticationProvider trong class SpringSecurityConfig như sau:

 @Bean
 public DaoAuthenticationProvider authenticationProvider() {
        final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(encoder());
        return authProvider;
 }

 

CustomUserDetailsService

package thecoderoot.tutorial.springsecurity.service;

import javax.annotation.PostConstruct;

import thecoderoot.tutorial.springsecurity.model.MyUserPrincipal;
import thecoderoot.tutorial.springsecurity.model.User;
import thecoderoot.tutorial.springsecurity.model.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.web.context.WebApplicationContext;


@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private WebApplicationContext applicationContext;

    @Autowired
    private UserRepository userRepository;

    public CustomUserDetailsService() {
        super();
    }

    @PostConstruct
    public void completeSetup() {
        userRepository = applicationContext.getBean(UserRepository.class);
    }

    @Override
    public UserDetails loadUserByUsername( String username) {
        User appUser = userRepository.findByEmail(username);

        if (appUser == null) {
            throw new UsernameNotFoundException(username);
        }
        return new MyUserPrincipal(appUser);
    }

}

CustomUserDetailsService sẽ làm nhiệm vụ tìm kiếm User, ở ví dụ này chúng ta sẽ dùng Repository để query.

 

MyUserPrincipal

package thecoderoot.tutorial.springsecurity.model;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.Arrays;

public class MyUserPrincipal extends org.springframework.security.core.userdetails.User {
    private User user;

    public MyUserPrincipal(User user){
        super(user.getEmail(), user.getPassword(), Arrays.asList(new SimpleGrantedAuthority(user.getRoleName())));
    }
}

 

(Optional): Phân cấp cho các Role:

Nếu chúng ta khai báo như trên thì các Role sẽ có vai trò ngang nhau, nghĩa là với Role nào thì chỉ được đăng nhập vào trang của Role đó và các Role khác thì hoàn toàn không có quyền truy cập. Tuy nhiên trong bài toán thực tế nếu chúng ta muốn phân cấp ra 1 Role  cao hơn có quyền xem các trang của các Role khác thì sao ? Chúng ta sẽ sử dụng khai báo sau:

    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl r = new RoleHierarchyImpl();
        r.setHierarchy("ROLE_ADMIN >  ROLE_USER");
        return r;
    }

Spring Security hỗ trợ chúng ta bằng class RoleHierarchy, trong ví dụ này chúng ta sẽ xếp cho ROLE_ADMIN cao hơn ROLE_USER. Các bạn có thể tự test điểu này bằng cách bỏ ra hoặc thêm đoạn code vào class SpringSecurityConfig.

 

login.html

<!DOCTYPE html>
<html xmlns="https://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
    <title>Login - thecoderoot.com</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!--css-->
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <link rel="stylesheet" th:href="@{/css/form.css}">

    <!--js-->
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
</head>
<body ng-app="">
<div class="container">
    <div id="loginbox" style="margin-top:50px;" class="mainbox col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2">
        <div class="panel panel-info login-form">
            <div class="panel-heading login-form-header">
                <div class="panel-title">Đăng nhập</div>
            </div>
            <div style="padding-top:30px" class="panel-body login-form">
                <div style="display:none" id="login-alert" class="alert alert-danger col-sm-12"></div>
                <form id="loginform" name="loginForm"
                      th:action="@{/login}"
                      method="post"
                      class="form-horizontal"
                      role="form">
                    <div class="form-group"
                         ng-class="{ 'has-error' : loginForm.username.$touched && loginForm.username.$invalid}"
                         style="padding: 0 1.0em ">
                        <input id="login-username"
                               type="email"
                               ng-model="username"
                               ng-class="{ 'has-error' : loginForm.username.$touched && loginForm.username.$invalid}"
                               class="form-control has-error"
                               name="username"
                               placeholder="Email"
                               required>
                        <div ng-show="loginForm.username.$touched && loginForm.username.$invalid">
                            <span class="error-text">Please input a valid email</span>
                        </div>
                        </span>
                    </div>

                    <div class="form-group"
                         style="padding: 0 1.0em"
                         ng-class="{ 'has-error' : loginForm.password.$touched && loginForm.password.$invalid}">
                        <input id="login-password"
                               type="password"
                               class="form-control error"
                               name="password"
                               ng-model="password"
                               placeholder="Password"
                               required>
                        <div ng-show="loginForm.password.$touched && loginForm.password.$invalid">
                            <span class="error-text">Please input a password</span>
                        </div>
                    </div>

                    <!--submit button-->
                    <div style="margin-top:10px; padding: 1.0em;" class="form-group text-center">
                        <!-- Button -->
                        <div class="col-sm-12 controls pull-right action-button-container">
                            <button type="submit" href="#" class="btn action-button">Đăng nhập</button>
                        </div>
                    </div>

                    <div style="border-top: 1px solid#bbb; padding-top: 0.5em; font-size:85%">
                        <div class="pull-left">
                            Bạn chưa đăng ký ?
                            <a th:href="@{/register}">Đăng ký ngay.</a>
                        </div>
                        <div class="pull-right">
                            Quên mật khảu?<a th:href="@{/forget-password}"> &nbsp;Khôi phục tại đây.</a>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>


</body>
</html>

Trên chỉ là một form login html hoàn toàn bình thường, chỉ duy nhất một điều các bạn cần chú ý, đó là attribute <th:action="@{/login}" method="post"> của tag <form>. Method này sẽ được gọi đến hàm xử lý Login của Spring Security. Ở ví dụ này chúng ta đang sử dụng kỹ thuật form login, tức là người dùng sẽ nhập vào 1 form của ứng dụng web và gửi đến Spring Security ngay trên ứng dụng đó. Ngoài ra Spring Security còn hỗ trợ phương thức Basic Authentication, tức là sẽ kiểm tra Header của mỗi request được gửi đến Server, phương pháp này sẽ được giới thiệu đến với các bạn trong những bài viết sau cho bảo mật cho Server Restful Web Service.

Xử lý Security trên Thymeleaf

Chúng ta sẽ sử dụng tag <sec> để sử dụng các hàm của Spring Security trên Frontend.

<span sec:authorize="isAuthenticated()">
                Your username: <span sec:authentication="name"></span>
                - <span sec:authentication="principal.authorities"></span> |
                <a th:href="@{/logout}">Thoát</a>
</span>

Đoạn code trên sẽ kiểm tra User đã Login thành công hay chưa và ghi ra username (ở đây là email) của User cùng với Role Name của User đó. Và để logout chúng ta có attribute <th:href="@{/logout}"> để logout khỏi Spring Security và có thể bắt đầu login lại với một user khác.

Testing

Chuẩn bị data:

Sau cùng chúng ta sẽ cùng nhau test thử thành quả. Trước khi tiến hành test ứng dụng chúng ta sẽ cần phải setup trước data để test. Các bạn chạy đoạn script sau để tạo 2

insert into testspringsecurity.test_user(id,email,password,phone,role_name) 
values (1,'minhnhatict@gmail.com','$2a$10$EblZqNptyYvcLm/VwDCVAuBjzZOI7khzdyGPBr08PpIi0na624b8.', '31312343','ROLE_ADMIN');

insert into testspringsecurity.test_user(id,email,password,phone,role_name) 
values (2,'minhnhatict_user@gmail.com','$2a$10$EblZqNptyYvcLm/VwDCVAuBjzZOI7khzdyGPBr08PpIi0na624b8.', '31312343','ROLE_ADMIN');

Với đoạn script trên chúng ta sẽ tạo 2 account:

  • minhnhatict@gmail.com có role là ROLE_ADMIN.
  • minhnhatict_user@gmail.com có role là ROLE_USER
  • Cả 2 đều có password là 123456, được mã hoá với định dạng  Bcrypt.
  • Các bạn chú ý: tên role phải bắt đầu bằng ROLE_.

Ứng dụng chúng ta sẽ được như sau:

Bấm vào link dành cho Admin:

Các bạn sẽ lập tức được trả về trang login, và phải nhập username + password. Ví dụ như bên dưới nhập account của user có role ROLE_ADMIN.

Chúng ta sẽ truy cập vào được link /admin (và cả link /user nếu chúng ta có khai báo RoleHierarchy).

Bấm vào link dành cho User

Tương tự cho việc đăng nhập cho user có ROLE_USER

Do giới hạn của bài viết nên ngừoi viết không thể liệt kê và giải thích một cách chi tiết cho từng đoạn code và ý nghĩa của chúng đến với nguời đọc, các bạn độc giả có thể download source code đầy đủ trong bài viết để tìm hiểu thêm.

Các bạn download source code tại đây. Và đừng quên setup Database trước khi chạy thử.

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

AutoCode.VN

 

minhnhatict@gmail.com Spring Security