Skip to content
Open
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
DB_USER=root
DB_PW=1234
DB_URL=jdbc:mysql://localhost:3306/umc10th

JWT_SECRET_KEY=BASE64_32

KAKAO_CLIENT_ID=KAKAO_REST_API_KEY
KAKAO_CLIENT_SECRET=KAKAO_CLIENT_SECRET
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
// OAuth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// 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'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.example.umc10th.domain.member.controller;
package com.example.umc10th.domain.auth.controller;

import com.example.umc10th.domain.member.dto.SignupRequestDto;
import com.example.umc10th.domain.member.dto.SignupResponseDto;
import com.example.umc10th.domain.auth.dto.LoginRequestDto;
import com.example.umc10th.domain.auth.dto.LoginResponseDto;
import com.example.umc10th.domain.auth.dto.SignupRequestDto;
import com.example.umc10th.domain.auth.dto.SignupResponseDto;
import com.example.umc10th.domain.auth.service.AuthService;
import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
import com.example.umc10th.domain.member.service.AuthService;
import com.example.umc10th.global.apiPayload.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -24,4 +26,10 @@ public ApiResponse<SignupResponseDto> signup(@Valid @RequestBody SignupRequestDt
SignupResponseDto response = authService.signup(request);
return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_CREATED, response);
}

@PostMapping("/login")
public ApiResponse<LoginResponseDto> login(@Valid @RequestBody LoginRequestDto request) {
LoginResponseDto response = authService.login(request);
return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_LOGIN, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.umc10th.domain.auth.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record LoginRequestDto(
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
String email,

@NotBlank(message = "비밀번호는 필수입니다.")
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.umc10th.domain.auth.dto;

public record LoginResponseDto(
String accessToken
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.umc10th.domain.member.dto;
package com.example.umc10th.domain.auth.dto;

import com.example.umc10th.domain.member.enums.Gender;
import jakarta.validation.Valid;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.umc10th.domain.member.dto;
package com.example.umc10th.domain.auth.dto;

import java.time.LocalDateTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.umc10th.domain.auth.exception;

import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
import com.example.umc10th.global.apiPayload.exception.ProjectException;

public class AuthException extends ProjectException {

public AuthException(BaseErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.umc10th.domain.auth.exception.code;

import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum AuthErrorCode implements BaseErrorCode {
INVALID_LOGIN(HttpStatus.UNAUTHORIZED, "AUTH401", "이메일 또는 비밀번호가 올바르지 않습니다.");

private final HttpStatus status;
private final String code;
private final String message;

AuthErrorCode(HttpStatus status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.example.umc10th.domain.auth.oauth;

import com.example.umc10th.domain.auth.oauth.dto.KakaoDTO;
import com.example.umc10th.domain.auth.oauth.dto.OAuthDTO;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.enums.SocialType;
import com.example.umc10th.domain.member.exception.code.MemberErrorCode;
import com.example.umc10th.domain.member.repository.MemberRepository;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class CustomOAuthService extends DefaultOAuth2UserService {

private static final String KAKAO_ACCOUNT = "kakao_account";
private static final String PROFILE = "profile";
private static final String DEFAULT_NICKNAME_PREFIX = "kakao_user_";

private final MemberRepository memberRepository;

@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuthMember = super.loadUser(userRequest);

SocialType providerId = getProviderId(userRequest);
String socialUid = getRequiredString(oAuthMember.getAttributes(), "id", "Kakao id is required.");

OAuthDTO dto = switch (providerId) {
case KAKAO -> toKakaoDto(oAuthMember, socialUid);
default -> throw unsupportedProvider();
};

Member member = memberRepository.findBySocialTypeAndSocialUid(dto.socialType(), dto.socialUid())
.orElseGet(() -> memberRepository.save(Member.createSocial(
dto.socialType(),
dto.socialUid(),
dto.email(),
dto.nickname(),
dto.profileImageUrl()
)));

return new OAuthMember(member, oAuthMember.getAttributes());
}

private SocialType getProviderId(OAuth2UserRequest userRequest) {
try {
return SocialType.valueOf(userRequest.getClientRegistration()
.getRegistrationId()
.toUpperCase());
} catch (IllegalArgumentException e) {
throw unsupportedProvider();
}
}

@SuppressWarnings("unchecked")
private KakaoDTO toKakaoDto(OAuth2User oAuthMember, String socialUid) {
Map<String, Object> attributes = oAuthMember.getAttributes();
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get(KAKAO_ACCOUNT);
if (kakaoAccount == null) {
throw invalidUserInfo("Kakao account is required.");
}

Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get(PROFILE);
String email = getRequiredString(kakaoAccount, "email", "Kakao email is required.");
String nickname = getString(profile, "nickname");
String profileImageUrl = getString(profile, "profile_image_url");

if (nickname == null || nickname.isBlank()) {
nickname = DEFAULT_NICKNAME_PREFIX + socialUid;
}

return new KakaoDTO(socialUid, email, nickname, profileImageUrl);
}

private String getRequiredString(Map<String, Object> attributes, String key, String message) {
String value = getString(attributes, key);
if (value == null || value.isBlank()) {
throw invalidUserInfo(message);
}
return value;
}

private String getString(Map<String, Object> attributes, String key) {
if (attributes == null) {
return null;
}
Object value = attributes.get(key);
return value != null ? String.valueOf(value) : null;
}

private OAuth2AuthenticationException unsupportedProvider() {
OAuth2Error error = new OAuth2Error(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER.getCode());
return new OAuth2AuthenticationException(error, MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER.getMessage());
}

private OAuth2AuthenticationException invalidUserInfo(String message) {
OAuth2Error error = new OAuth2Error("OAUTH_INVALID_USER_INFO");
return new OAuth2AuthenticationException(error, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.example.umc10th.domain.auth.oauth;

import com.example.umc10th.domain.member.entity.Member;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

public class OAuthMember implements OAuth2User {

private static final String DEFAULT_ROLE = "ROLE_USER";

private final Member member;
private final Map<String, Object> attributes;
private final Collection<? extends GrantedAuthority> authorities;

public OAuthMember(Member member, Map<String, Object> attributes) {
this.member = member;
this.attributes = attributes;
this.authorities = List.of(new SimpleGrantedAuthority(DEFAULT_ROLE));
}

public Member getMember() {
return member;
}

@Override
public Map<String, Object> getAttributes() {
return attributes;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public String getName() {
return member.getSocialUid();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.umc10th.domain.auth.oauth;

import com.example.umc10th.domain.auth.dto.LoginResponseDto;
import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.security.AuthMember;
import com.example.umc10th.global.security.JwtUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class OAuthSuccessHandler implements AuthenticationSuccessHandler {

private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;

@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException, ServletException {
OAuthMember oAuthMember = (OAuthMember) authentication.getPrincipal();
String accessToken = jwtUtil.createAccessToken(AuthMember.from(oAuthMember.getMember()));

response.setStatus(MemberSuccessCode.MEMBER_LOGIN.getStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());

ApiResponse<LoginResponseDto> responseBody = ApiResponse.onSuccess(
MemberSuccessCode.MEMBER_LOGIN,
new LoginResponseDto(accessToken)
);
objectMapper.writeValue(response.getWriter(), responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.umc10th.domain.auth.oauth.dto;

import com.example.umc10th.domain.member.enums.SocialType;

public record KakaoDTO(
String socialUid,
String email,
String nickname,
String profileImageUrl
) implements OAuthDTO {

@Override
public SocialType socialType() {
return SocialType.KAKAO;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.umc10th.domain.auth.oauth.dto;

import com.example.umc10th.domain.member.enums.SocialType;

public interface OAuthDTO {

SocialType socialType();

String socialUid();

String email();

String nickname();

String profileImageUrl();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.umc10th.domain.auth.service;

import com.example.umc10th.domain.auth.dto.LoginRequestDto;
import com.example.umc10th.domain.auth.dto.LoginResponseDto;
import com.example.umc10th.domain.auth.dto.SignupRequestDto;
import com.example.umc10th.domain.auth.dto.SignupResponseDto;

public interface AuthService {

SignupResponseDto signup(SignupRequestDto request);

LoginResponseDto login(LoginRequestDto request);
}
Loading