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.