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àoSecurityContext
.SecurityContext
giữ thông tin xác thựcAuthentication
.Authentication
đại diện cho đối tượng xác thựcPrincipal
(bao gồm thông tinGrantedAuthority
- phản ánh quyền hạn truy cập ứng dụng củaPrincipal
).UserDetails
chứa các thông tin cần thiết để tạo đối tượngAuthentication
UserDetailsService
là interface tạo raUserDetails
từ thông tinusername
đăng nhập. (UserDetailsService
thường được sử dụng bởiAuthenticationProvider
.UserDetailsService
giao tiếp với cơ sở dữ liệu MySQL thông quaSpring Data JPA
).JwtAuthTokenFilter
kế thừaOncePerRequestFilter
sẽ tiền xử lý HTTP request, từ thông tin Token tạo ra đối tượngAuthentication
và lưu nó vàoSecurityContext
.JwtProvider
validates, parser Token và generate Token từ thông tinUserDetails
.- Từ thông tin username và password trong request đăng nhập tạo ra instance của
UsernamePasswordAuthenticationToken
là implement của interfaceAuthentication
(để biết điều này phải đọc sâu vào trong code mới thấy được). RồiAuthenticationManager
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ụngDaoAuthenticationProvider
cùng vớiUserDetailsService
&PasswordEncoder
validate instance củaUsernamePasswordAuthenticationToken
. Nếu thành công,AuthenticationManager
trả về một đối tượngAuthentication
đầ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ứcdoFilterInternal()
- 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ụngUserDetailsService
), kiểm tra Authorization.AuthenticationEntryPoint
là class sẽ handle các lỗi xác thựcAuthenticationException
.- 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ẽ đượcAuthenticationEntryPoint
xử lý. Nếu Access Token hợp lệ thì sẽ tạo raAuthentication
đượ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ủaAuthentication
, đã giải thích bên trên) - Sử dụng instance của
UsernamePasswordAuthenticationToken
như đối tượngAuthentication
và lưu vào trongSecurityContext
để 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, GrantedAuthority
s:
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
và @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