Bảo mật Spring Boot RESTful Service sử dụng Basic Authentication

Xem thêm các chuyên mục:

Nhóm thành viên của o7planning đã xây dựng một website tuyệt vời và miễn phí giúp mọi người học tiếng Anh, học từ vựng dễ dàng hơn. Hãy truy cập để học tiếng Anh ngay bây giờ:

1- Mục tiêu của ví dụ

Tài liệu được viết dựa trên:
  • Spring Boot 2.x (Or >= 1.5.9)

  • Eclipse 4.7 Oxygen

Xem thêm:
Trong bài viết này, tôi sẽ hướng dẫn bạn tạo ra một ứng dụng RESTful Web Service và bảo mật nó với Basic Authentication. Điều này có nghĩa là ứng dụng của bạn sẽ cung cấp các nguồn dữ liệu (Resource), nhưng người dùng muốn sử dụng nguồn dữ liệu này họ phải được xác thực (authenticate) với phương thức xác thực cơ bản (Basic Authentication).

Basic Authentication (Xác thực cơ bản)

Để truy cập vào các nguồn dữ liệu (Resource) được bảo mật bởi Basic Authentication, người dùng phải gửi một request và trong request đó có chứa thông tin username/password được đính kèm trên Header.
Bạn có thể sử dụng trình duyệt để truy cập vào một nguồn dữ liệu được bảo mật bởi Basic Authentication, trong trường hợp này một hộp thoại (dialog) sẽ hiển thị cho phép bạn nhập vào username/password, thông tin này sẽ được đính kèm trong request để gửi đến REST Server.

2- Tạo Spring Boot project

Trên Eclipse tạo một dự án Spring Boot.
Nhập vào:
  • Name: SbRestBasicAuthentication
  • Group: org.o7planning
  • Package: org.o7planning.sbrestbasicauth
Bước tiếp theo bạn cần lựa chọn các công nghệ sẽ sử dụng.
SbRestBasicAuthenticationApplication.java
package org.o7planning.sbrestbasicauth;

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

@SpringBootApplication
public class SbRestBasicAuthenticationApplication {

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

3- Cấu hình pom.xml

Trong ví dụ này chúng ta cần một thư viện để chuyển đổi (convert) XML sang đối tượng Java và ngược lại. Và một thư viện khác để chuyển đổi JSON sang Java và ngược lại.

JSON <==> Java

spring-boot-starter-web đã tích hợp sẵn thư viện jackson-databind, thư viện này giúp chuyển đổi JSON thành đối tượng Java và ngược lại.
 

XML <==> Java

Spring Boot sử dụng JAXB (Có sẵn trong JDK) như là một thư viện mặc định để chuyển đổi XMLJava. Tuy nhiên các lớp Java của bạn cần phải được chú thích (annotated) bởi @XmlRootElement,... Vì vậy lời khuyên của tôi là bạn nên sử dụng jackson-dataformat-xml như là một thư viện để chuyển đổi XMLJava. Để sử dụng jackson-dataformat-xml bạn cần khai báo nó trong tập tin pom.xml:
** pom.xml **
...

<dependencies>

        ...
    
    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
    </dependency>

    ...

</dependencies>

...
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>org.o7planning</groupId>
    <artifactId>SbRestBasicAuthentication</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SbRestBasicAuthentication</name>
    <description>Spring Boot +Rest + Basic Authentication</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.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-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
                
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>
        

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-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>

4- Security & AuthenticationEntryPoint

Các cấu hình bảo mật sẽ được viết trong lớp WebSecurityConfig. Trong bài viết này tôi không tập trung vào "Làm thế nào để lấy được các username trong cơ sở dữ liệu", vì vậy chúng ta tạo ra 2 UserName cố định và lưu trữ trong bộ nhớ. Người dùng truy cập vào nguồn dữ liệu của REST Service sẽ đăng nhập với 1 trong 2 username này.
WebSecurityConfig.java
package org.o7planning.sbrestbasicauth.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.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
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.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private AuthenticationEntryPoint authEntryPoint;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable();

