Day_41 ( 스프링 - 8 )

HD.Y·2023년 12월 26일
0

한화시스템 BEYOND SW

목록 보기
36/58
post-thumbnail

🦁 카카오톡 로그인 API 구현하기

  • 이번 시간엔 카카오 로그인 API를 구현해보는 실습을 진행하였다. 아직 프론트엔드 서버를 만드는것을 배우지 않아서, 일단은 강사님이 주신 프론트엔드 서버를 사용하여 카카오로 로그인 시 백엔드 서버에서 구현한 JWT 토큰을 클라이언트에게 반환해주는 것 까지 실습을 진행하였다.

  • 카카오 로그인 API 구현하는 방법은 카카오 개발자 홈페이지에 상세하게 설명되있다. 따라서 실습자체도, 홈페이지에 나와있는 순서대로 진행하였다.

  • 아래는 내가 실습해보면서 정리한 진행 순서이다.
    1) 카카오 개발자 홈페이지( https://developers.kakao.com ) 에 로그인 한다.
    2) 내 애플리케이션에서 "애플리케이션 추가하기" 를 클릭한다.
    3) 앱 아이콘과 앱 이름은 카카오톡 로그인하기에서 메일과 비밀번호를 입력 시 동의하기
      창이 나오는데 그때 나오는 앱 아이콘과 이름이다. 본인이 원하는데로 입력해주고,
      사업자명과 카테고리도 개발하려는 서비스에 맞게 입력 및 선택해준뒤 저장한다.
    4) 다음부터는 문서-카카오 로그인-설정하기 에 따라서 필수로 되있는 것은 반드시
      설정하고, 선택은 서비스에 맞게 설정해주면 된다. 나는 테스트를 위해 필수로 되있는
      것만 설정해줬다.
    5) 카카오 로그인에서 활성화 설정을 OFF 에서 ON 으로 바꿔준다.
    6) Redirect URI를 등록해준다. 나는 나중에 테스트를 위해 아래처럼 설정해 줬다.
      ➡ http://localhost:8080/member/kakao
    7) 카카오 로그인-동의항목으로 이동한다.
    8) 개인정보에 대한 여러가지 동의항목들이 있는데, 카카오 로그인은 서비스를 시작할 때
      카카오로부터 서비스에 대한 심사 신청을 해서 승인 받아야 실시할 수 있다. 따라서
      나는, 테스트만 해보기 위해 심사 신청을 받지 않고, 개인 정보에서 닉네임만 필수
      동의로 설정
    해줬다.
    9) 카카오 로그인을 테스트해보기 위해 나는 별도의 프론트엔드 서버가 없기 때문에,
      kakao.html 파일을 하나 만들었다.
    10) 도구 - JS SDK 데모 - 카카오 로그인 - 로그인 으로 가보면 카카오 로그인 기능을
       구현하기 위한 javascript 코드가 있다. 이것을 복사하여 내가 만든 kakao.html 파일에
       붙여넣는다. 이때 수정해야 할 부분이 2곳이 있다.
       ➡ 4번째 줄에 kakao.init( ); 여기에 아까 생성한 내 애플리케이션 정보를 보면
         javascript 키가 있는데 그것을 넣어줘야된다.
       ➡ 16번째 줄에 redirectUri: 여기에도 내 애플리케이션에 설정한 Redirect URI
         주소를 동일하게 입력해줘야 된다. 그런다음 kakao.html 파일을 클릭해보면
         아래처럼 카카오 로그인 버튼이 있을 것이고, 클릭하면 동의하기 창이 나올
         것이다.


    11) 다음으로 로그인 기능 구현을 위해 문서 - 카카오 로그인 - REST API 를 클릭하여
       나와있는대로 설정을 해주겠다. 작성한 코드는 모두 설명에서 필요하다고 되있는것을
       바탕으로 작성한 것이다.
    12) 인가코드 받기는 사용자가 카카오 로그인을 누르면, 동의화면을 호출하고, 사용자가
       동의하기를 클릭하면 인가받은 동의항목 정보를 갖고 있으면 발급된다.
    13) 발급받은 인가코드로 카카오 Access 토큰을 받아온다.
    14) 가져온 Access 토큰에서 사용자 nickname 을 추출하여 DB에 존재하는 사용자
       정보인지 확인 후 존재하지 않으면, 회원가입 후 JWT 토큰을 발급해주고, 존재하면
       바로 JWT 토큰을 발급해준다.
    15) 위의 과정을 코드로 구현하면 아래와 같다.

    // ✅ KakaoService 클래스
    @Service
    public class KakaoService {
    
     // 💻 카카오 Access 토큰 발급 메서드
     public String getKakaoToken(String code) {
         HttpHeaders headers = new HttpHeaders();
         headers.add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8");
    
         MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
         params.add("grant_type", "authorization_code");
         params.add("client_id", "ca49904e4f88fe41677a3ce0ccf9dbd1");
         params.add("redirect_uri", "http://localhost:8080/member/kakao");
         params.add("code", code);
    
         HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
         RestTemplate restTemplate = new RestTemplate();
         ResponseEntity<Object> response = restTemplate.exchange(
                 "https://kauth.kakao.com/oauth/token",
                 HttpMethod.POST,
                 request,
                 Object.class
         );
    
         String result = "" + response;
         String accessToken = "" + result.split(",")[1].split("=")[1];
    
         return accessToken;
     }
     
     // 💻 카카오 Access 토큰으로 사용자 정보 중 nickname 추출 메서드
     public String getUserInfo(String accessToken) {
         HttpHeaders headers2 = new HttpHeaders();
         headers2.add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
         headers2.add(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8");
         HttpEntity request2 = new HttpEntity<>(headers2);
         RestTemplate restTemplate2 = new RestTemplate();
         ResponseEntity<Object> response2 = restTemplate2.exchange(
                 "https://kapi.kakao.com/v2/user/me",
                 HttpMethod.GET,
                 request2,
                 Object.class
         );
    
         String result2 = "" + response2.getBody();
         String userName = result2.split("nickname=")[1].split("}")[0];
    
         return userName;
     }
    }
    
     // ✅ MemberService 클래스
     
     // 💻 카카오 회원가입
     public void kakaoSignup(String userName) {
             memberRepository.save(Member.builder()
                     .username(userName)
                     .password(passwordEncoder.encode("kakao"))
                     .authority("ROLE_USER")
                     .build());
     }
    
     // 💻 카카오 로그인
     public String kakaoLogin(String userName) {
         Optional<Member> result = memberRepository.findByUsername(userName);
         
         // 만약 사용자 nickname이 DB에 없다면, 회원 가입 후 JWT 토큰을 발급하고, 그렇지 않으면 바로 발급
         if(result.isEmpty()) {
             kakaoSignup(userName);
             return JwtUtils.generateAccessToken(userName, secretKey, expiredTimeMs);
         } else {
             return JwtUtils.generateAccessToken(userName, secretKey, expiredTimeMs);
         }
     }
     
     // ✅ MemberController 클래스
     @RequestMapping(method = RequestMethod.GET, value = "/kakao")
     public ResponseEntity kakaoLogin(String code) {
         String accessToken = kakaoService.getKakaoToken(code);
         String userName = kakaoService.getUserInfo(accessToken);
    
         return ResponseEntity.ok().body(memberService.kakaoLogin(userName));
     }

  • 위 코드들은 이전글(JWT 토큰) 에서 작성한 코드에 해당 내용들을 추가하여 작성한 것이다.

    ➡ 이렇게 한 후, 카카오 로그인을 클릭, 동의하기를 클릭하면 아래와 같이 JWT 토큰이
      발급되는 것을 볼 수 있다. 이것을 JWT 토큰 홈페이지에서 변환해보면 사용자
      nickname의 정보가 들어간 것을 볼 수 있고, DB를 확인해보면 정상적으로 추가되있을
      것이다.


OAuth 2.0 을 사용하여 카카오 로그인 API 구현하기

  • 카카오 개발자 사이트에서 내 애플리케이션을 생성했다면, 왼쪽 메뉴에서 "플랫폼" 을 클릭하여 아래와 같이 Web 란에 사이트 도메인을 입력해준다.

  • 다음으로 애플리케이션의 Redirect URLhttp://localhost:8080/login/oauth2/code/kakao 이것으로 설정해 준다.

  • pom.xml 파일에 "OAuth 2.0" 과 관련된 라이브러리를 추가해준다.

          <dependency>
              <groupId>org.springframework.security</groupId>
              <artifactId>spring-security-oauth2-client</artifactId>
          </dependency>

    OAuth 2.0 은 간단하게 소셜 로그인 API 를 사용하기 위한 프로토콜이다.


  • application.yml 파일에 카카오 로그인과 관련된 설정을 추가해준다.

spring:
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: [내 애플리케이션 REST API Key 로 수정]
            scope:  // 카카오에서 사용할 기능 범위 설정
              - profile_nickname
              - profile_image
            authorization-grant-type: authorization_code // 인증을 위한 코드
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            client-name: Kakao
            client-authentication-method: POST
        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id
   

scope는 카카오톡에서 사용할 기능들에 대한 범위 인데, 카카오에서 개인정보
  동의항목 심사 신청을 받지 않았다면 개인정보 관련해서는 위의 2가지 밖에
  없다.


  • SecurityConfig 클래스에 OAuth 2.0 관련 설정을 추가해준다.
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    @Value("${jwt.secret-key}")
    private String secretKey;

    private final MemberRepository memberRepository;
    private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
    private final MemberOAuth2Service memberOAuth2Service;
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) {
        try {

            http.csrf().disable()
                    .authorizeHttpRequests()
                    .antMatchers("/member/*","/member/login", "/member/signup").permitAll()
                    .antMatchers("/order/create", "/order/list").hasAuthority("ROLE_USER")
                    .anyRequest().authenticated()
                    .and()
                    .formLogin().disable()
                    .addFilterBefore(new JwtFilter(secretKey, memberRepository), UsernamePasswordAuthenticationFilter.class)
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    // ✅ 여기부터 추가한 부분
                    .and()
                    // OAuth 2.0 로그인 처리
                    .oauth2Login()
                    // 로그인 성공 시 실행할 커스텀 핸들러 설정
                    .successHandler(oAuth2AuthenticationSuccessHandler)
                    // 인증이 완료된 사용자에 대한 정보를 가져옴
                    .userInfoEndpoint()
                    // userInfoEndpoint로부터 얻은 사용자 정보를 처리할 사용자 서비스를 지정
                    .userService(memberOAuth2Service);

            return  http.build();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

  • 로그인을 하기 위한 UserOAuth2Service 클래스를 만들어준다.

    @RequiredArgsConstructor
    @Service
    public class UserOAuth2Service extends DefaultOAuth2UserService {
       private final MemberService memberService;
       private final MemberRepository memberRepository;
    
     @Override
     public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
         OAuth2User oAuth2User = super.loadUser(userRequest);
         Map<String, Object> attributes = oAuth2User.getAttributes();
    
         Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
         String userName = (String) properties.get("nickname");
    
         Optional<Member> result = memberRepository.findByUsername(userName);
    		
         // DB에 없으면 회원 가입
         if(result.isEmpty()) {
             memberService.kakaoSignup(userName);
         }
         // 로그인 처리(JWT 토큰 발급)
         return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), attributes, "id");
     }
    }

  • 로그인 성공 시 처리할(나는 jwt 토큰을 반환해줄 것이다) 커스텀 핸들러 OAuth2AuthenticationSuccessHandler 클래스를 만든다.
