프로젝트 spring gateway+jwt+redis 정리하기

·2026년 2월 25일

blog만들어보기!

목록 보기
1/1

0. Jwt


@Component
public class JwtProvider {

  private SecretKey key; //대칭키 암호화 방식
  // Key타입은 비대칭키(공개키/개인키) + 대칭키 포함

  private static final long ACCESS_TOKEN_EXPIRE  = 1000L * 60 * 60;           // 1시간
  private static final long REFRESH_TOKEN_EXPIRE = 1000L * 60 * 60 * 24 * 7;  // 7일


  public JwtProvider(@Value("${jwt.secret}") String secret) {
    this.key = Keys.hmacShaKeyFor(secret.getBytes());
    // secret문자열 -> byte 배열 전환 후 -> HMAC-SHA 알고리즘 키 객체로
    /// 나중에 비대칭키로 변경 고려하기
  }

  public String createAccessToken(Long userId, String roleName) {
    return Jwts.builder()
        .setSubject(String.valueOf(userId))
        .claim("role", roleName)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE))
        .signWith(key, SignatureAlgorithm.HS256)// 해당 알고리즘으로 서명
        .compact();
  }

  public String createRefreshToken(Long userId) {
    return Jwts.builder()
        .setSubject(String.valueOf(userId))
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE))
        .signWith(key, SignatureAlgorithm.HS256) // 해당 알고리즘으로 서명
        .compact();
  }

  public Claims parseClaims(String token) { // 페이로더 분석
    return Jwts.parserBuilder()
        .setSigningKey(key) // 검증할 키 설정, 헤더에 있는 알고리즘을 확인 먼저함. 그래서 위처럼 작성할 필요X.
        .build()
      .parseClaimsJws(token)   // 1) header/payload Base64Url 디코딩 → JSON 변환
                               // 2) header의 alg 확인
                               // 3) (header + "." + payload)를 같은 알고리즘 + key로 다시 "서명값 계산"
                               // 4) 계산한 서명값과 token의 signature 비교
                               // 5) exp 등 유효성 검사 후 통과하면 Claims 반환
        .getBody();
  }

  public boolean isValid(String token) {
    try {
      parseClaims(token);
      return true;
    } catch (JwtException | IllegalArgumentException e) {
      return false;
    }
  }

  public Long getUserId(String token) {
    return Long.parseLong(parseClaims(token).getSubject());
  }

  public String getRole(String token) {
    return parseClaims(token).get("role", String.class);
  }
}

1. 회원가입

  public TokenResponseDTO signUp(SignUpRequestDTO dto) {

    // 1. Auth DB 중복 체크
    if (localAccountRepository.existsBySignId(dto.getSignId())) {
      throw new IllegalArgumentException("이미 사용 중인 아이디입니다.");
    }
    if (localAccountRepository.existsByEmail(dto.getEmail())) {
      throw new IllegalArgumentException("이미 사용 중인 이메일입니다.");
    }

    // 2. User 서비스에 프로필 생성 요청 (gRPC) → userId 반환
    Long userId = userGrpcClient.createProfile(
        dto.getEmail(),
        dto.getUserName(),
        dto.getNickname(),
        dto.getProfileImg()
    );

    // 3. 로컬 계정 저장
    AuthLocalAccount account = new AuthLocalAccount();
    account.setUserId(userId);
    account.setSignId(dto.getSignId());
    account.setEmail(dto.getEmail());
    account.setPassword(passwordEncoder.encode(dto.getPassword()));
    account.setRoleName(RoleStatus.USER);
    localAccountRepository.save(account);

    // 4. JWT 발급
    return issueToken(userId, RoleStatus.USER.name());
  }
  
	//  토큰 발급 + Redis 저장
  private TokenResponseDTO issueToken(Long userId, String role) {
    String accessToken = jwtProvider.createAccessToken(userId, role);
    String refreshToken = jwtProvider.createRefreshToken(userId);

    redisTemplate.opsForValue().set(
        REFRESH_KEY_PREFIX + userId,
        refreshToken,
        REFRESH_EXPIRE_DAYS,
        TimeUnit.DAYS
    );

    return new TokenResponseDTO(accessToken, refreshToken, userId, role);
  }

2. login (현재 생각에는 회원가입 -> login 될수 있게 하기 위해서 위 회원가입에도 token 생성이 존재.)


@Override
  public TokenResponseDTO login(LoginRequestDTO dto) {

    // 1. signId로 계정 조회
    AuthLocalAccount account = localAccountRepository.findBySignId(dto.getSignId())
        .orElseThrow(() -> new IllegalArgumentException("아이디 또는 비밀번호가 올바르지 않습니다."));

    // 2. 비밀번호 검증
    if (!passwordEncoder.matches(dto.getPassword(), account.getPassword())) {
      throw new IllegalArgumentException("아이디 또는 비밀번호가 올바르지 않습니다.");
    }
    // 3. JWT 발급
    return issueToken(account.getUserId(), account.getRoleName().name());
  }

