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

[Spring security] - Spring Boot Security JWT Architecture

0 0 249

Người đăng: TheLight

Theo Viblo Asia

Bài viết này mình lại tiếp tục về chủ đề bảo mật nằm trong loạt bài viết về Spring Security JWT.

Mọi người có thể tìm đọc các bài viết liên quan tại đây!

Spring Boot Security JWT Architecture

Chúng ta có thể thấy quy trình xác thực của Spring security: đầu tiên là nhận HTTP request, filter, xác thực, lưu trữ dữ liệu đối tượng xác thực (Authentication), tạo mã xác thực JWT (generate token), get User details, authorize, handle exception,...

Giải thích về các class trong hình vẽ trên:

  • SecurityContextHolder cung cấp quyền truy cập vào SecurityContext.
  • SecurityContext giữ thông tin xác thực Authentication.
  • Authentication đại diện cho đối tượng xác thực Principal (bao gồm thông tin GrantedAuthority - phản ánh quyền hạn truy cập ứng dụng của Principal ).
  • UserDetails chứa các thông tin cần thiết để tạo đối tượng Authentication
  • UserDetailsService là interface tạo ra UserDetails từ thông tin username đăng nhập. (UserDetailsService thường được sử dụng bởi AuthenticationProvider. UserDetailsService giao tiếp với cơ sở dữ liệu MySQL thông qua Spring Data JPA).
  • JwtAuthTokenFilter kế thừa OncePerRequestFilter sẽ tiền xử lý HTTP request, từ thông tin Token tạo ra đối tượng Authentication và lưu nó vào SecurityContext.
  • JwtProvider validates, parser Token và generate Token từ thông tin UserDetails.
  • Từ thông tin username và password trong request đăng nhập tạo ra instance của UsernamePasswordAuthenticationToken là implement của interface Authentication (để biết điều này phải đọc sâu vào trong code mới thấy được). Rồi AuthenticationManager sẽ sử dụng instance này để xác thực tài khoản đăng nhập.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
...
}
và
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
...
}
  • AuthenticationManager sử dụng DaoAuthenticationProvider cùng với UserDetailsService & PasswordEncoder validate instance của UsernamePasswordAuthenticationToken. Nếu thành công, AuthenticationManager trả về một đối tượng Authentication đầy đủ thông tin (bao gồm cả các quyền).
  • OncePerRequestFilter là Filter thực thi một lần duy nhất cho mỗi Request tới API. Nó cung cấp một phương thức doFilterInternal() - trong phương thức này ta sẽ triển khai: parse và validate chuỗi JWT, lấy thông tin người dùng (sử dụng UserDetailsService), kiểm tra Authorization.
  • AuthenticationEntryPoint là class sẽ handle các lỗi xác thực AuthenticationException.
  • Resful API sẽ được bảo vệ bởi Method Security Expressions (thông qua quyền hạn cho phép).

Nhận HTTP Request

Request có thể đến từ Browser, web service client, Ajax,... - Spring không quan tâm. Nhưng Request này sẽ phải đi qua một chuỗi các bộ lọc (Filter chain) cho các mục đích xác thực và ủy quyền.

Chuỗi bộ lọc đó sẽ được áp dụng cho đến khi tìm thấy bộ lọc xác thực có tham gia trong cấu hình của chúng ta (trong bài viết là class AuthenticationFilter kế thừa class OncePerRequestFilter ).

Filter Request

Trong bài viết Spring Boot Security - JWT Authentication mình sử dụng AuthenticationFilter kế thừa class OncePerRequestFilter (abtract class) vào chuỗi các bộ lọc.

class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { ... http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); }
}

Class AuthenticationFilter sẽ validate Access Token trong Header của Request trước khi Request đến Resource (các api):

public class AuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtils jwtUtils; @Autowired private UserDetailsServiceImpl userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String token = getTokenFromRequest(request); if (token != null && jwtUtils.validateJwtToken(token)) { String username = jwtUtils.getUserNameFromJwtToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { logger.error("Cannot set user authentication: {}", e); } filterChain.doFilter(request, response); } ...
}

Có 2 trường hợp xảy ra.

  • Signin/Signup: là 2 api không được bảo vệ quyền truy cập.
  • Các api được bảo vệ quyền truy cập: Nếu Access Token null/invalid thì lỗi AuthenticationException xảy ra sẽ được AuthenticationEntryPoint xử lý. Nếu Access Token hợp lệ thì sẽ tạo ra Authentication được dùng ở phía sau đó.

