Bagaimana cara menyegarkan token di keamanan Spring

Aug 15 2020

Garis ini:

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

Melempar kesalahan seperti ini ketika token jwt saya kedaluwarsa:

JWT kedaluwarsa pada 2020-05-13T07: 50: 39Z. Waktu saat ini: 2020-05-16T21: 29: 41Z.

Lebih khusus lagi, fungsi inilah yang memunculkan pengecualian "ExpiredJwtException":

Bagaimana cara menangani pengecualian ini? Haruskah saya menangkap mereka dan mengirim kembali ke klien pesan kesalahan dan memaksa mereka untuk masuk kembali?

Bagaimana cara menerapkan fitur token penyegaran? Saya menggunakan Spring dan mysql di backend dan vuejs di front end.

Saya menghasilkan token awal seperti ini:

   @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;
        }

Dan kemudian di kelas 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();
        }

Untuk info lebih lanjut, berikut adalah fungsi doFilterInternal saya yang memfilter setiap permintaan:

   @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);
        }
    } 

Jawaban

3 doctore Aug 16 2020 at 17:38

Ada 2 pendekatan utama untuk menghadapi situasi seperti itu:


Kelola akses dan segarkan token

Dalam hal ini, alurnya adalah sebagai berikut:

  1. Pengguna login ke aplikasi (termasuk usernamedan password)

  2. Aplikasi backend Anda mengembalikan informasi kredensial yang diperlukan dan:

    2.1 Akses token JWT dengan waktu kedaluwarsa biasanya "rendah" (15, 30 menit, dll).

    2.2 Segarkan token JWT dengan waktu kedaluwarsa lebih besar dari waktu akses.

  3. Mulai sekarang, aplikasi frontend Anda akan digunakan access tokendi Authorizationheader untuk setiap permintaan.

Saat backend kembali 401, aplikasi frontend akan mencoba menggunakan refresh token(menggunakan titik akhir tertentu) untuk mendapatkan kredensial baru, tanpa memaksa pengguna untuk masuk lagi.

Segarkan aliran token (Ini hanya contoh, biasanya hanya token penyegaran yang dikirim)

Jika tidak ada masalah, maka pengguna dapat terus menggunakan aplikasi tersebut. Jika backend mengembalikan new 401=> frontend harus dialihkan ke halaman login.


Kelola hanya satu token Jwt

Dalam kasus ini, alirannya mirip dengan yang sebelumnya dan Anda dapat membuat titik akhir Anda sendiri untuk menangani situasi seperti itu: /auth/token/extend(misalnya), termasuk Jwt yang kedaluwarsa sebagai parameter permintaan.

Sekarang terserah Anda untuk mengatur:

  • Berapa lama token Jwt yang kedaluwarsa akan "valid" untuk memperpanjangnya?

Titik akhir baru akan memiliki perilaku yang sama dengan penyegaran di bagian sebelumnya, maksud saya, akan mengembalikan token Jwt baru atau 401lebih, dari sudut pandang frontend alirannya akan sama.


Satu hal penting , terlepas dari pendekatan yang ingin Anda ikuti, "titik akhir baru" harus dikecualikan dari titik akhir terautentikasi Spring yang diperlukan, karena Anda akan mengelola keamanan sendiri:

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

Anda dapat memanggil API untuk mendapatkan token penyegaran seperti di bawah ini

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

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

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

Harap diperhatikan bahwa,

  • base64encode adalah metode untuk mengenkripsi otorisasi klien. Anda dapat menggunakan secara online dihttps://www.base64encode.org/
  • yang refresh_token adalah nilai String dari grant_type
  • yourRefreshToken adalah token penyegaran yang diterima dengan token akses JWT

Hasilnya bisa dilihat sebagai

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

Semoga berhasil.