Skip to content

Commit

Permalink
[BE] 카카오 OAuth 로그인 기능 구현 (#826)
Browse files Browse the repository at this point in the history
* feat: Kakao 로그인 기능 구현

* feat: redirect-uri 변경

* feat: client id 업데이트

* test: 테스트 위치 변경

* feat: 로그인 생성 후 행사 생성 페이지로 리다이렉트

* feat: 카카오 로그인 예외 처리

* refactor: 카카오 로그인 방식 변경

* style: 메소드 이름 변경
  • Loading branch information
Arachneee authored Nov 16, 2024
1 parent d693a5b commit 6ce2a86
Show file tree
Hide file tree
Showing 81 changed files with 1,356 additions and 793 deletions.
1 change: 1 addition & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {

implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'com.auth0:java-jwt:4.4.0'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
33 changes: 27 additions & 6 deletions server/src/main/java/server/haengdong/application/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,43 @@


import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import server.haengdong.domain.TokenProvider;
import server.haengdong.domain.user.Role;
import server.haengdong.exception.AuthenticationException;
import server.haengdong.exception.HaengdongErrorCode;

@Slf4j
public class AuthService {

private static final String TOKEN_NAME = "eventToken";
private static final String TOKEN_NAME = "accessToken";
private static final String CLAIM_SUB = "sub";
private static final String ROLE = "role";

private final TokenProvider tokenProvider;
private final EventService eventService;

public AuthService(TokenProvider tokenProvider) {
public AuthService(TokenProvider tokenProvider, EventService eventService) {
this.tokenProvider = tokenProvider;
this.eventService = eventService;
}

public String createToken(String eventId) {
Map<String, Object> payload = Map.of(CLAIM_SUB, eventId);
public String createGuestToken(Long userId) {
Map<String, Object> payload = Map.of(CLAIM_SUB, userId, ROLE, Role.GUEST);

return tokenProvider.createToken(payload);
}

public String findEventIdByToken(String token) {
public String createMemberToken(Long userId) {
Map<String, Object> payload = Map.of(CLAIM_SUB, userId, ROLE, Role.MEMBER);

return tokenProvider.createToken(payload);
}

public Long findUserIdByToken(String token) {
validateToken(token);
Map<String, Object> payload = tokenProvider.getPayload(token);
return (String) payload.get(CLAIM_SUB);
return (Long) payload.get(CLAIM_SUB);
}

private void validateToken(String token) {
Expand All @@ -38,4 +50,13 @@ private void validateToken(String token) {
public String getTokenName() {
return TOKEN_NAME;
}

public void checkAuth(String eventToken, Long userId) {
boolean hasEvent = eventService.existsByTokenAndUserId(eventToken, userId);

if (!hasEvent) {
log.warn("[행사 접근 불가] Cookie EventId = {}, UserId = {}", eventToken, userId);
throw new AuthenticationException(HaengdongErrorCode.FORBIDDEN);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
import server.haengdong.domain.bill.BillRepository;
import server.haengdong.domain.event.Event;
import server.haengdong.domain.event.EventRepository;
import server.haengdong.domain.member.Member;
import server.haengdong.domain.member.MemberRepository;
import server.haengdong.domain.eventmember.EventMember;
import server.haengdong.domain.eventmember.EventMemberRepository;
import server.haengdong.domain.step.Steps;
import server.haengdong.exception.HaengdongErrorCode;
import server.haengdong.exception.HaengdongException;
Expand All @@ -28,22 +28,22 @@ public class BillService {

private final BillRepository billRepository;
private final EventRepository eventRepository;
private final MemberRepository memberRepository;
private final EventMemberRepository eventMemberRepository;

@Transactional
public void saveBill(String eventToken, BillAppRequest request) {
Event event = getEvent(eventToken);
List<Long> memberIds = request.memberIds();
List<Member> members = memberIds.stream()
List<EventMember> eventMembers = memberIds.stream()
.map(this::findMember)
.toList();

Bill bill = request.toBill(event, members);
Bill bill = request.toBill(event, eventMembers);
billRepository.save(bill);
}

private Member findMember(Long memberId) {
return memberRepository.findById(memberId)
private EventMember findMember(Long memberId) {
return eventMemberRepository.findById(memberId)
.orElseThrow(() -> new HaengdongException(HaengdongErrorCode.MEMBER_NOT_FOUND));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@
import server.haengdong.domain.bill.BillRepository;
import server.haengdong.domain.event.Event;
import server.haengdong.domain.event.EventRepository;
import server.haengdong.domain.member.Member;
import server.haengdong.domain.member.MemberRepository;
import server.haengdong.domain.member.UpdatedMembers;
import server.haengdong.domain.eventmember.EventMember;
import server.haengdong.domain.eventmember.EventMemberRepository;
import server.haengdong.domain.eventmember.UpdatedMembers;
import server.haengdong.exception.HaengdongErrorCode;
import server.haengdong.exception.HaengdongException;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class MemberService {
public class EventMemberService {

private final MemberRepository memberRepository;
private final EventMemberRepository eventMemberRepository;
private final EventRepository eventRepository;
private final BillRepository billRepository;

Expand All @@ -40,12 +40,12 @@ public MembersSaveAppResponse saveMembers(String token, MembersSaveAppRequest re

validateMemberSave(memberNames, event);

List<Member> members = memberNames.stream()
.map(name -> new Member(event, name))
List<EventMember> eventMembers = memberNames.stream()
.map(name -> new EventMember(event, name))
.toList();

List<Member> savedMembers = memberRepository.saveAll(members);
return MembersSaveAppResponse.of(savedMembers);
List<EventMember> savedEventMembers = eventMemberRepository.saveAll(eventMembers);
return MembersSaveAppResponse.of(savedEventMembers);
}

private void validateMemberSave(List<String> memberNames, Event event) {
Expand All @@ -59,7 +59,7 @@ private void validateMemberSave(List<String> memberNames, Event event) {
}

private boolean isDuplicatedMemberNames(Set<String> uniqueMemberNames, Event event) {
return memberRepository.findAllByEvent(event).stream()
return eventMemberRepository.findAllByEvent(event).stream()
.anyMatch(member -> uniqueMemberNames.contains(member.getName()));
}

Expand All @@ -68,7 +68,7 @@ public List<MemberAppResponse> getCurrentMembers(String token) {

return billRepository.findFirstByEventOrderByIdDesc(event)
.map(Bill::getMembers)
.orElseGet(() -> memberRepository.findAllByEvent(event))
.orElseGet(() -> eventMemberRepository.findAllByEvent(event))
.stream()
.map(MemberAppResponse::of)
.toList();
Expand All @@ -77,38 +77,38 @@ public List<MemberAppResponse> getCurrentMembers(String token) {
public MembersDepositAppResponse findAllMembers(String token) {
Event event = getEvent(token);

List<Member> members = memberRepository.findAllByEvent(event);
List<EventMember> eventMembers = eventMemberRepository.findAllByEvent(event);

return MembersDepositAppResponse.of(members);
return MembersDepositAppResponse.of(eventMembers);
}

@Transactional
public void updateMembers(String token, MembersUpdateAppRequest request) {
Event event = getEvent(token);
UpdatedMembers updatedMembers = new UpdatedMembers(request.toMembers(event));
List<Member> originMembers = memberRepository.findAllByEvent(event);
List<EventMember> originEventMembers = eventMemberRepository.findAllByEvent(event);

updatedMembers.validateUpdatable(originMembers);
memberRepository.saveAll(updatedMembers.getMembers());
updatedMembers.validateUpdatable(originEventMembers);
eventMemberRepository.saveAll(updatedMembers.getMembers());
}

@Transactional
public void deleteMember(String token, Long memberId) {
memberRepository.findById(memberId)
eventMemberRepository.findById(memberId)
.ifPresent(member -> deleteMember(token, member));
}

private void deleteMember(String token, Member member) {
Event event = member.getEvent();
private void deleteMember(String token, EventMember eventMember) {
Event event = eventMember.getEvent();
if (event.isTokenMismatch(token)) {
throw new HaengdongException(HaengdongErrorCode.MEMBER_NOT_FOUND);
}

billRepository.findAllByEvent(event).stream()
.filter(bill -> bill.containMember(member))
.forEach(bill -> bill.removeMemberBillDetail(member));
.filter(bill -> bill.containMember(eventMember))
.forEach(bill -> bill.removeMemberBillDetail(eventMember));
billRepository.flush();
memberRepository.delete(member);
eventMemberRepository.delete(eventMember);
}

private Event getEvent(String token) {
Expand Down
43 changes: 32 additions & 11 deletions server/src/main/java/server/haengdong/application/EventService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import server.haengdong.application.request.EventAppRequest;
import server.haengdong.application.request.EventGuestAppRequest;
import server.haengdong.application.request.EventLoginAppRequest;
import server.haengdong.application.request.EventUpdateAppRequest;
import server.haengdong.application.response.EventAppResponse;
Expand All @@ -23,7 +24,8 @@
import server.haengdong.domain.event.EventImage;
import server.haengdong.domain.event.EventImageRepository;
import server.haengdong.domain.event.EventRepository;
import server.haengdong.domain.member.Member;
import server.haengdong.domain.eventmember.EventMember;
import server.haengdong.domain.eventmember.EventMemberRepository;
import server.haengdong.exception.AuthenticationException;
import server.haengdong.exception.HaengdongErrorCode;
import server.haengdong.exception.HaengdongException;
Expand All @@ -39,16 +41,32 @@ public class EventService {
private final RandomValueProvider randomValueProvider;
private final BillRepository billRepository;
private final EventImageRepository eventImageRepository;
private final EventMemberRepository eventMemberRepository;
private final UserService userService;

@Value("${image.base-url}")
private String baseUrl;

@Transactional
public EventAppResponse saveEventGuest(EventGuestAppRequest request) {
Long userId = userService.joinGuest(request.toUserRequest());
String token = randomValueProvider.createRandomValue();
Event event = new Event(request.eventName(), userId, token);
eventRepository.save(event);

eventMemberRepository.save(new EventMember(event, request.nickname()));
return EventAppResponse.of(event);
}

@Transactional
public EventAppResponse saveEvent(EventAppRequest request) {
String token = randomValueProvider.createRandomValue();
Event event = request.toEvent(token);
Event event = new Event(request.name(), request.userId(), token);
eventRepository.save(event);

String nickname = userService.findNicknameById(request.userId());
eventMemberRepository.save(new EventMember(event, nickname));

return EventAppResponse.of(event);
}

Expand All @@ -58,11 +76,10 @@ public EventDetailAppResponse findEvent(String token) {
return EventDetailAppResponse.of(event);
}

public void validatePassword(EventLoginAppRequest request) throws HaengdongException {
public EventAppResponse findByGuestPassword(EventLoginAppRequest request) {
Event event = getEvent(request.token());
if (event.isPasswordMismatch(request.password())) {
throw new AuthenticationException(HaengdongErrorCode.PASSWORD_INVALID);
}
userService.validateUser(event.getUserId(), request.password());
return EventAppResponse.of(event);
}

public List<MemberBillReportAppResponse> getMemberBillReports(String token) {
Expand All @@ -77,14 +94,14 @@ public List<MemberBillReportAppResponse> getMemberBillReports(String token) {
.toList();
}

private MemberBillReportAppResponse createMemberBillReportResponse(Entry<Member, Long> entry) {
Member member = entry.getKey();
private MemberBillReportAppResponse createMemberBillReportResponse(Entry<EventMember, Long> entry) {
EventMember eventMember = entry.getKey();
Long price = entry.getValue();

return new MemberBillReportAppResponse(
member.getId(),
member.getName(),
member.isDeposited(),
eventMember.getId(),
eventMember.getName(),
eventMember.isDeposited(),
price
);
}
Expand Down Expand Up @@ -175,4 +192,8 @@ public List<EventImageSaveAppResponse> findImagesDateBefore(Instant date) {
.map(EventImageSaveAppResponse::of)
.toList();
}

public boolean existsByTokenAndUserId(String eventToken, Long userId) {
return eventRepository.existsByTokenAndUserId(eventToken, userId);
}
}
46 changes: 46 additions & 0 deletions server/src/main/java/server/haengdong/application/KakaoClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package server.haengdong.application;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.http.HttpHeaders;
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.RestClient;
import server.haengdong.application.response.KakaoTokenResponse;
import server.haengdong.config.KakaoProperties;
import server.haengdong.exception.HaengdongErrorCode;
import server.haengdong.exception.HaengdongException;

@RequiredArgsConstructor
@EnableConfigurationProperties(KakaoProperties.class)
@Component
public class KakaoClient {

private final KakaoProperties kakaoProperties;
private final RestClient restClient;

public KakaoTokenResponse join(String code, String redirectUri) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", kakaoProperties.clientId());
params.add("redirect_uri", redirectUri);
params.add("code", code);

try {
return restClient.post()
.uri(kakaoProperties.baseUri() + kakaoProperties.tokenRequestUri())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.body(params)
.retrieve()
.body(KakaoTokenResponse.class);
} catch (Exception e) {
throw new HaengdongException(HaengdongErrorCode.KAKAO_LOGIN_FAIL, e);
}
}

public String getClientId() {
return kakaoProperties.clientId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package server.haengdong.application;

import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import server.haengdong.application.response.KakaoTokenResponse;

@RequiredArgsConstructor
@Service
public class KakaoUserService {

private static final String NICKNAME_KEY = "nickname";

private final UserService userService;
private final KakaoClient kakaoClient;

public Long joinByKakao(String code, String redirectUri) {
KakaoTokenResponse kakaoToken = kakaoClient.join(code, redirectUri);
String idToken = kakaoToken.idToken();
DecodedJWT decodedJWT = JWT.decode(idToken);

String memberNumber = decodedJWT.getSubject();
String nickname = decodedJWT.getClaim(NICKNAME_KEY).asString();

return userService.join(memberNumber, nickname);
}

public String getClientId() {
return kakaoClient.getClientId();
}
}
Loading

0 comments on commit 6ce2a86

Please sign in to comment.