Hướng dẫn lập trình Spring Security

Hướng dẫn lập trình Spring Security

Trong bài viết lần này, mình sẽ giúp các bạn bước đầu tìm hiểu Spring Secuirty thông qua xây dựng các chức năng:

  • Đăng nhập
  • Đăng xuất
  • Phân quyền

Bài viết vẫn sẽ áp dụng kiến trúc của ứng dụng MyContact. Nếu có chỗ nào khó hiểu, các bạn có thể tham khảo lại bài viết về MyContact nhé.

Bài viết sử dụng các dự án sau của Spring Framework:

  • Spring Boot để khởi tạo và chạy dự án
  • Spring Security để mã hóa mật khẩu và xác thực người dùng
  • Spring MVC để xây dựng web app
  • Spring Data để thao tác với cơ sở dữ liệu, cụ thể mình sẽ dùng Spring Data JPA - một thành viên trong gia đình Spring Data

Các công nghệ khác mình sẽ sử dụng:

  • Hệ quản trị CSDL: MySQL
  • ORM framework: Hibernate (cái này đã được tích hợp sẵn trong Spring Data)
  • Template engine: Thymeleaf 2.1 (do phiên bản Spring Boot hiện tại mình đang dùng là 1.4.3 chưa hỗ trợ Thymeleaf 3.0)

Các công cụ bao gồm:

  • Ubuntu 16.04
  • JDK 1.8
  • Eclipse Neon đã cài đặt Spring Tool Suite

Source code của dự án: https://github.com/yuen26/spring-security


Spring Security

Giới thiệu

Spring Security là một dự án nổi bật trong hệ sinh thái Spring. Spring Security cung cấp các dịch vụ bảo mật toàn diện cho các ứng dụng doanh nghiệp có nền tảng Java EE.

Spring Security cung cấp 2 cơ chế cơ bản:

  • Authentication (xác thực): là tiến trình thiết lập một principal. Principal có thể hiểu là một người, hoặc một thiết bị, hoặc một hệ thống nào đó có thể thực hiện một hành động trong ứng dụng của bạn.
  • Authorization (phân quyền) hay Access-control: là tiến trình quyết định xem một principal có được phép thực hiện một hành động trong ứng dụng của bạn hay không. Trước khi diễn tiến tới Authorization, principal cần phải được thiết lập bởi Authentication.

Ta có thể thấy đây là 2 cơ chế khá phổ biến trong các dịch vụ bảo mật, không chỉ riêng Spring Security.

Các thành phần cốt lõi

Security, SecurityContext và Authentication

SecurityContext là interface cốt lõi của Spring Security, lưu trữ tất cả các chi tiết liên quan đến bảo mật trong ứng dụng. Khi chúng ta kích hoạt Spring Security trong ứng dụng thì SecurityContext cũng sẽ được kích hoạt theo.

Chúng ta sẽ không truy cập trực tiếp vào SecurityContext, thay vào đó sẽ sử dụng lớp SecurityContextHolder. Lớp này lưu trữ security context hiện tại của ứng dụng, bao gồm chi tiết của principal đang tương tác với ứng dụng. Spring Security sẽ dùng một đối tượng Authentication để biểu diễn thông tin này. Đoạn code dưới đây sẽ giúp chúng ta lấy được username của principal đã được xác thực (username ở đây ta nên hiểu là username trong cặp username - password mà người dùng nhập vào khi đăng nhập):

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
    String username = ((UserDetails) principal).getUsername();
} else {
    String username = principal.toString();
}

Đoạn code này có thể đặt ở bất kỳ đâu trong ứng dụng.

UserDetails và UserDetailsService

Trong đoạn code trên, chúng ta có được một principal từ đối tượng Authentication. Principal đơn giản chỉ là một đối tượng và sẽ được ép kiểu sang UserDetails.

