Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags more
Archives
Today
Total
관리 메뉴

요리사에서 IT개발자로

(스파르타 코딩클럽)Vue.js 와 SpringBoot 네이버 소셜로그인 연동하기 본문

TIL

(스파르타 코딩클럽)Vue.js 와 SpringBoot 네이버 소셜로그인 연동하기

H.S-Backend 2024. 8. 10. 15:12

네이버에서 제공하는 로그인 API를 프론트엔드에 적용하기 위해 사용할 수 있는 네이버 아이디 로그인 SDK는 사용하지 않았습니다.

  1. 프론트엔드에서 Naver 로그인시 제공받은 Access토큰을 백엔드에서 받아
  2. Naver에 접근하여 유저의 정보를 가지고 온 후에 
  3. 검증을하고 나의 프로젝트에 맞게 User정보를 리팩토링
  4. 저장 및 Jwt토큰, 쿠키(리프레시토큰)를 발급 해줍니다.

 

 현재 진행 중인 프로젝트를 일부분을 수정하고 소셜로그인 기능만 보여드리는 것을 보여드리는 것 입니다.

 

소셜로그인이란?
소셜네트워킹 사이트의 정보를 이용해 타사 애플리케이션과 손쉽게 로그인할 수 있는 프로세스

https://www.okta.com/kr/blog/2020/08/social-login/

 

소셜 로그인이란? 정의, 이점 및 구현 가치

소셜 로그인은 소셜 네트워킹 사이트의 정보를 이용해 타사 애플리케이션과 플랫폼에 손쉽게 로그인할 수 있는 프로세스를 말합니다. 이 프로세스가 제공하는 특징 및 이점, 그리고 구현 가치

www.okta.com

 


https://developers.naver.com/products/login/api/api.md

 

네이버 로그인 - INTRO

환영합니다 네이버 로그인의 올바른 적용방법을 알아볼까요? 네이버 로그인을 통해 신규 회원을 늘리고, 기존 회원은 간편하게 로그인하게 하려면 제대로 적용하는 것이 중요합니다! 이에 올바

developers.naver.com

 

네이버 개발자 센터에서 로그인을 하고 

 

애플리케이션 이름은 본인이 확인할 수 있는 제목 입력하시면됩니다.

 

 

발급되는 ClientId와 SecretKey를 기억해놓으면됩니다.

 

 

 

Spring Boot 프로젝트에 환경 변수를 설정해 줍니다.

 

위와 같이 Naver에서 제공해준 NaverClientID와 Secret키를 환경변수에 저장을한 뒤

# OAuth2 registration for Naver
spring.security.oauth2.client.registration.naver.client-name=naver
spring.security.oauth2.client.registration.naver.client-id=${NAVER_CLIENT_ID}
spring.security.oauth2.client.registration.naver.client-secret=${NAVER_CLIENT_SECRET}
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8081/auth/callback/naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email,nickname,profileImage

# OAuth2 provider for Naver
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response

 

위와 같이 작성을 해줍니다.


build.gradle에 

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

build를 해주고


WebSecurityConfig 

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final UserDetailServiceImpl userDetailsService;
    private final JwtUtil jwtUtil;

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

    @Bean
    public JwtAuthorizationFilter jwtAuthenticationFilter() {
       return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       http
          .csrf(csrf -> csrf.disable())  // CSRF 보호 비활성화

          .formLogin(form -> form.disable())  // 기본 폼 로그인 방식 비활성화

          .httpBasic(basic -> basic.disable())  // HTTP Basic 인증 방식 비활성화

          .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  // 세션 설정 : STATELESS

          .authorizeHttpRequests(authorize -> authorize
             .requestMatchers(HttpMethod.POST, "/api/**").permitAll()
             .anyRequest().authenticated())

          .logout(logout -> logout
             .logoutUrl("/api/logout")
             .logoutSuccessHandler((request, response, authentication) -> {
                response.setStatus(HttpServletResponse.SC_OK);
             }))

          .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);  // JWT 필터 추가

       return http.build();
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
       return new WebMvcConfigurer() {

          @Override
          public void addCorsMappings(CorsRegistry registry) {
             registry.addMapping("/**")
                .allowedOrigins("http://localhost:8081")
                .exposedHeaders("authorization") // 이 부분을 추가합니다.
                .allowCredentials(true) // 쿠키 인증 요청 허용
                .allowedHeaders("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH" ,"OPTIONS");
          }

          @Override
          public void addResourceHandlers(ResourceHandlerRegistry registry) {
             registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/META-INF/resources/");
             registry.addResourceHandler("index.html")
                .addResourceLocations("classpath:/META-INF/resources/")
                .setCacheControl(CacheControl.noStore()) // 브라우저 resource 저장 사용 x
                .setCachePeriod(0); // 서버 캐시 사용하지 않음.
          }
       };
    }
}

 

