Cấu hình xác thực người dùng với Spring Security

1. Thiết lập dự án

1.1 Thêm các dependency cần thiết

Ngoài các dependency có sẵn, cần thêm thủ công dependency cho Druid connection pool:
<!--Khai báo druid connection pool-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.10</version>
    </dependency>
Cấu hình resources trong pom.xml để Mapper.xml và mapper cùng thư mục:
<build>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
      </resource>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>**/*.xml</include>
        </includes>
      </resource>
    </resources>
</build>    
Thêm dependency Lombok:
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.8</version>
      <scope>provided</scope>
    </dependency>

1.2 Cấu hình file application.yml

spring:
  #Cấu hình thông tin database
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    data-username: root
    password: password123
    url: jdbc:mysql://localhost:3306/ten_database?useUnicode=true&characterEncoding=utf8
    #Xử lý mã hóa ký tự
  http:
    encoding:
      charset: utf-8
      force-request: true
      force-response: true
server:
  port: 8080
  tomcat:
    uri-encoding: UTF-8

1.3 Cấu trúc thư mục dự án

1.4 Tạo Entity User

package com.example.auth.entity;

import java.util.Collection;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * User Entity - Đại diện cho người dùng trong hệ thống
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserEntity implements UserDetails {
  private static final long serialVersionUID = 1L;

  //Các trường dữ liệu

  /**
   * ID người dùng
   */
  private String id;

  /**
   * Họ và tên
   */
  private String fullName;

  /**
   * Số điện thoại
   */
  private String phoneNumber;

  /**
   * Điện thoại cố định
   */
  private String homePhone;

  /**
   * Địa chỉ
   */
  private String address;

  /**
   * Trạng thái hoạt động
   */
  private Boolean enabled;

  /**
   * Tên đăng nhập
   */
  private String username;

  /**
   * Mật khẩu
   */
  private String password;

  /**
   * Ảnh đại diện
   */
  private String avatar;

  /**
   * Ghi chú
   */
  private String note;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return null;
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }
}

1.5 Tạo Response Entity

package com.example.auth.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * Response Data - Wrapper cho dữ liệu trả về API
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse {

  /**
   * Mã trạng thái
   */
  private Integer statusCode;

  /**
   * Thông báo
   */
  private String message;

  /**
   * Dữ liệu
   */
  private Object data;


  public static ApiResponse ok(String message) {
    return new ApiResponse(200, message, null);
  }

  public static ApiResponse ok(String message, Object data) {
    return new ApiResponse(200, message, data);
  }

  public static ApiResponse fail(String message) {
    return new ApiResponse(500, message, null);
  }

  public static ApiResponse fail(String message, Object data) {
    return new ApiResponse(500, message, data);
  }

}

1.6 Cấu hình SecurityConfig

package com.example.auth.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.example.auth.entity.UserEntity;
import com.example.auth.entity.ApiResponse;
import com.example.auth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
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.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private UserService userService;

  //Bean mã hóa mật khẩu
  @Bean
  PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService);
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .anyRequest()
        .authenticated()
        .and()
        .formLogin()
        .usernameParameter("username")
        .passwordParameter("password")
        .loginProcessingUrl("/toLogin")
        //Chuyển hướng đến trang đăng nhập nếu chưa đăng nhập
        .loginPage("/login")
        .successHandler(new AuthenticationSuccessHandler() {

          /**
           * Xử lý khi đăng nhập thành công
           *
           * @param req Request
           * @param resp Response
           * @param authentication Thông tin xác thực
           */
          @Override
          public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter writer = resp.getWriter();
            UserEntity user = (UserEntity) authentication.getPrincipal();
            ApiResponse response = ApiResponse.ok("Đăng nhập thành công!", user);
            String jsonStr = new ObjectMapper().writeValueAsString(response);
            writer.write(jsonStr);
            writer.flush();
            writer.close();
          }
        }).failureHandler(new AuthenticationFailureHandler() {

      /**
       * Xử lý khi đăng nhập thất bại
       *
       * @param req Request
       * @param resp Response
       * @param exception Ngoại lệ xác thực
       */
      @Override
      public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException, ServletException {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter writer = resp.getWriter();
        ApiResponse response = ApiResponse.fail("Đăng nhập thất bại!");
        
        if (exception instanceof LockedException) {
          response.setMessage("Tài khoản bị khóa, vui lòng liên hệ quản trị viên!");
        } else if (exception instanceof BadCredentialsException) {
          response.setMessage("Tên đăng nhập hoặc mật khẩu không đúng!");
        } else if (exception instanceof DisabledException) {
          response.setMessage("Tài khoản đã hết hạn, vui lòng liên hệ quản trị viên!");
        } else if (exception instanceof AuthenticationCredentialsNotFoundException) {
          response.setMessage("Tài khoản chưa được xác thực, vui lòng liên hệ quản trị viên!");
        } else if (exception instanceof AccountStatusException) {
          response.setMessage("Trạng thái tài khoản bất thường, vui lòng liên hệ quản trị viên!");
        } else {
          response.setMessage("Đã xảy ra lỗi không xác định, vui lòng liên hệ quản trị viên!");
        }
        
        writer.write(new ObjectMapper().writeValueAsString(response));
        writer.flush();
        writer.close();
      }
    }).permitAll()
        .and()
        .logout()
        .logoutSuccessHandler(new LogoutSuccessHandler() {

          /**
           * Xử lý khi đăng xuất thành công
           *
           * @param req Request
           * @param resp Response
           * @param authentication Thông tin xác thực
           */
          @Override
          public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter writer = resp.getWriter();
            ApiResponse response = ApiResponse.ok("Đăng xuất thành công!");
            writer.write(new ObjectMapper().writeValueAsString(response));
            writer.flush();
            writer.close();
          }
        }).permitAll()
        .and()
        .csrf()
        .disable();
  }
}