@Component
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {  // SimpleUrl : 인증 성공 시의 동작을 정의

    @Value("${jwt.secret-key}")
    private String secretKey;

    @Value("${jwt.token.expired-time-ms}")
    private int expiredTimeMs;

    // 인증 성공 시 실행되는 메서드
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {

        // 로그인 성공한 사용자 목록.
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();

        Map<String, Object> properties = (Map<String, Object>) oAuth2User.getAttributes().get("properties");
        String nickname = (String) properties.get("nickname");
        String jwt = JwtUtils.generateAccessToken(nickname, secretKey, expiredTimeMs);

        String url = makeRedirectUrl(jwt); // 리다이렉트 url 생성
        System.out.println("url: " + url);

        if (response.isCommitted()) {
            logger.debug("응답이 이미 커밋된 상태입니다. " + url + "로 리다이렉트하도록 바꿀 수 없습니다.");
            return;
        }
        getRedirectStrategy().sendRedirect(request, response, url); // 생성된 리다이렉트 url로 리다이렉트 시킴
    }

    private String makeRedirectUrl(String token) {
        return UriComponentsBuilder.fromUriString("http://localhost:3000/oauth2/redirect/"+token)
                .build().toUriString();
    }
}

Swagger UI 란❓

  • Swagger 는 REST API를 설계, 빌드, 문서화 및 사용하는 데 도움이되는 OpenAPI 사양을 중심으로 구축 된 오픈 소스 도구 세트입니다.

  • 간단하게 말하면, 내가 REST API를 설계했을때, 다른 사람 즉 코드를 이해하지 못한 사람이나 처음보는 사람에게 내가 만든 것을 테스트 해보기 쉽게 웹 페이지를 제공하는 것으로 이해할 수 있을것 같다.

  • 사용방법
    1) pom.xml 파일에 라이브러리를 추가해준다.

     ```java
     		    <dependency>
    		<groupId>io.springfox</groupId>
    		<artifactId>springfox-boot-starter</artifactId>
    		<version>3.0.0</version>
    	</dependency>
     ```

    2) application.yml 파일에 아래와 같이 추가해준다. (사유 : 스프링 버전과 호환 문제)

    spring:
       mvc:
         pathmatch:
           matching-strategy: ant_path_matcher

    3) SwaggerConfig 클래스를 작성한다.

    
    @Configuration
    @EnableSwagger2
    public class SwaggerConfig  {
    
      @Bean
      public Docket newsApiAll() {
          return new Docket(DocumentationType.SWAGGER_2)
                  .groupName("00. All Device API REST Service")
                  .apiInfo(apiInfo())
                  .select()
                  .paths(PathSelectors.any())
                  .build();
      }
    
      @Bean
      public Docket newsApiAccelerator() {
          return new Docket(DocumentationType.SWAGGER_2)
                  .groupName("01. Accelerator Guide Program Service")
                  .apiInfo(apiInfo())
                  .select()
                  .paths(regex("/acl.*"))
                  .build();
      }
    
      @Bean
      public Docket api() {
          return new Docket(DocumentationType.SWAGGER_2)
                  .groupName("test")  // select definition 오른쪽 상단위 출력 표시
                  .select()
                  .apis(RequestHandlerSelectors.basePackage("com.example.springdoc"))
                  .paths(PathSelectors.any())
                  .build()
                  .apiInfo(apiInfo());
      }
    
      private ApiInfo apiInfo() {
          return new ApiInfoBuilder()
                  .title("Spring Boot Test")
                  .description("스프링 부트 테스트")
                  .version("2.0.0")
                  .license("Apache License Version 2.0")
                  .licenseUrl("https://www.apache.org/licenses/LICENSE-2.0\"")
                  .contact(new Contact("블로그 주소", "https://velog.io/@passion_hd", "test@naver.com"))
                  .build();
      }
    }

    4) MemberController/ProductController 클래스 및 MemberLoginReq 클래스를
      작성한다.

    // MemberController 클래스
    @RestController
    @Api(value = "회원 컨트롤러 v1", tags = "회원 API")
    @RequestMapping("/member")
    public class MemberController {
    
      @ApiOperation("로그인")
      @RequestMapping(method = RequestMethod.POST, value = "/login")
      public ResponseEntity login(MemberLoginReq memberLoginReq) {
          return ResponseEntity.ok().body("login process");
      }
    
      @ApiOperation("회원가입")
      @RequestMapping(method = RequestMethod.POST, value = "/signup")
      public ResponseEntity signup(MemberSignupReq memberSignupReq) {
          return ResponseEntity.ok().body("signup process");
      }
    }
    
    // ProductController 클래스
    @RestController
    @Api(value = "상품 컨트롤러 v1", tags = "상품 API")
    @RequestMapping("/product")
    public class ProductController {
      @RequestMapping(method = RequestMethod.GET, value = "/list")
      public ResponseEntity list() {
          return ResponseEntity.ok().body("product list");
      }
    }
    // MemberLoginReq 클래스
    @Getter
    @Setter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class MemberLoginReq {
    
      @ApiParam(value = "회원의 이메일을 입력", required = true, example = "test01@naver.com")
      private String email;
    
      @ApiParam(value = "회원의 패스워드를 입력", required = true)
      private String password;
    }

  • localhost:8080/swagger-ui/ 로 접속하면 아래와 같이 내가 설정한대로 Swagger 화면이 출력될 것이다.

  • 각각의 API를 클릭해보면 내가 설정한대로 출력되는 것을 볼 수 있다.

  • Try it out을 클릭하여 데이터를 입력하고 요청해보면 결과가 반환된다.

  • 이처럼, Swagger UI는 개인이 설정하는것에 따라 출력되기 때문에, 여러가지 설정법들을 활용하면 보기 쉽게 구성하는 것이 가능하다.

  • 내가 위에서 설정한 것은 가장 기본적으로 설정한 것이고, 추가적으로 Swagger 관련 사용법을 찾아보면 다양한 설정할 수 있는 내용들을 찾아볼 수 있을 것이다.


