TIL

[231207] 심화 주차 조별 과제

진진리 2023. 12. 7. 22:01
728x90

필터 예외 처리

  • 필터쪽에서 발생한 Exception에 대해서도 원하는 형식으로 반환할 수 있도록 구현하고자 했다.
  • 필터에서 발생한 예외를 처리하는 방법으로 필터를 추가하는 방법이 있다고 듣게 되어서 이에 대해 찾아보고 구현해보았다.

ExceptionHandleFilter

  • jwt 관련 예외를 다룬다
public class ExceptionHandleFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try{
            filterChain.doFilter(request, response);
        }catch (ExpiredJwtException e){
            //토큰의 유효기간 만료
            setErrorResponse(response, ErrorCode.TOKEN_EXPIRED);
        }catch (JwtException | IllegalArgumentException | SecurityException e){
            //유효하지 않은 토큰
            setErrorResponse(response, ErrorCode.INVALID_TOKEN);
        }

    }
    private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode){
        ObjectMapper objectMapper = new ObjectMapper();
        response.setStatus(errorCode.getCode().value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ErrorResponse errorResponse = new ErrorResponse(errorCode.getCode().value(), errorCode.getMessage());
        try{
            response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
        }catch (IOException e){
            e.printStackTrace();
        }
    }


    public record ErrorResponse(Integer code, String message) {

    }

}

 