CORS 설정을 해줍니다.


OAuth2CallbackController

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class OAuth2CallbackController {

    private final OAuth2CallbackServiceImpl oAuth2CallbackService;

    @PostMapping("/naver")
    public ResponseEntity<CommonResponseDto<Void>> naverCallback(
       @RequestBody Map<String, String> params,
       HttpServletResponse httpServletResponse
    ) {
       oAuth2CallbackService.handleNaverLogin(params, httpServletResponse);

       return ResponseEntity.ok().body(new CommonResponseDto<>(
          HttpStatus.OK.value(), "네이버 로그인이 완료되었습니다.", null));
    }
}

 

 

OAuth2CallbackServiceImpl 

@Service
@RequiredArgsConstructor
public class OAuth2CallbackServiceImpl {

    private final JwtUtil jwtUtil;
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final RefreshTokenService refreshTokenService;

    @Value("${spring.security.oauth2.client.registration.naver.client-id}")
    private String clientId;

    @Value("${spring.security.oauth2.client.registration.naver.client-secret}")
    private String clientSecret;

    @Value("${spring.security.oauth2.client.registration.naver.redirect-uri}")
    private String redirectUri;

    @Value("${spring.security.oauth2.client.provider.naver.user-info-uri}")
    private String userInfoUrl;

    @Value("${jwt.refresh-expire-time}")
    private int refreshTokenExpireTime; // 초 단위

    public void handleNaverLogin(Map<String, String> params, HttpServletResponse httpServletResponse) {
       String code = params.get("code");
       String state = params.get("state");
       validateStatusCodeAndState(code, state);

       String tokenUrl = buildTokenUrl(code, state);
       String accessToken = getAccessToken(tokenUrl);
       ResponseEntity<String> userInfoResponse = getUserInfo(accessToken);

       checkStatusCode(userInfoResponse);
       processNaverLogin(userInfoResponse.getBody(), httpServletResponse);
    }

    private String buildTokenUrl(String code, String state) {
       return "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code"
          + "&client_id=" + clientId
          + "&client_secret=" + clientSecret
          + "&redirect_uri=" + redirectUri
          + "&code=" + code
          + "&state=" + state;
    }

    private String getAccessToken(String tokenUrl) {
       RestTemplate restTemplate = new RestTemplate();
       ResponseEntity<String> response = restTemplate.getForEntity(tokenUrl, String.class);
       checkNaverAccessCode(response);
       return extractAccessToken(response.getBody());
    }

    private ResponseEntity<String> getUserInfo(String accessToken) {
       HttpHeaders headers = new HttpHeaders();
       headers.set(JwtUtil.AUTHORIZATION_HEADER, JwtUtil.BEARER_PREFIX + accessToken);
       HttpEntity<String> entity = new HttpEntity<>(headers);

       RestTemplate restTemplate = new RestTemplate();
       return restTemplate.exchange(userInfoUrl, HttpMethod.GET, entity, String.class);
    }

    public String extractAccessToken(String responseBody) {
       try {
          ObjectMapper objectMapper = new ObjectMapper();
          JsonNode rootNode = objectMapper.readTree(responseBody);
          return rootNode.path("access_token").asText();
       } catch (Exception e) {
          throw new GlobalException(ErrorCode.FAILED_TO_EXTRACT_ACCESS_TOKEN);
       }
    }

    public void processNaverLogin(String userInfoResponseBody, HttpServletResponse httpServletResponse) {
       try {
          ObjectMapper objectMapper = new ObjectMapper();
          JsonNode rootNode = objectMapper.readTree(userInfoResponseBody);
          String email = rootNode.path("response").path("email").asText();
          String name = rootNode.path("response").path("name").asText();
          String nickname = rootNode.path("response").path("nickname").asText();

          Optional<User> optionalUser = userRepository.findByEmail(email);
          User user = optionalUser.orElseGet(() -> createUser(email, name, nickname));

          jwtUtil.issueTokens(user, httpServletResponse, refreshTokenService, refreshTokenExpireTime);
       } catch (Exception e) {
          throw new GlobalException(ErrorCode.FAILED_TO_PROCESS_NAVER_LOGIN);
       }
    }

