어제 JWT토큰 기능까지 완료했다.
그래서 로그인 → 회원여부검사 → 회원인증 시, JWT토큰 발급까지의 로직을 완료했다. 그리고 이전에 보내려던 요청 쪽으로 다시 요청을 보내게하는 것도 완료. JWT토큰을 쿠키에 달아두었기 때문에, 실제 인증이 필요한 로직에서는 쿠키만 확인하면 된다.
비회원 인증처리
오늘은 회원가입 인증이 되지 않은 사람을 처리할 것이다.

package com.example.GoldenReport.Domain;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
@Id
private String id;
private String username;
private position position;
}
ERD를 보고 Member 테이블에 username이라는 컬럼을 추가했다. password는 내가 폼로그인을 개발했다면 password도 필요하겠지만 이번에는 네이버 OAuth2 로그인이 모든 인증기능을 담당하므로 이렇게 password는 굳이 필요하지 않다.
package com.example.GoldenReport.DTO.Signup;
public class SignupRequestDTO {
private String username;
}
RequsetDTO.
username만 필요하고, 나머지는 서버에서 알아서 만들어야한다.
id는 네이버 로그인에서 가져온 식별자로 id를 삼고자한다.
이전에 GDG 플젝트랙에서는 회원가입 따로, OAuth2 로그인 따로 개발해서 나중에 둘을 연동하는 작업이 필요했는데, 이번에는 OAuth2 로그인 정보를 쿠키로 달아놓고 회원가입을 진행하고자한다.
따라서 회원가입을 하더라도 먼저 네이버 로그인이 선행되어야한다는 뜻.
} else {
try {
System.out.println("There is No Member with userId: " + userId);
Cookie cookie = new Cookie("Token", jwtFilter.generateAccessToken(userId, 300));
cookie.setMaxAge(300);
cookie.setPath("/");
response.addCookie(cookie);
response.setStatus(HttpServletResponse.SC_FOUND);
response.sendRedirect("/signup");
} catch (IOException e) {
e.printStackTrace();
}
}
이렇게, 약 5분 정도 유효시간을 갖고 있는 JWT토큰을 발급시켜 쿠키에 달아두고, /signup에서 이 코드를 뽑아서 사용할 것이다.
public ResponseEntity<HTTPResponseDTO> signup(
@ModelAttribute SignupRequestDTO signupRequestDTO,
@CookieValue(value="Authentication", required = false) String token) {
return signupService.signup(token, signupRequestDTO);
}
컨트롤러에는 @CookieValue라는 어노테이션이 있는데, value 값을 key로하는 쿠키를 찾아온다. required 변수는 만약 쿠키가 없을 때, false라면 token 값이 null이 되고, true라면 곧바로 400 값을 반환한다. 여기서는 쿠키가 없다면 OAuth2 로그인을 하지 않은 것이므로 후속처리를 위해 false로 설정
package com.example.GoldenReport.Service.LogInAndSignUp;
import com.example.GoldenReport.DTO.HTTPResponseDTO.HTTPResponseDTO;
import com.example.GoldenReport.DTO.Signup.SignupRequestDTO;
import com.example.GoldenReport.Domain.Member;
import com.example.GoldenReport.Domain.position;
import com.example.GoldenReport.Repository.MemberRepository;
import com.example.GoldenReport.Service.JWT.JWTFilter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.Optional;
@Service
public class SignupService {
MemberRepository memberRepository;
JWTFilter jwtFilter;
public SignupService(MemberRepository memberRepository,
JWTFilter jwtFilter) {
this.memberRepository = memberRepository;
this.jwtFilter = jwtFilter;
}
public ResponseEntity<HTTPResponseDTO> signup(String token, SignupRequestDTO signupRequestDTO){
try {
if (token == null) {
throw new RuntimeException("token is null");
}
String username = signupRequestDTO.getUsername();
Optional<String> decodeJWT = jwtFilter.getPlainString(token);
if(decodeJWT == null) {
throw new RuntimeException("token is null");
}
String userId = decodeJWT.get();
Member member = Member.builder()
.id(userId)
.username(username)
.position(position.USER)
.build();
memberRepository.save(member);
HTTPResponseDTO response = HTTPResponseDTO.builder()
.status(200)
.message("Signup Successful")
.build();
return ResponseEntity
.status(HttpStatus.OK)
.body(response);
} catch (RuntimeException e){
return ResponseEntity
.status(HttpStatus.FOUND)
.location(URI.create("/oauth2/authorization/naver"))
.build();
}
}
}
회원가입 로직
토큰이 없을 때, 또는 JWT 토큰해독된 값이 null일 때 런타임에러를 던져 네이버 로그인 창으로 다시 이동하도록 설정했다. 이외에는 로그인창으로 정보를 받는다.
지금보니까 약간 코드가 지저분한데 언젠가 리팩토링을 할 수 있다면 좋겠다.