WebSecurityConfig

  • 만든 필터를 추가해준다
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    // 생략

    @Bean
    public ExceptionHandleFilter ExceptionHandleFilter() {
        return new ExceptionHandleFilter();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.csrf((csrf) -> csrf.disable());

        httpSecurity.sessionManagement((sessionManageMent) ->
                sessionManageMent.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        httpSecurity.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/users/**").permitAll() // '/api/users/'로 시작하는 요청 모두 접근 허가
                        .requestMatchers(HttpMethod.GET,"/api/products/**").permitAll()
                        .requestMatchers(HttpMethod.GET,"/api/reviews/**").permitAll()
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        httpSecurity.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
        httpSecurity.addFilterBefore(ExceptionHandleFilter(), JwtAuthorizationFilter.class);

        return httpSecurity.build();
    }
}

초기 데이터 설정

  • 팀 과제를 진행하면서 상품을 미리 DB에 저장하고자 하였다.
  • 처음에는 sql 문으로 DB에 등록하고자 했는데 팀원이 가지고 있는 Enum 클래스에 저장된 상품 데이터를 DB에 저장해야 했기 때문에 저장 방법에 대해 고민하게 되었다.
  • 그래서 선택한 방법은 테스트 코드를 작성해서 데이터를 넣어서 MySQL DB에 반영되도록 하였다.

ENUM 형식 데이터

public enum Sandwich15 {
    SpicyBbq (1L, "스파이시 바비큐", 7500L, "부드러운 풀드포크에 매콤함 맛을 더했다!"),
    SpicyShrimp (2L, "스파이시 쉬림프", 7900L, "탱글한 쉬림프에 이국적인 시즈닝을 더해 색다른 매콤함을 만나보세요!"),
    SpicyItalian (3L, "스파이시 이탈리안", 6900L, "페퍼로니 & 살라미가 입안 가득 페퍼로니의 부드러운 매콤함을 만나보세요!"),
    SteakNCheese (4L, "스테이크 & 치즈", 7900L, "육즙이 쫙~풍부한 비프 스테이크의 풍미가 입안 한가득"),
    ChickenBaconAvocado (5L, "치킨 베이컨 아보카도", 7900L, "담백하게 닭 가슴살로 만든 치킴 슬라이스와 베이컨, 부드러운 아보카도의 만남"),
    RoastChicken (6L, "로스트치킨", 7300L, "오븐에 구워 담백한 저칼로리 닭가슴살의 건강한 풍미"),
    RotisserieBarbecueChicken (7L, "로티세리 바비큐 치킨", 7300L, "촉촉한 바비큐 치킨의 풍미가득, 손으로 찢어 더욱 부드러운 치킨의 혁명"),
    Kbbq (8L, "k-비비큐", 7300L, "써브웨이의 코리안 스타일 샌드위치! 마늘, 간장 그리고 은은한 붚맛까지!"),
    PulledPorkBarbecue (9L, "풀드 포크 바비큐", 7200L, "미국 스타일의 풀드 포크 바비큐가 가득 들어간 샌드위치"),
    SubwayClub (10L, "써브웨이 클럽", 7100L, "고소한 베이컨, 담백한 치킨 슬라이스에 햄까지 더해 완벽해진 조황를 즐겨보세요!"),
    chickenTeriyaki (11L, "치킨 데리야끼", 7000L, "담백한 스트립에 달콤짭쪼름한 써브웨이 특제 데리야끼 소스와의 환상적인 만남"),
    ItalianBMT (12L, "이탈리안 비엠티", 6900L, "페퍼로니, 살라미 그리고 햄이 만들어내는 최상의 조화! 전세계가 사랑하는 써브웨이의 베스트셀러!"),
    BLT (13L, "비엘티", 6600L, "오리지널 아메리칸 스타일 베이컨의 풍미와 바삭함 그대로~"),
    ChckenSlice (14L, "치킨 슬라이스", 6500L, "달가슴살로 만든 치킨 슬라이스로 즐기는 담백한 맛!"),
    Tuna (15L, "참치", 5800L, "남녀노소 누구나 좋아하는 담백한 참치와 고소한 마요네즈의 완벽한 조화"),
    Ham (16L, "햄", 5800L, "풍부한 햄이 만들어내는 담백함을 입 안 가득 즐겨보세요!"),
    EggMayo (17L, "에그마요", 5500L, "부드러운 달걀과 고소한 마요네즈가 만나 더 부드러운 스테디셀러"),
    VeggieDelite (18L, "베지", 4900L, "갓 구운 빵과 신선한 8가지 야채로 즐기는 깔끔한 한끼");

    private final Long id;
    private final String name;
    private final Long price;
    private final String productDetails;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Long getPrice() {
        return price;
    }

    public String getProductDetails() {
        return productDetails;
    }

    Sandwich15(Long id, String name, Long price, String productDetails) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.productDetails = productDetails;
    }
}

 

 

ProductRepositoryTest

  • 테스트 DB와 분리할 필요가 없었기 때문에 따로 application-test.properties 파일을 작성하지 않았다.
  • 테스트코드에는 따로 환경변수를 설정해줄 필요가 있다는 것을 알게 되었다.
  • 데이터를 등록하고자 할 때에만 @Disabled를 지우는 방식으로 데이터를 저장하였다.
@DataJpaTest
@Import(JpaConfig.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ProductRepositoryTest {

    @Autowired
    ProductRepository productRepository;

    @Test
    @Disabled
    @Transactional
    @Rollback(value = false)
    void saveBread() {
        List<Product> productList = new ArrayList<>();
        // for문으로 enum 데이터를 list에 저장
        for (Bread data : Bread.values()) {
            Product product = new Product("bread", data.getName(), data.getPrice(),
                data.getProductDetails());
            productList.add(product);
        }
        productRepository.saveAll(productList);
    }
    
    // 생략
    
}

save()와 saveAll()의 차이

전까지는 JpaRepository에 객체를 저장할 때 단일 객체는 save()로, 여러 개인 경우에는 saveAll()으로 저장한다고만 생각했다.
팀원을 통해 for문에서 객체를 하나씩 저장하는 코드에 대해서 sql 쿼리가 여러 번 나가기 때문에 비효율적이라는 것을 알게 되었다.

 

  • saveAll(): 1건마다 인스턴스 내부의 save() 함수 호출
  • N건의 저장에 대하여 save()와 함수 호출 개수는 동일
    • save()의 경우 호출마다 트랜잭션이 생성됨
    • saveAll()의 경우 Bean 객체의 내부 함수를 호출하기 때문에 하나의 트랜잭션에서 실행됨
      • 더 효율적 !

필터에서 권한별 접근을 설정

이번 프로젝트 주제가 백오피스다보니 사용자의 권한에 따라 다른 기능을 제공하는 것이 주요 목표였다.
상품/사용자/매출 관리 등은 관리자만 접근하는 기능이고 주문/리뷰 생성은 사용자만 사용할 수 있는 기능이었다.
처음에는 서비스단에서 사용자의 권한을 확인하고 예외를 throw하는 로직을 일일이 추가해주었다.
그런데 기능이 많아지면서 이를 필터단에서 처리하면 좋겠다는 생각이 들었고 이를 팀원분께서 구현해주셨다.

WebSecurityConfig

  • hasAuthority(USER.getAuthority()) 사용
  • httpSecurity.exceptionHandling으로 권한 접근을 막는 예외 처리
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.csrf((csrf) -> csrf.disable());

        httpSecurity.sessionManagement((sessionManageMent) ->
                sessionManageMent.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        httpSecurity.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/users/**").permitAll() // '/api/users/'로 시작하는 요청 모두 접근 허가
                        .requestMatchers(GET,"/api/products/**").permitAll()
                        .requestMatchers(GET,"/api/reviews/**").permitAll()
                        .requestMatchers("/api/carts/**")
                            .hasAuthority(USER.getAuthority()) // 장바구니 기능은 USER만 사용 가능하다.
                        .requestMatchers("/api/sales/**")
                            .hasAuthority(ADMIN.getAuthority()) // 매출 관리 기능은 ADMIN 사용 가능하다.
                        .requestMatchers(POST, "/api/orders/**")
                            .hasAuthority(USER.getAuthority()) // 주문(생성)은 USER만 사용 가능하다.
                        .requestMatchers(PATCH, "/api/reviews/**")
                            .hasAuthority(USER.getAuthority())
                        .requestMatchers(POST, "/api/reviews/**")
                            .hasAuthority(USER.getAuthority()) // 리뷰 수정과 등록은 USER만 가능하다
                        .requestMatchers("/api/products/**")
                            .hasAuthority(ADMIN.getAuthority()) // 상품 조회 제외 기능은 ADMIN만 가능하다 (조회는 위에서 처리)
                        .requestMatchers("/api/admin/**")
                            .hasAuthority(ADMIN.getAuthority()) // 유저 관리 기능은 ADMIN만 가능하다.
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        httpSecurity.exceptionHandling(authenticationManager -> authenticationManager
            .accessDeniedHandler(customAccessDeniedHandler));

        httpSecurity.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
        httpSecurity.addFilterBefore(ExceptionHandleFilter(), JwtAuthorizationFilter.class);

        return httpSecurity.build();
    }
}

 

 

UserDetailsImpl

  • getAuthorities() 수정
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);
        return authorities;
    }

 

 

CustomAccessDeminedHandler

  • 필터에서 발생한 예외를 handling할 수 있음!
@Component
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
        AccessDeniedException accessDeniedException) throws IOException {

        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);

        ErrorResponseDto responseDto = new ErrorResponseDto(ErrorCode.ACCESS_DENIED,
            ErrorCode.ACCESS_DENIED.getMessage());
        response.getWriter().write(objectMapper.writeValueAsString(responseDto));
    }
}

 

예외 코드, 메시지 전달

 

이번 팀 과제를 통해서 필터에서 권한별 접근일 막고, 예외처리를 할 수 있는 방법을 알게되어서 좋았다.

N+1 문제

JPA에서 sql 쿼리를 날릴 때 n+1 문제가 발생할 수 있다는 정도만 알고 있었는데
이번 팀과제를 하면서 팀원분이 이에 대해 알려주셔서 정리해보려고 한다.

 

원인

  • JPA의 Entity를 조회할 때 query 내부에 존재하는 다른 연관관계 접근할 때 또 다시 한번 쿼리가 발생
  • 1번 조회해야 할 것을 N개 종류의 데이터 각각을 추가로 조회하게 되어서 N+1번 DB 조회를 하게 되는 문제

발생하는 시기

  • 주로 @ManyToOne 연관관계를 가진 엔티티에서 주로 발생
  • 즉시 로딩으로 데이터를 가져오는 경우
  • 지연 로딩으로 데이터를 가져온 이후 하위 엔티티를 조회하는 경우

테스트

  • 장바구니인 Cart와 User는 다대일 관계
  • 장바구니 비우기 기능 구현: 접근하는 user에 해당하는 모든 장바구니를 삭제

1. deleteAllByUser(user) 쿼리 메소드

  • 사용자 id에 해당하는 장바구니를 개수만큼를 4번 조회하여 삭제한다.

 

2. jpql 활용: delete from Cart c where c.user = :user

CartRepository

  • 사용자 id에 해당하는 cart를 한 번에 삭제한다.

아직 팀원들이 모두다 JPQL에 대해 알지 못해서 일부분에만 적용하였다.
이번에 쿼리를 직접 보면서 N+1 문제에 대해 알게 되어서 좋았다.

소셜 로그인 구현

팀원 분께서 소셜 로그인을 맡아 구현하셨는데 이번 기회에 해당 코드를 보고 정리해보고자 한다.

소셜 로그인 과정

인증 코드 요청

  • 카카오
https://kauth.kakao.com/oauth/authorize?client_id={clientId}&redirect_uri=http://localhost:8080/api/users/kakao/callback&response_type=code
  • 네이버
https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id={clientId}&redirect_uri=http://localhost:8080/api/users/naver/callback&state=test
  • 인증 코드를 받을 API를 Controller에 추가해준다.

 

UserController

    @GetMapping("/kakao/callback")
    public ResponseEntity<?> kakaoLogin(String code, HttpServletResponse response)
        throws JsonProcessingException {

        String token = kakaoService.kakaoLogin(code);
        response.setHeader(JwtUtil.AUTHORIZATION_HEADER, token);
        return ResponseEntity.ok().build();
    }

    @GetMapping("/naver/callback")
    public ResponseEntity<?> naverLogin(String code, HttpServletResponse response)
        throws JsonProcessingException {

        String token = naverService.naverLogin(code);
        response.setHeader(JwtUtil.AUTHORIZATION_HEADER, token);
        return ResponseEntity.ok().build();
    }

 

KakaoService

@Service
@RequiredArgsConstructor
public class KakaoService {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    private final RestTemplate restTemplate;
    private final JwtUtil jwtUtil;
    private final ObjectMapper objectMapper;

    /**
     * 환경 변수로 카카오 디벨로퍼스에서 받은 ClientId 값을 넣으면 됩니다.
     */
    @Value("${kakao.client.id}")
    private String clientId;

    @Value(("${kakao.client.secret}"))
    private String clientSecret;

    public String kakaoLogin(String code) throws JsonProcessingException {
        String token = getToken(code);

        KaKaoUserDto kaKaoUserDtoInfo = getKakaoUserInfo(token);

        User kakaoUser = accountIntegrationOrSignupOrLogin(kaKaoUserDtoInfo);

        return jwtUtil.createToken(kakaoUser.getUsername());
    }

    private String getToken(String code) throws JsonProcessingException {
        URI uri = getUri("https://kauth.kakao.com", "/oauth/token");

        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("client_id", clientId);
        body.add("client_secret", clientSecret);
        body.add("redirect_uri", "http://localhost:8080/api/users/kakao/callback");
        body.add("code", code);

        RequestEntity<MultiValueMap<String, String>> request = getRequestEntity(uri, headers, body);

        ResponseEntity<String> response = restTemplate.exchange(request, String.class);

        return objectMapper.readTree(response.getBody()).get("access_token").asText();
    }

    private KaKaoUserDto getKakaoUserInfo(String token) throws JsonProcessingException {
        URI uri = getUri("https://kapi.kakao.com", "/v2/user/me");

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + token);
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        RequestEntity<MultiValueMap<String, String>> request =
            getRequestEntity(uri, headers, new LinkedMultiValueMap<>());

        ResponseEntity<String> response = restTemplate.exchange(request, String.class);

        JsonNode jsonNode = objectMapper.readTree(response.getBody());
        long kakaoId = jsonNode.get("id").asLong();
        String email = null;
        if (jsonNode.get("kakao_account").get("email") != null) { // 이메일 동의를 했을 때만 이메일을 받아온다.
            email = jsonNode.get("kakao_account").get("email").asText();
        }

        return new KaKaoUserDto(kakaoId, email);
    }

    private User accountIntegrationOrSignupOrLogin(KaKaoUserDto kaKaoUserDtoInfo) {
        User kakaoUser = userRepository.findByKakaoId(kaKaoUserDtoInfo.getId()).orElse(null);
        if (kakaoUser == null) {
            User sameEmailUser = userRepository.findByEmail(kaKaoUserDtoInfo.getEmail())
                .orElse(null);
            if (sameEmailUser == null) {
                // 기존 이메일중에 카카오 이메일과 같은 게 없으면 새로 회원가입
                String username = UUID.randomUUID().toString();
                String password = passwordEncoder.encode(UUID.randomUUID().toString());
                String email = kaKaoUserDtoInfo.getEmail();
                Long kakaoId = kaKaoUserDtoInfo.getId();
                kakaoUser = User.kakaoSignup(username, password, email, kakaoId);
            } else {
                // 같은 이메일이 있으면 계정 통합
                kakaoUser = sameEmailUser;
                kakaoUser.kakaoIntegration(kaKaoUserDtoInfo.getId());
            }
            userRepository.save(kakaoUser);
        }
        return kakaoUser;
    }

    private static URI getUri(String url, String path) {
        URI uri = UriComponentsBuilder
            .fromUriString(url)
            .path(path)
            .encode()
            .build()
            .toUri();
        return uri;
    }

    private RequestEntity<MultiValueMap<String, String>> getRequestEntity(
        URI uri, HttpHeaders headers, MultiValueMap<String, String> body) {

        RequestEntity<MultiValueMap<String, String>> request = RequestEntity
            .post(uri)
            .headers(headers)
            .body(body);
        return request;
    }

}
  • KakaoLogin()
    • getToken(): 카카오에게 인증 코드로 토큰을 요청하고 응답받음  >> 인증 코드로 토큰 요청
    • getKakaoUserInfo(): 카카오에게 토큰으로 사용자 정보를 요청하고 kakaoId와 email을 응답받음
      • >> 토큰으로 API 호출
    • accountIntegrationOrSignupOrLongin():
      1. kakaoId로 사용자를 찾고 기존 사용자면 로그인
      2. 기존 사용자가 아니지만 카카오 이메일이 같은 사용자가 있으면 통합: kakaoId를 추가 등록
      3. 같은 이메일이 없으면 새로 회원가입
      4. 새로운 사용자를 저장하고 로그인
    • username으로 jwt 토큰을 생성하여 반환

 

