Skip to content

Commit

Permalink
feat: OAuth2 로그인 구현 (#5)
Browse files Browse the repository at this point in the history
* docs: 카카오 oauth2 로그인 설정(application.yml)

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: AuthController 리팩터링

- 기존의 토큰 로그인 방식에서 OAuth 로그인으로 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: AuthService 리팩터링

- 기존의 토큰 로그인 방식에서 OAuth 로그인으로 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: OAuth 기능 구현(kakao)

- 기존의 토큰 로그인 방식에서 OAuth 로그인으로 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 불필요한 로직 제거

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: SignupRequest를 LoginRequest로 리팩터링

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 인증 서버 이름을 PathVariable로 받던 방식을 body로 받도록 리팩터링

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: AuthService에서 사용하지 않는 NicknameGenerator 필드 제거

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 기본 생성자를 어노테이션으로 대체

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: OAuthAdapter 메서드의 역할 분리

- OAuthProperties 내부 클래스 이름 변경 (Client -> User) (yml 설정 파일과 일치 시키기 위한 변경)

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 클래스 이름 변경

- InMemoryProviderRepository -> OAuthProviderRepository

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: OAuth 로그인에 필요한 통신 로직 추상화

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 개행 추가 및 final 키워드 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* remove: 불필요해진 클래스 제거

- OAuthTokenResponse

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: AuthService 리팩터링

- 불필요한 개행 삭제
- 메서드 분리

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 메서드 네이밍 변경

- getAccessToken -> getAccessTokenResponse

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 메서드 네이밍 수정 및 Json타입을 객체 타입으로 파싱하기 위한 Response 재생성

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* move: OAuth 설정 파일 경로 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: Member 로직과 Auth 로직을 분리

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: 어노테이션으로 현재 OAuth 인증기관의 정보를 가져오는 기능 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: OAuthConfig 파일 수정

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* move: 파일 구조 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: OAuthProperties 리팩터링

- 기존의 불필요했던 구조를 최적화

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* remove: 불필요해진 클래스 제거

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 기존의 id로 토큰을 만드는 방식을 이메일로 만들도록 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: email, nickname 정보로 member를 생성하는 메서드 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: post 요청의 바디를 여러번 읽기 위한 설정 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: RestTemplate 설정 추가 및 리팩터링

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 인증기관에서 받아오는 정보 변경으로 인한 리팩터링

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: 인증 기관에 대한 모든 요청을 담당하는 클래스 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: Json 타입을 변환해주는 mapper 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 컨트롤러 리팩터링

- 로그인 메서드 파라미터에 @OAuthAuthority 적용

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* docs: oauth2 구조 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* rename: 클래스명 변경

- RegisteredEvent -> ValidatedLoginEvent

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: Json 데이터의 key 값으로 value를 찾아 반환하는 메서드 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: event 관련 설정을 담당하는 클래스를 만들어 역할 분리

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: OAuth 기관들을 관리하는 enum 클래스 생성

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: request 메세지의 관한 처리를 담당하는 클래스 생성

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 기존의 http 요청 메세지 관련 작업을 RequestProcessor에서 하도록 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 설정 파일에서 사용하던 Setter 제거 및 인증 기관을 enum 타입으로 바인딩하도록 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: OAuthProvider 클래스를 레코드로 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 파일 구조 변경으로 인한 import 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 불필요해진 필드와 메서드 제거

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 설정 파일에 값을 손쉽게 바인딩하기 위해 @ConfigurationPropertiesScan을 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* remove: 불필요해진 클래스 제거

- OAuthTokenResponse

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* move: 파일 계층 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* feat: 유효하지 않은 Json 데이터에 대한 예외처리 기능 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 커스텀한 예외를 던지도록 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* test: 불필요한 테스트 클래스 제거

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* docs: 설정 파일 수정

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* test: 테스트 클래스 경로 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* test: 프로덕션 코드 수정으로 인한 테스트 코드 변경

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* test: KakaoOAuthRequesterTest 테스트 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* test: MemberServiceTest 테스트 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* test: 테스트를 쉽게 하기 위해 Fake 클래스 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* test: 테스트를 쉽게 하기 위해 Fixture 클래스 추가

- OAuthProviderFixture

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* test: OAuthPropertiesTest 테스트 추가

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>

* refactor: 계층 변경

* refactor: 계층 변경

* remove: 불필요해진 클래스 제거

* refactor: 중복되는 메서드 제거

* feat: 회원 정보의 keyWord를 담는 DTO 클래스 생성

* refactor: 코트 최적화 및 네이밍 수

* docs: google 소셜 로그인 설정 추가

* test: 테스트 설정 파일 수정

* test: 프로덕션 코드 리팩터링으로 인한 테스트 코드 수정

* docs: 로그인 api 문서화

* remove: 불필요해진 클래스 제거

* refactor: 공백 제거 및 final 키워드 추가

* refactor: 토큰 만드는 메서드 네이밍 변경 및 @Getter 제거

* refactor: 기존에 모호하게 작성했던 변수명을 구체적으로 변경

- a, b -> parentPath, childPath

* feat: 등록된 소셜 로그인 기관 중 해당 요청의 기관을 찾을 수 없을 때 발생시킬 예외 클래스 생성

* refactor: 등록된 소셜 기관이 없을 때 OAuthPlatformNotFountException를 던지도록 수정

* test: OAuthPlatformTest 테스트 클래스 추가

* docs: oauth2 client-secret 키 암호화

---------

Co-authored-by: devholic22 <hyunjoon.tech@gmail.com>
Co-authored-by: sosow0212 <sosow0212@naver.com>
  • Loading branch information
3 people authored Feb 10, 2024
1 parent 3df4a84 commit 8928b38
Show file tree
Hide file tree
Showing 57 changed files with 813 additions and 332 deletions.
25 changes: 25 additions & 0 deletions src/docs/asciidoc/member.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
:toc: left
:source-highlighter: highlightjs
:sectlinks:
:toclevels: 2
:sectlinks:
:sectnums:

== Member

=== 로그인

==== 요청

include::{snippets}/auth-controller-web-mvc-test/do_signup/http-request.adoc[]
include::{snippets}/auth-controller-web-mvc-test/do_signup/request-fields.adoc[]


==== 응답
include::{snippets}/auth-controller-web-mvc-test/do_signup/http-response.adoc[]
include::{snippets}/auth-controller-web-mvc-test/do_signup/request-fields.adoc[]





2 changes: 2 additions & 0 deletions src/main/java/com/atwoz/AtwozApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

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;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableJpaAuditing
@EnableAsync
@SpringBootApplication
@ConfigurationPropertiesScan
public class AtwozApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

public class CorsCustomFilter extends OncePerRequestFilter {

Expand All @@ -19,6 +20,10 @@ protected void doFilterInternal(final HttpServletRequest request,
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "*");
filterChain.doFilter(request, response);
ContentCachingRequestWrapper contentCachingRequestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper contentCachingResponseWrapper = new ContentCachingResponseWrapper(response);

filterChain.doFilter(contentCachingRequestWrapper, contentCachingResponseWrapper);
contentCachingResponseWrapper.copyBodyToResponse();
}
}
44 changes: 9 additions & 35 deletions src/main/java/com/atwoz/member/application/auth/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@

