프로젝트-Biz Chemy- 로그인

Jonguk Kim·2022년 1월 1일
0

Project

목록 보기
2/3
post-thumbnail

1. 와이어 프레임

2. DB

3. API

4. 구현

1) 카카오 로그인 전체 흐름

  1. 프론트에서 받은 인가 코드를 카카오에 전달하여, 토큰을 발급받는다.

  2. 토큰으로 API 호출하면, 카카오에서 토큰 유효성 확인 후, 카카오 정보를 응답으로 전달한다.

  3. 토큰에 담긴 유저 정보를 활용해 프로젝트 전용 토큰으로 새롭게 발급 후 프론트 에게 돌려준다.

2) 카카오 로그인 프론트엔드

"https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code"
  • REST_API_KEY: 카카오 디벨로퍼 > 내 애플리케이션 > 앱 키

  • REDIRECT_URI: 카카오 디벨로퍼 > 내 애플리케이션 > 플랫폼 > Web > 등록하러 가기

    • Redirect_URI는 반드시 프론트에서 접근할 수 있는 Host로 지정 (localhost:3000)
  • 플랫폼 추가: 카카오 디벨로퍼 > 내 애플리케이션 > 플랫폼

    • Web에서 사용할 것이기 때문에 Web 플랫폼에 사이트 도메인을 추가
  • 인가 코드를 백으로 전달 (GET 서버주소/user/kakao/callback?code)

3) 카카오 로그인 백엔드

Controller

  • 프론트에서 GET 요청한 API 호출 ( @GetMapping("/user/kakao/callback") )
  • parameter에 있는 code를 이용하여 서비스 호출
  • 서비스 처리한 사용자 정보 반환
    // 카카오 로그인
    @GetMapping("/user/kakao/callback")
    public UserResponseDto kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
        // 카카오 서버로부터 받은 인가 코드, JWT 토큰
        return userService.kakaoLogin(code, response);
    }

Service

  • 인가 코드로 액세스 토큰 요청
  • 액세스 토큰으로 카카오 사용자 정보 가져오기
  • 카카오 사용자 정보로 회원 가입
  • 강제 로그인 처리
    public UserResponseDto kakaoLogin(String code, HttpServletResponse response) throws JsonProcessingException {
        // 1. "인가 코드"로 "액세스 토큰" 요청
        String accessToken = getAccessToken(code);

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

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

        // 4. 강제 로그인 처리
        return forceLogin(kakaoUser, response);
    }

- 인가 코드로 액세스 토큰 요청

  • HTTP Header 생성
  • HTTP Body 생성
  • HTTP 요청 보내기
  • HTTP 응답 (JSON -> Java 객체)
  • 액세스 토큰 파싱
  • 액세스 토큰 반환
    // 1. "인가 코드"로 "액세스 토큰" 요청
    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", "{REST_API_KEY}");                  // REST API 키
        body.add("redirect_uri", "http://localhost:3000/user/kakao/callback");      // Redirect URI
        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) -> 액세스 토큰 파싱
        // JSON -> Java Object
        String responseBody = response.getBody();
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(responseBody);
        return jsonNode.get("access_token").asText();
    }

- 액세스 토큰으로 카카오 사용자 정보 가져오기

  • HTTP Header 생성
    • Bearer + 액세스 토큰
  • HTTP 요청 보내기
  • HTTP 응답 (JSON -> Java 객체)
  • 카카오 사용자 정보 파싱
    • 카카오 ID
    • 카카오 이메일
    • 카카오 닉네임
    • 카카오 프로필 이미지
      • 이미지 NULL 값 초기화
      • 이미지 동의, 이미지 등록 => 이미지 URL 저장
      • 아니면 NULL 값 저장
    • 카카오 성별
      • 성별 NULL 값 초기화
      • 성별 동의, 성별 등록 => 성별 저장
      • 아니면 NULL 값 저장
    • 카카오 연령대
      • 연령대 NULL 값 초기화
      • 연령대 동의, 연령대 등록 => 연령대 저장
      • 아니면 NULL 값 저장
  • 카카오 사용자 정보 반환
    // 2. "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
    private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
        // HTTP Header 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken);      // JWT 토큰
        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
        );

        // HTTP 응답 (JSON) -> 액세스 토큰 파싱
        // JSON -> Java Object
        // 이 부분에서 카톡 프로필 정보 가져옴
        JSONObject body = new JSONObject(response.getBody());
        System.out.println(body);
        // ID (카카오 기본키)
        Long id = body.getLong("id");
        // 아이디 (이메일)
        String username = body.getJSONObject("kakao_account").getString("email");
        // 닉네임
        String nickname = body.getJSONObject("properties").getString("nickname");

        // profile_image_needs_agreement: true (이미지 동의 안함), false (이미지 동의)
        // is_default_image: true (기본 이미지), false (이미지 등록됨)
        // 프로필 이미지
        String profileImage = "";
        // 이미지 동의 및 등록 되었으면
        if (!body.getJSONObject("kakao_account").getBoolean("profile_image_needs_agreement") &&
                !body.getJSONObject("kakao_account").getJSONObject("profile").getBoolean("is_default_image")) {
            profileImage = body.getJSONObject("kakao_account").getJSONObject("profile").getString("profile_image_url");
        }

        // has_gender: false (성별 선택 안함), true (성별 선택)
        // gender_needs_agreement: true (성별 동의 안함), false (성별 동의)
        // 성별 (male, female, unchecked)
        String gender = "";
        // 성별 선택 및 성별 동의 되었으면
        if (body.getJSONObject("kakao_account").getBoolean("has_gender") &&
                !body.getJSONObject("kakao_account").getBoolean("gender_needs_agreement")) {
            gender = body.getJSONObject("kakao_account").getString("gender");
        }

        // has_age_range: false (연령대 선택 안함), true (연령대 선택)
        // age_range_needs_agreement: true (연령대 동의 안함), false (연령대 동의)
        // 연령대
        String ageRange = "";
        // 이미지 동의 및 등록 되었으면
        if (body.getJSONObject("kakao_account").getBoolean("has_age_range") &&
                !body.getJSONObject("kakao_account").getBoolean("age_range_needs_agreement")) {
            ageRange = body.getJSONObject("kakao_account").getString("age_range");
        }

        return KakaoUserInfoDto.builder()
                .id(id)
                .username(username)
                .nickname(nickname)
                .profileImage(profileImage)
                .gender(gender)
                .ageRange(ageRange)
                .build();
    }

