본문으로 바로가기

목차

    1. 환경

    • Spring Boot 2.5.6 (Gradle)
    • JDK 11(Java 11)
    • IntelliJ
    • Postman

    2. JWT

    • 클라이언트와 서버, 서비스와 서비스 사이 통신 시 권한 인가(Authorization)를 위해 사용하는 토큰
    • Bearer Authentication (JWT 혹은 OAuth에 대한 토큰을 사용하면 Bearer 타입 인증)
    • 회원 인증정보 교류할 때 많이 사용합니다.
    • 확장성이 좋아 토큰 기반 인증을 지원하는 다른 서비스에 접근할 수 있습니다.
    • 사용자 인증에 필요한 모든 정보는 토큰 자체에 포함하기 때문에 별도의 인증 저장소(인증서버, DB, 세션 등)가 필요 없습니다.
    • 담을 내용에 따라 크기가 커질 수 있고, 생성 비용이 많이 듭니다. (그러나 요즘 컴퓨팅 성능으로는 신경 쓰지 않아도 될 정도입니다.) 

    3. JWT 구조

    Header.Payload.Signature

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNjM5OTg1NTg5LCJleHAiOjE2NDAwNzE5ODksInN1YiI6ImhlbGxvIEp3dCJ9.7q0elwhYCoHJUTS17HPqUBTwL8C2PsV57ME0-qU5hkM

    4. 의존성

    Spring Boot에 의존성을 추가해 줍니다.

    dependencies {
    	implementation 'io.jsonwebtoken:jjwt:0.9.1'
    }
    <dependencies>
      <dependency>
          <groupId>io.jsonwebtoken</groupId>
          <artifactId>jjwt</artifactId>
          <version>0.9.1</version>
      </dependency>
    </dependencies>

    5. 구현

    시나리오

    1. 토큰 생성

    • url에 정상적으로 userId를 포함하여 보내면 다음과 같이 토큰 값과 토큰 정보에 대한 Json 형태로 응답합니다.

    토큰 발급

    2. 토큰 인증

    • 발급받은 토큰을 Http Header의 Authorization에 정상적인 값을 넣어서 보내면 성공했다는 Json 형태로 응답합니다.

    토큰 인증

    5.1. JwtProvider

    • 토큰을 생성하고 검증할 클래스를 생성합니다.
    • secretKey는 application.properties 또는 application.yml에 저장합니다. 
    jwt.password=testPassword
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Header;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import lombok.extern.log4j.Log4j2;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    import java.time.Duration;
    import java.util.Base64;
    import java.util.Date;
    
    @Component
    public class JwtProvider {
    
        @Value("${jwt.password}")
        private String secretKey;
    
        //==토큰 생성 메소드==//
        public String createToken(String subject) {
            Date now = new Date();
            Date expiration = new Date(now.getTime() + Duration.ofDays(1).toMillis()); // 만료기간 1일
    
            return Jwts.builder()
                    .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // (1)
                    .setIssuer("test") // 토큰발급자(iss)
                    .setIssuedAt(now) // 발급시간(iat)
                    .setExpiration(expiration) // 만료시간(exp)
                    .setSubject(subject) //  토큰 제목(subject)
                    .signWith(SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(secretKey.getBytes())) // 알고리즘, 시크릿 키
                    .compact();
        }
    
        //==Jwt 토큰의 유효성 체크 메소드==//
        public Claims parseJwtToken(String token) {
            token = BearerRemove(token); // Bearer 제거
            return Jwts.parser()
                    .setSigningKey(Base64.getEncoder().encodeToString(secretKey.getBytes()))
                    .parseClaimsJws(token)
                    .getBody();
        }
        
        //==토큰 앞 부분('Bearer') 제거 메소드==//
        private String BearerRemove(String token) {
            return token.substring("Bearer ".length());
        }
    }

    5.2. 컨트롤러 생성

    • 토큰 생성(발급) 컨트롤러와 토큰 인증 컨트롤러를 만들어줍니다.

    토큰 생성(발급) 컨트롤러

    //==토큰 생성 컨트롤러==//
    @GetMapping(value = "/tokenCreate/{userId}")
    public TokenResponse createToken(@PathVariable("userId") String userId) throws Exception {
        String token = jwtProvider.createToken(userId); // 토큰 생성
        Claims claims = jwtProvider.parseJwtToken("Bearer "+ token); // 토큰 검증
    
        TokenDataResponse tokenDataResponse = new TokenDataResponse(token, claims.getSubject(), claims.getIssuedAt().toString(), claims.getExpiration().toString());
        TokenResponse tokenResponse = new TokenResponse("200", "OK", tokenDataResponse);
    
        return tokenResponse;
    }

     

    토큰 인증 컨트롤러

    //==토큰 인증 컨트롤러==//
    @GetMapping(value = "/checkToken")
    public TokenResponseNoData checkToken(@RequestHeader(value = "Authorization") String token) throws Exception {
        Claims claims = jwtProvider.parseJwtToken(token);
    
        TokenResponseNoData tokenResponseNoData = new TokenResponseNoData("200", "success");
        return tokenResponseNoData;
    }

     

    전체 소스 

    import io.jsonwebtoken.Claims;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.RequiredArgsConstructor;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequiredArgsConstructor
    public class TestController {
    
        private final JwtProvider jwtProvider;
    
        //==토큰 생성 컨트롤러==//
        @GetMapping(value = "/tokenCreate/{userId}")
        public TokenResponse createToken(@PathVariable("userId") String userId) throws Exception {
            String token = jwtProvider.createToken(userId); // 토큰 생성
            Claims claims = jwtProvider.parseJwtToken("Bearer "+ token); // 토큰 검증
    
            TokenDataResponse tokenDataResponse = new TokenDataResponse(token, claims.getSubject(), claims.getIssuedAt().toString(), claims.getExpiration().toString());
            TokenResponse tokenResponse = new TokenResponse("200", "OK", tokenDataResponse);
    
            return tokenResponse;
        }
    
        //==토큰 인증 컨트롤러==//
        @GetMapping(value = "/checkToken")
        public TokenResponseNoData checkToken(@RequestHeader(value = "Authorization") String token) throws Exception {
            Claims claims = jwtProvider.parseJwtToken(token);
    
            TokenResponseNoData tokenResponseNoData = new TokenResponseNoData("200", "success");
            return tokenResponseNoData;
        }
    
        //==Response DTO==//
        @Data
        @AllArgsConstructor
        static class TokenResponse<T> {
    
            private String code;
            private String msg;
            private T data;
        }
    
        //==Response DTO==//
        @Data
        @AllArgsConstructor
        static class TokenResponseNoData<T> {
    
            private String code;
            private String msg;
        }
    
        //==Response DTO==//
        @Data
        @AllArgsConstructor
        static class TokenDataResponse {
            private String token;
            private String subject;
            private String issued_time;
            private String expired_time;
        }
    }

    5.3. 예외 처리

    • 토큰 예외는 대표적으로 다음과 같은 예외가 존재합니다.
    • @RestControllerAdvice, @ExceptionHandler, @ResponseStatus를 이용해서 예외를 처리합니다.
    UnsupportedJwtException : jwt가 예상하는 형식과 다른 형식이거나 구성
    MalformedJwtException : 잘못된 jwt 구조
    ExpiredJwtException : JWT의 유효기간이 초과
    SignatureException : JWT의 서명실패(변조 데이터)

     

    예외 시 Response Body

    UnsupportedJwtException 예외 시 응답
    {
        "code": "401",
        "msg": "UnsupportedJwtException"
    }
    
    MalformedJwtException 예외 시 응답
    {
        "code": "402",
        "msg": "MalformedJwtException"
    }
    
    ExpiredJwtException 예외 시 응답
    {
        "code": "403",
        "msg": "ExpiredJwtException"
    }
    
    SignatureException 예외 시 응답
    {
        "code": "404",
        "msg": "ExpiredJwtException"
    }

     

     

    예외처리 코드

    import io.jsonwebtoken.ExpiredJwtException;
    import io.jsonwebtoken.MalformedJwtException;
    import io.jsonwebtoken.SignatureException;
    import io.jsonwebtoken.UnsupportedJwtException;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.MissingRequestHeaderException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseStatus;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    @RestControllerAdvice
    public class ExceptionController {
    
        @ExceptionHandler(Exception.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public Response ServerException2(Exception e) {
            e.printStackTrace();
            return new Response("500", "서버 에러");
        }
    
        @ExceptionHandler(MissingRequestHeaderException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Response MissingRequestHeaderException(Exception e) {
            e.printStackTrace();
            return new Response("400", "MissingRequestHeaderException");
        }
    
        @ExceptionHandler(UnsupportedJwtException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Response UnsupportedJwtException(Exception e) {
            e.printStackTrace();
            return new Response("401", "UnsupportedJwtException");
        }
    
        @ExceptionHandler(MalformedJwtException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Response MalformedJwtException(Exception e) {
            e.printStackTrace();
            return new Response("402", "MalformedJwtException");
        }
    
        @ExceptionHandler(ExpiredJwtException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Response ExpiredJwtException(Exception e) {
            e.printStackTrace();
            return new Response("403", "ExpiredJwtException");
        }
    
        @ExceptionHandler(SignatureException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Response SignatureException(Exception e) {
            e.printStackTrace();
            return new Response("404", "SignatureException");
        }
    
        //Response DTO
        @Data
        @AllArgsConstructor
        static class Response {
            private String code;
            private String msg;
        }
    }

    6. 테스트

    • 테스트는 Postman을 사용했습니다. Spring Boot를 실행해 테스트합니다.

    1. 토큰 발급

    토큰 발급 성공

    2. 토큰 인증

    토큰 인증 성공

    3. 예외 상황

    • 예외 처리 응답도 잘 작동하는 것을 확인할 수 있습니다.

    토큰 값이 없을 경우
    MalformedJwtException
    ExpiredJwtException
    SignatureException