4회차 미션은 회원 엔티티를 추가하고 회원가입
, 로그인
, 내 정보 조회
기능을 새롭게 추가해야 합니다.
로그인을 할때는 JWT를 사용하며 내 정보 조회시 요청 헤더에 반드시 유효한 토큰의 정보가 있어야 합니다!
설명하지 않은 모든 코드는 다음 PR에 있습니다!
https://github.com/JSCODE-EDU/project-class-HiiWee/pull/9
1. 회원가입 구현
회원가입 기능 요구사항
- 기능 사항
- 회원가입 시
이메일
,패스워드
를 받아서, DB에이메일
,패스워드
,회원 가입 시간
을 저장해야 한다. - 유저에 대한 정보가 저장될 때,
id
(PK, primary key)도 같이 Auto-increment 형식으로 저장돼야 한다.
- 회원가입 시
- 검증 사항
-
이메일
에 반드시@
가 1개만 포함되어 있어야 한다. -
이메일
에 공백이 포함될 수 없다. - 중복된
이메일
이 존재할 수 없다. -
패스워드
에 공백이 포함될 수 없다. -
패스워드
는 8자 이상 15자 이하여야 한다.
-
Member 엔티티
회원가입을 구현하기 위한 Member 엔티티는 다음과 같다.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Embedded
private Email email;
@Embedded
private Password password;
@CreatedDate
private LocalDateTime createdAt;
protected Member() {
}
@Builder
private Member(final String email, final String password) {
this.email = Email.from(email);
this.password = Password.from(password);
}
public Long getId() {
return id;
}
// Java Bean Property 규약을 지키자!
public String getEmail() {
return email.getValue();
}
public String getPassword() {
return password.getValue();
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
}
JPQL을 쿼리 메소드로 변경하기
회원가입을 구현하면서 이미 존재하는 이메일을 검색할때 작성했던 쿼리 메소드에서 특정 속성을 찾을 수 없다는 오류를 만났었습니다! 사실 원인은 간단했지만, 게시글 검색에서도 이와 동일한 이슈가 있었기에 정리해 봅니다!
// 다음과 같이 사용하길 원함
Optional<Member> findByEmailValueAndPasswordValue(String email, String password);
이전에 JPQL로 작성했던 Post의 Title 값 객체의 String 값으로 키워드를 조회하는 미션이 있었습니다.
이번에도 회원가입시 이미 존재하는 email 검증을 위해 Email 값 객체의 String값으로 존재하는 메일을 조회해야 했습니다.
하지만, ApplicationContextException이 발생하면서 쿼리 메소드로 변환할때 emailValue라는 필드를 찾을 수 없다는 예외를 만났습니다.
IllegalArgumentException: Failed to create query for method public abstract java.util.Optional
com.example.anonymousboard.member.repository.MemberRepository.findByEmailValueAndPasswordValue(java.lang.String,java.lang.String)!
Unable to locate Attribute with the the given name [emailValue] on this ManagedType [com.example.anonymousboard.member.domain.Member]
오류의 원인은 단순했지만, 이유가 불분명 했습니다. Email은 위와 같이 값 객체로 선언하고 있기에 객체 탐색을 하게 된다면 email -> value가 맞았는데 여기서는 emailValue라는 하나의 속성을 찾지 못한다는 오류가 발생했습니다.
결국 다음 문서에서 원인을 유추할 수 있었습니다. https://www.baeldung.com/the-persistence-layer-with-spring-data-jpa#1-automatic-custom-queries
JPA의 쿼리 메소드는 getter, setter가 없고 필드만 존재하는 엔티티도 조회할 수 있다. 하지만, getter를(setter도?)정의하게 되면 Spring Data Jpa는 해당 getter를 이용해 속성의 이름을 유추하고 있었기에 getEmailValue에서 Java Bean Property 규약에 의해 emailValue라는 값을 찾으려고 하기 때문에 예외가 발생했습니다.
따라서 단순하게 getter의 이름을 getEmail로 실제 필드명과 일치하도록 변경하면 깔끔하게 쿼리 메소드로 로직을 구현할 수 있었습니다.
회원 이메일, 비밀번호 검증시 정규표현식 이용
회원가입을 할때는 위와 같이 비밀번호 확인과 일치하는지와, 유니크한 이메일인지를 확인하는 절차 외에도
이메일 형식, 패스워드 형식에 대한 검증사항이 존재합니다.
검증사항에 해당되는 내용이 크게 복잡하지 않았으므로, 정규 표현식을 이용해 간단하게 검증할 수 있습니다!
- 이메일 검증
^[a-z]{1}[a-z0-9_\\.]+@[a-z\\.]+\\.[a-zA-Z]+$
첫글자는 반드시 영어로 시작되어야 하며, 1개의@
,.
가 필요합니다. 아이디 형식은 소문자 + 숫자만 가능합니다.
- 비밀번호 검증
^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,15}$
8 ~ 15글자로 제한되며 반드시 1개이상의 영어, 숫자, 지정된 특수문자(@$!%*#?&)가 포함되어야 합니다.
2. JWT를 이용한 로그인
로그인 요구사항
- 기능 사항
- 로그인 시
이메일
,패스워드
값을 받는다. - 로그인에 성공했을 때, JWT를 활용해 Access Token 값을 응답해야 한다.
- JWT의 payload에는 사용자의
id
(PK, primary key)가 반드시 담겨있어야 한다.
- 로그인 시
JWT 개요
JWT는 Json Web Token으로 세션기반 로그인과 달리 토큰 기반 인증을 하므로, 서버에 사용자의 상태를 굳이 저장하지 않아도 되기에 stateless한 방식입니다.
JWT는 Header
, Payload
, Signature
로 구성되어 있습니다.
- Header: 토큰의 타입, JWT 서명을 생성할때 사용되는 알고리즘을 포함합니다.
{ "typ": "JWT", "alg": "HS512" }
- Payload: 사용자에 대한 정보인 Claim을 담습니다. JWT에서 제공하는 형식도 있지만, 사용자가 커스텀하게 Claim을 만들어 담을 수 있습니다.
- standard claims
- standard claims
- Signature: header를 인코딩한 값, payload를 인코딩 한 값을 합친후 백엔드 서버에서 들고있는 개인키를 통해 암호화 되어있습니다. 결국 서버에 존재하는 개인키를 통해서만 암호화를 풀 수 있습니다.
위와 같은 특징 때문에 악성 사용자가 임의로 Header나 Payload값을 변경하여 요청을 보내게 된다면 signature를 다시 디코딩하여 입력받은 Header와 Payload와 비교했을때 다른 값을 가지게 되므로 인증이 불가능해 집니다.
JWT 의존성 추가
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
jjwt-api가 실제 코드레벨에서 구현할때 사용되며 나머지 두 개의 의존성은 런타임에 사용됩니다.
로그인 요청부터 응답까지의 흐름 분석
JWT를 사용하기 앞서 간단하게 로그인 요청의 흐름을 분석하겠습니다.
어찌보면 JWT라는 개념이 처음 사용하는 사람의 입장에서는 상당히 난해하다고 느껴질 수 있습니다. 저 또한 그랬었기에, 현재 구현하고자 하는 로그인 로직의 흐름을 분석해보고 하나씩 구현해보겠습니다!
로그인 요청
: 사용자의 로그인 요청(이메일, 비밀번호)이 들어옵니다.이메일 검증
: 사용자의 이메일과 비밀번호에 해당되는 회원이 존재하는지 검증합니다.토큰 생성
: 2.를 정상적으로 통과했다면 jwt토큰을 생성합니다. 이때 2에서 조회한 사용자의id
값을 jwt payload에 담습니다.응답 생성
: 생성한 토큰을 통해 응답을 생성하고 클라이언트에게 반환합니다.
1, 2, 4의 경우 기본적인 비즈니스 로직의 흐름이므로 3에 대해서 좀 더 자세히 알아보겠습니다.
토큰 생성
// AuthService.createToken(...)
public TokenResponse createToken(final LoginRequest loginRequest) {
Member member = memberRepository.findByEmailValueAndPasswordValue(loginRequest.getEmail(),
loginRequest.getPassword()).orElseThrow(LoginFailedException::new);
String accessToken = jwtTokenProvider.createAccessToken(member.getId());
return TokenResponse.from(accessToken);
}
정상적으로 이메일, 패스워드를 통해 사용자를 정상적으로 조회할 수 있다면 JwtTokenProvider에서 사용자의 id값을 넘겨주면서 토큰을 생성하도록 합니다.
// application-local.yml
security:
jwt:
token:
secret-key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno
expire-length:
access: 3600000
@Slf4j
@Component
public class JwtTokenProvider {
private static final String AUTHORIZATION_ID = "id";
private final Key signingKey; // (1)
private final long validityInMilliseconds; // (2)
// (3)
public JwtTokenProvider(@Value("${security.jwt.token.secret-key}") final String signingKey,
@Value("${security.jwt.token.expire-length.access}") final long validityInMilliseconds) {
byte[] keyBytes = signingKey.getBytes(StandardCharsets.UTF_8);
this.signingKey = Keys.hmacShaKeyFor(keyBytes); // (4)
this.validityInMilliseconds = validityInMilliseconds;
}
public String createAccessToken(final Long id) {
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder() // (5)
.claim(AUTHORIZATION_ID, id)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(signingKey)
.compact();
}
}
- (1): 서버에 저장된 SecretKey를 java.base.Key라는 객체로 저장합니다.
- (2): 생성된 AccessToken의 유효시간을 가집니다.
- (3): 생성자를 통해 application.yml에서 프로퍼티를 가져옵니다.
- (4): jjwt-api의 keys를 통해 지정된 키 바이트 배열을 기반으로 HMAC-SHA 알고리즘에 사용할 새 SecretKey 인스턴스를 생성합니다. 이때 지정되는 알고리즘의 종류로는 매개인자에 넘어온 키의 길이에 따라
SHA-512
,SHA-384
,SHA-256
중에 하나로 결정됩니다. - (5): JwtBuilder를 통해 실제 Token을 발행합니다.
setIssuedAt
: 토큰 발행 시간(현재시간)setExpiration
: 토큰 파기 시간(현재시간 + 토큰 유지 시간)signWith
: 서버에 존재하는 개인키로 Signature를 만듭니다.compact
: 토큰 발행
발행된 토큰은 TokenResponse라는 응답 객체에 감싸지고, Response Body에 담겨져 클라이언트에게 응답하게 됩니다.
사용자의 로그인이 1시간뒤면 무조건 파기되므로, 이를 방지하기 위해 AccessToken보다 유효시간이 훨씬 긴 RefreshToken을 만들기도 합니다. RefreshToken은 액세스 토큰의 유효시간이 지났고, 아직 RefreshToken의 유효시간이 남아있다면 사용자의 액세스 토큰을 새로 만들어 클라이언트에게 응답하게 됩니다.
이떄 Refresh Token은 DB에 저장하여 보관해야 합니다. 성능적인 이점을 얻기 위해 여기서 Redis를 도입해 Refresh Token을 캐싱해 사용하기도 합니다!(잘은 모르지만..)
Postman을 통해 회원가입 및 로그인을 하게되면 아래 JWT 토큰을 응답받게 됩니다.
jwt.io에 생성된 토큰값과 서버의 개인키를 붙여넣기하여 결과를 보면 Signature Verified
로 인증된 토큰임을 확인할 수 있습니다.
3. 내 정보 조회
내 정보 조회 요구사항
- 기능 사항
- 사용자가 요청을 보낼 때 Header에 JWT 토큰을 넘기도록 한다.
- 응답값에는
id
,이메일
,회원 가입 시간
이 포함되어야 한다.
- 검증 사항
- Header에 JWT 토큰이 담겨있지 않다면 에러로 응답한다.
- Header에 담겨있는 JWT 토큰이 올바르지 않거나 조작되었다면 에러로 응답한다.
- Header에 담겨있는 JWT 토큰의 만료기간이 지났다면 에러로 응답한다.
내 자신의 정보를 조회하는건 기본적으로 로그인된 사용자만이 조회할 수 있습니다. 현재 로그인은 JWT로 구현했으므로, 로그인이 필요한 인증에서 클라이언트는 서버에서 받은 토큰을 헤더에 포함시켜서 요청을 보내야 합니다.
현재 프로젝트는 클라이언트를 구현하고 있지 않으므로, 클라이언트가 요청 헤더에 토큰값을 같이 보내준다고 가정하고 진행합니다.
내 정보 조회 요청부터 응답까지의 흐름 분석
로그인과 마찬가지로 내 정보 조회 요청부터 응답까지의 흐름을 분석해보겠습니다.
사용자의 로그인
: 선행조건으로 반드시 로그인 한 상태여야 한다.
내 정보 조회 요청
:Authorization
이라는 헤더에 "Bearer " prefix가 붙은 생성된 토큰값을 넣어서 내 정보 조회 요청을 합니다.토큰 유효성 확인
: 토큰이 없거나, 올바르지 않거나(조작), 유효시간이 지난 토큰의 경우와 같이 다양한 상황에서 토큰의 유효성이 보장되지 않을 수 있기 때문에 Handler에 가기전에 토큰의 유효성을 검증해야 합니다.Payload에서 사용자 id값 꺼내기
: 올바른 토큰임이 보장됐다면 토큰을 서버의 개인키로 복호화하여 id값을 꺼내어 handler에게 전달해야 합니다.사용자 id를 통해 DB조회
: id값은 PK이므로 해당 값을 이용해 DB조회를 합니다.응답 객체 만들고 응답 완료
: 모든 상황이 정상적이었다면 패스워드와 같은 민감정보는 제외하고 클라이언트에게 내 정보에 대한 응답을 넘겨줍니다.
Spring Security를 사용하고 있지 않으므로, 수동으로 토큰에 대한 유효성과, Payload에서 사용자의 id값을 꺼내야 합니다.
토큰에 대한 유효성 검사
우선 토큰에 대한 유효성 검증으로는 Spring Interceptor를 이용했습니다.
인터셉터의 preHandle()메서드는 디스패처 서블릿 이후, 요청 핸들러 호출 이전에 실행되므로 토큰을 검증하기 적합하고, 더 나아가 ControllerAdvice를 통해 인터셉터에서 발생한 예외를 공통적으로 처리할 수 있습니다. 또한 내 정보 조회 뿐만아니라 여러곳에서 인증/인가에 대한 작업이 추가될 수 있으므로 공통적인 인터페이스를 둘 수 있습니다.
토큰에서 id값 꺼내오기
토큰에서 id값을 꺼내는 과정역시 여러곳에서 사용될 수 있습니다. (내가 쓴 게시글 조회, 내 댓글 조회 등) 따라서 ArgumentResolver를 이용해 공통적으로 사용할 수 있는 인터페이스를 두었습니다.
커스텀한 애노테이션이 붙은 Argument라면 해당 resolver를 호출하도록 하는 방식으로 사용됩니다.
추가적으로 Interceptor의 preHandle 호출 이후 실제 handler가 호출되기전에 ArgumentResolver가 호출되므로 ArgumentResolver까지 로직이 진행됐다면 이미 인증/인가를 마친 요청이라고 판단되므로, ArgumentResolver에서 별도의 검증 작업은 수행하지 않았습니다.
우선 인터셉터와 ArgumentResolver는 다음과 같이 WebMvcConfig에 등록합니다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private static final String ALLOWED_METHOD_NAMES = "GET,HEAD,POST,DELETE,TRACE,OPTIONS,PATCH,PUT";
private final AuthInterceptor authInterceptor;
private final JwtTokenProvider jwtTokenProvider;
public WebMvcConfig(final AuthInterceptor authInterceptor, final JwtTokenProvider jwtTokenProvider) {
this.authInterceptor = authInterceptor;
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods(ALLOWED_METHOD_NAMES.split(","));
}
// 인터셉터 적용 및 적용 uri, 제외 uri설정
@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/members/me")
.excludePathPatterns("/login")
.excludePathPatterns("/members/signup")
.excludePathPatterns("/posts/**");
}
// ArgumentResolver 등록
@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authenticationPrincipalArgumentResolver());
}
// Bean 수동 등록
@Bean
public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() {
return new AuthenticationPrincipalArgumentResolver(jwtTokenProvider);
}
}
AuthInterceptor
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final JwtTokenProvider jwtTokenProvider; //(1)
public AuthInterceptor(final JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response,
final Object handler) {
if (CorsUtils.isPreFlightRequest(request)) { // (2)
return true;
}
if (notExistHeader(request)) { // (3)
throw new JwtException(String.format("인증을 위한 헤더를 찾을 수 없습니다.:%d", NO_AUTHORIZATION_HEADER.value()));
}
if (JwtTokenExtractor.extractAccessToken(request) == null) { // (4)
throw new JwtException(String.format("헤더의 포멧이 일치하지 않습니다.:%d", INVALID_HEADER_FORMAT.value()));
}
jwtTokenProvider.validateToken(JwtTokenExtractor.extractAccessToken(request)); // (5)
return true;
}
private boolean notExistHeader(final HttpServletRequest request) {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
return Objects.isNull(authorizationHeader);
}
}
- (1): JwtTokenProvider에 대한 의존성을 주입받는다.
- (2): CORS 정책에 의거해 만약 들어오는 요청이 Preflight 요청이라면 통과된다.
- (3): Authorization 헤더가 존재하는지의 여부를 확인한다.
- (4): 토큰 extractor를 통해 헤더가 "Bearer "로 시작하는지 검사한다.(따로 설명하진 않겠습니다.)
- (5): JwtTokenProvider에게 토큰이 유효한지 검사한다.
// JwtTokenProvider.validateToken()
public void validateToken(final String token) {
try {
Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(token);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token : {}", token);
throw new JwtException(String.format("지원하지 않는 JWT 토큰 형식:%d", UNSUPPORTED_JWT.value()));
} catch (ExpiredJwtException e) {
log.info("Expired JWT token : {}", token);
throw new JwtException(String.format("토큰 기한 만료:%d", EXPIRED_JWT.value()));
} catch (MalformedJwtException e) {
log.info("Invalid JWT token : {}", token);
throw new JwtException(String.format("유효하지 않은 JWT 토큰:%d", MALFORMED_JWT.value()));
} catch (SignatureException e) {
log.info("Invalid JWT signature : {}", token);
throw new JwtException(String.format("잘못된 JWT 시그니처:%d", INVALID_SIGNATURE.value()));
}
}
위의 로직은 예외에 작성된 로직을 살펴보면 금방 이해할 수 있습니다. parseClaimJws(...)메서드는 내부적으로 아래와 같은 이미 정의되어 있는 jwt 예외를 발생시키므로 적절하게 이용하면 됩니다.
또한 발생되는 예외에 대한 적절한 처리를 할 수 있도록 모두 JwtException으로 예외를 다시 던져주고 있으며 ControllerAdvice에서는 다음과 같이 예외를 처리하고 있습니다.
@ExceptionHandler(JwtException.class)
public ResponseEntity<ErrorResponse> handleJwtException(final JwtException e) {
String[] messageAndErrorCode = e.getMessage().split(":");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ErrorResponse.builder()
.message(messageAndErrorCode[0])
.errorCode(Integer.parseInt(messageAndErrorCode[1]))
.build()
);
}
AuthenticationPrincipalArgumentResolver
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtTokenProvider tokenProvider;
public AuthenticationPrincipalArgumentResolver(final JwtTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
// (1)
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(Login.class);
}
// (2)
@Override
public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String token = JwtTokenExtractor.extractAccessToken(request);
return tokenProvider.getParsedAuthInfo(token);
}
}
- (1): 해당 파라미터에
@Login
애노테이션이 붙어있는지 확인하고 boolean값을 반환한다. 이를 통해 해당 ArgumentResolver가 resolving 할 수 있는 파라미터인지 판단합니다. - (2):
Authorization
헤더에서 토큰값을 꺼내고, TokenProvider를 토큰에서 id값을 뽑아 AuthInfo 객체를 받아오고 리턴합니다.
// JwtTokenProvider의 getParsedAuthInfo 메서드
public AuthInfo getParsedAuthInfo(final String token) {
try {
Jws<Claims> claimsJws = Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(token);
return AuthInfo.from(claimsJws.getBody().get(AUTHORIZATION_ID));
} catch (ExpiredJwtException e) {
return AuthInfo.from(e.getClaims().get(AUTHORIZATION_ID));
}
}
- 위 메서드에서는 token값에서 id값을 꺼내고 id를 이용해 AuthInfo 객체를 생성합니다. ArgumentResolver는 인터셉터 이후 실행된다. 따라서 간발의 차이로 토큰의 유효시간이 끝났다는 예외가 발생할 수 있지만, 이미 AuthInterceptor에서 유효한 토큰이라고 검증했으므로, 해당 예외가 발생해도 예외에서 claim을 꺼내와서 객체를 생성하고 반환합니다.
내 정보 조회 비즈니스 로직
// MemberController
@GetMapping("/members/me")
public ResponseEntity<MyInfoResponse> findMyInfo(@Login AuthInfo authInfo) { // (1)
MyInfoResponse myInfoResponse = memberService.findMyInfo(authInfo);
return ResponseEntity.ok(myInfoResponse);
}
// MemberService
public MyInfoResponse findMyInfo(final AuthInfo authInfo) {
Member member = findMemberObject(authInfo);
return MyInfoResponse.from(member); // (3)
}
// (2)
private Member findMemberObject(final AuthInfo authInfo) {
return memberRepository.findById(authInfo.getId())
.orElseThrow(MemberNotFoundException::new);
}
- (1): Custom ArgumentResolver는 @Login을 보고 Argument resolving 지원 여부를 판단하고 resolving한 Argument는 AuthInfo 객체가 됩니다.
- (2): id를 통해 회원를 조회합니다. 없다면 not found 예외가 발생합니다.
- (3): 조회한 회원에서 패스워드와 같은 민감정보는 제외하고 응답을 생성합니다.
AuthInterceptor에서 정상적으로 토큰에 대한 검증이 이루어졌고, ArgumentResolver를 통해 요청한 사용자가 누구인지 AuthInfo객체를 통해 받아왔다면 해당 유저를 db에서 조회하고 패스워드를 제외한 값을 응답한다.
마치면서
이번에 JWT를 사용할때는 Spring Security를 걷어내고 사용해봤습니다!
오직 JWT만을 사용하게 되면서 해당 기술의 동작방식과 흐름을 이해할 수 있었던 게기가 됐습니다 ㅎㅎ
또한 기존 스프링에서 제공하는 훌륭한 기술들로도 충분히 깔끔한 로직을 만들 수 있다는것을 깨달았습니다. 앞으로 새로운 기술의 도입이 필요할때, 기존의 방식으로는 구성할 수 있지 않을까란 고민을 한 번 해보게 될 것 같습니다!