Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ dependencies {
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'

// OAuth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

tasks.named('test') {
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/umc/domain/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
import org.springframework.web.bind.annotation.RestController;
import umc.domain.auth.dto.AuthReqDTO;
import umc.domain.auth.dto.AuthResDTO;
import umc.domain.auth.exception.code.AuthSuccessCode;
import umc.domain.auth.service.AuthService;
import umc.domain.member.dto.MemberReqDTO;
import umc.domain.member.dto.MemberResDTO;
import umc.domain.member.exception.code.MemberSuccessCode;
import umc.global.apiPayload.ApiResponse;

@RestController
Expand All @@ -26,6 +24,14 @@ public ApiResponse<AuthResDTO.SignUpDTO> signUp(
@RequestBody @Valid AuthReqDTO.SignUpDTO reqDto
){
AuthResDTO.SignUpDTO resDto = authService.signUp(reqDto);
return ApiResponse.onSuccess(MemberSuccessCode.CREATED, resDto);
return ApiResponse.onSuccess(AuthSuccessCode.SIGN_UP, resDto);
}

@PostMapping("/login")
public ApiResponse<AuthResDTO.LoginDTO> login(
@RequestBody @Valid AuthReqDTO.LoginDTO reqDto
) {
AuthResDTO.LoginDTO resDto = authService.login(reqDto);
return ApiResponse.onSuccess(AuthSuccessCode.LOGIN_SUCCESS, resDto);
}
}
14 changes: 10 additions & 4 deletions src/main/java/umc/domain/auth/dto/AuthReqDTO.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package umc.domain.auth.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.*;
import umc.domain.member.enums.Gender;

import java.util.List;
Expand All @@ -27,6 +24,7 @@ public record SignUpDTO(
@NotBlank
String address,
@Valid
@NotEmpty
List<TermDTO> terms,
@Valid
List<FoodPreferenceDTO> foodPreferences
Expand All @@ -43,4 +41,12 @@ public record FoodPreferenceDTO(
Long foodId
) {}
}

public record LoginDTO(
@NotBlank
@Email
String email,
@NotBlank
String password
) {}
}
5 changes: 5 additions & 0 deletions src/main/java/umc/domain/auth/dto/AuthResDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ public record SignUpDTO(
LocalDate birth,
String address
) {}

@Builder
public record LoginDTO(
String accessToken
) {}
}
18 changes: 13 additions & 5 deletions src/main/java/umc/domain/auth/exception/code/AuthErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@
@AllArgsConstructor
public enum AuthErrorCode implements BaseErrorCode {

DUPLICATED_EMAIL(HttpStatus.NOT_FOUND, "AUTH400_1", "이미 가입되어있는 이메일입니다."),
REQUIRED_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "AUTH404_1", "필수 약관은 동의해야 합니다."),
INVALID_TERM(HttpStatus.BAD_REQUEST, "AUTH404_2", "유효하지 않은 약관입니다."),
INVALID_FOOD(HttpStatus.BAD_REQUEST, "AUTH404_3", "유효하지 않은 음식입니다."),
TERMS_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH404_4", "모든 약관에 대한 동의 여부가 필요합니다."),
// 400
REQUIRED_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "AUTH400_1", "필수 약관은 동의해야 합니다."),
INVALID_TERM(HttpStatus.BAD_REQUEST, "AUTH400_2", "유효하지 않은 약관입니다."),
INVALID_FOOD(HttpStatus.BAD_REQUEST, "AUTH400_3", "유효하지 않은 음식입니다."),
TERMS_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH400_4", "모든 약관에 대한 동의 여부가 필요합니다."),
NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH400_5", "지원하지 않는 소셜 로그인입니다."),

// 401
OAUTH_MISSING_ATTRIBUTES(HttpStatus.UNAUTHORIZED, "AUTH401_1", "소셜 로그인 필수 정보가 누락되었습니다."),
INVALID_LOGIN_FORM(HttpStatus.UNAUTHORIZED, "AUTH401_2", "아이디나 비밀번호가 틀렸습니다."),

