-
Notifications
You must be signed in to change notification settings - Fork 0
JWT
- JWT는 웹표준 (RFC 7519)으로 두 개체 사이에 JSON 객체를 사용하여 가볍고 독립적인 (self-contained) 방식으로 정보를 안전성 있게 전달해준다.
- 전자식 서명으로 되어있기 때문에 신뢰있고 검증된 정보라고 할 수 있다.
- URL, Cookie, Header와 같이 사용할 수 있는 문자가 제한된 환경에서 정보를 주고받을 수도 있다.
- 필요한 모든 정보를 자체적으로 지니고 있다.
- 토큰에 대한 기본정보(생성 날짜, 만료 날짜...), 전달할 정보(로그인 유저 정보..) 등
- 로그인을 하면, 서버는 유저의 정보에 기반한 토큰을 발급하여 유저에게 전달한다. 그 후, 유저가 서버에 요청을 할 때 마다 JWT를 포함하여 전달합니다. 서버가 클라이언트에서 요청을 받을때 마다, 해당 토큰이 유효하고 인증됐는지 검증을 하고, 유저가 요청한 작업에 권한이 있는지 확인하여 작업을 처리한다.
- 서버측에서는 세션을 유지 할 필요가 없어진다. 즉 유저가 로그인되어있는지 안되어있는지 신경 쓸 필요가 없고, 유저가 요청을 했을때 토큰만 확인하면 되므로 세션 관리가 필요 없어서 서버 자원과 비용을 절감할 수 있다.
- 공개키/비밀키 등을 사용하여 서명되어 있기 때문에 송신자(sender)가 누구인지 알 수 있으며, 이로 인해 안전하게 정보들을 교환할 수 있다. 또한, 서명은 헤더와 페이로드를 사용하여 산출되기 때문에 해당 내용이 조작되지 않았다는 것을 증명할수도 있다.
HEADER.PAYLOAD.SIGNATURE
헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분을 점(.)으로 구분하는 구조- Signature를 해싱하기 위한 알고리즘 정보
- JWT를 어떻게 검증(Verify)하는가에 대한 내용을 담고 있다.
- Base64 URL-Safe 인코딩된 문자열을 담고 있다.
- Base64 인코딩의 경우
+,/,=이 포함되지만 JWT는 URI에서 파라미터로 사용할 수 있도록 URL-Safe 한 Base64url 인코딩을 사용한다.
- Base64 인코딩의 경우
- Base64 URL-Safe 인코딩된 문자열을 담고 있다.
// 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-
JWT의 내용(토큰에 담을 정보)
- 정보의 한 ‘조각’ 을 클레임(claim) 이라고 부르고, 이는 name / value의 한 쌍으로 이뤄져있다. 토큰에는 여러 개의 클레임 들을 넣을 수 있으며 Claim Set이라고 부른다.
- Claim Set은 JWT에 대한 내용(토큰 생성자(클라이언트)의 정보, 생성 일시 등)이나 클라이언트와 서버 간 주고 받기로 한 값들로 구성된다.
-
Claim 종류
-
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의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용딘다. 일회용 토큰에 사용하면 유용하다.
-
Public claim
-
충돌이 방지된 (collision-resistant) 이름을 가지고 있어야 하기 때문에 claim 이름을 URI 형식으로 짓는다.
{ "https://seongbeen.com/jwt_claims/is_admin": true }
-
-
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하지 않으므로 제거되어야 합니다. 패딩이 한 개 생길 때도 있고, 두 개 생길 때도 있는데 전부 제거해줘도 디코딩 할 때 전혀 문제가 되지 않는다.
-
점(.)을 구분자로 해서 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도 자동으로 설정한다.
-
- 점(.)을 구분자로 해서 Header, Payload, Signature를 합친 것
eyJhbGciOiJFUzI1NiIsImtpZCI6IktleSBJRCJ9.eyJpYXQiOjE1ODYzNjQzMjcsImlzcyI6ImppbmhvLn
NoaW4ifQ.eyJhbGciOiJFUzI1NiIsImtpZCI6IktleSBJRC9.eyJpYXQiOjE1ODYzNjQzMjcsImlzcyI6Imp
pbmhvLnNoaW4ifQ.MEQCIBSOVBBsCeZ_8vHulOvspJVFU3GADhyCHyzMiBFVyS3qAiB7Tm_ME
Xi2kLusOBpanIrcs2NVq24uuVDgH71M_fIQGg- 이렇게 완성된 JWT는 Header의
alg,kid속성과공개 키를 이용해 검증할 수 있다. 서명 검증이 성공하면 JWT의 모든 내용을 신뢰할 수 있게되고, 페이로드의 값으로 접근 제어나 원하는 처리를 할 수 있게 된다.
- API 요청을 허가하는데 사용
- 리소스에 직접 접근할 수 있도록 해주는 정보만을 가지고 있다. 즉, 클라이언트는 Access token이 있어야 서버 자원에 접근할 수 있다.
- 짧은 만료기간을 가진다.
- 새로운 Access token을 발급받기 위한 정보를 갖는다. 즉, 클라이언트가 Access token이 없거나 만료되었다면 Refresh token을 통해 Auth Server에 요청해서 재발급 받을 수 있다.
- 긴 만료기간을 가진다.
- Refresh Token은 중요하기 때문에 외부에 노출되지 않도록 엄격하게 관리해야 하므로 주로 데이터베이스에 저장한다.
- 한 번 사용된 Refresh token은 폐기
-
회원가입, 로그인, 인증 프로세스 흐름도 with Access token

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

