4차산업혁명의 일꾼/웹개발

OAuth와 SSR

르무엘 2024. 6. 18. 03:22

OAuth (Open Authorization)는 웹사이트나 애플리케이션이 사용자의 비밀번호를 공유하지 않고도 다른 애플리케이션 또는 웹사이트에 제한된 자원 접근 권한을 부여할 수 있도록 하는 개방형 표준으로 즉, OAuth는 안전한 위임(access delegation)을 위한 프로토콜 규약이다.

주요 개념

  1. 자원 소유자 (Resource Owner): 자원을 소유한 사용자입니다. 예를 들어, 사용자가 Google Drive에 저장된 파일을 가지고 있다면, 사용자가 자원 소유자
  2. 클라이언트 (Client): 자원 소유자가 접근을 허락한 제3의 애플리케이션입니다. 예를 들어, 사용자가 자신의 Google Drive 파일에 접근하도록 허용한 웹 애플리케이션이 클라이언트
  3. 자원 서버 (Resource Server): 보호된 자원이 저장된 서버입니다. Google Drive 서버가 그 예
  4. 인증 서버 (Authorization Server): 자원 소유자가 자원 접근 권한을 클라이언트에게 부여하기 위해 사용되는 서버로, 이 서버는 클라이언트에게 액세스 토큰을 발급
  5. 액세스 토큰 (Access Token): 클라이언트가 자원 서버에 접근하기 위해 사용하는 토큰으로, 이는 제한된 시간 동안 유효하며, 특정 자원에 대한 접근 권한을 나타낸다.

카카오 로그인 OAuth 흐름

 public void kakaoLogin(String code) throws JsonProcessingException {
        // 1. "인가 코드"로 "액세스 토큰" 요청
        String accessToken = getAccessToken(code);

        // 2. "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
        KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);

        // 3. "카카오 사용자 정보"로 필요시 회원가입
        Users kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);

        // 4. 강제 로그인 처리
        forceLogin(kakaoUser);
    }
  1. 사용자 인증: 사용자가 클라이언트 애플리케이션에서 자신의 계정으로 로그인 
  2. 권한 부여 요청: 클라이언트는 인증 서버에 사용자의 자원에 대한 접근 권한을 요청 
  3. 사용자 동의: 사용자는 클라이언트에게 특정 자원에 대한 접근 권한을 부여할지 여부를 결정 
  4. 권한 부여 코드 발급: 사용자가 동의하면, 인증 서버는 클라이언트에게 권한 부여 코드를 발급 
 private String getAccessToken(String code) throws JsonProcessingException {
        // HTTP Header 생성
        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", "클라이언트ID");
        body.add("redirect_uri", "http://lmshi.shop:8083/user/kakao/callback");
        body.add("code", code);

        // HTTP 요청 보내기
        HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
                new HttpEntity<>(body, headers);
        RestTemplate rt = new RestTemplate();
        ResponseEntity<String> response = rt.exchange(
                "https://kauth.kakao.com/oauth/token",
                HttpMethod.POST,
                kakaoTokenRequest,
                String.class
        );

        // HTTP 응답 (JSON) -> 액세스 토큰 파싱
        String responseBody = response.getBody();
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(responseBody);
        return jsonNode.get("access_token").asText();
    }
  1. 액세스 토큰 요청: 클라이언트는 권한 부여 코드를 이용해 인증 서버에서 액세스 토큰을 요청 
  2. 액세스 토큰 발급: 인증 서버는 클라이언트에게 액세스 토큰을 발급 
  3. 자원 서버 접근: 클라이언트는 액세스 토큰을 사용해 자원 서버에서 자원을 요청
private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
        // HTTP Header 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken);
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        // HTTP 요청 보내기
        HttpEntity<MultiValueMap<String, String>> kakaoUserInfoRequest = new HttpEntity<>(headers);
        RestTemplate rt = new RestTemplate();
        ResponseEntity<String> response = rt.exchange(
                "https://kapi.kakao.com/v2/user/me",
                HttpMethod.POST,
                kakaoUserInfoRequest,
                String.class
        );

        String responseBody = response.getBody();
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(responseBody);
        Long id = jsonNode.get("id").asLong();
        String nickname = jsonNode.get("properties")
                .get("nickname").asText();
        String email = jsonNode.get("kakao_account")
                .get("email").asText();
         
        return new KakaoUserInfoDto(id, nickname, email);
    }

 

자원 제공: 자원 서버는 액세스 토큰을 검증한 후 클라이언트에게 요청된 자원을 제공하고 서버에서 중복체크 후 회원가입후 로그인 처리한다.

