JWT 토큰 발급받기

알파로그·2023년 5월 14일
0

✏️ JWT

  • JSON Web Token 라이브러리

⚠️ WebSecurityConfigureAdapter

  • JWT 토큰을 발급받기 위해 사용했던 추상 객체로,
    Spring Security 라이브러리를 의존하면 사용할 수 있었다.
  • 2022 년 2월 21일에 업데이트된 Spring Security 5.7.0-M2 버전 이후부터 서비스가 종료되었다.
    • 즉, 이 객체를 사용하지 않고 최신 방법으로 JWT 토큰을 발급받을 예정이다.
    • 만약 W**ebSecurityConfigureAdapter 객체를 사용하고 싶다면 버전을 낮춰야한다.**
      • java 11
      • spring boot 2.7.3

✏️ 환경설정

📍 Dependency

implementation 'org.springframework.boot:spring-boot-starter-security'

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

📍 Application yml

  • JWT 의 암호화 복호화를 위한 Secrit key 를 추가한다.
    • HS256 알고리즘을 사용해야 하기 때문에 256 bit 보다 커야한다.
    • 한단어 당 8bit 이므로 32 글자 이상이 필요하다.
jwt:
  secret: VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa

✏️ TokenInfo

  • 클라이언트에 토큰을 보내기 위한 DTO
    • grantType
      • HTTP header 에 prefix 로 붙여주는 타입으로 Bearer 를 사용한다.
package com.baeker.baeker.base.security.jwt;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Builder
@Data
@AllArgsConstructor
public class JwtTokenInfo {

    private String grantType;
    private String accessToken;
    private String refreshToken;
}

✏️ JwtTokenProbider

  • Access Token 과 Refresh Token 을 생성하는 Class
  • 86480000
    • Date 생성자에 입력하는 수치로 토큰의 유효기간을 뜻한다.
    • 86480000 는 24시간을 뜻한다.
      • 246060*1000=86480000
    • 보통 토큰은 30분 정도로 생성하는데 원활한 test 를 위해 24시간으로 세팅했다.
package com.baeker.baeker.base.security.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

@Slf4j
@Component
public class JwtTokenProvider {

    private final Key key;

    public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
        byte[] keyByes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyByes);
    }

    //-- Access Token, Refresh Token 생성 --//
    // user 정보 기반으로 생성됨 //
    public JwtTokenInfo generateToken(Authentication authentication) {

        // 권한 생성
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();

        // Access Token 생성
        Date accessTokenExpiresIn = new Date(now + 86400000);
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim("auth", authorities)
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        // Refresh Token 생성
        String refreshToken = Jwts.builder()
                .setExpiration(new Date(now + 86400000))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        return JwtTokenInfo.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    //-- JWT 복호화 --//
    public Authentication getAuthentication(String accessToken) {

        // Token 복호화
        Claims claims = parseClaims(accessToken);

        if (claims.get("auth") == null)
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");

        // Claim 에서 권한 정보 가져오기
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get("auth").toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        // UserDetails 객체를 생성해 Authentication 리턴
        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    //-- Token 정보 검증 --//
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT Token", e);
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT Token", e);
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims String is empty", e);
        }
        return false;
    }

    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(accessToken)
                    .getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }
}

✏️ JwtAuthenticationFilter

  • 클라이언트 요청시 JWT 를 인증하는 커스텀 필터
  • UsernamePasswordAuthenticationFilter 이전에 실행된다.
    • JwtAuthenticationFilter 를 통과 하면 UsernamePasswordAuthenticationFilter 이후 필터는 통과한것으로 본다는 의미이다.
    • 즉, Username + PW 를 통과한 인증을 JWT 를 통해 수행한다는 의미
package com.baeker.baeker.base.security.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // Request Header 에서 JWT 추출
        String token = resolveToken((HttpServletRequest) request);

        // Token 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

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

        return null;
    }
}
profile
잘못된 내용 PR 환영

0개의 댓글