Việc bảo mật thông tin giúp chúng ta giới hạn quyền truy cập vào các trang web, tài nguyên tĩnh, API hay các thông tin nhạy cảm của khách hàng là rất quan trọng.
Ở cấp độ cơ sở hạ tầng, chúng ta có thể áp dụng một số phương pháp, ví dụ như tường lửa (firewall), proxy server, whitelist IP,… Tuy nhiên, vẫn cần phải xây dựng bảo mật ở cấp độ ứng dụng, đặc biệt với những ứng dụng có logic phân quyền người dùng phức tạp thì chúng ta sẽ sử dụng đến việc phải đăng nhập để có thể bảo mật hơn. Với Spring Security, mọi thứ sẽ đơn giản hơn rất nhiều.
Spring security là gì?
Đầu tiên chúng ta nên tìm hiểu Spring Security là gì. Spring Security một phần của Spring Framework, là framework hỗ trợ lập trình viên triển khai các biện pháp bảo mật ở cấp độ ứng dụng. Cùng với Spring MVC, cả hai là bộ khung toàn diện để phát triển các hệ thống web an toàn và bảo mật cao.
Spring Security hoạt động xoay quanh 2 vấn đề chính là xử lý xác thực và xử lý ủy quyền ở cấp độ Web request cũng như cấp độ method invocation.
Spring Security rất mạnh mẽ và có khả năng tùy biến cao, lập trình viên có thể sử dụng cấu hình có sẵn của framework hoặc tùy chỉnh theo từng bài toán của hệ thống.
Bài viết này sẽ tập trung giới thiệu về Spring Security và các tính năng chính của framework này trong việc xây dựng phát triển ứng dụng.
1. Giới thiệu
Trong bài hôm nay chúng ta sẽ tìm hiểu sự kết hợp giữa Spring Security một phần cực kỳ quan trọng trong các hệ thống bảo mật ngày nay, đó là JWT .
JWT (Json web Token) là một chuỗi mã hóa được gửi kèm trong Header của client request có tác dụng giúp phía server xác thực request người dùng có hợp lệ hay không. Được sử dụng phổ biến trong các hệ thống API ngày nay.
2. Cài đặt
Ở dự án này tôi sử dụng java 8 và maven 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>base_java</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>base_java</name>
<description>base_java</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.13</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.json/json -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20230227</version>
</dependency>
<dependency>
<groupId>org.glassfish.hk2.external</groupId>
<artifactId>bean-validator</artifactId>
<version>2.4.0-b12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Cấu trúc thư mục của tôi ở dự án này:
2.1 Implement
Ban đầu, chúng ta sẽ tạo ra class User
và UserDetails
để giao tiếp với Spring Security.Trong bài viết có sử dụng Lombok
2.2 Tạo User
Tạo ra class User
tham chiếu với database.
package com.example.base_java.entity;
import com.example.base_java.entity.enumeration.Sex;
import lombok.Data;
import org.hibernate.annotations.Where;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
@Entity
@Data
@Where(clause = "is_deleted = false")
public class User extends BaseEntity {
private String userName;
private String password;
private String roleId;
private String email;
private String firstName;
private String lastName;
private String imageUrl;
@Enumerated(EnumType.STRING)
@Column(length = 20, nullable = false)
private Sex sex;
private String phone;
}
2.3 Tạo UserRepository
kế thừa JpaRepository
để truy xuất thông tin từ database.
package com.example.base_java.repository;
import com.example.base_java.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, String> {
User findByUserName(String username);
}
2.4 Tham chiếu User với UserDetails
Mặc định Spring Security sử dụng một đối tượng UserDetails
để chứa toàn bộ thông tin về người dùng. Vì vậy, chúng ta cần tạo ra một class mới giúp chuyển các thông tin của User
thành UserDetails
CustomUserDetails.java
package com.example.base_java.config;
import com.example.base_java.entity.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Data
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {
User user;
String role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority(role));
}
// // bạn cũng có thể đặt mặc định role khi đăng nhập bằng 1 role bất kì
// @Override
// public Collection<? extends GrantedAuthority> getAuthorities() {
// // Mặc định mình sẽ để tất cả là ROLE_USER. Để demo cho đơn giản.
// return Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
// }
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Khi người dùng đăng nhập thì Spring Security sẽ cần lấy các thông tin UserDetails
hiện có để kiểm tra. Vì vậy, bạn cần tạo ra một class kế thừa lớp UserDetailsService
mà Spring Security cung cấp để làm nhiệm vụ này.
UserService.java
package com.example.base_java.config.security;
import com.example.base_java.config.CustomUserDetails;
import com.example.base_java.entity.Role;
import com.example.base_java.entity.User;
import com.example.base_java.repository.RoleRepository;
import com.example.base_java.repository.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 javax.transaction.Transactional;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Override
public UserDetails loadUserByUsername(String username) {
// Kiểm tra xem user có tồn tại trong database không?
User user = userRepository.findByUserName(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
String role = roleRepository.findById(user.getRoleId()).map(Role::getRoleName).orElse("USER"); //mặc định sẽ gán quyền User cho các tài khoản không có role
return new CustomUserDetails(user, role);
}
// JWTAuthenticationFilter sẽ sử dụng hàm này
@Transactional
public UserDetails loadUserById(String id) {
User user = userRepository.findById(id).orElseThrow(
() -> new UsernameNotFoundException("User not found with id : " + id)
);
String role = roleRepository.findById(user.getRoleId()).map(Role::getRoleName).orElse("ROLE_USER"); //mặc định sẽ gán quyền User cho các tài khoản không có role
return new CustomUserDetails(user, role);
}
}
2.5 Cấu hình JWT
Sau khi có các thông tin về người dùng, chúng ta cần mã hóa thông tin người dùng thành chuỗi JWT
. Tôi sẽ tạo ra một class JwtTokenProvider
để làm nhiệm vụ này.
package com.example.base_java.config.jwt;
import com.example.base_java.config.CustomUserDetails;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
@Slf4j
public class JwtTokenProvider {
// chữ kí
private final String JWT_SECRET = "hihi";
// thời hạn token
private final long JWT_EXPIRATION = 604800000L;
public String generateToken(CustomUserDetails userDetails) {
// Lấy thông tin user
Date now = new Date();
Date expiryDate = new Date(now.getTime() + JWT_EXPIRATION);
// Tạo chuỗi json web token từ id của user.
return Jwts.builder()
.setSubject(userDetails.getUser().getId())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, JWT_SECRET)
.compact();
}
public String getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(JWT_SECRET)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(authToken);
return true;
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
log.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty.");
}
return false;
}
}
2.6 Cấu hình và phân quyền
Bây giờ, chúng ta bắt đầu cấu hình Spring Security bao gồm việc kích hoạt bằng @EnableWebSecurity
.
package com.example.base_java.config.security;
import com.example.base_java.config.jwt.AuthenticationEntryPointJwt;
import com.example.base_java.config.jwt.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter implements WebMvcConfigurer{
public final UserService userService;
@Autowired
private AuthenticationEntryPointJwt unauthorizedHandler;
public SecurityConfiguration(UserService userService) {
this.userService = userService;
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
// Get AuthenticationManager Bean
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
// Password encoder, để Spring Security sử dụng mã hóa mật khẩu người dùng
return new BCryptPasswordEncoder();
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
// Phương thức này dùng để xác thực người dùng (authentication)
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userService) // Cung cấp userservice cho spring security
.passwordEncoder(passwordEncoder()); // cung cấp password encoder
}
// tạo list tất cả những api không cần quyền
public static List<RequestMatcher> PERMIT_ALL_URLS = Arrays.asList(
new AntPathRequestMatcher("/auth/login"),
new AntPathRequestMatcher("/auth/register")
);
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.requestMatchers(PERMIT_ALL_URLS.toArray(new RequestMatcher[]{})).permitAll()
.anyRequest().authenticated() // Tất cả các request khác đều cần phải xác thực mới được truy cập
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Thêm một lớp Filter kiểm tra jwt
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
Điểm khác biệt ở đây là sự xuất hiện của JwtAuthenticationFilter
. Đây là một lớp Filter
do tôi tự tạo ra.
JwtAuthenticationFilter
Có nhiệm vụ kiểm tra request của người dùng trước khi nó tới đích. Nó sẽ lấy Header Authorization
ra và kiểm tra xem chuỗi JWT người dùng gửi lên có hợp lệ không.
package com.example.base_java.config.jwt;
import com.example.base_java.config.security.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserService customUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
if(userDetails != null) {
UsernamePasswordAuthenticationToken
authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails
.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception ex) {
log.error("failed on set user authentication", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
// Kiểm tra xem header Authorization có chứa thông tin jwt không
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
2.7 Tạo Controller
Vì phần này chúng ta làm việc với JWT
, nên các request sẽ dưới dạng Rest API.
Tôi tạo ra 3 api:
/api/auth/login
: Cho phép request mà không cần xác thực./api/auth/hihi
: Là một api bất kỳ nào đó, phải xác thực mới lấy được thông tin./api/auth/register
: Cho phép tạo một user mặc định
2.8 Tạo thông tin User trong database
Trước hết bạn cần cấu hình cho hibernate kết tới tới h2 database trong file resources/appication.properties
server.port=8686
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:6868/demo
spring.datasource.username=demo
spring.datasource.password=demo
spring.datasource.driver-class-name =com.mysql.jdbc.Driver
spring.main.allow-circular-references: true
server.servlet.context-path=/api
#spring.jpa.show-sql: trueCopy
spring.graphql.graphiql.enabled=true
spring.application.name=hihi
upload.folder.path=src/main/resources/uploads
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
Chúng ta sẽ call api /api/auth/register
để tạo ra một tài khoản
- Phần xử lí thông tin tài khoản, ở đây tôi có sử dụng một bảng role để map với từng tài khoản với nhau, nếu role Id không tồn tại thì mặc định role đăng nhập ROLE_USER
3. Chạy thử
Tôi sẽ thực hiện chạy trên postman
3.1 Tạo tài khoản
3.2 Chạy api login và lấy mã token
Chúng ta sẽ sử dụng tài khoản đã được tạo ở api register để đăng nhập
Sau khi login chúng ta sẽ nhận được một mã token và mã token này sẽ được sử dụng để thực hiện các request khác.
3.3 Chạy thử api khi chưa và có token
Khi chạy project lên sau đó chúng ta request thử tới địa chỉ http://localhost:8686/api/auth/hihi
mà không xác thực.
Kết quả trả về mã lỗi 403
kèm theo message Access Denied
.
- Bây giờ chúng ta sẽ sử dụng đến token sau khi đã login
Khi này chúng ta đã có thể thực hiện request này thành công.
4. Kết luận
Bài viết đã hoàn thành vai trò giới thiệu và tóm tắt những tính năng chính của Spring Security và JWT trong việc phát triển ứng dụng Web.
Với Spring Security, lập trình viên có thể tùy chỉnh bảo mật cho hệ thống của mình theo cách đơn giản nhất, thông qua các cấu hình và annotation. Điều này giúp lập trình viên có thể dễ dàng tiếp cận những khía cạnh bảo mật, rút ngắn thời gian phát triển cũng như chi phí vận hành và bảo trì hệ thống.
Rất cảm ơn mọi người đã dành thời gian đọc bài viết của mình, hi vọng nó có thể giúp ích được cho các bạn, nếu có gì chưa chính xác thì mọi người có thể đóng góp ý kiến và cho mình lời khuyên.
Link git soucre code tham khảo: https://gitlab.com/maypham/base_java
8 comments
Hay quá
rất cảm ơn bạn
100 điểm
hihi
Bài viết rất hay, mong bạn viết thêm những bài viết liên quan đến chủ đề này.
rất cảm ơn bạn hy vọng những kiến thức này có thể giúp ích được cho bạn !!
Mình đã thử và thành công. Mình làm được, bạn cũng làm được.
tôi rất hạnh phúc khi bài viết này đã giúp ích được cho bạn