Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat #20 카카오 로그인 로직 변경 #21

Merged
merged 23 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
288dd3d
feat: 임시유저 상태 추가
koreaioi Jan 8, 2025
7815143
feat: 임시유저 인가 경로 추가 설정
koreaioi Jan 8, 2025
93aead6
feat: 임시유저 생성 시 tmpEmail 주입 (UserDetails 때문)
koreaioi Jan 8, 2025
36c7806
fix: 카카오 로그인 구현 방식 변경 (프론트에서 인가코드 전달)
koreaioi Jan 8, 2025
ce5ede4
feat: JwtProvider 임시 토큰 생성 추가
koreaioi Jan 8, 2025
197fcf5
feat: JwtService 임시 토큰 응답 메서드
koreaioi Jan 8, 2025
5c1a8a1
feat: KakaoAuthCode Dto 추가
koreaioi Jan 8, 2025
0e5d6a6
feat: application-local, dev 임시토큰 만료기간 환경변수 추가
koreaioi Jan 8, 2025
b6b2ea3
feat: 임시 유저 저장
koreaioi Jan 8, 2025
7dc49bf
feat: 회원가입 절차 중 오류 발생시 임시유저는 생겨나므로 조건에 TEMPORARY 추가
koreaioi Jan 8, 2025
5816cdb
feat: 카카오 로그인 최종 응답 - userId와 헤더에 Token 반환
koreaioi Jan 8, 2025
983b10c
feat: 회원가입 진행이 중단된 유저 엣지 케이스 로직 추가
koreaioi Jan 8, 2025
2038155
feat: TmpMemberDto 추가
koreaioi Jan 8, 2025
a3d2e44
feat: @Valid 추가
koreaioi Jan 8, 2025
c168c7a
feat: Members 경로 삭제
koreaioi Jan 8, 2025
3818a46
remove: 출력문 삭제
koreaioi Jan 8, 2025
9deb9f9
fix: 임시유저 권한 경로 삭제
koreaioi Jan 8, 2025
0fdc43d
fix: 토큰 email Claims에는 더미 데이터 저장
koreaioi Jan 8, 2025
dbaa479
fix: 임시 유저 email DB에는 null 저장
koreaioi Jan 8, 2025
863cd2a
feat: UserStatus - ACTIVE, INACTIVE 추가 및 반영
koreaioi Jan 8, 2025
d6f4cad
remove: 사용하지 않는 상수 제거
koreaioi Jan 8, 2025
feca89d
feat: UserStatus 추가
koreaioi Jan 8, 2025
650522c
remove: 임시유저 권한 경로 삭제
koreaioi Jan 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.web.bind.annotation.*;

import static com.gachtaxi.domain.members.controller.ResponseMessage.*;
import static com.gachtaxi.global.auth.kakao.dto.KaKaoDTO.KakaoAuthCode;
import static com.gachtaxi.global.auth.kakao.dto.KaKaoDTO.OauthKakaoResponse;