Tạo Authentication từ Access Token

AuthenticationFilter lấy được thông tin username/password từ Access Token trong Header của Request. Tiếp tục thực hiện:

  • Tạo ra instance của UsernamePasswordAuthenticationToken (là implements của Authentication, đã giải thích bên trên)
  • Sử dụng instance của UsernamePasswordAuthenticationToken như đối tượng Authentication và lưu vào trong SecurityContext để các bộ lọc trong tương lai sử dụng (ví dụ: Authentication filters)
 ... String token = getTokenFromRequest(request); if (token != null && jwtUtils.validateJwtToken(token)) { String username = jwtUtils.getUserNameFromJwtToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } ...

Lưu đối tượng Authentication vào SecurityContext

SecurityContextHolder.getContext().setAuthentication(authentication);

SecurityContextHolder là đối tượng cơ bản nhất lưu trữ thông tin chi tiết về security context hiện tại của ứng dụng. Spring Security sử dụng đối tượng Authentication để đại diện cho thông tin này và ta có thể truy vấn đối tượng Authentication này từ bất kỳ đâu trong ứng dụng:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// thông tin user đã được xác thực
Object principal = authentication.getPrincipal();
// hoặc cast đối tượng về đối tượng do ta định nghĩa như bên dưới (UserDetailsImpl implements UserDetails)
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();

Ủy quyền đối tượng Authentication cho AuthenticationManagager

Sau khi đối tượng Authentication được tạo chúng ta sẽ truyền nó như một input parameter vào phương thức authenticate() của AuthenticationManager:

public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager là một interface. Trong Spring Security thì implementation mặc định của nó là ProviderManager:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { private List providers; ...
}

Authenticate với AuthenticationProvider

ProviderManager ủy quyền cho một danh sách các AuthenticationProvider, mỗi provider này sẽ cố gắng xác thực người dùng, sau đó sẽ trả về một đối tượng Authentication hoặc sẽ throw ra một exception:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { ... public ProviderManager(List<AuthenticationProvider> providers) { this(providers, (AuthenticationManager)null); } ... public Authentication authenticate(Authentication authentication) throws AuthenticationException { ... while(var8.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var8.next(); ... try { result = provider.authenticate(authentication); if (result != null) { this.copyDetails(authentication, result); break; } } ... } } ...
} 

Dưới đây là một vài authentication providers Spring ung cấp (rất nhiều ?):

  • DaoAuthenticationProvider
  • PreAuthenticatedAuthenticationProvider
  • LdapAuthenticationProvider
  • ActiveDirectoryLdapAuthenticationProvider
  • JaasAuthenticationProvider
  • CasAuthenticationProvider
  • RememberMeAuthenticationProvider
  • AnonymousAuthenticationProvider
  • RunAsImplAuthenticationProvider
  • OpenIDAuthenticationProvider

DaoAuthenticationProvider

DaoAuthenticationProvider hoạt động tốt với thông tin từ form đăng nhập hoặc xác thực HTTP Basic Authentication mà yêu cầu xác thực là tên username/password đơn giản. Nó xác thực người dùng bằng cách so sánh mật khẩu được gửi trong UsernamePasswordAuthenticationToken với mật khẩu được lấy từ UserDetailsService (as a DAO).

Cấu hình sử dụng UserDetailsService với AuthenticationManagerBuilder:

class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserDetailsServiceImpl userDetailsService; @Override public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
}

Sử dụng UsernamePasswordAuthenticationToken:

@Autowired
AuthenticationManager authenticationManager;
...
Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password) );

Lấy thông tin chi tiết người dùng với UserDetailsService

Chúng ta có thể lấy được thông tin người dùng trong đối tượng Authentication (authentication.getPrincipal()). Sau đó ép kiểu về đối tượng UserDetails để lấy được thông tin username, password, GrantedAuthoritys:

UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()

Để sử dụng nhiều thông tin hơn ta tạo ra một đối tượng custom User imlements UserDetails và một customer service implements UserDetailsService và override lại phương thức loadUserByUsername()

@Service
public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserRepository userRepository; @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username).orElseThrow( () -> new UsernameNotFoundException("User Not Found with -> username or email : " + username)); return UserPrinciple.build(user); // UserPrinciple implements UserDetails }
}

