일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- oracle
- 집합_SET
- 정수형타입
- 사용자예외클래스생성
- 대덕인재개발원
- 인터페이스
- GRANT VIEW
- Java
- NestedFor
- 예외처리
- 추상메서드
- 자바
- EnhancedFor
- cursor문
- 컬렉션프레임워크
- abstract
- exception
- 참조형변수
- 어윈 사용법
- 자동차수리시스템
- 오라클
- 컬렉션 타입
- 생성자오버로드
- 한국건설관리시스템
- 제네릭
- 메소드오버로딩
- 다형성
- 환경설정
- 객체 비교
- 예외미루기
- Today
- Total
거니의 velog
(41) 리액트 소셜 로그인 4 본문
5. 자동 회원 추가 및 회원정보의 반환
* API 서버가 Access Token을 처리해서 사용자의 이메일 정보를 추출하는 것을 확인했다면 이를 이용해서 데이터베이스에 존재하는 회원인지 처음으로 접근한 회원인지에 따라서 데이터베이스에서 조회 혹은 추가(회원가입)를 해 주어야 한다.
(1) MemberService 회원 처리
* 회원정보는 MemberDTO 타입을 통해서 처리되어야 하므로 Member 엔티티 객체를 MemberDTO 객체로 변환하는 entityToDTO() 를 추가한다.
package com.unlimited.mallapi.service;
import java.util.stream.Collectors;
import org.springframework.transaction.annotation.Transactional;
import com.unlimited.mallapi.domain.Member;
import com.unlimited.mallapi.dto.MemberDTO;
// 트랜잭션 처리 어노테이션, rollbackFor 속성을 사용하여 모든 예외에 대해 롤백 처리
@Transactional(rollbackFor = Exception.class)
public interface MemberService {
// Kakao의 AccessToken을 이용해 회원 정보를 가져오는 메서드
MemberDTO getKakaoMember(String accessToken);
// Entity(Member)를 DTO(MemberDTO)로 변환하는 디폴트 메서드
default MemberDTO entityToDTO(Member member) {
// Member 엔티티에서 MemberDTO로 필요한 정보를 복사하여 변환
MemberDTO dto = new MemberDTO(
member.getEmail(),
member.getPw(),
member.getNickname(),
member.isSocial(),
// Member 엔티티에 속한 MemberRole 열거형을 문자열 리스트로 변환
member.getMemberRoleList().stream().map(memberRole -> memberRole.name()).collect(Collectors.toList()));
return dto; // 변환된 DTO 반환
}
}
* MemberServiceImpl에서는 MemberRepository와 PasswordEndoder를 주입해서 이메일로 회원을 조회하거나 추가한다(org.springframework.security.crypto.password.PasswordEncoder 추가).
@Service
@RequiredArgsConstructor
@Log4j2
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
* 만일 해당 이메일을 가진 회원이 없다면 새로운 회원을 추가할 때 패스워드를 임의로 생성한다. 이렇게 생성된 패스워드는 PasswordEncoder를 통해서 알아볼 수 없게 되므로 관리자와 사용자 모두 패스워드를 알 수 없게 된다(단방향 암호화).
단방향 암호화(One-Way Hashing)은 데이터를 암호화할 때, 암호화된 결과를 다시 복호화할 수 없는 방식으로 수행되는 암호화 기법입니다.
즉, 원본 데이터를 암호화하여 해시값(일련의 고정된 길이의 문자열)을 생성하고, 이 해시값을 저장합니다.
그런 다음 나중에 입력된 데이터도 동일한 알고리즘을 사용하여 해시값을 생성하고, 저장된 해시값과 비교함으로써 데이터 일치 여부를 확인합니다.
다음은 단방향 암호화의 특징과 사용되는 상황에 대한 설명입니다:
1. 복호화 불가능: 단방향 암호화는 암호화된 값을 다시 원본 데이터로 되돌릴 수 없습니다. 이는 주로 보안 강화를 위해 사용되며, 원본 데이터의 복원이 불가능하도록 보장합니다.
2. 고정된 출력 길이: 어떤 크기의 입력이든 간에, 해시 함수의 출력 길이는 고정되어 있습니다. 이는 일반적으로 알고리즘에 따라 정해진 길이의 해시값이 생성되는 특징을 나타냅니다.
3. 충돌 가능성: 서로 다른 입력이 같은 해시값을 생성하는 경우를 충돌이라고 합니다. 강력한 단방향 해시 함수는 충돌이 발생할 확률을 매우 낮게 유지하려고 노력합니다. 충돌이 발생하면, 보안성이 약화될 수 있습니다.
4. 주로 비밀번호 저장에 사용: 단방향 암호화는 주로 사용자의 비밀번호를 저장할 때 적용됩니다. 사용자가 입력한 비밀번호를 저장하는 대신, 해당 비밀번호의 해시값을 저장함으로써 보안을 강화할 수 있습니다.
5. 대표적인 알고리즘: 대표적인 단방향 암호화 알고리즘으로는 MD5, SHA-1, SHA-256, SHA-3 등이 있습니다. 그러나 MD5와 SHA-1은 현재 보안상의 이유로 권장되지 않습니다. 보다 안전한 해시 함수인 SHA-256, SHA-3 등을 사용하는 것이 좋습니다.
예시로 사용자 비밀번호를 저장하는 상황에서, 원본 비밀번호를 저장하지 않고 그에 대한 해시값을 저장함으로써, 시스템이 사용자의 비밀번호를 알지 못하더라도 로그인 등의 인증을 수행할 수 있도록 합니다.
* 패스워드를 모르기 때문에 일반 로그인은 불가능하게 된다. 대신 카카오 로그인 후에는 회원정보를 수정할 수 있도록 구성해서 사용자가 원하는 패스워드를 지정하여 변경할 수 있도록 유도해야 한다.
/**
* 임시 비밀번호를 생성하는 메서드입니다.
*
* @return 생성된 임시 비밀번호
*/
private String makeTempPassword() {
// StringBuffer 객체를 생성하여 임시 비밀번호를 저장할 준비를 합니다.
StringBuffer buffer = new StringBuffer();
// 10자리의 임시 비밀번호를 생성하기 위한 반복문입니다.
for (int i = 0; i < 10; i++) {
// Math.random()을 이용하여 0 이상 55 미만의 난수를 생성하고, 65를 더하여 ASCII 코드값을 얻습니다.
// 이를 char로 캐스팅하여 알파벳 대문자(A-Z) 또는 기호를 생성합니다.
buffer.append((char) ((int) (Math.random() * 55) + 65));
}
// 생성된 임시 비밀번호를 문자열로 변환하여 반환합니다.
return buffer.toString();
}
* 이메일이 존재하지 않는다면 새로운 com.unlimited.mallapi.domain.Member 객체를 생성해야 하는데 이를 위한 makeSocialMember()를 추가한다(import 필요).
/**
* 소셜 회원을 생성하는 메서드입니다.
*
* @param email 생성할 소셜 회원의 이메일
* @return 생성된 소셜 회원 객체
*/
private Member makeSocialMember(String email) {
// 임시 비밀번호를 생성하는 makeTempPassword 메서드를 호출합니다.
String tempPassword = makeTempPassword();
// 생성된 임시 비밀번호를 로그에 기록합니다.
log.info("tempPassword: " + tempPassword);
// 소셜 회원의 기본 닉네임을 설정합니다.
String nickname = "소셜회원";
// Member 객체를 Builder 패턴을 사용하여 생성합니다.
Member member = Member.builder()
.email(email) // 이메일 설정
.pw(passwordEncoder.encode(tempPassword)) // 임시 비밀번호를 암호화하여 저장
.nickname(nickname) // 닉네임 설정
.social(true) // 소셜 회원 여부 설정
.build();
// 회원 역할(Role)을 추가합니다. 여기서는 일반 사용자 역할을 추가했습니다.
member.addRole(MemberRole.USER);
// 생성된 소셜 회원 객체를 반환합니다.
return member;
}
* 추가된 메서드를 이용해서 getKakaoMember()를 아래와 같이 구성한다.
/**
* Kakao Access Token을 이용하여 Kakao 사용자 정보를 가져오는 메서드입니다.
*
* @param accessToken Kakao API에 접근하기 위한 Access Token
* @return Kakao 사용자 정보에 대한 DTO(데이터 전송 객체) 객체
*/
@Override
public MemberDTO getKakaoMember(String accessToken) {
// Kakao API를 통해 사용자 이메일 정보를 얻어옵니다.
String email = getEmailFromKakaoAccessToken(accessToken);
// 얻어온 Kakao 사용자 이메일을 로그에 기록합니다.
log.info("Kakao 사용자 이메일: " + email);
// 사용자 이메일로 회원 정보를 데이터베이스에서 조회합니다.
Optional<Member> result = memberRepository.findById(email);
// 조회된 회원 정보가 존재하는 경우, 해당 회원의 정보를 DTO로 변환하여 반환합니다.
if (result.isPresent()) {
MemberDTO memberDTO = entityToDTO(result.get());
return memberDTO;
}
// 조회된 회원 정보가 없는 경우, 새로운 소셜 회원을 생성하고 데이터베이스에 저장합니다.
Member socialMember = makeSocialMember(email);
memberRepository.save(socialMember);
// 생성된 소셜 회원의 정보를 DTO로 변환하여 반환합니다.
MemberDTO memberDTO = entityToDTO(socialMember);
return memberDTO;
}
(2) 컨트롤러의 결과 처리
* MemberService 쪽에서 카카오 로그인 사용자에 대한 처리 결과로 반환되는 MemberDTO는 SocialController에서 일반 로그인과 동일하게 JSON 데이터가 될 수 있도록 처리한다. 이 때, JWTUtil을 이용해서 API 서버 접근 시에 사용할 Access Token과 Refresh Token을 발행해서 추가한다.
package com.unlimited.mallapi.controller;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.unlimited.mallapi.dto.MemberDTO;
import com.unlimited.mallapi.service.MemberService;
import com.unlimited.mallapi.util.JWTUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@RestController
@Log4j2
@RequiredArgsConstructor
public class SocialController {
private final MemberService memberService;
/**
* Kakao Access Token을 이용하여 Kakao 사용자 정보를 조회하는 API 엔드포인트입니다.
*
* @param accessToken Kakao API에 접근하기 위한 Access Token
* @return 사용자 정보를 담은 맵 또는 메시지 (현재는 더미 데이터 반환)
*/
@GetMapping("/api/member/kakao")
public Map<String, Object> getMemberFromKakao(@RequestParam("accessToken") String accessToken) {
// 엔드포인트 호출을 로그에 기록합니다.
log.info("Kakao 사용자 정보 조회를 위한 엔드포인트 호출");
// 전달받은 Access Token을 로그에 기록합니다.
log.info("전달받은 Access Token: " + accessToken);
// Kakao 사용자 정보를 조회하고, 해당 정보를 MemberDTO로 받아옵니다.
MemberDTO memberDTO = memberService.getKakaoMember(accessToken);
// MemberDTO에서 사용자 정보를 담은 맵(claims)을 추출합니다.
Map<String, Object> claims = memberDTO.getClaims();
// JWTUtil을 사용하여 Access Token 및 Refresh Token을 생성합니다.
String jwtAccessToken = JWTUtil.generateToken(claims, 10); // 10분 동안 유효한 Access Token
String jwtRefreshToken = JWTUtil.generateToken(claims, 60 * 24); // 24시간 동안 유효한 Refresh Token
// 생성된 토큰들을 맵에 추가합니다.
claims.put("accessToken", jwtAccessToken);
claims.put("refreshToken", jwtRefreshToken);
// 최종적으로 사용자 정보 및 토큰들을 반환합니다.
return claims;
}
}
* 브라우저에서 결과를 확인하면 로그인 결과는 동일한 화면이지만, API 서버와 리액트 양쪽 모두 많은 변화가 있게 된다. 우선 카카오 로그인을 브라우저에서 실행한다.
* API 서버의 로그를 확인하면 아래와 같이 임의의 패스워드가 생성된 것을 확인할 수 있고, 데이터베이스에 해당 이메일이 없다면 insert문이 처리되는 것을 확인할 수 있다.
* 브라우저의 KakaoRedirectPage에서는 API 서버에서 반환한 사용자 정보를 콘솔창에서 확인할 수 있다.
(3) 리액트의 로그인 처리
* API 서버에서 보내준 데이터는 기존의 로그인 데이터의 구성과 동일하기 때문에 이를 이용해서 로그인 처리를 한다. 로그인 처리는 loginSlice의 login()을 사용할 수 있다.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { loginPost } from "../api/memberApi";
import { setCookie, getCookie, removeCookie } from "../util/cookieUtil";
(...)
const loginSlice = createSlice({
name: "LoginSlice",
initialState: loadMemberCookie() || initState, // 쿠키가 없다면 초깃값사용
reducers: {
login: (state, action) => {
console.log("login.....");
// 소셜로그인 회원이 사용
const payload = action.payload;
setCookie("member", JSON.stringify(payload), 1); //1일
return payload;
},
logout: (state, action) => {
console.log("logout....");
removeCookie("member");
return { ...initState };
},
},
extraReducers: (builder) => {
(...)
},
});
export const { login, logout } = loginSlice.actions;
export default loginSlice.reducer;
* KakaoRedirectPage에서는 API 서버에서 전송한 결과를 dispatch()를 이용해서 login()을 호출한다.
import React, { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { getAccessToken, getMemberWithAccessToken } from "../../api/kakaoApi";
import { useDispatch } from "react-redux";
import { login } from "../../slices/loginSlice";
const KakaoRedirectPage = () => {
const [searchParams] = useSearchParams();
const authCode = searchParams.get("code");
const dispatch = useDispatch();
useEffect(() => {
getAccessToken(authCode).then((accessToken) => {
console.log(accessToken);
getMemberWithAccessToken(accessToken).then((memberInfo) => {
console.log("-------------------");
console.log(memberInfo);
dispatch(login(memberInfo));
});
});
}, [authCode, dispatch]);
return (
<div>
<div>Kakao Login Redirect</div>
<div>{authCode}</div>
</div>
);
};
export default KakaoRedirectPage;
* 화면에서 카카오 로그인을 실행하면 API 서버에서 전송된 결과로 쿠키가 생성되는 것을 확인할 수 있다.
(4) 화면 이동 처리
* 로그인에 대한 쿠키 처리가 완료되었다면 마지막으로 화면을 이동시켜 주어야 한다. 이 때 카카오 로그인 사용자라면 회원정보를 수정하도록 이동해서 임시로 발행된 패스워드가 아니라 자신이 직접 패스워드나 닉네임을 지정할 수 있어야 한다.
* 만일 소셜 로그인한 사용자가 일반 회원이었다면 기존과 동일하게 / 경로로 이동할 수 있도록 처리한다. 이동에 대한 처리는 기존에 만들어둔 useCustomLogin을 재사용한다.
import React, { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { getAccessToken, getMemberWithAccessToken } from "../../api/kakaoApi";
import { useDispatch } from "react-redux";
import { login } from "../../slices/loginSlice";
import useCustomLogin from "../../hooks/useCustomLogin";
const KakaoRedirectPage = () => {
const [searchParams] = useSearchParams();
const authCode = searchParams.get("code");
const dispatch = useDispatch();
const { moveToPath } = useCustomLogin();
useEffect(() => {
getAccessToken(authCode).then((accessToken) => {
console.log(accessToken);
getMemberWithAccessToken(accessToken).then((memberInfo) => {
console.log("-------------------");
console.log(memberInfo);
dispatch(login(memberInfo));
// 소셜 회원이 아니라면
if (memberInfo && !memberInfo.social) {
moveToPath("/");
} else {
moveToPath("/member/modify");
}
});
});
}, [authCode, dispatch]); // moveToPath 또한 의존성 배열에서 무한 루프이므로 제외
return (
<div>
<div>Kakao Login Redirect</div>
<div>{authCode}</div>
</div>
);
};
export default KakaoRedirectPage;
* 변경된 코드는 현재 데이터베이스에 없는 사용자가 카카오 로그인을 한 경우 /member/modify 로 이동하게 되는데 아직은 해당 페이지가 없기 때문에 에러 화면(404 Not Found)만 보게 된다.
'SpringBoot_React 풀스택 프로젝트' 카테고리의 다른 글
(43) 장바구니 API 만들기 1 (0) | 2024.03.08 |
---|---|
(42) 리액트 소셜 로그인 5 (0) | 2024.03.08 |
(40) 리액트 소셜 로그인 3 (0) | 2024.03.07 |
(39) 리액트 소셜 로그인 2 (0) | 2024.03.07 |
(38) 리액트 소셜 로그인 1 (0) | 2024.03.06 |