Skip to content
Seongbeen Kim edited this page Jul 12, 2021 · 3 revisions

JWT(JSON Web Token)란?

  • JWT는 웹표준 (RFC 7519)으로 두 개체 사이에 JSON 객체를 사용하여 가볍고 독립적인 (self-contained) 방식으로 정보를 안전성 있게 전달해준다.
    • 전자식 서명으로 되어있기 때문에 신뢰있고 검증된 정보라고 할 수 있다.
  • URL, Cookie, Header와 같이 사용할 수 있는 문자가 제한된 환경에서 정보를 주고받을 수도 있다.
  • 필요한 모든 정보를 자체적으로 지니고 있다.
    • 토큰에 대한 기본정보(생성 날짜, 만료 날짜...), 전달할 정보(로그인 유저 정보..) 등

JWT를 사용하는 이유

Authorization

  • 로그인을 하면, 서버는 유저의 정보에 기반한 토큰을 발급하여 유저에게 전달한다. 그 후, 유저가 서버에 요청을 할 때 마다 JWT를 포함하여 전달합니다. 서버가 클라이언트에서 요청을 받을때 마다, 해당 토큰이 유효하고 인증됐는지 검증을 하고, 유저가 요청한 작업에 권한이 있는지 확인하여 작업을 처리한다.
    • 서버측에서는 세션을 유지 할 필요가 없어진다. 즉 유저가 로그인되어있는지 안되어있는지 신경 쓸 필요가 없고, 유저가 요청을 했을때 토큰만 확인하면 되므로 세션 관리가 필요 없어서 서버 자원과 비용을 절감할 수 있다.

Information Exchange

  • 공개키/비밀키 등을 사용하여 서명되어 있기 때문에 송신자(sender)가 누구인지 알 수 있으며, 이로 인해 안전하게 정보들을 교환할 수 있다. 또한, 서명은 헤더와 페이로드를 사용하여 산출되기 때문에 해당 내용이 조작되지 않았다는 것을 증명할수도 있다.

구성

HEADER.PAYLOAD.SIGNATURE
헤더(Header), 페이로드(Payload), 서명(Signature)  부분을 (.)으로 구분하는 구조

Header

  • Signature를 해싱하기 위한 알고리즘 정보
  • JWT를 어떻게 검증(Verify)하는가에 대한 내용을 담고 있다.
    • Base64 URL-Safe 인코딩된 문자열을 담고 있다.
      • Base64 인코딩의 경우 +, /, =이 포함되지만 JWT는 URI에서 파라미터로 사용할 수 있도록 URL-Safe 한 Base64url 인코딩을 사용한다.
// Base64URLSafe 인코딩 되기 전 가지고 있는 내용
{
    "alg": "ES256",
    "kid": "Key ID"
}

{
    "alg": "HS256",
    "typ": "JWT"
}
  • alg : Signature 시 사용하는 알고리즘
    • ex) HMAC SHA256, RSA, ECDSA
  • kid : Signature 시 사용하는 키(Public/Private Key)를 식별하는 값
  • typ : Signature 시 사용하는 토큰의 타입
  • 위와 같은 JSON 객체를 문자열로 직렬화하고 UTF-8과 Base64 URL-Safe로 인코딩하면 다음과 같이 Header를 생성할 수 있다.
Base64URLSafe(UTF-8('{"alg": "ES256","kid": "Key ID"}'))

// 결과
eyJhbGciOiJFUzI1NiIsImtpZCI6IktleSBJRCJ9

