Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8c40eea
feat: RefreshToken 저장소를 Redis에서 MySQL로 마이그레이션
bbbang105 Jan 20, 2026
93a1839
feat: 토큰 재발급 API에 원자적 업데이트 및 토큰 검증 추가
bbbang105 Jan 20, 2026
800f5af
feat: ClientInfoExtractor 유틸 추가 및 토큰에 IP/UserAgent 저장
bbbang105 Jan 20, 2026
cab83dc
feat: 회원 탈퇴 시 RefreshToken revoke 처리
bbbang105 Jan 20, 2026
b6450c4
refactor: JwtUtil에 expiryAt 계산 메서드 추출
bbbang105 Jan 20, 2026
0baaa04
[feat] : RefreshToken Cleanup Scheduler 추가
bbbang105 Jan 20, 2026
1f0d42c
[test] : DB 통합 테스트를 위한 Testcontainers 설정 추가
bbbang105 Jan 20, 2026
edc350b
[test] : RefreshToken 관련 테스트 코드 추가
bbbang105 Jan 20, 2026
548c462
[ci] : commit-labeler에 ci 라벨 추가
bbbang105 Jan 20, 2026
8debc82
[ci] : prod-cicd PR 머지 시에만 실행되도록 수정
bbbang105 Jan 20, 2026
f9c2d21
[chore] : open-api JSON 파일 gitignore 처리
bbbang105 Jan 20, 2026
d98a809
[ci] : PR Auto Assign 워크플로우 추가
bbbang105 Jan 20, 2026
4a881a4
[chore] : CODEOWNERS에 팀원 추가
bbbang105 Jan 20, 2026
bbafaa0
[chore] : static/docs 디렉토리 gitkeep 추가
bbbang105 Jan 20, 2026
49bcdb4
fix: RefreshToken 관련 @Transactional 누락 수정
bbbang105 Jan 20, 2026
7324607
fix: QueryDSL bulk update 시 updatedDate 수동 갱신 추가
bbbang105 Jan 20, 2026
ca59967
fix: 토큰 재발급 API에서 JwtFilter 제외
bbbang105 Jan 20, 2026
a3f7aba
fix: 로그아웃 API에서 JwtFilter 제외
bbbang105 Jan 20, 2026
2b8bafc
refactor: LocalDateTime.now() 중복 호출 제거
bbbang105 Jan 21, 2026
c2412b3
fix: TokenService에 validateToken() 추가
bbbang105 Jan 21, 2026
9bad834
chore: 미사용 DistributedLock 관련 코드 제거
bbbang105 Jan 21, 2026
4209ea6
fix: redisson 의존성 제거
bbbang105 Jan 21, 2026
6663547
feat: 로컬 yaml db 설정 변경
bbbang105 Jan 21, 2026
6171a67
fix: local 프로파일 JPA 설정 개선
bbbang105 Jan 21, 2026
e1c5eb7
chore: 최신 버전을 사용한다
bbbang105 Jan 21, 2026
cb0d50e
fix: gitkeep을 제거하지 않도록 변경한다
bbbang105 Jan 21, 2026
69be202
Merge branch 'develop' into feature/#315/refresh-token
bbbang105 Jan 22, 2026
b4d4643
fix: conflict를 해결한다
bbbang105 Jan 22, 2026
a3d22c0
fix: 슬래시를 제거한다
bbbang105 Jan 22, 2026
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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @bbbang105
* @bbbang105 @anxi01
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
addReviewers: true
addAssignees: author
reviewers:
- bbang105
- bbbang105
- anxi01
15 changes: 15 additions & 0 deletions .github/workflows/auto-assign.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Auto Assign

on:
pull_request:
types: [opened, ready_for_review]

jobs:
assign:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: kentaro-m/auto-assign-action@v2.0.1
with:
configuration-path: '.github/auto-assign-config.yaml'
23 changes: 13 additions & 10 deletions .github/workflows/commit-labeler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ jobs:

for (const c of commits.data) {
const msg = c.commit.message.toLowerCase();
if (msg.includes("[feat]")) labels.add("🚀 feat");
if (msg.includes("[fix]")) labels.add("🚨 fix");
if (msg.includes("[docs]")) labels.add("📄 docs");
if (msg.includes("[style]")) labels.add("🌱 style");
if (msg.includes("[refactor]")) labels.add("🔄 refactor");
if (msg.includes("[chore]")) labels.add("⚒️ chore");
if (msg.includes("[hotfix]")) labels.add("🛟 hotfix");
if (msg.includes("[release]")) labels.add("💫 release");
if (msg.includes("[rename]")) labels.add("🎫 rename");
if (msg.includes("[remove]")) labels.add("✂️ remove");
if (msg.includes("feat")) labels.add("🚀 feat");
if (msg.includes("fix")) labels.add("🚨 fix");
if (msg.includes("docs")) labels.add("📄 docs");
if (msg.includes("style")) labels.add("🌱 style");
if (msg.includes("refactor")) labels.add("🔄 refactor");
if (msg.includes("test")) labels.add("✅ test");
if (msg.includes("perf")) labels.add("⚡️ perf");
if (msg.includes("chore")) labels.add("⚒️ chore");
if (msg.includes("ci")) labels.add("🔧 ci");
if (msg.includes("hotfix")) labels.add("🛟 hotfix");
if (msg.includes("release")) labels.add("💫 release");
if (msg.includes("rename")) labels.add("🎫 rename");
if (msg.includes("remove")) labels.add("✂️ remove");
}

