Dựng một project spring boot sử dụng JWT

Published: August 01, 2023  •  go

JWT theo mình hiểu là công nghệ để xác thực người dùng trong việc sử dụng REST APIs ngoài ra còn có nhiều cách xác thực người dùng khác như sử dụng Session, Cookie hay sử dụng trực tiếp username và password


Cách hoạt động của JWT:

JWT theo mình hiểu là công nghệ để xác thực người dùng trong việc sử dụng REST APIs ngoài ra còn có nhiều cách xác thực người dùng khác như sử dụng Session, Cookie hay sử dụng trực tiếp username và password

Sơ đồ minh họa:

ảnh

Khi người dùng đăng nhập lần đầu vào hệ thống, người dùng cần gửi thông tin chứa mật khẩu cho server.

Server kiểm tra tính hợp lệ của thông tin đó. Nếu hợp lệ, server sẽ gửi một đoạn token chứa thông tin của người dùng để sử dụng trong hệ thống vào 1 chuỗi đã được mã hóa gọi là token

Những lần request sau lên server, người dùng sẽ không cần gửi thông tin về mật khẩu nữa. Thay vào đó chuỗi token sẽ được gắn vào phần header của request đến server. Server sẽ kiểm tra sự hợp lệ của chuỗi này và sẽ trả về respone tương ứng

Ưu điểm:

Khi so sách với việc xác thực bằng session thì JWT có một số ưu điểm như:


Tạo project Spring boot- Security

Thêm các dependency:

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.auth0/auth0 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>auth0</artifactId>
    <version>1.19.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.auth0/auth0-spring-security-api -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>auth0-spring-security-api</artifactId>
    <version>1.4.0</version>
</dependency>

Các dependency được sử dụng cho việc sử dụng jwt để xác thực người dùng trong REST-APIs

Cấu trúc project như sau. Chú ý phần khoanh đỏ

ảnh

Tạo config cho Spring Security: (tạm thời chưa chú ý đến UserDetailServiceImpl)

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailServiceImpl userService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/").permitAll()
                .anyRequest().authenticated();
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
    
    @Bean
    public JWTAuthenticationFilter jwtAuthenticationFilter() {
        return new JWTAuthenticationFilter();
    }

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

Có tác dụng khai báo cho Spring Security ko sử dụng session để xác thực người dùng sau khi người dùng đã login thành công vào hệ thống. Điều này cần thiết là vì khi xác thực jwt thành công lần đầu, Spring sẽ sử dụng session của client đó để xác thực cho những lần request sau. Điều này ko đảm bảo an toàn.

.antMatchers("/").permitAll() .anyRequest().authenticated();

Tất cả các request có path "/" thì ko cần xác thực. Còn những request có path khác thì bắt buộc phải xác thực.

http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

Có tác dụng như 1 filter để kiểm tra tất cả các request trc khi đến controller kể cả request có path = "/"

Tiếp tục đến class JWTAuthenticationFilter

@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Autowired
    private UserDetailServiceImpl userDetailService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        if (request.getServletPath().equals("/login")&&request.getMethod().equals("POST")) {
            String username = request.getParameter("username");
            String password = request.getParameter("password");
            CustomUserDetail user = (CustomUserDetail) userDetailService.loadUserByUsername(username);
            System.out.println(tokenProvider.generateToken(user));
        } else {
            try {
                String jwt = getJwtFromRequest(request);
                if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                    String idUser = tokenProvider.getUserFromJWT(jwt);
                    UserDetails userDetails = userDetailService.loadUserById(idUser);
                    if (userDetails != null) {
                        UsernamePasswordAuthenticationToken authentication
                                = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            } catch (Exception e) {
                SecurityContextHolder.clearContext();
            }
        }
        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

JWTAuthenticationFilter được extend từ OncePerRequestFilter có tác dụng kiểm tra từng request một trc khi đến controller. Sử dụng method doFilterInternal để thực hiện công việc kiểm tra này.

UsernamePasswordAuthenticationToken được kế thừa từ Authentication có tác dụng sử dụng Username và Password để xác thực người dùng. Username và Password được lấy từ DB hoặc dựa vào Claims của token sau khi được decode

UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

SecurityContextHolder.getContext().setAuthentication(authentication);

Sau khi xác thực thành công một người dùng nó sẽ đưa quyền xác thực vào cho SecurityContextHolder

Cuối cùng là JwtTokenProvider

@Component
@Slf4j
public class JwtTokenProvider {

    static final long EXPIRATION_TIME = 604800000L;

    private static final String SECRET = "A4343434343434343";

    public String generateToken(CustomUserDetail userDetail) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME);
        return Jwts.builder()
                .setSubject(userDetail.getUser().get_id())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }

    public String getUserFromJWT(String token) {
        return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody().getSubject();
    }

    public boolean validateToken(String authToken) {
        try {
            System.out.println(Jwts.parser().setSigningKey(SECRET).parseClaimsJws(authToken));
            return true;
        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
            log.error("ERROR: ", e);
        }
        return false;
    }
}

Lớp này có nhiệm vụ là tạo token khi có requet gửi username và password vào "/login" với method POST, thực hiện kiểm tra tính hợp lệ của token, và cuối cùng là lấy ra các thông tin cần thiết của người dùng đã được mã hóa trong token để sử dụng cho việc thực hiện gọi APIs

Để Spring security kiểm tra và thực hiện xác thực được người dùng ta cần có thêm UserDetailsUserDetailsService. Đây là 2 interface build-in của Spring Security.

@Getter
@Setter
@ToString
public class CustomUserDetail implements UserDetails {

    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Implements UserDetails để tạo ra 1 người dùng có một số hành vi cơ bản như có đã hết hạn hay chưa đã được kích hoạt hay chưa, có những role gì ...

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    private UserRepo userRepo;

    @Autowired
    public UserDetailServiceImpl(UserRepo userRepo) {
        this.userRepo = userRepo;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<User> userList = userRepo.findByUsername(username);
        if (userList.size() == 0) {
            return null;
        } else {
            CustomUserDetail customUserDetail = new CustomUserDetail();
            customUserDetail.setUser(userList.get(0));
            return customUserDetail;
        }
    }

    public UserDetails loadUserById(String id) {
        CustomUserDetail customUserDetail = new CustomUserDetail();
        customUserDetail.setUser(userRepo.findBy_id(id));
        return customUserDetail;
    }
    
}

Implements UserDetailsService để tạo ra 1 lớp có chứ năng lấy thông tin của UserDetails

Clone project về chạy thôi👏👏👏

DONE


We'll never share your email with anyone else.