기존 JWT 토큰에는 email과 role 정보만 포함되어 있었습니다. 이를 통해 다른 서비스 모듈에서 FeignClient를 사용하여 userId를 가져오는 번거로움이 있었습니다.
JWT 토큰에 userId를 포함시켜, 다른 서비스 모듈에서 별도로 FeignClient를 사용하지 않고도 userId를 활용할 수 있도록 했습니다.
JWTUtil.javapackage com.zerobase.user.util;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class JWTUtil {
private final Key secretKey;
public JWTUtil(@Value("${spring.jwt.secret}") String secret) {
// HMAC-SHA256 키 생성
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
// 토큰에서 이메일을 추출하는 메서드
public String getEmail(String token) {
Claims claims = parseToken(token);
return claims.get("email", String.class);
}
// 토큰에서 userId를 추출하는 메서드
public Long getUserId(String token) {
Claims claims = parseToken(token);
return claims.get("userId", Long.class);
}
// 토큰에서 역할(Role)을 추출하는 메서드
public String getRole(String token) {
Claims claims = parseToken(token);
return claims.get("role", String.class);
}
// 토큰의 유효성을 검증하는 메서드 (예외를 던지는 방식)
public void validateToken(String token) throws JwtException {
parseToken(token);
}
// 내부적으로 토큰을 파싱하여 Claims를 얻는 메서드
private Claims parseToken(String token) throws JwtException {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
}
// JWT 생성 메서드
public String createJwt(String category, String email, String role, Long expiredMs, Long userId) {
return Jwts.builder()
.claim("category", category)
.claim("email", email)
.claim("role", role)
.claim("userId", userId)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
userId 포함: createJwt 메소드에 userId를 추가하여 JWT 토큰에 userId가 포함되도록 했습니다.getUserId 메소드 추가: userId를 Long 타입으로 추출할 수 있도록 메소드를 추가했습니다.secretKey 생성 방식 수정: Keys.hmacShaKeyFor 메소드를 사용하여 secretKey를 생성하도록 변경했습니다.LoginFilter 문제점JWT 토큰에 userId가 포함되지 않아, 다른 서비스 모듈에서 FeignClient를 통해 userId를 가져와야 했습니다. 이를 개선하기 위해 JWT 토큰에 userId를 포함시켰습니다.
LoginFilter.javapackage com.zerobase.user.jwt;
import static com.zerobase.user.dto.response.ValidErrorCode.CREATE_TOKEN_ERROR;
import static com.zerobase.user.dto.response.ValidErrorCode.ILLEGAL_ARGUMENT__ERROR;
import static com.zerobase.user.dto.response.ValidErrorCode.INTERNAL_SERVER_ERROR;
import static com.zerobase.user.dto.response.ValidErrorCode.LOGIN_FAIL_ERROR;
import static com.zerobase.user.dto.response.ValidErrorCode.USER_NOT_FOUND_ERROR;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zerobase.user.dto.request.LoginRequestDTO;
import com.zerobase.user.dto.response.LoginSuccessDTO;
import com.zerobase.user.entity.ProfileEntity;
import com.zerobase.user.entity.RefreshEntity;
import com.zerobase.user.entity.UserEntity;
import com.zerobase.user.exception.BizException;
import com.zerobase.user.repository.ProfileRepository;
import com.zerobase.user.repository.RefreshRepository;
import com.zerobase.user.repository.UserRepository;
import com.zerobase.user.util.CookieUtil;
import com.zerobase.user.util.JWTUtil;
import com.zerobase.user.util.ResponseUtil;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
// Logger 적용으로 로깅 개선
@Slf4j
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// JWTUtil 주입
private final JWTUtil jwtUtil;
private final RefreshRepository refreshRepository;
private final UserRepository userRepository;
private final ProfileRepository profileRepository;
private final CookieUtil cookieUtil;
@Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// JSON 형태로 받은 요청에서 email, password를 추출
LoginRequestDTO loginRequest = null;
try {
// JSON 데이터를 LoginRequest DTO로 변환
loginRequest = new ObjectMapper().readValue(request.getInputStream(),
LoginRequestDTO.class);
log.debug("Login attempt for user with email: {}", loginRequest.getEmail());
} catch (IOException e) {
log.error("Failed to parse login request", e);
throw new RuntimeException(e);
}
// 추출된 email과 password 사용
String email = loginRequest.getEmail();
String password = loginRequest.getPassword();
log.debug("Attempting to authenticate user with email: {}", email);
// 스프링 시큐리티에서 email과 password를 검증하기 위해서는 token에 담아야 함
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
email, password, null);
log.debug("Generated authentication token for user: {}", email);
// token에 담은 검증을 위한 AuthenticationManager로 전달
return authenticationManager.authenticate(authToken);
}
// 로그인 성공 시 실행하는 메소드 (여기서 JWT를 발급)
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authentication) {
// 유저 정보
String email = authentication.getName();
log.info("Authentication successful for user: {}", email);
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
// 유저 엔티티 조회
UserEntity userEntity = userRepository.findByEmail(email)
.orElseThrow(() -> new BizException(USER_NOT_FOUND_ERROR));
Long userEntityId = userEntity.getId();
try {
// 토큰 생성
String access = jwtUtil.createJwt("access", email, role, 1800000L, userEntityId);
String refresh = jwtUtil.createJwt("refresh", email, role, 86400000L, userEntityId);
log.info("Generated access and refresh tokens for user: {}", email);
// Refresh 토큰 저장
addRefreshEntity(email, refresh, 86400000L);
// 응답 설정
response.setHeader("access", access);
response.addCookie(cookieUtil.createCookie("refresh", refresh));
// JSON 응답 생성
Optional<ProfileEntity> optionalProfileEntity = profileRepository.findByUserId(
userEntityId);
LoginSuccessDTO loginSuccessDTO = LoginSuccessDTO.builder()
.Id(userEntityId)
.profileCheck(optionalProfileEntity.isPresent())
.build();
ResponseUtil.setJsonResponse(response, HttpServletResponse.SC_OK, loginSuccessDTO);
} catch (IOException e) {
log.error("Failed to write the response", e);
throw new BizException(ILLEGAL_ARGUMENT__ERROR);
} catch (JwtException e) { // jwtUtil.createJwt(...) 에서 발생할 수 있는 예외
log.error("Failed to create JWT token", e);
throw new BizException(CREATE_TOKEN_ERROR);
} catch (Exception e) { // 기타 모든 예외
log.error("Unexpected error during authentication", e);
throw new BizException(INTERNAL_SERVER_ERROR);
}
log.debug("Access and refresh tokens sent in response for user: {}", email);
}
private void addRefreshEntity(String email, String refresh, Long expiredMs) {
// 만료일자
Date date = new Date(System.currentTimeMillis() + expiredMs);
RefreshEntity refreshEntity = RefreshEntity.builder()
.email(email)
.refresh(refresh)
.expiration(date.toString())
.build();
refreshRepository.save(refreshEntity);
}
// 로그인 실패 시 실행하는 메소드
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed) {
log.warn("Authentication failed for user: {}", request.getParameter("email"), failed);
// 로그인 실패 시 401 응답 코드 반환
try {
// JSON 응답 생성
ResponseUtil.setJsonResponse(response, HttpServletResponse.SC_UNAUTHORIZED,
LOGIN_FAIL_ERROR);
} catch (IOException e) {
log.error("Failed to write the response", e);
}
}
}
userId 포함: createJwt 메소드에 userId를 추가하여 JWT 토큰에 userId가 포함되도록 했습니다.successfulAuthentication 메소드에서 다양한 예외를 처리하도록 try-catch 블록을 확장했습니다.LoginSuccessDTO에 userId를 포함시켰습니다.JWT 토큰에 포함된 userId를 Gateway에서 추출하여 다른 서비스 모듈로 전달함으로써, FeignClient를 사용하지 않고도 userId를 활용할 수 있도록 합니다.
JwtGatewayFilterFactory.javapackage com.zerobase.gateway.jwt;
import com.zerobase.gateway.util.JWTUtil;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class JwtGatewayFilterFactory extends
AbstractGatewayFilterFactory<JwtGatewayFilterFactory.Config> {
private final JWTUtil jwtUtil;
@Autowired
public JwtGatewayFilterFactory(JWTUtil jwtUtil) {
super(Config.class);
this.jwtUtil = jwtUtil;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String accessToken = exchange.getRequest().getHeaders().getFirst("access");
if (accessToken == null) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
try {
// 토큰 검증
jwtUtil.validateToken(accessToken);
// 이메일, 역할, userId 정보를 추출하여 헤더에 추가
String email = jwtUtil.getEmail(accessToken);
String role = jwtUtil.getRole(accessToken);
Long userId = jwtUtil.getUserId(accessToken);
// 기존 헤더를 복사하고 새로운 헤더를 추가
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
headers.add("X-User-Email", email);
headers.add("X-User-Role", role);
headers.add("X-User-Id", String.valueOf(userId));
// 새로운 ServerHttpRequestDecorator를 생성하여 헤더를 교체
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
return headers;
}
};
// 변경된 요청으로 Exchange 생성
ServerWebExchange mutatedExchange = exchange.mutate()
.request(mutatedRequest)
.build();
return chain.filter(mutatedExchange);
} catch (JwtException e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
};
}
public static class Config {
// 필요한 설정이 있으면 추가
}
}
userId 추출 및 헤더 추가: JWT 토큰에서 userId를 추출하여 X-User-Id 헤더에 추가했습니다.401 Unauthorized 응답을 반환하도록 했습니다.JWT 토큰에서 추출한 userId를 컨트롤러에서 활용하여, 별도의 FeignClient 호출 없이도 유저 정보를 처리할 수 있도록 합니다.
// 여행참여 게시글 등록
@PostMapping
public ResponseEntity<?> createPost(
@Valid @RequestBody PostDTO postDTO,
@RequestHeader("X-User-Email") String userEmail,
@RequestHeader("X-User-Id") Long userId) { // userId를 Long 타입으로 변경
log.info("email: " + userEmail);
log.info("userId: " + userId);
postService.createPost(postDTO, userEmail, userId);
return ResponseEntity.status(HttpStatus.CREATED).body(ResponseMessage.success());
}
X-User-Id 헤더 추가: 컨트롤러 메소드에서 @RequestHeader("X-User-Id")를 통해 userId를 직접 받아 사용할 수 있도록 했습니다.postService.createPost 메소드에 userId를 추가로 전달하여, 필요한 로직에서 활용할 수 있도록 했습니다.이번 포스팅에서는 JWT 토큰에 userId를 포함시켜 보다 효율적인 인증 방식을 구현하는 방법을 살펴보았습니다. 이러한 접근 방식을 통해 다른 서비스 모듈과의 통신을 간소화하고, 애플리케이션의 성능과 유지보수성을 향상시킬 수 있습니다.
userId를 포함시켜, 다른 서비스 모듈에서 FeignClient를 사용하지 않고도 유저 정보를 활용할 수 있도록 했습니다.userId를 포함하고, 예외 처리를 강화했습니다.userId를 추출하여 헤더에 추가함으로써, 다른 서비스 모듈에서 이를 활용할 수 있게 했습니다.userId를 받아 서비스 로직에 전달함으로써, 보다 효율적인 유저 정보 처리를 구현했습니다.