Jwt
JWT(Json Web Token)이란 정보 수/발신자 간 정보를 JSON 객체로 안전하게 전송하기 위한 방법을 정의하는 개방형 표준(FC 7519)이다. 이 토큰은 서명되어 있으며 확인과 신뢰가 가능하다. 거의 모든 프로그래밍 언어에서 지원되며 자가 수용적이며 쉽게 전달될 수 있다.
자가 수용적(Self-contained) 특징
JWT는 필요한 정보를 자체적으로 지니고 있다. 발급된 토큰은 토큰에 대한 기본 정보와 전달할 정보(사용자 정보) 그리고 신뢰할 수 있는 토큰이라는 걸 의미하는 시그니처를 포함하고 있다. 웹 서버의 경우 토큰 전달 방법은 HTTP 헤더에 넣을 수도 있고 URL 파라미터(쿼리 스트링)로도 넣을 수 있다. 보통 헤더에 넣는 방법만 사용해왔었는데 최근 진행했던 프로젝트를 통해 URL 파라미터로 넘겨주는 방법을 사용했었다.
JWT 사용 목적
JWT의 사용 목적은 암호화가 아닌 무결성에 있다.
1. 사용자 인증
- 일반적으로 알고 있는 목적 로그인 시 서버는 사용자의 정보를 기반으로 한 토큰을 생성하여 사용자에게 전달합니다. 그 후 매 서버로부터 전달받은 토큰을 같이 동봉하여 요청과 같이 보냅니다. 서버는 받은 토큰이 올바른 토큰인지 검증하여 사용자가 권한 유/무를 판단하여 작업을 처리합니다.
2. 정보 교환
- 토큰은 사용자의 인증 목적이 아닌 정보 교환의 목적으로 사용될 수 있다. 그 이유는 토큰에 서명이 포함되어 있으므로 수/발신자가 바뀌진 않았는지 정보가 조작되지 않았는지 검증할 수 있다.
내부 구조
JWT는 점(.)으로 구분된 헤더와 페이로드 그리고 시그니처 3가지의 형태로 구성되어 있다.
1. Header
헤더는 토큰의 타입과 Base64Url로 인코딩된 서명 알고리즘 2가지의 형태로 구성되어있다.
{
alg: "RS256",
type: "JWT"
}
서명 알고리즘 종류
- HS256은 Key를 조합하여 Hash 함수를 구현하는 방식으로 작동한다. 대칭형 알고리즘이며 하나의 Private Key를 사용한다. 수/발신자만이 공유하고 있는 Key와 Message를 혼합하여 랜덤 Hash 값을 만든다. MAC 특성상 역산이 불가능하므로 수신된 메시지와 Hash 값을 다시 계산하여 계산된 HMAC과 전송된 HMAC이 일치하는지 확인하는 방식이다.
- RS256은 Message를 SHA256 알고리즘으로 해싱 한 뒤 Private Key로 서명하고 Public Key로 서명의 유효성을 검증한다. HS256이 하나의 Private Key를 사용하는 반면 RS256은 Private Key와 Public Key 2개의 키를 사용하는 비대칭형 알고리즘이다.
- RSASSA-PSS는 RS 알고리즘의 업데이트 버전이다. RS와 마찬가지로 비대칭형 알고리즘이다.
- ECDSA는 P-256 P-384 P-521 곡선이 있는 타원 곡선 알고리즘이다. ECDSA 알고리즘 사용 시 지정해야 하는 Key들의 유형을 정의해야 한다.
알고리즘 | 곡선 | 키 요구사항 |
ES256 | P-256 | P-256 곡선에서 생성된 키(secp256r1 또는 prime256v1) |
ES384 | P-384 | P-384 곡선에서 생성된 키(secp384r1) |
ES512 | P-521 | P-521 곡선에서 생성된 키(secp521r1) |
2. Payload
페이로드는 사용자의 entity 및 추가 데이터에 대한 설명이며 Registered(등록)과 Public(공개) 그리고 Private(개인) 3가지의 클레임으로 이루어져 있다.
Registered 클레임은 서비스에 필요한 정보가 아닌 토큰 고유의 정보가 담겨있다. 토큰 고유 정보는 다음과 같다.
{
iss: "발급자",
exp: "만료 시간",
sub: "제목",
aud: "수신자",
nbf: "활성 날짜",
iat: "발급된 시간",
jti: "고유 식별자 중복 처리 방지를 위해 사용",
}
Public 공개 클레임은 공급자가 원하는 대로 지정 가능하다. 공개 부분은 충돌을 방지하기 위해 URI 형식이나 IANA 사이트에 정의된 내용으로 작성해야 한다.
Private 개인 클레임은 Registered와 Public이 아닌 송/수신자(클라이언트 <ㅡ> 서버) 서로 협의하에 사용되는 클레임이다. 공개 클레임과 달리 충돌될 수 있으니 유의해야 한다.
다음은 두 개의 Registered 클레임과 하나의 Public 클레임 두 개의 Private 클레임으로 구성되어 있는 Payload
{
iss: "공급자", // Registered Claim
exp: "만료 시간", // Registered Claim
publicClaim: true, // Public Claim
userId: 1, // Private Claim
username: "사용자", // Private Claim
}
장점 | 단점 |
별도의 저장소가 필요 없으므로 서버 자원 절약 | Claim에 데이터가 많아질 수록 네트워크 통신 비용 증가 |
불필요한 인증 과정 감소로 인한 트래픽 위험 저조 | Payload 암호화가 되지않으므로 보안성 취약 |
서버에 저장되지 않으므로 Stateless 확장성 용이 | 토큰 탈취 시 대처하기 어려움 |
브라우저 뿐만 아니라 모바일에서도 사용 가능 | 쿠키와 세션과 다르게 특정 사용자 제어 불가능 |
3. Signature
Signature는 토큰의 위변조 여부를 확인한다. 인코딩된 Header와 Payload를 더한 뒤 Private Key로 해싱 하여 생성한다. Header와 Payload는 Base64Url로 인코딩된 값이므로 제3자가 복호화 및 조작이 가능하다. Signature 부분은 Private Key가 유출되지 않는 이상 복호화 및 조작이 불가능하다.
OpenSSL을 활용한 RS256 알고리즘 서명
RS256은 2개의 Key를 사용하는데 서명에 Private Key를 검증에 Public Key를 사용한다. HS256과 다르게 Private Kety를 클라이언트와 공유할 필요가 없으므로 HS256 방식보다 보안성에 이점이 있다. 보통 HS256 알고리즘을 많이 사용하는데 최근 진행했던 프로젝트에서 RS256이라는 알고리즘을 알게 되어 직접 사용해 봤다. Prviate와 Public Key를 OpenSSL 환경에서 직접 생성하여 프로젝트 폴더에 넣고 fs 모듈로 Key를 읽어 서명과 검증을 하는 방식이다.
1. 각자의 방법으로 OpenSSL에 접속한다.
다음 명령어를 입력하여 Private Key를 생성한다.
genrsa -des3 -out private.pem 2048
2. pass phrase 비밀번호를 입력한다. 해당 비밀번호는 추후 토큰 생성 메서드에 매개 변수로 들어간다.
unable to write 'random state'
e is 65537 (0x10001)
Enter pass phrase for private.pem:
Verifying - Enter pass phrase for private.pem:
3. OpenSSL > bin 경로에 private.pem 파일이 생성되었다.
4. 위에서 생성한 비밀키를 활용해 공개키를 생성한다.
rsa -in private.pem -outform PEM -pubout -out public.pem
5. 비밀키 생성 시 입력했던 비밀번호를 입력한다.
OpenSSL> rsa -in private.pem -outform PEM -pubout -out public.pem
Enter pass phrase for private.pem:
6. OpenSSL > bin 경로에 public.pem 파일이 생성되었다.
7. private.pem 파일의 pass phrase(암호화) 확인
Proc-Type: 비밀키가 암호화되었음을 나타냄
DEK-Info: DES 방식의 EDE3 KEY를 사용해 CBC 운영모드를 사용한다라는 의미(암호화 방식에 따라 상이)
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,46F659B3E2B81871
8. private와 public pem 파일을 프로젝트 내부로 옮긴다.
9. 비밀키와 공개키 코드 작성
const privateKey = {
key: fs.readFileSync(path.join(__dirname, "./private.pem")),
passphrase: "9999", // 비밀키 비밀번호
};
const publicKey = fs.readFileSync(path.join(__dirname, "./public.pem"));
console.log(privateKey);
console.log(publicKey);
각 키를 콘솔로 찍어보면 Buffer 형식의 데이터를 확인할 수 있다.
{
key: <Buffer 2d 2d 2d 2d 2d 42 45 47 49 4e 20 52 53 41 20 50 52 49 56 41 54 45 20 4b 45 59 2d 2d 2d 2d 2d 0a 50 72 6f 63 2d 54 79 70 65 3a 20 34 2c 45 4e 43 52 59 ... 1693 more bytes>,
passphrase: '9999'
}
<Buffer 2d 2d 2d 2d 2d 42 45 47 49 4e 20 50 55 42 4c 49 43 20 4b 45 59 2d 2d 2d 2d 2d 0a 4d 49 49 42 49 6a 41 4e 42 67 6b 71 68 6b 69 47 39 77 30 42 41 51 45 ... 401 more bytes>
10. 서명(sign) 과정이다. sign 메서드를 사용해 토큰을 생성한다. 페이로드와 비밀키 그리고 옵션 3개의 인수를 넣어준다.
const privateKey = {
key: fs.readFileSync(path.join(__dirname, "./private.pem")),
passphrase: "9999", // 비밀키 비밀번호
};
const payload = {
user: "사용자",
username: "윤승근",
};
const options: SignOptions = {
subject: "비대칭 알고리즘",
algorithm: "RS256",
}
const token = sign(payload, privateKey, options);
console.log(token);
콘솔로 토큰 값 확인 후 JWT 공식 문서의 Debugger에 넣어 돌리면 다음과 같이 발급된 걸 확인할 수 있다.
11. 공개키를 이용한 토큰 검증(decoded) 과정이다. verify 메서드를 사용해 검증한다. 토큰과 공개키 2개의 인수를 넣는다.
const publicKey = fs.readFileSync(path.join(__dirname, "./public.pem"));
const token = sign(payload, privateKey, options);
const decoded = verify(token, publicKey);
console.log(decoded);
콘솔로 검증된 토큰의 값을 확인할 수 있다.
전체 코드는 다음과 같다.
import path from "path";
import fs from "fs";
import { sign, SignOptions, verify } from "jsonwebtoken";
const __dirname = path.resolve();
const privateKey = {
key: fs.readFileSync(path.join(__dirname, "./private.pem")),
passphrase: "9999", // 비밀키 비밀번호
};
const publicKey = fs.readFileSync(path.join(__dirname, "./public.pem"));
const payload = {
user: "사용자",
username: "윤승근",
};
const options: SignOptions = {
subject: "비대칭 알고리즘",
algorithm: "RS256",
}
const token = sign(payload, privateKey, options);
const decoded = verify(token, publicKey);
참고 자료
https://www.iana.org/assignments/jwt/jwt.xhtml#IETF
https://cloud.google.com/apigee/docs/api-platform/reference/policies/jwt-policies-overview?hl=ko
'네트워크' 카테고리의 다른 글
OSI 7 Layer (0) | 2022.10.09 |
---|---|
URI URL (0) | 2022.10.05 |
Dto Repository Architecture (0) | 2022.08.31 |
Nginx (0) | 2022.08.11 |
쿠키와 세션 (0) | 2022.08.04 |