- 토큰 기반 인증 절차
- 클라이언트가 서버에 아이디/비밀번호를 담아 로그인 요청을 보냄.
- 아이디/비밀번호가 일치하는지 확인 후, 클라에게 보낼 암호화 된 토큰 생성
- Access Token과 Refresh Token을 모두 생성
- 두 종류의 토큰이 같은 정보를 담을 필요는 없다.
- 토큰을 클라이언트에게 전송하면, 클라는 토큰을 저장
- 저장 위치는 Local Storage, Session Storage, Cookie 등
- 클라이언트가 HTTP Header(Authorization Header) 또는 쿠키에 토큰을 담아 요청 전송
- Bearer authentication 이용
- 서버는 토큰을 검증하여 인증된 사용자일 경우, 요청 처리 후 응답을 보낸다.
토큰의 길이가 길어지면 네트워트에 부하를 줄 수 있다.
토큰은 자동으로 삭제되지 않는다.
✔ 토큰 기반 자격 증명의 특징
- 토큰에 포함된 인증된 사용자 정보는 서버 측에서 별도의 관리를 하지 않는다.
- 생성된 토큰을 헤더에 포함시켜 요청 전송 시, 인증된 사용자인지를 증명하는 수단으로 사용
- 토큰 내에 인증된 사용자 정보 등을 포함하므로 세션에 비해 상대적으로 많은 네트워크 트래픽 사용
- 기본적으로 서버에서 토큰을 관리하지 않아서 보안성 측면에서 좀 더 불리하다.
- 인증된 사용자 request의 상태를 유지할 필요가 없어 서버의 확장성 면에서 유리하고, 세션 불일치 문제 해결
- 토큰에 포함되는 사용자 정보는 암호화 되지 않기 때문에 민감한 정보는 토큰에 포함하면 안된다.
- 기본적으로 토큰이 만료되기 전까지는 토큰을 무효화 시킬 수 없다.
- CSR 방식의 애플리케이션에 적합한 방식이다.
✔ 세션 기반 자격 증명의 특징
- 세션은 인증된 사용자 정보를 서버 측 세션 저장소에서 관리
- 생성된 사용자 세션의 고유 ID인 세션 ID는 클라이언트의 쿠키에 저장되어 request 전송 시, 인증된 사용자인지를 증명하는 수단으로 사용됨.
- 세션 ID만 클라이언트 쪽에서 사용하므로 상대적으로 적은 네트워크 트래픽을 사용
- 서버 측에서 세션 정보를 관리하므로 보안성 측면에서 조금 더 유리하다.
- 서버의 확장성 면에서는 세션 불일치 문제가 발생할 가능성이 높다.
- 세션 데이터가 많아질수록 서버의 부담이 가중될 수 있다.
- SSR 방식의 애플리케이션에 적합한 방식.
- JWT의 종류
- 액세스 토큰 (Access Token)
- 리프레시 토큰 (Refresh Token)
- Access Token은 보호된 정보들에 접근할 수 있는 권한부여에 사용됨.
- 권한을 부여받는 데엔 Access Token만 있으면 되지만, 짧은 유효기간을 주어 탈취되어도 오래 사용할 수 없도록 한다.
- Access Token의 유효기간이 만료되면 Refresh Token을 사용하여 새로운 Access Token을 발급 받는다.
- 리프레시토큰이 탈취되는 것을 방지 : redis 사용하여 토큰을 DB에 키값쌍으로 저장 하여 대조하는 방법도 있음.
- JWT 구조
- Heder
- 어떤 종류의 토큰인지 (지금 경우엔 JWT)
- 어떤 알고리즘으로 Sign할지 정의
JSON 객체를 base64 방식으로 인코딩하면 JWT의 첫 부분이 생성- Payload ( 민감한 정보는 담지 않는 것이 좋다.)
- 서버에서 활용할 수 있는 유저의 정보
- 어떤 정보에 접근 가능한지에 대한 권한
- 기타 필요한 정보
- Signature
- 원하는 비밀키와 Header에서 지정한 알고리즘을 사용하여 Header와 Payload에 대해서 단방향 암호화를 수행
- 토큰의 위변조 유무를 검증 시 사용
- ✔ JWT 생성 기능 구현
- (1) 메서드는 Plain Text 형태인 Secret Key의 byte[]를 Base64 형식의 문자열로 인코딩 해준다.
- jjwt가 버전업 되면서 Plain Text 자체를 Secret Key로 사용하는 것을 권장하지 않고 있다.
- (2) 메서드는 인증된 사용자에게 JWT를 최초로 발급해주기 위한 JWT 생성 메서드이다.
- (2-1) Base64 형식 Secret Key 문자열을 이용해 Key(
java.security.Key
) 객체를 얻는다.- (2-2)
setClaims()
JWT에 포함 시킬 Custom Claims를 추가한다. (주로 인증된 사용자와 관련된 정보 추가)- (2-3)
setSubject()
JWT에 대한 제목 추가- (2-4)
setIssuedAt()
JWT 발행 일자를 설정, 파라미터 타입은java.util.Date
타입이다.- (2-5)
setExpiration()
JWT의 만료일시 지정. 파라미터는 역시 Date 타입- (2-6)
signWith()
에 서명을 위한 Key 객체를 설정- (2-7) compact() 를 통해 JWT를 생성하고 직렬화한다.
- (3) 메서드는 Access Token이 만료되었을 경우, 새로 생성할 수 있게 해주는 Refresh Token을 생성하는 메서드이다. (별도의 Custom Claims는 추가할 필요 없다.)
- (4) 메서드는 JWT의 서명에 사용할 Secret Key를 생성해준다.
- (4-1)
Decoders.BASE64.decode()
메서드는 Base64 형식으로 인코딩 된 시크릿키를 디코딩 한 후, byte[]를 반환.- (4-2)
Keys.hmacShaKeyFor()
는 key byte array를 기반으로 적절한 HMAC 알고리즘을 적용한 Key 객체를 생성한다.
(jjwt 최신버전에서는 내부적으로 적절한 HMAC알고리즘을 지정해준다.)
✔ JWT 생성 기능 테스트
- (1) 에서 테스트에 사용할 Secret Key를 Base64 형식으로 인코딩 한 후, 인코딩 된 시크릿 키를 각 테스트 케이스에서 사용
- (2) Plain Text인 시크릿키가 Base64 형식으로 인코딩이 정상적으로 수행이 되는지 테스트
- Base64 형식으로 인코딩 된 시크릿키를 디코딩한 값이 원본과 일치하는지 테스트
- (3) JwtTokenizer가 Access Token을 정상적으로 생성하는지 테스트
- (4) Refresh Token을 정상적으로 생성하는지 테스트
✔ JWT 검증 기능 구현
- JwtTokenizer 클래스에 JWT 검증을 위한 메서드 추가
- jjwt에서는 JWT를 생성할 때 서명에 사용된 시크릿키를 이용해 내부적으로 Signature를 검증 한 후, 검증에 성공하면 JWT를 파싱해서 Claims를 얻을 수 있다.
- (1) 의
setSigningKey()
메서드로 서명에 사용된 시크릿키를 설정- (2) 의
parseClaimsJws()
메서드로 JWT를 파싱해서 Claims를 얻는다.
- verifySignature() 메서드는 Signature를 검증하는 용도로 Claims를 리턴할 필요는 없다.
- 파라미터로 사용한
jws
는 Signature가 포함된 JWT라는 의미
✔ JWT 검증 기능 테스트
- (1) 에서 구현한 JwtTokenizer의
verifySignature()
메서드가 Signature를 검증하는지 테스트
- 생성된 JWT를
verifySignature()
로 전달해서 Exception이 발생하지 않는다면 검증이 잘 수행된 것으로 본다.- (2) JWT 생성시 지정한 만료일시가 지나면 JWT가 정말 만료되는지 테스트
- 생성되는 JWT의 만료 주기를 짧게 준 후에 첫번째 Signature 검증을 수행하고, 만료일시가 지나도록 지연시간을 준 뒤, 두 번째 검증을 수행했을 경우
ExpiredJwtException
이 발생하면 정상적으로 JWT가 만료