Trong ví dụ này, chúng ta sử dụng UserRepository (implementation của Spring Data JPARepository) sau đó build đối tượng custom user (hay chính là UserDetails)

Lấy thông tin quyền hạn của người dùng

Authentication cung cấp phương thức getAuthorities() trả về danh sách đối tượng GrantedAuthority là quyền hạn của người dùng (ROLE_ADMIN, ROLE_PM, ROLE_USER...):

public interface Authentication extends Principal, Serializable { Collection getAuthorities(); ...
}

Cuối cùng là sử dụng HTTPSecurity và Method Security để bảo vệ Resource

WebSecurityConfigurerAdapter là class chốt chặn triển khai bảo mật. Chúng ta cung cấp các cấu hình cho phương thức configure(HttpSecurity http) để cấu hình resource nào cần bảo vệ, exception handler nào được lựa chọn, filter nào được sử dụng và sử dụng khi nào,....:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(). authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() ...; http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); }
}

Method Security Expressions

Spring cung cấp 1 số annotations để kiểm tra ủy quyền trước và sau khi check, filter các đối số đã gửi hoặc giá trị trả về: @PreAuthorize, @PreFilter, @PostAuthorize@PostFilter.

Để sử dụng các annotations này chúng ta cần thêm annotations @EnableGlobalMethodSecurity:

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ...
}

Ví dụ sử dụng:

@RestController
public class TestRestAPIs { @GetMapping("/api/test/user") @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") public String userAccess() { return ">>> User Contents!"; } @GetMapping("/api/test/pm") @PreAuthorize("hasRole('PM') or hasRole('ADMIN')") public String projectManagementAccess() { return ">>> Project Management Board"; } @GetMapping("/api/test/admin") @PreAuthorize("hasRole('ADMIN')") public String adminAccess() { return ">>> Admin Contents"; }
}

Handle AuthenticationException

Nếu HTTP Request tới một resource được bảo vệ mà không được xác thực, AuthenticationEntryPoint sẽ được gọi. Tại thời điểm này, một AuthenticationException throw ra, phương thức commence() sẽ được kích hoạt (trigger):

@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { logger.error("Unauthorized error. Message - {}", e.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized"); }
}

Tổng kết

Trên đây là hướng dẫn để mọi người hiểu hơn về kiến trúc và cách cấu hình Spring Security Server để xác thực, phân quyền người dùng. Hy vọng bạn sẽ hiểu được ý tưởng tổng thể của bài viết và áp dụng nó vào dự án của bạn một cách thoải mái.

Nguồn: https://thenewstack.wordpress.com/2021/11/24/spring-security-spring-boot-security-jwt-architecture/

Follow me: thenewstack.wordpress.com

Bình luận

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

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

Tạo ra virus bằng tool (Part1)

Virus. Tác hại của nó để lại cũng nặng nề:. . Gây khó chịu cho chúng ta là tác hại đầu tiên.

0 0 33

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

Facebook và google "hiểu" chúng ta như thế nào?

Tổng quan. Đã bao giờ bạn gặp những tình huống dưới đây và đặt câu hỏi thắc mắc tại sao chưa.

0 0 32

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

Mã hoá dữ liệu trên Android với Jetpack Security

Jetpack Security (JetSec) là thư viện được xây dựng từ Tink - dự án mã nguồn mở, bảo mật đa nền tảng của Google. Jetpack Security được sử dụng cho việc mã hoá File và SharedPreferences.

0 0 51

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

Tái hiện vụ bị đánh cắp 2 triệu DAI (~2 triệu USD) của Akropolis

Tổng quan. .

0 0 96

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

Bảo mật internet: HTTPS và SSL/TLS như giải thích cho trẻ 5 tuổi

(Mình chém gió đấy, trẻ 5 tuổi còn đang tập đọc mà hiểu được cái này thì là thần đồng, là thiên tài, là mình cũng lạy). . . Xin chào các bạn.

0 0 78

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

Phân biệt server xịn và server pha ke bằng SSL Pinning

Xin chào các bạn, trong bài viết này mình muốn chia sẻ về một kĩ thuật rất nên dùng khi cần tăng tính bảo mật của kết nối internet: SSL Pinning. Trong bài viết trước, mình đã giải thích khá kĩ về SSL,

0 0 572