-
Notifications
You must be signed in to change notification settings - Fork 0
fix: 로그아웃 로직 중복 수정 #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: 로그아웃 로직 중복 수정 #115
Changes from 25 commits
320d799
3dd250b
a261c6b
b3df814
709e1f6
150b439
0eaaa4a
8e92556
3c527fe
e5b0ce1
c9e0921
fa96985
8d8026d
0820ecd
3c50ed7
c602e4f
b04081c
166e9c0
7c046c1
9442721
f0252be
11ba9c2
6023d99
4ceb289
3055b56
32dfe9d
76a153a
e268b25
7e691cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
jaelyangChoi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - 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 | ||
|
|
||
jaelyangChoi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # 교체 배포 | ||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -32,6 +30,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class JwtFilter extends OncePerRequestFilter { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final JwtUtil jwtUtil; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final TokenService tokenService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final ObjectMapper objectMapper = new ObjectMapper(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rejectInvalidToken(response, ErrorType.INVALID_ACCESS_TOKEN); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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 |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -32,6 +33,7 @@ | |
| * 소셜 로그인/회원가입이 아닌, | ||
| * 일반 회원가입 후 /login 경로로, id, password로 로그인한 경우 (개발 용도) | ||
| */ | ||
| @Slf4j | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainLogging enabled: avoid token leakage elsewhere.
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 || trueLength 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 tokenThe 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 |
||
| @RequiredArgsConstructor | ||
| public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter { | ||
|
|
||
|
|
@@ -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)); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.