JWT
Jason Web Token의 약자로 전자 서명 된 URL-safe의 JSON이다.
거대하고 복잡한 단일 어플리케이션 개발에서 서비스를 작은 단위로 분할하여 민첩한 개발을 할 수 있는
Microservice Architecture 로 전향할 때 각각의 API server 들은
client에 대한 인증(Authentication)과 인가(Authorization)를 위한 매커니즘이 필요한데
JWT는 MSA의 인증,인가에 사용할 수 있는 서명된 JSON 이기 때문에 안성맞춤이다.
서버측 부하를 낮출 수 있고 능률적인 접근 권한 관리를 할 수 있으며 분산환경에 더 잘 대응할 수 있다.
MSA로 여러 API 웹 서비스를 만들려면토큰을 사용한 인증 방식에 대해 이해해야 한다.
서버 기반 인증
기존의 인증방식은 서버측에 유저 정보를 저장한다.
대표적으로 세션이 이에 해당하고 필요에 의해 파일, DB 등에도 저장할 수 있다.
일반적인 웹어플리케이션 개발 시 이 방법을 사용한다.
서버 기반 인증의 문제점
사용자 수가 늘어날 수록 세션에 담아야 할 정보가 함께 증가하기 때문에 메모리 과부화 문제가 발생한다.
우리가 만드는 어플리케이션은 사용자가 많지 않기 때문에 문제 될 것이 없지만
아주 많은 사용자가 이용하는 대규모 포털 서비스를 개발한다고 생각해 보면 문제의 소지가 있다.
많은 트래픽을 감당하기 위해 프로세스를 늘리거나 서버 장비를 추가하는 경우 확장이 쉽지 않다.
쿠키는 단일 도메인 및 서브 도메인에서만 작동하기 때문에 여러 도메인에서 관리하는 것은 번거롭다.
(Cross-origin resource sharing)
토큰 기반 인증 원리
- 사용자가 로그인한다.
- 서버측에서 로그인을 인증하고 맞을 경우 클라이언트 측에 signed 토큰을 발급해 준다.
signed 란? 해당 토큰이 서버에서 정상적으로 발급된 토큰임을 증명하는 signature를 담고 있다는 것을 의미한다. - 클라이언트측에서 서버로 부터 전달받은 토큰을 저장하고 서버에 요청할 때마다 해당 토큰을 함께 서버에 전달 한다.
- 서버는 요청이 올때 마다 토큰을 검증한다.
토큰 기반 인증의 장점
- 확장성이 뛰어나다. 서버가 늘어나도 토큰을 인증하는 방식만 알고 있다면 영향이 없다.
- 클라이언트가 서버로 요청할 때 더이상 쿠키를 전달하지 않기 때문에 쿠키를 사용함으로써 발생하는 취약점이 사라진다.
물론 토큰을 사용하는 환경에서도 취약점이 존재할 수 있으니 대비해야 한다. - 다른 서비스에서도 권한을 공유할 수 있다. (ex. OAuth 를 통해 Facebook 에서 인증하고 Facebook의 일부 권한만 사용가능)
- CORS 문제가 해결되나. 어떤 도메인에서도 토큰만 유효하다면 처리가 가능하다.
- JWT는 웹표준 RFC7591에 등록되어 있다. 따라서 여러 환경에서 지원이 가능하다. (.NET, Ruby, Java, Node.js, Python 등)
단점
- 더 많은 필드가 추가되면 토큰이 커질 수 있다.
- 모든 요청에 대해 토큰이 전송되므로 데이터 트리팩 크기에 영향이 있다.
JWT 구조
JWT는 header.payload.signature 로 이루어져 있다. . 을 구분자로 사용한다.
header
토큰의 타입(type)과 해싱 알고리즘(alg)을 지정합니다. 보통 HMAC SHA256 혹은 RSA가 사용되며 토큰을 검증할 때 사용한다.
const header = { "type": "JWT", "alg": "HS256" };
payload
토큰에 담을 정보
- 토큰은 클레임으로 이루어져 있으며 name / value 쌍으로 이루어져 있다.
- registered claim: 서비스에 필요한 정보가 아니라 토큰에 대한 정보들을 담기위해 이름이 이미 정해진 claim
iss(발급자), sub(제목), aud(대상자), exp(만료시간), nbf(토큰의 활성날짜), iat(발급된시간), jti(JWT 고유식별자, 일회용 토큰에 사용) - public claim: 충돌을 방지된 이름을 가지고 있어야 하며 이를 위해 URI 형식으로 이름을 짓는다.
- private claim: server/client 간 협의하에 사용되는 claim
const payload = {
"iss": "pnpsecure.com",
"exp": "1685270000000",
"https://pnpsecure.com/jwt/is_admin": true,
"userId": "yhkim", "username": "younghoi"
};
const encodPayload = new Buffer(JSON.stringify(payload)).toString('base64').replace('=', '');
signature
header의 인코딩값, 정보의 encoding 값을 합친 후 주어진 비밀키로 해쉬를 하여 생성함
HMACHASH256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)
사용
실제 개발 시에는 다양한 라이브러리가 있기 때문에 쉽게 token을 만들고 검증할 수 있다.
JJWT
한가지 예로 JJWT 라이브러리를 활용한 방법을 살펴 보자.
private static final String headerStr = "X-JWT";
private String secretKey = Properties.getProperty("pnp.jwt.secretKey").trim();
private byte[] keyBytes = Converter.parseBase64Binary(secretKey);
private SignatureAlgorithm al = SignatureAlgorithm.HS256;
private final Key KEY = new SecretKeySpec(keyBytes, al.getJcaName());
// 토큰 생성 (로그인 시 생성)
public String getUserToken(HttpServletRequest req, HttpServletResponse res, UserDto user) {
String jwt = Jwts.builder()
.headerParam("type", "JWT")
.subject(user.getEmail())
.claim("uid", user.getId())
.expriation(new Date(System.currentTimeMillis() + 1 * (1000 * 60 * 60 * 24)))
.signWith(KEY, al)
.compact();
return jwt;
}
// 토큰 검사 (Interceptor 에서 사용)
public boolean validToken(String jwt) {
if (CommonUtil.isNotNull(jwt)) {
String userKey = Jwts.parser().setSigningKey(KEY).parseClaimsJws(jwt).getBody().get("uid").toString();
String key = RedisClientTemplate.get(RedisSourceType.USER_KEY.getKey(userKey), redisTemplate);
// 만료 검사
Date now = new Date();
if (Jwst.parser().setSigningKey(KEY).parseClaimsJws(jwt).getBody().getExpiration().before(now)) {
return false;
}
if (key.equals(jwt)) {
return true;
}
return false;
}
return false;
}
'IT' 카테고리의 다른 글
Flyway (Spring Boot + JPA) (0) | 2019.09.10 |
---|---|
Inverted Index (역색인) 원리 (0) | 2019.08.26 |
ASCII / ANSI / UNICODE / UTF-8 쉽게 이해하기 (0) | 2018.09.10 |
Java 유료화?? (1) | 2018.09.02 |
티스토리 소스코드 보기 좋게 넣기 - 업로드 없이 간편하게 사용 (Java, JavaScript, Python 등) (0) | 2018.08.27 |