- vừa được xem lúc

Spring Security 6.x JWT Refresh Token Phân quyền đơn giản nhất

0 0 14

Người đăng: Vũ Văn Huy

Theo Viblo Asia

image.png

Mục tiêu

  • Tối ưu hiệu năng bằng việc chỉ lấy token khi cần thiết.
  • Sử dụng Method Security và HttpSecurity để thiết lập phân quyền.
  • Phân quyền truy cập bằng roles.
  • Kiểm soát session từ phía máy chủ và đăng xuất từ phía backend.
  • Dễ dàng tích hợp với các phương thức xác thực. Đơn giản và dễ sử dụng.

Kiến thức cần thiết

Vòng đời của một Request trong Spring

image.png

  1. Client gửi một request lên Server. Request đó sẽ đi qua lần lượt các Filter được cài đặt sẵn trong ứng dụng Spring Boot. Các Filter này có thể thực hiện các chức năng như xác thực, kiểm tra quyền truy cập, hoặc thực hiện các xử lý trước khi request đến được Controller.
  2. Sau khi đi qua các Filter, Request được chuyển đến Controller. Controller làm nhiệm vụ xử lý nghiệp vụ liên quan đến request như xử lý dữ liệu, gọi các service hoặc repository để truy xuất dữ liệu, và chuẩn bị dữ liệu cho Response.
  3. Sau khi Controller xử lý xong, dữ liệu Response được tạo ra. Response này chứa kết quả của xử lý request và các thông tin khác như headers và status code.
  4. Response đi ngược lại qua các Filter mà Request đã đi qua. Các Filter này có thể thực hiện các xử lý sau khi Controller đã hoàn thành việc xử lý request, ví dụ như ghi log, thêm hoặc sửa đổi headers, hoặc nén dữ liệu Response.
  5. Cuối cùng, dữ liệu Response được trả về cho Client. Client có thể nhận và xử lý dữ liệu Response theo nhu cầu của ứng dụng.

Cách Spring Security sử dụng filter để kiểm tra phân quyền.

image.png

  • Spring Security sử dụng các Filter để kiểm tra và quản lý quyền truy cập của người dùng.
  • Khi tạo một dự án Spring Security sử dụng Session, khi bạn truy cập vào trang web bằng trình duyệt và kiểm tra phần Developer Tools -> Application -> Cookies, bạn sẽ thấy rằng Spring Security tạo một cookie có tên là với giá trị có dạng BED8B60A50738BEE47366C2F0ACBE9C2.
  • Khi Client gửi Request lần đầu tiên lên Server, Server sẽ tạo một JSESSIONID đại diện cho Session của Client. Sau đó, Server gửi JSESSIONID này về Client dưới dạng một Cookie với tên là JSESSIONID.
  • Khi Client gửi các Request tiếp theo, trình duyệt sẽ tự động đính kèm Cookie JSESSIONID vào mỗi Request. Server nhận được Cookie này để xác định người dùng gửi Request đó.

Khi sử dụng Spring Security Session, bạn hãy thử xoá Cookie JSESSIONID sau đó truy cập lại trang web, Server sẽ không tìm thấy Session, bạn sẽ ngay lập tức logout.

Lưu ý rằng khi sử dụng Basic HttpSecurity, khi xoá JSESSIONID sẽ không tạo hiệu ứng trên vì nó sử dụng một kỹ thuật khác để xác định Session.

image.png

  • Khi kiểm tra trạng thái đăng nhập của Session hiện tại, Spring Security SessionManagementFilter gọi hàm this.securityContextHolderStrategy.getContext().getAuthentication();.
  • Authentication Context chứa thông tin đã đăng nhập của người dùng.

Làm thế nào để tích hợp JWT.

image.png

Người dùng đăng nhập thành công, thay vì sử dụng JSESSIONID Server sẽ gán JWT token vào Cookie, mọi Request được gửi lên sẽ kèm theo JWT Cookie đó.

Ý tưởng ở đây chúng ta sẽ đặt một Filter nằm trước những Spring Security Filter cần kiểm tra Authentication Context. Filter đó đảm nhiệm việc lấy thông tin đang nhập của Session hiện tại nếu có.

