Skip to content

Commit

Permalink
feat: 중간 기능 추가 (#17)
Browse files Browse the repository at this point in the history
* feat: user 도메인 추가 [#1]

* chore: 설정 파일 properties에서 yml로 변경 [#1]

* chore: gitignore 수정 [#1]

* feat: OAuth 로 리다이렉트하는 uri 추가 [#1]

* feat: google oauth client 추가 [#1]

* test: 인증 redirect uri 생성 테스트 추가 [#1]

* refactor: 소셜 인증 redirect 메서드 이름 변경 [#1]

* feat: id token 가져오는 기능 추가 [#1]

* test: id token 가져오는 기능 테스트 코드 추가 [#1]

* feat: id token에서 유저 정보 가져오는 기능 추가 [#1]

* fix: json 매핑 오류 수정 [#1]

* test: id token에서 유저 정보 추출 테스트 코드 추가 [#1]

* refactor: id token 이미지 받는 기능 제거 및 id token 받는 dto 이름 변경 [#1]

* feat: id token의 정보로 유저 엔티티 만드는 기능 추가 [#1]

* test: id token 정보로 유저 엔티티 만드는 기능 테스트 코드 추가 [#1]

* feat: jwt 토큰 생성 및 파싱 기능 추가 [#1]

* test: jwtProvider 테스트 코드 추가 [#1]

* refactor: User 도메인 이름 Member로 변경 [#1]

* chore: jwt 의존성 추가 [#1]

* feat: 로그인 service 추가 [#1]

* chore: gitignore 수정 [#1]

* test: fixture 추가 및 로그인 service 테스트 코드 추가 [#1]

* test: 테스트 코드 fixture 로 변경 [#1]

* feat: 로그인 controller 추가 [#1]

* refactor: user 패키지 이름 member로 변경 [#1]

* feat: 유저 인증 인터셉터 기능 추가 [#1]

* test: 인증 헤더 파싱 테스트 코드 추가 [#1]

* feat: 예외 관련 처리 구체화 및 advice 추가 [#1]

* test: controller 테스트 코드 추가 [#1]

* refactor: member 도메인 이름 변경 [#3]

* refactor: 예외 구조 변경 [#3]

* fix: auth transaction 추가 [#1]

* feat: 로컬 이미지 저장 기능 추가 [#3]

* feat: 피드 생성 service 기능 추가 [#3]

* chore: 개발용 초기화 sql 추가 [#3]

* test: 피드 생성 service 테스트 코드 추가 [#3]

* fix: 이미지 저장 안되는 버그 수정 [#3]

* chore: gitignore 수정 [#3]

* feat: 피드 저장 controller 추가 [#3]

* test: controller 단위 테스트 용 super class 추가 및 fixture 수정 [#3]

* test: 이미지 업로드 단위 테스트 추가 [#3]

* chore: memberRepository 패키지 위치 변경 [#3]

* style: 안 쓰는 코드 제거 [#3]

* feat: 피드 수정 기능 추가 (#6)

* test: authControllerTest 단위 테스트로 변경

* feat: 피드 수정 service 추가

* test: 피드 관련 테스트 코드 추가

Closed #5

* feat: 팔로우 언팔로우 기능 추가 (#8)

* feat: 팔로우기능 service 추가

* test: 팔로우 service 테스트 코드 추가

* feat: 팔로우 controller 추가

* feat: 언팔로우 기능 service 추가

* test: 언팔로우 serivce 테스트 코드 추가

* feat: 팔로우/언팔로우 controller 추가

* test: 팔로우/언팔로우 controller 테스트 코드 추가

* feat: 피드 수정 기능 추가 (#9)

* test: authControllerTest 단위 테스트로 변경

* feat: 피드 수정 service 추가

* test: 피드 관련 테스트 코드 추가

* feat: 피드 수정 controller 추가

* test: 피드 수정 controller 테스트 코드 추가

* feat: 피드 삭제 기능 추가 (#11)

* feat: 피드 삭제 기능 추가

* test: 피드 삭제 테스트 코드 추가

* feat: 댓글 생성 기능 추가 (#13)

* feat: 댓글 생성 기능 service 추가

* test: 댓글 생성 service 테스트 코드 추가

* feat: 댓글 생성 controller 추가

* test: 댓글 생성 controller 테스트 코드 추가

* fix: 댓글 양방향 관계 추가 및 삭제 시 함께 삭제 되도록 수정 (#16)
  • Loading branch information
wlsh44 authored Feb 26, 2023
1 parent b636698 commit eb20581
Show file tree
Hide file tree
Showing 81 changed files with 2,999 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ out/
*.iml
out
gen

.DS_Store
src/main/resources/static
src/main/resources/application.yml
data-dev.sql
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/example/sns/SnsApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@ConfigurationPropertiesScan
@SpringBootApplication
public class SnsApplication {

Expand Down
47 changes: 47 additions & 0 deletions src/main/java/com/example/sns/auth/application/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.sns.auth.application;

import com.example.sns.auth.application.dto.OAuthUserInfoDto;
import com.example.sns.auth.infrastructure.GoogleClient;
import com.example.sns.auth.infrastructure.JwtProvider;
import com.example.sns.auth.presentation.dto.TokenResponse;
import com.example.sns.member.domain.Member;
import com.example.sns.member.domain.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.URI;

@Service
@RequiredArgsConstructor
public class AuthService {

private final MemberRepository memberRepository;
private final GoogleClient client;
private final JwtProvider jwtProvider;

public URI getAuthRedirectURI() {
return client.getAuthRedirectURI();
}

@Transactional(readOnly = true)
public TokenResponse signIn(String code) {
OAuthUserInfoDto userInfo = getUserInfo(code);
Member member = memberRepository.findBySocialId(userInfo.getSocialId())
.orElseGet(() -> signUp(userInfo));
String token = jwtProvider.createToken(member.getId());

return new TokenResponse(token);
}

private OAuthUserInfoDto getUserInfo(String code) {
String idToken = client.getIdToken(code);
return client.getUserInfo(idToken);
}

@Transactional
public Member signUp(OAuthUserInfoDto userInfo) {
Member member = Member.createUserFrom(userInfo);
return memberRepository.save(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.sns.auth.application.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class OAuthUserInfoDto {

private String name;
private String email;

@JsonProperty("sub")
private String socialId;
}
38 changes: 38 additions & 0 deletions src/main/java/com/example/sns/auth/config/AuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.sns.auth.config;

import com.example.sns.auth.presentation.AuthArgumentResolver;
import com.example.sns.auth.presentation.AuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class AuthConfig implements WebMvcConfigurer {

private final AuthProperties properties;
private final AuthInterceptor authInterceptor;

@Bean
public WebClient oauthProvider() {
return WebClient.create(properties.getTokenUrl());
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/auth/**");
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthArgumentResolver());
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/example/sns/auth/config/AuthProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.sns.auth.config;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;

import java.util.List;

@Getter
@ConstructorBinding
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "oauth.google")
public class AuthProperties {

private final String redirectUri;
private final String authUri;
private final String clientId;
private final String clientSecret;
private final String tokenUrl;
private final String grantType;
private final List<String> scopes;
}
18 changes: 18 additions & 0 deletions src/main/java/com/example/sns/auth/exception/AuthException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.sns.auth.exception;

import com.example.sns.common.exception.SnsException;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public class AuthException extends SnsException {
public static final String ERROR_MSG = "예상하지 못한 인증 에러입니다.";

public AuthException(String errorMsg, HttpStatus status) {
super(errorMsg, status);
}

public AuthException() {
super(ERROR_MSG, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.sns.auth.exception;

import org.springframework.http.HttpStatus;

public class AuthExtractException extends AuthException {

public static final String ERROR_MSG = "올바르지 않은 인증 헤더입니다.";

public AuthExtractException() {
super(ERROR_MSG, HttpStatus.UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.sns.auth.exception;

import org.springframework.http.HttpStatus;

public class ExpiredTokenException extends AuthException {

public static final String ERROR_MSG = "만료된 토큰입니다.";

public ExpiredTokenException() {
super(ERROR_MSG, HttpStatus.UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.sns.auth.exception;

import org.springframework.http.HttpStatus;

public class InvalidTokenException extends AuthException {

public static final String ERROR_MSG = "올바르지 않은 토큰입니다.";

public InvalidTokenException() {
super(ERROR_MSG, HttpStatus.UNAUTHORIZED);
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/example/sns/auth/exception/OAuthException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.sns.auth.exception;

import org.springframework.http.HttpStatus;

public class OAuthException extends AuthException {

public static final String ERROR_MSG = "OAuth 인증에 실패했습니다.";

public OAuthException() {
super(ERROR_MSG, HttpStatus.BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.example.sns.auth.infrastructure;

import com.example.sns.auth.application.dto.OAuthUserInfoDto;
import com.example.sns.auth.config.AuthProperties;
import com.example.sns.auth.exception.AuthException;
import com.example.sns.auth.exception.OAuthException;
import com.example.sns.auth.infrastructure.dto.GoogleAuthResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.nio.charset.StandardCharsets;

@Component
@RequiredArgsConstructor
public class GoogleClient {

private static final int PAYLOAD_INDEX = 1;
private static final String JWT_DELIMITER = "\\.";
private static final String SCOPE_DELIMITER = " ";
private static final String RESPONSE_TYPE = "code";

private final AuthProperties properties;
private final WebClient oauthProvider;
private final ObjectMapper objectMapper;

public URI getAuthRedirectURI() {
MultiValueMap<String, String> parameters = getRedirectParameters();
return UriComponentsBuilder
.fromUriString(properties.getAuthUri())
.queryParams(parameters)
.build()
.toUri();
}

private MultiValueMap<String, String> getRedirectParameters() {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add("client_id", properties.getClientId());
parameters.add("redirect_uri", properties.getRedirectUri());
parameters.add("response_type", RESPONSE_TYPE);
parameters.add("scope", String.join(SCOPE_DELIMITER, properties.getScopes()));
return parameters;
}

public String getIdToken(String code) {
try {
GoogleAuthResponse googleAuthResponse = oauthProvider.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.bodyValue(getTokenUriFormData(code))
.retrieve()
.bodyToMono(GoogleAuthResponse.class)
.block();

return googleAuthResponse.getIdToken();
} catch (NullPointerException e) {
throw new AuthException();
} catch (Exception e) {
throw new OAuthException();
}
}

private MultiValueMap<String, String> getTokenUriFormData(String code) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("client_id", properties.getClientId());
formData.add("client_secret", properties.getClientSecret());
formData.add("grant_type", properties.getGrantType());
formData.add("redirect_uri", properties.getRedirectUri());
formData.add("code", code);
return formData;
}

public OAuthUserInfoDto getUserInfo(String idToken) {
String payload = getPayload(idToken);
String decode = decodeBase64Payload(payload);

try {
return objectMapper.readValue(decode, OAuthUserInfoDto.class);
} catch (JsonProcessingException e) {
throw new AuthException();
}
}

private String getPayload(String idToken) {
return idToken.split(JWT_DELIMITER)[PAYLOAD_INDEX];
}

private String decodeBase64Payload(String payload) {
return new String(Base64Utils.decode(payload.getBytes()), StandardCharsets.UTF_8);
}
}
Loading

0 comments on commit eb20581

Please sign in to comment.