Payload

  • JWT의 내용(토큰에 담을 정보)

    • 정보의 한 ‘조각’ 을 클레임(claim) 이라고 부르고, 이는 name / value의 한 쌍으로 이뤄져있다. 토큰에는 여러 개의 클레임 들을 넣을 수 있으며 Claim Set이라고 부른다.
    • Claim Set은 JWT에 대한 내용(토큰 생성자(클라이언트)의 정보, 생성 일시 등)이나 클라이언트와 서버 간 주고 받기로 한 값들로 구성된다.
  • Claim 종류

    1. Registered claim

      • 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기 위하여 이름이 이미 정해진 Claim이며, Registered claim의 사용은 모두 선택적이다.
      • iss: 토큰 발급자 (issuer)
      • sub: 토큰 제목 (subject)
      • aud: 토큰 대상자 (audience)
      • exp: 토큰의 만료시간 (expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (예: 1480849147370) 언제나 현재 시간보다 이후로 설정되어 있어야 한다.
      • nbf: Not Before를 의미하며, 토큰의 활성 날짜와 비슷한 개념. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.
      • iat: 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 할 수 있다.
      • jti: JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용딘다. 일회용 토큰에 사용하면 유용하다.
    2. Public claim

      • 충돌이 방지된 (collision-resistant) 이름을 가지고 있어야 하기 때문에 claim 이름을 URI 형식으로 짓는다.

        {
            "https://seongbeen.com/jwt_claims/is_admin": true
        }
    3. Private claim

      • 양 측간에 (보통 클라이언트 <->서버) 협의하에 사용되는 claim 이름이다. Public claim과는 달리 이름이 중복되어 충돌이 될 수 있으니 사용할때에 유의해야 한다.

        {
            "username": "seongbeen"
        }
  • 예제 Payload

    // Base64URLSafe 인코딩 되기 전 가지고 있는 내용
    {
        "iss": "seongbeen.kim",
        "iat": "1586364327"
    }
  • 위와 같은 JSON 객체를 문자열로 직렬화하고 Base64 URL-Safe로 인코딩하면 다음과 같이 Payload를 생성할 수 있다.

    Base64URLSafe('{"iss": "seongbeen.kim","iat": "1586364327"}')
    
    // 결과
    eyJpYXQiOjE1ODYzNjQzMjcsImlzcyI6ImppbmhvLnNoaW4ifQ

    주의: base64로 인코딩을 할 때 dA==처럼 뒤에 = 문자가 한두개 붙을 때가 있다. 이 문자는 base64 인코딩의 padding 문자라고 부른다. JWT 토큰은 URL의 파라미터로 전달 될 때도 있는데, 이 = 문자는 url-safe하지 않으므로 제거되어야 합니다. 패딩이 한 개 생길 때도 있고, 두 개 생길 때도 있는데 전부 제거해줘도 디코딩 할 때 전혀 문제가 되지 않는다.

Signature

  • 점(.)을 구분자로 해서 Header와 Payload를 합친 문자열을 서명한 값

  • Signature는 Header의 alg에 정의된 알고리즘, 비밀 키, Payload의 JWT내용을 이용해 생성하고 Base64 URL-Safe로 인코딩한다.

    Base64URLSafe(Sign('ES256', '${PRIVATE_KEY}', 'eyJhbGciOiJFUzI1NiIsImtpZCI6IktleSBJRCJ9.eyJpYXQiOjE1ODYzNjQzMjcsImlzcyI6ImppbmhvLnNoaW4ifQ')))
    
    // 결과
    MEQCIBSOVBBsCeZ_8vHulOvspJVFU3GADhyCHyzMiBFVyS3qAiB7Tm_MEXi2kLusOBpanIrcs2NVq24uuVDgH71M_fIQGg
    • signWith를 사용할때 JJWT는 연관된 알고리즘 식별자와 함께 alg header도 자동으로 설정한다.

JWT

  • 점(.)을 구분자로 해서 Header, Payload, Signature를 합친 것
