Làm cách nào để làm mới mã thông báo trong bảo mật mùa xuân

Aug 15 2020

Đường thẳng này:

Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();

Gặp lỗi như thế này khi mã thông báo jwt của tôi hết hạn:

JWT hết hạn vào 2020-05-13T07: 50: 39Z. Thời điểm hiện tại: 2020-05-16T21: 29: 41Z.

Cụ thể hơn, chính hàm này sẽ ném ngoại lệ "ExpiredJwtException":

Làm cách nào để xử lý các trường hợp ngoại lệ này? Tôi có nên bắt họ và gửi lại cho khách hàng một thông báo lỗi và buộc họ đăng nhập lại không?

Làm cách nào để triển khai tính năng mã thông báo làm mới? Tôi đang sử dụng Spring và mysql trong phần phụ trợ và vuejs trong giao diện người dùng.

Tôi tạo mã thông báo ban đầu như thế này:

   @Override
        public JSONObject login(AuthenticationRequest authreq) {
            JSONObject json = new JSONObject();
    
            try {
                Authentication authentication = authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(authreq.getUsername(), authreq.getPassword()));
    
                UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
                List<String> roles = userDetails.getAuthorities().stream().map(item -> item.getAuthority())
                        .collect(Collectors.toList());
    
                if (userDetails != null) {
    
                    final String jwt = jwtTokenUtil.generateToken(userDetails);
    
    
                    JwtResponse jwtres = new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(),
                            userDetails.getEmail(), roles, jwtTokenUtil.extractExpiration(jwt).toString());
    
                    return json.put("jwtresponse", jwtres);
                }
            } catch (BadCredentialsException ex) {
                json.put("status", "badcredentials");
            } catch (LockedException ex) {
                json.put("status", "LockedException");
            } catch (DisabledException ex) {
                json.put("status", "DisabledException");
            }
    
            return json;
        }

Và sau đó trong lớp JwtUtil:

   public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            return createToken(claims, userDetails.getUsername());
        }
    
   private String createToken(Map<String, Object> claims, String subject) {
            return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + EXPIRESIN))
                    .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
        }

Để biết thêm thông tin, đây là hàm doFilterInternal của tôi lọc mọi yêu cầu:

   @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException, ExpiredJwtException, MalformedJwtException {

        try {

            final String authorizationHeader = request.getHeader("Authorization");

            String username = null;
            String jwt = null;

            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                jwt = authorizationHeader.substring(7);
                username = jwtUtil.extractUsername(jwt);
            }

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userService.loadUserByUsername(username);

                boolean correct = jwtUtil.validateToken(jwt, userDetails);

                if (correct) {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());

                    usernamePasswordAuthenticationToken
                            .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

                }
            }

            chain.doFilter(request, response);
        } catch (ExpiredJwtException ex) {
            resolver.resolveException(request, response, null, ex);
        }
    } 

Trả lời

3 doctore Aug 16 2020 at 17:38

Có 2 cách tiếp cận chính để đối phó với những tình huống như vậy:


Quản lý quyền truy cập và làm mới mã thông báo

Trong trường hợp này, quy trình là như sau:

  1. Người dùng đăng nhập vào ứng dụng (bao gồm usernamepassword)

  2. Ứng dụng phụ trợ của bạn trả về mọi thông tin xác thực bắt buộc và:

    2.1 Truy cập mã thông báo JWT với thời gian hết hạn thường là "thấp" (15, 30 phút, v.v.).

    2.2 Làm mới mã thông báo JWT với thời gian hết hạn lớn hơn thời gian truy cập.

  3. Từ bây giờ, ứng dụng giao diện người dùng của bạn sẽ sử dụng access tokentrong Authorizationtiêu đề cho mọi yêu cầu.

Khi chương trình phụ trợ trở lại 401, ứng dụng giao diện người dùng sẽ cố gắng sử dụng refresh token(sử dụng một điểm cuối cụ thể) để lấy thông tin đăng nhập mới mà không buộc người dùng phải đăng nhập lại.

Làm mới luồng mã thông báo (Đây chỉ là một ví dụ, thường chỉ mã làm mới được gửi)

Nếu không có vấn đề gì xảy ra thì người dùng sẽ có thể tiếp tục sử dụng ứng dụng. Nếu backend trả về new 401=> frontend sẽ chuyển hướng đến trang đăng nhập.


Chỉ quản lý một mã thông báo Jwt

Trong trường hợp này, quy trình tương tự như quy trình trước đó và bạn có thể tạo điểm cuối của riêng mình để đối phó với các tình huống như vậy: /auth/token/extend(ví dụ), bao gồm tham số Jwt đã hết hạn của yêu cầu.

Bây giờ tùy thuộc vào bạn quản lý:

  • Bao nhiêu thời gian một mã thông báo Jwt đã hết hạn sẽ "hợp lệ" để gia hạn?

Ý tôi là, điểm cuối mới sẽ có hành vi tương tự như làm mới trong phần trước, ý tôi là, sẽ trả về một mã thông báo Jwt mới hoặc 401lâu hơn, từ quan điểm của giao diện người dùng, luồng sẽ giống nhau.


Một điều quan trọng , không phụ thuộc vào cách tiếp cận bạn muốn làm theo, "điểm cuối mới" nên được loại trừ khỏi các điểm cuối được xác thực Spring bắt buộc, vì bạn sẽ tự quản lý bảo mật:

public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
  ..

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.
      ..
      .authorizeRequests()
      // List of services do not require authentication
      .antMatchers(Rest Operator, "MyEndpointToRefreshOrExtendToken").permitAll()
      // Any other request must be authenticated
      .anyRequest().authenticated()
      ..
   }
}
2 QuocTruong Aug 16 2020 at 18:18

Bạn có thể gọi API để nhận mã làm mới như bên dưới

POST https://yourdomain.com/oauth/token 

Header
  "Authorization": "Bearer [base64encode(clientId:clientSecret)]" 

Parameters
  "grant_type": "refresh_token"
  "refresh_token": "[yourRefreshToken]"

Xin lưu ý rằng,

  • base64encode là phương pháp mã hóa ủy quyền máy khách. Bạn có thể sử dụng trực tuyến tạihttps://www.base64encode.org/
  • các refresh_token là giá trị chuỗi của grant_type
  • yourRefreshTokenmã làm mới nhận được bằng mã thông báo truy cập JWT

Kết quả có thể được xem là

{
    "token_type":"bearer",
    "access_token":"eyJ0eXAiOiJK.iLCJpYXQiO.Dww7TC9xu_2s",
    "expires_in":20,
    "refresh_token":"7fd15938c823cf58e78019bea2af142f9449696a"
}

Chúc may mắn.