- 카카오 사용자 정보로 필요 시 회원 가입

  • DB에 카카오 ID 존재하는지 확인
    • 카카오 아이디 없으면, DB 저장, 가입완료 상태: false
    • 카카오 아이디 있으면, 추가정보 입력 완료되었는지 확인
      • 추가정보 입력 안했으면, 가입완료 상태: false
      • 추가정보 입력 완료 했으면, 가입완료 상태: true
  • 카카오 사용자 반환
    // 3. "카카오 사용자 정보"로 필요시 회원가입
    private User registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
        // DB 에 중복된 Kakao Id 가 있는지 확인
        Long kakaoId = kakaoUserInfo.getId();
        User kakaoUser = userRepository.findByKakaoId(kakaoId)
                .orElse(null);

        // nullable = false
        String username = kakaoUserInfo.getUsername();                  // 카카오 아이디 (이메일)
        String password = UUID.randomUUID().toString();                 // 카카오 비밀번호 암호화
        String encodedPassword = passwordEncoder.encode(password);
        String nickname = kakaoUserInfo.getNickname();                  // 카카오 닉네임

        // nullable = true
        String profileImage = kakaoUserInfo.getProfileImage();          // 카카오 프로필 이미지 (이미지 객체에 저장)
        String gender = kakaoUserInfo.getGender();                      // 카카오 성별
        String ageRange = kakaoUserInfo.getAgeRange().substring(0, 2).concat("대");  // 카카오 연령대

        // 가입 여부
        if (kakaoUser == null) {
            // 사용자 저장
            kakaoUser = User.builder()
                        .kakaoId(kakaoId)
                        .username(username)
                        .password(encodedPassword)
                        .nickname(nickname)
                        .profileImage(profileImage)
                        .gender(gender)
                        .ageRange(ageRange)
                        .build();
            userRepository.save(kakaoUser);
            signStatus = false;                 // 처음 가입하면 false => 추가 정보 입력 페이지로 이동
        } else if (!kakaoUser.isStatus()) {     // 카카오 가입은 되었으나, 추가정보 입력 안했으면 false
            signStatus = false;
        } else {
            signStatus = true;                  // 이미 가입했으면 true => 메인 페이지로 이동
        }

        return kakaoUser;
    }

- 강제 로그인 처리

  • 강제 로그인 처리
  • JWT 토큰 생성 및 반환
    • Bearer + 생성된 토큰
  • 사용자 정보 반환
    • 가입완료 상태: false
      • 카카오 정보 반환
      • NULL 값 반환 (소개글, 위치, 경도, 위도, MBTI, 관심사)
    • 가입완료 상태: true
      • 카카오 정보 반환
      • 추가 정보에서 입력한 값 반환 (소개글, 위치, 경도, 위도, MBTI, 관심사)
    // 4. 강제 로그인 처리
    private UserResponseDto forceLogin(User kakaoUser, HttpServletResponse response) {
        UserDetailsImpl userDetails = new UserDetailsImpl(kakaoUser);
        Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // JWT 토큰 생성
        String token = JwtTokenUtils.generateJwtToken(userDetails);

        // Header 에 JWT 토큰 담아서 응답
        response.addHeader("Authorization", "Bearer " + token);

        // 추가정보입력 또는 가입한 사용자 (Location, Mbti NULL 값이 없어야함)
        String intro = null;
        String location = null;
        String longitude = null;
        String latitude = null;
        String mbti = null;
        List<InterestListDto> interestListDtos = new ArrayList<>();
        if (signStatus) {
            intro = userDetails.getUser().getIntro();
            location = userDetails.getUser().getLocation().getLocation();
            longitude =userDetails.getUser().getLocation().getLongitude();
            latitude = userDetails.getUser().getLocation().getLatitude();
            mbti = userDetails.getUser().getMbti().getMbti();
            for (int i = 0; i < userDetails.getUser().getUserInterestList().size(); i++) {
                interestListDtos.add(InterestListDto.builder()
                                                .interest(userDetails.getUser().getUserInterestList().get(i).getInterest().getInterest())
                                                .build());
            }
        }

        // Body 에 반환
        return UserResponseDto.builder()
                .nickname(userDetails.getUser().getNickname())
                .gender(userDetails.getUser().getGender())
                .ageRange(userDetails.getUser().getAgeRange())
                .profileImage(userDetails.getUser().getProfileImage())
                .intro(intro)
                .location(location)
                .longitude(longitude)
                .latitude(latitude)
                .mbti(mbti)
                .interestList(interestListDtos)
                .signStatus(signStatus)
                .build();
    }

