Skip to content
Merged
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
3ee9754
Remove: 실제 DB 연결 테스트 삭제
GitJIHO Aug 1, 2025
44acc94
Feat: AuthUser 도메인 설정
GitJIHO Aug 1, 2025
0fa2cf7
Feat: Token 도메인 설정
GitJIHO Aug 1, 2025
f583c2a
Feat: OAuthMapping 도메인 설정
GitJIHO Aug 1, 2025
81ffc7b
Feat: DeviceType 도메인 설정
GitJIHO Aug 1, 2025
f279114
Feat: OAuthProvider 도메인 설정
GitJIHO Aug 1, 2025
10278f8
Feat: OAuthToken 도메인 설정
GitJIHO Aug 1, 2025
66d6c94
Feat: OAuthUserInfo 도메인 설정
GitJIHO Aug 1, 2025
cea90f4
Feat: TokenType 도메인 설정
GitJIHO Aug 1, 2025
b8b5f0b
Feat: OAuth관련 Exceptions 설정
GitJIHO Aug 1, 2025
92b4d84
Feat: Token 관련 Exceptions 설정
GitJIHO Aug 1, 2025
5a896e7
Feat: OAuthMapping 도메인을 저장할 Entity 설정
GitJIHO Aug 1, 2025
ab1b8bf
Feat: Token repository 인터페이스 구성
GitJIHO Aug 1, 2025
5f957ff
Feat: OAuth repository 인터페이스 구성
GitJIHO Aug 1, 2025
05e8c10
Feat: OAuthMapping용 JPA를 상속받은 인터페이스 구성
GitJIHO Aug 1, 2025
3d99d51
Feat: OauthRepository 구현체 구현
GitJIHO Aug 1, 2025
05af223
Feat: Token 저장용 Redis를 활용한 구현체 구현
GitJIHO Aug 1, 2025
9119306
Feat: 계층간의 소통을 위한 Auth관련 DTO 구현
GitJIHO Aug 1, 2025
0ac7ec2
Feat: Jwt 비즈니스 로직 중 예외 처리를 위한 Validator 구현
GitJIHO Aug 1, 2025
3ede311
Feat: Jwt를 생성하기 위한 Factory 구현
GitJIHO Aug 1, 2025
cf38f82
Feat: Jwt의 핵심 비즈니스 로직 구현
GitJIHO Aug 1, 2025
a02aff0
Feat: OAuth의 비즈니스 로직 확장성을 위한 인터페이스 구성
GitJIHO Aug 1, 2025
ea2616e
Feat: OAuth중 KAKAO관련 구현체 구현
GitJIHO Aug 1, 2025
0970f25
Feat: Kakao 로직에 필요한 dto 구현
GitJIHO Aug 1, 2025
cb576b9
Feat: OauthLogin을 위한 핵심 비즈니스 로직 구현
GitJIHO Aug 1, 2025
5488fff
Feat: @Login 어노테이션 설정
GitJIHO Aug 1, 2025
5c1f7d5
Feat: 인증 부분적 활용을 위한 @RequireAuth 어노테이션 구현
GitJIHO Aug 1, 2025
c40576a
Feat: presentation단의 인증 활용을 위한 리졸버 구현
GitJIHO Aug 1, 2025
c2df5ff
Feat: DispatcherServlet단 이후 인터셉트하여 인증로직을 처리할 인터셉터 구현
GitJIHO Aug 1, 2025
212f362
Feat: OAuth관련 엔드포인트 swagger 설정을 위한 인터페이스 구성
GitJIHO Aug 1, 2025
9c82d31
Feat: OAuthApi 컨트롤러 구현체 구현
GitJIHO Aug 1, 2025
0d07490
Chore: Redis 설정 안정성과 유동성을 확보한 방향으로 리팩터링
GitJIHO Aug 1, 2025
f63bae7
Feat: RestTemplate Bean 주입
GitJIHO Aug 1, 2025
a5f537b
Feat: 에러 global 처리용 advice 구현
GitJIHO Aug 1, 2025
c71f1cf
Feat: USER 도메인용 인터페이스 구성
GitJIHO Aug 1, 2025
3d2013e
Feat: USER 도메인 구현체 구현
GitJIHO Aug 1, 2025
bb3bf28
Feat: UserStatus 도메인 구성
GitJIHO Aug 1, 2025
5537be2
Feat: User 저장용 Entity 구현
GitJIHO Aug 1, 2025
31c446d
Feat: User상태 저장용 enum 설정
GitJIHO Aug 1, 2025
1fdd4cd
Feat: 유저용 exception 설정
GitJIHO Aug 1, 2025
828d2fb
Feat: 비즈니스 로직에서 활용한 User 저장용 인터페이스 구성
GitJIHO Aug 1, 2025
7d80160
Feat: User repository를 위한 jpa repository 상속받은 인터페이스 구성
GitJIHO Aug 1, 2025
be31e09
Feat: UserRepository 구현체 구현
GitJIHO Aug 1, 2025
806a328
Feat: User단에서의 계층간 소통을 위한 dto 구현
GitJIHO Aug 1, 2025
06f7f5c
Feat: User 생성용 Factory 구현
GitJIHO Aug 1, 2025
1987b6d
Feat: User용 핵심 비즈니스 로직 구현
GitJIHO Aug 1, 2025
b382b76
Feat: User용 presentation 단에서 사용할 swagger및 rest 설정 인터페이스 구성
GitJIHO Aug 1, 2025
16619f4
Feat: 유저용 controller 엔드포인트 구현
GitJIHO Aug 1, 2025
010f6ad
Chore: gradle 의존성 정리
GitJIHO Aug 1, 2025
894fb86
Refactor: 자동 리팩터링
GitJIHO Aug 1, 2025
84a98f6
Test: MySQL 테스트 컨테이너 설정
GitJIHO Aug 1, 2025
c053693
Test: Redis 테스트 컨테이너 설정
GitJIHO Aug 1, 2025
80c9122
Test: MySQL의 테스트 컨테이너를 primary로 테스트 환경에서 설정
GitJIHO Aug 1, 2025
224d409
Test: Redis의 테스트 컨테이너를 primary로 테스트 환경에서 설정
GitJIHO Aug 1, 2025
bc9b442
Test: Redis 및 MySQL primary bean 설정용 TestConfig 구성
GitJIHO Aug 1, 2025
98aa638
Test: gitignore된 환경변수들을 테스트환경에서 임의의 값으로 사용할 수 있도록 구성
GitJIHO Aug 1, 2025
b64fadd
Test: 테스트 환경에서 테스트컨테이너로 띄운 Redis와 MySQL의 정상 connection 여부 테스트
GitJIHO Aug 1, 2025
3be685a
Test: SpringBootTest를 활용한 통합 테스트들 진행
GitJIHO Aug 1, 2025
7a5283a
Test: DataJpaTest를 활용한 db 테스트 진행
GitJIHO Aug 1, 2025
30dac83
Test: Auth의 비즈니스 로직 유닛 테스트
GitJIHO Aug 1, 2025
9c03bf6
Test: Auth의 Domain단 유닛 테스트
GitJIHO Aug 1, 2025
528ad02
Test: Interceptor 테스트
GitJIHO Aug 1, 2025
794c9fd
Test: Auth의 인프라 repository단 유닛 테스트
GitJIHO Aug 1, 2025
7c16c66
Test: Auth의 엔티티 유닛테스트
GitJIHO Aug 1, 2025
1f49e3e
Test: Resolver 테스트
GitJIHO Aug 1, 2025
4c133ad
Test: User의 비즈니스 로직 유닛 테스트
GitJIHO Aug 1, 2025
06bb025
Test: User의 도메인단 유닛 테스트
GitJIHO Aug 1, 2025
bfcc5f4
Chore: PR_TEST단에서의 임시 컨테이너 제거 및 환경변수 임시 추가 제거
GitJIHO Aug 1, 2025
afdc166
Chore: Deploy 워크플로우에서 build중 test에 활용될 임시 env 제거
GitJIHO Aug 1, 2025
39719ac
Fix: UserController UserApi의 구현체로 수정
GitJIHO Aug 2, 2025
313892b
Chore: entity Builder의 접근제한자 public, dto Builder의 접근제한자 protected로 변경
GitJIHO Aug 2, 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
9 changes: 0 additions & 9 deletions .github/workflows/dev_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,6 @@ jobs:

