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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'

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

tasks.named('test') {
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/example/umc9th/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.umc9th.config;

import com.example.umc9th.domain.member.service.command.CustomOAuth2UserService;
import com.example.umc9th.global.jwt.JwtFilter;
import com.example.umc9th.global.jwt.JwtUtil;
import com.example.umc9th.global.auth.CustomUserDetailsService;
Expand All @@ -8,6 +9,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
Expand Down Expand Up @@ -36,11 +38,13 @@ public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
private final CustomEntryPoint customEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomOAuth2UserService customOAuth2UserService;

// μ•„λž˜ 3κ°œλŠ” Swagger에 λŒ€ν•œ URL
private String[] allowUrl = {
"/auth/sign-up",
"/auth/login", // 둜그인 URL μΆ”κ°€
"/oauth2/**",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
Expand All @@ -64,6 +68,12 @@ SecurityFilterChain filterChain(HttpSecurity http, CorsConfigurationSource corsC
.authenticationEntryPoint(customEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
)
.oauth2Login((oauth2) -> oauth2
.defaultSuccessUrl("/swagger-ui/index.html", true) // 둜그인 성곡 μ‹œ λ¦¬λ””λ ‰μ…˜λ  경둜
.userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
.userService(customOAuth2UserService))
.redirectionEndpoint(endpoint -> endpoint
.baseUri("/oauth2/callback/*")))
// // formLogin μ„€μ •
// .formLogin(formLogin -> formLogin
// // Form loginμ—μ„œ μ‚¬μš©ν•˜λŠ” SecurityContextRepository μ„€μ •
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,34 @@
import com.example.umc9th.domain.member.dto.res.MemberResponseDTO;
import com.example.umc9th.domain.member.entity.Member;
import com.example.umc9th.domain.member.service.command.MemberCommandService;
import com.example.umc9th.domain.member.service.command.OAuth2Service;
import com.example.umc9th.global.apiPayload.ApiResponse;
import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;


@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class MemberController {

private final MemberCommandService memberCommandService;
private final OAuth2Service oAuth2Service;

@PostMapping("/sign-up")
@PostMapping("/auth/sign-up")
public ApiResponse<MemberResponseDTO.SignUpResponseDTO> signUp(@RequestBody MemberRequestDTO.SignUpRequestDTO dto) {
Member member = memberCommandService.signUp(dto);
MemberResponseDTO.SignUpResponseDTO responseDTO = MemberResponseDTO.SignUpResponseDTO.from(member);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, responseDTO);
}

@PostMapping("/login")
@PostMapping("/auth/login")
public ApiResponse<MemberResponseDTO.LoginResponseDTO> signin(@RequestBody MemberRequestDTO.LoginRequestDTO dto){
MemberResponseDTO.LoginResponseDTO responseDTO = memberCommandService.login(dto);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, responseDTO);
}
// @GetMapping("/oauth2/callback/kakao")
// public ApiResponse<MemberResponseDTO.LoginResponseDTO> loginWithKakao(@RequestParam("code") String code) {
// return ApiResponse.onSuccess(GeneralSuccessCode.OK, oAuth2Service.login(code));
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.umc9th.domain.member.dto.res;

import com.example.umc9th.domain.member.entity.ROLE;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

@RequiredArgsConstructor
public class CustomOAuth2User implements OAuth2User {

private final OAuth2Response oAuthResponse;
private final ROLE role;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();

collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return role.toString();
}
});

return collection;
}

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

@Override
public String getName() {
return oAuthResponse.getEmail();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.umc9th.domain.member.dto.res;

import lombok.RequiredArgsConstructor;

import java.util.Map;

@RequiredArgsConstructor
public class KakaoResponse implements OAuth2Response {

private final Map<String, Object> attribute;


@Override
public String getProvider() {
return "kakao";
}

@Override
public String getProviderId() {
return attribute.get("id").toString();
}

@Override
public String getEmail() {
Map<String, Object> account = (Map<String, Object>) attribute.get("kakao_account");
if (account == null) {
return null;
}
return account.get("email").toString();
}

@Override
public String getName() {
Map<String, Object> account = (Map<String, Object>) attribute.get("kakao_account");
if (account == null) {
return null;
}
Map<String, Object> profile = (Map<String, Object>) account.get("profile");

if (profile == null) {
return null;
}

return profile.get("nickname").toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.example.umc9th.domain.member.dto.res;

import lombok.Getter;

public class OAuth2DTO {

@Getter
public static class OAuth2TokenDTO {
String token_type;
String access_token;
String refresh_token;
Long expires_in;
Long refresh_token_expires_in;
String scope;
}

@Getter
public static class KakaoProfile {
private Long id;
private String connected_at;
private Properties properties;
private KakaoAccount kakao_account;

@Getter
public class Properties {
private String nickname;
private String profile_image;
private String thumbnail_image;
}

@Getter
public class KakaoAccount {
private String email;
private Boolean is_email_verified;
private Boolean email_needs_agreement;
private Boolean has_email;
private Boolean profile_nickname_needs_agreement;
private Boolean profile_image_needs_agreement;
private Boolean email_needs_argument;
private Boolean is_email_valid;
private Profile profile;

@Getter
public class Profile {
private String nickname;
private String thumbnail_image_url;
private String profile_image_url;
private Boolean is_default_nickname;
private Boolean is_default_image;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.umc9th.domain.member.dto.res;

public interface OAuth2Response {

String getProvider();
String getProviderId();
String getEmail();
String getName();
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ public class Member {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

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

@Column(name = "username", unique = true, nullable = false)
private String username;

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

@Column(name = "role")
@Enumerated(EnumType.STRING)
@Builder.Default
private ROLE role = ROLE.USER;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.umc9th.domain.member.entity;

public enum ROLE {
USER;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ public enum MemberErrorCode implements BaseErrorCode {

//401
BAD_CREDENTIAL(HttpStatus.UNAUTHORIZED, "MEMBER401_1", "μœ νš¨ν•œ 인증 자격이 μ•„λ‹™λ‹ˆλ‹€."),
OAUTH_TOKEN_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401_2", "μ†Œμ…œ 둜그인 μ‹€νŒ¨"),
OAUTH_USER_INFO_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER401_3", "μ†Œμ…œ λ‘œκ·ΈμΈμ—μ„œ μ‚¬μš©μž 정보λ₯Ό κ°€μ Έμ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."),

//404
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "ν•΄λ‹Ή ID의 맴버λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.");
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "ν•΄λ‹Ή ID의 맴버λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
;

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByUsername(String username);

Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.umc9th.domain.member.service.command;

import com.example.umc9th.domain.member.dto.res.CustomOAuth2User;
import com.example.umc9th.domain.member.dto.res.KakaoResponse;
import com.example.umc9th.domain.member.dto.res.OAuth2Response;
import com.example.umc9th.domain.member.entity.Member;
import com.example.umc9th.domain.member.entity.ROLE;
import com.example.umc9th.domain.member.exception.code.MemberErrorCode;
import com.example.umc9th.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

private final MemberRepository memberRepository;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
OAuth2User oAuth2User = super.loadUser(userRequest); // λΆ€λͺ¨ 클래슀의 λ©”μ„œλ“œλ₯Ό 톡해 μœ μ € 정보λ₯Ό λ°›μ•„μ˜΄
log.info("getAttributes : {}", oAuth2User.getAttributes());

String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuthResponse = null;
if(registrationId.equals("kakao")){
oAuthResponse = new KakaoResponse(oAuth2User.getAttributes());
}
else{
// λ‹€λ₯Έ μ†Œμ…œ 둜그인 μ‹œ ν™•μž₯ κ°€λŠ₯
throw new OAuth2AuthenticationException(MemberErrorCode.OAUTH_USER_INFO_FAIL.getCode());
}
String email = oAuthResponse.getEmail();
Member existData = memberRepository.findByEmail(email).orElse(null);
ROLE role = ROLE.USER;

if(existData == null){
Member member = Member.builder()
.username(oAuthResponse.getName())
.email(email)
.password(null)
.role(role)
.build();
memberRepository.save(member);
}
return new CustomOAuth2User(oAuthResponse, role);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.umc9th.domain.member.service.command;

import com.example.umc9th.domain.member.dto.res.MemberResponseDTO;

public interface OAuth2Service {

MemberResponseDTO.LoginResponseDTO login(String code);
}
Loading