eyJhbGciOiJFUzI1NiIsImtpZCI6IktleSBJRCJ9.eyJpYXQiOjE1ODYzNjQzMjcsImlzcyI6ImppbmhvLn
NoaW4ifQ.eyJhbGciOiJFUzI1NiIsImtpZCI6IktleSBJRC9.eyJpYXQiOjE1ODYzNjQzMjcsImlzcyI6Imp
pbmhvLnNoaW4ifQ.MEQCIBSOVBBsCeZ_8vHulOvspJVFU3GADhyCHyzMiBFVyS3qAiB7Tm_ME
Xi2kLusOBpanIrcs2NVq24uuVDgH71M_fIQGg
  • 이렇게 완성된 JWT는 Header의 alg, kid 속성과 공개 키를 이용해 검증할 수 있다. 서명 검증이 성공하면 JWT의 모든 내용을 신뢰할 수 있게되고, 페이로드의 값으로 접근 제어나 원하는 처리를 할 수 있게 된다.

Access token

  • API 요청을 허가하는데 사용
  • 리소스에 직접 접근할 수 있도록 해주는 정보만을 가지고 있다. 즉, 클라이언트는 Access token이 있어야 서버 자원에 접근할 수 있다.
  • 짧은 만료기간을 가진다.

Refresh token

  • 새로운 Access token을 발급받기 위한 정보를 갖는다. 즉, 클라이언트가 Access token이 없거나 만료되었다면 Refresh token을 통해 Auth Server에 요청해서 재발급 받을 수 있다.
  • 긴 만료기간을 가진다.
  • Refresh Token은 중요하기 때문에 외부에 노출되지 않도록 엄격하게 관리해야 하므로 주로 데이터베이스에 저장한다.
    • 한 번 사용된 Refresh token은 폐기

JWT 프로세스

  1. 회원가입, 로그인, 인증 프로세스 흐름도 with Access token

    spring-boot-authentication-jwt-spring-security-flow

    • 사용자 등록
      1. 클라이언트가 회원가입 요청
      2. DB에 사용자 저장
      3. 클라이언트에게 회원가입 완료 응답
    • 사용자 로그인
      1. 클라이언트가 id와 password를 입력하여 로그인을 시도
      2. 서버는 id, password를 확인하고 secret key를 통해 JWT 토큰(Access Token) 발급
      3. 토큰 (Access Token), 사용자 정보, 권한 등을 담은 응답을 클라이언트에 전달
      4. 클라이언트가 API 요청할 때 Authorization header에 Access token을 담아서 전달
        • 클라이언트가 보호되는 자원에 접근하려면 JWT가 반드시 HTTP Authorization Header에 추가되어야 한다.
      5. 서버는 JWT Signature를 체크하고 Payload로부터 사용자 정보, 권한 등 확인 및 정보 추출
      6. 클라이언트에게 요청에 대한 응답 전달
  2. Refresh Token이 추가된 흐름도

    spring-boot-refresh-token-jwt-example-flow

    • 사용자 로그인
      1. 클라이언트가 id와 password를 입력하여 로그인 시도
      2. 서버는 id, password를 확인하고 secret key를 통해 JWT 토큰 발급 (Access token + Refresh token)
      3. Access token, Refresh token, 토큰 타입, 사용자 정보, 권한 등을 담은 응답을 클라이언트에 전달
      4. 클라이언트가 API 요청할 때 Authorization header에 Access token을 담아서 전달
      5. 서버에서 JWT 인증 시, 만료된 토큰일 경우 ExpiredJwtException 호출
      6. 클라이언트에 Unauthorized error 전달
      7. 클라이어언트가 유효한 Refresh token을 보내 Access token 재발급 요청
      8. 서버에서 유효한 Refresh token인지 확인
        • 만료된 Refresh token일 경우, 오류를 반환하여 사용자에게 로그인 요청
        • 존재하지 않은 Refresh token일 경우, 오류 반환
      9. 클라이언트에 새로운 Access token + 새로운 Refresh token + 토큰 타입이 담긴 응답 전달