// 409
DUPLICATED_EMAIL(HttpStatus.CONFLICT, "AUTH409_1", "이미 가입되어있는 이메일입니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
package umc.domain.auth.exception.code;

public enum AuthSuccessCode {
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import umc.global.apiPayload.code.BaseSuccessCode;

@Getter
@AllArgsConstructor
public enum AuthSuccessCode implements BaseSuccessCode {

// 200
LOGIN_SUCCESS(HttpStatus.OK, "AUTH200_1", "로그인이 성공적으로 완료되었습니다."),

// 201
SIGN_UP(HttpStatus.CREATED, "AUTH201_1", "회원가입이 완료되었습니다."),
;

private final HttpStatus status;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package umc.domain.auth.oauth;

import lombok.Getter;
import umc.domain.auth.exception.AuthException;
import umc.domain.auth.exception.code.AuthErrorCode;

@Getter
public class OAuthAttributeMissingException extends AuthException {

private final String attribute;

public OAuthAttributeMissingException(String attribute) {
super(AuthErrorCode.OAUTH_MISSING_ATTRIBUTES);
this.attribute = attribute;
}
}
54 changes: 54 additions & 0 deletions src/main/java/umc/domain/auth/oauth/OAuthAttributes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package umc.domain.auth.oauth;

import umc.domain.auth.exception.AuthException;
import umc.domain.auth.exception.code.AuthErrorCode;
import umc.domain.auth.oauth.dto.KakaoDTO;
import umc.domain.auth.oauth.dto.OAuthDTO;
import umc.domain.member.enums.SocialType;

import java.util.Map;

public class OAuthAttributes {

public static OAuthDTO of(String registrationId, Map<String, Object> rawAttributes) {
SocialType provider;
try {
provider = SocialType.valueOf(registrationId.toUpperCase());
} catch (IllegalArgumentException e) {
throw new AuthException(AuthErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER);
}
return switch (provider) {
case KAKAO -> ofKakao(rawAttributes);
default -> throw new AuthException(AuthErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER);
};
}

private static OAuthDTO ofKakao(Map<String, Object> rawAttributes) {
String id = requireString(rawAttributes, "id");

Map<String, Object> kakaoAccount = requireMap(rawAttributes, "kakao_account");
String email = requireString(kakaoAccount, "email");

Map<String, Object> profile = requireMap(kakaoAccount, "profile");
String nickname = requireString(profile, "nickname");

return new KakaoDTO(id, email, nickname);
}

private static String requireString(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null || value.toString().isBlank()) {
throw new OAuthAttributeMissingException(key);
}
return value.toString();
}

@SuppressWarnings("unchecked")
private static Map<String, Object> requireMap(Map<String, Object> map, String key) {
Object value = map.get(key);
if (!(value instanceof Map<?, ?>)) {
throw new OAuthAttributeMissingException(key);
}
return (Map<String, Object>) value;
}
}
22 changes: 22 additions & 0 deletions src/main/java/umc/domain/auth/oauth/OAuthMemberService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package umc.domain.auth.oauth;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import umc.domain.auth.oauth.dto.OAuthDTO;
import umc.domain.member.converter.MemberConverter;
import umc.domain.member.entity.Member;
import umc.domain.member.repository.MemberRepository;

@Service
@RequiredArgsConstructor
public class OAuthMemberService {

private final MemberRepository memberRepository;

@Transactional
public Member loadOrCreate(OAuthDTO oAuthDto) {
return memberRepository.findBySocialTypeAndSocialUid(oAuthDto.getSocialType(), oAuthDto.getSocialUid())
.orElseGet(() -> memberRepository.save(MemberConverter.toMember(oAuthDto)));
}
}
32 changes: 32 additions & 0 deletions src/main/java/umc/domain/auth/oauth/dto/KakaoDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package umc.domain.auth.oauth.dto;

import lombok.RequiredArgsConstructor;
import umc.domain.member.enums.SocialType;

@RequiredArgsConstructor
public class KakaoDTO implements OAuthDTO {

private final String id;
private final String email;
private final String name;

@Override
public SocialType getSocialType() {
return SocialType.KAKAO;
}

@Override
public String getSocialUid() {
return id;
}

@Override
public String getEmail() {
return email;
}

@Override
public String getName() {
return name;
}
}
10 changes: 10 additions & 0 deletions src/main/java/umc/domain/auth/oauth/dto/OAuthDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package umc.domain.auth.oauth.dto;

import umc.domain.member.enums.SocialType;

public interface OAuthDTO {
SocialType getSocialType();
String getSocialUid();
String getEmail();
String getName();
}
41 changes: 35 additions & 6 deletions src/main/java/umc/domain/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package umc.domain.auth.service;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -15,6 +19,8 @@
import umc.domain.member.repository.FoodRepository;
import umc.domain.member.repository.MemberRepository;
import umc.domain.member.repository.TermRepository;
import umc.global.security.entity.AuthMember;
import umc.global.security.util.JwtUtil;

import java.util.List;
import java.util.Map;
Expand All @@ -30,6 +36,8 @@ public class AuthService {
private final PasswordEncoder passwordEncoder;
private final TermRepository termRepository;
private final FoodRepository foodRepository;
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;

@Transactional
public AuthResDTO.SignUpDTO signUp(AuthReqDTO.SignUpDTO reqDto) {
Expand All @@ -47,6 +55,22 @@ public AuthResDTO.SignUpDTO signUp(AuthReqDTO.SignUpDTO reqDto) {
return AuthConverter.toSignUpDTO(member);
}

public AuthResDTO.LoginDTO login(AuthReqDTO.LoginDTO reqDto) {
Authentication authentication;

try {
authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(reqDto.email(), reqDto.password())
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

일반 로그인에서 AuthenticationManager.authenticate()를 사용하셨군요!

저는 직접 findByEmailpasswordEncoder.matches()로 검증하는 흐름을 먼저 떠올렸는데, 이 방식은 Spring Security 인증 흐름을 그대로 따라 UserDetailsService, PasswordEncoder, provider 구조와 연결된다는 장점이 있는 것 같아요,

저도 이 방식으로 리팩토링해보면 좋을 것 같습니다! ☺️

} catch (AuthenticationException e) {
throw new AuthException(AuthErrorCode.INVALID_LOGIN_FORM);
}

String accessToken = jwtUtil.createAccessToken((AuthMember) authentication.getPrincipal());

return new AuthResDTO.LoginDTO(accessToken);
}

private void addTermsToMember(Member member, List<AuthReqDTO.SignUpDTO.TermDTO> termDTOs) {
List<Term> allTerms = termRepository.findAll();

Expand All @@ -58,7 +82,7 @@ private void addTermsToMember(Member member, List<AuthReqDTO.SignUpDTO.TermDTO>
.map(AuthReqDTO.SignUpDTO.TermDTO::termId)
.collect(Collectors.toSet());

if (!allTermIds.containsAll(requestedTermIds)) {
if (termDTOs.size() != requestedTermIds.size() || !allTermIds.equals(requestedTermIds)) {
throw new AuthException(AuthErrorCode.TERMS_MISMATCH);
}

Expand All @@ -75,10 +99,15 @@ private void addTermsToMember(Member member, List<AuthReqDTO.SignUpDTO.TermDTO>
}

private void addFoodsToMember(Member member, List<AuthReqDTO.SignUpDTO.FoodPreferenceDTO> foodDTOs) {
foodDTOs.forEach(foodDTO -> {
Food food = foodRepository.findById(foodDTO.foodId())
.orElseThrow(() -> new AuthException(AuthErrorCode.INVALID_FOOD));
member.addPreferenceFood(food);
});
List<Long> foodIds = foodDTOs.stream()
.map(AuthReqDTO.SignUpDTO.FoodPreferenceDTO::foodId)
.toList();

List<Food> foods = foodRepository.findAllById(foodIds);
if (foodIds.size() != foods.size()) {
throw new AuthException(AuthErrorCode.INVALID_FOOD);
}

foods.forEach(member::addPreferenceFood);
}
}
15 changes: 10 additions & 5 deletions src/main/java/umc/domain/member/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package umc.domain.member.controller;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import umc.domain.member.dto.MemberReqDTO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import umc.domain.member.dto.MemberResDTO;
import umc.domain.member.exception.code.MemberSuccessCode;
import umc.domain.member.service.MemberService;
import umc.global.apiPayload.ApiResponse;
import umc.global.security.entity.AuthMember;

import java.time.LocalDate;

Expand Down Expand Up @@ -40,8 +43,10 @@ public ApiResponse<MemberResDTO.HomeViewDTO> getHome(
}

@GetMapping("/me")
public ApiResponse<MemberResDTO.MyPageViewDTO> getMyPage(){
MemberResDTO.MyPageViewDTO resDto = memberService.getMyPage(1L);
public ApiResponse<MemberResDTO.MyPageViewDTO> getMyPage(
@AuthenticationPrincipal AuthMember authMember
){
MemberResDTO.MyPageViewDTO resDto = memberService.getMyPage(authMember.getMember().getId());
return ApiResponse.onSuccess(MemberSuccessCode.MY_PAGE_VIEW, resDto);
}
}
Loading