@RequestMapping("/auth")
Expand All @@ -22,9 +23,9 @@ public class AuthController {
private final AuthService authService;
private final JwtService jwtService;

@GetMapping("/login/kakao")
public ApiResponse<OauthKakaoResponse> kakaoLogin(@RequestParam("code") String authcode, HttpServletResponse response) {
OauthKakaoResponse res = authService.kakaoLogin(authcode, response);
@PostMapping("/login/kakao")
public ApiResponse<OauthKakaoResponse> kakaoLogin(@RequestBody KakaoAuthCode kakaoAuthCode, HttpServletResponse response) {
koreaioi marked this conversation as resolved.
Show resolved Hide resolved
OauthKakaoResponse res = authService.kakaoLogin(kakaoAuthCode.authCode(), response);
ResponseMessage OAUTH_STATUS = (res.status() == OauthLoginStatus.LOGIN)
? LOGIN_SUCCESS
: UN_REGISTER;
Expand Down
30 changes: 21 additions & 9 deletions src/main/java/com/gachtaxi/domain/members/entity/Members.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,23 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Members extends BaseEntity {

@Column(name = "email", nullable = false, unique = true)
// UserDetails를 만드는 과정에서 null 방지를 위해 임시로 저장하는 이메일
// 추후 로직 개선하겠습니다.
private final static String TMP_EMAIL = "[email protected]";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그냥 null로 저장이 되면 될 것 같은데, 차후에 개선 부탁드릴게요!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 저희는 로직상 이메일 인증 로직이 따로 되어있고, 이메일 인증 로직시 입력받는 가천 이메일의 정보로 유저를 식별 가능하니까,
실제 이메일이 입력되기 전까지는 해당 이메일 필드를 null로 저장하는 것이 더 적합해보이긴합니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실제 DB에서 임시 유저의 Email은 null로 저장하고
토큰을 만들기 위해서는 더미 이메일을 넣어주도록 수정했습니다!


@Column(name = "email", unique = true)
private String email;

@Column(name = "profile_picture")
private String profilePicture;

@Column(name = "nickname", nullable = false)
@Column(name = "nickname")
private String nickname;

@Column(name = "real_name", nullable = false)
@Column(name = "real_name")
private String realName;

@Column(name = "student_number", nullable = false, unique = true)
@Column(name = "student_number", unique = true)
private Long studentNumber;

@Column(name = "phone_number", unique = true) // 피그마 참고, 일단 null 허용
Expand All @@ -49,22 +53,22 @@ public class Members extends BaseEntity {
private Gender gender;

// 이용 약관 동의
@Column(name = "terms_agreement", nullable = false)
@Column(name = "terms_agreement")
@ColumnDefault("true")
private Boolean termsAgreement;

// 개인정보 수집 동의
@Column(name = "privacy_agreement", nullable = false)
@Column(name = "privacy_agreement")
@ColumnDefault("true")
private Boolean privacyAgreement;

// 광고성 정보 수신 동의
@Column(name = "marketing_agreement", nullable = false)
@Column(name = "marketing_agreement")
@ColumnDefault("false")
private Boolean marketingAgreement;

// 2차 인증 (전화번호)
@Column(name = "two_factor_authentication", nullable = false)
@Column(name = "two_factor_authentication")
@ColumnDefault("false")
private Boolean twoFactorAuthentication;

Expand All @@ -76,7 +80,7 @@ public class Members extends BaseEntity {
* */

public static Members of(UserSignUpRequestDto dto){
return Members.builder()
return com.gachtaxi.domain.members.entity.Members.builder()
koreaioi marked this conversation as resolved.
Show resolved Hide resolved
//.profilePicture(dto.profilePicture())
.email(dto.email())
.nickname(dto.nickName())
Expand All @@ -93,4 +97,12 @@ public static Members of(UserSignUpRequestDto dto){
.twoFactorAuthentication(dto.twoFactorAuthentication())
.build();
}

public static Members ofKakaoId(Long kakaoId){
return Members.builder()
.kakaoId(kakaoId)
.email(TMP_EMAIL)
.role(Role.TEMPORARY)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

role은 차후에 admin, user를 구분할 때 쓰여야할 것 같은데, status나 deletedAt을 사용하는 것은 어떨까요?

기본 유저 객체 생성시 @PrePersist로 status를 UNACTIVE로 설정하거나, 아니면 소프트 딜리트가 적용된 상태를 기본 상태로 두고
회원가입이 완료되면 status를 바꾸거나, 소프트 딜리트 된것을 해제하는 식으로요!

이렇게 전역적으로 관리를 해두면 차후 로직에서 user를 조회해올 때 ACTIVE인 유저만 조회하기, 혹은 삭제되지 않은 유저만 조회하기 등으로 일관성있는 개발이 가능할 것 같아요!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 status로 관리하면 좋을 것 같습니다!!
저희가 메일 인증, 전화번호 인증 두 가지 인증이 있으니까 EMAIL_PENDING, PHONE_PENDING... 등의 상태를 만들어서 관리하면 좋을 것 같아요!!

Copy link
Member Author

@koreaioi koreaioi Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACTIVE와 INACTIVE인 UserStatus를 만들도록 하겠습니다!
소프트 딜리트는 좀 더 공부해보도록 하겠습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserStatus는 회원가입 여부 (활성화 상태)만 나타내는 게 좋을거 같아요!
이미 유저 엔티티에 2차 인증 필드가 전화번호 인증을 나타내고 있기 때문에요!

.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.gachtaxi.domain.members.entity.enums;

public enum Role {
MEMBER, ADMIN
TEMPORARY, MEMBER, ADMIN
koreaioi marked this conversation as resolved.
Show resolved Hide resolved
}
18 changes: 16 additions & 2 deletions src/main/java/com/gachtaxi/domain/members/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.gachtaxi.domain.members.service;

import com.gachtaxi.domain.members.dto.request.TmpMemberDto;
import com.gachtaxi.domain.members.entity.Members;
import com.gachtaxi.global.auth.jwt.service.JwtService;
import com.gachtaxi.global.auth.kakao.util.KakaoUtil;
import com.gachtaxi.global.auth.mapper.OauthMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Optional;

import static com.gachtaxi.domain.members.entity.enums.Role.TEMPORARY;
import static com.gachtaxi.global.auth.kakao.dto.KaKaoDTO.*;


Expand All @@ -32,13 +35,24 @@ public OauthKakaoResponse kakaoLogin(String authCode, HttpServletResponse respon

Long kakaoId = userInfo.id();
Optional<Members> optionalMember = memberService.findByKakaoId(kakaoId);
// System.out.println(optionalMember.get().getKakaoId());
koreaioi marked this conversation as resolved.
Show resolved Hide resolved

if(optionalMember.isEmpty()) {
return oauthMapper.toKakaoUnRegisterResponse(userInfo);
TmpMemberDto tmpDto = memberService.saveTmpMember(kakaoId);

jwtService.responseTmpAccessToken(tmpDto, response);
return oauthMapper.toKakaoUnRegisterResponse(tmpDto.userId());
}

// 회원 가입 진행 중 중단된 유저 또한 다시 임시 토큰을 재발급해준다.
if(optionalMember.get().getRole() == TEMPORARY){
TmpMemberDto tmpDto = TmpMemberDto.of(optionalMember.get());
jwtService.responseTmpAccessToken(tmpDto, response);
return oauthMapper.toKakaoUnRegisterResponse(tmpDto.userId());
}

Members member = optionalMember.get();
jwtService.responseJwtToken(member.getId(), member.getEmail(), member.getRole(), response);
return oauthMapper.toKakaoLoginResponse(userInfo, member.getId());
return oauthMapper.toKakaoLoginResponse(member.getId());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.gachtaxi.domain.members.service;

import com.gachtaxi.domain.members.dto.request.TmpMemberDto;
import com.gachtaxi.domain.members.dto.request.UserSignUpRequestDto;
import com.gachtaxi.domain.members.entity.Members;
import com.gachtaxi.domain.members.exception.DuplicatedStudentNumberException;
Expand All @@ -19,6 +20,7 @@ public class MemberService {
private final JwtService jwtService;
private final MemberRepository memberRepository;

//TODO 최종 회원가입 절차에서 사용
@Transactional
public void saveMember(UserSignUpRequestDto dto, HttpServletResponse response) {
checkDuplicatedStudentNumber(dto);
Expand All @@ -27,6 +29,14 @@ public void saveMember(UserSignUpRequestDto dto, HttpServletResponse response) {
jwtService.responseJwtToken(newMember.getId(), newMember.getEmail(), newMember.getRole(), response);
}

// 임시 유저 저장
@Transactional
public TmpMemberDto saveTmpMember(Long kakaoId){
Members tmpMember = Members.ofKakaoId(kakaoId);
memberRepository.save(tmpMember);
return TmpMemberDto.of(tmpMember);
}

public Optional<Members> findByKakaoId(Long kakaoId) {
return memberRepository.findByKakaoId(kakaoId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.gachtaxi.global.auth.jwt.service;

import com.gachtaxi.domain.members.dto.request.TmpMemberDto;
import com.gachtaxi.domain.members.entity.enums.Role;
import com.gachtaxi.global.auth.jwt.dto.JwtTokenDto;
import com.gachtaxi.global.auth.jwt.exception.CookieNotFoundException;
Expand Down Expand Up @@ -36,6 +37,11 @@ public void responseJwtToken(Long userId, String email, Role role, HttpServletRe
setCookie(jwtToken.refreshToken(), response);
}

public void responseTmpAccessToken(TmpMemberDto tmpMemberDto, HttpServletResponse response) {
String tmpAccessToken = jwtProvider.generateTmpAccessToken(tmpMemberDto.userId(), tmpMemberDto.email(), tmpMemberDto.role().name());
setHeader(tmpAccessToken, response);
}

public JwtTokenDto reissueJwtToken(HttpServletRequest request) {
String refreshToken = extractRefreshToken(request);
if(jwtExtractor.isExpired(refreshToken)){
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/gachtaxi/global/auth/jwt/util/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class JwtProvider {
private static final String ID_CLAIM = "id";
private static final String EMAIL_CLAIM = "email";
private static final String ROLE_CLAIM = "role";
private static final String TMP_EMAIL_CLAIM = "tmpEmail";
private final Key key;

public JwtProvider(@Value("${gachtaxi.auth.jwt.key}") String secretKey) {
Expand All @@ -27,6 +28,9 @@ public JwtProvider(@Value("${gachtaxi.auth.jwt.key}") String secretKey) {
@Value("${gachtaxi.auth.jwt.accessTokenExpiration}")
private Long accessTokenExpiration;

@Value("${gachtaxi.auth.jwt.tmpAccessTokenExpiration}")
private Long tmpAccessTokenExpiration;

@Value("${gachtaxi.auth.jwt.refreshTokenExpiration}")
private Long refreshTokenExpiration;

Expand All @@ -42,6 +46,19 @@ public String generateAccessToken(Long id, String email, String role) {
.compact(); // 최종 문자열 생성
}

public String generateTmpAccessToken(Long id, String email, String role) {
return Jwts.builder()
.claim(ID_CLAIM, id)
.claim(EMAIL_CLAIM, email)
.claim(EMAIL_CLAIM, TMP_EMAIL_CLAIM)
.claim(ROLE_CLAIM, role)
.setSubject(ACCESS_TOKEN_SUBJECT) // 사용자 정보(고유 식별자)
.setIssuedAt(new Date()) // 발행 시간
.setExpiration(new Date(System.currentTimeMillis() + tmpAccessTokenExpiration)) // 만료 시간
.signWith(key, SignatureAlgorithm.HS256) // 서명 알고리즘
.compact(); // 최종 문자열 생성
}

public String generateRefreshToken(Long id, String email, String role) {
return Jwts.builder()
.claim(ID_CLAIM, id)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.gachtaxi.global.auth.kakao.dto;

import com.gachtaxi.global.auth.enums.OauthLoginStatus;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;

public class KaKaoDTO {

public record KakaoAuthCode(
@NotBlank String authCode
){}

public record KakaoAccessToken(
String access_token,
String token_type,
Expand Down Expand Up @@ -34,7 +39,6 @@ public record Profile(
@Builder
public record OauthKakaoResponse(
Long userId,
Long kakaoId,
OauthLoginStatus status
){}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@
@Component
public class OauthMapper {

public OauthKakaoResponse toKakaoUnRegisterResponse(KakaoUserInfoResponse userInfo) {
public OauthKakaoResponse toKakaoUnRegisterResponse(Long userId) {
return OauthKakaoResponse.builder()
.kakaoId(userInfo.id())
.userId(userId)
.status(UN_REGISTER)
.build();
}

// jwt 토큰 추가 할 것.
public OauthKakaoResponse toKakaoLoginResponse(KakaoUserInfoResponse userInfo, Long userId) {
public OauthKakaoResponse toKakaoLoginResponse(Long userId) {
return OauthKakaoResponse.builder()
.userId(userId)
.kakaoId(userInfo.id())
.status(LOGIN)
.build();
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/gachtaxi/global/config/PermitUrlConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public String[] getPublicUrl(){
};
}

public String[] getTmpMemberUrl(){
return new String[]{
"/api/tmp-members/**"
};
}

public String[] getMemberUrl(){
return new String[]{

Expand Down
8 changes: 5 additions & 3 deletions src/main/java/com/gachtaxi/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.gachtaxi.global.config;


import com.gachtaxi.domain.members.entity.enums.Role;
import com.gachtaxi.global.auth.jwt.authentication.CustomAccessDeniedHandler;
import com.gachtaxi.global.auth.jwt.authentication.CustomAuthenticationEntryPoint;
import com.gachtaxi.global.auth.jwt.filter.JwtAuthenticationFilter;
Expand All @@ -24,6 +23,8 @@
import java.util.Arrays;
import java.util.List;

import static com.gachtaxi.domain.members.entity.enums.Role.*;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
Expand All @@ -46,8 +47,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti

http.authorizeHttpRequests((auth) -> auth
.requestMatchers(permitUrlConfig.getPublicUrl()).permitAll()
.requestMatchers(permitUrlConfig.getMemberUrl()).hasRole(Role.MEMBER.name())
.requestMatchers(permitUrlConfig.getAdminUrl()).hasRole(Role.ADMIN.name())
.requestMatchers(permitUrlConfig.getTmpMemberUrl()).hasRole(TEMPORARY.name())
.requestMatchers(permitUrlConfig.getMemberUrl()).hasAnyRole(MEMBER.name(), ADMIN.name())
.requestMatchers(permitUrlConfig.getAdminUrl()).hasRole(ADMIN.name())
.anyRequest().authenticated());

http.exceptionHandling(e -> e
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ gachtaxi:
key: ${JWT_SECRET_KEY}
accessTokenExpiration: ${JWT_ACCESS_TOKEN_EXPIRATION}
refreshTokenExpiration: ${JWT_REFRESH_TOKEN_EXPIRATION}
tmpAccessTokenExpiration: ${JWT_TMP_ACCESS_TOKEN_EXPIRATION}
cookieMaxAge: ${JWT_COOKIE_MAX_AGE}
secureOption: ${COOKIE_SECURE_OPTION}
cookiePathOption: ${COOKIE_PATH_OPTION}
1 change: 1 addition & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ gachtaxi:
key: ${JWT_SECRET_KEY}
accessTokenExpiration: ${JWT_ACCESS_TOKEN_EXPIRATION}
refreshTokenExpiration: ${JWT_REFRESH_TOKEN_EXPIRATION}
tmpAccessTokenExpiration: ${JWT_TMP_ACCESS_TOKEN_EXPIRATION}
cookieMaxAge: ${JWT_COOKIE_MAX_AGE}
secureOption: ${COOKIE_SECURE_OPTION}
cookiePathOption: ${COOKIE_PATH_OPTION}
Loading