-
사용자 로그인
- 클라이언트가 id와 password를 입력하여 로그인 시도
- 서버는 id, password를 확인하고 secret key를 통해 JWT 토큰 발급 (Access token + Refresh token)
- Access token, Refresh token, 토큰 타입, 사용자 정보, 권한 등을 담은 응답을 클라이언트에 전달
- 클라이언트가 API 요청할 때 Authorization header에 Access token을 담아서 전달
- 서버에서 JWT 인증 시, 만료된 토큰일 경우 ExpiredJwtException 호출
- 클라이언트에 Unauthorized error 전달
- 클라이어언트가 유효한 Refresh token을 보내 Access token 재발급 요청
- 서버에서 유효한 Refresh token인지 확인
- 만료된 Refresh token일 경우, 오류를 반환하여 사용자에게 로그인 요청
- 존재하지 않은 Refresh token일 경우, 오류 반환
- 클라이언트에 새로운 Access token + 새로운 Refresh token + 토큰 타입이 담긴 응답 전달
-
사용자 로그인
- 최초 발급 시 Access Token과 Refresh Token 을 발급
- 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는 토큰 내에 모든 정보를 다 가지고 있기 때문에 서버에서 한번 발급된 토큰에 대한 변경은 불가능하다. 토큰을 잘못 발행하여 삭제하고 싶더라도 Signature만 맞으면 유효한 토큰으로 인식하기 때문에 서버에서는 한번 발급된 토큰의 정보를 바꾸는 일 등이 불가능하다.
- JWT는 기본적으로 Payload에 대한 정보를 암호화 하지 않는다. 단순히 BASE64URL로 인코딩만 하기 때문에 중간에 패킷을 가로채거나 기타 방법으로 토큰을 취득했으면 디코딩을 통해 데이터를 볼 수 있다.
- JWE(JSON Web Encryption)를 통해 암호화 하거나 중요 데이터를 Payload에 넣지 말아야 한다.
- JSON Web Signature (JWS)는 JSON 데이터 구조를 사용하는 서명 표준으로 RFC7515 표준이다.
- JSON으로 전자 서명을하여 URL-safe 문자열로 표현한 것이다.
- JSON Web Encryption (JWE)는 JSON 데이터 구조를 사용하는 암호화 방법으로 RFC7516 표준이다.
- JSON을 암호화하여 URL-safe 문자열로 표현한 것이다.
- 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());
}
}- 서비스를 이용하는 회원의 토큰 검증을 통해 인증된 사용자인지 확인하는 인터셉터
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());
}
}
}- 인증된 회원 식별 번호를 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 {
}-
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);
}
}-
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);
}
}- 인증된 회원(@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] 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