    private User createUser(String email, String name, String nickname) {
       User newUser = User.builder()
          .email(email)
          .username(email)
          .password(passwordEncoder.encode("temporary_password"))
          .name(name)
          .nickname(nickname)
          .userRole(UserRole.USER)
          .userStatus(UserStatus.ACTIVE)
          .build();
       return userRepository.save(newUser);
    }

    private void validateStatusCodeAndState(String code, String state) {
       if (code == null || state == null) {
          throw new GlobalException(ErrorCode.MISSING_REQUIRED_PARAMETERS);
       }
    }

    private void checkNaverAccessCode(ResponseEntity<String> response) {
       if (response.getStatusCode() != HttpStatus.OK) {
          throw new GlobalException(ErrorCode.FAILED_TO_GET_ACCESS_TOKEN_FROM_NAVER);
       }
    }

    private void checkStatusCode(ResponseEntity<String> userInfoResponse) {
       if (userInfoResponse.getStatusCode() != HttpStatus.OK) {
          throw new GlobalException(ErrorCode.FAILED_TO_GET_USER_INFO_FROM_NAVER);
       }
    }
}

 

User

@Table(name = "db_users")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Entity
public class User extends TimeStamp {

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

    @Column(name = "username", nullable = false, unique = true)
    private String username;

    @Email
    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "nickname", nullable = false, unique = true)
    private String nickname;

    @Column(name = "introduce")
    private String introduce;

    @Column(name = "image", columnDefinition = "TEXT")
    private String image;

    @Builder.Default
    @Column(name = "point")
    private Long point = 0L;

    @Column(name = "status", nullable = false)
    @Enumerated(EnumType.STRING)
    private UserStatus userStatus;

    @Column(name = "userRole", nullable = false)
    @Enumerated(EnumType.STRING)
    private UserRole userRole;

 


이제 Vue.js로 넘어가서

Login.vue

<template>
      <div class="social-login">
        <a :href="naverLoginUrl" class="social-btn naver-btn">
          <i class="fas fa-n"></i> 네이버
        </a>
      </div>

 

clientId와 redirectUrl은 Naver API에서 제공받은 clientId를 입력해주는 것.

export default {
  name: 'Login',
  data() {
    return {
      loginData: {
        username: '',
        password: ''
      },
      errorMessage: '',
      clientId: '_mFzmKmjK57NuQA5jw2I',
      redirectUri: 'http://localhost:8081/auth/callback/naver',
      state: 'random_state_string'
    };
  },
  computed: {
    naverLoginUrl() {
      const baseUrl = 'https://nid.naver.com/oauth2.0/authorize';
      const responseType = 'code';
      return `${baseUrl}?client_id=${this.clientId}&response_type=${responseType}&redirect_uri=${this.redirectUri}&state=${this.state}`;
    }
  },

 

OAuth2Callback.vue

<template>
  <div>
    <h1>네이버 로그인 콜백 처리 중...</h1>
  </div>
</template>

<script>
import axios from '@/axios';

export default {
  name: 'OAuthCallback',
  async mounted() {
    const urlParams = new URLSearchParams(window.location.search);
    const code = urlParams.get('code');
    const state = urlParams.get('state');

    if (code && state) {
      try {
        const response = await axios.post('/auth/naver', {
          code: code,
          state: state
        }, {
          headers: {
            'Content-Type': 'application/json'
          }
        });
        const accessToken = response.headers['authorization'];
        localStorage.setItem('accessToken', accessToken);
        this.$store.commit('setAccessToken', accessToken);
        this.$store.commit('setAuthenticated', true);
        this.$router.push('/');
      } catch (error) {
        console.error('네이버 로그인 실패:', error);
      }
    } else {
      console.error('네이버 로그인 콜백 처리 중 오류 발생: code 또는 state가 없습니다.');
    }
  }
};
</script>

 

axios.js

const instance = axios.create({
  baseURL: 'http://localhost:8080/api', // Backend server URL
  timeout: 5000,
  withCredentials: true, // 자격 증명 포함
});

 

index.js

import {createRouter, createWebHistory} from 'vue-router';

import OAuthCallback from "@/components/OAuthCallback.vue";

const routes = [
  {
    path: '/auth/callback/naver',
    name: 'OAuthCallback',
    component: OAuthCallback
  }
];
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
});

export default router;

Vue.js 포트는 8081로 해놨으며

 

Spring Boot 포트 8080 입니다.

 

 

 Vue.js로 만든 웹페이지에서 네이버 로그인 시 동작하는 것을 하나하나 확인해보겠습니다.

 

 

