목차
1. 환경
- Spring Boot 2.5.6 (Gradle)
- JDK 11(Java 11)
- IntelliJ
- Postman
2. JWT
- 클라이언트와 서버, 서비스와 서비스 사이 통신 시 권한 인가(Authorization)를 위해 사용하는 토큰
- Bearer Authentication (JWT 혹은 OAuth에 대한 토큰을 사용하면 Bearer 타입 인증)
- 회원 인증과 정보 교류할 때 많이 사용합니다.
- 확장성이 좋아 토큰 기반 인증을 지원하는 다른 서비스에 접근할 수 있습니다.
- 사용자 인증에 필요한 모든 정보는 토큰 자체에 포함하기 때문에 별도의 인증 저장소(인증서버, DB, 세션 등)가 필요 없습니다.
- 담을 내용에 따라 크기가 커질 수 있고, 생성 비용이 많이 듭니다. (그러나 요즘 컴퓨팅 성능으로는 신경 쓰지 않아도 될 정도입니다.)
3. JWT 구조
- JWT는 .을 기준으로 헤더(Header) - 내용(Payload) - 서명(Signature)으로 구성되어있습니다.
- https://veneas.tistory.com/entry/JWTJSON-Web-Token-%EA%B5%AC%EC%A1%B0 (자세한 내용은 다음 링크에서 확인해주세요)
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. 예외 상황
- 예외 처리 응답도 잘 작동하는 것을 확인할 수 있습니다.
'Backend > Spring' 카테고리의 다른 글
[Spring Boot] 스프링 스케줄 작업 적용하기(@EnableScheduling, @Scheduled) (0) | 2022.02.02 |
---|---|
[Spring Boot] Filter 를 이용하여 Response Body 핸들링 (HttpServletResponseWrapper) (0) | 2022.01.27 |
[Spring Boot] 스프링 부트 Log4J2 취약점 조치 (CVE-2021-44832) (0) | 2021.12.29 |
[Spring Boot] 스프링 부트 Log4J2 취약점 조치 (CVE-2021-45105) (0) | 2021.12.18 |
[Spring Boot] 스프링 부트 Logback 취약점 조치 (CVE-2021-42550) (0) | 2021.12.18 |