🐼 제약조건 설정하기

  • 제약조건이란 데이터의 무결성을 지키기 위해, 데이터를 입력받을 때 실행되는 검사 규칙을 의미한다. DB에서 테이블을 만들고 속성을 추가할 때 우리는 제약조건을 추가한다.

  • 하지만, 그동안 실습과정에서 테이블인 Entity 클래스를 만들때 별도의 제약조건이라곤 primary keyAuto-Increment 를 설정해준거 외엔 없었다.

  • 따라서, 속성에 제약조건을 설정하기 위해선 속성 변수 위에 @Column 어노테이션을 달아서 설정한다.

  • @Column(nullable=false, length=50, unique=true) 라고 할때, nullabe=falseNot NULL 제약조건이고, lengthVARCHAR(50) 과 같이 문자열의 길이를 지정한다. 또한 unique=trueUNIQUE 제약조건으로 중복값을 허용하지 않는 설정이다.

  • 이처럼, 앞으로는 Entity 클래스를 생성할 때 설계한 ERD와 동일하게 제약조건을 설정해줘야 한다.

🐶 입력값 검증하기

  • 입력값 검증은 REST API 개발 시 입력으로 받을 값을 검증하는 방법이다.

  • 먼저 pom.xml 파일에 라이브러리를 추가해준다.

    		    <dependency>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-starter-validation</artifactId>
    			</dependency>
  • 다음으로 입력받는 DTO 클래스에 아래와 같이 입력값 검증을 위한 설정을 추가해줄 수 있다.

    @Getter
    @Setter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class MemberLoginReq {
    
      @Pattern(regexp = "^[a-zA-Z0-9+-\\_.]+@naver.com+$")
      @ApiParam(value = "회원의 이메일을 입력", required = true, example = "test01@naver.com")
      private String email;
    
      @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,15}$")
      @Size(min= 1, max = 20) // 문자열 길이 지정
      @Max(10) // 숫자 크기 지정
      @ApiParam(value = "회원의 패스워드를 입력", required = true, example = "qwer1234")
      private String password;
    }

    @Pattern 은 입력받는 데이터의 형식을 지정하는 것이다. 위에서는 이메일 형식으로
      입력받도록 설정한 것이다.
    @NotNull 은 말그대로 null 이 아니어야 된다는 것을 설정한 것이다.
    @Min(10) 은 최소길이가 10자라는 것을 설정한 것이다.
    ➡ 이외에도 다양한 입력값 검증을 위한 어노테이션들이 있으며, 검색해보면 알 수 있다.

  • 검증을 하기 위해서는 Controller 클래스의 DTO 객체 앞에 @Valid 를 붙이면 된다.