NaverService

@Service
@RequiredArgsConstructor
public class NaverService {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    private final RestTemplate restTemplate;
    private final JwtUtil jwtUtil;
    private final ObjectMapper objectMapper;

    @Value("${naver.client.id}")
    private String clientId;

    @Value(("${naver.client.secret}"))
    private String clientSecret;


    public String naverLogin(String code) throws JsonProcessingException {
        String token = getToken(code);
        NaverUserDto naverUserDtoInfo = getNaverUserInfo(token);
        User naverUser = accountIntegrationOrSignupOrLogin(naverUserDtoInfo);
        return jwtUtil.createToken(naverUser.getUsername());
    }

    private String getToken(String code) throws JsonProcessingException {
        URI uri = getUri("https://nid.naver.com", "/oauth2.0/token");

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("client_id", clientId);
        body.add("client_secret", clientSecret);
        body.add("code", code);
        body.add("state", "test");

        RequestEntity<MultiValueMap<String, String>> request = RequestEntity.post(uri)
            .body(body);

        ResponseEntity<String> response = restTemplate.exchange(request, String.class);
        return objectMapper.readTree(response.getBody()).get("access_token").asText();
    }

    private NaverUserDto getNaverUserInfo(String token) throws JsonProcessingException {
        URI uri = getUri("https://openapi.naver.com", "/v1/nid/me");

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + token);

