JWT(Json Web Token)
사용자 인증 및 권한 부여를 위해 사용되는 토큰 기반 인증 방식이다.
주로 RESTful API와 같은 무상태(stateless) 환경에서 데이터를 안전하게 주고받기 위해 사용된다.
JWT의 구조
JWT는 세 가지 주요 구성 요소로 이루어져 있다.
- Header: 토큰 타입과 서명 알고리즘 정보를 포함하는 헤더 부분.
- Payload: 사용자 정보와 클레임(claim)을 포함하는 본문 부분.
토큰의 발급 시각(iat), 만료 시각(exp), 대상자(aud) 등 포함.- Signature: Header와 Payload를 합친 후, 지정된 알고리즘과 비밀키를 사용해 암호화한 값.
특징
- 안전성: Signature를 통해 토큰의 무결성을 확인할 수 있다.
- 독립성: 서버 상태를 유지하지 않아도 되므로 확장성이 뛰어나다.
- 사용 편리성: HTTP 헤더를 통해 간단히 전송할 수 있다.
사용 사례
- 사용자 인증: 로그인 후 발급된 토큰을 통해 사용자를 인증한다.
- 권한 부여: 특정 리소스에 대한 접근 권한을 부여한다.
- 정보 교환: 클라이언트와 서버 간에 데이터를 안전하게 주고받는다.
JWT의 처리
- 클라이언트 : JWT를 로컬 스토리지나 쿠키에 저장. 이를 통해 서버와의 통신 시 토큰을 포함하여 요청.
- 서버 : 클라이언트로부터 받은 JWT를 검증하여 요청의 유효성을 확인.
JWT 검증 방식
- 서명 검증: 서버는 JWT의 서명을 확인하여 토큰이 변조되지 않았는지 검증한다. 이를 위해 비밀키를 사용하여 서명을 재생성하고, JWT의 서명과 비교한다.
- 클레임 검증: JWT의 만료 시간(exp), 발급 시각(iat) 등을 확인하여 토큰이 유효한지 판단.
- HTTP 헤더 사용: 클라이언트는 요청 시 JWT를 HTTP 헤더(보통 Authorization 헤더의 Bearer 스킴)에 포함시켜 서버에 전달.
재발급 방식과 주기 (Access Token과 Refresh Token)
- Access Token은 짧은 유효기간을 가지며, 만료 시 Refresh Token을 사용해 새로운 Access Token을 발급받는다.
- Refresh Token은 일반적으로 더 긴 유효기간을 가지며, 클라이언트가 서버에 요청하여 새로운 Access Token을 발급받을 때 사용된다.
- 재발급 주기: 서버는 특정 시간마다 Access Token을 재발급하거나, 사용자가 로그아웃할 때 Refresh Token을 무효화한다.
다른 API 서비스 호출 시 인증 처리
- API Gateway 활용: 클라이언트는 API Gateway에 요청을 보낼 때 JWT를 포함시킨다. Gateway는 JWT를 검증한 후 요청을 적절한 서비스로 전달한다.
- 마이크로서비스 아키텍처: 여러 API 서비스가 있을 경우, 각 서비스는 JWT를 검증하여 사용자의 인증 상태를 확인한다.
- 서비스 간 인증: 서비스 간 통신 시 JWT를 사용하여 요청의 출처와 유효성을 검증한다.
JWT 토큰 발급 예시 (로그인)
app.post('/login', (req, res) => { const { id, password } = req.body; const user = findUserById(id); if(!user) return res.status(401).jason({ message: 'Invalid credentials'}); const accessToken = jwt.sign({id:user.id}, SECRET_KEY, {expiresIn:'15m'}); const refreshToken = jwt.sign({id:user.id}, REFRESH_SECRET_KEY, {expiresIn:'7d'}); refreshTokens.push(refreshToken); res.json({ accessToken, refreshToken}); });
JWT 토큰 검증 예시 (인가된 사용자 정보 조회)
app.get('/account', (req, res) => { const authHeader = req.headers.authorization; if(!authHeader) return res.status(401).json({ message: 'Authorization header missing' }); const token = authHeader.split(' ')[1]; try { const decoded = jwt.verify(token, SECRET_KEY); const user = findUserById(decoded.id); if(!user) return res.status(401).json("{ message: 'User not found' }); res.json({id:user.id}); } catch (err) { res.status(403).json({ message: 'Invalid or expired token'}); } });
JWT 토큰 갱신
app.post('/token', (req, res) => { const { refreshToken } = req.body; if(!refreshToken || !refreshTokens.includes(refreshToken)) { return res.status(403).json({ messasge: 'Invalid refresh token' }); } try { const decoded = jwt.verify(refreshToken, REFRESH_SECRET_KEY); const newAccessToken = jwt.sign({id:decoded.id}, SECRET_KEY, {expiresIn:'15m'}); res.json({ accessToken: newAccessToken }); } catch (err) { res.status(403).json({ message: 'Invalid or expired refresh token' }); } });
클라이언트 로그인 요청 (JWT 토큰 발급)
- http 메서드 : post
- url : [hostIp]:[hostPort]/login
- req : { id:id, password:password }
로그인 응답
- res.data.accessToken, res.data.refreshToken
클라이언트 요청 (JWT 토큰 포함 및 검증)
- http 메서드 : get
- url : [hostIp]:[hostPort]/account
- req : { headers: { Authorization: 'Bearer #{accessToken}'}
요청 응답
- res.data
클라이언트 Access Token 갱신
- http 메서드 : post
- url : [hostIp]:[hostPort]/token
- req : { refreshToken:refreshToken }
로그인 응답
- res.data.accessToken