Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
66467c4
feat: 휴면 전환, 계정 하드 삭제 스케쥴러 구현
Jungeunsun565 Aug 6, 2026
4dfa507
feat: github oauth 추가
Jungeunsun565 Aug 8, 2026
1becaf8
fix: github oauth로 MyPageServiceImpl 수정
Jungeunsun565 Aug 8, 2026
92e5d35
refactor: Service 계층 조회 메서드에 readOnly 트랜잭션 적용
Jungeunsun565 Aug 8, 2026
eb3d3e4
fix: 불필요한 이메일 주소 제거 및 FeignClient 어노테이션 수정
Jungeunsun565 Aug 8, 2026
bb9c216
Merge pull request #27 from nhnacademy-aiot3-insighton/feature/auth
Jungeunsun565 Aug 8, 2026
41c57a4
feat: BusinessException 기반 전역 예외 처리기 추가
Jungeunsun565 Aug 8, 2026
a52c55b
refactor: 컨트롤러 응답에서 ApiResponse 제거, DTO 직접 반환
Jungeunsun565 Aug 8, 2026
a0070af
fix: Feign Circuit Breaker ID의 하이픈 제거 방지 설정 추가
Jungeunsun565 Aug 8, 2026
eb4fbb6
Merge pull request #29 from nhnacademy-aiot3-insighton/feature/auth
Jungeunsun565 Aug 8, 2026
ede8d8f
refactor: HeaderAuthenticationFilter X-User-Role 헤더 사용
Jungeunsun565 Aug 9, 2026
8a4dbed
Merge pull request #30 from nhnacademy-aiot3-insighton/refactor/heade…
Jungeunsun565 Aug 9, 2026
2338e5f
test: User, UserCredential, UserRole, Oauth 엔티티 테스트 작성
Jungeunsun565 Aug 9, 2026
04bbc4b
Merge pull request #32 from nhnacademy-aiot3-insighton/test/entity
Jungeunsun565 Aug 9, 2026
2a76b47
test: UserRepository, UserRoleRepository, UserCredentialRepository, O…
Jungeunsun565 Aug 9, 2026
3f2a170
Merge pull request #34 from nhnacademy-aiot3-insighton/test/repository
Jungeunsun565 Aug 9, 2026
284b984
fix: 분산 락을 Redisson으로 교체해 스케줄러 중복 실행 방지
Jungeunsun565 Aug 12, 2026
c341f66
fix: OAuth 로그인 시 기존 사용자 lastLoginAt 미갱신 문제 수정
Jungeunsun565 Aug 12, 2026
5ec222e
fix: Resilience4j 설정이 적용되지 않던 문제 수정
Jungeunsun565 Aug 12, 2026
bec00e5
fix: rebase 후 테스트 실패 수정 (Redisson AUTH, lastLoginAt)
Jungeunsun565 Aug 12, 2026
8e216e2
fix: OAuth 이메일 검증 및 중복 처리 강화
Jungeunsun565 Aug 12, 2026
6aef4f3
fix: Dockerfile 수정
Jungeunsun565 Aug 12, 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: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ RUN mvn clean package -DskipTests -B

FROM eclipse-temurin:21-jre

ENV TZ=Asia/Seoul

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
Expand Down
23 changes: 23 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,35 @@
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