		// Tất cả các request gửi tới Web Server yêu cầu phải được xác thực
		// (authenticated).
		http.authorizeRequests().anyRequest().authenticated();

		// Sử dụng AuthenticationEntryPoint để xác thực user/password
		http.httpBasic().authenticationEntryPoint(authEntryPoint);
	}

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

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		
		String password = "123";

		String encrytedPassword = this.passwordEncoder().encode(password);
		System.out.println("Encoded password of 123=" + encrytedPassword);
		
		
		InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> //
		mngConfig = auth.inMemoryAuthentication();

		// Định nghĩa 2 người dùng, lưu trữ trong bộ nhớ.
		// ** Spring BOOT >= 2.x (Spring Security 5.x)
		// Spring auto add ROLE_
		UserDetails u1 = User.withUsername("tom").password(encrytedPassword).roles("USER").build();
		UserDetails u2 = User.withUsername("jerry").password(encrytedPassword).roles("USER").build();

		mngConfig.withUser(u1);
		mngConfig.withUser(u2);

		// If Spring BOOT < 2.x (Spring Security 4.x)):
		// Spring auto add ROLE_
		// mngConfig.withUser("tom").password("123").roles("USER");
		// mngConfig.withUser("jerry").password("123").roles("USER");
	}

}
Lớp AuthenticationEntryPointImpl mở rộng (extends) từ lớp BasicAuthenticationEntryPoint, nó được sử dụng để kiểm tra username/password đính kèm theo request có hợp lệ hay không.
AuthenticationEntryPointImpl.java
package org.o7planning.sbrestbasicauth.auth;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
public class AuthenticationEntryPointImpl extends BasicAuthenticationEntryPoint {

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
			throws IOException, ServletException {
		response.addHeader("WWW-Authenticate", "Basic realm=" + getRealmName());
		response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
		PrintWriter writer = response.getWriter();
		writer.println("HTTP Status 401 - " + authEx.getMessage());
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		// RealmName xuất hiện trên cửa sổ đăng nhập (Firefox).
		setRealmName("o7planning");
		super.afterPropertiesSet();
	}

}

5- Model, DAO, Controller

Lớp Employee đại diện cho một nhân viên.
Employee.java
package org.o7planning.sbrestbasicauth.model;

public class Employee {

    private String empNo;
    private String empName;
    private String position;

    public Employee() {

    }

    public Employee(String empNo, String empName, String position) {
        this.empNo = empNo;
        this.empName = empName;
        this.position = position;
    }

    public String getEmpNo() {
        return empNo;
    }

    public void setEmpNo(String empNo) {
        this.empNo = empNo;
    }

    public String getEmpName() {
        return empName;
    }

    public void setEmpName(String empName) {
        this.empName = empName;
    }

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }

}
Lớp EmployeeDAO được chú thích (annotate) bởi @Repository để thông báo với Spring rằng nó là một Spring BEAN. Lớp này bao gồm các phương thức giúp truy vấn danh sách các nhân viên (employee), tạo nhân viên, sửa đổi thông tin nhân viên, và xóa nhân viên.
EmployeeDAO.java
package org.o7planning.sbrestbasicauth.dao;
 

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.o7planning.sbrestbasicauth.model.Employee;
import org.springframework.stereotype.Repository;

@Repository
public class EmployeeDAO {

    private static final Map<String, Employee> empMap = new HashMap<String, Employee>();

    static {
        initEmps();
    }

    private static void initEmps() {
        Employee emp1 = new Employee("E01", "Smith", "Clerk");
        Employee emp2 = new Employee("E02", "Allen", "Salesman");
        Employee emp3 = new Employee("E03", "Jones", "Manager");

        empMap.put(emp1.getEmpNo(), emp1);
        empMap.put(emp2.getEmpNo(), emp2);
        empMap.put(emp3.getEmpNo(), emp3);
    }

    public Employee getEmployee(String empNo) {
        return empMap.get(empNo);
    }

    public Employee addEmployee(Employee emp) {
        empMap.put(emp.getEmpNo(), emp);
        return emp;
    }