if (labels.size > 0) {
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/prod-cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ permissions:
jobs:
build-and-push:
name: Build and Push to ECR
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
environment: prod
steps:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ out/
### Claude Code ###
.claude
/docs

### Auto-generated files ###
src/main/resources/static/docs/open-api-3.0.1.json
10 changes: 6 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ dependencies {
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.46.0'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down Expand Up @@ -94,6 +91,12 @@ dependencies {

// Jsoup
implementation 'org.jsoup:jsoup:1.21.2'

// Testcontainers
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:mysql'
}

// QueryDSL 디렉토리
Expand Down Expand Up @@ -132,7 +135,6 @@ openapi3 {
tasks.withType(GenerateSwaggerUI).configureEach {
dependsOn 'openapi3'

delete file('src/main/resources/static/docs/')
copy {
from "build/resources/main/static/docs"
into "src/main/resources/static/docs/"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package side.onetime.auth.handler;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import side.onetime.auth.dto.GoogleUserInfo;
import side.onetime.auth.dto.KakaoUserInfo;
import side.onetime.auth.dto.NaverUserInfo;
Expand All @@ -17,13 +25,9 @@
import side.onetime.domain.User;
import side.onetime.repository.RefreshTokenRepository;
import side.onetime.repository.UserRepository;
import side.onetime.util.ClientInfoExtractor;
import side.onetime.util.JwtUtil;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;

@Slf4j
@Component
@RequiredArgsConstructor
Expand All @@ -38,6 +42,7 @@ public class OAuthLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHand
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final ClientInfoExtractor clientInfoExtractor;

/**
* OAuth2 인증 성공 처리 메서드.
Expand Down Expand Up @@ -141,12 +146,27 @@ private void handleNewUser(HttpServletRequest request, HttpServletResponse respo
private void handleExistingUser(HttpServletRequest request, HttpServletResponse response, User user) throws IOException {
Long userId = user.getId();
String browserId = jwtUtil.hashUserAgent(request.getHeader("User-Agent"));
String userIp = clientInfoExtractor.extractClientIp(request);
String userAgent = clientInfoExtractor.extractUserAgent(request);

// 기존 브라우저의 ACTIVE 토큰 revoke
refreshTokenRepository.revokeByUserIdAndBrowserId(userId, browserId);

// 새 토큰 생성
String jti = UUID.randomUUID().toString();
String accessToken = jwtUtil.generateAccessToken(userId, "USER");
String refreshTokenValue = jwtUtil.generateRefreshToken(userId, browserId, jti);

LocalDateTime now = LocalDateTime.now();
LocalDateTime expiryAt = jwtUtil.calculateRefreshTokenExpiryAt(now);

String accessToken = jwtUtil.generateAccessToken(userId,"USER");
String refreshToken = jwtUtil.generateRefreshToken(userId, browserId);
refreshTokenRepository.save(new RefreshToken(userId, browserId, refreshToken));
RefreshToken refreshToken = RefreshToken.create(
userId, jti, browserId, refreshTokenValue,
now, expiryAt, userIp, userAgent
);
refreshTokenRepository.save(refreshToken);

String redirectUri = String.format(ACCESS_TOKEN_REDIRECT_URI, "true", accessToken, refreshToken);
String redirectUri = String.format(ACCESS_TOKEN_REDIRECT_URI, "true", accessToken, refreshTokenValue);
getRedirectStrategy().sendRedirect(request, response, redirectUri);
}
}
12 changes: 8 additions & 4 deletions src/main/java/side/onetime/controller/TokenController.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package side.onetime.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import side.onetime.dto.token.request.ReissueTokenRequest;
import side.onetime.dto.token.response.ReissueTokenResponse;
import side.onetime.global.common.ApiResponse;
Expand All @@ -24,13 +26,15 @@ public class TokenController {
* 액세스 토큰 재발행 API.
*
* @param reissueAccessTokenRequest 리프레쉬 토큰을 포함한 요청 객체
* @param httpRequest HttpServletRequest (IP, User-Agent 추출용)
* @return 재발행된 액세스 토큰과 리프레쉬 토큰을 포함하는 응답 객체
*/
@PostMapping("/action-reissue")
public ResponseEntity<ApiResponse<ReissueTokenResponse>> reissueToken(
@Valid @RequestBody ReissueTokenRequest reissueAccessTokenRequest) {
@Valid @RequestBody ReissueTokenRequest reissueAccessTokenRequest,
HttpServletRequest httpRequest) {

ReissueTokenResponse reissueTokenResponse = tokenService.reissueToken(reissueAccessTokenRequest);
ReissueTokenResponse reissueTokenResponse = tokenService.reissueToken(reissueAccessTokenRequest, httpRequest);
return ApiResponse.onSuccess(SuccessStatus._REISSUE_TOKENS, reissueTokenResponse);
}
}
32 changes: 26 additions & 6 deletions src/main/java/side/onetime/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
package side.onetime.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import side.onetime.domain.enums.GuideType;
import side.onetime.dto.user.request.*;
import side.onetime.dto.user.response.*;
import side.onetime.dto.user.request.CreateGuideViewLogRequest;
import side.onetime.dto.user.request.LogoutUserRequest;
import side.onetime.dto.user.request.OnboardUserRequest;
import side.onetime.dto.user.request.UpdateUserPolicyAgreementRequest;
import side.onetime.dto.user.request.UpdateUserProfileRequest;
import side.onetime.dto.user.request.UpdateUserSleepTimeRequest;
import side.onetime.dto.user.response.GetGuideViewLogResponse;
import side.onetime.dto.user.response.GetUserPolicyAgreementResponse;
import side.onetime.dto.user.response.GetUserProfileResponse;
import side.onetime.dto.user.response.GetUserSleepTimeResponse;
import side.onetime.dto.user.response.OnboardUserResponse;
import side.onetime.global.common.ApiResponse;
import side.onetime.global.common.status.SuccessStatus;
import side.onetime.service.UserService;
Expand All @@ -28,9 +47,10 @@ public class UserController {
*/
@PostMapping("/onboarding")
public ResponseEntity<ApiResponse<OnboardUserResponse>> onboardUser(
@Valid @RequestBody OnboardUserRequest onboardUserRequest) {
@Valid @RequestBody OnboardUserRequest onboardUserRequest,
HttpServletRequest httpRequest) {

OnboardUserResponse onboardUserResponse = userService.onboardUser(onboardUserRequest);
OnboardUserResponse onboardUserResponse = userService.onboardUser(onboardUserRequest, httpRequest);
return ApiResponse.onSuccess(SuccessStatus._ONBOARD_USER, onboardUserResponse);
}

Expand Down
Loading
Loading