본문 바로가기
프로젝트/낙낙(KnockKnock)

Spring Security JWT 유저, Oauth에 대한 대대적인 공사-2(JWT)

by 티코딩 2023. 9. 13.

1편 - Local User (https://thcoding.tistory.com/114)

2편 - (현재글)JWT (https://thcoding.tistory.com/115)

3편 - OAuth2 (https://thcoding.tistory.com/116)

4편 - OAuth2 test(https://thcoding.tistory.com/118)

 

 

저번 포스팅은 local User이 사용하는 코드에대한 리뷰였고 이번엔 JWT에 관한 코드들을 리뷰해보겠다.

JWT 란, Json Web token의 약자고, 일종의 사용기한이 있는 신분증같은것이다. 예를 들어, 유저가 비밀번호를 업데이트를 하고자 한다 치면, 이 유저는 인증이 되어있어야한다. 이 유저가 로그인할때 우리는 토큰을 주고, 유저는 이 토큰을 갖고 비밀번호를 업데이트하려고 하면 우리는 승인해준다. 

 

ㅇ JwtConfig

@Configuration
public class JwtConfig {
    private String secret;
    private final UserDetailsService userDetailsService;
    @Autowired
    public JwtConfig(@Value("${jwt.secret}")String secret, UserDetailsService userDetailsService) {
        this.secret = secret;
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public AuthTokenProvider jwtProvider() {
        return new AuthTokenProvider(secret,userDetailsService);
    }
}

configuration 뜻 그대로 구성이다. jwt 토큰과 관련된 구성이다. 

 

ㅇ JwtUtils

@Component
public class JwtUtils {
    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.expirationMs}")
    private int expirationMs;

    /**
     * 사용자 식별자를 기반으로 JWT 토큰을 생성합니다.
     *
     * @param userId 사용자 식별자
     * @return 생성된 JWT 토큰
     */
    public String generateToken(String userId) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expirationMs);

        return Jwts.builder()
                .setSubject(userId)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, secretKey)
                .compact();
    }

    /**
     * 주어진 JWT 토큰에서 사용자 식별자를 추출합니다.
     *
     * @param token JWT 토큰
     * @return 사용자 식별자
     */
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody();

        return Long.parseLong(claims.getSubject());
    }

    /**
     * 주어진 JWT 토큰의 유효성을 검사합니다.
     *
     * @param token JWT 토큰
     * @return 토큰의 유효성 여부
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (SignatureException ex) {
            System.out.println("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            System.out.println("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            System.out.println("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            System.out.println("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            System.out.println("JWT claims string is empty");
        }
        return false;
    }
}

Jwt 토큰을 생성하고 검증한다.

 

ㅇ TokenValidFailedException

public class TokenValidFailedException extends RuntimeException {

    public TokenValidFailedException() {
        super("Failed to generate Token.");
    }

    private TokenValidFailedException(String message) {
        super(message);
    }
}

예외는 따로 이렇게 빼준다.

 

ㅇ AuthToken

@Slf4j
@RequiredArgsConstructor
public class AuthToken {

    @Getter
    private final String token;
    private final Key key;

    private static final String AUTHORITIES_KEY = "role";

    AuthToken(String id, Date expiry, Key key) {
        this.key = key;
        this.token = createAuthToken(id, expiry);
    }

    AuthToken(String id, String role, Date expiry, Key key) {
        this.key = key;
        this.token = createAuthToken(id, role, expiry);
    }

    private String createAuthToken(String id, Date expiry) {
        return Jwts.builder()
                .setSubject(id)
                .signWith(key, SignatureAlgorithm.HS256)
                .setExpiration(expiry)
                .compact();
    }

    private String createAuthToken(String id, String role, Date expiry) {
        return Jwts.builder()
                .setSubject(id)
                .claim(AUTHORITIES_KEY, role)
                .signWith(key, SignatureAlgorithm.HS256)
                .setExpiration(expiry)
                .compact();
    }

    public boolean validate() {
        return this.getTokenClaims() != null;
    }

    public Claims getTokenClaims() {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (SecurityException e) {
            log.info("Invalid JWT signature.");
        } catch (MalformedJwtException e) {
            log.info("Invalid JWT token.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token.");
        } catch (IllegalArgumentException e) {
            log.info("JWT token compact of handler are invalid.");
        }
        return null;
    }

    public Claims getExpiredTokenClaims() {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token.");
            return e.getClaims();
        }
        return null;
    }
}

 

ㅇ AuthTokenProvider

@Slf4j
public class AuthTokenProvider {
    private final UserDetailsService userDetailsService;

    private final Key key;
    private static final String AUTHORITIES_KEY = "role";

    public AuthTokenProvider(String secret, UserDetailsService userDetailsService) {
        this.key = Keys.hmacShaKeyFor(secret.getBytes());
        this.userDetailsService = userDetailsService;

    }

    public AuthToken createAuthToken(String id, Date expiry) {
        return new AuthToken(id, expiry, key);
    }

    public AuthToken createAuthToken(String id, String role, Date expiry) {
        return new AuthToken(id, role, expiry, key);
    }

    public AuthToken convertAuthToken(String token) {
        return new AuthToken(token, key);
    }

    public Authentication getAuthentication(AuthToken authToken) throws TokenValidFailedException {
        UserDetails userDetails = userDetailsService.loadUserByUsername(authToken.getTokenClaims().getSubject());
        return new UsernamePasswordAuthenticationToken(userDetails, authToken, userDetails.getAuthorities());
//        if(authToken.validate()) {
//
//            Claims claims = authToken.getTokenClaims();
//            Collection<? extends GrantedAuthority> authorities =
//                    Arrays.stream(new String[]{claims.get(AUTHORITIES_KEY).toString()})
//                            .map(SimpleGrantedAuthority::new)
//                            .collect(Collectors.toList());
//
//            log.debug("claims subject := [{}]", claims.getSubject());
//            User principal = new User(claims.getSubject(), "", authorities);
//
//            return new UsernamePasswordAuthenticationToken(principal, authToken, authorities);
//        } else {
//            throw new TokenValidFailedException();
//        }
    }
}

여기서 문제가 있었다. 마지막 부분에 주석처리한부분에서 deeplify 포스팅에선 주석처리된 부분으로 되어있었는데, 내 프로젝트에서는 오류가 발생해 블로깅해 바꿔주니 아주 잘 되었다. 진짜 이거때문인지 모르고 뭐때문인지 별걸 다 해봤는데 결국 여기서 문제가 있었던거였다. 한 3일은 날린거같다ㅠ

ㅇ TokenAuthenticationFilter

@Slf4j
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final AuthTokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException, java.io.IOException {

        String tokenStr = HeaderUtil.getAccessToken(request);
        AuthToken token = tokenProvider.convertAuthToken(tokenStr);
            //토큰이 유효하면 토큰 기반으로 인증 객체 생성함
            if (token.validate()) {
                Authentication authentication = tokenProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        filterChain.doFilter(request, response);
    }
}

사용자의 요청을 필터링하고, HTTP 요청 헤더에 포함된 토큰을 기반으로 사용자를 인증함. OncePerRequestFilter를 extends 했기때문에 한 요청당 한번만 이 필터를 거치게 된다. 그래서 테스트할때 디버깅하다보면, 여기서 어어어어어어어어엄청나게 걸린다.

String tokenStr = HeaderUtil.getAccessToken(request); 여기서 http요청 헤더에서 accessToken을 추출한다. 그다음 convertAuthToken으로 AuthToken객체로 변환한다. 그리고 주석대로 토큰이 유효하면 인증객체를 생성한다.

 

ㅇ HeaderUtil

public class HeaderUtil {

    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer ";

    public static String getAccessToken(HttpServletRequest request) {
        String headerValue = request.getHeader(HEADER_AUTHORIZATION);

        if (headerValue == null) {
            return null;
        }

        if (headerValue.startsWith(TOKEN_PREFIX)) {
            return headerValue.substring(TOKEN_PREFIX.length());
        }

        return null;
    }
}

 

다음 포스팅에선 Oauth2 에 관련한 나머지 코드들을 리뷰해보겠다.