기본 콘텐츠로 건너뛰기

jwt 파싱2 (signature)

 

http://changpd.blogspot.com/2021/04/jwt-1headerpayload.html

에이어서 두번째  signature 파싱처리

signature부분은 토큰의 변조 방지를 담당한다. 

토큰내용이야 base64로 인코딩되어있어 얼마든지 디코딩이 가능하나

변조는 불가능해야 한다. 


jwt파싱 (step1-1, SignatureAlgorithm)

첫번째 파싱한 header를 다시 JwsHeader로 타입캐스팅한다. 
그리고 header에 저장된 alg값을 읽어온다. 

이값이 없으면 더이상 진행이 되지 않는다. 

// =============== Signature =================
if (base64UrlEncodedDigest != null) { //it is signed - validate the signature

JwsHeader jwsHeader = (JwsHeader) header;

SignatureAlgorithm algorithm = null;

if (header != null) {
String alg = jwsHeader.getAlgorithm();
if (Strings.hasText(alg)) {
algorithm = SignatureAlgorithm.forName(alg);
}
}

if (algorithm == null || algorithm == SignatureAlgorithm.NONE) {
//it is plaintext, but it has a signature. This is invalid:
String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " +
"algorithm.";
throw new MalformedJwtException(msg);
}


jwt파싱(step1-2, 키설정)

대략적인 알고리즘까진 정해졌다. 
담에 하는짓은 키값을 저장한다. 
키값이 설정이 안되어있으면, claims, playload에서 가져온다. 

//digitally signed, let's assert the signature:
Key key = this.key;

if (key == null) { //fall back to keyBytes

byte[] keyBytes = this.keyBytes;

if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
if (claims != null) {
key = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
} else {
key = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
}
}

if (!Objects.isEmpty(keyBytes)) {

Assert.isTrue(algorithm.isHmac(),
"Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.");

key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
}
}


jwt파싱 (Step1-3, 재검증)

주석의 내용을 보면 signature 영역을 제외한 부분을 재검증을 위해 다시 생성한다. 
여기서 체크하는 부분은 크게 2가지다. 

첫번째는 alg,key의 유효성검사를 진행하고

//re-create the jwt part without the signature.  This is what needs to be signed for verification:
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;

JwtSignatureValidator validator;
try {
validator = createSignatureValidator(algorithm, key);
} catch (IllegalArgumentException e) {
String algName = algorithm.getValue();
String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " +
"algorithm, but the specified signing key of type " + key.getClass().getName() +
" may not be used to validate " + algName + " signatures. Because the specified " +
"signing key reflects a specific and expected algorithm, and the JWT does not reflect " +
"this algorithm, it is likely that the JWT was not expected and therefore should not be " +
"trusted. Another possibility is that the parser was configured with the incorrect " +
"signing key, but this cannot be assumed for security reasons.";
throw new UnsupportedJwtException(msg, e);
}

if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) {
String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " +
"asserted and should not be trusted.";
throw new SignatureException(msg);
}

두번째는 디코딩한결과의 header+payload와 Digest 영역을 재검증한다. 
소스를 

createSignatureValidator 함수를 쭈욱 따라가보자
signature


jwt파싱2 (만료시간, NotBeFore)

대충 여기까지 통과를 했으면, claims에 담긴 만료시간이 유효한지 체크한다. 
그리고 notBefore (이시간이전에는 토큰처리안함, 엠바고 같은)

이쪽은 소스코드까지 안봐도 될듯하여 생략


jwt파싱3 (claims 유효성검사)

claims에 저장된값들의 유효성을 마지막으로 체크한다. 
그냥 소스만 한번 쓰윽 보면 좋을듯 

private void validateExpectedClaims(Header header, Claims claims) {
for (String expectedClaimName : expectedClaims.keySet()) {

Object expectedClaimValue = expectedClaims.get(expectedClaimName);
Object actualClaimValue = claims.get(expectedClaimName);

if (
Claims.ISSUED_AT.equals(expectedClaimName) ||
Claims.EXPIRATION.equals(expectedClaimName) ||
Claims.NOT_BEFORE.equals(expectedClaimName)
) {
expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class);
actualClaimValue = claims.get(expectedClaimName, Date.class);
} else if (
expectedClaimValue instanceof Date &&
actualClaimValue != null &&
actualClaimValue instanceof Long
) {
actualClaimValue = new Date((Long)actualClaimValue);
}

InvalidClaimException invalidClaimException = null;

if (actualClaimValue == null) {
String msg = String.format(
ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE,
expectedClaimName, expectedClaimValue
);
invalidClaimException = new MissingClaimException(header, claims, msg);
} else if (!expectedClaimValue.equals(actualClaimValue)) {
String msg = String.format(
ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE,
expectedClaimName, expectedClaimValue, actualClaimValue
);
invalidClaimException = new IncorrectClaimException(header, claims, msg);
}

if (invalidClaimException != null) {
invalidClaimException.setClaimName(expectedClaimName);
invalidClaimException.setClaimValue(expectedClaimValue);
throw invalidClaimException;
}
}
}


여기까지 무사통과되면 return 값은 다음과 같다. 