private Users registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
        // DB 에 중복된 Kakao Id 가 있는지 확인
        Long kakaoId = kakaoUserInfo.getId();
        Users kakaoUser = userRepository.findByKakaoId(kakaoId)
                .orElse(null);
        if (kakaoUser == null) {
            String kakaoEmail = kakaoUserInfo.getEmail();
            Users sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);

            if(sameEmailUser != null){
                kakaoUser = sameEmailUser;
                // 기존 회원 정보에 카카오 ID 추가
                kakaoUser.setKakaoId(kakaoId);
            }else{
                //신규회원 가입
                // user name 카카오 닉네임
                String nickname = kakaoUserInfo.getNickname();

                // password : random UUID
                String password = UUID.randomUUID().toString();
                String encodedPassword = passwordEncoder.encode(password);

                // email : kakao email
                String email = kakaoUserInfo.getEmail();
                // role : 일반 사용자
                UserRoleEnum role = UserRoleEnum.USER;

                kakaoUser = new Users(nickname, encodedPassword, email, role, kakaoId);

            }

            userRepository.save(kakaoUser);

        }
        return kakaoUser;
    }

    private void forceLogin(Users kakaoUser) {
        UserDetails userDetails = new UserDetailsImpl(kakaoUser);
        Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

 

OAuth는 특히 소셜 로그인 기능을 구현할 때 많이 사용되며, Google, Facebook, Twitter, Naver, Kakao 등 다양한 서비스에서 표준적으로 채택하고 있으며, 이를 통해 사용자는 비밀번호를 공유하지 않고도 다른 애플리케이션과 안전하게 데이터를 공유할 수 있다.

 

위의 과정을 프론트 코드로 구현해보면

카카오에서 발급받은 클라이언트 아이디를 발급하고, 해당 url을 등록해야 한다.

<button id="login-kakao-btn" onclick="location.href='https://kauth.kakao.com/oauth/authorize?client_id=클라이언트아이디&redirect_uri=http://http://lmshi.shop:8083/user/kakao/callback&response_type=code'">
  카카오로 로그인하기
</button>

 

SSR

SSR (Server-Side Rendering)는 웹 애플리케이션의 HTML을 서버 측에서 렌더링하여 클라이언트(브라우저)로 전달하는 기술입니다. 이는 클라이언트 측에서 자바스크립트가 실행되기 전에 완전한 HTML을 제공하여 초기 로드 시간을 단축하고 SEO(검색 엔진 최적화)를 향상시키는 데 유용합니다.

SSR의 주요 특징 및 이점

  1. 빠른 초기 로드: 서버에서 미리 렌더링된 HTML을 클라이언트에 제공하므로, 클라이언트가 페이지를 로드할 때 초기 콘텐츠를 빠르게 볼 수 있습니다. 이는 사용자 경험을 크게 향상시킵니다.
  2. SEO 향상: 검색 엔진 크롤러는 자바스크립트를 실행하지 않거나 제한적으로 실행하기 때문에, 서버에서 미리 렌더링된 HTML은 크롤러가 콘텐츠를 더 쉽게 인덱싱할 수 있게 합니다. 이는 검색 엔진 최적화를 개선하는 데 도움을 줍니다.
  3. 더 나은 성능: 클라이언트가 서버에서 이미 렌더링된 HTML을 수신하면, 브라우저는 이를 바로 표시할 수 있습니다. 이는 특히 네트워크 속도가 느리거나 클라이언트 장치의 성능이 낮을 때 유용합니다.

SSR의 단점

  1. 서버 부하 증가: 모든 요청마다 서버가 HTML을 렌더링해야 하기 때문에 서버의 부하가 증가할 수 있습니다. 이는 고성능 서버가 필요하게 만들 수 있습니다.
  2. 복잡성 증가: 클라이언트 측에서만 렌더링하는 것보다 SSR을 구현하고 유지 관리하는 것이 더 복잡할 수 있습니다. 서버와 클라이언트 양쪽에서 코드가 실행되어야 하기 때문입니다.

SSR의 사용 사례

  1. Next.js: React 애플리케이션을 위한 프레임워크로, 서버 사이드 렌더링을 쉽게 구현할 수 있게 해줍니다.
  2. Nuxt.js: Vue.js 애플리케이션을 위한 프레임워크로, 역시 서버 사이드 렌더링을 지원합니다.
  3. Angular Universal: Angular 애플리케이션에서 SSR을 가능하게 해줍니다

 

SSR vs CSR (Client-Side Rendering)

  • CSR (Client-Side Rendering): 클라이언트 측에서 자바스크립트를 사용하여 HTML을 렌더링하는 방식입니다. 일반적으로 초기 로드 속도는 느리지만, 페이지 전환이 빠르고 부드럽습니다.
  • SSR: 서버 측에서 HTML을 미리 렌더링하여 클라이언트에 제공하는 방식입니다. 초기 로드 속도가 빠르고, SEO에 유리합니다.

SSR은 초기 로딩 속도를 개선하고 SEO를 강화하는 데 유리하지만, 서버에 더 많은 부하를 주고 구현 복잡성이 증가할 수 있습니다. 반면, CSR은 동적 인터랙션이 많은 애플리케이션에서 유리합니다. 두 가지 접근 방식은 각각의 장단점이 있으므로, 애플리케이션의 요구 사항에 따라 적절한 방식을 선택하는 것이 중요합니다.

// pages/index.js
import React from 'react';

const Home = ({ data }) => {
  return (
    <div>
      <h1>Server-Side Rendering Example</h1>
      <p>{data.message}</p>
    </div>
  );
};

// getServerSideProps 함수는 요청 시마다 호출되어 데이터를 미리 가져옵니다.
export async function getServerSideProps() {
  // 예제 데이터를 가져오는 부분 (API 호출 등)
  const data = { message: 'This is rendered on the server side' };

  // props를 반환하여 페이지 컴포넌트에 전달
  return {
    props: { data },
  };
}

export default Home;

 

LIST