Skip to content

Commit

Permalink
OAuth2를 이용한 소셜 로그인 구현 (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
kmw2378 authored Mar 7, 2024
2 parents 2c35951 + 7cd4454 commit 7adc5e9
Show file tree
Hide file tree
Showing 33 changed files with 823 additions and 49 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

implementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock:4.0.4")

compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
public class SecurityConfig {
private static final String ORIGIN_PATTERN = "*";
private static final String CORS_CONFIGURATION_PATTERN = "/**";

public static final String API_V_1 = "/api/v1/";

@Bean
public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
http.httpBasic().disable()
Expand All @@ -29,6 +30,7 @@ public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception
.and()
.authorizeHttpRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.requestMatchers(API_V_1 + "oauth/login").permitAll()
.requestMatchers(API_V_1 + "categories/**").permitAll()
.requestMatchers(API_V_1+"products/**").permitAll()
.requestMatchers(PathRequest.toH2Console()).permitAll()//TODO 2024 03 02 19:39:16 : 개발단계 이후 제거 요망
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.kakaoshare.backend.common.util;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.Arrays;

public class EnumValidator implements ConstraintValidator<EnumValue, String> {
private EnumValue enumValue;

@Override
public void initialize(final EnumValue constraintAnnotation) {
this.enumValue = constraintAnnotation;
}

@Override
public boolean isValid(final String value, final ConstraintValidatorContext context) {
final Enum<?>[] enumConstants = enumValue.enumClass().getEnumConstants();
if (enumConstants == null) {
return false;
}

return Arrays.stream(enumConstants)
.anyMatch(enumConstant -> convertible(value, enumConstant));
}

private boolean convertible(final String value, final Enum<?> enumConstant) {
final String trimValue = value.trim();
if (enumValue.ignoreCase()) {
return trimValue.equalsIgnoreCase(enumConstant.name());
}

return trimValue.equals(enumConstant.name());
}
}
20 changes: 20 additions & 0 deletions src/main/java/org/kakaoshare/backend/common/util/EnumValue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.kakaoshare.backend.common.util;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = EnumValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumValue {
Class<? extends Enum<?>> enumClass();
String message() default "";
Class<?> [] groups() default {};
Class<? extends Payload>[] payload() default {};
boolean ignoreCase() default false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.kakaoshare.backend.common.util;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.util.Map;

public final class MultiValueMapConverter {
private static final ObjectMapper MAPPER = new ObjectMapper();

private MultiValueMapConverter() {

}

public static MultiValueMap<String, String> convert(final Object object) {
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
try {
final Map<String, String> convertedValue = MAPPER.convertValue(object, new TypeReference<Map<String, String>>() {
});
params.setAll(convertedValue);
return params;
} catch (Exception e) {
throw new IllegalArgumentException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.kakaoshare.backend.domain.member.controller;

import lombok.RequiredArgsConstructor;
import org.kakaoshare.backend.domain.member.dto.oauth.authenticate.OAuthLoginRequest;
import org.kakaoshare.backend.domain.member.dto.oauth.authenticate.OAuthLoginResponse;
import org.kakaoshare.backend.domain.member.service.oauth.OAuthService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RequestMapping("/api/v1/oauth")
@RestController
public class OAuthController {
private final OAuthService oAuthService;

@PostMapping("/login")
public ResponseEntity<OAuthLoginResponse> login(@ModelAttribute final OAuthLoginRequest oAuthAuthenticateRequest) {
return ResponseEntity.ok(oAuthService.login(oAuthAuthenticateRequest));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.kakaoshare.backend.domain.member.dto.oauth.authenticate;

public record OAuthLoginRequest(String provider, String code) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.kakaoshare.backend.domain.member.dto.oauth.authenticate;

import lombok.Builder;

@Builder
public record OAuthLoginResponse(String grantType, String accessToken) {
public static OAuthLoginResponse of(final String grantType,
final String accessToken) {
return OAuthLoginResponse.builder()
.grantType(grantType)
.accessToken(accessToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.kakaoshare.backend.domain.member.dto.oauth.profile;

import org.kakaoshare.backend.common.util.EnumValue;
import org.kakaoshare.backend.domain.member.entity.Gender;
import org.kakaoshare.backend.domain.member.entity.Member;

import java.util.Map;

public abstract class OAuthProfile {
protected final Map<String, Object> attributes;

protected OAuthProfile(final Map<String, Object> attributes) {
this.attributes = attributes;
}

@EnumValue(enumClass = Gender.class, message = "성별이 잘못되었습니다.", ignoreCase = true)
public abstract String getGender();
public abstract String getName();
public abstract String getPhoneNumber();
public abstract String getProvider();
public abstract String getProviderId();

public Member toEntity() {
return Member.builder()
.name(getName())
.gender(Gender.from(getGender()))
.phoneNumber(getPhoneNumber())
.providerId(getProviderId())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.kakaoshare.backend.domain.member.dto.oauth.profile;

import org.kakaoshare.backend.domain.member.dto.oauth.profile.detail.KakaoProfile;

import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;

public enum OAuthProfileFactory {
KAKAO("kakao", KakaoProfile::new);

private final String registrationId;
private final Function<Map<String, Object>, OAuthProfile> mapper;

OAuthProfileFactory(final String provider,
final Function<Map<String, Object>, OAuthProfile> mapper) {
this.registrationId = provider;
this.mapper = mapper;
}

public static OAuthProfile of(final Map<String, Object> attributes,
final String registrationId) {
return Arrays.stream(values())
.filter(value -> value.registrationId.equals(registrationId))
.findAny()
.map(value -> value.mapper.apply(attributes))
.orElseThrow(() -> new IllegalArgumentException());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.kakaoshare.backend.domain.member.dto.oauth.profile.detail;

import org.kakaoshare.backend.domain.member.dto.oauth.profile.OAuthProfile;

import java.util.Map;

public class KakaoProfile extends OAuthProfile {
public KakaoProfile(final Map<String, Object> attributes) {
super(attributes);
}

@Override
public String getGender() {
return String.valueOf(getAccount().get("gender"));
}

@Override
public String getName() {
return String.valueOf(getAccount().get("name"));
}

@Override
public String getPhoneNumber() {
return String.valueOf(getAccount().get("phone_number"));
}

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

@Override
public String getProviderId() {
return String.valueOf(attributes.get("id"));
}

private Map<String, Object> getAccount() {
return (Map<String, Object>) attributes.get("kakao_account");
}

private Map<String, Object> getProfile() {
return (Map<String, Object>) getAccount().get("profile");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.kakaoshare.backend.domain.member.dto.oauth.token;

import lombok.Builder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;

@Builder
public record OAuthTokenRequest(String code, String client_id, String client_secret, String grant_type, String redirect_id) {
public static OAuthTokenRequest of(final ClientRegistration registration,
final String code) {
return OAuthTokenRequest.builder()
.code(code)
.client_id(registration.getClientId())
.client_secret(registration.getClientSecret())
.grant_type(registration.getAuthorizationGrantType().getValue())
.redirect_id(registration.getRedirectUri())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.kakaoshare.backend.domain.member.dto.oauth.token;

import lombok.Builder;

@Builder
public record OAuthTokenResponse(String access_token, String refresh_token, Long expires_in) {
public static OAuthTokenResponse of(final String accessToken) {
return OAuthTokenResponse.builder()
.access_token(accessToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package org.kakaoshare.backend.domain.member.entity;

import java.util.Arrays;

public enum Gender {
MALE, FEMALE
MALE, FEMALE;

public static Gender from(final String gender) {
return Arrays.stream(values())
.filter(value -> value.name().equals(gender.toUpperCase()))
.findAny()
.orElseThrow(() -> new IllegalArgumentException());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,25 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.kakaoshare.backend.domain.base.entity.BaseTimeEntity;
import org.kakaoshare.backend.domain.funding.entity.Funding;
import org.kakaoshare.backend.domain.order.entity.Order;

import java.util.List;

import static org.kakaoshare.backend.domain.member.entity.Role.USER;


@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;

@Column(nullable = false)
private String email;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Gender gender;
Expand All @@ -44,19 +40,39 @@ public class Member extends BaseTimeEntity {
@Column(nullable = false)
private String providerId;

@Builder.Default
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Role role = USER;

@OneToMany(mappedBy = "member")
private List<Order> orders;

@OneToMany(mappedBy = "member")
private List<Funding> funding;

protected Member() {

}

@Builder
public Member(final Long memberId, final String email, final Gender gender, final String name, final String phoneNumber, final String providerId) {
public Member(final Long memberId, final Gender gender, final String name, final String phoneNumber, final String providerId) {
this.memberId = memberId;
this.email = email;
this.gender = gender;
this.name = name;
this.phoneNumber = phoneNumber;
this.providerId = providerId;
}

@Override
public String toString() {
return "Member{" +
"memberId=" + memberId +
", gender=" + gender +
", username='" + name + '\'' +
", phoneNumber='" + phoneNumber + '\'' +
", providerId='" + providerId + '\'' +
", role=" + role +
'}';
}
}
Loading

0 comments on commit 7adc5e9

Please sign in to comment.