3. post글쓰기


 @Override
  public PostResponseDTO createPost(Long userId, PostCreateRequestDTO dto) {
    PostEntity postEntity = new PostEntity();
    postEntity.setUserId(userId);
    postEntity.setTitle(dto.getTitle());
    postEntity.setContent(dto.getContent());
    postEntity.setThumbnail(dto.getThumbnail());
    postEntity.setIsPublished(dto.getIsPublished());
    postEntity.setSeriesId(dto.getSeriesId());
    postEntity.setIsCrawled(false);

    PostEntity savedPost = postRepository.save(postEntity);
    log.info("게시글 작성 완료: postId={}, userId={}", savedPost.getPostId(), userId);

    return convertToResponseDTO(postEntity);
  }

 // 게시글 작성 (jwt 인증 필요)
  @PostMapping
  public ResponseEntity<PostResponseDTO> createPost(
      @RequestHeader("X-User-Id") Long userId,  // ← Gateway가 넣어준 userId
      @Valid @RequestBody PostCreateRequestDTO dto) {

    log.info("게시글 작성 요청: userId={}, title={}", userId, dto.getTitle());

    PostResponseDTO response = postService.createPost(userId, dto);
    return ResponseEntity.ok(response);
  }

4. config 와 filter

// 1) jwt 검증 코드
 public Claims parseClaims(String token) { // 페이로더 분석
    return Jwts.parserBuilder()
        .setSigningKey(key) // 검증할 키 설정, 헤더에 있는 알고리즘을 확인 먼저함. 그래서 위처럼 작성할 필요X.
        .build()
      .parseClaimsJws(token)   // 1) header/payload Base64Url 디코딩 → JSON 변환
                               // 2) header의 alg 확인
                               // 3) (header + "." + payload)를 같은 알고리즘 + key로 다시 "서명값 계산"
                               // 4) 계산한 서명값과 token의 signature 비교
                               // 5) exp 등 유효성 검사 후 통과하면 Claims 반환
        .getBody();
  }

  public boolean isValid(String token) {
    try {
      parseClaims(token);
      return true;
    } catch (JwtException | IllegalArgumentException e) {
      return false;
    }
  }

// 2) jwtAuthFilter
@Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    /**
     * exchange.getRequest() : 현재 요청 정보 (헤더, 경로, 메서드, 바디 등)
     * exchange.getResponse() : 현재 응답 정보 (상태코드, 헤더, 바디 등)
     * exchange.mutate() : 요청이나 응답을 새로 만들어서 수정할 때 사용
     */
    String path = exchange.getRequest().getPath().value();
    HttpMethod method = exchange.getRequest().getMethod();  // ← HTTP 메서드 가져오기

    log.debug("요청: {} {}", method, path);

    // 1. 인증 불필요 경로 -> 원본 그대로 다음 필터로 넘김
    if (isPublicPath(path,method)) {
      return chain.filter(exchange);
    }

    // 2. 공개 경로가 아닐시 Authorization 헤더에서 토큰 추출
    String token = extractToken(exchange.getRequest());

    // 3. 토큰 검증
    if (token == null || !jwtProvider.isValid(token)) {

      log.warn("인증 실패: {} {}", method, path);
      exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // 401 응답 설정
      return exchange.getResponse().setComplete(); // Mono<Void> 반환 → 요청 끝내기
    }

    // 4. userId, role 추출
    Long userId = jwtProvider.getUserId(token);
    String role = jwtProvider.getRole(token);

    log.info("인증 성공: userId={}, role={}", userId, role);

    // 5. 헤더 추가하여 내부 서비스로 전달
    /// Spring WebFlux는 대부분 불변 객체. 그렇기에 한 번 만들어지면 내용을 바꿀 수 없다.
    /// mutate() 를 호출해서 새로운 복사본 생성 -> 수정, 복사본에 변경사항을 적용.
    ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
        .header("X-User-Id", String.valueOf(userId))
        .header("X-User-Role", role)
        .build();

    return chain.filter(exchange.mutate().request(modifiedRequest).build());
  }

  private boolean isPublicPath(String path, HttpMethod method) {

    // 회원가입, 로그인, 토큰 재발급
    if (path.startsWith("/auth/signup") ||
        path.startsWith("/auth/login") ||
        path.startsWith("/auth/refresh")) {
      return true;
    }

    // 게시글 조회만 허용 (GET /blog/posts/*)
    if (path.startsWith("/blog/posts/") && HttpMethod.GET.equals(method)) {
      return true;
    }

    return false;
  }
  private String extractToken(ServerHttpRequest request) {
    String bearer = request.getHeaders().getFirst("Authorization");
    if (bearer != null && bearer.startsWith("Bearer ")) {
      return bearer.substring(7);
    }
    return null;
  }


3) JwtAuth필러를 사용하기 위한 filter
 @Bean
  public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        /**
         *RouteSpec의 주요 메서드
         * .path("/auth/**") : 요청 경로 매칭
         * .filters(f -> ...) :해당 라우트에 적용할 필터 등록
         * .id("my-route") : 라우트 ID 명시적으로 지정 (생략 가능)
         * .order(10) : 라우트 매칭 순서
         * .host : ("www.google.com") 호스트 기반 라우팅
         */

        // Auth Service (필터 적용 안 함)
        .route("auth-service", r -> r
            .path("/auth/**")
            .uri("http://localhost:1001"))
        
        // User Service (필터 적용)
        .route("user-service", r -> r
            .path("/users/**")
            .filters(f -> f.filter(jwyAuthFilter))
            .uri("http://localhost:1002"))
        
        // Blog Service (필터 적용)
        .route("blog-service", r -> r
            .path("/blog/**")
            .filters(f -> f.filter(jwyAuthFilter))
            .uri("http://localhost:1003"))
        
        .build();
  }
  • 프로젝트를 진행하면서 jwtprovider를 AuhService와 gateWay에서 공통으로 사용하니 common모듈로 분리
  • 금주 목표로 진행해야할 것 : tag(2), comments(1), like(3), bookmark(5), series(4)
profile
# h

0개의 댓글