Skip to content

Commit

Permalink
Merge pull request #14 from 2024-Iris/auth
Browse files Browse the repository at this point in the history
Authentication 구현 및 테스트 코드 작성
  • Loading branch information
dokkisan authored May 10, 2024
2 parents 34c0739 + bcd3363 commit 09c5b69
Show file tree
Hide file tree
Showing 28 changed files with 766 additions and 43 deletions.
2 changes: 2 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## 🚀 개요

<!---- 변경 사항 및 관련 이슈에 대해 간단하게 작성해주세요. 어떻게보다 무엇을 왜 수정했는지 설명해주세요. -->


Expand All @@ -22,6 +23,7 @@ Issue-ID: __issuefy-##__
## 🔎 상세 내용

## ✅ Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

- [ ] 커밋 메시지 컨벤션에 맞게 작성했습니다.
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.gradle
.idea/**
**/main/**/application.yml
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# issuefy spring
[![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2F2024-Iris%2Fissuefy-spring%2F&count_bg=%23948DED&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=Issuefy&edge_flat=false)](https://hits.seeyoufarm.com)


[![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2F2024-Iris%2Fissuefy-spring%2F&count_bg=%23948DED&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=Issuefy&edge_flat=false)](https://hits.seeyoufarm.com)

[![Deploy to Amazon ECS](https://github.com/2024-Iris/issuefy-spring/actions/workflows/ecs_deploy.yml/badge.svg)](https://github.com/2024-Iris/issuefy-spring/actions/workflows/ecs_deploy.yml)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=2024-Iris_issuefy-spring&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=2024-Iris_issuefy-spring)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=2024-Iris_issuefy-spring&metric=coverage)](https://sonarcloud.io/summary/new_code?id=2024-Iris_issuefy-spring)
Expand Down
19 changes: 17 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,29 @@ configurations {
}

dependencies {
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: '3.2.0'
implementation group: 'io.micrometer', name: 'micrometer-registry-prometheus', version: '1.12.4'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-webflux', version: '3.2.5'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// Monitoring
implementation group: 'io.micrometer', name: 'micrometer-registry-prometheus', version: '1.12.4'
runtimeOnly 'io.netty:netty-resolver-dns-native-macos:4.1.100.Final:osx-aarch_64'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

// REST Docs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

// Test
testImplementation group: 'com.squareup.okhttp3', name: 'mockwebserver', version: '4.12.0'
}

def jacocoDir = layout.buildDirectory.dir("reports/")
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"url": "git+https://github.com/2024-Iris/issuefy-spring.git"
},
"release": {
"branches": ["main"],
"branches": [
"main"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
Expand Down
40 changes: 25 additions & 15 deletions src/docs/asciidoc/api-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,60 @@ endif::[]
:operation-http-request-title: Example Request
:operation-http-response-title: Example Response

= Issuefy API Guide
= Issuefy API 명세서

== 1. Repositories
== 1. 리포지토리

=== 1.1 Subscribe Repository
=== 1.1 리포지토리 추가

==== HTTP Request
==== HTTP 요청

include::{snippets}/issuefy/repositories/post/http-request.adoc[]

==== Request Fields
==== 요청 필드

include::{snippets}/issuefy/repositories/post/request-fields.adoc[]

==== HTTP Response
==== HTTP 응답 예시

include::{snippets}/issuefy/repositories/post/http-response.adoc[]

==== Response Fields
==== 응답 필드

include::{snippets}/issuefy/repositories/post/response-fields.adoc[]

=== 1.2 Get Subscribed Repositories
=== 1.2 등록한 리포지토리 목록

==== HTTP Request
==== HTTP 요청

include::{snippets}/issuefy/repositories/get/http-request.adoc[]

==== HTTP Response
==== HTTP 응답

include::{snippets}/issuefy/repositories/get/http-response.adoc[]

== 2. Issues

=== 2.1 Get Issues by Subscribed Repository
=== 2.1 등록한 리포지토리의 이슈 목록

==== HTTP Request
==== HTTP 요청

include::{snippets}/issuefy/issues/get/http-request.adoc[]

==== Path Parameters
==== Path 파라미터

include::{snippets}/issuefy/issues/get/path-parameters.adoc[]

==== HTTP Response
==== HTTP 응답

include::{snippets}/issuefy/issues/get/http-response.adoc[]
include::{snippets}/issuefy/issues/get/http-response.adoc[]

== 2. 로그인

==== HTTP 요청

include::{snippets}/issuefy/oauth/login/http-request.adoc[]

==== HTTP 응답

include::{snippets}/issuefy/oauth/login/http-response.adoc[]
17 changes: 17 additions & 0 deletions src/main/java/site/iris/issuefy/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package site.iris.issuefy.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // CORS를 적용할 URL 패턴 설정
.allowedOrigins("http://localhost:3000") // 허용할 Origin 설정
.allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드 설정
.allowedHeaders("*"); // 허용할 HTTP 헤더 설정
}
}
25 changes: 25 additions & 0 deletions src/main/java/site/iris/issuefy/config/WebClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package site.iris.issuefy.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

@Bean(name = "apiWebClient")
public WebClient apiWebClient() {
return WebClient.builder()
.baseUrl("https://api.github.com/")
.build();
}

@Bean(name = "accessTokenWebClient")
public WebClient accessTokenWebClient() {
return WebClient.builder()
.baseUrl("https://github.com/")
.build();
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package site.iris.issuefy.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import site.iris.issuefy.dto.OauthResponse;
import site.iris.issuefy.service.AuthenticationService;
import site.iris.issuefy.vo.UserDto;

@Slf4j
@RestController
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authenticationService;

@GetMapping("/api/login")
public ResponseEntity<OauthResponse> login(@RequestParam String code) {
UserDto userDto = authenticationService.githubLogin(code);
String tempJWT = "RETURNTESTJWT";
return ResponseEntity.ok()
.body(OauthResponse.of(userDto.getLogin(), userDto.getAvatar_url(), tempJWT));
}
}
16 changes: 16 additions & 0 deletions src/main/java/site/iris/issuefy/dto/OauthResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package site.iris.issuefy.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class OauthResponse {
private String userName;
private String avatarURL;
private String JWT;

public static OauthResponse of(String userName, String avatarURL, String JWT) {
return new OauthResponse(userName, avatarURL, JWT);
}
}
18 changes: 18 additions & 0 deletions src/main/java/site/iris/issuefy/entity/Jwt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package site.iris.issuefy.entity;

import lombok.Getter;

@Getter
public class Jwt {
private final String accessToken;
private final String refreshToken;

private Jwt(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}

public static Jwt of(String accessToken, String refreshToken) {
return new Jwt(accessToken, refreshToken);
}
}
7 changes: 7 additions & 0 deletions src/main/java/site/iris/issuefy/entity/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package site.iris.issuefy.entity;

public class User {
private Long id;
private String nickname;
private String avatarUrl;
}
13 changes: 13 additions & 0 deletions src/main/java/site/iris/issuefy/repository/UserRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package site.iris.issuefy.repository;

import org.springframework.stereotype.Repository;

import site.iris.issuefy.entity.User;

@Repository
public class UserRepository {

public User findByNickname(String nickname) {
return new User();
}
}
78 changes: 78 additions & 0 deletions src/main/java/site/iris/issuefy/service/AuthenticationService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package site.iris.issuefy.service;

import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import lombok.extern.slf4j.Slf4j;
import site.iris.issuefy.vo.OauthDto;
import site.iris.issuefy.vo.UserDto;

@Slf4j
@Service
public class AuthenticationService {

private static final int KEY_INDEX = 0;
private static final int VALUE_INDEX = 1;
private static final int REQUIRE_SIZE = 2;
private final GithubAccessTokenService githubAccessTokenService;
private final WebClient webClient;

// 2개의 WebClient Bean중에서 apiWebClient Bean을 사용하기 위해 생성자를 만들었습니다.
@Autowired
public AuthenticationService(GithubAccessTokenService githubAccessTokenService,
@Qualifier("apiWebClient") WebClient webClient) {
this.githubAccessTokenService = githubAccessTokenService;
this.webClient = webClient;
}

public UserDto githubLogin(String code) {
String accessToken = githubAccessTokenService.getToken(code);
log.info("accessToken : {}", accessToken);

OauthDto oauthDto = parseOauthDto(accessToken);
log.info(oauthDto.toString());

return getUserInfo(oauthDto);
}

private OauthDto parseOauthDto(String accessToken) {
ConcurrentMap<String, String> responseMap = new ConcurrentHashMap<>();
String[] pair = accessToken.split("&");

for (String pairStr : pair) {
String[] keyValue = pairStr.split("=");
if (keyValue.length == REQUIRE_SIZE) {
responseMap.put(keyValue[KEY_INDEX], keyValue[VALUE_INDEX]);
} else {
// null 값 대신 빈 문자열
responseMap.put(keyValue[KEY_INDEX], "");
}
}

// 필수 키가 없으면 예외 발생
if (!responseMap.containsKey("access_token") || !responseMap.containsKey("token_type")) {
throw new IllegalArgumentException("Response does not contain all required keys");
}

return OauthDto.fromMap(responseMap);
}

private UserDto getUserInfo(OauthDto oauthDto) {
return webClient.get()
.uri("/user")
.headers(headers -> {
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setBearerAuth(oauthDto.getAccess_token());
})
.retrieve()
.bodyToMono(UserDto.class)
.block();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package site.iris.issuefy.service;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;

@Service
public class GithubAccessTokenService {

private final WebClient webClient;
@Value("${github.client-secret}")
private String clientSecret;
@Value("${github.client-id}")
private String clientId;

public GithubAccessTokenService(@Qualifier("accessTokenWebClient") WebClient webClient) {
this.webClient = webClient;
}

public String getToken(String code) {
return webClient.post()
.uri("/login/oauth/access_token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData("client_id", clientId)
.with("client_secret", clientSecret)
.with("code", code))
.retrieve()
.bodyToMono(String.class)
.block();
}
}
Loading

0 comments on commit 09c5b69

Please sign in to comment.