        RequestEntity<MultiValueMap<String, String>> request = RequestEntity.post(uri)
            .headers(headers).body(new LinkedMultiValueMap<>());

        ResponseEntity<String> response = restTemplate.exchange(request, String.class);
        JsonNode jsonNode = objectMapper.readTree(response.getBody());

        String naverId = jsonNode.get("response").get("id").asText();
        String email = null;
        if (jsonNode.get("response").get("email") != null) {
            email = jsonNode.get("response").get("email").asText();
        }
        return new NaverUserDto(naverId, email);
    }

    private User accountIntegrationOrSignupOrLogin(NaverUserDto naverUserDtoInfo) {
        User naverUser = userRepository.findByNaverId(naverUserDtoInfo.getId()).orElse(null);
        if (naverUser == null) {
            User sameEmailuser = userRepository.findByEmail(naverUserDtoInfo.getEmail())
                .orElse(null);
            if (sameEmailuser == null) {
                naverUser = signup(naverUserDtoInfo);
            } else {
                naverUser = sameEmailuser;
                naverUser.naverIntegration(naverUserDtoInfo.getId());
            }
            userRepository.save(naverUser);
        }
        return naverUser;
    }

    private static URI getUri(String url, String path) {
        URI uri = UriComponentsBuilder
            .fromUriString(url)
            .path(path)
            .encode()
            .build()
            .toUri();
        return uri;
    }

    private User signup(NaverUserDto naverUserDtoInfo) {
        User naverUser;
        String username = UUID.randomUUID().toString();
        String password = passwordEncoder.encode(UUID.randomUUID().toString());
        String email = naverUserDtoInfo.getEmail();
        String naverId = naverUserDtoInfo.getId();
        naverUser = User.naverSignup(username, password, email, naverId);
        return naverUser;
    }
}

 

 

 

소셜 로그인 테스트

테스트를 위한 html 코드

<!DOCTYPE html>
<html lang="en">
<body>
<div>
  <button id="login-kakao-btn" onclick="location.href='https://kauth.kakao.com/oauth/authorize?client_id={clientId}&redirect_uri=http://localhost:8080/api/users/kakao/callback&response_type=code'">
    카카오로 로그인하기
  </button>
  https://kauth.kakao.com/oauth/authorize?client_id={clientId}&redirect_uri=http://localhost:8080/api/users/kakao/callback&response_type=code
</div>

<div>
  <button id="login-naver-btn" onclick="location.href='https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id={clientId}&redirect_uri=http://localhost:8080/api/users/naver/callback&state=test'">
    네이버로 로그인하기
  </button>
  https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id={clientID}&redirect_uri=http://localhost:8080/api/users/naver/callback&state=test
</div>
</body>
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
</html>

 

URL의 {client_id}에 카카오 디벨로퍼스에서 받은 REST API KEY를 넣어준다.

 

웹페이지 모습

 

'카카오로 로그인하기' 버튼을 누른다.

전체 동의하기를 누르고 계속하기 버튼을 누른다.

jwt 토큰이 온 것을 확인할 수 있다.