[Spring] JWT 다루기

이병수·2024년 2월 6일
0

스프링 정리

목록 보기
21/24
post-thumbnail

JWT 다루기

먼저 프로젝트 설정을 추가해야한다.

JWT dependencey 추가한다.

compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

application.properties에 jwt.secret.key 값을 추가하자. (임의로 정함)

jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==

JwtUtil

하나의 모듈로서 동작하는 클래스
(JWT 관련 기능들을 가진 JwtUtil이라는 클래스를 만들어서 JWT 관련 기능을 수행)


JwtUtil 클래스 안에 5가지의 기능을 넣을 것임

일단 먼저 JwtUtil에서 필요한 JWT 데이터를 선언할 것이다.

// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";

// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";

// Token 식별자   Bearer 란 JWT 혹은 OAuth에 대한 토큰을 사용한다는 표시
public static final String BEARER_PREFIX = "Bearer ";

// 토큰 만료시간
public final long TOKEN_TIME = 60 * 60 * 1000L;

// application.properties 에 있는 secretKey 값을 가져오는 방법
@Value("${jwt.secret.key}")
private String secretKey;	

private Key key;

// 암호화 알고리즘 HS256으로 사용할 것임
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

// 로그 찍는 것
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");

// @PostConstruct는 딱 한 번만 받아오면 되는 값을 사용 할 때마다 요청을 새로 호출하는 실수를 방지하기 위해 사용
@PostConstruct 
public void init() {
	byte[] bytes = Base64.getDecoder().decode(secretKey);
	key = Keys.hmacShaKeyFor(bytes); // key값에 우리가 사용할 secret 값이 담겨진다.		
}

이렇게 미리 데이터를 선언한다.


JWT 생성

public String createToken(String username, UserRoleEnum role) {
	Date date = new Date();
	// 이 코드를 통해서 jwt 토큰을 생성할 수 있다.
	return BEARER_PREFIX + Jwts.builder()
		.setSubject(username)				// 사용자 식별자값(ID) (pk든 아이디값이든 유저이름이든 상관없음)
		.claim(AUTHORIZATION_KEY, role)		// 사용자 권한   앞에는 key, 뒤에는 value 권한값
		.setExpiration(new Date(date.getTime() + TOKEN_TIME)) 	// 만료시간
		.setIssuedAt(date)					// 발급입
		.signWith(key,signatureAlgorithm)  	// 암호화 알고리즘 -> 암호화 알고리즘과 키를 넣어줌
		.compact();
	// 이 암호화 코드는 기호에 맞게 옵션을 부여하여 설정하면 된다.
}

JWT 토큰을 생성할 때, Jwts 클래스 안에서 builder 를 통해서 식별자값, 권한, 만료시간, 발급일, 암호화 값을 넣어주었다.
하지만 이렇게 하나하나 넣어줄 필요없이 나중에 프로젝트에 맞게 옵션을 부여하여 설정하면 된다.


생성된 JWT를 Cookie에 저장

public void addJwtToCookie(String token, HttpServletResponse response) {
	try {
		// 공백 불가능 하기 때문에 공백을 %20으로 바꿔주고 인코딩하여 token으로 처리 (쿠키는 공백이 불가능)
		token = URLEncoder.encode(token, "utf-8").replaceAll("\\+","%20");

		// 쿠키를 설정하여 Name-Value 값으로 쿠키 생성 (헤더값과 url 토큰값을 넣음)
		Cookie cookie = new Cookie(AUTHORIZATION_HEADER,token);
		cookie.setPath("/");		// path도 이렇게 한번 줘봄

		response.addCookie(cookie);		// Response 객체에 Cookie를 추가
	} catch (UnsupportedEncodingException e) {
		logger.error(e.getMessage());
	}
}

쿠키에 JWT 토큰을 저장할 때, 처음 JWT 데이터를 만들 때

public static final String BEARER_PREFIX = "Bearer "

이렇게 설정한 적이 있다. 위에서 createToken 메서드에서 해당 토큰의 값 안에는 띄어쓰기가 포함되어 있는데, 쿠키에는 공백이 들어있으면 안되기 때문에 해당 공백을 %20으로 바꿔주었다.

파라미터 값으로 들어온 HttpServletResponse 값에 가공된 쿠키의 값을 넣어주면 된다.


Cookie에 들어있던 JWT 토큰을 Substring