- name: 🏗️ Build with Gradle
run: ./gradlew build
env:
REDIS_HOST: ${{ secrets.REDIS_HOST }}
REDIS_PORT: ${{ secrets.REDIS_PORT }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
MYSQL_HOST: ${{ secrets.MYSQL_HOST }}
MYSQL_PORT: ${{ secrets.MYSQL_PORT }}
MYSQL_USER: ${{ secrets.MYSQL_USER }}
MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }}
MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }}

- name: Log in to Docker Hub
uses: docker/login-action@v2
Expand Down
25 changes: 0 additions & 25 deletions .github/workflows/pr_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,6 @@ jobs:
test:
runs-on: ubuntu-latest

services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ddip_test
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h localhost"
--health-interval=5s
--health-timeout=5s
--health-retries=10

steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -37,15 +21,6 @@ jobs:

- name: Run Gradle Tests
run: ./gradlew test jacocoTestReport
env:
MYSQL_HOST: localhost
MYSQL_PORT: 3306
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_DATABASE: ddip_test
REDIS_HOST: ${{ secrets.REDIS_HOST }}
REDIS_PORT: ${{ secrets.REDIS_PORT }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}

- name: Write coverage comments on PR
uses: madrapps/jacoco-report@v1.6.1
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ dependencies {
// Database & Redis
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.23.1'
implementation 'org.springframework.session:spring-session-data-redis'

// Lombok
Expand All @@ -51,6 +50,8 @@ dependencies {

// Test Dependencies
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.testcontainers:testcontainers:1.20.5'
testImplementation 'org.testcontainers:junit-jupiter:1.20.5'

// monitoring
implementation 'io.micrometer:micrometer-registry-prometheus'
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/knu/ddip/DdipApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
@SpringBootApplication
public class DdipApplication {

public static void main(String[] args) {
SpringApplication.run(DdipApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(DdipApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.knu.ddip.auth.business.dto;

public record JwtRefreshRequest(
String refreshToken,
String deviceType
) {
}
7 changes: 7 additions & 0 deletions src/main/java/com/knu/ddip/auth/business/dto/JwtResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.knu.ddip.auth.business.dto;

public record JwtResponse(
String accessToken,
String refreshToken
) {
}
16 changes: 16 additions & 0 deletions src/main/java/com/knu/ddip/auth/business/dto/OAuthContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.knu.ddip.auth.business.dto;

import com.knu.ddip.auth.domain.DeviceType;
import com.knu.ddip.auth.domain.OAuthProvider;
import lombok.AccessLevel;
import lombok.Builder;

@Builder(access = AccessLevel.PRIVATE)
public record OAuthContext(OAuthProvider oAuthProvider, DeviceType deviceType) {
public static OAuthContext of(OAuthProvider oAuthProvider, DeviceType deviceType) {
return OAuthContext.builder()
.oAuthProvider(oAuthProvider)
.deviceType(deviceType)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.knu.ddip.auth.business.dto;

import lombok.AccessLevel;
import lombok.Builder;

import java.util.UUID;

@Builder(access = AccessLevel.PROTECTED)
public record OAuthLoginResponse(
String accessToken,
String refreshToken,
UUID OAuthMappingEntityId,
boolean needRegister
) {
public static OAuthLoginResponse toJwt(String accessToken, String refreshToken) {
return OAuthLoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.needRegister(false)
.build();
}

public static OAuthLoginResponse toSignUp(UUID OAuthMappingEntityId) {
return OAuthLoginResponse.builder()
.OAuthMappingEntityId(OAuthMappingEntityId)
.needRegister(true)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.knu.ddip.auth.business.dto;

import com.knu.ddip.auth.domain.OAuthProvider;
import com.knu.ddip.auth.domain.OAuthToken;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;

import java.util.UUID;

@Getter
@Builder(access = AccessLevel.PROTECTED)
public class OAuthMappingEntityDto {
private final UUID id;
private final String socialUserId;
private final String socialUserEmail;
private final String socialUserName;
private final OAuthProvider provider;
private final UUID userId;
private final OAuthToken oauthToken;
private final boolean temporary;

public static OAuthMappingEntityDto create(UUID id, String providerId, String providerEmail,
String providerName, OAuthProvider provider,
UUID userId, OAuthToken oauthToken, boolean temporary) {
return OAuthMappingEntityDto.builder()
.id(id)
.socialUserId(providerId)
.socialUserEmail(providerEmail)
.socialUserName(providerName)
.provider(provider)
.userId(userId)
.oauthToken(oauthToken)
.temporary(temporary)
.build();
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/knu/ddip/auth/business/dto/OAuthTokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.knu.ddip.auth.business.dto;

import com.knu.ddip.auth.domain.OAuthToken;
import lombok.AccessLevel;
import lombok.Builder;

@Builder(access = AccessLevel.PRIVATE)
public record OAuthTokenDto(
String accessToken,
String refreshToken,
long expiresIn) {

public static OAuthTokenDto from(OAuthToken oAuthToken) {
Copy link
Contributor

Choose a reason for hiding this comment

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

혹시 from을 사용하신 이유가 있으신가요? 제 생각엔 from은 입력값에 가공이나 변환이 필요한 경우에 사용하고 of는 그대로 입력값을 받아 생성할 때 사용하는게 자연스럽다 생각하는데 혹시 어떻게 생각하시나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

저는 from은 단일 인자값을 기반으로, of는 여러 인자값을 기반으로 static 생성자를 구성할 때 사용하는걸로 이해하여 사용했습니다. 기헌님같은 경우에는 위 코드의 경우 of로 구성하는것이 좀 더 자연스럽다고 생각하시는걸까요?

Copy link
Contributor

Choose a reason for hiding this comment

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

아하 넵 그렇군요

https://docs.oracle.com/javase/tutorial/datetime/overview/naming.html

자바 네이밍 컨벤션에 이런 기준이 있긴 한데 또 다른 분들 의견을 참고해보니 적절하다 생각하는 네이밍을 사용하는 것이 더 중요한 것 같긴 하네요!

그냥 의미상 자연스러운 정적 펙토리 메서드로 만들어 쓰면 될 것 같습니다~!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

아하 좋은 자료 감사합니다 😄

return OAuthTokenDto.builder()
.accessToken(oAuthToken.getAccessToken())
.refreshToken(oAuthToken.getRefreshToken())
.expiresIn(oAuthToken.getExpiresIn())
.build();
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/knu/ddip/auth/business/dto/TokenDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.knu.ddip.auth.business.dto;

import lombok.AccessLevel;
import lombok.Builder;

@Builder(access = AccessLevel.PRIVATE)
public record TokenDTO(
String value
) {
public static TokenDTO from(String value) {
return TokenDTO.builder().value(value).build();
}
}
72 changes: 72 additions & 0 deletions src/main/java/com/knu/ddip/auth/business/service/JwtFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.knu.ddip.auth.business.service;

import com.knu.ddip.auth.domain.Token;
import com.knu.ddip.auth.domain.TokenType;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;

@Component
public class JwtFactory {
public static final long REFRESH_TOKEN_VALIDITY_MILLISECONDS = 14 * 24 * 60 * 60 * 1000;
private static final long ACCESS_TOKEN_VALIDITY_MILLISECONDS = 30 * 60 * 1000;
private final SecretKey secretKey;

public JwtFactory(@Value("${SECRET_KEY}") String secret) {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes());
}

public Token createAccessToken(UUID userId) {
return createToken(userId, TokenType.ACCESS, ACCESS_TOKEN_VALIDITY_MILLISECONDS);
}

public Token createRefreshToken(UUID userId) {
return createToken(userId, TokenType.REFRESH, REFRESH_TOKEN_VALIDITY_MILLISECONDS);
}

private Token createToken(UUID userId, TokenType tokenType, long validityInMilliseconds) {
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);

String tokenValue = Jwts.builder()
.subject(String.valueOf(userId))
.issuedAt(now)
.expiration(validity)
.claim("type", tokenType.name())
.signWith(secretKey)
.compact();

return Token.of(tokenType, tokenValue, String.valueOf(userId), now, validity);
}

public Optional<Token> parseToken(String tokenValue) {
if (tokenValue == null || tokenValue.isEmpty()) {
return Optional.empty();
}

try {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(tokenValue)
.getPayload();

String subject = claims.getSubject();
Date issuedAt = claims.getIssuedAt();
Date expiration = claims.getExpiration();
String tokenTypeStr = claims.get("type", String.class);
TokenType tokenType = TokenType.valueOf(tokenTypeStr);

return Optional.of(Token.of(tokenType, tokenValue, subject, issuedAt, expiration));
} catch (Exception e) {
return Optional.empty();
}
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/knu/ddip/auth/business/service/JwtService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.knu.ddip.auth.business.service;

import com.knu.ddip.auth.business.dto.JwtRefreshRequest;
import com.knu.ddip.auth.business.dto.JwtResponse;
import com.knu.ddip.auth.business.dto.TokenDTO;
import com.knu.ddip.auth.business.validator.JwtValidator;
import com.knu.ddip.auth.domain.Token;
import com.knu.ddip.auth.exception.TokenBadRequestException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
@RequiredArgsConstructor
public class JwtService {
private final JwtFactory jwtFactory;
private final TokenRepository tokenRepository;
private final JwtValidator JWTValidator;

public JwtResponse refreshAccessToken(JwtRefreshRequest request) {
String refreshTokenValue = request.refreshToken();
String deviceType = request.deviceType();

Token refreshToken = jwtFactory.parseToken(refreshTokenValue)
.orElseThrow(() -> new TokenBadRequestException("유효하지 않은 토큰입니다."));

UUID userId = UUID.fromString(refreshToken.getSubject());

JWTValidator.validateRefreshToken(refreshToken, userId, deviceType);

Token newAccessToken = jwtFactory.createAccessToken(userId);
Token newRefreshToken = jwtFactory.createRefreshToken(userId);

TokenDTO newRefreshTokenDTO = newRefreshToken.toTokenDTO();

tokenRepository.saveToken(userId, deviceType, newRefreshTokenDTO);
tokenRepository.updateLastRefreshTime(userId, deviceType);

return new JwtResponse(newAccessToken.getValue(), newRefreshToken.getValue());
}

public void logout(UUID userId, String deviceType) {
tokenRepository.removeToken(userId, deviceType);
}
}
Loading
Loading