Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
320d799
feat: Redis 설정
jaelyangChoi Aug 3, 2025
3dd250b
feat: 소셜로그인, 재발급 시 refresh token을 redis에 저장/삭제/조회
jaelyangChoi Aug 3, 2025
a261c6b
feat: 로그아웃, 회원 탈퇴 시 Redis에서 Refresh Token 삭제
jaelyangChoi Aug 3, 2025
b3df814
refactor: jwtFilter 중복 코드 제거
jaelyangChoi Aug 4, 2025
709e1f6
feat: 블랙리스트 토큰 여부 적용
jaelyangChoi Aug 4, 2025
150b439
feat: 블랙리스트 토큰 저장 구현
jaelyangChoi Aug 4, 2025
0eaaa4a
feat: 탈퇴 시 블랙리스트 토큰 저장 적용
jaelyangChoi Aug 4, 2025
8e92556
test: 탈퇴 시 블랙리스트 토큰 저장 적용
jaelyangChoi Aug 4, 2025
3c527fe
Merge branch 'develop' into feature/#97-redis
jaelyangChoi Aug 10, 2025
e5b0ce1
fix: 누락된 문서 추가
jaelyangChoi Aug 10, 2025
c9e0921
Merge branch 'develop' into feature/#97-redis
jaelyangChoi Aug 10, 2025
fa96985
refactor: 로그아웃, 탈퇴 시 사용 API 통일
jaelyangChoi Aug 14, 2025
8d8026d
새로ìšci: 개발계용 dockerfile 생성
jaelyangChoi Aug 17, 2025
0820ecd
Merge branch 'develop' into feature/#97-redis
jaelyangChoi Aug 17, 2025
3c50ed7
ci: 개발계 배포 스크립트
jaelyangChoi Aug 19, 2025
c602e4f
test: Redis 미기동으로 테스트 실패 방지
jaelyangChoi Aug 19, 2025
b04081c
ci: 배포 스크립트 수정
jaelyangChoi Aug 19, 2025
166e9c0
feat: RedisConfig 삭제 (Spring Boot 자동설정 사용)
jaelyangChoi Aug 19, 2025
7c046c1
test: redis mocking
jaelyangChoi Aug 21, 2025
9442721
chore: 에러코드 수정
jaelyangChoi Aug 21, 2025
f0252be
chore: 불필요 요소 삭제
jaelyangChoi Aug 21, 2025
11ba9c2
Merge branch 'develop' into feature/#97-redis
jaelyangChoi Aug 21, 2025
6023d99
fix: JWT 로그아웃 필터 유형 변경
jaelyangChoi Aug 22, 2025
4ceb289
test: JWT 로그아웃 필터 관련 테스트 코드 수정
jaelyangChoi Aug 22, 2025
3055b56
fix: 트랜잭션 전파 수정
jaelyangChoi Aug 22, 2025
32dfe9d
feat: 에러코드 추가
jaelyangChoi Aug 22, 2025
76a153a
chore: 불필요 요소 삭제
jaelyangChoi Aug 22, 2025
e268b25
Merge remote-tracking branch 'origin/develop' into feature/#97-redis
jaelyangChoi Aug 23, 2025
7e691cf
chore: 주석 수정
jaelyangChoi Aug 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Deploy To EC2 (DEV)

on:
push:
branches:
- 'feature/**'

jobs:
deploy-dev:
runs-on: ubuntu-latest
steps:
- name: Github Repository 파일 불러오기
uses: actions/checkout@v4

- name: JDK 21버전 설치
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21

- name: application.yml 파일 만들기
run: |
mkdir -p ./capturecat-core/src/main/resources
echo "${{ secrets.DEV_APPLICATION_PROPERTIES }}" > ./capturecat-core/src/main/resources/application-dev.yml

- name: 테스트 및 빌드하기 (core만 빌드)
run: ./gradlew :capturecat-core:clean :capturecat-core:build

- name: 빌드된 파일 이름 변경하기
run: mv ./capturecat-core/build/libs/capturecat-core-*.jar ./project.jar

- name: SCP로 EC2에 빌드된 파일 전송하기
uses: appleboy/[email protected]
with:
host: ${{ secrets.DEV_EC2_HOST }}
username: ${{ secrets.DEV_EC2_USERNAME }}
key: ${{ secrets.DEV_EC2_PRIVATE_KEY }}
source: project.jar
target: /home/ubuntu/capturecat-server/tobe

- name: SSH로 EC2에 접속하여 서비스 재기동
uses: appleboy/[email protected]
with:
host: ${{ secrets.DEV_EC2_HOST }}
username: ${{ secrets.DEV_EC2_USERNAME }}
key: ${{ secrets.DEV_EC2_PRIVATE_KEY }}
script_stop: true
script: |
set -e

# 준비
sudo mkdir -p /home/ubuntu/capturecat-server/current /home/ubuntu/capturecat-server/tobe
sudo chown -R ubuntu:ubuntu /home/ubuntu/capturecat-server || true

# 포트 80 선점 프로세스 종료 (있으면)
sudo fuser -k -n tcp 80 || true
sleep 3

# 교체 배포
sudo rm -rf /home/ubuntu/capturecat-server/current
mkdir /home/ubuntu/capturecat-server/current
mv /home/ubuntu/capturecat-server/tobe/project.jar /home/ubuntu/capturecat-server/current/project.jar
cd /home/ubuntu/capturecat-server/current

# JVM 옵션
# export JAVA_OPTS="-Xms512m -Xmx1024m"

# dev 프로필로 실행
sudo nohup java ${JAVA_OPTS} -jar project.jar --spring.profiles.active=dev > output.log 2>&1 &

# 실행 확인
sleep 2
if [ ! -f output.log ]; then
echo "output.log 파일이 없음! (jar 실행조차 안됨, 권한 등 문제)"
exit 1
fi
if grep -q "Permission denied" output.log; then
echo "output.log Permission denied 에러 발견, 배포 실패!"
exit 1
fi

# 최대 30초 대기하며 헬스 체크
for i in {1..30}; do
if ps -ef | grep '[p]roject.jar' > /dev/null; then
echo "프로세스 실행됨, 헬스체크 진입!"
for j in {1..30}; do
if curl -sf https://dev.capture-cat.com/health | grep -q '"status":"UP"'; then
echo "헬스체크 성공!"
break 2
fi
echo "Waiting for health... ($j sec)"
sleep 1
if [ $j -eq 30 ]; then
echo "헬스체크 실패! 배포 실패!"
tail -n 80 output.log || true
exit 1
fi
done
fi
sleep 1
if [ $i -eq 30 ]; then
echo "실행 실패!"
exit 1
fi
done