import com.atwoz.global.event.Events;
import com.atwoz.member.application.auth.dto.LoginRequest;
import com.atwoz.member.application.auth.dto.SignupRequest;
import com.atwoz.member.domain.auth.RegisteredEvent;
import com.atwoz.member.application.event.ValidatedLoginEvent;
import com.atwoz.member.domain.auth.TokenProvider;
import com.atwoz.member.domain.member.Member;
import com.atwoz.member.domain.member.MemberRepository;
import com.atwoz.member.domain.member.NicknameGenerator;
import com.atwoz.member.exception.exceptions.member.MemberAlreadyExistedException;
import com.atwoz.member.exception.exceptions.member.MemberNotFoundException;
import com.atwoz.member.infrastructure.auth.dto.MemberInfoResponse;
import com.atwoz.member.infrastructure.auth.dto.OAuthProviderRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -18,37 +14,15 @@
@Service
public class AuthService {

private final MemberRepository memberRepository;
private final TokenProvider tokenProvider;
private final NicknameGenerator nicknameGenerator;
private final OAuthRequester oAuthRequester;

@Transactional
public String signup(final SignupRequest request) {
validateExistedMember(request.email());
public String login(final LoginRequest request, final OAuthProviderRequest provider) {
String accessToken = oAuthRequester.getAccessToken(request.code(), provider);
MemberInfoResponse memberInfoResponse = oAuthRequester.getMemberInfo(accessToken, provider);
Events.raise(new ValidatedLoginEvent(memberInfoResponse.email(), memberInfoResponse.name()));

Member member = Member.createDefaultRole(request.email(), request.password(), nicknameGenerator);
Member signupMember = memberRepository.save(member);
Events.raise(new RegisteredEvent(member.getId(), member.getEmail(), member.getNickname()));

return tokenProvider.create(signupMember.getId());
}

private void validateExistedMember(final String email) {
if (memberRepository.existsByEmail(email)) {
throw new MemberAlreadyExistedException();
}
}

@Transactional(readOnly = true)
public String login(final LoginRequest request) {
Member member = findMemberByEmail(request.email());
member.validatePassword(request.password());

return tokenProvider.create(member.getId());
}

private Member findMemberByEmail(final String email) {
return memberRepository.findByEmail(email)
.orElseThrow(MemberNotFoundException::new);
return tokenProvider.createTokenWith(memberInfoResponse.email());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.atwoz.member.application.auth;

import com.atwoz.member.domain.auth.JsonMapper;
import com.atwoz.member.domain.auth.OAuthConnectionManager;
import com.atwoz.member.infrastructure.auth.dto.MemberInfoResponse;
import com.atwoz.member.infrastructure.auth.dto.OAuthProviderRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class OAuth2Requester implements OAuthRequester {

private static final String KEY = "access_token";

private final OAuthConnectionManager oAuthConnectionManager;
private final JsonMapper jsonMapper;

@Override
public String getAccessToken(final String code, final OAuthProviderRequest provider) {
String accessTokenResponse = oAuthConnectionManager.getAccessTokenResponse(provider, code);
return jsonMapper.getValueByKey(accessTokenResponse, KEY);
}

@Override
public MemberInfoResponse getMemberInfo(final String accessToken, final OAuthProviderRequest oAuthProviderRequest) {
String memberInfoResponse = oAuthConnectionManager.getMemberInfoResponse(accessToken,
oAuthProviderRequest.userInfoUri());

return jsonMapper.extractMemberInfoFrom(memberInfoResponse, oAuthProviderRequest.memberInfoKeyWordRequest());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.atwoz.member.application.auth;

import com.atwoz.member.infrastructure.auth.dto.MemberInfoResponse;
import com.atwoz.member.infrastructure.auth.dto.OAuthProviderRequest;

public interface OAuthRequester {

String getAccessToken(final String code, final OAuthProviderRequest provider);

MemberInfoResponse getMemberInfo(final String accessToken, final OAuthProviderRequest oAuthProviderRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import jakarta.validation.constraints.NotBlank;

public record LoginRequest(
@NotBlank(message = "이메일을 입력해주세요.")
String email,
@NotBlank(message = "인증 서버가 정해지지 않았습니다.")
String provider,

@NotBlank(message = "패스워드를 입력해주세요.")
String password
@NotBlank(message = "인증 코드가 비었습니다.")
String code
) {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.atwoz.member.domain.auth;
package com.atwoz.member.application.event;

import com.atwoz.global.event.Event;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class RegisteredEvent extends Event {
public class ValidatedLoginEvent extends Event {

private final Long memberId;
private final String email;
private final String nickname;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.atwoz.member.application.member;

import com.atwoz.member.application.event.ValidatedLoginEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class MemberEventHandler {

private final MemberService memberService;

@EventListener
public void registerIfNotMemberExist(final ValidatedLoginEvent event) {
memberService.create(event.getEmail(), event.getNickname());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.atwoz.member.application.member;

import com.atwoz.member.domain.member.Member;
import com.atwoz.member.domain.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class MemberService {

private final MemberRepository memberRepository;

@Transactional
public void create(final String email, final String nickname) {
if (!memberRepository.existsByEmail(email)) {
memberRepository.save(Member.createWithOAuthLogin(email, nickname));
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/atwoz/member/config/OAuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.atwoz.member.config;

import com.atwoz.member.ui.auth.support.resolver.OAuthArgumentResolver;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@RequiredArgsConstructor
@Configuration
public class OAuthConfig implements WebMvcConfigurer {

private final OAuthArgumentResolver oAuthArgumentResolver;

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(oAuthArgumentResolver);
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/atwoz/member/config/RestTemplateConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.atwoz.member.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
DefaultUriBuilderFactory defaultUriBuilderFactory = new DefaultUriBuilderFactory();
defaultUriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(defaultUriBuilderFactory);
return restTemplate;
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/atwoz/member/domain/auth/JsonMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.atwoz.member.domain.auth;

import com.atwoz.member.infrastructure.auth.dto.MemberInfoKeyWordRequest;
import com.atwoz.member.infrastructure.auth.dto.MemberInfoResponse;

public interface JsonMapper {

String getValueByKey(final String json, final String key);

MemberInfoResponse extractMemberInfoFrom(final String memberInfoResponse,
final MemberInfoKeyWordRequest memberInfoKeyWordRequest);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.atwoz.member.domain.auth;

import com.atwoz.member.infrastructure.auth.dto.OAuthProviderRequest;

public interface OAuthConnectionManager {

String getAccessTokenResponse (final OAuthProviderRequest oAuthProviderRequest, final String code);

String getMemberInfoResponse(final String accessToken, final String userInfoUrl);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public interface TokenProvider {

String create(final Long id);
String createTokenWith(final String email);

Long extract(final String token);
}
18 changes: 8 additions & 10 deletions src/main/java/com/atwoz/member/domain/member/Member.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.atwoz.member.domain.member;

import com.atwoz.global.domain.BaseEntity;
import com.atwoz.member.exception.exceptions.member.PasswordNotMatchedException;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand Down Expand Up @@ -31,9 +30,6 @@ public class Member extends BaseEntity {
@Column(nullable = false, unique = true)
private String email;

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String nickname;

Expand All @@ -46,19 +42,21 @@ public boolean isAdmin() {
}

public static Member createDefaultRole(final String email,
final String password,
final NicknameGenerator nicknameGenerator) {
return Member.builder()
.email(email)
.password(password)
.nickname(nicknameGenerator.createRandomNickname())
.memberRole(MemberRole.MEMBER)
.build();
}

public void validatePassword(final String password) {
if (!this.password.equals(password)) {
throw new PasswordNotMatchedException();
}
public static Member createWithOAuthLogin(final String email,
final String nickname) {

return Member.builder()
.email(email)
.nickname(nickname)
.memberRole(MemberRole.MEMBER)
.build();
}
}
Loading

0 comments on commit 8928b38

Please sign in to comment.