Spring boot OAuth 로그인(1)

2023. 12. 7. 20:59Springboot

0. 개요

       Spring boot를 이용한 OAuth로그인 구현

       해당 로그인은 회원가입을 하지 않고 타사의 사이트의 접근 권한을 얻고 그 권한을 이용하여

       개발할 수 있도록 도와주는 프레임워크이다. 구글, 카카오, 네이버등과 같은 사이트에서

       로그인을 하면 직접 구현한 사이트에서도 로그인 인증을 받을 수 있도록 되는 구조이다.

 1. Spring boot 코드 짜기 전에 해야할 것

       1. kakao Devloper 로그인

           https://developers.kakao.com/console/app

 

카카오계정

 

accounts.kakao.com

       2. 애플리케이션 추가하기

 

       

      3. 애플리케이션 추가하기

 

 

        4. REST API KEY 체크하기

 

 

        5. 활성화 설정 OFF -> ON과 Redirect uri 설정하기

 

 

 

        6. 동의항목 체크하기 및 권한 설정

 

 

        7. 항목 이름 email 추가하기

            - 필자는 email을 사용하기에 추가함, 필요없다면 하지 않아도 됩니다.

            - 추가 이후, 6번으로 돌아가 상태 확인하기

 

** 체크 사항!

https://kauth.kakao.com/oauth/authorize?client_id={your REST_API_KEY}&redirect_uri=http://localhost:8080/api/member/kakao/callback&response_type=code'
# REST_API_KEY는 각자 다르기에 {your REST_API_KEY} 빼고 자신이 발급받은 키를 넣어줍니다.
# REST_API_KEY 기억 안 나시면 위에 4번으로 돌아가셔서 확인하기
# redirect_uri을 http://localhost:8080/api/member/callback으로 설정하지 않았다면
# 자신이 커스텀한 redirect_uri부분을 넣으셔야합니다.

2. Requirements

  • Spring boot version 3.2.0

 3. Stacks

  Springboot

  MySQL

 

  • Loombook
  • Spring Data JPA 
  • Spring Boot DevTools
  • Spring Web
  • JWT token
  • MySQL Driver
  • OAuth2

3. Code

     1. application.yml

          - 해당 oauth 구현을 위해 kakao, naver, google 설정을 합니다.

          - ${parameter}에 해당하는 것은 intellij의 환경변수를 지정한 내용입니다.

spring:
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
    properties:
      hibernate:
        format_sql: true
    defer-datasource-initialization: true
  datasource:
    url: jdbc:mysql://localhost:3306/{db_name}
    username: ${your_username}
    password: ${your_password}
    driver-class-name: com.mysql.cj.jdbc.Driver
  h2:
    console:
      enabled: true

jwt:
  secret_key: ${jwt.secret_key}

kakao:
  client_id : 8bbcb11360f192ffb599e80a0fc3489a
  redirect_uri : http://localhost:8080/api/member/kakao/callback

naver:
  client_id : 3jo14itcz2ELeaKj8BPQ
  client_secret : DUp0cF33Dw
  redirect_uri : http://localhost:8080/api/member/naver/callback

google :
  client_id : 807257127729-2eg30f8m4cq2ngtm1d6q4o2o68p5q8bm.apps.googleusercontent.com
  client_secret : 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
  redirect_uri : http://localhost:8080/api/member/google/callback

 

   2. WebSecurityConfig.java

          - SecurityFilterChain() : authorizateHttpRequests.requestMatcher를 통해 로그인 하지 않은 사용자가

                                                사용할 수 있는 페이지와 정적파일을 지정해줍니다.

                                                anyRequest().authenticated()를 통해 위의 지정한 파일 제외하고는 

                                                인증이 필요하도록 설정해줍니다.

package com.tmproject.Common.Security;

