Skip to content

Commit

Permalink
회원가입 및 로그인 & 인가 기능 구현 (#82)
Browse files Browse the repository at this point in the history
* feat: (#29) 카카오 OAuth를 통해 정보를 받아서 가입하는 기능 구현

* chore: (#29) OAuth API요청에 대한 환경변수 설정

* feat: (#29) Member의 랜덤 닉네임을 지정하기 위한 수 생성기 구현

* chore: (#29) JJWT라이브러리 의존성 추가

* feat: (#29) 로그인한 회원에 대한 정보를 JWT로 반환하는 기능 추가

* chore: (#29) 토큰 관련 환경변수 설정 추가

* refactor: (#29) ResponseDto를 record형식으로 변환

* feat: (#29) 인증정보를 확인하는 AuthenticationFilter구현

* feat: (#29) 멤버가 존재하는지 확인한 후 반환하는 ArgumentResolver구현

* test: (#29) loginWithKakao메서드에 대한 컨트롤러 단위 테스트 작성

* refactor: (#29) Member엔티티 필드명 수정 및 추가

* test: (#29) Member 등록에 대한 검증 추가

* chore: (#29) test를 위한 yaml파일을 추가하여 환경 분리

* refactor: (#29) conflict 해결

* refactor: (#29) Controller Swagger를 위한 어노테이션 추가

* feat: (#29) CORS설정 및 ArgumentResolver등록

* chore: (#29) test환경 환경변수만 존재하도록 수정

* refactor: (#29) 네이밍, 상수화, 위치변경 등의 작업 수행

* fix: (#29) 멤버의 이름에 포함되는 숫자가 고정되는 문제 해결

* refactor: (#29) ObjectMapper Bean으로 등록

* refactor: (#29) 매직넘버 상수화 및 변수, 메서드명 수정

* refactor: (#29) @JsonProperty를 @JsonNaming으로 변경

* chore: (#29) test용 production url 수정

* refactor: (#29) 상수 및 변수명 수정

* test: (#29) 토큰에 대한 검증 추가

* refactor: (#29) 토큰을 파싱할 때 유효성 검사 추가

* refactor: (#29) 로그인 api nickname 필드 추가

* refactor: (#29) 토큰 검증 DisplayName 변경

* refactor: (#29) Swagger tag name 변경

* refactor: (#29) TokenProcessorTest의 필드를 빈을 사용하도록 변경
  • Loading branch information
jeomxon authored and tjdtls690 committed Sep 12, 2023
1 parent 0612f02 commit bd7d335
Show file tree
Hide file tree
Showing 35 changed files with 826 additions and 31 deletions.
5 changes: 5 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'

runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
runtimeOnly 'com.h2database:h2'

compileOnly 'org.projectlombok:lombok'
Expand All @@ -35,6 +38,8 @@ dependencies {
testImplementation 'io.rest-assured:rest-assured'
testImplementation 'io.rest-assured:spring-mock-mvc'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured'
testImplementation 'io.rest-assured:spring-mock-mvc'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@ConfigurationPropertiesScan
@SpringBootApplication
public class VotogetherApplication {

Expand Down
9 changes: 9 additions & 0 deletions backend/src/main/java/com/votogether/config/JpaConfig.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package com.votogether.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaConfig {

@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper().registerModule(new JavaTimeModule());
}

}
46 changes: 46 additions & 0 deletions backend/src/main/java/com/votogether/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.votogether.config;

import com.votogether.global.jwt.JwtAuthorizationArgumentResolver;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

private final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver;
private final String origins;

public WebMvcConfig(
final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver,
@Value("${votogether.openapi.prod-url}") final String origins
) {
this.jwtAuthorizationArgumentResolver = jwtAuthorizationArgumentResolver;
this.origins = origins;
}

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(jwtAuthorizationArgumentResolver);
}

@Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("*")
.allowedOrigins(origins)
.allowedMethods(
HttpMethod.POST.name(),
HttpMethod.GET.name(),
HttpMethod.PATCH.name(),
HttpMethod.PUT.name(),
HttpMethod.DELETE.name()
)
.exposedHeaders(HttpHeaders.LOCATION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.votogether.domain.auth.controller;

import com.votogether.domain.auth.dto.LoginResponse;
import com.votogether.domain.auth.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
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;

@Tag(name = "회원 인증", description = "회원 인증 API")
@RequiredArgsConstructor
@RestController
public class AuthController {

private final AuthService authService;

@Operation(summary = "카카오 로그인 하기", description = "카카오 계정으로 로그인을 한다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "카카오 로그인 성공"),
@ApiResponse(responseCode = "400", description = "올바르지 않은 요청"),
@ApiResponse(responseCode = "401", description = "올바르지 않은 인증코드")
})
@GetMapping("/auth/kakao/callback")
public ResponseEntity<LoginResponse> loginByKakao(@RequestParam final String code) {
return ResponseEntity.ok(authService.register(code));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.votogether.domain.auth.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoMemberResponse(
Long id,
KakaoAccount kakaoAccount
) {

@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoAccount(
String email,
String ageRange,
String birthday,
String gender
) {
}

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

public record LoginResponse(
String accessToken,
String nickname
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.votogether.domain.auth.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public record OAuthAccessTokenResponse(
String tokenType,
String accessToken,
Integer expiresIn,
String refreshToken,
Integer refreshTokenExpiresIn
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.votogether.domain.auth.service;

import com.votogether.domain.auth.dto.KakaoMemberResponse;
import com.votogether.domain.auth.dto.LoginResponse;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.member.service.MemberService;
import com.votogether.global.jwt.TokenProcessor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class AuthService {

private final KakaoOAuthClient kakaoOAuthClient;
private final MemberService memberService;
private final TokenProcessor tokenProcessor;

@Transactional
public LoginResponse register(final String code) {
final String accessToken = kakaoOAuthClient.getAccessToken(code);
final KakaoMemberResponse response = kakaoOAuthClient.getMemberInfo(accessToken);

final Member member = Member.from(response);
final Member registeredMember = memberService.register(member);
final String token = tokenProcessor.generateToken(registeredMember);
return new LoginResponse(token, member.getNickname());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.votogether.domain.auth.service;

import com.votogether.domain.auth.dto.KakaoMemberResponse;
import com.votogether.domain.auth.dto.OAuthAccessTokenResponse;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
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.RestTemplate;

@Getter
@ConfigurationProperties(prefix = "oauth.kakao")
@Component
public class KakaoOAuthClient {

private static final RestTemplate restTemplate = new RestTemplate();

private final MultiValueMap<String, String> info = new LinkedMultiValueMap<>();

public String getAccessToken(final String code) {
info.add("code", code);

final OAuthAccessTokenResponse response = restTemplate.postForObject(
"https://kauth.kakao.com/oauth/token",
info,
OAuthAccessTokenResponse.class
);
return response.accessToken();
}

public KakaoMemberResponse getMemberInfo(final String accessToken) {
final HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

final HttpEntity<Void> request = new HttpEntity<>(headers);

final KakaoMemberResponse response = restTemplate.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.GET,
request,
KakaoMemberResponse.class
).getBody();
return response;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.votogether.domain.member.entity;

import com.votogether.domain.auth.dto.KakaoMemberResponse;
import com.votogether.domain.common.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down Expand Up @@ -64,6 +65,19 @@ private Member(
this.point = point;
}

public static Member from(final KakaoMemberResponse response) {
final NicknameNumberGenerator nicknameNumberGenerator = new NicknameNumberGenerator();
return Member.builder()
.nickname("익명의 손님" + nicknameNumberGenerator.generate())
.gender(Gender.valueOf(response.kakaoAccount().gender().toUpperCase()))
.ageRange(response.kakaoAccount().ageRange())
.birthday(response.kakaoAccount().birthday())
.socialType(SocialType.KAKAO)
.socialId(String.valueOf(response.id()))
.point(0)
.build();
}

public void plusPoint(final int point) {
this.point = this.point + point;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.votogether.domain.member.entity;

public class NicknameNumberGenerator implements NumberGenerator {

private static int number = 0;

@Override
public int generate() {
return ++number;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.votogether.domain.member.entity;

public interface NumberGenerator {

int generate();

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public enum SocialType {

GOOGLE,
KAKAO,
;

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.votogether.domain.member.repository;

import com.votogether.domain.member.entity.Member;
import com.votogether.domain.member.entity.SocialType;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findBySocialIdAndSocialType(final String socialId, final SocialType socialType);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.votogether.domain.member.service;

import com.votogether.domain.member.entity.Member;
import com.votogether.domain.member.repository.MemberRepository;
import java.util.Optional;
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 Member register(final Member member) {
final Optional<Member> maybeMember = memberRepository.findBySocialIdAndSocialType(
member.getSocialId(),
member.getSocialType()
);
return maybeMember.orElseGet(() -> memberRepository.save(member));
}

@Transactional(readOnly = true)
public Member findById(final Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new IllegalArgumentException("해당 Id를 가진 회원은 존재하지 않습니다."));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@ public ResponseEntity<Void> save(
) {
// TODO : 일단 돌아가게 하기 위한 member 저장 (실제 어플에선 삭제될 코드)
final Member member = Member.builder()
.socialType(SocialType.KAKAO)
.socialId("tjdtls690")
.nickname("Abel")
.gender(Gender.MALE)
.birthday("0718")
.ageRange("10~14")
.socialType(SocialType.GOOGLE)
.socialType(SocialType.KAKAO)
.socialId("tjdtls690")
.point(100)
.ageRange("30~39")
.birthday("0101")
.build();

final Long postId = postService.save(request, member, images);
Expand Down
11 changes: 11 additions & 0 deletions backend/src/main/java/com/votogether/global/jwt/Auth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.votogether.global.jwt;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
}
Loading

0 comments on commit bd7d335

Please sign in to comment.