JWT 토큰 재발급 프로세스

  1. 최초 발급 시 Access Token과 Refresh Token 을 발급
  2. Access Token으로 API를 사용하다가 만료시간이 지나면 만료시간을 길게 준 Refresh Token을 이용해서 Access Token을 재발급
    • 클라이언트가 Access Token의 만료시간을 알 수 있기 때문에 클라이언트에서 판단하여 만료시간이 넘었으면 Refresh token을 보내 Access Token 재발급을 요청하거나 TokenExpiredError가 발생했을 때 재발급해준다.
    • 유효한 Refresh Token으로 요청이 들어오면 새로운 Access Token을 발급하고, 만료된 Refresh Token으로 요청이 들어오면 오류를 반환해, 사용자에게 로그인을 요구한다.

장점

  • 모든 정보는 토큰 자체에 포함하기 때문에 별도의 인증 저장소가 필요없다.
  • 분산 마이크로 서비스 환경에서 중앙 집중식 인증 서버와 데이터베이스에 의존하지 않는 쉬운 인증 및 인가 방법을 제공
  • 무상태(stateless)이며 확장성이 있다.
    • 기존 서버에 세션을 저장하는 방식에서 서버 여러 대를 사용하여 요청을 분산하였다면 어떤 유저가 로그인했을 때 그 유저는 처음 로그인한 서버에만 요청을 내보내도록 설정해야 했지만, 토큰을 사용하게 될 경우 토큰 값만 알고 있다면 어떤 서버로 요청이 들어가던 상관이 없다.
  • Base64 URL Safe Encoding을 사용 → URL, Cookie, Header 모두 사용 가능
  • REST 서비스로 제공 가능
  • 독립적인 JWT
  • 내장된 만료

단점

  • Payload의 claim이 많아질 수록 JWT 토큰이 커지기 때문에 API 호출마다 크기가 커진만큼 네트워크 사용량이 증가한다.
  • 토큰이 서버에 저장되지 않고 클라이언트에 저장 → 데이터베이스에서 사용자 정보를 조작하더라도 토큰에 적용할 수 없다.
  • 한 번 발행된 토큰을 제거할 수 없다. (기간 만료 제외) 이러한 이유 때문에 Blacklist를 만들어 관리
    • JWT는 토큰 내에 모든 정보를 다 가지고 있기 때문에 서버에서 한번 발급된 토큰에 대한 변경은 불가능하다. 토큰을 잘못 발행하여 삭제하고 싶더라도 Signature만 맞으면 유효한 토큰으로 인식하기 때문에 서버에서는 한번 발급된 토큰의 정보를 바꾸는 일 등이 불가능하다.
      • JWT를 쓴다면 Expire time을 꼭 명시적으로 두도록 하고 Refresh Token을 이용하여 토큰을 재발행 하도록 해야한다.
  • JWT는 기본적으로 Payload에 대한 정보를 암호화 하지 않는다.  단순히 BASE64URL로 인코딩만 하기 때문에 중간에 패킷을 가로채거나 기타 방법으로 토큰을 취득했으면 디코딩을 통해 데이터를 볼 수 있다.
    • JWE(JSON Web Encryption)를 통해 암호화 하거나 중요 데이터를 Payload에 넣지 말아야 한다.

JWS

  • JSON Web Signature (JWS)는 JSON 데이터 구조를 사용하는 서명 표준으로 RFC7515 표준이다.
    • JSON으로 전자 서명을하여 URL-safe 문자열로 표현한 것이다.

JWE

  • JSON Web Encryption (JWE)는 JSON 데이터 구조를 사용하는 암호화 방법으로 RFC7516 표준이다.
    • JSON을 암호화하여 URL-safe 문자열로 표현한 것이다.

적용한 코드

JwtManager

  • Jwt 토큰 생성 및 검증해주는 객체
package com.project.kodesalon.common;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;

import static com.project.kodesalon.common.ErrorCode.EXPIRED_JWT_TOKEN;
import static com.project.kodesalon.common.ErrorCode.INVALID_JWT_TOKEN;

@Slf4j
@Component
public class JwtManager {

    private static final String MEMBER_ID = "memberId";

