API Security luôn là đề tài được quan tâm nhiều nhất tại bất kỳ ứng dụng nào hôm nay Tây Java xin được chia sẻ phương pháp tích hợp Spring Security kết hợp Json Web Token sử dụng mô hình phân quyền Role Based Access Control.
1. Technical Stacks
– Java 17
– Spring Boot 3
– Spring Security
– JWT
– Swagger
– Docker
2. Thêm dependency
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </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>
3. Cấu hình Spring Security
@Configuration @RequiredArgsConstructor public class AppConfig { private final UserService userService; private final PreFilter preFilter; private String[] WHITE_LIST = {"/auth/**"}; @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(@NonNull CorsRegistry registry) { registry.addMapping("**") .allowedOrigins("http://localhost:8500") .allowedMethods("GET", "POST", "PUT", "DELETE") // Allowed HTTP methods .allowedHeaders("*") // Allowed request headers .allowCredentials(false) .maxAge(3600); } }; } @Bean public PasswordEncoder getPasswordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain configure(@NonNull HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorizeRequests -> authorizeRequests.requestMatchers(WHITE_LIST).permitAll().anyRequest().authenticated()) .sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS)) .authenticationProvider(provider()).addFilterBefore(preFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public WebSecurityCustomizer webSecurityCustomizer() { return webSecurity -> webSecurity.ignoring() .requestMatchers("/actuator/**", "/v3/**", "/webjars/**", "/swagger-ui*/*swagger-initializer.js", "/swagger-ui*/**"); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } @Bean public AuthenticationProvider provider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userService.userDetailsService()); provider.setPasswordEncoder(getPasswordEncoder()); return provider; } }
4. Tạo Prefilter
@Component @Slf4j public class PreFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { log.info("---------------- PreFilter ----------------"); // TODO verify token filterChain.doFilter(request, response); } }
5. Tạo Authentication Controller
@Slf4j @Validated @RestController @RequestMapping("/auth") @Tag(name = "Authentication Controller") @RequiredArgsConstructor public class AuthenticationController { private final AuthenticationService authenticationService; @PostMapping("/access") public ResponseEntity login(@RequestBody SignInRequest request) { return new ResponseEntity<>(authenticationService.authenticate(request), HttpStatus.OK); } @PostMapping("/refresh") public String refresh() { // TODO gọi service JWT service return "success"; } @PostMapping("/logout") public String logout() { // TODO gọi service JWT service return "success"; } }
6. Tạo Authentication Service
@Service @RequiredArgsConstructor public class AuthenticationService { private final UserRepository userRepository; private final AuthenticationManager authenticationManager; private final JwtService jwtService; public TokenResponse authenticate(SignInRequest signInRequest) { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword())); var user = userRepository.findByUsername(signInRequest.getUsername()).orElseThrow(() -> new UsernameNotFoundException("Username or Password is incorrect")); String accessToken = jwtService.generateToken(user); return TokenResponse.builder() .accessToken(accessToken) .refreshToken("refresh_token") .userId(user.getId()) .build(); } }
7. Tạo JWT Service
– JwtService.class
public interface JwtService { String generateToken(UserDetails user); // TODO code here .... }
– JwtServiceImpl.class
@Service public class JwtServiceImpl implements JwtService { @Override public String generateToken(UserDetails user) { // TODO xu ly tao ra token return "access-tokern"; } }
8. Tạo User Service
- UserService.class public interface UserService { UserDetailsService userDetailsService(); long saveUser(UserRequestDTO request); void updateUser(long userId, UserRequestDTO request); void changeStatus(long userId, UserStatus status); void deleteUser(long userId); UserDetailResponse getUser(long userId); PageResponse<?> getAllUsers(int pageNo, int pageSize); }
– UserServiceImpl.class
@Service @Slf4j @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserRepository userRepository; @Override public UserDetailsService userDetailsService() { return username -> userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found")); } ... }
– UserRepository.class
@Repository public interface UserRepository extends JpaRepository<User, Long> { Optional findByEmail(String email); Optional findByUsername(String username); @Query(value = "select r from Role r inner join UserHasRole ur on r.id = ur.user.id where ur.id= :userId") List findAllRolesByUserId(Long userId); }
9. Tạo User Entity kế thừa tứ UserDetail
@Setter @Getter @Entity @Builder @AllArgsConstructor @NoArgsConstructor @Table(name = "tbl_user") public class User extends AbstractEntity implements UserDetails, Serializable { @Column(name = "first_name") private String firstName; @Column(name = "last_name") private String lastName; @Column(name = "date_of_birth") @Temporal(TemporalType.DATE) private Date dateOfBirth; @Enumerated(EnumType.STRING) @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Column(name = "gender") private Gender gender; @Column(name = "phone") private String phone; @Column(name = "email") private String email; @Column(name = "username") private String username; @Column(name = "password") private String password; @Enumerated(EnumType.STRING) @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Column(name = "type") private UserType type; @Enumerated(EnumType.STRING) @JdbcTypeCode(SqlTypes.NAMED_ENUM) @Column(name = "status") private UserStatus status; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "user") private Set addresses = new HashSet<>(); @OneToMany(mappedBy = "user") private Set roles = new HashSet<>(); @OneToMany(mappedBy = "user") private Set groups = new HashSet<>(); public void saveAddress(Address address) { if (address != null) { if (addresses == null) { addresses = new HashSet<>(); } addresses.add(address); address.setUser(this); // save user_id } } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }