[ Phần 3] Spring Security 6 with JWT, Oauth2
1. Giới thiệu
- Tiếp nối phần 2 trong series, phần này mình sẽ triển khai code Security vào project. Mọi người có thể đọc lại phần 2 ở đây: https://viblo.asia/p/phan-2-them-security-vao-project-5pPLkAD64RZ
2. Triển khai code
Lưu ý: mọi người hãy nhớ cần phải thêm Security dependency vào project trước nhé(mình đã chia sẻ ở phần trước). Để làm việc được với JWT chúng ta cũng cần thêm một số dependency vào project:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
Trước khi tiếp tụcvới bài này mọi người nên tìm hiểu về lý thuyết của JWT và vào trang https://jwt.io/ để test thử phần encode của nó.
2.1. SecurityConfiguration
Trước khi đi vào trong việc triển khai Spring Security, như mình đã trình bày ở Serie Spring Boot 3 và Spring Security 6 (https://viblo.asia/s/spring-boot-3-va-spring-security-6-gwd43EBXLX9) chúng ta cần phải cấu hình web security cho các HTTP request, nếu không cấu hình thì tất cả các request đều phải xác thực, tuy nhiên chúng ta cần các api liên quan đến authentication và authorization phải được ngoại lệ. Chính vì vậy, bước đầu tiên chúng ta cần làm là tạo package config và tạo class SecurityConfiguration
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/api/auth/**").permitAll()
.anyRequest()
.authenticated())
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(Customizer.withDefaults());
return http.build();
}
2.2. Register User
Tất nhiên rồi, để có thể xác thực được thì đầu tiên chúng ta cần phải có phương thức để tạo user đã.
2.2.1. Tạo User
2.2.1. Tạo UserDto
Đầu tiên cần phải lưu ý một điều rằng chúng ta không thể trả về cho client data có dạng User được, vì User entity có chứa cả mật khẩu. Chính vì vậy, chúng ta sẽ phải tạo ra DTO class, ở đây sẽ là package dto và class UserDto:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private Long id;
private String name;
private String email;
private Date dateOfBirth;
private String address;
private String phoneNumber;
@Enumerated(EnumType.STRING)
private Role role;
}
2.2.2. Tạo UserMapper
Ngoài ra cũng cần có package mapper và class UserMapper để convert User thành UserDto :
public class UserMapper {
public static UserDto mapToUserDto(User user) {
UserDto userDto = new UserDto();
userDto.setUserId(user.getUserId());
userDto.setUsername(user.getUsername());
userDto.setFirstName(user.getFirstName());
userDto.setLastName(user.getLastName());
userDto.setEmail(user.getEmail());
userDto.setRole(user.getRole());
return userDto;
}
}
2.2.3. Tạo RegisterRequest
Bởi vì client sẽ gửi cho chúng ta data một object gồm 3 cặp key-value là username, email và password, nên chúng ta cũng cần phải tạo package model và class RegisterRequest:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RegisterRequest {
private String name;
private String email;
private String password;
}
2.2.4. Tạo AuthenticationResponse
Gửi đến đã có chuẩn rồi thì gửi trả lại cũng cần phải có một chuẩn để client dễ dàng sử dụng cho mục đích của họ, nên chúng ta sẽ define thêm class AuthenticationResponse như sau:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationResponse {
private String token;
private UserDto userDto;
}
2.2.5. Tạo AuthenticationService và AuthenticationServiceImpl
Tiếp theo chúng ta cần phải tạo interface AuthenticationService để phục vụ cho logic đăng ký:
public interface AuthenticationService {
AuthenticationResponse register(RegisterRequest request);
}
Và tương tự như các service khác, class AuthenticationSerrviceImpl được tạo để implement AuthenticationService:
@Override
public AuthenticationResponse register(RegisterRequest request) {
User newUser = new User();
newUser.setUsername(request.getUsername());
newUser.setPassword(request.getPassword());
newUser.setEmail(request.getEmail());
newUser.setRole(Role.ADMIN);
User createdUser = userRepository.save(newUser);
return AuthenticationResponse.builder()
.userDto(UserMapper.mapToUserDto(createdUser))
.build();
}
2.2.6. Tạo JwtService và JwtServiceImpl
Ở đây chúng ta đã trả về userDto, tuy nhiên vẫn còn thiếu token nữa(token chính là jwt dùng để xác thực với backend). Chúng ta sẽ tạo ra token từ UserDto để gửi cho client. Để có thể làm được việc này, đầu tiên chúng ta cần tạo ra interface JwtService chứa method generateToken():
public interface JwtService {
String generateToken(User user);
}
Nếu mọi người đã tìm hiểu về JWT rồi thì mọi người sẽ biết rằng để tạo và xác thực JWT token, chúng ta cần một Secret Key. Secret key được sử dụng để ký token nhằm đảm bảo tính toàn vẹn và xác thực của token. Khi tạo JWT, secret key sẽ được sử dụng để tạo chữ ký (signature) của token. Khi xác thực JWT, secret key sẽ được sử dụng để kiểm tra chữ ký nhằm đảm bảo rằng token chưa bị thay đổi và đến từ nguồn đáng tin cậy. Nên việc đầu tiên khi tạo JwtServiceImpl sẽ là khai báo SECRET_KEY:
@Service
public class JwtServiceImpl implements JwtService {
private static final String SECRET_KEY =
"404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970";
@Override
public String generateToken(User user) {
return "";
}
}
Mã SECRET_KEY này mọi người có thể nhập gì cũng được, hoặc có thể dùng trang này: https://generate.plus/en/base64 random lấy 1 mã cũng được.
Sau khi đã có mã SECRET_KEY rồi, chúng ta cần phải convert nó thành Key để sử dụng cho mục đích ký và xác thực JWT token(ở đây sử dụng thuật toán HMAC-SHA để mã hóa):
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
Đây là JwtServiceImpl hoàn chỉnh:
@Service
public class JwtServiceImpl implements JwtService {
private static final String SECRET_KEY =
"404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970";
@Override
public String generateToken(User user) {
return Jwts
.builder()
.setSubject(user.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
}
2.2.7. Cập nhật lại AuthenticationServiceImpl
Sau khi đã có token rồi, chúng ta cần update lại method register():
@Override
public AuthenticationResponse register(RegisterRequest request) {
User newUser = new User();
newUser.setUsername(request.getUsername());
newUser.setPassword(request.getPassword());
newUser.setEmail(request.getEmail());
User createdUser = userRepository.save(newUser);
String jwtToken = jwtService.generateToken(createdUser);
return AuthenticationResponse.builder()
.userDto(UserMapper.mapToUserDto(createdUser))
.token(jwtToken)
.build();
}
2.2.8. Tạo Token entity
Như vậy chúng ta đã hoàn thành được logic cho việc đăng ký user rồi, tuy nhiên nếu mọi người để ý thì sẽ thấy token này chỉ được gửi cho client thôi, chứ backend sau khi thực hiện xong method này thì sẽ không còn thông tin của token nữa. Như vậy thì khi client gửi request kèm token, backend sẽ không thể nào xác thực được liệu token này có đúng là của user đó không. Để có thể xác thực được, chúng ta cần lưu nó vào database. Token entity:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "tokens")
public class Token {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer id;
@Column(unique = true)
public String token;
public String tokenType = "Bearer ";
public boolean revoked;
public boolean expired;
public Long userId;
}
2.2.9. Tạo TokenRepository
Tiếp theo sẽ là TokenRepository:
public interface TokenRepository extends JpaRepository<Token, Integer> {
}
2.2.10. Cập nhật AuthenticationServiceImpl
Như vậy sau khi lấy được jwtToken: String jwtToken = jwtService.generateToken(createdUser); Chúng ta sẽ lưu token này vào database:
@Service
@RequiredArgsConstructor
public class AuthenticationServiceImpl implements AuthenticationService {
private final UserRepository userRepository;
private final TokenRepository tokenRepository; // thêm TokenRepository
private final JwtServiceImpl jwtService;
@Override
public AuthenticationResponse register(RegisterRequest request) {
User newUser = new User();
newUser.setUsername(request.getUsername());
newUser.setPassword(request.getPassword());
newUser.setEmail(request.getEmail());
User createdUser = userRepository.save(newUser);
String jwtToken = jwtService.generateToken(createdUser);
// lưu token vào database
Token token = Token.builder()
.userId(createdUser.getUserId())
.token(jwtToken)
.expired(false)
.revoked(false)
.build();
tokenRepository.save(token);
return AuthenticationResponse.builder()
.userDto(UserMapper.mapToUserDto(createdUser))
.token(jwtToken)
.build();
}
}
2.2.11. Tạo AuthenticationController
Cuối cùng chúng ta cần tạo AuthenticationController để nhận request từ client:
@RequestMapping("/api/auth")
@RestController
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationServiceImpl authenticationService;
@PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register(@RequestBody RegisterRequest registerRequest) {
AuthenticationResponse response = authenticationService.register(registerRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
3. Kết
Sau khi đã viết xong logic cho phần đăng ký user, chúng ta sử dụng Postman để test logic:
Vì bài viết khá dài, nên mình sẽ viết logic authenticate ở phần sau. Source code: https://github.com/hachnv8/spring-security
All rights reserved