    private final String secretKey;
    private final long accessExpirationMs;

    public JwtManager(@Value("${spring.jwt.secret}") final String secretKey, @Value("${spring.jwt.accessExpirationMs}") final long accessExpirationMs) {
        this.secretKey = secretKey;
        this.accessExpirationMs = accessExpirationMs;
    }

    public String generateJwtToken(final Long memberId) {
        Date issueTime = new Date();
        return Jwts.builder()
                .setHeaderParam("typ", Header.JWT_TYPE)
                .claim(MEMBER_ID, memberId)
                .setIssuedAt(issueTime)
                .setExpiration(new Date(issueTime.getTime() + accessExpirationMs))
                .signWith(SignatureAlgorithm.HS256, getSignKey())
                .compact();
    }

    public Long getMemberIdFrom(final String token) {
        return Jwts.parser()
                .setSigningKey(getSignKey())
                .parseClaimsJws(token)
                .getBody()
                .get(MEMBER_ID, Long.class);
    }

    public boolean validateToken(final String token) {
        try {
            Jwts.parser()
                    .setSigningKey(getSignKey())
                    .parseClaimsJws(token);

            return true;
        } catch (SignatureException e) {
            log.info("Invalid JWT signature: {}", e.getMessage());
            throw new JwtException(INVALID_JWT_TOKEN);
        } catch (MalformedJwtException e) {
            log.info("Invalid JWT token: {}", e.getMessage());
            throw new JwtException(INVALID_JWT_TOKEN);
        } catch (ExpiredJwtException e) {
            log.info("JWT token is expired: {}", e.getMessage());
            throw new JwtException(EXPIRED_JWT_TOKEN);
        } catch (UnsupportedJwtException e) {
            log.info("JWT token is unsupported: {}", e.getMessage());
            throw new JwtException(INVALID_JWT_TOKEN);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims string is empty: {}", e.getMessage());
            throw new JwtException(INVALID_JWT_TOKEN);
        }
    }

    private Key getSignKey() {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        byte[] secretKeyBytes = secretKey.getBytes();
        return new SecretKeySpec(secretKeyBytes, signatureAlgorithm.getJcaName());
    }
}

LoginInterceptor

  • 서비스를 이용하는 회원의 토큰 검증을 통해 인증된 사용자인지 확인하는 인터셉터
package com.project.kodesalon.common.interceptor;

import com.project.kodesalon.common.JwtManager;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

import static com.project.kodesalon.common.ErrorCode.INVALID_HEADER;

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    public static final String LOGIN_MEMBER = "loginMember";
    private static final int BEARER_LENGTH = 7;
    private static final String LOG_ID = "logId";

    private final JwtManager jwtManager;

    public LoginInterceptor(JwtManager jwtManager) {
        this.jwtManager = jwtManager;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String uuid = UUID.randomUUID().toString();
        String requestURI = request.getRequestURI();
        MDC.put(LOG_ID, uuid);
        log.info("REQUEST : [logId : {}] [requestURI : {}] [handler : {}]", uuid, requestURI, handler);

        String token = parseTokenFrom(request);
        jwtManager.validateToken(token);
        Long memberId = jwtManager.getMemberIdFrom(token);
        request.setAttribute(LOGIN_MEMBER, memberId);
        return true;
    }

    private String parseTokenFrom(HttpServletRequest request) {
        try {
            return request.getHeader("Authorization").substring(BEARER_LENGTH);
        } catch (NullPointerException e) {
            throw new JwtException(INVALID_HEADER);
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        String requestURI = request.getRequestURI();
        String logId = MDC.get(LOG_ID);
        log.info("RESPONSE : [logId : {}] [requestURI : {}] [handler : {}]", logId, requestURI, handler);
        MDC.clear();
        if (ex != null) {
            log.error("afterCompletion error : {}", ex.getMessage());
        }
    }
}

Login

  • 인증된 회원 식별 번호를 ArgumentResolver를 통해 받아오기 위해 사용된 어노테이션
package com.project.kodesalon.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}

LoginMemberArgumentResolver

  • LoginIntercepto에서 전달받은 인증된 회원의 식별 번호를 @Login이 적용된 곳에 전달해주기 위한 ArgumentResolver
package com.project.kodesalon.common.argumentresolver;

import com.project.kodesalon.common.annotation.Login;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;

import static com.project.kodesalon.common.interceptor.LoginInterceptor.LOGIN_MEMBER;

@Slf4j
@Component
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
        boolean hasMemberIdType = Long.class.isAssignableFrom(parameter.getParameterType());
        return hasLoginAnnotation && hasMemberIdType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        return request.getAttribute(LOGIN_MEMBER);
    }
}

WebConfig

  • LoginInterceptor, LoginMemberArgumentResolver을 적용해주기 위한 설정 bean
package com.project.kodesalon.common.config;

import com.project.kodesalon.common.argumentresolver.LoginMemberArgumentResolver;
import com.project.kodesalon.common.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final LoginInterceptor loginInterceptor;
    private final LoginMemberArgumentResolver loginMemberArgumentResolver;

    public WebConfig(LoginInterceptor loginInterceptor, LoginMemberArgumentResolver loginMemberArgumentResolver) {
        this.loginInterceptor = loginInterceptor;
        this.loginMemberArgumentResolver = loginMemberArgumentResolver;
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/api/v1/**")
                .excludePathPatterns("/api/v1/auth/**", "/api/v1/members/join");
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(loginMemberArgumentResolver);
    }
}

MemberController

  • 인증된 회원(@Login 적용)들이 접근할 수 있는 API들이 포함된 컨트롤러
package com.project.kodesalon.model.member.controller;

import com.project.kodesalon.common.annotation.Login;
import com.project.kodesalon.model.member.service.MemberService;
import com.project.kodesalon.model.member.service.dto.ChangePasswordRequest;
import com.project.kodesalon.model.member.service.dto.ChangePasswordResponse;
import com.project.kodesalon.model.member.service.dto.CreateMemberRequest;
import com.project.kodesalon.model.member.service.dto.SelectMemberResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("/api/v1/members")
public class MemberController {
    private final MemberService memberService;

    public MemberController(final MemberService memberService) {
        this.memberService = memberService;
    }

    @PostMapping("/join")
    public ResponseEntity<Void> join(@RequestBody @Valid final CreateMemberRequest createMemberRequest) {
        memberService.join(createMemberRequest);
        return ResponseEntity.ok().build();
    }

    @GetMapping
    public ResponseEntity<SelectMemberResponse> selectMember(@Login Long memberId) {
        SelectMemberResponse selectMemberResponse = memberService.selectMember(memberId);
        return ResponseEntity.ok().body(selectMemberResponse);
    }

    @PutMapping("/password")
    public ResponseEntity<ChangePasswordResponse> changePassword(@Login Long memberId, @RequestBody @Valid final ChangePasswordRequest changePasswordRequest) {
        ChangePasswordResponse changePasswordResponse = memberService.changePassword(memberId, changePasswordRequest);
        return ResponseEntity.ok().body(changePasswordResponse);
    }
}

참조

JWT 공식 홈페이지

JWT를 소개합니다.

JWT ( JSON WEB TOKEN ) 이란?

JWT(JSON Web Token) 인증 체계

[JWT] JSON Web Token - 서버기반/토큰기반 인증의 차이와 JWT의 장단점

Spring Boot Token based Authentication with Spring Security & JWT

Spring Boot 2 JWT Authentication with Spring Security

Spring Boot Refresh Token with JWT example

OAuth 2.0 Threat Model and Security Considerations

액세스 토큰 재발급

쉽게 알아보는 서버 인증 2편(Access Token + Refresh Token)

Refresh Token과 Sliding Sessions를 활용한 JWT의 보안 전략

Clone this wiki locally