UserDetails là một interface cốt lõi của Spring Security. Nó đại diện cho một principal nhưng theo một cách mở rộng và cụ thể hơn. Vậy UserDetails cung cấp cho ta những thông tin gì? UserDetails bao gồm các method sau:

  • getAuthorities(): trả về danh sách các quyền của người dùng
  • getPassword(): trả về password đã dùng trong qúa trình xác thực
  • getUsername(): trả về username đã dùng trong qúa trình xác thực
  • isAccountNonExpired(): trả về true nếu tài khoản của người dùng chưa hết hạn
  • isAccountNonLocked(): trả về true nếu người dùng chưa bị khóa
  • isCredentialsNonExpired(): trả về true nếu chứng thực (mật khẩu) của người dùng chưa hết hạn
  • isEnabled(): trả về true nếu người dùng đã được kích hoạt

Chúng ta có thể thấy UserDetails mới chỉ cung cấp các phương thức để truy cập các thông tin cơ bản của người dùng. Để mở rộng thêm các thông tin, chúng ta sẽ tạo một lớp CustomUserDetails implements org.springframework.security.userdetails.UserDetails (tên lớp là tùy ý, bạn đặt tên thế nào cũng được).

Câu hỏi tiếp theo đặt ra là ta sẽ tạo implementation của UserDetails ở đâu trong ứng dụng? Câu trả lời là ta sẽ dùng UserDetailsService. UserDetailsService là một interface có duy nhất một phương thức:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

Tham số truyền vào chỉ gồm có username của người dùng. Ta sẽ tìm kiếm trong CSDL, record thỏa mãn username. Nếu không tìm thấy, ta sẽ ném ra ngoại lệ UsernameNotFoundException.

Phương thức loadUserByUsername() sẽ trả về một implementation của UserDetails. Implementation ở đây có thể là:

  • org.springframework.security.core.userdetails.User
  • CustomUserDetails implements org.springframework.security.userdetails.UserDetails mà mình đã nói ở trên

Nhiệm vụ của chúng ta là cần phải tạo một lớp UserDetailsServiceImpl implements UserDetailsService.

GrantedAuthority

Ở phần trên, mình đã đề cập đến phương thức getAuthorities(). Phưong thức này sẽ trả về một tập hợp các đối tượng GrantedAuthority. Một GrantedAuthority là một quyền được ban cho principal. Các quyền đều có tiền tố là ROLE_, ví dụ như ROLE_ADMIN, ROLE_MEMBER ...


Áp dụng Spring Security trong thực tế

Trong bài viết lần này, mình sẽ hướng dẫn các bạn xây dựng các chức năng sau:

  • Đăng nhập
  • Đăng xuất
  • Phân quyền

Người dùng được chia là 3 nhóm:

  • Khách (guest)
  • Thành viên (member), có quyền ROLE_MEMBER
  • Quản trị viên (admin), có cả quyền ROLE_MEMBERROLE_ADMIN

Ứng dụng của chúng ta sẽ gồm có 4 trang:

  • Trang đăng nhập /login
  • Trang chủ /: chỉ cho phép ROLE_MEMBER truy cập
  • Trang admin /admin: chỉ cho ROLE_ADMIN truy cập
  • Trang 403 /403: nếu ROLE_MEMBER vào trang admin, sẽ bị redirect về trang này

Cơ sở dữ liệu

Đầu vào của bài toán sẽ gồm có 2 thực thể là User (người dùng) và Role (quyền hạn). Hai thực thể này có mối liên kết là Many to Many, nên cơ sở dữ liệu sẽ gồm có 3 bảng như sau:

Bảng user:

CREATE TABLE `user` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `email` varchar(255) NOT NULL,
 `password` varchar(60) NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Do mình sẽ lựa chọn thuật toán Bcrypt để mã hóa mật khẩu nên trường password sẽ có độ dài 60 ký tự.

Bảng role:

CREATE TABLE `role` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(255) NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Bảng user_role:

CREATE TABLE `user_role` (
 `user_id` int(11) NOT NULL,
 `role_id` int(11) NOT NULL,
 PRIMARY KEY (`user_id`,`role_id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Khởi tạo dự án

Với công cụ Spring Tool Suite, chúng ta có thể nhanh chóng tạo 1 dự án Spring bằng Spring Boot như sau:

Đầu tiên, ta chọn File -> New -> Other:
alt text

Gõ từ khóa spring, chọn Spring Starter Project rồi chọn Next:
alt text

Sau đó nhập các thông tin cơ bản như Name, Group, Artifact, Package. Các bạn chú ý Name không được có dấu cách nhé. Ở đây mình sẽ chọn Type là Maven và Java Version là 8:
alt text

Sau đó, chọn Next. Spring Boot sẽ yêu cầu chọn các dependency cho dự án. Ở đây, mình sẽ chọn các dependency sau:

  • Web ở trong nhóm Web
  • Security ở trong nhóm Core
  • JPA và MySQL trong nhóm SQL
  • Thymeleaf trong nhóm Template Engines

Sau đó chọn Next:
alt text

Chọn Finish:
alt text

Qúa trình khởi tạo dự án bắt đầu. Spring Boot sẽ tự động tải các dependency mà ta đã chọn ở trên và tiến hành cấu hình:
alt text

File 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.yuen.spring</groupId>
    <artifactId>security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringSecurity</name>
    <description>Demo project for Spring Security</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Ta thấy cấu hình chung của dự án sẽ gói gọn trong file SpringMyContactApplication.java:

package com.yuen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringSecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityApplication.class, args);
    }
}


Cấu hình Spring Security

Để kích hoạt Spring Security, trước tiên ta cần phải viết một lớp kế thừa abstract class WebSecurityConfigurerAdapter:

WebSecurityConfig.java

package com.yuen.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

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

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/register").permitAll()
                .antMatchers("/").hasRole("MEMBER")
                .antMatchers("/admin").hasRole("ADMIN")
                .and()
            .formLogin()
                .loginPage("/login")
                .usernameParameter("email")
                .passwordParameter("password")
                .defaultSuccessUrl("/")
                .failureUrl("/login?error")
                .and()
            .exceptionHandling()
                .accessDeniedPage("/403");
    }

}

Thoạt đầu nhìn lớp này chắc các bạn có vẻ hơi ngợp. Ok, các bạn cứ từ từ nghe mình giải thích nhé :D

  • Annotation @Configuration xác định lớp WebSecurityConfig của ta là một lớp dùng để cấu hình.
  • Annotation @EnableWebSecurity sẽ kích hoạt việc tích hợp Spring Security với Spring MVC.
  • Trong lớp WebSecurityConfig, ta cần phải gọi đến interface UserDetailsService để cấu hình. Do đó ta sẽ inject UserDetailsService.
  • Trong Spring Security, việc mã hóa mật khẩu sẽ do interface PasswordEncoder đảm nhận. PasswordEncoder có implementation là BCryptPasswordEncoder sẽ giúp chúng ta mã hóa mật khẩu bằng thuật toán BCrypt. Nhưng để sử dụng được PasswordEncoder, ta phải cấu hình để PasswordEncoder trở thành một Bean.
  • Trong phương thức configure(HttpSecurity http), ta sẽ cấu hình các chi tiết về bảo mật:

Phân quyền request
Trong đoạn code:

.authorizeRequests()
    .antMatchers("/register").permitAll()
    .antMatchers("/").hasRole("MEMBER")
    .antMatchers("/admin").hasRole("ADMIN")
    .and()

antMatchers(): khai báo đường dẫn của request
permitAll(): cho phép tất cả các user đều được phép truy cập.
hasRole(roleName): chỉ cho phép các user có GrantedAuthority là ROLE_roleName mới được phép truy cập
Các bạn có thể tham khảo thêm các phương thức khác tại đây

Đăng nhập
Trong đoạn code:

.formLogin()
    .loginPage("/login")
    .usernameParameter("email")
    .passwordParameter("password")
    .defaultSuccessUrl("/")
    .failureUrl("/login?error")
    .and()

loginPage(): đường dẫn tới trang chứa form đăng nhập
usernameParameter()passwordParameter(): gía trị của thuộc tính name của 2 input nhập username và password
defaultSuccessUrl(): đường dẫn tới trang đăng nhập thành công
failureUrl(): đường dẫn tới trang đăng nhập thất bại

Trong Spring Security, trang xử lý submit form mặc định là /login. Nếu bạn muốn custom thì có thể dùng loginProcessingUrl().

Đăng xuất
Trong Spring Security, mặc định trang đăng xuất có đường dẫn là /logout. Sau khi đăng xuất, sẽ redirect về trang /login?logout. Ở đây mình sẽ giữ nguyên cấu hình mặc định. Nếu các bạn muốn custom thì có thể tham khảo tại đây

Từ chối truy cập
Khi người dùng không đủ quyền để truy cập vào một trang, ta sẽ redirect người dùng về một trang 403 nào đó:

.exceptionHandling()
                .accessDeniedPage("/403");

CSRF token
Nếu chúng ta sử dụng annotation @EnableWebSecurity và template engine Thymeleaf thì form login mặc định sẽ có thêm CSRF token, nên chúng ta không cần phải cấu hình gì thêm.


DataAccess layer

Cấu hình DataSource, JPA, Hibernate

Các cấu hình liên quan đến DataSource, JPA, Hibernate chúng ta sẽ viết trong file application.properties của java/main/resources. Các bạn có thể tham khảo cấu hình sau:

# ===============================
# DATASOURCE
# ===============================

# Set here configurations for the database connection

# Connection url for the database "mycontact"
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false

# MySQL username and password 
spring.datasource.username=root
spring.datasource.password=root

# Keep the connection alive if idle for a long time (needed in production)
spring.datasource.dbcp.test-while-idle=true
spring.datasource.dbcp.validation-query=SELECT 1

# ===============================
# JPA / HIBERNATE
# ===============================

# Use spring.jpa.properties.* for Hibernate native properties (the prefix is
# stripped before adding them to the entity manager).

# Show or not log for each sql query
spring.jpa.show-sql=true

# Hibernate ddl auto (create, create-drop, update): with "update" the database
# schema will be automatically updated accordingly to java entities found in
# the project
spring.jpa.hibernate.ddl-auto=update

# Naming strategy
spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.ImprovedNamingStrategy

# Allows Hibernate to generate SQL optimized for a particular DBMS
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect


Domain

Do có 2 thực thể User và Role nên mình sẽ tạo 2 lớp User.javaRole.java trong package com.yuen.domain. Các bạn chú ý 2 lớp này đều phải implement Serializable và có các getter/setter cho các thuộc tính để sau này chúng ta có thể truy xuất các thuộc tính trong View..

Lớp User.java

package com.yuen.domain;

import java.io.Serializable;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

@Entity
@Table(name = "user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    private int id;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "password", nullable = false)
    private String password;

    @ManyToMany
    @JoinTable(
            name = "user_role",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

}

Lớp Role.java

package com.yuen.domain;

import java.io.Serializable;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

@Entity
@Table(name = "role")
public class Role implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    private int id;

    @Column(name = "name", nullable = false)
    private String name;

    @ManyToMany(mappedBy = "roles")
    private Set<User> users;

    public Role() {
    }

    public Role(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<User> getUsers() {
        return users;
    }

    public void setUsers(Set<User> users) {
        this.users = users;
    }

}

Trong đó, annotation @ManyToMany@JoinTable xác định các thành phần tham gia vào liên kết Many to Many của 2 bảng User và Role.


Repository

UserRepository.java

package com.yuen.repository;

import org.springframework.data.repository.CrudRepository;

import com.yuen.domain.User;

public interface UserRepository extends CrudRepository<User, Integer> {

    User findByEmail(String email);

}

RoleRepository.java

package com.yuen.repository;

import org.springframework.data.repository.CrudRepository;

import com.yuen.domain.Role;

public interface RoleRepository extends CrudRepository<Role, Integer> {

    Role findByName(String name);

}


Service Layer

UserDetailsService cũng giống như các interface Service thông thường, cũng cần phải có implementation nên mình sẽ tạo một lớp UserDetailsServiceImpl implements UserDetailsService:

package com.yuen.service;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.transaction.annotation.Transactional;

import com.yuen.domain.Role;
import com.yuen.domain.User;
import com.yuen.repository.UserRepository;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }

        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
        Set<Role> roles = user.getRoles();
        for (Role role : roles) {
            grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
        }

        return new org.springframework.security.core.userdetails.User(
                user.getEmail(), user.getPassword(), grantedAuthorities);
    }

}

Lớp UserDetailsServiceImpl sẽ phải ghi đè phương thức loadByUsername(String username) của UserDetailsService.

Tham số username ở đây ta cần phải hiểu là email mà người dùng nhập vào. Mình sẽ tìm kiếm trong CSDL user thỏa mãn username này. Nếu như không tìm thấy, sẽ throw new UsernameNotFoundException("User not found");

Phương thức này sẽ trả về một org.springframework.security.core.userdetails.User - là một implementation của UserDetails. Hàm khởi tạo của lớp này sẽ nhận 3 tham số: username, password và grantedAuthorities của user. Cách tìm grantedAuthorities các bạn có thể xem trong đoạn code trên.


Web Layer

Controller

Do việc xử lý submit form login sẽ do Spring Security quản lý, nên trong Controller ta chỉ cần khai báo trang hiển thị form login.

MainController.java

package com.yuen.controller;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {

    @GetMapping("/")
    public String index() {
        return "index";
    }

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

    @GetMapping("/403")
    public String accessDenied() {
        return "403";
    }

    @GetMapping("/login") 
    public String getLogin() {
        return "login";
    }

}


View

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Login Page</title>
<link href="../static/css/style.css" th:href="@{/css/style.css}" rel="stylesheet" />
</head>
<body>
    <div class="main-content">
        <p th:if="${param.error}" class="error">Invalid email or password</p>
        <p th:if="${param.logout}" class="success">You have been logged out</p>
        <h3>Login to continue</h3>
        <form th:action="@{/login}" method="POST">
            <input type="text" name="email" placeholder="Your email" /><br />
            <input type="password" name="password" placeholder="Your password" /><br />
            <button type="submit">Login</button> <br />
        </form>
    </div>
</body>
</html>

Trong đó:

  • Ta sẽ lấy các tham số trên URL thông qua đối tượng ${param}.
  • /login là trang mặc định xử lý submit form login trong Spring Security.

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" 
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8" />
<title>Index Page</title>
<link href="../static/css/style.css" th:href="@{/css/style.css}" rel="stylesheet" />
</head>
<body>
    Logged user: <span sec:authentication="name"></span> <br/> 
    Roles: <span sec:authentication="principal.authorities"></span>
    <form action="#" th:action="@{/logout}" method="POST">
        <button type="submit">Logout</button>
    </form>
</body>
</html>

Trong trang chủ, mình sẽ show ra username và roles của người dùng. Để làm được điều này, ta cần phải tích hợp thêm phần mở rộng của Thymeleaf:
Trong pom.xml, ta thêm dependency:

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>

Trang đăng xuất mặc định trong Spring Security là /logout và phải để trong 1 form có HTTP method là POST. Nếu các bạn muốn chuyển về dạng như thế này: <a th:href='/logout'>Logout</a>, thì trong Controller, ta sẽ phải viết phưong thức logout như sau:

@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth != null) {
        new SecurityContextLogoutHandler().logout(request, response, auth);
    }
    return "redirect:/";
}

admin.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Admin Page</title>
</head>
<body>
    <h2>Welcome, Admin</h2>
</body>
</html>

403.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>403 Page</title>
</head>
<body>
    <h2>403 Error: Access denied</h2>
</body>
</html>


Data seeding

Trước khi chạy dự án, ta sẽ tạo các seeder. Cụ thể ở đây mình sẽ tạo 2 role là ROLE_ADMINROLE_MEMBER (nếu 2 role này chưa có trong CSDL) và 2 tài khoản admin và member:

package com.yuen.config;

import java.util.HashSet;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import com.yuen.domain.Role;
import com.yuen.domain.User;
import com.yuen.repository.RoleRepository;
import com.yuen.repository.UserRepository;

@Component
public class DataSeedingListener implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired 
    private PasswordEncoder passwordEncoder;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent arg0) {
        // Roles
        if (roleRepository.findByName("ROLE_ADMIN") == null) {
            roleRepository.save(new Role("ROLE_ADMIN"));
        }

        if (roleRepository.findByName("ROLE_MEMBER") == null) {
            roleRepository.save(new Role("ROLE_MEMBER"));
        }

        // Admin account
        if (userRepository.findByEmail("admin@gmail.com") == null) {
            User admin = new User();
            admin.setEmail("admin@gmail.com");
            admin.setPassword(passwordEncoder.encode("123456"));
            HashSet<Role> roles = new HashSet<>();
            roles.add(roleRepository.findByName("ROLE_ADMIN"));
            roles.add(roleRepository.findByName("ROLE_MEMBER"));
            admin.setRoles(roles);
            userRepository.save(admin);
        }

        // Member account
        if (userRepository.findByEmail("member@gmail.com") == null) {
            User user = new User();
            user.setEmail("member@gmail.com");
            user.setPassword(passwordEncoder.encode("123456"));
            HashSet<Role> roles = new HashSet<>();
            roles.add(roleRepository.findByName("ROLE_MEMBER"));
            user.setRoles(roles);
            userRepository.save(user);
        }
    }

}
  • Ở đây mình sử dụng AplicationListener để publish một sự kiện trong Spring. Với tham số ContextRefreshedEvent, đoạn code trong onApplicationEvent() sẽ được publish khi Spring Context start hoặc refresh.
  • Để mã hóa được mật khẩu, ta sẽ inject PasswordEncoder, rồi dùng hàm encode()để mã hóa.

Đây là cây thư mục cuối cùng của dự án:
alt text


Chạy dự án

Để chạy dự án, ta sẽ chuột phải tên dự án, rồi chọn Run As -> Spring Boot App:
alt text

Lúc này dự án của chúng ta sẽ chạy trên cổng 8080.

Đầu tiên, ta vào đường dẫn http://localhost:8080/, thì sẽ bị redirect về trang login http://localhost:8080/login:
alt text

Thử nhập sai email hoặc password:
alt text

Đăng nhập vào tài khoản member:
alt text

Sau đó truy cập vào trang /admin thì sẽ bị redirect về /403:
alt text

Sau khi đăng xuất:
alt text

Đăng nhập vào tài khoản admin:
alt text

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Nguyễn Tuấn Anh

9 bài viết.
77 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
33 28
MyContact là một ứng dụng mà mình thường viết mỗi khi học một ngôn ngữ hay công nghệ mới. MyContact chỉ là một ứng dụng CRUD đơn giản, cho phép ngư...
Nguyễn Tuấn Anh viết hơn 1 năm trước
33 28
White
12 0
Giới thiệu Spring Framework Trong bài viết này, mình sẽ giới thiệu cho các bạn về một trong những Java EE framework rất nổi bật và phổ biến hiện ...
Nguyễn Tuấn Anh viết hơn 1 năm trước
12 0
White
7 5
Viết ứng dụng đầu tiên với Spring Trong bài viết này, mình sẽ hướng dẫn các bạn viết ứng dụng HelloWorld kinh điển trong Spring. Các công cụ mình...
Nguyễn Tuấn Anh viết hơn 1 năm trước
7 5
Bài viết liên quan
White
0 0
Trong bài viết này, một số hình ảnh hoặc nọi dung có thể bị thiếu do quá trình chế bản. Vui lòng xem nội dung ở blog gốc sau: (Link) (Link), chúng...
programmerit viết gần 3 năm trước
0 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
9 bài viết.
77 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!