JWT Series

JWT (Json Web Token) 은 현재 대부분의 인증 허가 관련해서 사용되는 기술입니다. 필요한 모든 정보를 한 객체에 담아서 전달하기에 JWT 하나로 많은 인증이 가능합니다. 또한 웹 표준을 따르기에 대부분의 언어가 이를 지원합니다.

JWT 구조

image

JWT 는 헤더, 페이로드, 시그니처가 “.” 를 구분자로 하나의 토큰을 이룹니다.

JWT 디버거

해당 사이트에서 실제 JWT 와 해석된 형태를 확인해볼 수 있습니다.

다음부터는 가장 간단한 JWT 예시를 들어 설명하겠습니다.

JWT 생성

TOKEN_TYPE = "JWT"

@Autowired
private JwtProp jwtProp;

@PostMapping("login")
public ResponseEntity<?> login(@RequestBody AuthenticationRequest request) {
	String username = request.getUsername();
	String password = request.getPassword();

	//로그인 처리
	...

	//사용자 권한
	List<String> roles = new ArrayList<>();
	roles.add("ROLE_USER");
	roles.add("ROLE_ADMIN");

	byte[] signingKey = jwtProp.getSecretKey().getBytes();

	String jwt = Jwts.builder()
			.signWith(Keys.hmacShaKeyFor(signingKey), Jwts.SIG.HS256)
			.header()
			.add("typ", TOKEN_TYPE)
			.and()
			.expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 5))
			.claim("uid", username)
			.claim("rol", roles)
			.compact();

	return new ResponseEntity<String>(jwt, HttpStatus.OK);
}

Login 시 JWT 를 생성하는 가장 간단한 예제입니다.

  • JwtProp 는 application.yml 요소를 관리하는 클래스입니다.

JWT 생성 과정

  1. username, password 를 받으면 일련의 로그인 처리를 진행합니다. 여기서는 생략하였습니다.
  2. 로그인 성공 시 username, role 등을 가져옵니다.
  3. 사전에 생성한 secretKey 를 signingKey 로 가져옵니다.

JWT 생성을 위한 재료가 준비되었으니 본격적으로 JWT 를 생성합니다.

  • Jwts 라이브러리를 사용하여 build 합니다.
  • Keys.hmacShaKeyFor() 을 사용해 secretKey 와 사용할 알고리즘을 함께 등록합니다.
  • 헤더에 토큰 타입 등을 추가합니다.
  • 만료 기한을 설정합니다. 여기에서는 5일의 만료 기한을 두었습니다.
  • Payload 에 들어갈 정보들을 Claim 형태로 저장합니다.

위 과정을 통해

image

실제 JWT 를 생성하고 해당 정보들을 확인할 수 있습니다.

JWT 토큰 확인


@GetMapping("/user/info")
public ResponseEntity<?> userInfo(@RequestHeader(name = "Authorization") String header) {
	log.info("=====header=====");
	log.info("Authorization : " + header);

	// Authorization : Bearer ${jwt}
	String jwt = header.replace(SecurityConstants.TOKEN_PREFIX, "").trim();
	byte[] signingKey = jwtProp.getSecretKey().getBytes();

	Jws<Claims> parsedToken = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(signingKey))
			.build()
			.parseSignedClaims(jwt);

	String username = parsedToken.getPayload().get("uid").toString();
	log.info("username :" + username);

	Claims claims = parsedToken.getPayload();
	Object roles = claims.get("rol");
	log.info("roles :" + roles);

	return new ResponseEntity<String>(parsedToken.toString(), HttpStatus.OK);
}

이는 가장 기본적인 JWT 해석 예시입니다. 처음 토큰이 생성되면 클라이언트는 서버에 요청을 보낼 때마다 해당 토큰을 헤더에 넣어서 보냅니다. 서버는 헤더에 존재하는 JWT 를 통해 인증되었음을 확인하고 요청을 처리할 수 있습니다.

따라서 서버에서는 헤더에 존재하는 JWT 값을 파싱하는 과정이 필요합니다.

  • 우선 해당 JWT 의 서명을 검증하기 위한 절차가 필요합니다.
  • 생성시 사용했었던 secretKey 를 사용해 JWT 를 검증합니다.
  • 이후 헤더값에서 토큰 부분만 가져와 parsedToken 으로 파싱합니다.
  • parsedToken.getPayLoad() 를 통해 Claims 를 가져올 수 있습니다.

Claim

이때 Claim 이란

{
  "sub": "1234567890",
  "name": "Tester",
  "iat": 1516239022
}

위와 같이 Payload 가 존재할 때 데이터 각각의 Key 를 Claim 이라 부릅니다.

따라서 Claims 에서 get(“claim_name”) 을 통해 각각의 데이터에 접근할 수 있습니다.

Claim은 3가지로 구분됩니다.

  • registered claim
  • public claim
  • private claim

Reserved Claim

JWT 표준에 정의된 이름의 데이터를 뜻합니다. iss(issuer), exp(expiration time), sub(subject), aud(audience), iat(issued at) 등으로 3글자로 미리 정해진 이름이 존재합니다.

Public Claim

사용자가 자유롭게 정의할 수 있는 데이터입니다. 위에서는 name 등을 자유롭게 정의한 경우를 뜻합니다. 일반적인 정보나 기본 프로필 정보 등을 포함합니다. JWT 의 페이로드를 검증하는 모든 이들은 이러한 공개 클레임을 열어볼 수 있습니다.

Private Claim

특정 애플리케이션 또는 시스템에서만 의미가 있는 정보입니다. 일반적으로 서버와 해당 어플리케이션 간에만 공유됩니다. 예를 들어 role 등의 정보는 어플리케이션에 따라 의미가 달라질 수 있는 데이터입니다.

업데이트:

댓글남기기