LazySecurityContextProviderFilter chịu trách nhiệm xử lý logic JWT Token, yêu cầu làm mới Token khi cần thiết.

Lý do đặt JWT ở Cookie thay vì Request Header: Có thể kiểm soát session từ phía máy chủ và chủ động đăng xuất từ phía backend. Điều này cho phép quản lý session từ phía máy chủ, bao gồm việc kiểm soát thời gian sống của session và có thể đăng xuất người dùng từ phía máy chủ.

Thiết lập dự án

Cấu trúc thư mục

image.png

Thư viện cần có

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>3.2.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.huyvu</groupId> <artifactId>springsecurityjwt</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springsecurityjwt</name> <description>springsecurityjwt</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </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>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </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> </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> 

SecurityUtils.java

package com.huyvu.springsecurityjwt.security; import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.Assert; import java.util.Arrays;
import java.util.Date; @Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SecurityUtils { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String AUTHORIZATION_PREFIX = "Bearer_"; private static final int SIX_HOURS_MILLISECOND = 1000 * 60 * 60 * 6; private static final int SIX_HOURS = 3600 * 6; private static final String USER_CLAIM = "user"; private static final String ISSUER = "auth0"; @Value("${jwt-key}") private static String SECRET_KEY = "iloveu3000"; private static final Algorithm ALGORITHM = Algorithm.HMAC256(SECRET_KEY); @SneakyThrows public static String createToken(JwtTokenVo jwtTokenVo) { var builder = JWT.create(); var tokenJson = OBJECT_MAPPER.writeValueAsString(jwtTokenVo); builder.withClaim(USER_CLAIM, tokenJson); return builder .withIssuedAt(new Date()) .withIssuer(ISSUER) .withExpiresAt(new Date(System.currentTimeMillis() + SIX_HOURS_MILLISECOND)) .sign(ALGORITHM); } @SneakyThrows public static DecodedJWT validate(String token) { var verifier = JWT.require(ALGORITHM) .withIssuer(ISSUER) .build(); return verifier.verify(token); } @SneakyThrows public static JwtTokenVo getValueObject(DecodedJWT decodedJWT) { var userClaim = decodedJWT.getClaims().get(USER_CLAIM).asString(); return OBJECT_MAPPER.readValue(userClaim, JwtTokenVo.class); } public static String getToken(HttpServletRequest req) { var cookies = req.getCookies(); var authCookie = Arrays.stream(cookies) .filter(e -> e.getName().equals(AUTHORIZATION_HEADER)) .findFirst() .orElseThrow(); String authorizationHeader = authCookie.getValue(); Assert.isTrue(authorizationHeader.startsWith(AUTHORIZATION_PREFIX), "Authorization header must start with '" + AUTHORIZATION_PREFIX + "'."); String jwtToken = authorizationHeader.substring(AUTHORIZATION_PREFIX.length()); return jwtToken; } public static void setToken(HttpServletResponse res, String token) { var cookie = new Cookie(AUTHORIZATION_HEADER, AUTHORIZATION_PREFIX + token); cookie.setMaxAge(SIX_HOURS); cookie.setPath("/"); res.addCookie(cookie); } public static JwtTokenVo getSession(){ var authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication instanceof AnonymousAuthenticationToken) { throw new AccessDeniedException("Not authorized."); } return (JwtTokenVo) authentication.getPrincipal(); }
}
  • Lớp này được dùng để tạo, giải mã, đọc Token từ Request và ghi Token vào Response, các lớp Business sẽ sử dụng hàm getSession() để lấy thông tin đăng nhập hiện tại.
  • Token sử dụng thuật toán HMAC256, sử dụng SECRET_KEY.
  • Key của Cookie là AUTHORIZATION_HEADER, value của JWT token nên được bắt đầu bằng Bearer.
  • Khi tạo Token chúng ta parse POJO JwtTokenVo thành JSON và thêm vào Claims.
  • Token Cookie sẽ được ghi vào Response.

JwtTokenVo.java