public String substringToken(String tokenValue) {
	// Cookie에 있는 토큰의 값을 가져와서 검사를 한다
	if(StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
		return tokenValue.substring(7); // BAERER_PREFIX 숫자의 길이가 7이므로 그 이후의 값을 짤라서 가져옴
	}
	logger.error("Not Found Token");
	throw new NullPointerException("Not Found Token");
}

나중에 쿠키의 값을 가져와서 체크를 할 때 사용될 메서드이다.
해당 쿠키 안에 JWT 토큰이 있을 때, if문을 통해 해당 토큰이 있는지 확인하고, BEARER_PREFIX 값으로 시작되는지 확인하면 해당 토큰의 값을 짤라서 반환한다.
왜냐하면 BEARER_PREFIX 값이 "Bearer " 이므로 7글자이기 때문에 그 뒤의 토큰값을 가져오는 형식이다.

그 외에는 예외처리한다.


JWT 검증

public boolean validateToken(String token) {
	try {
		// Jwts.parserBuilder() 를 사용하여 JWT를 파싱, JWT 가 위변조되지 않았는지 secretKey(key)값을 넣어 확인
		Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
		return true;
	} catch (SecurityException | MalformedJwtException | SignatureException e) {
		logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
	} catch (ExpiredJwtException e) {
		logger.error("Expired JWT token, 만료된 JWT token 입니다.");
	} catch (UnsupportedJwtException e) {
		logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
	} catch (IllegalArgumentException e) {
		logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
	}
	return false;
}

위에서 Cookie에 들어있던 JWT 토큰을 잘라서 반환된 값을 받아와서 검증한다.
Jwts.parserBuilder()를 사용해서 JWT를 파싱한다.
이 검증 메서드 자체는 따로 커스텀해서 더 좋게 만들 수 있다.


JWT에서 사용자 정보 가져오기

public Claims getUserInfoFromToken(String token) {
	// Jwts.parserBuilder() 와 secretKey를 사용하여 JWT의 Claims를 가져와 담겨 있는 사용자의 정보를 사용
	return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
	}

JWT의 구조 중 Payload 부분에는 토큰에 담긴 정보가 들어있다.
여기에 담긴 정보의 한 '조각'을 클레임(claim)이라고 부르고, 이는 key-value 의 한쌍으로 이루어져있다.
토큰에는 여러개의 클레임 들을 넣을 수 있다.


이렇게 JWT 를 다루는 방법에 대해 작성했지만 커스텀을 통해 더 좋게 만들 수 있다.

JWT 테스트

@GetMapping("/create-jwt")
public String createJwt(HttpServletResponse res) {
    // Jwt 생성
    String token = jwtUtil.createToken("Robbie", UserRoleEnum.USER);

    // Jwt 쿠키 저장
    jwtUtil.addJwtToCookie(token, res);

    return "createJwt : " + token;
}

@GetMapping("/get-jwt")
public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
   // JWT 토큰 substring
   String token = jwtUtil.substringToken(tokenValue);

   // 토큰 검증
   if(!jwtUtil.validateToken(token)){
       throw new IllegalArgumentException("Token Error");
   }

   // 토큰에서 사용자 정보 가져오기
   Claims info = jwtUtil.getUserInfoFromToken(token);
   // 사용자 username
   String username = info.getSubject();
   System.out.println("username = " + username);
   // 사용자 권한
   String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
   System.out.println("authority = " + authority);

   return "getJwt : " + username + ", " + authority;
}

토큰을 생성해서 쿠키에 저장하고 반환해주는 메서드가 createJwt 메서드이다.
HttpServletResponse 객체에 쿠키 값이 저장되어서 클라이언트에게 반환이 된다.

그리고 getJwt 메서드는 해당 쿠키 값을 읽어와서 검증하고 토큰에서 사용자의 정보를 가져오는 방법이다.

@CookieValue 어노테이션을 통해서 쿠키를 전달받을 수 있으며, value 속성을 전달 받을 쿠키의 이름을 지정하면 된다.

위에서 토큰을 생성했을 때,

.setSubject(username)

유저이름을 저장했으니

Claims info = jwtUtil.getUserInfoFromToken(token);
String username = info.getSubject();

Claim 객체를 통해 해당 유저의 이름을 반환 받을 수 있고 사용자 권한 또한 가져올 수 있다.

profile
백엔드 개발자가 되고 싶어요

0개의 댓글