# 잔여 tobe 폴더 정리
sudo rm -rf /home/ubuntu/capturecat-server/tobe
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
@Getter
public enum ErrorCode {

S3_UPLOAD_FAILED_IO("S3 업로드 중 I/O 오류 발생"),
S3_DOWNLOAD_FAILED_SDK("S3 업로드 중 AWS SDK 오류 발생"),
S3_UPLOAD_FAILED("S3 업로드 중 오류 발생"),
S3_DOWNLOAD_FAILED("S3 다운로드 중 오류 발생"),
S3_DELETE_FAILED("S3 파일 삭제 중 오류 발생"),
LOCAL_UPLOAD_FAILED("로컬 PC 업로드 중 I/O 오류 발생"),
LOCAL_DELETE_FAILED("로컬 PC 삭제 중 I/O 오류 발생");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.capturecat.client.upload;

import java.io.IOException;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -18,7 +16,7 @@

@Slf4j
@Component
@Profile({ "dev", "prod" })
@Profile({"dev", "prod"})
@RequiredArgsConstructor
public class S3FileUploader extends AbstractFileUploader {

Expand All @@ -38,10 +36,9 @@ public String upload(MultipartFile file) {

try {
s3Client.putObject(request, RequestBody.fromBytes(file.getBytes()));
} catch (IOException e) {
throw new UploadException(ErrorCode.S3_UPLOAD_FAILED_IO, e);
} catch (SdkException e) {
throw new UploadException(ErrorCode.S3_DOWNLOAD_FAILED_SDK, e);
} catch (Exception e) {
log.error("S3FileUploader.upload error,", e);
throw new UploadException(ErrorCode.S3_UPLOAD_FAILED, e);
}

return String.join("/", s3Properties.urlPrefix(), key);
Expand All @@ -56,7 +53,8 @@ public void delete(String fileName) {
.build();
s3Client.deleteObject(deleteRequest);
} catch (SdkException e) {
throw new DeleteException(ErrorCode.S3_UPLOAD_FAILED_IO, e);
log.error("S3FileUploader.delete error = {}", e.getMessage());
throw new DeleteException(ErrorCode.S3_DELETE_FAILED, e);
}
}
}
1 change: 1 addition & 0 deletions capturecat-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.modelmapper:modelmapper:3.2.0'
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
Expand Down
2 changes: 1 addition & 1 deletion capturecat-core/src/docs/asciidoc/user.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ operation::tutorialComplete[snippets='curl-request,http-request,request-headers,
소셜 서비스 연결 해제가 모종의 이유로 실패하더라도 삭제 처리는 롤백하지 않습니다. (별도 회원 안내 필요)

==== 성공
operation::withdraw[snippets='curl-request,http-request,request-headers,http-response,response-fields']
operation::withdraw[snippets='curl-request,http-request,request-headers,request-fields,http-response,response-fields']

==== 실패
회원 탈퇴가 실패했다면 HTTP 상태 코드와 함께 <<에러-객체-형식, 에러 객체>>가 돌아옵니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@ConfigurationPropertiesScan(basePackages = "com.capturecat.core.support")
@ConfigurationPropertiesScan
@SpringBootApplication
public class CaptureCatApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@

import jakarta.validation.Valid;

import org.springframework.http.HttpHeaders;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

import com.capturecat.core.api.user.dto.UserReqDto;
import com.capturecat.core.api.user.dto.UserReqDto.JoinReqDto;
import com.capturecat.core.api.user.dto.UserReqDto.WithdrawReqDto;
import com.capturecat.core.api.user.dto.UserRespDto;
import com.capturecat.core.api.user.dto.UserRespDto.InfoRespDto;
import com.capturecat.core.api.user.dto.UserRespDto.JoinRespDto;
import com.capturecat.core.config.jwt.JwtUtil;
import com.capturecat.core.service.auth.LoginUser;
import com.capturecat.core.service.auth.TokenService;
import com.capturecat.core.service.user.UserService;
import com.capturecat.core.support.response.ApiResponse;

Expand All @@ -29,13 +31,20 @@
public class UserController {

private final UserService userService;
private final TokenService tokenService;

/**
* 일반 회원 가입 (소셜 로그인 x)
*/
@PostMapping("/join")
public ApiResponse<JoinRespDto> join(@RequestBody @Valid JoinReqDto joinReqDto, BindingResult bindingResult) {
JoinRespDto joinRespDto = userService.join(joinReqDto);
return ApiResponse.success(joinRespDto);
}

/**
* 튜토리얼(시작하기) 완료 업데이트
*/
@PostMapping("/tutorialComplete")
public ApiResponse<?> tutorialCompleted(@AuthenticationPrincipal LoginUser loginUser) {
userService.updateTutorialCompleted(loginUser);
Expand All @@ -49,9 +58,16 @@ public ApiResponse<?> tutorialCompleted(@AuthenticationPrincipal LoginUser login
* 3) 회원 및 관련 데이터 삭제
*/
@DeleteMapping("/withdraw")
public ApiResponse<?> withdraw(@AuthenticationPrincipal LoginUser loginUser,
@RequestBody @Valid WithdrawReqDto req, BindingResult bindingResult) {
String resultMessage = userService.withdraw(loginUser, req.getReason().trim());
public ApiResponse<?> withdraw(@AuthenticationPrincipal LoginUser loginUser, @RequestHeader HttpHeaders headers,
@RequestBody @Valid WithdrawReqDto reqDto, BindingResult bindingResult) {
//1. 소셜 계정 연동 해제 및 회원 정보 삭제
String resultMessage = userService.withdraw(loginUser, reqDto.getReason());

//2. Refresh Token 삭제 및 Access Token 블랙리스트 등록
String accessHeader = headers.getFirst(HttpHeaders.AUTHORIZATION);
String refreshHeader = headers.getFirst(JwtUtil.REFRESH_TOKEN_HEADER);
tokenService.revokeUserTokens(accessHeader, refreshHeader);

return ApiResponse.success(resultMessage);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
http.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.addFilterBefore(new JwtFilter(jwtUtil), JwtLoginFilter.class)
.addFilterBefore(new JwtFilter(jwtUtil, tokenService), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(
new JwtLoginFilter(authenticationManager(authenticationConfiguration), tokenService),
UsernamePasswordAuthenticationFilter.class)
.addFilterAt(new JwtLogoutFilter(tokenService), LogoutFilter.class)
.addFilterBefore(new JwtLogoutFilter(tokenService), LogoutFilter.class)
.authorizeHttpRequests(
authorizeRequests -> authorizeRequests
.requestMatchers("/health", "/docs/**", "/token/reissue", "/v1/auth/**", "/v1/user/join")
.requestMatchers("/health", "/docs/**", "/token/reissue", "/v1/auth/**", "/v1/user/join", "/logout")
.permitAll()
.anyRequest()
.hasRole("USER"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@

import com.fasterxml.jackson.databind.ObjectMapper;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import com.capturecat.core.service.auth.LoginUser;
import com.capturecat.core.service.auth.TokenService;
import com.capturecat.core.support.error.ErrorType;
import com.capturecat.core.support.response.ApiResponse;

Expand All @@ -32,6 +30,7 @@
public class JwtFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
private final TokenService tokenService;
private final ObjectMapper objectMapper = new ObjectMapper();

@Override
Expand All @@ -40,25 +39,18 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

// Authorization 헤더에서 "Bearer <token>" 추출
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);

if (authHeader == null || !authHeader.startsWith(JwtUtil.BEARER_PREFIX)) {
log.info("Authorization header missing or malformed");
log.info("Authorization header missing or malformed, url={}", request.getRequestURI());
filterChain.doFilter(request, response); // 다음 필터로 넘김 (비인증 요청 허용할 수 있음)
return;
}
String accessToken = jwtUtil.resolveToken(authHeader); // "Bearer " 이후 토큰

String accessToken = authHeader.substring(JwtUtil.BEARER_PREFIX.length()); // "Bearer " 이후 토큰

//토큰 만료 검증. 만료 시 client에 즉시 응답. client는 재발급 요청 수행.
try {
jwtUtil.isExpired(accessToken);
} catch (ExpiredJwtException | SignatureException | MalformedJwtException | IllegalArgumentException e) {
rejectInvalidToken(response, ErrorType.ACCESS_TOKEN_EXPIRED);

}

// 토큰 유형 검사
if (!jwtUtil.isAccessToken(accessToken)) {
// 토큰 유효성 검사
if (!jwtUtil.isAccessToken(accessToken)
|| !jwtUtil.isValid(accessToken)
|| tokenService.isBlacklisted(accessToken)) {
//만료 시 client에 즉시 응답. client는 재발급 요청 수행.
Comment on lines 47 to 53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix validation order to prevent NPE/JWT parsing exceptions on malformed tokens

isAccessToken(...) parses claims and will throw for empty/invalid tokens. Evaluate isValid(...) first; also guard for null/empty tokens from resolveToken(...). Otherwise a bad header can 500 the request.

- String accessToken = jwtUtil.resolveToken(authHeader); // "Bearer " 이후 토큰
+ String accessToken = jwtUtil.resolveToken(authHeader); // "Bearer " 이후 토큰
@@
- if (!jwtUtil.isAccessToken(accessToken)
-   || !jwtUtil.isValid(accessToken)
-   || tokenService.isBlacklisted(accessToken)) {
+ if (accessToken == null
+   || !jwtUtil.isValid(accessToken)
+   || !jwtUtil.isAccessToken(accessToken)
+   || tokenService.isBlacklisted(accessToken)) {
   //만료 시 client에 즉시 응답. client는 재발급 요청 수행.
   rejectInvalidToken(response, ErrorType.INVALID_ACCESS_TOKEN);
   return;
 }

Optionally import and use StringUtils.hasText(...) if you adopt the resolveToken change.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String accessToken = jwtUtil.resolveToken(authHeader); // "Bearer " 이후 토큰
String accessToken = authHeader.substring(JwtUtil.BEARER_PREFIX.length()); // "Bearer " 이후 토큰
//토큰 만료 검증. 만료 시 client에 즉시 응답. client는 재발급 요청 수행.
try {
jwtUtil.isExpired(accessToken);
} catch (ExpiredJwtException | SignatureException | MalformedJwtException | IllegalArgumentException e) {
rejectInvalidToken(response, ErrorType.ACCESS_TOKEN_EXPIRED);
}
// 토큰 유형 검사
if (!jwtUtil.isAccessToken(accessToken)) {
// 토큰 유효성 검사
if (!jwtUtil.isAccessToken(accessToken)
|| !jwtUtil.isValid(accessToken)
|| tokenService.isBlacklisted(accessToken)) {
//만료 시 client에 즉시 응답. client는 재발급 요청 수행.
String accessToken = jwtUtil.resolveToken(authHeader); // "Bearer " 이후 토큰
// 토큰 유효성 검사
if (accessToken == null
|| !jwtUtil.isValid(accessToken)
|| !jwtUtil.isAccessToken(accessToken)
|| tokenService.isBlacklisted(accessToken)) {
//만료 시 client에 즉시 응답. client는 재발급 요청 수행.
rejectInvalidToken(response, ErrorType.INVALID_ACCESS_TOKEN);
return;
}
🤖 Prompt for AI Agents
In capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtFilter.java
around lines 47 to 53, the current validation calls jwtUtil.isAccessToken(...)
first which parses claims and can throw on null/empty/malformed tokens; first
guard against null/empty accessToken returned by jwtUtil.resolveToken(...)
(e.g., use StringUtils.hasText(accessToken) or a null/empty check), then call
jwtUtil.isValid(accessToken) before jwtUtil.isAccessToken(accessToken), and only
if both checks pass perform the blacklist check; reorder the boolean sequence
and add the null/empty guard to prevent NPE/JWT parsing exceptions on malformed
tokens.

rejectInvalidToken(response, ErrorType.INVALID_ACCESS_TOKEN);
return;
}
Expand All @@ -68,7 +60,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
String role = jwtUtil.getRole(accessToken);
LoginUser loginUser = new LoginUser(username, role);
Authentication authToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());

// SecurityContext에 등록
SecurityContextHolder.getContext().setAuthentication(authToken);

Expand All @@ -81,4 +72,9 @@ private void rejectInvalidToken(HttpServletResponse response, ErrorType errorTyp
objectMapper.writeValue(response.getWriter(), ApiResponse.error(errorType));
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
return "/logout".equals(path); // 로그아웃은 스킵
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import com.capturecat.core.api.user.dto.UserReqDto.LoginReqDto;
import com.capturecat.core.domain.user.UserRole;
Expand All @@ -32,6 +33,7 @@
* 소셜 로그인/회원가입이 아닌,
* 일반 회원가입 후 /login 경로로, id, password로 로그인한 경우 (개발 용도)
*/
@Slf4j
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Logging enabled: avoid token leakage elsewhere.

  • Adding @slf4j here is fine, and your success log doesn’t include tokens, which is good.
  • Cross-check other classes (e.g., TokenService) to ensure access/refresh tokens aren’t logged at any level. Sensitive token values should never hit logs.

Scan for any logging that outputs tokens:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find log lines that might include "token" in their message
rg -nP --type=java -C1 'log\.(trace|debug|info|warn|error)\s*\(.*token' capturecat-core || true

# A few more token-specific strings commonly logged
rg -nP --type=java -C1 'Refresh Token:|Blacklist Token:' capturecat-core || true

Length of output: 1681


Critical: Remove all logging of sensitive token values

The scan confirms that several log statements are exposing raw token and secret values. These must be removed or masked immediately to prevent security risks:

• capturecat-core/src/main/java/com/capturecat/core/service/auth/SocialService.java:162

log.info("Apple /token params: {}", params.get("client_secret")); // exposes client_secret

• capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java:75

log.info("Refresh Token: {}", refreshToken); // exposes raw refresh token

• capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java:148

log.info("Blacklist Token: {}", accessToken); // exposes raw access token

The debug log on line 123 of TokenService (“Deleting refresh token for user {}”) is acceptable, since it only logs the username. All other logs that print token or secret values must be removed or replaced with non-sensitive identifiers (e.g., user IDs, token hashes). Sensitive token values should never appear in any log output.

🤖 Prompt for AI Agents
In
capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtLoginFilter.java
around line 36 and in the related files
capturecat-core/src/main/java/com/capturecat/core/service/auth/SocialService.java
line 162,
capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java
lines 75 and 148, remove any logging that prints raw tokens or client secrets;
instead log only non-sensitive identifiers (e.g., user ID, username) or a
deterministic hash/truncated fingerprint of the token if you need traceability,
and update messages to avoid including secret values; ensure no println/log
statements output the actual token or client_secret and add a short comment
noting that sensitive values must not be logged.

@RequiredArgsConstructor
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

Expand Down Expand Up @@ -64,6 +66,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR

//토큰 발급
Map<TokenType, String> tokenMap = tokenIssueService.issue(username, UserRole.fromRoleString(role));
log.info("[JwtLoginFilter.successfulAuthentication] 사용자 로그인({}), 토큰 발급", username);

//Header에 실어 응답
response.setHeader(HttpHeaders.AUTHORIZATION, JwtUtil.BEARER_PREFIX + tokenMap.get(TokenType.ACCESS));
Expand Down
Loading