밥 먹고오니까 이렇게 JWT 인증기간이 만료되어서 에러가 발생하는데, 이거는 RuntimeException으로 묶어서 네이버 OAuth2 로그인으로 리다이렉트 되도록 예외처리를 해주었다.
ExpiredJwtException 처리하기
이제 대략적으로 로그인 / 회원가입 / 인증기능에 대해서 구현이 완료되었고, 한 가지가 더 필요한데,
JWT토큰이 만료되었을 때 JWT토큰을 재발급해야한다. 만약 인증요청을 보내는데 JWT토큰이 만료되었으면 내부적으로 리프레시 토큰을 통해 새 JWT토큰을 발급받아서 쿠키에 달아놓고, 유저가 다시 요청을 보내게하면 된다.
JWT토큰이 만료될 경우의 수는 몇 가지가 있는데,
- 회원가입 시 보내는 임시 JWT토큰의 만료
- RefreshToken의 만료
- AccessToken의 만료
가 있다.
첫 번째 만료사항은 다시 OAuth2 로그인을하도록 리다이렉트 시키고, 두 번째 만료사항은 다시 OAuth2 로그인을하도록 설정하면된다.
따라서 내가 신경써야할 사항은 AccessToken의 만료고, 여기서 전역Exception 처리를 통해 JWT Access 토큰의 만료에 대해 일괄적인 처리를 할 것이다.
예전에는 그냥 전역Exception용 코드를 만들었는데, 이번에는 ResponseEntityExceptionHandler라는 클래스를 상속받아볼 예정이다.
package com.example.GoldenReport.Controller;
import com.example.GoldenReport.Domain.RefreshToken;
import com.example.GoldenReport.Repository.RefreshTokenRepository;
import com.example.GoldenReport.Service.JWT.JWTFilter;
import com.nimbusds.jwt.proc.ExpiredJWTException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.net.URI;
import java.util.Arrays;
import java.util.Optional;
/**
* AccessToken의 만료
*/
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
RequestCache requestCache = new HttpSessionRequestCache();
RefreshTokenRepository refreshTokenRepository;
JWTFilter jwtFilter;
GlobalExceptionHandler(RefreshTokenRepository refreshTokenRepository,
JWTFilter jwtFilter) {
this.refreshTokenRepository = refreshTokenRepository;
this.jwtFilter = jwtFilter;
}
@ExceptionHandler(ExpiredJWTException.class)
public ResponseEntity<?> handleException(ExpiredJWTException ex, NativeWebRequest nativeRequest) {
try {
HttpServletRequest request = nativeRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = nativeRequest.getNativeResponse(HttpServletResponse.class);
if (request == null || response == null) {
throw new RuntimeException("request or response is null");
}
SavedRequest savedRequest = requestCache.getRequest(request, response);
String refreshToken = Arrays.stream(request.getCookies())
.filter(cookie -> "Authentication".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
if (refreshToken == null) {
throw new RuntimeException("Authentication_Refresh header not found");
}
Optional<String> userId = jwtFilter.getPlainString(refreshToken);
if (userId.isEmpty()) {
throw new RuntimeException("JWT Error");
}
RefreshToken token = refreshTokenRepository.findByToken(refreshToken).get(0);
if(!token.getUserId().equals(userId.get())) {
throw new RuntimeException("Unequalized userId between refreshToken and TokenDataBase");
}
Cookie cookie = new Cookie("Authentication", jwtFilter.generateAccessToken(userId.get()));
cookie.setPath("/");
response.addCookie(cookie);
String redirectUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : "/";
return ResponseEntity
.status(HttpStatus.FOUND)
.location(URI.create(redirectUrl))
.build();
} catch (RuntimeException e) {
e.printStackTrace();
return ResponseEntity
.status(HttpStatus.FOUND)
.location(URI.create("/oauth2/authorization/naver"))
.build();
}
}
}
짰다.
코드가 좀 복잡한 편이기도하고, 다른 클래스에서 조금 중복되는 기능들도 있는데 조만간 클래스를 하나 열어서 리팩토링을 해볼까한다.
String jwtToken = jwtFilter.generateAccessToken(userId);
String refreshToken = jwtFilter.generateRefreshToken(userId);
Cookie cookie = new Cookie("Token", jwtToken);
cookie.setMaxAge(JWTType.ACCESS.getValidTime());
cookie.setPath("/");
Cookie cookie2 = new Cookie("RefreshToken", refreshToken);
cookie2.setMaxAge(JWTType.REFRESH.getValidTime());
cookie2.setPath("/");
response.addCookie(cookie);
response.addCookie(cookie2);
그래서 SuccessHadler에 RefreshToken 기능을 달아주었다.
만약 RefreshToken조차도 만료되면 즉시 RuntimeException이 발생하여 OAuth2 로그인 창으로 이동한다.
인증기능 붙여주기
이제 기존에 개발한 리뷰작성 기능과 작품검색 기능에 인증기능을 붙여주도록한다.
package com.example.GoldenReport.Service.Movie;
import com.example.GoldenReport.DTO.MovieInfo.MovieResult;
import com.example.GoldenReport.DTO.MovieInfo.TMDBGetMoviewInfo;
import com.example.GoldenReport.DTO.MovieInfo.TMDBMovieInfo;
import com.example.GoldenReport.DTO.MovieSerchResultDTO.MovieSearchRequestDTO;
import com.example.GoldenReport.DTO.MovieSerchResultDTO.MovieSearchResultDTO;
import com.example.GoldenReport.Repository.MemberRepository;
import com.example.GoldenReport.Service.JWT.JWTFilter;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Service
public class MovieSearchService {
private final RestTemplate restTemplate;
private final JWTFilter jwtFilter;
private final MemberRepository memberRepository;
@Autowired
public MovieSearchService(RestTemplate restTemplate,
JWTFilter jwtFilter,
MemberRepository memberRepository) {
this.restTemplate = restTemplate;
this.jwtFilter = jwtFilter;
this.memberRepository = memberRepository;
}
@Value("${tmdb.url.normal}")
private String tmdbUrl;
@Value("${tmdb.url.added}")
private String tmdbUrlPlus;
public ResponseEntity<?> SearchMovie(HttpServletRequest httpServletRequest,
MovieSearchRequestDTO movieSearchRequestDTO){
String accessToken = Arrays.stream(httpServletRequest.getCookies())
.filter(cookie -> "Authentication".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
if (accessToken == null) {
return ErrorHandler();
}
Optional<String> optionalUserId = jwtFilter.getPlainString(accessToken);
if (optionalUserId.isEmpty()) {
return ErrorHandler();
}
String userId = optionalUserId.get();
boolean isMember = memberRepository.existsById(userId);
if (isMember) {
return SearchMovie(movieSearchRequestDTO);
} else {
return ErrorHandler();
}
}
private ResponseEntity<MovieSearchResultDTO> SearchMovie(MovieSearchRequestDTO movieSearchRequestDTO) {
try {
int count = 0;
List<MovieResult> movieResultList = new ArrayList<>();
String query = movieSearchRequestDTO.getQuery();
String queryToURL = URLEncoder.encode(query, "UTF-8");
int page = movieSearchRequestDTO.getPage();
TMDBGetMoviewInfo getMovieFromTMDB = restTemplate.getForObject(tmdbUrl + queryToURL + tmdbUrlPlus + page,
TMDBGetMoviewInfo.class);
TMDBMovieInfo[] result = getMovieFromTMDB.getResults();
for (TMDBMovieInfo movieInfo : result) {
MovieResult movieResult = MovieResult.builder()
.index(count+1)
.movieInfo(movieInfo)
.build();
movieResultList.add(movieResult);
count++;
}
MovieSearchResultDTO movieSearchResultDTO = MovieSearchResultDTO.builder()
.code(200)
.amount(movieResultList.size())
.result(movieResultList)
.build();
return ResponseEntity.ok(movieSearchResultDTO);
} catch (UnsupportedEncodingException e){
e.printStackTrace();
MovieSearchResultDTO movieSearchResultDTO = MovieSearchResultDTO.builder()
.code(400)
.amount(0)
.build();
return ResponseEntity.badRequest().body(movieSearchResultDTO);
}
}
private ResponseEntity<?> ErrorHandler() {
return ResponseEntity
.status(HttpStatus.FOUND)
.location(URI.create("/oauth2/authorization/naver"))
.build();
}
}
작품검색기능
기존의 검색기능을 private 접근제어자로 돌리고 기존메서드를 호출하고, JWT토큰을 검사하는 새로운 메서드를 public으로 오버라이딩하여 요청이 들어왔을 때 호출하도록한다.
저 기능이 되게 여러 곳에서도 쓰이고, 기능도 좀 복잡하고해서 JWTFilter에 HttpServletRequest에서 쿠키를 빼내 JWT토큰 검사하는 기능을 JWTFilter에 넣어주고자한다.
public Optional<String> getPlainString(String token) {
return jwtProvider.decodeJWT(token);
}
public Optional<String> getPlainString(HttpServletRequest request) {
String accessToken = Arrays.stream(request.getCookies())
.filter(cookie -> "Authentication".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
if (accessToken == null) {
throw new NullPointerException("Access token is null");
}
return getPlainString(accessToken);
}
오버라이딩은 참 재밌어
암튼 이 기능을 JWTFilter에 위임하고,
public ResponseEntity<?> SearchMovie(HttpServletRequest httpServletRequest,
MovieSearchRequestDTO movieSearchRequestDTO){
try {
Optional<String> optionalUserId = jwtFilter.getPlainString(httpServletRequest);
if (optionalUserId.isEmpty()) {
throw new NullPointerException("JWT Token is Empty");
}
String userId = optionalUserId.get();
boolean isMember = memberRepository.existsById(userId);
if (isMember) {
return SearchMovie(movieSearchRequestDTO);
} else {
return ResponseEntity
.status(HttpStatus.FOUND)
.location(URI.create("/signup"))
.build();
}
} catch (NullPointerException e) {
e.printStackTrace();
return ErrorHandler();
}
}
그리고 이렇게 메서드 내용을 좀 줄일 수 있다. NPE는 그냥 터뜨리는거고, 나중에 자바 표준API의 에러를 찾아보고 바꿔줄 예정이다.
package com.example.GoldenReport.Service.Movie;
import com.example.GoldenReport.DTO.HTTPResponseDTO.HTTPResponseDTO;
import com.example.GoldenReport.DTO.ReviewDTO.ReviewRequestDTO;
import com.example.GoldenReport.Domain.Review;
import com.example.GoldenReport.Repository.MemberRepository;
import com.example.GoldenReport.Repository.ReviewRepository;
import com.example.GoldenReport.Service.JWT.JWTFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.Optional;
@Service
public class WriteReviewService {
ReviewRepository reviewRepository;
MemberRepository memberRepository;
JWTFilter jwtFilter;
public WriteReviewService(ReviewRepository reviewRepository,
JWTFilter jwtFilter,
MemberRepository memberRepository) {
this.reviewRepository = reviewRepository;
this.jwtFilter = jwtFilter;
this.memberRepository = memberRepository;
}
public ResponseEntity<?> saveReview(HttpServletRequest httpServletRequest,
ReviewRequestDTO reviewRequestDTO) {
try {
Optional<String> optionalUserId = jwtFilter.getPlainString(httpServletRequest);
if (optionalUserId.isEmpty()) {
throw new NullPointerException();
}
String userId = optionalUserId.get();
boolean isMember = memberRepository.existsById(userId);
if (isMember) {
return saveReview(reviewRequestDTO);
} else {
return ResponseEntity
.status(HttpStatus.FOUND)
.location(URI.create("/signup"))
.build();
}
} catch (NullPointerException e) {
return ErrorHandler();
}
}
private ResponseEntity<HTTPResponseDTO> saveReview(ReviewRequestDTO reviewRequestDTO) {
try {
Review review = Review.builder()
.content(reviewRequestDTO.getContent())
.media_type(reviewRequestDTO.getMedia_type())
.movieId(reviewRequestDTO.getVideo_id())
.memberId(1010101010) //로그인 기능 작성 후 추가
.build();
reviewRepository.save(review);
HTTPResponseDTO response = HTTPResponseDTO.builder()
.status(200)
.message("성공!!")
.build();
return ResponseEntity.ok(response);
} catch (Exception e) {
HTTPResponseDTO response = HTTPResponseDTO.builder()
.status(500)
.message("Internal Server Error")
.build();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
}
private ResponseEntity<?> ErrorHandler() {
return ResponseEntity
.status(HttpStatus.FOUND)
.location(URI.create("/oauth2/authorization/naver"))
.build();
}
}
이렇게...?
솔직히 코드를 줄이면 더 압출할 수 있을 것 같긴하다. NPE 검사가 다소 많은 편이기 때문에..
MemberRepository에 무언갈 추가하면 될 것 같긴하다만..
우선은 이정도로 해보고 테스트는 내일 해보도록한다.
JWT토큰이 전에 한 번 했는데도 여전히 다소 어려운 편인 것 같다.. 그래도 꾸준히 익혀놓으면 좋을 듯하다.
'개인 프로젝트 > [2026] 골든리포트!' 카테고리의 다른 글
| [골든리포트!] 7) JWT토큰 인증과정을 Spring Security 필터체인에 태워보기 (0) | 2026.02.18 |
|---|---|
| [골든리포트!] 6) 필터체인에서 JWT필터 인증 처리하기 (0) | 2026.02.13 |
| [골든리포트!] 4) 스프링 세션 설정 및 JWT토큰 발급 (0) | 2026.02.10 |
| [골든리포트!] 3) Spring Security Config로 스프링 OAuth 다루기 (0) | 2026.02.10 |
| [골든리포트!] 2) TMDB API 가져와서 리뷰 작성기능 넣기 (0) | 2026.02.05 |