네이버 로그인 버튼을 클릭

 

 

위와 같은 네이버 로그인 화면이 나오는 것을 확인할 수 있고 또한

 

프론트에 입력해놓은 값이 보이는 것을 확인할 수 있습니다.

redirectUrl과 ClientId를 틀리시면 안됩니다.

 

로그인을 진행을 하면 NaverLogin.vue 페이지를 만들어 놓은곳으로 이동을 하고

 

axios.js에 설정해놓은 백엔드API주소 및 NaverLogin.vue에 정의해둔 백엔드 API 주소로 접근을 하게 될 것입니다.

params로 받은 Code와 state를 들고 

OAuth2CallbackServiceImpl로 접근을 합니다.

 

 로그인한 정보(Proxy)가

 

 

null인지 아닌지를 검증을하고

private void validateStatusCodeAndState(String code, String state) {
    if (code == null || state == null) {
       throw new GlobalException(ErrorCode.MISSING_REQUIRED_PARAMETERS);
    }
}

 

 buildTokenUrl의 매개변수로 code와 state값을 지정해줍니다.

Naver API에서 발급받은 clientId, secretKey, redirectUrl을 환경변수에 작성해놓은 값

 

백엔드에서 application-properties에 작성해둔부분을 참고 

spring.security.oauth2.client.registration.naver.client-id=${NAVER_CLIENT_ID}
spring.security.oauth2.client.registration.naver.client-secret=${NAVER_CLIENT_SECRET}
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8081/auth/callback/naver

 

url이 만들어진것을 확인할 수 있습니다.

 

 

이제 tokenUrl을 RestTemplate으로 재정의

 

네이버에서 제공해준 accessToken이 있는지를 검증합니다.

없다면 Exception처리가 발생

response 안의 body를 확인해보시면

Naver에서 제공해준 access_token안의 로그인한 사용자의 정보가 담긴것을 확인할 수 있습니다.

RestTemplate을 사용하여 웹서비스와 상호작용을 하는것도 확인할 수 있습니다.

 

RestTemplate은 Spring의 HTTP 클라이언트로, 아래와 같은 이유로 많이 사용됩니다:

 

  • 간편한 HTTP 요청: RestTemplate은 GET, POST, PUT, DELETE와 같은 다양한 HTTP 메소드를 간편하게 사용할 수 있게 도와줍니다. 이를 통해 외부 API에 쉽게 요청을 보낼 수 있습니다.
  • 자동으로 JSON/XML 변환: RestTemplate은 요청의 응답을 JSON 또는 XML 형태로 자동 변환해주는 기능을 제공합니다. 예를 들어, 외부 API에서 JSON 형식의 응답을 받으면 이를 자동으로 Java 객체로 변환할 수 있습니다.
  • 예외 처리 기능 제공: RestTemplate은 다양한 HTTP 상태 코드에 대한 예외를 처리할 수 있는 기능을 제공합니다. 예를 들어, 404 Not Found나 500 Internal Server Error와 같은 응답이 왔을 때 적절한 예외를 던져줍니다.
  • 헤더 설정 및 요청 파라미터 관리: RestTemplate은 요청에 대한 HTTP 헤더 설정이나 URL 파라미터를 간편하게 관리할 수 있도록 도와줍니다. 이를 통해 인증 토큰이나 사용자 지정 헤더를 쉽게 추가할 수 있습니다.
  • 유연성: Spring의 다양한 기능과 통합할 수 있으며, 특히 RestTemplate을 활용하면 OAuth2와 같은 인증 로직을 구현할 때도 쉽게 활용할 수 있습니다. 또한, RestTemplate은 필요에 따라 커스터마이징이 가능해 유연한 사용이 가능합니다.

 

 

 

extractAccessToken (액세트 토큰을 추출)

ObjectMapper와 JsonNode는 JSON 데이터의 파싱 및 처리에서 매우 유용합니다. ObjectMapper는 JSON 데이터를 Java 객체로 변환하는 데 유연하고, JsonNode는 트리 형태로 JSON 데이터를 다루는 데 효과적입니다. 이러한 도구들을 통해 JSON 데이터를 처리할 때 발생할 수 있는 오류를 최소화하고, 데이터를 효율적으로 관리할 수 있습니다.

ObjectMapper 

