Spring Boot + OAuth2 #3

홍준식·2024년 2월 25일

기존에 작성한 token service는 java-jwt를 사용하고있지만 jjwt가 더 많은 사용자와 레퍼런스를 보유하고 있고 가장 최근까지 업데이트되고있기에 패키지를 교체하도록 하였다.

jjwt의 경우

return Jwts.builder()  
        .subject(sub)  
        .issuedAt(now)  
        .expiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_PERIOD))  
        .signWith(SECRET)  
        .compact();

위와 같이 키 생성을 하고

 return Jwts.parser()  
        .verifyWith(SECRET)  
        .build()  
        .parseSignedClaims(token)  
        .getPayload()  
        .getSubject();

위와 같이 키를 검증 및 내용을 가져올 수 있다.

jwt는 헤더, 페이로드, 시그니처로 구성되어있고
헤더에는 알고리즘과 토큰의 타입을
페이로드에는 전달하는 데이터를
시그니처에는 헤더와 페이로드를 인코딩 한 값과 비밀키 값을 해싱한 값으로 구성되어있기에 토큰을 위조하면 시그니처 값이 달라져 위변조 여부를 판별할 수 있도록 한다.

또한 페이로드에는 등록된 클레임, 공개 클레임, 비공개 클레임이 존재하고
등록된 클레임은

  • iss: issuer
  • sub: subject, 토큰 제목. 개개인에 부여되는 식별자 (이메일 등)
  • aud: audience, 토큰 대상자
  • exp: expiration 토큰 만료시간
  • nbf: not before, 토큰 활성 날짜로 nbf 이후부터 토큰이 활성화된다
  • iat: issued at, 토큰 발급 시간
    가 존재한다.

공개 클레임은 충돌이 방지되도록 URI 형태의 이름을 가진다.

{
    "https://www.leporem.art/jwt_claims/is_staff": true
}

비공개 클레임은 클라이언트와 서버 간의 협의 하에 사용되는 클레임으로 공개 클레임과 달리 충돌할 수 있다.

사용자 ID값만을 식별할 것이기에 별도의 클레임 없이 sub와 exp만을 사용하도록 하였다.

package com.palindrome.studit.global.config.security.application;  
  
import com.palindrome.studit.domain.user.exception.InvalidTokenException;  
import io.jsonwebtoken.JwtException;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.io.Decoders;  
import io.jsonwebtoken.security.Keys;  
import lombok.RequiredArgsConstructor;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.stereotype.Service;  
  
import javax.crypto.SecretKey;  
import java.util.Date;  
  
@RequiredArgsConstructor  
@Service  
public class TokenService {  
    private SecretKey SECRET;  
    @Value("${jwt.access-token.expire-period}")  
    private int ACCESS_TOKEN_EXPIRE_PERIOD;  
    @Value("${jwt.refresh-token.expire-period}")  
    private int REFRESH_TOKEN_EXPIRE_PERIOD;  
  
    @Autowired  
    public TokenService(@Value("${jwt.secret}") String SECRET_KEY) {  
        byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);  
        this.SECRET = Keys.hmacShaKeyFor(keyBytes);  
    }  
  
    public String createAccessToken(String sub) {  
        Date now = new Date();  
        return Jwts.builder()  
                .subject(sub)  
                .issuedAt(now)  
                .expiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_PERIOD))  
                .signWith(SECRET)  
                .compact();  
    }  
  
    public String createRefreshToken() {  
        Date now = new Date();  
        return Jwts.builder()  
                .issuedAt(now)  
                .expiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRE_PERIOD))  
                .signWith(SECRET)  
                .compact();  
    }  
  
    public String parseSubject(String token) throws InvalidTokenException {  
        try {  
            return Jwts.parser()  
                    .verifyWith(SECRET)  
                    .build()  
                    .parseSignedClaims(token)  
                    .getPayload()  
                    .getSubject();  
        } catch (JwtException e) {  
            throw new InvalidTokenException();  
        }  
    }  
  
    public Date parseExpiration(String token) throws InvalidTokenException {  
        try {  
            return Jwts.parser()  
                    .verifyWith(SECRET)  
                    .build()  
                    .parseSignedClaims(token)  
                    .getPayload()  
                    .getExpiration();  
        } catch (JwtException e) {  
            throw new InvalidTokenException();  
        }  
    }  
}

access token 검증 API

jwt 정보를 전달할 DTO를 추가한다.

package com.palindrome.studit.domain.user.dto;  
  
import lombok.Builder;  
import lombok.Getter;  
  
import java.util.Date;  
  
@Builder  
@Getter  
public class JwtDTO {  
    private String sub;  
    private Date exp;  
}
package com.palindrome.studit.domain.user.api;  
  
import com.palindrome.studit.domain.user.dto.JwtDTO;  
import com.palindrome.studit.domain.user.exception.InvalidTokenException;  
import com.palindrome.studit.global.config.security.application.TokenService;  
import lombok.RequiredArgsConstructor;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.*;  
  
@RequiredArgsConstructor  
@RestController  
@RequestMapping("/auth")  
public class AuthController {  
    private final TokenService tokenService;  
  
    @GetMapping("/token-info/{accessToken}")  
    public JwtDTO getJwtInfo(@PathVariable String accessToken) throws InvalidTokenException {  
        return JwtDTO.builder()  
                .sub(tokenService.parseSubject(accessToken))  
                .exp(tokenService.parseExpiration(accessToken))  
                .build();  
    }  
  
    @ExceptionHandler({ InvalidTokenException.class })  
    public ResponseEntity<Object> invalidTokenException(final InvalidTokenException e) {  
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());  
    }  
}

AuthController를 추가하여 토큰을 검증하도록 한다.

0개의 댓글