1.7 Tạo Controller

API endpoint sau khi đăng nhập thành công:
package com.example.auth.controller;

import com.example.auth.entity.ApiResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MainController {

  @GetMapping("/hello")
  public Object hello() {
    return ApiResponse.ok("Xin chào thế giới!");
  }
}
API endpoint đăng nhập:
package com.example.auth.controller;

import com.example.auth.entity.ApiResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthController {

  @GetMapping("/login")
  public Object loginPage() {
    return ApiResponse.fail("Bạn chưa đăng nhập!");
  }

}

1.8 Tạo Service

package com.example.auth.service;

import com.example.auth.entity.UserEntity;
import com.example.auth.mapper.UserMapper;
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;

/**
 * Service xử lý thông tin người dùng
 */
@Service
public class UserService implements UserDetailsService {

  @Autowired
  private UserMapper userMapper;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserEntity user = userMapper.findByUsername(username);
    if (user == null) {
      throw new UsernameNotFoundException("Tên đăng nhập không tồn tại!");
    }
    return user;
  }
}

1.9 Tạo Mapper

package com.example.auth.mapper;

import org.apache.ibatis.annotations.Mapper;
import com.example.auth.entity.UserEntity;
import org.springframework.stereotype.Component;

@Mapper
@Component
public interface UserMapper {

  UserEntity findByUsername(String username);
}
File MyBatis XML mapping:
<?xml version="1.0" encoding="UTF-8" ?>

<mapper namespace="com.example.auth.mapper.UserMapper">
  <resultMap id="UserResultMap" type="com.example.auth.entity.UserEntity">
    <result property="id" column="id" jdbcType="INTEGER"/>
    <result property="fullName" column="full_name" jdbcType="VARCHAR"/>
    <result property="phoneNumber" column="phone_number" jdbcType="CHAR"/>
    <result property="homePhone" column="home_phone" jdbcType="VARCHAR"/>
    <result property="address" column="address" jdbcType="VARCHAR"/>
    <result property="enabled" column="enabled" jdbcType="BIT"/>
    <result property="username" column="username" jdbcType="VARCHAR"/>
    <result property="password" column="password" jdbcType="VARCHAR"/>
    <result property="avatar" column="avatar" jdbcType="VARCHAR"/>
    <result property="note" column="note" jdbcType="VARCHAR"/>
  </resultMap>

  <select id="findByUsername" resultMap="UserResultMap">
    SELECT * FROM users u WHERE u.username = #{username}
  </select>

</mapper>

2. Kiểm thử với Postman

2.1 Gọi API cần đăng nhập

Gọi endpoint /hello mà không có token/session, hệ thống sẽ chuyển hướng đến trang đăng nhập.

2.2 Thực hiện đăng nhập

Gửi request đến endpoint /toLogin với username và password.

2.3 Gọi lại API sau khi đăng nhập

Sau khi đăng nhập thành công, có thể truy cập endpoint /hello.

2.4 Đăng xuất

Gọi endpoint /logout để đăng xuất khỏi hệ thống.

2.5 Kiểm tra sau đăng xuất

Sau khi đăng xuất, các API yêu cầu xác thực sẽ trả về lỗi 401.

3. Các lỗi thường gặp

Lỗi: Tài khoản bị khóa liên tục khi đăng nhập Nếu gặp thông báo tài khoản bị khóa, nguyên nhân có thể do Entity User implement UserDetails và các phương thức sau trả về false: - isAccountNonExpired() - isAccountNonLocked() - isCredentialsNonExpired() - isEnabled() Giải pháp: Đổi tất cả các giá trị return false thành return true để cho phép đăng nhập bình thường.

Thẻ: spring-security Java authentication Backend spring-boot

Đăng vào ngày 23 tháng 6 lúc 16:07