import com.tmproject.Common.Jwt.JwtAuthenticationFilter;
import com.tmproject.Common.Jwt.JwtAuthorizationFilter;
import com.tmproject.Common.Jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final MemberDetailsServiceImpl memberDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, memberDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/member/login").permitAll()
                        .requestMatchers("/api/member/signup").permitAll()
                        .requestMatchers("/api/member/loginPage").permitAll()
                        .requestMatchers("/api/member/kakao/callback").permitAll()
                        .requestMatchers("/api/member/naver/callback").permitAll()
                        .requestMatchers("/api/member/google/callback").permitAll()
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        http.formLogin((formLogin) ->
                formLogin
                        .loginPage("/api/member/loginPage").permitAll()
                // loginPage 임시 지정
        );

        // 필터 관리
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

 

 

   3. JwtAuthenitcationFilter, JwtAuthorizationFilter, JwtUtil.java 

        - 기본적인 JWT토큰 발급을 받는 클래스 파일입니다.

        - 해당 사항은 링크로 대체합니다.

https://pjh3797.tistory.com/25

 

Jwt를 이용한 로그인, 회원가입을 구현한 할일 카드 만들기(3)

1. Requirements Spring boot version 3.1.5 2. Stacks Springboot MySQL BootStrap Loombook Spring Data JPA Spring Boot DevTools Spring Web Thymeleaf Spring Security MySQL Driver Validation JWT 3. Code 변경사항에 글씨를 진하게 표시해 놨습니다

pjh3797.tistory.com

 

   4. KakaoController.java

       - '** 체크 사항!'의 URL을 직접 홈페이지에 넣어서 OAUTH 로그인 하시면

         kakaoLogin()로 진입하여 코드를 받아옵니다.

      - 해당 HttpServletResponse를 통하여 cookie를 넣어주고 view로 돌아오게 됩니다.

 

package com.example.test11.temp.Controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.tmproject.Common.Jwt.JwtUtil;
import com.tmproject.api.member.service.KakaoService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@RequiredArgsConstructor
@Controller
@Slf4j
@RequestMapping("/api")
public class KakaoController {
    private final KakaoService kakaoService;

    @GetMapping("/member/kakao/callback")
    public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
        log.info("code : "+code);
        String token = kakaoService.kakaoLogin(code);
        Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7));
        cookie.setPath("/");
        response.addCookie(cookie);
        //
        return "redirect:/";
    }
}

 

    5. KakaoMemberInfoDto.java

       - memberRepository에 넣기 위한 id, nikname, email dto생성

package com.tmproject.api.member.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class KakaoMemberInfoDto {
    private Long id;
    private String nickname;
    private String email;
}

 

    6. KakaoSerivce.java

       - kakaoLogin()에 해당하는 주석 처리 확인하기!

package com.example.test11.temp.Service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tmproject.Common.Jwt.JwtUtil;
import com.tmproject.api.member.dto.KakaoMemberInfoDto;
import com.tmproject.api.member.entity.Member;
import com.tmproject.api.member.entity.MemberRoleEnum;
import com.tmproject.api.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.UUID;

@Slf4j(topic = "KAKAO Login")
@RequiredArgsConstructor
@Service
public class KakaoService {

    private final PasswordEncoder passwordEncoder;
    private final MemberRepository memberRepository;
    private final RestTemplate restTemplate;
    // spring boot에서 수동으로 빈 등록해서 관리를 유도함
    // 추가적인 설정을 한 RestTemplate
    private final JwtUtil jwtUtil;

    private static String kakao_client_id = "8bbcb11360f192ffb599e80a0fc3489a";

    public String kakaoLogin(String code) throws JsonProcessingException {
        String accessToken = getToken(code);
        log.info("인가 코드로 액세스 토큰 요청 : "+accessToken);

        KakaoMemberInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);
        log.info("토큰으로 카카오 API 호출 : 액세스 토큰으로 카카오 사용자 정보 가져오기 : "+kakaoUserInfo.getNickname());

