JWT token là một chuỗi ký tự encode và được đính kèm với một SIGNATURE để đảm bảo tính toàn vẹn của token.
Nhờ đó mà bất kỳ sửa đổi nào cũng sẽ khiến JWT token trở thành không hợp lệ.
Thông thường một ứng dụng sử dụng JWT (Json web Token) để authentication sẽ có flow như bên sau:
Chúng ta sẽ sử dụng Spring boot để tạo và config một web application có apply spring security. Các dependency cần thiết trong file pom.xml như sau:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- ... dependency working with database ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- ... OTHER DEPENDENCIES: lombok ... -->
</dependencies>
Khi chúng ta add spring-security vào application thì Spring boot sẽ tự động đăng ký filters chain để chặn và kiểm tra tính hợp lệ của tất các các request từ phía client gửi lên. Mỗi filter trong filters chain sẽ có một nhiệm vụ khác nhau, ví dụ:
Nhiệm vụ của chúng ta sẽ là tạo ra 1 api login để generate JWT token (Step 1 + Step 2) và JWTFilter để xác thực token (Step 3) trong authentication flow
/**
* @author <a href="mailto:phuongdp.tech@gmail.com">PhuongDP</a>
*/
@RestController
@RequestMapping("/api/login")
@AllArgsConstructor
public class LoginController {
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
@PostMapping()
public ResponseEntity<JWTToken> authorize(@Valid @RequestBody LoginRequestDto requestDto) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
requestDto.getAccount().toLowerCase(),
requestDto.getPassword()
);
// This line will call the implementation of UserDetailsService
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
// If have no exception is thrown, it's mean account valid
String jwt = tokenProvider.createToken(authentication);
return ResponseEntity.ok(JWTToken.builder().accessToken(jwt).build());
}
}
AuthenticationService
để xác thực và response về JWT token khi account hợp lệ.
/**
* @author <a href="mailto:phuongdp.tech@gmail.com">PhuongDP</a>
*/
@AllArgsConstructor
@Service
public class AuthenticationService implements UserDetailsService {
private final AccountRepository accountRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(final String account) {
return accountRepository
.findByAccount(account)
.map(this::createSpringSecurityUser)
.orElseThrow(() -> new UsernameNotFoundException("Account " + account + " was not found in the database"));
}
private org.springframework.security.core.userdetails.User createSpringSecurityUser(Account account) {
List<GrantedAuthority> grantedAuthorities =
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + account.getAuthority().name()));
return new org.springframework.security.core.userdetails.User(account.getAccount(), account.getPassword(), grantedAuthorities);
}
}
UserDetailsService
là một interface định nghĩa duy nhất một method loadUserByUsername() với ý nghĩa tạo ra một cơ chế mở "làm thể nào để xác thực user tồn tại hay không?". Chúng ta sẽ cần phải implement và cài đặt chi tiết cách thức xác thực./**
* @author <a href="mailto:phuongdp.tech@gmail.com">PhuongDP</a>
*/
@Component
public class TokenProvider {
private String secretKey;
private final String AUTHORITIES_KEY = "authorities";
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
LocalDateTime exp = LocalDateTime.now().plusHours(1);
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.setExpiration(DateTimeUtils.convertToUtilDate(exp))
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
public Authentication getAuthentication(String token) {
if (!StringUtils.hasText(token)) {
return null;
}
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
Collection<? extends GrantedAuthority> authorities = Arrays
.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.filter(auth -> !auth.trim().isEmpty())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
} catch (Exception e) {
return null;
}
}
}
TokenProvider
cung cấp 2 methods:
SecurityContext
có thông tin Authentication thì request được xác định là authorized, ngược lại thì sẽ là 1 unauthorized request.
Authentication
: Là thông tin (username/authorities...) của currently authentucated user.SecurityContext
: Nơi lưu thông tin của currently authenticated user.SecurityContextHolder
: Là một helper class giúp ta access vào SecurityContext
.Nhiệm vụ của chúng sẽ sẽ cần tạo ra JWTFilter
để xác thực token và đảm bảo rằng JWTFilter
cần chạy trước filter chain của spring-security. Nếu token hợp lệ thì một UsernamePasswordAuthenticationToken
sẽ được khởi tạo và đưa vào SecurityContext
, ngược lại thì 1 HTTP Status 401 sẽ được response
/**
* @author <a href="mailto:phuongdp.tech@gmail.com">PhuongDP</a>
*/
public class JWTFilter extends GenericFilterBean {
public static final String AUTHORIZATION_HEADER = "Authorization";
private final TokenProvider tokenProvider;
public JWTFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = resolveToken(httpServletRequest);
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
if (Objects.nonNull(authentication)) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
/**
* @author <a href="mailto:phuongdp.tech@gmail.com">PhuongDP</a>
*/
@AllArgsConstructor
public class JWTConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
@Override
public void configure(HttpSecurity http) {
JWTFilter customFilter = new JWTFilter(tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
JWTConfigurer
có mục đích config đảm bảo JWTFilter
sẽ chạy trước UsernamePasswordAuthenticationFilter
(Một trong những filter có chức năng kiểm tra thông request đã authenticated hay chưa thông qua username/password)
/**
* @author <a href="mailto:phuongdp.tech@gmail.com">PhuongDP</a>
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@AllArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CorsFilter corsFilter;
private final TokenProvider tokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.csrf()
.disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/login").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.apply(securityConfigurerAdapter());
// @formatter:on
}
private JWTConfigurer securityConfigurerAdapter() {
return new JWTConfigurer(tokenProvider);
}
}
SecurityConfiguration
chúng ta có một số config quan trọng như sau:
/api/login
JWTConfigurer
và JwtAuthenticationEntryPoint
Chúng ta sẽ cần 1 account demo để thực hiện test. Khi start app tôi đã sử dụng CommandLineRunner
để khởi tạo 1 account admin/123456
Chúng ta sẽ thử call 1 protected API mà không có JWT token đi kèm. Kết quả mong đợi thì request này sẽ nhận về HTTP 401 Unauthorized
Call API login với thông tin account admin/123456
để nhận về JWT token
Sử dụng JWT token vừa nhận được add vào request header và call protected API
Với spring-security chúng ta có nhiều cách thức triển khai: OAuth2, SAML2... Trên đây là 1 cách thức đơn giản triển khai cùng với JWT Token.
Hi vọng rằng sau bài viết này các bạn có thể hiểu và áp dụng spring-security được với app của mình