Updated:

1. 서버 기반 인증 방식의 문제점

사용자의 인증 여부를 판단하는 대표적인 방법으로는 서버, 토큰 기반 인증이 있다.
먼저 서버 인증 방식으로는 세션, 쿠키가 있는데
서버에 사용자 정보를 저장해야하므로, 사용자 수가 많아질수록 서버에 부담이 늘어나며
세션을 사용하면 분산된 시스템 설계가 매우 복잡하므로, 서버를 확장하는게 어려워진다.
게다가 웹 어플리케이션에서 세션을 관리할 때 사용되는 쿠키는 단일 도메인 및 서브 도메인에서만 작동하도록 설계되어있어서
CORS(Cross-Origin Resource Sharing) 문제가 발생하기도 한다.

2. 토큰 기반 인증 방식

이러한 문제를 해결하기위한 방법으로 토큰 기반 인증 방식인 JWT(Json Web Token)가 있다.
사용자의 인증 정보가 담겨있는 토큰을 클라이언트에 저장하기 때문에 서버에서는 별도의 저장소가 필요 없으므로 완전한 무상태(Stateless)를 가질 수 있다.
이로 인해서 서버의 확장성(Scalability)이 용이해진다. 어떤 디바이스나 도메인에서도 토큰만 유효하면 요청을 정상 처리할 수 있다는 것이다.
이는 HMAC(Hash-based Message Authentication) 기법이라고도 불리는데, 발급 후에는 토큰의 정보를 변경하는 행위가 불가능하다. 즉, 토큰이 변조되면 바로 알아차릴 수 있다.

토큰 기반 인증 시스템의 흐름은 다음과 같다.

  • 유저가 아이디와 비밀번호로 로그인을 한다
  • 서버측에서 해당 계정정보를 검증한다
  • 계정정보가 정확하다면, 서버측에서 유저에게 signed 토큰을 발급한다
  • 클라이언트 측에서 전달받은 토큰을 저장한다
  • 서버에 요청을 할 때마다, 해당 토큰을 함께 서버에 전달한다
  • 서버는 토큰을 검증하고, 요청에 응답한다
    token

3. JWT 구성

이번엔 JWT 구성에 대해 알아보자
JWT는 . 을 기준으로 헤더(header), 정보(payload), 서명(signature) 으로 이루어져 있다.
jwt

헤더(header)는 토큰의 타입과 해싱 알고리즘을 지정하는 정보를 포함한다.

  • typ: 토큰의 타입을 지정한다
  • alg: 해싱 알고리즘을 지정한다

정보(payload)는 토큰에 담을 정보가 들어간다. 정보의 한 덩어리를 클레임(claim)이라고 부르며, 클레임은 key-value의 한 쌍으로 이루어져있다.

  • jti: JWT의 고유 식별자로서, 주로 일회용 토큰에 사용한다.
  • aud: 토큰 대상자(audience)
  • iat: 토큰이 발급된 시간 (issued at)
  • iss: 토큰 발급자(issuer)
  • sub: 토큰 제목(subject)
  • exp: 토큰의 만료시간(expiraton), 시간은 NumericDate 형식으로 되어있어야 하며, (예: 1480849147370) 항상 현재 시간보다 이후로 설정되어있어야 한다.
  • nbf: Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념. NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.

서명(signature)은 해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용된다.
헤더(header)의 인코딩 값과 정보(payload)의 인코딩 값을 합친 후에 주어진 비밀키를 통해 해쉬값을 생성한다.

4. JWT 토큰을 생성하는 컨트롤러 작성

원리와 구성에 대해 이해했으니, JWT 토큰을 생성하여 리턴하는 컨트롤러를 만들어보자
먼저 gradle에 io.jsonwebtoken:jjwt 를 추가한다.

implementation("io.jsonwebtoken:jjwt:0.9.1")

업체코드와 비밀키를 받아서 토큰을 생성해주는 컨트롤러를 만들어준다.
Map 형태의 header와 payload를 Jwts builder로 JWT 토큰을 생성한다.

import com.fourtwod.config.auth.TokenDto;
import com.fourtwod.web.handler.ApiResult;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
@Api(tags = {"토큰 API"})
public class TokenController {

	@ApiOperation(value = "토큰 생성", notes = "토큰을 생성합니다.", response = ApiResult.class)
	@ApiImplicitParams({
			@ApiImplicitParam(name = "code", value = "업체 코드", required = true, dataType = "string", paramType = "query", defaultValue = "")
			, @ApiImplicitParam(name = "secretKey", value = "비밀키", required = true, dataType = "string", paramType = "query", defaultValue = "")
	})
	@GetMapping("/token")
	public ApiResult token(@RequestParam("code") String code, @RequestParam("secretKey") String secretKey) {
		return new ApiResult<>(
			TokenDto.builder()
				.user(code)
				.token(getJWTToken(code, secretKey))
				.build()
		);
	}

	private String getJWTToken(String id, String secretKey) {
		// 헤더(header)
		Map<String, Object> headers = new HashMap<>();
		headers.put("alg", "HS256");
		headers.put("typ", "JWT");

		// 정보(payload)
		Map<String, Object> payloads = new HashMap<>();
		payloads.put("jti", id);
		payloads.put("aud", "localhost");
		payloads.put("iat", System.currentTimeMillis());

		// JWT 토큰 생성
		return "Bearer " + Jwts.builder()
				.setHeader(headers)
				.setClaims(payloads)
				.setIssuedAt(new Date())
				.setSubject(secretKey)
				.signWith(SignatureAlgorithm.HS256, secretKey.getBytes())
				.compact();
	}
}

code와 secretKey를 포함하여 api를 호출하면 아래와 같이 JWT 토큰을 응답받을 수 있다.
클라이언트가 JWT 토큰을 전달 받았으니, 인증이 필요한 API를 호출 시 header에 토큰을 담아서 요청할 수 있다.

Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJsb2NhbGhvc3QiLCJzdWIiOiI3ZThiNmUwZWMxOGNhMTdhOWZjNTRhMjA4NDg2NTZkMCIsImlhdCI6MTY2OTYxMjQ4MiwianRpIjoiZnJvbnQifQ.XHLMTiwn4iPvnzMCQVNRW60VYLWUh3vxBSsvycsEj7c

여기까지 따라왔다면 이상함?을 감지해야된다.
토큰 기반 인증 시스템의 흐름을 보면 아무한테나 토큰을 발급해 주지 않는다.
서버와 약속된 사용자 정보(code) 와 secretKey 에 대해서만 발급해주도록 검증하는 코드도 필요하다.

인증된 사용자 검증 여부는 서버에서도 진행되어야 한다.
다음 포스트에서는 서버에서 클라이언트의 요청을 받을 시
JWT 토큰을 디코딩하여 인증된 사용자 여부를 확인하는 Filter를 만들어보자

Leave a comment