90 phút
Spring Security và JWT
Cài đặt
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
JWT Service
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
public String generateToken(UserDetails user) {
return Jwts.builder()
.subject(user.getUsername())
.claim("authorities", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey())
.compact();
}
public String extractUsername(String token) {
return getClaims(token).getSubject();
}
public boolean isValid(String token, UserDetails user) {
try {
Claims claims = getClaims(token);
return claims.getSubject().equals(user.getUsername())
&& claims.getExpiration().after(new Date());
} catch (JwtException e) {
return false;
}
}
private Claims getClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
}
JWT Filter
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = header.substring(7);
try {
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtService.isValid(token, user)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
} catch (Exception e) {
// invalid token
}
filterChain.doFilter(request, response);
}
}
Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config
) throws Exception {
return config.getAuthenticationManager();
}
}
Authentication Service
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManager authManager;
private final UserRepository userRepository;
private final JwtService jwtService;
private final PasswordEncoder passwordEncoder;
public AuthResponse register(RegisterRequest request) {
if (userRepository.existsByEmail(request.email())) {
throw new BusinessException("Email already exists");
}
User user = User.builder()
.name(request.name())
.email(request.email())
.password(passwordEncoder.encode(request.password()))
.role(UserRole.USER)
.build();
userRepository.save(user);
String token = jwtService.generateToken(toUserDetails(user));
return new AuthResponse(token, "Bearer");
}
public AuthResponse login(LoginRequest request) {
authManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.email(), request.password()));
User user = userRepository.findByEmail(request.email())
.orElseThrow(() -> new NotFoundException("User not found"));
String token = jwtService.generateToken(toUserDetails(user));
return new AuthResponse(token, "Bearer");
}
private UserDetails toUserDetails(User user) {
return org.springframework.security.core.userdetails.User
.withUsername(user.getEmail())
.password(user.getPassword())
.authorities("ROLE_" + user.getRole().name())
.build();
}
}
Custom UserDetailsService
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
return org.springframework.security.core.userdetails.User
.withUsername(user.getEmail())
.password(user.getPassword())
.authorities("ROLE_" + user.getRole().name())
.build();
}
}
Auth Controller
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
public AuthResponse register(@Valid @RequestBody RegisterRequest req) {
return authService.register(req);
}
@PostMapping("/login")
public AuthResponse login(@Valid @RequestBody LoginRequest req) {
return authService.login(req);
}
@GetMapping("/me")
public String me(Authentication auth) {
return auth.getName();
}
}
record RegisterRequest(
@NotBlank String name,
@Email @NotBlank String email,
@Size(min = 8) String password
) {}
record LoginRequest(
@Email @NotBlank String email,
@NotBlank String password
) {}
record AuthResponse(String token, String type) {}
Method-level Security
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<UserResponse> users() {
return List.of();
}
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')")
@DeleteMapping("/posts/{id}")
public void deletePost(@PathVariable Long id) {
// ...
}
}
application.yml
jwt:
secret: ${JWT_SECRET:your-256-bit-secret-key-base64-encoded-here}
expiration: 86400000 # 24 hours in ms
Bài tập thực hành
Hãy implement JWT authentication hoàn chỉnh!