    public Employee updateEmployee(Employee emp) {
        empMap.put(emp.getEmpNo(), emp);
        return emp;
    }

    public void deleteEmployee(String empNo) {
        empMap.remove(empNo);
    }

    public List<Employee> getAllEmployees() {
        Collection<Employee> c = empMap.values();
        List<Employee> list = new ArrayList<Employee>();
        list.addAll(c);
        return list;
    }

}
Lớp MainRESTController được chú thích (annotate) bởi @RestController để thông báo với Spring rằng nó là một Spring Restful Controller,
MainRESTController.java
package org.o7planning.sbcrudrestful.controller;

import java.util.List;

import org.o7planning.sbcrudrestful.dao.EmployeeDAO;
import org.o7planning.sbcrudrestful.model.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController

public class MainRESTController {

    @Autowired
    private EmployeeDAO employeeDAO;

    @RequestMapping("/")
    @ResponseBody
    public String welcome() {
        return "Welcome to RestTemplate Example.";
    }

    // URL:
    // http://localhost:8080/SomeContextPath/employees
    // http://localhost:8080/SomeContextPath/employees.xml
    // http://localhost:8080/SomeContextPath/employees.json
    @RequestMapping(value = "/employees", //
            method = RequestMethod.GET, //
            produces = { MediaType.APPLICATION_JSON_VALUE, //
                    MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public List<Employee> getEmployees() {
        List<Employee> list = employeeDAO.getAllEmployees();
        return list;
    }

    // URL:
    // http://localhost:8080/SomeContextPath/employee/{empNo}
    // http://localhost:8080/SomeContextPath/employee/{empNo}.xml
    // http://localhost:8080/SomeContextPath/employee/{empNo}.json
    @RequestMapping(value = "/employee/{empNo}", //
            method = RequestMethod.GET, //
            produces = { MediaType.APPLICATION_JSON_VALUE, //
                    MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public Employee getEmployee(@PathVariable("empNo") String empNo) {
        return employeeDAO.getEmployee(empNo);
    }

    // URL:
    // http://localhost:8080/SomeContextPath/employee
    // http://localhost:8080/SomeContextPath/employee.xml
    // http://localhost:8080/SomeContextPath/employee.json

    @RequestMapping(value = "/employee", //
            method = RequestMethod.POST, //
            produces = { MediaType.APPLICATION_JSON_VALUE, //
                    MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public Employee addEmployee(@RequestBody Employee emp) {

        System.out.println("(Service Side) Creating employee: " + emp.getEmpNo());

        return employeeDAO.addEmployee(emp);
    }

    // URL:
    // http://localhost:8080/SomeContextPath/employee
    // http://localhost:8080/SomeContextPath/employee.xml
    // http://localhost:8080/SomeContextPath/employee.json
    @RequestMapping(value = "/employee", //
            method = RequestMethod.PUT, //
            produces = { MediaType.APPLICATION_JSON_VALUE, //
                    MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public Employee updateEmployee(@RequestBody Employee emp) {

        System.out.println("(Service Side) Editing employee: " + emp.getEmpNo());

        return employeeDAO.updateEmployee(emp);
    }

    // URL:
    // http://localhost:8080/SomeContextPath/employee/{empNo}
    @RequestMapping(value = "/employee/{empNo}", //
            method = RequestMethod.DELETE, //
            produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
    @ResponseBody
    public void deleteEmployee(@PathVariable("empNo") String empNo) {

        System.out.println("(Service Side) Deleting employee: " + empNo);

        employeeDAO.deleteEmployee(empNo);
    }

}

Giải thích:

  • produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE }
  • produces = { "application/json" , "application/xml" }
Thuộc tính produces được sử dụng để quy định một URL sẽ chỉ tạo ra (trả về cho người dùng) các dữ liệu với các định dạng nào. Chẳng hạn "application/json", "application/xml".

6- Chạy ứng dụng

Để chạy ứng dụng, nhấn phải chuột vào Project chọn:
  • Run As/Spring Boot App
Test ứng dụng với trình duyệt:
Xem thêm:

Xem thêm các chuyên mục: