Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#421] UserDetails Member도메인에서 분리 #422

Merged
merged 5 commits into from
Jan 7, 2024
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ dependencies {

// Slack
implementation 'com.slack.api:slack-api-client:1.29.0'

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

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public enum ErrorCode {
NOT_FOUND_TIMETABLE("시간표가 존재하지 않습니다.", HttpStatus.NOT_FOUND),
NOT_FOUND_IMAGE("이미지가 존재하지 않습니다.", HttpStatus.NOT_FOUND),
NOT_FOUND_OBJECT("존재하지 않는 객체입니다.", HttpStatus.NOT_FOUND),
NOT_FOUND_OAUTH_PROVIDER("지원하지 않는 소셜 로그인 플랫폼 입니다.", HttpStatus.NOT_FOUND),

// 403
FORBIDDEN_UPDATE("수정 권한이 없습니다.", HttpStatus.FORBIDDEN),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.festival.common.interceptor;

import com.festival.common.security.JwtTokenProvider;
import com.festival.common.security.jwt.JwtTokenProvider;
import com.festival.common.util.JwtTokenUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Expand All @@ -10,7 +10,6 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
@RequiredArgsConstructor
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.festival.common.security.config;

import com.festival.common.infra.Alert.discord.DiscordService;
import com.festival.common.security.JwtTokenProvider;
import com.festival.common.security.jwt.JwtTokenProvider;
import com.festival.common.security.oauth.CustomOAuth2UserService;
import com.festival.common.security.oauth.OAuthSuccessHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -12,7 +14,6 @@
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@RequiredArgsConstructor
@Configuration
Expand All @@ -24,6 +25,8 @@ public class SecurityConfig {

// private final SlackService slackService;
private final DiscordService discordService;
private final CustomOAuth2UserService customOAuth2UserService;
private final com.festival.common.security.oauth.OAuthSuccessHandler OAuthSuccessHandler;

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -45,6 +48,12 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
.userService(customOAuth2UserService)
)
.successHandler(OAuthSuccessHandler)
)
.headers().frameOptions().disable()
.and();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package com.festival.common.security;
package com.festival.common.security.jwt;

import com.festival.common.exception.ErrorCode;
import com.festival.common.exception.custom_exception.BadRequestException;
import com.festival.common.exception.custom_exception.InvalidException;
import com.festival.common.redis.RedisService;
import com.festival.common.security.dto.JwtTokenRes;
import com.festival.common.util.JwtTokenUtils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -23,11 +19,9 @@
import org.springframework.stereotype.Component;

import java.security.Key;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Component
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.festival.common.security.dto;
package com.festival.common.security.jwt;

import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.festival.common.security.oauth;


import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Map;

@Slf4j
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);

/**
* OAuth2 서비스 id(구글, 카카오, 네이버)
* OAuth2 로그인 진행 시 키가 되는 필드 값(PK)
*/
String provider = userRequest.getClientRegistration().getRegistrationId();
String attributeKey = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();


OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(provider, attributeKey, oAuth2User.getAttributes());
Map<String, Object> memberAttribute = oAuth2Attribute.convertToMap();

return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_MEMBER")),
memberAttribute, "email");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.festival.common.security.oauth;

