Skip to content

Commit

Permalink
feat: 회원 생성 및 로그인 응답 기능 구현 (#47)
Browse files Browse the repository at this point in the history
* feat: 회원 엔티티 생성 및 테스트코드 추가

* feat: 카카오 OAuth 환경변수 추가 및 클래스 바인딩

* feat: authorization code를 받기 위한 queryString generator 추가

* feat: Authorization code의 parameter 만드는 로직 분리 및 테스트 코드 추가

* feat: 회원 가입/로그인 요청 api 및 소셜 로그인 페이지 반환

* refactor: member관련 클래스 네이밍과 폴더 위치 변경

* refactor: 로그인 페이지 요청 방식 Resttemplate -> response (redirect)하도록 변경

* style: 코드 포맷 재적용 및 사용하지 않는 클래스 삭제

* chore: config 파일 업데이트

* refactor: 테스트 코드 추가 및 코드 포맷 재적용

* refactor: 사용하지 않는 코드 제거

* refactor: CRLF -> LF로 변경

* fix: config 커밋, config 최근 커밋으로 변경

* feat: 테스트 코드 추가 및 패키지 구조 변경

* refactor: revert merge

* fix: merge confilt해결 및 예외처리 추가

* test: oauth properties가 없을 때의 테스트코드 추가

* feat: 코드리뷰에 따른 기능 분리 및 테스트 코드 변경

* fix: 테스트코드 관련 code smell 제거

* feat: Authorization grant 받기 예외 코드 및 테스트 코드 추가

* feat: Authorization Token 요청 및 반환 코드, 에러 반환 테스트 코드 추가

* refactor: AuthenticationService에서 서버에 요청보내는 로직 OAuth2AuthorizationServerRequestService로 분리

* test: 로그인 요청 테스트 코드 추가

* feat: 토큰 발급 요청 기능 테스트 코드 추가 및 RestTemplate 필드변수로 변경

* test: restTemplate 및 서비스 테스트 추가

* refactor: 에러 메세지 이름 변경

* refacotr: 변수명 및 entity default 명 변경

* feat: 토큰 정보 조회 기능 및 테스트 추가

* feat: 사용자 토큰 정보 조회 및 테스트 코드 & Resttemplate 테크트 코드 변경

* fix: encoding, formatting, tab 문제로 인한 파일 삭제 후 다시 작성

* feat: JWT 토큰 제공 서비스 및 테스트 코드 추가

* feat: 토큰 인증 코드 및 테스트 코드 작성

* feat: 로그인 및 회원가입 기능 추가

- 회원의 socialId string -> long으로 변경

* feat: 회원 로그인 테스트 코드 추가

* chore: 코드 포메팅 재 설정

* feat: config 파일 업데이트

* feat: Window용 포트 redis 포트 변경 추가

* refacotr: develop 업데이트 사항 merge

* refactor: develop 업데이트 부분 merge

* fix: TimeConfig 삭제 및 코드 스멜 변경

* refactor: 코르리뷰 반영
  • Loading branch information
parksey committed Nov 7, 2023
1 parent 3305dff commit 316dafd
Show file tree
Hide file tree
Showing 34 changed files with 808 additions and 178 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ dependencies {
// Firebase Admin
implementation 'com.google.firebase:firebase-admin:9.2.0'

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

// JSON parser
implementation 'org.json:json:20230618'

// Asciidoctor
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'

Expand Down
54 changes: 36 additions & 18 deletions src/main/java/com/moabam/api/application/AuthenticationService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.moabam.api.application;

import static com.moabam.global.common.util.OAuthParameterNames.*;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
Expand All @@ -13,9 +12,10 @@
import com.moabam.api.dto.AuthorizationTokenInfoResponse;
import com.moabam.api.dto.AuthorizationTokenRequest;
import com.moabam.api.dto.AuthorizationTokenResponse;
import com.moabam.api.dto.LoginResponse;
import com.moabam.api.dto.OAuthMapper;
import com.moabam.global.common.constant.GlobalConstant;
import com.moabam.global.common.util.TokenConstant;
import com.moabam.global.common.util.CookieUtils;
import com.moabam.global.config.OAuthConfig;
import com.moabam.global.error.exception.BadRequestException;
import com.moabam.global.error.model.ErrorMessage;
Expand All @@ -29,6 +29,8 @@ public class AuthenticationService {

private final OAuthConfig oAuthConfig;
private final OAuth2AuthorizationServerRequestService oauth2AuthorizationServerRequestService;
private final MemberService memberService;
private final JwtProviderService jwtProviderService;

public void redirectToLoginPage(HttpServletResponse httpServletResponse) {
String authorizationCodeUri = getAuthorizationCodeUri();
Expand All @@ -37,36 +39,46 @@ public void redirectToLoginPage(HttpServletResponse httpServletResponse) {

public AuthorizationTokenResponse requestToken(AuthorizationCodeResponse authorizationCodeResponse) {
validAuthorizationGrant(authorizationCodeResponse.code());

return issueTokenToAuthorizationServer(authorizationCodeResponse.code());
}

public AuthorizationTokenInfoResponse requestTokenInfo(AuthorizationTokenResponse authorizationTokenResponse) {
String tokenValue = generateTokenValue(authorizationTokenResponse.accessToken());
ResponseEntity<AuthorizationTokenInfoResponse> authorizationTokenInfoResponse
= oauth2AuthorizationServerRequestService.tokenInfoRequest(oAuthConfig.provider().tokenInfo(), tokenValue);
ResponseEntity<AuthorizationTokenInfoResponse> authorizationTokenInfoResponse =
oauth2AuthorizationServerRequestService.tokenInfoRequest(oAuthConfig.provider().tokenInfo(), tokenValue);

return authorizationTokenInfoResponse.getBody();
}

@Transactional
public LoginResponse signUpOrLogin(HttpServletResponse httpServletResponse,
AuthorizationTokenInfoResponse authorizationTokenInfoResponse) {
LoginResponse loginResponse = memberService.login(authorizationTokenInfoResponse);
issueServiceToken(httpServletResponse, loginResponse.id());

return loginResponse;
}

private String getAuthorizationCodeUri() {
AuthorizationCodeRequest authorizationCodeRequest = OAuthMapper.toAuthorizationCodeRequest(oAuthConfig);
return generateQueryParamsWith(authorizationCodeRequest);
}

private String generateTokenValue(String token) {
return TokenConstant.TOKEN_TYPE + GlobalConstant.SPACE + token;
return "Bearer" + GlobalConstant.SPACE + token;
}

private String generateQueryParamsWith(AuthorizationCodeRequest authorizationCodeRequest) {
UriComponentsBuilder authorizationCodeUri = UriComponentsBuilder
.fromUriString(oAuthConfig.provider().authorizationUri())
.queryParam(RESPONSE_TYPE, CODE)
.queryParam(CLIENT_ID, authorizationCodeRequest.clientId())
.queryParam(REDIRECT_URI, authorizationCodeRequest.redirectUri());
UriComponentsBuilder authorizationCodeUri = UriComponentsBuilder.fromUriString(
oAuthConfig.provider().authorizationUri())
.queryParam("response_type", "code")
.queryParam("client_id", authorizationCodeRequest.clientId())
.queryParam("redirect_uri", authorizationCodeRequest.redirectUri());

if (!authorizationCodeRequest.scope().isEmpty()) {
String scopes = String.join(GlobalConstant.COMMA, authorizationCodeRequest.scope());
authorizationCodeUri.queryParam(SCOPE, scopes);
String scopes = String.join(",", authorizationCodeRequest.scope());
authorizationCodeUri.queryParam("scope", scopes);
}

return authorizationCodeUri.toUriString();
Expand All @@ -91,15 +103,21 @@ private AuthorizationTokenResponse issueTokenToAuthorizationServer(String code)

private MultiValueMap<String, String> generateTokenRequest(AuthorizationTokenRequest authorizationTokenRequest) {
MultiValueMap<String, String> contents = new LinkedMultiValueMap<>();
contents.add(GRANT_TYPE, authorizationTokenRequest.grantType());
contents.add(CLIENT_ID, authorizationTokenRequest.clientId());
contents.add(REDIRECT_URI, authorizationTokenRequest.redirectUri());
contents.add(CODE, authorizationTokenRequest.code());
contents.add("grant_type", authorizationTokenRequest.grantType());
contents.add("client_id", authorizationTokenRequest.clientId());
contents.add("redirect_uri", authorizationTokenRequest.redirectUri());
contents.add("code", authorizationTokenRequest.code());

if (authorizationTokenRequest.clientSecret() != null) {
contents.add(CLIENT_SECRET, authorizationTokenRequest.clientSecret());
contents.add("client_secret", authorizationTokenRequest.clientSecret());
}

return contents;
}

private void issueServiceToken(HttpServletResponse response, Long id) {
response.addHeader("token_type", "Bearer");
response.addCookie(CookieUtils.tokenCookie("access_token", jwtProviderService.provideAccessToken(id)));
response.addCookie(CookieUtils.tokenCookie("refresh_token", jwtProviderService.provideRefreshToken(id)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.moabam.api.application;

import java.util.Base64;

import org.json.JSONObject;
import org.springframework.stereotype.Service;

import com.moabam.global.config.TokenConfig;
import com.moabam.global.error.exception.UnauthorizedException;
import com.moabam.global.error.model.ErrorMessage;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class JwtAuthenticationService {

private final TokenConfig tokenConfig;

public boolean isTokenValid(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(tokenConfig.getKey())
.build()
.parseClaimsJwt(token);
return true;
} catch (ExpiredJwtException expiredJwtException) {
return false;
} catch (Exception exception) {
throw new UnauthorizedException(ErrorMessage.AUTHENTICATIE_FAIL);
}
}

public String parseEmail(String token) {
String claims = token.split("\\.")[1];
String decodeClaims = new String(Base64.getDecoder().decode(claims));
JSONObject jsonObject = new JSONObject(decodeClaims);

return (String)jsonObject.get("id");
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/moabam/api/application/JwtProviderService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.moabam.api.application;

import java.util.Date;

import org.springframework.stereotype.Service;

import com.moabam.global.config.TokenConfig;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class JwtProviderService {

private final TokenConfig tokenConfig;

public String provideAccessToken(long id) {
return generateToken(id, tokenConfig.getAccessExpire());
}

public String provideRefreshToken(long id) {
return generateToken(id, tokenConfig.getRefreshExpire());
}

private String generateToken(long id, long expireTime) {
Date issueDate = new Date();
Date expireDate = new Date(issueDate.getTime() + expireTime);

return Jwts.builder()
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
.setIssuer(tokenConfig.getIss())
.setIssuedAt(issueDate)
.setExpiration(expireDate)
.claim("id", id)
.signWith(tokenConfig.getKey(), SignatureAlgorithm.HS256)
.compact();
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/moabam/api/application/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

import static com.moabam.global.error.model.ErrorMessage.*;

import java.security.SecureRandom;
import java.util.List;
import java.util.Optional;

import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.moabam.api.domain.entity.Member;
import com.moabam.api.domain.repository.MemberRepository;
import com.moabam.api.domain.repository.MemberSearchRepository;
import com.moabam.api.domain.repository.NotificationRepository;
import com.moabam.api.dto.AuthorizationTokenInfoResponse;
import com.moabam.api.dto.LoginResponse;
import com.moabam.api.dto.MemberMapper;
import com.moabam.global.error.exception.NotFoundException;

import lombok.RequiredArgsConstructor;
Expand All @@ -21,12 +28,33 @@ public class MemberService {

private final MemberRepository memberRepository;
private final MemberSearchRepository memberSearchRepository;
private final NotificationRepository notificationRepository;

public Member getById(Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException(MEMBER_NOT_FOUND));
}

@Transactional
public LoginResponse login(AuthorizationTokenInfoResponse authorizationTokenInfoResponse) {
Optional<Member> member = memberRepository.findBySocialId(authorizationTokenInfoResponse.id());
Member loginMember = member.orElse(signUp(authorizationTokenInfoResponse.id()));

return MemberMapper.toLoginResponse(loginMember.getId(), member.isEmpty());
}

private Member signUp(Long socialId) {
String randomNickName = createRandomNickName();
Member member = MemberMapper.toMember(socialId, randomNickName);

return memberRepository.save(member);
}

private String createRandomNickName() {
return RandomStringUtils.random(6, 0, 0, true, true, null,
new SecureRandom());
}

public Member getManager(Long roomId) {
return memberSearchRepository.findManager(roomId)
.orElseThrow(() -> new NotFoundException(MEMBER_NOT_FOUND));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import com.moabam.api.dto.AuthorizationTokenInfoResponse;
import com.moabam.api.dto.AuthorizationTokenResponse;
import com.moabam.global.common.constant.GlobalConstant;
import com.moabam.global.common.util.TokenConstant;
import com.moabam.global.error.exception.BadRequestException;
import com.moabam.global.error.handler.RestTemplateResponseHandler;
import com.moabam.global.error.model.ErrorMessage;
Expand Down Expand Up @@ -54,7 +53,7 @@ public ResponseEntity<AuthorizationTokenResponse> requestAuthorizationServer(Str

public ResponseEntity<AuthorizationTokenInfoResponse> tokenInfoRequest(String tokenInfoUri, String tokenValue) {
HttpHeaders headers = new HttpHeaders();
headers.add(TokenConstant.AUTHORIZATION, tokenValue);
headers.add("Authorization", tokenValue);
HttpEntity<Void> httpEntity = new HttpEntity<>(headers);

return restTemplate.exchange(tokenInfoUri, HttpMethod.GET, httpEntity, AuthorizationTokenInfoResponse.class);
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/moabam/api/domain/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class Member extends BaseTimeEntity {
private Long id;

@Column(name = "social_id", nullable = false, unique = true)
private String socialId;
private Long socialId;

@Column(name = "nickname", nullable = false, unique = true)
private String nickname;
Expand Down Expand Up @@ -79,11 +79,11 @@ public class Member extends BaseTimeEntity {
private LocalDateTime deletedAt;

@Builder
private Member(Long id, String socialId, String nickname, String profileImage, Bug bug) {
private Member(Long id, Long socialId, String nickname, Bug bug) {
this.id = id;
this.socialId = requireNonNull(socialId);
this.socialId = socialId;
this.nickname = requireNonNull(nickname);
this.profileImage = requireNonNullElse(profileImage, BaseImageUrl.PROFILE_URL);
this.profileImage = BaseImageUrl.PROFILE_URL;
this.bug = requireNonNull(bug);
this.role = Role.USER;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.moabam.api.domain.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.moabam.api.domain.entity.Member;

public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findBySocialId(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class NotificationRepository {

private final StringRedisRepository stringRedisRepository;

// TODO : 세연님 로그인 시, 해당 메서드 사용해서 해당 유저의 FCM TOKEN 저장하면 됩니다.
// TODO : 세연님 로그인 시, 해당 메서드 사용해서 해당 유저의 FCM TOKEN 저장하면 됩니다. Front와 상의 후 삭제예정
public void saveFcmToken(Long key, String value) {
stringRedisRepository.save(
String.valueOf(requireNonNull(key)),
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/moabam/api/dto/LoginResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.moabam.api.dto;

import lombok.Builder;

@Builder
public record LoginResponse(
Long id,
boolean isSignUp
) {

}
32 changes: 32 additions & 0 deletions src/main/java/com/moabam/api/dto/MemberMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.moabam.api.dto;

import com.moabam.api.domain.entity.Bug;
import com.moabam.api.domain.entity.Member;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class MemberMapper {

public static Member toMember(Long socialId, String nickName) {
return Member.builder()
.socialId(socialId)
.nickname(nickName)
.bug(Bug.builder().build())
.build();
}

public static LoginResponse toLoginResponse(Long memberId) {
return LoginResponse.builder()
.id(memberId)
.build();
}

public static LoginResponse toLoginResponse(Long memberId, boolean isSignUp) {
return LoginResponse.builder()
.id(memberId)
.isSignUp(isSignUp)
.build();
}
}
Loading

0 comments on commit 316dafd

Please sign in to comment.