package com.huyvu.springsecurityjwt.security; import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import org.springframework.security.core.GrantedAuthority; import java.util.ArrayList;
import java.util.List; import static lombok.AccessLevel.PRIVATE; @Data
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = PRIVATE)
public class JwtTokenVo { Long uId; String username; List<String> roles; // Thêm những thông tin cần thiết vào properties List<GrantedAuthority> getAuthorities() { if (roles == null) return new ArrayList<>(); return roles.stream().map(s -> (GrantedAuthority) () -> s).toList(); } }
  • Lớp POJO để lưu thông tin đăng nhập vào JWT.
  • Nếu cần lưu thêm những thông tin cần thiết các bạn có thể thêm vào lớp này.

LazySecurityContextProviderFilter.java

Sử lý logic JWT.

package com.huyvu.springsecurityjwt.security; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException;
import java.util.Date; import static javax.management.timer.Timer.ONE_HOUR; @Slf4j
@Component
@RequiredArgsConstructor
public class LazySecurityContextProviderFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException { var context = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(new LazyJwtSecurityContextProvider(req, res, context)); filterChain.doFilter(req, res); } @RequiredArgsConstructor static class LazyJwtSecurityContextProvider implements SecurityContext { private final HttpServletRequest req; private final HttpServletResponse res; private final SecurityContext securityCtx; @Override public Authentication getAuthentication() { if (securityCtx.getAuthentication() == null || securityCtx.getAuthentication() instanceof AnonymousAuthenticationToken) { try { var jwtToken = SecurityUtils.getToken(this.req); var decodedJWT = SecurityUtils.validate(jwtToken); // Kiểm tra thời gian hết hạn của token if (decodedJWT.getExpiresAt().before(new Date())) { throw new AuthenticationServiceException("Token expired."); } var valueObject = SecurityUtils.getValueObject(decodedJWT); var authToken = new PreAuthenticatedAuthenticationToken(valueObject, null, valueObject.getAuthorities()); // Kiểm tra thời gian tồn tại của token, nếu hơn một giờ thì làm mới  if(decodedJWT.getExpiresAt().before(new Date(System.currentTimeMillis() + ONE_HOUR))){ var token = SecurityUtils.createToken(valueObject); SecurityUtils.setToken(res, token); } authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(req)); securityCtx.setAuthentication(authToken); } catch (Exception e) { log.debug("Can't get authentication context: " + e.getMessage()); } } return securityCtx.getAuthentication(); } @Override public void setAuthentication(Authentication authentication) { securityCtx.setAuthentication(authentication); } } }
  • Lớp Filter này đảm nhiệm việc xử lý logic khi cần thấy thông tin đăng nhập từ Request.
  • Sử dụng design pattern Decorator, lớp chỉ thực hiện lấy Token khi được yêu cầu chứ không phải mọi Request. Điều này sẽ tối ưu hiệu năng của ứng dụng.
  • Khi lấy Token lớp sẽ đồng thời kiểm tra Refresh Token.

SecurityConfig.java

package com.huyvu.springsecurityjwt.security; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.session.SessionManagementFilter; @Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // Bật tính năng Method Security
public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http, LazySecurityContextProviderFilter lazySecurityContextProviderFilter) throws Exception { http .sessionManagement(se -> se.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Tắt tính năng Session In Memory Management trong Spring Security .authorizeHttpRequests(a -> { a.requestMatchers("/secured/**").authenticated(); a.requestMatchers("/admin/**").hasAuthority("admin"); a.anyRequest().permitAll(); }) .addFilterAfter(lazySecurityContextProviderFilter, SessionManagementFilter.class); // Đặt LazySecurityContextProviderFilter đứng trước SessionManagementFilter return http.build(); } }

HomeController.java

package com.huyvu.springsecurityjwt.controller; import com.huyvu.springsecurityjwt.security.JwtTokenVo;
import com.huyvu.springsecurityjwt.security.SecurityUtils;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Role;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import java.util.Arrays; @RestController
public class HomeController { @GetMapping String signing(Long uId, String username, String[] roles, HttpServletResponse res){ var token = SecurityUtils.createToken(new JwtTokenVo(uId, username, Arrays.stream(roles).toList())); SecurityUtils.setToken(res, token); return "signed"; } @GetMapping("/secured") String secured(){ var session = SecurityUtils.getSession(); return "Secured " + session; } @GetMapping("/admin") String admin(){ var session = SecurityUtils.getSession(); return "Admin " + session; } @PreAuthorize("hasAuthority('guest')") @GetMapping("/guest") String guest(){ var session = SecurityUtils.getSession(); return "Guest " + session; } @GetMapping("/business") String business(){ var session = SecurityUtils.getSession(); return "Business " + session; }
} 

Chúng ta tạo Controller để test các trường hợp.

Controller Test case
signing Nhận các biến đầu vào để mô phỏng chức năng đăng nhập
secured Yêu cầu Session đã đăng nhập thành công, sử dụng HttpSecurity
admin Yêu cầu Session đã đăng nhập thành công và có role là admin, sử dụng HttpSecurity
guest Yêu cầu Session đã đăng nhập thành côngvà có role là guest, sử dụng Method Security
business Yêu cầu Session đã đăng nhập thành công, trong trường hợp không sử dụng HttpSecurityMethod Security nhưng vẫn lấy thông tin đăng nhập hiện tại, ứng dụng sẽ trả lỗi AccessDeniedException

Testing

Không đăng nhập

image.png image.png image.png image.png

Đăng nhập với quyền admin, user, guest

image.png image.png image.png image.png image.png

Đăng nhập với quyền user

image.png image.png image.png image.png image.png

Source: https://github.com/huyvu8051/springsecurityjwt

Tại sao lại phải sử dụng lazy security context provider? Tại sao phải parse JwtTokenVo thành json string rồi mới thêm vào JWT claims? Tại sao lại exclude UserDetailsServiceAutoConfig? Tại sao đặt lazySecurityContextProviderFilter nằm trước SessionManagementFilter mà không phải nằm đầu tiên hoặc vị trí khác?

Bình luận

Bài viết tương tự

- vừa được xem lúc

How JSON Web Token(JWT) authentication works?

1. JWT (JSON Web Token) là gì . Thông tin này có thể xác minh và đáng tin cậy vì nó là chữ ký điện tử . Jwt có thể được đăng ký băng cách sử dụng bí mật (với thuật toán HMAC) hoặc cặp khóa public/private bằng RSA.

0 0 71

- vừa được xem lúc

JWT và ứng dụng xác thực người dùng trong Rails

JWT. Thời gian gần đây mình có init API thì mình có ứng dụng Json Web Token (JWT) để xây dựng xác thực người dùng. Nó có support những gì và ứng dụng của nó ra sao thì mình xin chia sẻ trong bài viết. Nó là gì.

0 0 175

- vừa được xem lúc

Tìm hiểu một chút về JWT

Hello mọi người, trong bài viết hôm nay mình cùng các bạn sẽ cùng tìm hiểu về JWT nhé. JWT ( Json Web Token ) là 1 tiêu chuẩn mở (RFC 7519) định nghĩa cách truyền thông tin một cách an toàn giữa các b

0 0 73

- vừa được xem lúc

Refresh token là gì? Cách hoạt động có khác gì so với token không?

Ở những bài trước chúng ta đã nói nhiều về JWT là gì? Vì sao phải sử dụng JWT, trong suốt quá trình sử dụng JWT chúng tôi có nhận được nhiều phản hồi về chủ đề JWT. Trong đó có một vấn đề mà có nhiều

0 0 101

- vừa được xem lúc

JWT và ứng dụng của nó

Khái niệm. JSON Web Token (JWT) là 1 tiêu chuẩn mở (RFC 7519) định nghĩa cách thức truyền tin an toàn giữa bên bằng 1 đối tượng JSON.

0 0 45

- vừa được xem lúc

JWT - Từ cơ bản đến chi tiết

Chào mọi người. Bài viết của mình ngày hôm nay với mục đích chia sẻ những kiến thức mà mình lượm nhặt được, gom chúng lại để tổng hợp cho các bạn.

0 0 45