ObjectMapper는 Jackson 라이브러리에서 제공하는 클래스입니다. JSON 데이터를 Java 객체로 변환하거나, Java 객체를 JSON으로 변환하는 데 사용됩니다. 이 클래스는 다음과 같은 이유로 많이 사용됩니다:

  • 유연성: ObjectMapper는 다양한 데이터 포맷(JSON, XML 등)을 지원하며, JSON 데이터를 쉽게 직렬화 및 역직렬화할 수 있습니다.
  • 자동 변환: JSON 데이터 구조를 Java 클래스에 자동으로 매핑해주기 때문에, 개발자가 일일이 JSON 필드를 처리하지 않아도 됩니다.
  • 에러 핸들링: JSON 데이터를 파싱할 때 발생할 수 있는 예외를 체계적으로 처리할 수 있습니다. 이를 통해 잘못된 JSON 데이터로 인한 오류를 쉽게 관리할 수 있습니다.

JsonNode 

JsonNode는 Jackson 라이브러리의 JsonNode 트리 모델을 사용하여 JSON 데이터를 표현하는 객체입니다. 트리 모델은 JSON 데이터를 노드 형태로 표현하여, 특정 데이터에 접근하거나 수정하는 것이 매우 쉽습니다.

  • 트리 형태의 접근: JSON 데이터를 트리 구조로 표현하므로, 계층적 JSON 구조에서 특정 필드에 쉽게 접근할 수 있습니다. 예를 들어, rootNode.path("access_token")을 사용해 access_token 필드의 값을 쉽게 추출할 수 있습니다.
  • 선택적 접근: JsonNode를 사용하면 JSON 데이터의 특정 필드만 선택적으로 접근할 수 있으며, 해당 필드가 존재하지 않을 경우에도 예외가 발생하지 않도록 안전하게 처리할 수 있습니다.
  • 비교적 경량: JsonNode는 전체 데이터를 Java 객체로 변환하지 않고도, JSON 데이터의 특정 부분에만 접근할 수 있게 해주기 때문에, 메모리 사용이 상대적으로 적습니다.

 

Naver에서 발급한 토큰안의 있는 나의 정보를 "파싱한다"라고 생각하면 좋습니다.

 

이제 access_token안의 나의 정보를 가져올 차례입니다.

 

 

userInfoUrl은 application-propeties에 작성해 놓은

spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me

 

이 링크를 통해 GET방식으로

Naver 에서 발급해준 access_token의 header와 Bearer을 붙여주고 나의 정보를 가지고 올 것 입니다.

 

정상적으로 200코드를 받지 못했다면 예외처리가 발생하게 됩니다.

정상적으로 처리가되어 userInfoResponse의 body를 보시면

Naver에서 로그인정보를 정상적으로 가져오는 것을 확인할 수 있습니다.

 

 

여기서 response안의 email을 가져오기위해 rootNode를 사용하였고 (key안의 value를 가져오기위해)

email과 name, nickname을 가져와서 userRepository에 저장을 합니다.

또한 

jwtUtil.issueToken이 Jwt토큰과 쿠키안의 RefreshToken을 발급하고 DB에 저장을 해줍니다.
public void issueTokens(User user, HttpServletResponse response, RefreshTokenService refreshTokenService, int refreshTokenExpireTime) {
    String accessToken = createAccessToken(user.getUsername(), user.getUserRole());
    String refreshToken = createRefreshToken(user.getUsername());

    refreshTokenService.createOrUpdateRefreshToken(user, refreshToken, LocalDateTime.now().plusSeconds(refreshTokenExpireTime));

    addJwtToHeader(AUTHORIZATION_HEADER, BEARER_PREFIX + accessToken, response);
    addRefreshTokenCookie(response, refreshToken);
}

 

이제 정상적으로 나의 DB에 저장이되고 웹페이지에서도 Jwt토큰과 Cookie를 발급받은것을 확인할 수 있습니다.

 

로컬스토리지에 저장하는 것은 당연히 프론트엔드에서 해줘야되는 부분입니다.
if (code && state) {
  try {
    const response = await axios.post('/auth/naver', {
      code: code,
      state: state
    }, {
      headers: {
        'Content-Type': 'application/json'
      }
    });
    const accessToken = response.headers['authorization'];
    localStorage.setItem('accessToken', accessToken);
    this.$store.commit('setAccessToken', accessToken);
    this.$store.commit('setAuthenticated', true);
    this.$router.push('/');
  } catch (error) {
    console.error('네이버 로그인 실패:', error);
  }
} else {
  console.error('네이버 로그인 콜백 처리 중 오류 발생: code 또는 state가 없습니다.');
}

 

 