@RestController
@Api(value = "회원 컨트롤러 v1", tags = "회원 API")
@RequestMapping("/member")
public class MemberController {

    @ApiOperation("로그인")
    @RequestMapping(method = RequestMethod.POST, value = "/login")
    public ResponseEntity login(@Valid MemberLoginReq memberLoginReq) {
        return ResponseEntity.ok().body("login process");
    }

    @ApiOperation("회원가입")
    @RequestMapping(method = RequestMethod.POST, value = "/signup")
    public ResponseEntity signup(@Valid MemberSignupReq memberSignupReq) {
        return ResponseEntity.ok().body("signup process");
    }
}

오늘의 느낀점 👀

  • 오늘은 소셜 로그인 중 하나인 카카오 로그인 API를 오전에 배워봤다. 카카오에서 API 문서를 상세하게 설명해놔서 문서대로 따라하니 구현하는 것은 어렵지 않았다.

  • 중요한 것은, 그 문서를 보고 어떤 내용을 해야되는지 캐치하는 것이라 생각한다. 그럴려면 어느정도 HTTP 요청/응답 등에 대한 개념을 숙지한 상태에서 진행해야 할 것이다.

  • Swagger 는 나중에 취업 시 과제제출때 많이 사용되기도 한다고 한다. 결국엔 내가 작성한 코드를 다른 사람이 테스트해보려면 일종의 테스트 순서를 적어논 문서가 필요한데 문서로 보기에는 한계가 있으니, 테스트 하는 절차 자체를 웹 서버로 구현해놓은 것이라 생각한다.

  • DB 프로젝트로 진행했던 "LONUA" 에 대해 오늘부터 모든 테이블을 생성하고, 각 테이블별 CRUD 기능을 구축해야된다. 또한 오늘 배운 제약조건 및 입력값 검증 등의 내용까지 포함해서 작성해야되므로, 앞으로 남은 시간엔 그 프로젝트를 완성시키는데 시간을 쓸까 한다.

profile
Backend Developer

0개의 댓글