Bảo mật Rest API trong SpringBoot sử dụng JWT
Đăng lúc: 10:35 AM - 13/04/2025 bởi Charles Chung - 40Trong bài này tôi sẽ hướng dẫn các bạn sinh JWT để xác thực các request tới rest api trong spring boot.

1. Giới thiệu về bảo mật REST API sử dụng JWT trong Spring Boot 3
Trong các ứng dụng web hiện đại, việc bảo mật các API là một yêu cầu bắt buộc nhằm đảm bảo rằng chỉ những người dùng đã được xác thực và có quyền mới có thể truy cập vào các tài nguyên hệ thống. Một trong những phương pháp phổ biến và hiệu quả để bảo vệ REST API là sử dụng JWT (JSON Web Token) – một chuẩn mở để truyền thông tin xác thực giữa client và server dưới dạng token an toàn và gọn nhẹ.
Với sự ra đời của Spring Boot 3 và Spring Security 6, cách cấu hình bảo mật cho REST API đã có nhiều thay đổi nhằm đơn giản hóa và chuẩn hóa các quy trình bảo mật, đồng thời hỗ trợ tốt hơn cho các kiến trúc không trạng thái (stateless), đặc biệt là khi kết hợp với JWT.
JWT hoạt động dựa trên nguyên tắc mã hóa thông tin người dùng vào một token sau khi đăng nhập thành công. Token này sẽ được client đính kèm trong các request tiếp theo dưới dạng Authorization
header. Server sẽ giải mã và xác thực token trước khi cho phép truy cập vào tài nguyên.
Khi kết hợp JWT với Spring Boot 3, quá trình bảo mật REST API sẽ bao gồm các bước chính như:
-
Xây dựng hệ thống xác thực và tạo JWT sau khi người dùng đăng nhập thành công.
-
Cấu hình bộ lọc (filter) để kiểm tra token trong mỗi request.
-
Tùy chỉnh Spring Security để kiểm soát truy cập dựa trên quyền (roles).
-
Triển khai các biện pháp bảo mật bổ sung như cấu hình CORS, CSRF, hoặc giới hạn truy cập theo IP nếu cần.
Việc sử dụng JWT giúp ứng dụng đạt được kiến trúc stateless, tăng tính mở rộng và phù hợp với các hệ thống phân tán như microservices.
2. Ví dụ
Trong ví dụ này chúng ta sẽ thực hiện các công việc sau:
- Đăng nhập: trước khi vào bất kỳ tài nguyên nào của rest api đều phải đăng nhập
- Sinh JWT: khi đăng nhập thành công thì trả về JWT cho người dùng, kèm cả thông tin của người dùng.
- Phân quyền: người dùng chia ra hai role (user, admin), mỗi role sẽ được truy cập các rest api chỉ định
- Lấy thông tin người dùng trong JWT
- Refresh token: mỗi khi token hết hạn, người dùng có thể gia hạn token với hành động refresh token.
Bước 1: Tạo project springboot với tên session12example1 và chọn các dependency như hình dưới
Cấu trúc project như sau:
Bước 2: Bổ sung thêm dependency jwt vào file pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <!-- hoặc jjwt-gson --> <version>0.11.5</version> <scope>runtime</scope> </dependency> |
Bước 3: Cấu hình kết nối trong file application.properties
1 2 3 4 5 6 7 |
spring.datasource.url= jdbc:sqlserver://localhost:1433;encrypt=true;trustServerCertificate=true;databaseName=session12example1 spring.datasource.username= sa spring.datasource.password= 123465 spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.SQLServerDialect spring.jpa.hibernate.ddl-auto= update spring.jpa.properties.hibernate.use_nationalized_character_data =true |
Bước 4: Code cho các lớp trong gói entities
Lớp Role (biểu diễn role của người dùng)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
package com.bkap.entities; import java.util.HashSet; import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; @Table(name="roles") @Entity public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name="roleid") private Long roleId; @Column(name="rolename") private String roleName; @ManyToMany(mappedBy = "roles") @JsonIgnoreProperties("roles") private Set<User> users = new HashSet<>(); public Long getRoleId() { return roleId; } public void setRoleId(Long roleId) { this.roleId = roleId; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } public Set<User> getUsers() { return users; } public void setUsers(Set<User> users) { this.users = users; } } |
Lớp User (biểu diễn người dùng)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
package com.bkap.entities; import java.util.HashSet; import java.util.Set; import com.bkap.models.UserModel; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinTable; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; @Table(name="users") @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "userid") private Long userId; @Column(name = "username",unique =true) private String username; private String password; @Column(name = "fullname") private String fullName; private String email; private String address; @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "userid"), inverseJoinColumns = @JoinColumn(name = "roleid")) @JsonIgnoreProperties("users") private Set<Role> roles = new HashSet<>(); public User() { // TODO Auto-generated constructor stub } public User(UserModel user, Role role) { this.username=user.getUsername(); this.fullName=user.getFullName(); this.password=user.getPassword(); this.address=user.getAddress(); this.email=user.getEmail(); this.roles.add(role); } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getFullName() { return fullName; } public void setFullName(String fullName) { this.fullName = fullName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Set<Role> getRoles() { return roles; } public void setRoles(Set<Role> roles) { this.roles = roles; } } |
Bước 5: Code cho các lớp trong gói models
Lớp UserModel (biểu diễn người dùng khi register)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
package com.bkap.models; public class UserModel { private String username; private String password; private String fullName; private String email; private String address; private String roleName; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getFullName() { return fullName; } public void setFullName(String fullName) { this.fullName = fullName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } } |
Lớp AuthRequest (biểu diễn username, password)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package com.bkap.models; public class AuthRequest { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } |
Lớp AuthResponse (biểu diễn token trả về, có thể bổ sung thêm các trường nếu cần)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package com.bkap.models; public class AuthResponse { private String token; public AuthResponse(String token) { this.token = token; } public String getToken() { return token; } } |
Bước 6: Code cho các interface trong repositories
Interface RoleRepository
1 2 3 4 5 6 7 8 9 10 11 |
package com.bkap.repositories; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.bkap.entities.Role; public interface RoleRepository extends JpaRepository<Role, Long> { Optional<Role> findByRoleName(String name); } |
Interface UserRepository
1 2 3 4 5 6 7 8 9 10 11 |
package com.bkap.repositories; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.bkap.entities.User; public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); } |
Bước 7: Code cho các lớp trong Component
Lớp JwtFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
package com.bkap.components; import java.io.IOException; 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.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import com.bkap.services.MyUserDetailsService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @Component public class JwtFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private MyUserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); String jwt = null; String username = null; // Lấy JWT từ header if (authHeader != null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); // Bỏ "Bearer " try { username = jwtUtil.extractUsername(jwt); } catch (Exception e) { System.out.println("JWT không hợp lệ: " + e.getMessage()); } } // Nếu có username và chưa xác thực if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // Gắn Authentication vào SecurityContext SecurityContextHolder.getContext().setAuthentication(authToken); } } chain.doFilter(request, response); } } |
Lớp JwtUtil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
package com.bkap.components; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; @Component public class JwtUtil { private final String SECRET = "welcome_to_bkap_lai_duc_chung_teacher"; public String generateToken(UserDetails userDetails, String fullName, String email) { Map<String, Object> claims = new HashMap<>(); claims.put("fullName", fullName); claims.put("email", email); return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 1 giờ .signWith(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256) .compact(); } private Claims extractAllClaims(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8))) .build() .parseClaimsJws(token) .getBody(); } public String extractFullName(String token) { return extractAllClaims(token).get("fullName", String.class); } public String extractEmail(String token) { return extractAllClaims(token).get("email", String.class); } public String extractUsername(String token) { return extractAllClaims(token).getSubject(); } public boolean isTokenExpired(String token) { Date expiration = extractAllClaims(token).getExpiration(); return expiration.before(new Date()); } public boolean validateToken(String token, UserDetails userDetails) { try { final String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } catch (Exception e) { System.out.println("Token validation error: " + e.getMessage()); return false; } } } |
Bước 8: Code cho lớp MyUserDetailsService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package com.bkap.services; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; 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 com.bkap.entities.User; import com.bkap.repositories.UserRepository; @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); List<GrantedAuthority> authorities = user.getRoles().stream() .map(role -> new SimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList()); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); } public Optional<User> getUser(String username) { return userRepository.findByUsername(username); } public User insert(User user){ return userRepository.save(user); } } |
Bước 9: Code cho lớp SecurityConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
package com.bkap.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import com.bkap.components.JwtFilter; import com.bkap.services.MyUserDetailsService; @Configuration @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Autowired private JwtFilter jwtFilter; @Autowired private MyUserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/register").permitAll() .anyRequest().authenticated() ) .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } } |
Bước 10: Code cho lớp HomeController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
package com.bkap.controllers; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import com.bkap.components.JwtUtil; import com.bkap.entities.Role; import com.bkap.entities.User; import com.bkap.models.AuthRequest; import com.bkap.models.AuthResponse; import com.bkap.models.UserModel; import com.bkap.repositories.RoleRepository; import com.bkap.services.MyUserDetailsService; import jakarta.servlet.http.HttpServletRequest; @RestController public class HomeController { @Autowired private AuthenticationManager authManager; @Autowired private JwtUtil jwtUtil; @Autowired private MyUserDetailsService userDetailsService; @Autowired RoleRepository repositoryRoleRepository; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody AuthRequest request) { try { Authentication authentication = authManager.authenticate( new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); UserDetails userDetails = (UserDetails) authentication.getPrincipal(); User user = userDetailsService.getUser(userDetails.getUsername()) .orElseThrow(() -> new RuntimeException("User not found")); String token = jwtUtil.generateToken(userDetails, user.getFullName(), user.getEmail()); return ResponseEntity.ok(new AuthResponse(token)); } catch (AuthenticationException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); } } @PostMapping("/register") public ResponseEntity<?> register(@RequestBody UserModel userModel) { Role role = repositoryRoleRepository.findByRoleName(userModel.getRoleName()).get(); var passencrypt=new BCryptPasswordEncoder().encode(userModel.getPassword()); userModel.setPassword(passencrypt); User user=new User(userModel, role); User u = userDetailsService.insert(user); return ResponseEntity.ok(u); } @GetMapping("/admin") @PreAuthorize("hasRole('ADMIN')") public String admin() { return "Chào Admin"; } @GetMapping("/user") @PreAuthorize("hasAnyRole('USER', 'ADMIN')") public String user() { return "Chào User"; } @GetMapping("/getprofile") @PreAuthorize("hasAnyRole('USER', 'ADMIN')") public ResponseEntity<?> getMyProfile(@RequestHeader("Authorization") String authHeader) { String token = authHeader.replace("Bearer ", ""); String username = jwtUtil.extractUsername(token); String fullName = jwtUtil.extractFullName(token); String email = jwtUtil.extractEmail(token); return ResponseEntity.ok(Map.of("username", username, "fullName", fullName, "email", email)); } @GetMapping("/refresh") @PreAuthorize("hasAnyRole('USER', 'ADMIN')") public ResponseEntity<AuthResponse> refreshToken(HttpServletRequest request) { final String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } String oldToken = authHeader.substring(7); String username = jwtUtil.extractUsername(oldToken); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (!jwtUtil.validateToken(oldToken, userDetails)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } User user = userDetailsService.getUser(userDetails.getUsername()) .orElseThrow(() -> new RuntimeException("User not found")); String newToken = jwtUtil.generateToken(userDetails, user.getFullName(), user.getEmail()); return ResponseEntity.ok(new AuthResponse(newToken)); } } |
Bước 11: Chạy và kiểm tra kết quả
Đăng ký admin thành công
Đăng ký user thành công
Lấy token user thành công
Gửi request tới vùng user
Gửi request tới vùng admin -> sẽ báo lỗi ko có quyền truy cập
Lấy thông tin người dùng đã login
Gọi Refresh token khi cần thiết
Source code tải tại đây
3. Video quay trong buổi dạy C2308G
thay lời cảm ơn!
Các bài cũ hơn
- Tạo Rest API trong Spring boot làm việc với database sử dụng JPA (08:57 AM - 08/04/2025)
- Giới thiệu Spring Boot Rest API (09:57 AM - 07/04/2025)
- Xử lý nghiệp vụ giỏ hàng-đặt hàng cơ bản trong Spring Boot (08:51 AM - 01/04/2025)
- Xây dựng chức năng đăng ký người dùng trong SpringBoot (03:50 PM - 29/03/2025)
- Bảo mật ứng dụng web với Spring Boot (09:12 AM - 28/03/2025)