Skip to content

Commit

Permalink
fix: #11 kakao 일반적인 oauth 로그인/회원가입 시나리오 구현 [일반 로그인/회원가입 분기처리 미적용]
Browse files Browse the repository at this point in the history
  • Loading branch information
psychology50 committed Dec 24, 2023
1 parent fd00d56 commit c3e0e87
Show file tree
Hide file tree
Showing 14 changed files with 174 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.kcy.fitapet.domain.member.exception.SmsErrorCode;
import com.kcy.fitapet.domain.member.service.module.MemberSaveService;
import com.kcy.fitapet.domain.member.service.module.MemberSearchService;
import com.kcy.fitapet.global.common.redis.sms.SmsRedisHelper;
import com.kcy.fitapet.global.common.resolver.access.AccessToken;
import com.kcy.fitapet.global.common.response.code.StatusCode;
import com.kcy.fitapet.global.common.response.exception.GlobalErrorException;
Expand All @@ -16,8 +17,8 @@
import com.kcy.fitapet.global.common.redis.forbidden.ForbiddenTokenService;
import com.kcy.fitapet.global.common.redis.refresh.RefreshToken;
import com.kcy.fitapet.global.common.redis.refresh.RefreshTokenService;
import com.kcy.fitapet.global.common.redis.sms.provider.SmsRedisProvider;
import com.kcy.fitapet.global.common.redis.sms.type.SmsPrefix;
import com.kcy.fitapet.global.common.security.jwt.exception.AuthErrorCode;
import com.kcy.fitapet.global.common.util.sms.SmsProvider;
import com.kcy.fitapet.global.common.util.sms.dto.SensInfo;
import com.kcy.fitapet.global.common.util.sms.dto.SmsReq;
Expand All @@ -44,7 +45,8 @@ public class MemberAuthService {

private final RefreshTokenService refreshTokenService;
private final ForbiddenTokenService forbiddenTokenService;
private final SmsRedisProvider smsRedisProvider;

private final SmsRedisHelper smsRedisHelper;

private final SmsProvider smsProvider;
private final JwtUtil jwtUtil;
Expand All @@ -54,15 +56,21 @@ public class MemberAuthService {
@Transactional
public Map<String, String> register(String requestAccessToken, SignUpReq dto) {
String accessToken = jwtUtil.resolveToken(requestAccessToken);
if (forbiddenTokenService.isForbidden(accessToken))
throw new GlobalErrorException(AuthErrorCode.FORBIDDEN_ACCESS_TOKEN);

String authenticatedPhone = jwtUtil.getPhoneNumberFromToken(accessToken);
smsRedisProvider.removeCode(authenticatedPhone, SmsPrefix.REGISTER);
smsRedisHelper.removeCode(authenticatedPhone, SmsPrefix.REGISTER);

Member requestMember = dto.toEntity(authenticatedPhone);
requestMember.encodePassword(bCryptPasswordEncoder);
validateMember(requestMember);

Member registeredMember = memberSaveService.saveMember(requestMember);
forbiddenTokenService.register(
AccessToken.of(accessToken, jwtUtil.getUserIdFromToken(accessToken),
jwtUtil.getExpiryDate(accessToken), false)
);

return generateToken(JwtUserInfo.from(registeredMember));
}
Expand Down Expand Up @@ -100,34 +108,31 @@ public SmsRes sendCode(SmsReq dto, SmsPrefix prefix) {
validateForSms(prefix, dto);
SensInfo smsInfo = smsProvider.sendCodeByPhoneNumber(dto);

smsRedisProvider.saveSmsAuthToken(dto.to(), smsInfo.code(), prefix);
LocalDateTime expireTime = smsRedisProvider.getExpiredTime(dto.to(), prefix);
smsRedisHelper.saveSmsAuthToken(dto.to(), smsInfo.code(), prefix);
LocalDateTime expireTime = smsRedisHelper.getExpiredTime(dto.to(), prefix);
log.info("인증번호 만료 시간: {}", expireTime);
return SmsRes.of(dto.to(), smsInfo.requestTime(), expireTime);
}

@Transactional
public String checkCodeForRegister(SmsReq smsReq, String requestCode) {
if (!smsRedisProvider.isCorrectCode(smsReq.to(), requestCode, SmsPrefix.REGISTER)) {
if (!smsRedisHelper.isCorrectCode(smsReq.to(), requestCode, SmsPrefix.REGISTER)) {
log.warn("인증번호 불일치 -> 사용자 입력 인증 번호 : {}", requestCode);
throw new GlobalErrorException(SmsErrorCode.INVALID_AUTH_CODE);
}

String token = jwtUtil.generateSmsAuthToken(SmsAuthInfo.of(1L, smsReq.to()));
smsRedisProvider.saveSmsAuthToken(smsReq.to(), token, SmsPrefix.REGISTER);

return token;
smsRedisHelper.removeCode(smsReq.to(), SmsPrefix.REGISTER);
return jwtUtil.generateSmsAuthToken(SmsAuthInfo.of(1L, smsReq.to()));
}

@Transactional(readOnly = true)
public void checkCodeForSearch(SmsReq req, String code, SmsPrefix prefix) {
if (!smsRedisProvider.isExistsCode(req.to(), prefix)) {
if (!smsRedisHelper.isExistsCode(req.to(), prefix)) {
StatusCode errorCode = SmsErrorCode.EXPIRED_AUTH_CODE;
log.warn("인증번호 유효성 검사 실패: {}", errorCode);
throw new GlobalErrorException(errorCode);
}

if (!smsRedisProvider.isCorrectCode(req.to(), code, prefix)) {
if (!smsRedisHelper.isCorrectCode(req.to(), code, prefix)) {
StatusCode errorCode = SmsErrorCode.INVALID_AUTH_CODE;
log.warn("인증번호 유효성 검사 실패: {}", errorCode);
throw new GlobalErrorException(errorCode);
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/com/kcy/fitapet/domain/oauth/api/OauthApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ public ResponseEntity<?> signIn(
public ResponseEntity<?> signUp(
@PathVariable("id") Long id,
@RequestParam("provider") ProviderType provider,
@RequestHeader("Authorization") String accessToken,
@RequestBody @Valid OauthSignUpReq req
) {
Jwt jwt = null;
if (ProviderType.NAVER.equals(provider)) {
return null; // TODO: 2023-12-24 네이버 로그인 구현
} else {
jwt = oAuthService.signUpByOIDC(id, provider, req);
jwt = oAuthService.signUpByOIDC(id, provider, accessToken, req);
}

return getResponseEntity(jwt);
Expand All @@ -99,11 +100,11 @@ public ResponseEntity<?> signUpSmsAuthorization(
@RequestBody @Valid SmsReq req
) {
if (code == null) {
SmsRes smsRes = oAuthService.sendCode(req, id, provider, SmsPrefix.OAUTH);
SmsRes smsRes = oAuthService.sendCode(req, id, provider);
return ResponseEntity.ok(SuccessResponse.from(smsRes));
}

String token = oAuthService.checkCertificationNumber(req, id, code);
String token = oAuthService.checkCertificationNumber(req, id, code, provider);
if (!StringUtils.hasText(token))
return ResponseEntity.status(HttpStatus.SC_UNAUTHORIZED).build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@

@Schema(description = "Oauth Sign Up Request")
public record OauthSignUpReq(
@Schema(description = "전화번호")
@NotBlank
String phone,
@Schema(description = "이름")
@NotBlank
String name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum OauthException implements StatusCode {
/* BAD REQUEST */
INVALID_PROVIDER(HttpStatus.BAD_REQUEST, "유효하지 않은 제공자입니다."),
INVALID_OAUTH_ID(HttpStatus.BAD_REQUEST, "ID와 제공자가 일치하지 않습니다."),
INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "제공자가 일치하지 않습니다."),

/* FORBIDDEN */
NOT_FOUND_MEMBER(HttpStatus.FORBIDDEN, "존재하지 않는 회원입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
import com.kcy.fitapet.domain.oauth.service.module.OauthClientHelper;
import com.kcy.fitapet.domain.oauth.service.module.OauthSearchService;
import com.kcy.fitapet.domain.oauth.type.ProviderType;
import com.kcy.fitapet.global.common.redis.forbidden.ForbiddenTokenService;
import com.kcy.fitapet.global.common.redis.oauth.OIDCTokenService;
import com.kcy.fitapet.global.common.redis.sms.provider.SmsRedisProvider;
import com.kcy.fitapet.global.common.redis.sms.SmsRedisHelper;
import com.kcy.fitapet.global.common.redis.sms.type.SmsPrefix;
import com.kcy.fitapet.global.common.resolver.access.AccessToken;
import com.kcy.fitapet.global.common.response.exception.GlobalErrorException;
import com.kcy.fitapet.global.common.security.jwt.JwtUtil;
import com.kcy.fitapet.global.common.security.jwt.dto.Jwt;
import com.kcy.fitapet.global.common.security.jwt.dto.JwtUserInfo;
import com.kcy.fitapet.global.common.security.jwt.dto.SmsAuthInfo;
import com.kcy.fitapet.global.common.security.jwt.exception.AuthErrorCode;
import com.kcy.fitapet.global.common.security.oauth.OauthApplicationConfig;
import com.kcy.fitapet.global.common.security.oauth.OauthClient;
import com.kcy.fitapet.global.common.security.oauth.OauthOIDCHelper;
Expand Down Expand Up @@ -49,9 +52,11 @@ public class OauthService {
private final OauthClientHelper oauthClientHelper;

private final JwtUtil jwtUtil;
private final ForbiddenTokenService forbiddenTokenService;

private final OIDCTokenService oidcTokenService;
private final SmsProvider smsProvider;
private final SmsRedisProvider smsRedisProvider;
private final SmsRedisHelper smsRedisHelper;

@Transactional
public Jwt signInByOIDC(Long id, String idToken, ProviderType provider, String nonce) {
Expand All @@ -68,43 +73,52 @@ public Jwt signInByOIDC(Long id, String idToken, ProviderType provider, String n
}

@Transactional
public Jwt signUpByOIDC(Long id, ProviderType provider, OauthSignUpReq req) {
public Jwt signUpByOIDC(Long id, ProviderType provider, String requestAccessToken, OauthSignUpReq req) {
String accessToken = jwtUtil.resolveToken(requestAccessToken);
String topic = jwtUtil.getPhoneNumberFromToken(accessToken);
validateToken(accessToken, topic, provider);

String phone = getPhoneByTopic(topic);
String idToken = oidcTokenService.findOIDCToken(req.idToken()).getToken();
OIDCDecodePayload payload = getPayload(provider, idToken, req.nonce());

Member member = (memberSearchService.isExistByPhone(req.phone()))
? memberSearchService.findByPhone(req.phone())
Member member = (memberSearchService.isExistByPhone(phone))
? memberSearchService.findByPhone(phone)
: Member.builder().uid(req.uid()).name(req.name())
.phone(req.phone()).isOauth(Boolean.TRUE).role(RoleType.USER).build();
.phone(phone).isOauth(Boolean.TRUE).role(RoleType.USER).build();
memberSaveService.saveMember(member);
OauthAccount oauthAccount = OauthAccount.of(id, provider, payload.email(), member);

forbiddenTokenService.register(
AccessToken.of(accessToken, jwtUtil.getUserIdFromToken(accessToken),
jwtUtil.getExpiryDate(accessToken), false)
);

log.info("success oauth signup member id : {} - oauth id : {} [provider: {}]",
member.getId(), oauthAccount.getOauthId(), oauthAccount.getProvider());
return generateToken(JwtUserInfo.from(member));
}

@Transactional
public SmsRes sendCode(SmsReq dto, Long id, ProviderType provider, SmsPrefix prefix) {
public SmsRes sendCode(SmsReq dto, Long id, ProviderType provider) {
SensInfo smsInfo = smsProvider.sendCodeByPhoneNumber(dto);
String key = makeTopic(dto.to(), provider);

smsRedisProvider.saveSmsAuthToken(dto.to(), smsInfo.code(), prefix);
LocalDateTime expireTime = smsRedisProvider.getExpiredTime(dto.to(), prefix);
smsRedisHelper.saveSmsAuthToken(key, smsInfo.code(), SmsPrefix.OAUTH);
LocalDateTime expireTime = smsRedisHelper.getExpiredTime(key, SmsPrefix.OAUTH);
log.info("인증번호 만료 시간: {}", expireTime);
return SmsRes.of(dto.to(), smsInfo.requestTime(), expireTime);
}

@Transactional
public String checkCertificationNumber(SmsReq req, Long id, String code) {
if (!smsRedisProvider.isCorrectCode(req.to(), code, SmsPrefix.REGISTER)) {
public String checkCertificationNumber(SmsReq req, Long id, String code, ProviderType provider) {
String key = makeTopic(req.to(), provider);
if (!smsRedisHelper.isCorrectCode(key, code, SmsPrefix.OAUTH)) {
log.warn("인증번호 불일치 -> 사용자 입력 인증 번호 : {}", code);
throw new GlobalErrorException(SmsErrorCode.INVALID_AUTH_CODE);
}

String token = jwtUtil.generateSmsOauthToken(SmsAuthInfo.of(id, req.to()));
smsRedisProvider.saveSmsAuthToken(req.to(), token, SmsPrefix.OAUTH);

return token;
smsRedisHelper.removeCode(key, SmsPrefix.OAUTH);
return jwtUtil.generateSmsOauthToken(SmsAuthInfo.of(id, key));
}

private OIDCDecodePayload getPayload(ProviderType provider, String idToken, String nonce) {
Expand All @@ -126,6 +140,27 @@ private void isValidRequestId(Long id, Long sub) {
}
}

private String makeTopic(String phoneNumber, ProviderType provider) {
return provider.name() + "@" + phoneNumber;
}

private void validateToken(String accessToken, String value, ProviderType provider) {
if (forbiddenTokenService.isForbidden(accessToken))
throw new GlobalErrorException(AuthErrorCode.FORBIDDEN_ACCESS_TOKEN);

ProviderType tokenProvider = getProviderByTopic(value);
if (!provider.equals(tokenProvider))
throw new GlobalErrorException(OauthException.INVALID_OAUTH_PROVIDER);
}

private ProviderType getProviderByTopic(String topic) {
return ProviderType.valueOf(topic.split("@")[0].toUpperCase());
}

private String getPhoneByTopic(String topic) {
return topic.split("@")[1];
}

private Jwt generateToken(JwtUserInfo jwtUserInfo) {
return Jwt.builder()
.accessToken(jwtUtil.generateAccessToken(jwtUserInfo))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.kcy.fitapet.global.common.redis.oauth;

import com.kcy.fitapet.domain.oauth.type.ProviderType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.kcy.fitapet.global.common.redis.sms;

import com.kcy.fitapet.global.common.redis.sms.service.SmsOauthService;
import com.kcy.fitapet.global.common.redis.sms.service.SmsPasswordService;
import com.kcy.fitapet.global.common.redis.sms.service.SmsRegisterService;
import com.kcy.fitapet.global.common.redis.sms.service.SmsUidService;
import com.kcy.fitapet.global.common.redis.sms.type.SmsPrefix;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
@RequiredArgsConstructor
public class SmsRedisHelper {
private final SmsOauthService smsOauthService;
private final SmsRegisterService smsRegisterService;
private final SmsPasswordService smsPasswordService;
private final SmsUidService smsUidService;

public void saveSmsAuthToken(String phone, String code, SmsPrefix prefix) {
switch (prefix) {
case OAUTH -> smsOauthService.save(phone, code, prefix);
case REGISTER -> smsRegisterService.save(phone, code, prefix);
case PASSWORD -> smsPasswordService.save(phone, code, prefix);
case UID -> smsUidService.save(phone, code, prefix);
}
}

public boolean isCorrectCode(String phone, String code, SmsPrefix prefix) {
return switch (prefix) {
case OAUTH -> smsOauthService.isCorrectCode(phone, code, prefix);
case REGISTER -> smsRegisterService.isCorrectCode(phone, code, prefix);
case PASSWORD -> smsPasswordService.isCorrectCode(phone, code, prefix);
case UID -> smsUidService.isCorrectCode(phone, code, prefix);
};
}

public boolean isExistsCode(String phone, SmsPrefix prefix) {
return switch (prefix) {
case OAUTH -> smsOauthService.isExistsCode(phone, prefix);
case REGISTER -> smsRegisterService.isExistsCode(phone, prefix);
case PASSWORD -> smsPasswordService.isExistsCode(phone, prefix);
case UID -> smsUidService.isExistsCode(phone, prefix);
};
}

public void removeCode(String phone, SmsPrefix prefix) {
switch (prefix) {
case OAUTH -> smsOauthService.removeCode(phone, prefix);
case REGISTER -> smsRegisterService.removeCode(phone, prefix);
case PASSWORD -> smsPasswordService.removeCode(phone, prefix);
case UID -> smsUidService.removeCode(phone, prefix);
};
}

public LocalDateTime getExpiredTime(String phone, SmsPrefix prefix) {
return switch (prefix) {
case OAUTH -> smsOauthService.getExpiredTime(phone, prefix);
case REGISTER -> smsRegisterService.getExpiredTime(phone, prefix);
case PASSWORD -> smsPasswordService.getExpiredTime(phone, prefix);
case UID -> smsUidService.getExpiredTime(phone, prefix);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public interface SmsRedisProvider {

default String getTopic(String phoneNumber, SmsPrefix prefix) {
String str = prefix.getTopic(phoneNumber);
return "sms" + str.substring(0, 1).toUpperCase() + str.substring(1)
+ ":" + phoneNumber;
return "sms" + str.substring(0, 1).toUpperCase()
+ str.substring(1).replace("@", ":");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

import com.kcy.fitapet.global.common.redis.sms.provider.SmsRedisProvider;
import com.kcy.fitapet.global.common.redis.sms.type.SmsPrefix;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor
public class SmsOauthService {
private final SmsRedisProvider smsRedisProvider;

public SmsOauthService(final SmsRedisProvider smsRedisProvider) {
this.smsRedisProvider = smsRedisProvider;
}

public void save(String phone, String code, SmsPrefix prefix) {
smsRedisProvider.saveSmsAuthToken(phone, code, prefix);
}
Expand All @@ -27,4 +27,8 @@ public boolean isExistsCode(String phone, SmsPrefix prefix) {
public void removeCode(String phone, SmsPrefix prefix) {
smsRedisProvider.removeCode(phone, prefix);
}

public LocalDateTime getExpiredTime(String phone, SmsPrefix prefix) {
return smsRedisProvider.getExpiredTime(phone, prefix);
}
}
Loading

0 comments on commit c3e0e87

Please sign in to comment.