드디어..JWT 구현 성공.. 하
JWT 사용하는이유
1. Session마다 다른 Client 정보를 가지고 있을 수 있기 때문에 세션 저장소가 필요함(JWT는 secret key)
2. 로그인 정보를 Server에 저장하지 않고 Client에 로그인 정보를 JWT로 암호화하여 저장을 통해 인증/인가
3. 서버에 저장하지 않기 때문에 stateless 토큰값을 준다
Session과 차이가 있다면?
세션방식의 JESSIONID는 Key로만 활용 (의미없는 값)
토큰은 유저를 설명할 수 있는 데이터를 포함
JWT 단점
1. 한 번 제공된 토큰은 회수가 어려움
세션은 서버에서 세션을 삭제하면 브라우저의 JESSIONID는 무용지물, 그러나 토큰은 한번 제공된 토큰을 회수할 수 없다.
(단, Refresh Token은 제외 - 밑에 설명)
2. 안정성의 우려(토큰의 유저정보포함) 즉, 민감한 정보를 토큰에 포함시키면 안된다(ex 패스워드, 개인정보 등)
JWT 구조
HEADER.PAYLOAD.SIGNATURE
HEADER
JWT를 검증하는데 필요한 정보를 가진 객체
SIGNATURE에 사용한 알고리즘, KEY의 ID 정보를 가지고 있음
JSON -> UTF-8 -> (Encode) -> Base64
이 순서 꼭 중요함, 이런순서를 기억해야 코딩을 자연스럽게 할 수 있음
PAYLOAD
실질적으로 인증에 필요한 데이터를 저장
각 필드를 Claim이라고 하고 그안에 username 포함함(프로그래밍 할 때 Claim 객체 생성해야하니까 그냥외워)
인증해야하니까 payload안에 있는 username 가져와서 유저 정보 조회해야함
토큰 발행시간(iat)와 토큰 만료시간(exp) 포함 - payload가 할게 진짜 많음. 순서만 꼭 기억하자(흐름!!)
Claim 추가 가능한데 민감정보 꼭 포함시키지말자
Payload도 Header처럼 암호화되지는 않음
JSON -> UTF-8 -> (Encode) -> Base64 이 순서 기억, 두 번 말했다
SIGNATURE
앞선 HEADER, PAYLOAD는 암호화하지 않았으니까 누가 하겠나, 토큰 자체의 진위여부를 여기서 판단한다.
HEADER, PAYLOAD 합쳐서 비밀키로 Hash를 생성해서 암호화한다. Hasmap을 쓰겠지?
SecretKey로 Hasing하고 Base64로 변환
SecretKey 노출 절대 안돼!!
Key Rolling
JWT 토큰 생성 매커니즘을 보면 Secret Key 노출되면 사실상 모든 데이터 유출됨
이런 문제 방지하려고 여러개 키를 두고 대비함, 즉 1개가 노출되어도 다른 데이터는 안전한 상태가 됨
Key Rolling은 필수가 아니지만 1개에 Unique한 ID(KID)를 연결시켜둔다. JWT토큰을 만들 때 헤더에 kid를 포함하여 제공하고 서버에서 토큰을 해석할때는 kid로 Secret Key를 찾아서 SIGNATURE를 검증함
이제 이걸 이해하기 위해 3일간 구글링을 하고 개고생했던 코드들을 볼까?
Header 작업
1. 의존성 주입(DI build)
2. Key 만들어서 받기
HashMap함수는..이렇게 쓰는건지 사실 잘 모르겠다. 원래 Map<K,V>해서 Map.of로 KEY값들을 설정해서 넣으려고 했는데 자바8은 지원 안되는듯..그냥 같이 공부하는 착한 고수동생님이 알려주심
3. Random 키를 Pair로 받아서 Key객체 만들어서 전달해준다
그전에 KEY만들어놓은거 아래에 toArray로 KID_SET 배열하나 만들어주고 Random 클래스 이용해서 객체 만들어주어야 겠지? 알고리즘 자료구죠 진짜..빡세게 공부해야겠다
PAYLOAD작업
4. 이제 키 객체 만들어서 받으려면 User에 대한 토큰을 만드는 작업을 해야한다.
위에 설명을 봤듯이 HEADER작업 때 알고리즘과 KID를 가지고 있으니까 PAYLOAD에서는 Claim 객체로 필드를 만들어서 유저의 JWT를 builder를 사용해서 리턴하게된다.
builder로 리턴하는 set값에는 토큰발행시간(iat)과 만료시간(exp)가 포함되어 있다
claim넣어주고 각 시간값들 넣어주고 Header매개변수로 반환할 값은 Pari<String, Key> 각 key id, Key의 1번째 객체를 반환해서 돌려주고 이어지게 second로 전달한다.
순서를 그냥 외워버리자 우선.....아 참! EXPIRATION_TIME 상수의 경우 별도의 JwtProperties 클래스를 생성해서 설정해두었다.
5. JWT 만들어주었으면 토큰에서 username을 찾을 수 있는 메서드도 있어야하지 않겠나
token으로 그안에 내용을 parsing해서 username을 찾는 방식이다. 그렇기 때문에 parserBuilder() 를 사용해서 claim을 파싱한다. 그래서 토큰을 그안에 넣어주는거고 그거에 바디를 찾아서 subject 를 반환하는데 subject는 createToken 메서드에서 username이라고 생각해주자! method 자체도 setSubject 아닌가!!
그리고 SigningKeyResolver는 JwtHeader를 통해서 SIGNATURE 검증에 필요한 KEY를 가져오도록 코드를 구현한 것(아래 이미지 그대로 설명함)
이게 아까 HEADER작업 때 보던 getKey 메서드로 반환한 값인 것, 비교해서 잘 파악해보자
그래서 아까 위에 getUsername 메서드에서 반환한 값을 살펴보면 parseBulider가 그 값을 찾아서 signature 검증할 때 사용할 수 있도록 구현한 것인데 만약에 signature 검증에 실패하면
아래처럼 SignatureException이 발생하고 만약에 만료가 되면 ExpiredJwtException이 발생한다. 자주 발생하는 오류라고 하니까 어떤 이유인지 잘 파악해두고 바로 문제를 해결해나가자
이제는 인증과 인가작업을 거쳐야한다
JWT Filter 작업
인증
1. 이제 JWT Filter 작업을 해야한다, 로그인에 성공하면 UsernamePasswordAuthenticationFilter를 상속한 객체가 User정보로 JWT Token을 생성하고 응답 쿠키에 값을 넣어준다
* JwtAuthenticationFilter 클래스만들어서 상속받기
인증에 성공을 하면 JWT 생성하고 응답에 넣어준다, 그러면 브라우저에서 사용이 가능하겠지?
조금 더 자세한 설명은 Cookie에 토큰의 이름과 createToken으로 생성한 token을 넣어주고 경로를 설정해서 응답에 토큰을 넣어서 던져준 후 redirect하게 하는것
만약에 성공하지 못한 경우에는 로그인페이지로 돌아가게 해주어야한다
인가
2. JwtAuthorizationFilter 클래스 만들기
stream으로 cookie에 있는 토큰을 꺼내서 JwtProperties에 이름과 같은지 비교한다. 첫번째 값을 가져오고 getValuefh 반환하고 못찾았다면 null을 반환한다. 만약 token의 값이 null 값이 아니라면(아래의 이미지와 설명을 본 후 돌아오도록)
authentication을 만들어서 SecurityContext에 넣어준다.
그리고 실패하는 경우 catch문처럼 cookie를 초기화해준다
JWT 토큰으로 User를 찾아서 UsernamePasswordAuthenticationToken을 만들어서 반환한다. 없다면 null 을 반환
Spring Security를 적용하면 각 필터를 각 순서에 맞게 실행한다. 목적과 요청에 따라 처리하는 경우도 있고 스킵되는 경우도 있으니 각 역할을 잘 공부해서 아래의 글을 읽어보면 좋다
마지막 Filter 작업
- (UsernamePassword)AuthenticationFilter : (아이디와 비밀번호를 사용하는 form 기반 인증) 설정된 로그인 URL로 오는 요청을 감시하며, 유저 인증 처리
- AuthenticationManager를 통한 인증 실행
- 인증 성공 시, 얻은 Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
- 인증 실패 시, AuthenticationFailureHandler 실행
- BasicAuthenticationFilter : HTTP 기본 인증 헤더(Basic Authorization)를 감시하여 처리한다.
이렇게되면 유저가 로그인하면 인증 JwtAuthenticationFilter가 실행되고 successfulAuthentication 성공하면 Cookie_name을 token으로 넣어서 내려주고 브라우저가 JWT를 받아서 요청을 하면 JwtAuthorizationFilter가 받아서 SecurityContext에 넣을지 초기화할지 파악한다.
정말 구현하는 코드들이 모두 달랐지만 저런 흐름과 방식을 알아두었다는게.. 참 이미 새벽3시가 넘어간다.. 몇 일 동안 잠을 너무 못잤다..
아무튼 어떻게든 알아내서 꿀잠잘 듯..
References :
진짜..너무 많아서 언제 끄고 어디다가 저장해뒀는지..아니면 사라졌는지 모르겠다..
'프로그래밍 > Spring' 카테고리의 다른 글
AOP & Logging (slf4j) (0) | 2022.08.05 |
---|---|
[SPRING BOOT]JWT, Thymeleaf, form (2/2) (0) | 2022.08.04 |
[DI, IoC, Bean] 개념 박살내기 (0) | 2022.08.01 |
Spring MVC Architecture (0) | 2022.07.27 |
게시판 만들기 (Usecase Diagram & API) (0) | 2022.07.26 |