4) 추가정보 입력 프론트엔드

  • 가입완료 상태: false
    • 추가정보 입력 페이지로 이동
    • 추가정보 미입력 시 다음 페이지로 이동 불가 (카카오에서 받은 정보 수정 가능)
  • 가입완료 상태: true
    • 메인 페이지로 이동

5) 추가정보 입력 백엔드

Controller

  • 프론트에서 PUT 요청한 API 호출
  • JWT 토큰 사용자 정보 추출, Body에 담긴 정보 추출
  • 서비스 처리한 사용자 정보 반환
    // 추가정보 입력
    @PutMapping("/api/profile")
    public UserResponseDto updateProfile(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody  UserRequestDto userRequestDto) {
        // 추가 정보 입력
        return userService.updateProfile(userDetails.getUser(), userRequestDto);
    }

Service

  • 사용자 아이디(이메일) 조회
  • 위치 조회
  • MBTI 조회
  • 사용자 DB 업데이트 및 저장
  • 기존 관심사 삭제
    • 모든 데이터를 삭제하고 바로 DB에 무언가 insert를 할거면 deleteAllInBatch()를 사용해야 함
  • 관심사 조회
  • 사용자_관심사 DB 저장
  • 사용자 정보 반환
    • 가입완료 상태: true
      • 카카오 정보 반환
      • 추가 정보에서 입력한 값 반환 (소개글, 위치, 경도, 위도, MBTI, 관심사)
    // 추가 정보 입력
    @Transactional
    public UserResponseDto updateProfile(User user, UserRequestDto userRequestDto) {
        // 사용자 조회
        User findUser = userRepository.findByUsername(user.getUsername()).orElseThrow(
                () -> new IllegalArgumentException("해당 사용자가 존재하지 않습니다.")
        );

        // 닉네임 필수값이므로, null 값이면 카카오 닉네임으로 설정
        if (userRequestDto.getNickname() == null) {
            userRequestDto.setNickname(user.getNickname());
        }

        // 추가정보 설정하여 업데이트 (닉네임, 프로필, 소개글, 위치, 관심사, mbti)
        // 위치 조회
        Location location = locationRepository.findByLocation(userRequestDto.getLocation()).orElseThrow(
                () -> new IllegalArgumentException("해당 위치가 존재하지 않습니다.")
        );

        // mbti 조회
        Mbti mbti = mbtiRepository.findByMbti(userRequestDto.getMbti()).orElseThrow(
                () -> new IllegalArgumentException("해당 MBTI 가 존재하지 않습니다.")
        );

        findUser.update(userRequestDto, location, mbti, true);

        // DB 저장
        userRepository.save(findUser);

        // 기존 관심사 삭제
        List<UserInterest> deleteUserInterest = userInterestRepository.findAllByUser(findUser);
        userInterestRepository.deleteAllInBatch(deleteUserInterest);

        // 관심사 리스트 조회
        List<UserInterest> userInterest = new ArrayList<>();
        for (int i = 0; i < userRequestDto.getInterestList().size(); i++) {
            Interest interest = interestRepository.findByInterest(userRequestDto.getInterestList().get(i).getInterest()).orElseThrow(
                    () -> new IllegalArgumentException("해당 관심사가 존재하지 않습니다.")
            );

            userInterest.add(UserInterest.builder()
                                        .user(findUser)
                                        .interest(interest)
                                        .build());
        }

        // DB 저장
        userInterestRepository.saveAll(userInterest);

        List<InterestListDto> interestListDtos = new ArrayList<>();
        for (int i = 0; i < findUser.getUserInterestList().size(); i++) {
            interestListDtos.add(InterestListDto.builder()
                    .interest(findUser.getUserInterestList().get(i).getInterest().getInterest())
                    .build());
        }

        // Body 에 반환
        return UserResponseDto.builder()
                .nickname(findUser.getNickname())
                .gender(findUser.getGender())
                .ageRange(findUser.getAgeRange())
                .profileImage(findUser.getProfileImage())
                .intro(findUser.getIntro())
                .location(findUser.getLocation().getLocation())
                .longitude(findUser.getLocation().getLongitude())
                .latitude(findUser.getLocation().getLatitude())
                .mbti(findUser.getMbti().getMbti())
                .interestList(interestListDtos)
                .signStatus(true)
                .build();
    }
profile
Just Do It

0개의 댓글