<!-- Prometheus -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.52.0</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableScheduling
public class InsightonAuthApplication {

public static void main(String[] args) {
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/nhnacademy/insightonauth/client/CoreClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.nhnacademy.insightonauth.client;

import com.nhnacademy.insightonauth.dto.core.ManagerGroupExistsResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "insighton-core")
public interface CoreClient {

@GetMapping("/internal/v1/users/{userId}/manager-groups/exists")
ManagerGroupExistsResponse existsManagerGroup(@PathVariable Long userId);
}
61 changes: 2 additions & 59 deletions src/main/java/com/nhnacademy/insightonauth/client/OauthClient.java
Original file line number Diff line number Diff line change
@@ -1,65 +1,8 @@
package com.nhnacademy.insightonauth.client;

import com.nhnacademy.insightonauth.dto.oauth.OauthUserInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;

import java.util.Map;
public interface OauthClient {

@Component
@RequiredArgsConstructor
public class OauthClient {

@Value("${oauth.google.client-id}")
private String clientId;

@Value("${oauth.google.client-secret}")
private String clientSecret;

@Value("${oauth.google.redirect-uri}")
private String redirectUri;

private final RestClient restClient = RestClient.create();

public OauthUserInfo getUserInfo(String provider, String code) {
String accessToken = requestAccessToken(code);
return requestUserInfo(accessToken);
}

private String requestAccessToken(String code) {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("code", code);
body.add("client_id", clientId);
body.add("client_secret", clientSecret);
body.add("redirect_uri", redirectUri);
body.add("grant_type", "authorization_code");

Map<String, Object> response = restClient.post()
.uri("https://oauth2.googleapis.com/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.body(Map.class);

return (String) response.get("access_token");
}

private OauthUserInfo requestUserInfo(String accessToken) {
Map<String, Object> userInfo = restClient.get()
.uri("https://www.googleapis.com/oauth2/v3/userinfo")
.header("Authorization", "Bearer " + accessToken)
.retrieve()
.body(Map.class);

return new OauthUserInfo(
(String) userInfo.get("email"),
(String) userInfo.get("name"),
(String) userInfo.get("sub")
);
}
OauthUserInfo getUserInfo(String code);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.nhnacademy.insightonauth.client;

import com.nhnacademy.insightonauth.exception.UnsupportedOAuthProviderException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@RequiredArgsConstructor
public class OauthClientResolver {
// 스프링이 bean 객체를 가져다줌
private final Map<String, OauthClient> clients;

public OauthClient resolve(String provider) {
OauthClient client = clients.get(provider + "OauthClient");
if (client == null) {
throw new UnsupportedOAuthProviderException(provider);
}
return client;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.nhnacademy.insightonauth.client.impl;

import com.nhnacademy.insightonauth.client.OauthClient;
import com.nhnacademy.insightonauth.dto.oauth.OauthUserInfo;
import com.nhnacademy.insightonauth.exception.EmailNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.util.List;
import java.util.Map;

@Component("githubOauthClient")
@RequiredArgsConstructor
public class GithubOauthClient implements OauthClient {

@Value("${oauth.github.client-id}")
private String clientId;

@Value("${oauth.github.client-secret}")
private String clientSecret;

@Value("${oauth.redirect-uri}")
private String redirectUri;

private final RestClient restClient = RestClient.builder()
.requestFactory(ClientHttpRequestFactoryBuilder.detect()
.build(ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(3))
.withReadTimeout(Duration.ofSeconds(5))))
.build();

@Override
public OauthUserInfo getUserInfo(String code) {
String accessToken = requestAccessToken(code);
return requestUserInfo(accessToken);
}

private String requestAccessToken(String code) {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("code", code);
body.add("client_id", clientId);
body.add("client_secret", clientSecret);
body.add("redirect_uri", redirectUri);

Map<String, Object> response = restClient.post()
.uri("https://github.com/login/oauth/access_token") // GitHub URL로 수정
.header("Accept", "application/json") // JSON 응답 요청 (필수)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.body(Map.class);

return (String) response.get("access_token");
}

private OauthUserInfo requestUserInfo(String accessToken) {
Map<String, Object> userInfo = restClient.get()
.uri("https://api.github.com/user") // GitHub URL로 수정
.header("Authorization", "Bearer " + accessToken)
.retrieve()
.body(Map.class);

String email = requestPrimaryEmail(accessToken);

String name = (String) userInfo.get("name");
if (name == null || name.isBlank()) {
name = (String) userInfo.get("login"); // name이 없으면 login(아이디)으로 대체
}

Object id = userInfo.get("id"); // GitHub은 sub가 아니라 id (숫자)

return new OauthUserInfo(
email,
name,
String.valueOf(id)
);
}

// 이메일 열람 불가의 경우 api를 통해서 가져와야함
private String requestPrimaryEmail(String accessToken) {
List<Map<String, Object>> emails = restClient.get()
.uri("https://api.github.com/user/emails")
.header("Authorization", "Bearer " + accessToken)
.retrieve()
.body(List.class);

// primary git에서 대표로 지정된 이메일 가져옴, verified 그 중에 인증된거 가져옴
return emails.stream()
.filter(e ->
Boolean.TRUE.equals(e.get("primary")) && Boolean.TRUE.equals(e.get("verified")))
.map(e -> (String) e.get("email"))
.findFirst()
.orElseThrow(() -> new EmailNotFoundException("GitHub 계정에서 이메일을 찾을 수 없습니다."));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.nhnacademy.insightonauth.client.impl;

import com.nhnacademy.insightonauth.client.OauthClient;
import com.nhnacademy.insightonauth.dto.oauth.OauthUserInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.util.Map;

@Component("googleOauthClient")
@RequiredArgsConstructor
public class GoogleOauthClient implements OauthClient {

@Value("${oauth.google.client-id}")
private String clientId;

@Value("${oauth.google.client-secret}")
private String clientSecret;

@Value("${oauth.redirect-uri}")
private String redirectUri;

private final RestClient restClient = RestClient.builder()
.requestFactory(ClientHttpRequestFactoryBuilder.detect()
.build(ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(3))
.withReadTimeout(Duration.ofSeconds(5))))
.build();

@Override
public OauthUserInfo getUserInfo(String code) {
String accessToken = requestAccessToken(code);
return requestUserInfo(accessToken);
}

private String requestAccessToken(String code) {
// FormHttpMessageConverter
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("code", code);
body.add("client_id", clientId);
body.add("client_secret", clientSecret);
body.add("redirect_uri", redirectUri);
body.add("grant_type", "authorization_code");

Map<String, Object> response = restClient.post()
.uri("https://oauth2.googleapis.com/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.body(Map.class);

return (String) response.get("access_token");
}

private OauthUserInfo requestUserInfo(String accessToken) {
Map<String, Object> userInfo = restClient.get()
.uri("https://www.googleapis.com/oauth2/v3/userinfo")
.header("Authorization", "Bearer " + accessToken)
.retrieve()
.body(Map.class);

return new OauthUserInfo(
(String) userInfo.get("email"),
(String) userInfo.get("name"),
(String) userInfo.get("sub")
);
}
}
Loading
Loading