        Member kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);
        log.info("필요시에 회원가입 : "+kakaoUser.getKakaoId());
        log.info("kakaoUser.getKakaoId() : "+kakaoUser.getKakaoId());
        log.info("kakaoUser.getKakaoUsername() : "+kakaoUser.getUsername());

        String createToken =  jwtUtil.createToken(kakaoUser.getUsername(), kakaoUser.getRole());
        log.info("JWT 토큰 반환 : "+createToken);
        return createToken;
    }

    private String getToken(String code) throws JsonProcessingException {
        // 요청 URL 만들기
        // 요청 uri = "https://kauth.kakao.com/oauth/token"
        log.info("인가 코드 : "+code);
        URI uri = UriComponentsBuilder
                .fromUriString("https://kauth.kakao.com")
                .path("/oauth/token")
                .encode()
                .build()
                .toUri();

        // HTTP Header 생성
        // Http Header에 데이터 유형 Content-type, application/x-www-form-urlencoded; charset=utf-8
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        // HTTP Body 생성
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("client_id", kakao_client_id);
        body.add("redirect_uri", "http://localhost:8080/api/member/kakao/callback");
        body.add("code", code);
        // 'https://kauth.kakao.com/oauth/authorize?client_id=8bbcb11360f192ffb599e80a0fc3489a
        // &redirect_uri=http://localhost:8080/api/member/kakao/callback&response_type=code'
        RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
                .post(uri)
                .headers(headers)
                .body(body);

        // HTTP 요청 보내기
        ResponseEntity<String> response = restTemplate.exchange(
                requestEntity,
                String.class
        );

        // HTTP 응답 (JSON) -> 액세스 토큰 파싱
        JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());

        return jsonNode.get("access_token").asText();
    }

    private KakaoMemberInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("https://kapi.kakao.com")
                .path("/v2/user/me")
                .encode()
                .build()
                .toUri();

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

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

        // HTTP 요청 보내기
        ResponseEntity<String> response = restTemplate.exchange(
                requestEntity,
                String.class
        );

        JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
        Long id = jsonNode.get("id").asLong();
        String nickname = jsonNode.get("properties")
                .get("nickname").asText();
        String email = jsonNode.get("kakao_account")
                .get("email").asText();

        log.info("카카오 사용자 정보: " + id + ", " + nickname + ", " + email);
        return new KakaoMemberInfoDto(id, nickname, email);
    }

    private Member registerKakaoUserIfNeeded(KakaoMemberInfoDto kakaoUserInfo) {
        // DB 에 중복된 Kakao Id 가 있는지 확인
        Long kakaoId = kakaoUserInfo.getId();
        Member kakaoUser = memberRepository.findByKakaoId(kakaoId).orElse(null);

        if (kakaoUser == null) {
            // 카카오 사용자 email 동일한 email 가진 회원이 있는지 확인
            String kakaoEmail = kakaoUserInfo.getEmail();
            Member sameEmailUser = memberRepository.findByEmail(kakaoEmail).orElse(null);
            if (sameEmailUser != null) {
                kakaoUser = sameEmailUser;
                // 기존 회원정보에 카카오 Id 추가
                kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId);
            } else {
                // 신규 회원가입
                // password: random UUID
                String password = UUID.randomUUID().toString();
                String encodedPassword = passwordEncoder.encode(password);

                // email: kakao email
                String email = kakaoUserInfo.getEmail();

                kakaoUser = new Member(kakaoUserInfo.getNickname(), encodedPassword, email, MemberRoleEnum.USER, kakaoId);
            }

            memberRepository.save(kakaoUser);
        }
        return kakaoUser;
    }
}

  

  8. MemberRepository.java

      - findByKakaoId(String kakaoId) : db에 중복되는 kakaoId가 존재하는지 확인합니다.

package com.tmproject.api.member.repository;

import com.tmproject.api.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByUsername(String username);
    Optional<Member> findByEmail(String email);
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
    boolean existsByEmailAndIdNot(String requestEmail, long memberId);
    boolean existsByUsernameAndIdNot(String username, long memberId);
    Optional<Member> findByUsernameOrEmail(String usernameOrEmail, String usernameOrEmail1);
    Optional<Member> findByKakaoId(Long kakaoId);
    Optional<Member> findByNaverId(String naverId);
    Optional<Member> findByGoogleId(String googleUserId);
}

 

 

'Springboot' 카테고리의 다른 글

Spring boot 순환 참조 문제  (0) 2023.12.12
Spring boot OAuth 로그인(0)  (0) 2023.12.11
Spring boot 환경 변수 설정  (1) 2023.12.05
Spring boot Test code(1)  (0) 2023.12.04
Spring boot 중복된 Bean 에러  (0) 2023.11.30