1. 네이버 로그인 API 통합 문제

문제점:
네이버 로그인 API를 프로젝트에 통합할 때, 초기 설정과 API 호출 과정에서 여러 문제가 발생했습니다. 특히 RestTemplate을 사용한 HTTP 요청에서 엑세스 토큰을 가져오는 과정에서 예상치 못한 오류가 발생하거나, API 호출 후 반환된 데이터를 처리하는 데 문제가 있었습니다.

해결 방법:

  • API Key 및 Secret 설정: 네이버 개발자 센터에서 제공하는 클라이언트 ID와 시크릿 키를 정확하게 설정하는 것이 가장 중요했습니다. 설정 파일에 API 키를 환경 변수로 분리하고, 코드에서는 이를 주입하여 사용했습니다.
  • RestTemplate 활용: RestTemplate을 사용하여 네이버 API에 HTTP 요청을 보내고, 엑세스 토큰을 추출하는 로직을 구현했습니다. 이때 RestTemplate의 응답 데이터를 JSON 파싱하여 필요한 데이터를 추출하는 방법을 사용했습니다. 또한, 예외 처리 기능을 추가하여 HTTP 요청이 실패할 경우 적절한 오류 메시지를 표시하도록 했습니다.

2. 프론트엔드에서의 네이버 로그인 버튼 표시 문제

문제점:
네이버 로그인 버튼이 프론트엔드에서 제대로 표시되지 않거나, SDK를 이용한 초기화 과정에서 에러가 발생했습니다. 특히 getLoginStatus 메서드를 사용할 때 자주 발생했습니다.

해결 방법:

  • 네이버 SDK 초기화: 네이버에서 제공하는 SDK를 적절히 초기화하는 것이 가장 중요했습니다. 프론트엔드 코드에서는 네이버 SDK를 로드한 후, 적절한 시점에 초기화 로직을 실행하도록 수정했습니다. 예를 들어, mounted 훅을 사용하여 컴포넌트가 마운트된 직후에 네이버 로그인 버튼을 초기화하는 방법을 사용했습니다.
  • 버튼 스타일링과 동적 로드: 버튼 스타일링을 커스터마이징하고, 네이버 로그인 버튼이 포함된 컴포넌트를 동적으로 로드하여 성능을 최적화했습니다.

3. JWT 토큰 발급 및 사용자 인증 처리 문제

문제점:
네이버 로그인을 통해 사용자를 인증한 후, JWT 토큰을 발급하고 사용자 세션을 유지하는 과정에서 어려움이 있었습니다. 특히, 네이버 로그인에서 받은 엑세스 토큰을 백엔드로 전달하고, 이를 기반으로 JWT 토큰을 생성하는 로직에서 오류가 발생했습니다.

해결 방법:

  • 백엔드에서 JWT 발급: 백엔드에서는 네이버 API로부터 받은 사용자 정보를 기반으로 JWT 토큰을 생성하는 로직을 추가했습니다. 사용자 정보가 DB에 존재하면 토큰을 생성하고, 존재하지 않으면 새로운 사용자를 생성한 후 토큰을 발급하도록 구현했습니다.
  • 프론트엔드에서의 토큰 관리: 프론트엔드에서는 JWT 토큰을 로컬 스토리지에 저장하고, 이를 활용하여 인증된 API 호출을 할 수 있도록 설정했습니다. 네트워크 요청 시 토큰이 없거나 유효하지 않으면 재로그인을 요구하는 로직도 추가했습니다.

결론

소셜 로그인은 사용자 경험을 개선할 수 있는 강력한 도구지만, 구현 과정에서 다양한 기술적 문제에 직면할 수 있습니다. 이번 프로젝트에서는 네이버 로그인을 통합하는 과정에서 API 설정, SDK 활용, JWT 토큰 처리 등의 문제를 해결하며 많은 것을 배울 수 있었습니다. 이러한 경험을 바탕으로 앞으로도 다양한 소셜 로그인 서비스를 보다 원활하게 통합할 수 있을 것입니다.

 

https://hs-backend.tistory.com/233

 

(스파르타 코딩클럽) 스프링 시큐리티에서 JWT토큰 인증을 어떻게 할까?

백엔드에서 정의한 WebSecurityConfig클래스에 permitAll 이 아닌 URI로 접근을 할 경우 인증절차가 되는 방법을 알아보고자 한다. .authorizeHttpRequests(authorize -> authorize .requestMatchers(HttpMethod.POST, "/api/**").p

hs-backend.tistory.com

반응형