import com.festival.common.exception.ErrorCode;
import com.festival.common.exception.custom_exception.NotFoundException;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@Getter
@Builder(access = AccessLevel.PRIVATE)
@SuppressWarnings("unchecked")
public class OAuth2Attribute {

private Map<String, Object> attributes; // 소셜 로그인 사용자의 속성 정보를 담는 Map
private String attributeKey; // 사용자 속성의 키 값
private String email; // 이메일
private String loginType;


/**
* @Description
* 해당 로그인인 서비스가 kakao인지 google인지 구분하여, 알맞게 매핑을 해주도록 합니다.
* 여기서 attributeKey는 OAuth2 로그인을 처리한 서비스 명("google","kakao","naver"..)이 되고,
* userNameAttributeName은 해당 서비스의 map의 키값이 되는 값이됩니다. {google="sub", kakao="id", naver="response"}
*/
static OAuth2Attribute of(String provider, String attributeKey, Map<String, Object> attributes) {
// 각 플랫폼 별로 제공해주는 데이터가 조금씩 다르기 때문에 분기 처리함.
return switch (provider) {
case "kakao" -> kakao(attributeKey, attributes);
default -> throw new NotFoundException(ErrorCode.NOT_FOUND_OAUTH_PROVIDER);
};
}

private static OAuth2Attribute kakao(String attributeKey, Map<String, Object> attributes) {
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");

return OAuth2Attribute.builder()
.email((String) kakaoAccount.get("email"))
.attributes(kakaoAccount)
.attributeKey(attributeKey)
.loginType("KAKAO")
.build();
}

// OAuth2Attribute -> Map<String, Object>
public Map<String, Object> convertToMap() {
Map<String, Object> map = new HashMap<>();
map.put("id", attributeKey);
map.put("key", attributeKey);
map.put("email", email);
map.put("loginType", loginType);

return map;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.festival.common.security.oauth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.festival.common.exception.ErrorCode;
import com.festival.common.exception.custom_exception.NotFoundException;
import com.festival.common.security.jwt.JwtTokenProvider;
import com.festival.common.security.jwt.JwtTokenRes;
import com.festival.domain.member.model.Member;
import com.festival.domain.member.model.MemberRole;
import com.festival.domain.member.repository.MemberRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.util.Optional;

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();


String loginType = oAuth2User.getAttribute("loginType");

// KAKAO_user123@naver.com
String email = loginType + "_" + oAuth2User.getAttribute("email");
Optional<Member> findMember = memberRepository.findByEmail(email);

// 회원이 아닌 경우에 회원 가입 진행
Member member = null;
if (findMember.isEmpty()) {
// KAKAO_user123
member = Member.builder()
.email(email)
.loginType(loginType)
.memberRole(MemberRole.MEMBER)
.build();

memberRepository.save(member);
} else {
member = findMember.orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_MEMBER));
}

// OAuth2User 객체에서 권한 가져옴
JwtTokenRes jwtToken = jwtTokenProvider.createJwtToken(member.getEmail(), member.getRole().getValue());
/*

String targetUrl = UriComponentsBuilder.fromUriString("http//localhost:8080/success")
.queryParam("accessToken", jwtToken.getAccessToken())
.queryParam("refreshToken", jwtToken.getRefreshToken())
.queryParam("memberId", String.valueOf(member.getId()))
.build().toUriString();

getRedirectStrategy().sendRedirect(request, response, targetUrl);
*/

response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=UTF-8");
response.setStatus(200);

response.getWriter().write(
objectMapper.writeValueAsString(jwtToken)
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.festival.common.security.service;

import com.festival.common.exception.custom_exception.NotFoundException;
import com.festival.domain.member.model.AuthenticationMember;
import com.festival.domain.member.model.Member;
import com.festival.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

import static com.festival.common.exception.ErrorCode.NOT_FOUND_MEMBER;

@RequiredArgsConstructor
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {

private final MemberRepository memberRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findByUsername(username).orElseThrow(() -> new NotFoundException(NOT_FOUND_MEMBER));

List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(member.getRole().name()));
return new AuthenticationMember(member, authorities);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.festival.common.exception.ErrorCode;
import com.festival.common.exception.custom_exception.BadRequestException;
import com.festival.common.security.dto.JwtTokenRes;
import com.festival.common.security.dto.MemberLoginReq;
import com.festival.common.security.jwt.JwtTokenRes;
import com.festival.domain.member.dto.MemberLoginReq;
import com.festival.common.util.JwtTokenUtils;
import com.festival.domain.member.dto.MemberJoinReq;
import com.festival.domain.member.service.MemberService;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.festival.domain.member.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Controller
public class MemberPageController {
@GetMapping("/home")
public String login(){
return "login";
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.festival.common.security.dto;
package com.festival.domain.member.dto;

import lombok.Builder;
import lombok.Getter;
Expand Down
Loading