if (base64UrlEncodedDigest != null) {
return new DefaultJws<Object>((JwsHeader) header, body, base64UrlEncodedDigest);
} else {
return new DefaultJwt<Object>(header, body);
}

여기서 body는 claims 혹은 payload이다. 


정리

- 파싱에서 에러가 발생하는 경우가 다양하다. 케이스에 대해 어느정도 숙지 해두는게 좋을듯
- 로그인 이후 일부 고객 정보는 payload영역에 저장할 경우 DB연결횟수를 줄일수 있다. 
- 다만 header,payload는 base64로 디코딩하기 때문에 민감 정보 노출에 주의할 필요가 있다. 
- signature영역이 변경될 경우 기존에 발급한 토큰은 모두 유효하지 않게 된다. 


댓글

이 블로그의 인기 게시물

메일서버가 스팸으로 취급받을때

설치한 메일서버를 통해 발송되는 메일이 스팸으로 들어가는 경우가 더러 있다. 이게 한번 들어가기는 쉬운데, 빠져나오기는 드럽게 힘든것 같다... 본인의 경우에는 우선 국내서비스에는 별 무리 없이 들어간다. (naver,daum 등) 그런데 해외메일 그중 Gmail, Hotmail 에는 에누리없이 스팸으로 간주되고 있었다. Gmail같은 경우에는 그래도 스팸함으로 발송은 제대로 되는반면에 Hotmail같은경우에는 아예 수신자체가 안되는 경우도 더러있다.. ㅡ,.ㅡ; 제일 좋은 방법은 Gmail,Hotmail에 전화걸어서 우리 메일서버 IP white Ip로 등록해달라!!! 하면 좋지만, 얘네들은 걸어봤자 자동응답기고, 문의채널은 구글 그룹스 게시판이 전부다.. 본론으로 들어가서. 해외 메일이 차단될 경우 내 매일서버ip가 스팸ip로 등록되 버린 경우일 수 있다. (본인처럼. ㅎ) 이것부터 조회 해보고 싶으면 RBL(real-time blocking List) 체크를 해야 하는데, RBL체크 해주는 사이트는 꽤 많이 있고, 그중 좀 깔끔해 보이는곳 하나 소개. http://www.anti-abuse.org/ 메일서버ip 입력하고 조회해보면 쭈루룩 리스트가 나온다. 그 중 빨간불이 들어온 부분이 메일 서버가 스팸서버가 된 각종 이유들이다.ㅋ 본인의 경우 CBL 때문에 걸렸는데, 내용은 아래와 같다. This IP address is HELO'ing as  "localhost.localdomain"  which violates the relevant standards (specifically: RFC5321). 메일서버 도메인에 별다른 작업을 안해놓아서 "localhost.localdomain" 으로 설정되어있었다. 만약 CBL만 바로 테스트 해보고 싶으면 http://cbl.abusea

[javascript] 특정시간에만 함수 실행

특정시간에만 팝업을 띄우려면?? 특정시간에만 로그인을 막으려면?? 특정시간에만 할일은 의외로 참 많다. 방법? 딱히 없다. 현재시간 구해서 시작시간, 종료시간 사이에 있을때 시작하는 수밖엔. if ((현재시간 > 시작시간) && (현재시간 < 종료시간)){ .. 팝업노출(); 공사페이지 리다이렉트(); 기타등등(); .. } 자바스크립트로 작성하면 다음과 같다. var startdate = "2014012008" ; var enddate = "2014012418" ; var now = new Date (); //현재시간 year = now. getFullYear (); //현재시간 중 4자리 연도 month = now. getMonth () + 1 ; //현재시간 중 달. 달은 0부터 시작하기 때문에 +1 if ((month + "" ). length < 2 ){ month = "0" + month; //달의 숫자가 1자리면 앞에 0을 붙임. } date = now. getDate (); //현재 시간 중 날짜. if ((date + "" ). length < 2 ){ date = "0" + date; } hour = now. getHours (); //현재 시간 중 시간. if ((hour + "" ). length < 2 ){ hour = "0" + hour; } today = year + "" + month + "" + date + "" + hour; //오늘 날짜 완성. / / 시간비교 i

스레드 동기화1 - syncronized

구현 스레드를 구현하는 방법은 2가지다. 1. Thread 클래스를 extends 한다. 2. Runnable 인터페이스를 implements 한다. 뭐 사실 Thread는 생성자의 변수로 Runnable을 취한다. public Thread (Runnable target) { init( null, target , "Thread-" + nextThreadNum () , 0 ) ; } 그리고 Runnable 인터페이스는 run() 이라는 단일함수를 갖는 인터페이스이다. @FunctionalInterface public interface Runnable { /** * When an object implementing interface <code> Runnable </code> is used * to create a thread, starting the thread causes the object's * <code> run </code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code> run </code> is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run () ; } 그렇기 때문에 람다식으로 표현이 가능한 것이다. 동기화 동기화는 동시에 같은자원의 접근을 제한하고자 할때 사용한다. 예를들어, 한 우물에서 물을 15번 길어야 하는 일을 해야할때 5명이서 3번만 하면 수고를 5배로 줄일수 있다. 그런데 우물은 하난데 동시에