Skip to content
Open
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
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