개발자입니다
[프로젝트] 네이버 아이디로 회원가입, 로그인 구현(React, SpringBoot) 본문
[프로젝트] 네이버 아이디로 회원가입, 로그인 구현(React, SpringBoot)
끈기JK 2023. 4. 9. 21:58
개요
네이버 로그인 버튼으로 네이버 계정을 연동해서 로그인 한다.
DB 에 회원 정보가 없을 경우 즉시 가입 시킨다.
Artify 에 가입한 사용자가 로그인할 경우 Artify 로 로그인 하라고 유도한다. (계정 통합 기능은 구현하지 않았다.)
주의사항! 토큰을 받아서 토큰을 백엔드 서버로 전달해 여기서 토큰을 얻어서 회원 정보를 가져오는게 맞으나, 토큰 정보를 프론트에서 받아서 URL 주소로 토큰 정보를 넘기는 방법을 사용하였다. 토큰 유효시간이 있지만 토큰 값이 캐싱되므로 해당 방법은 알맞지 않다.
흐름은 다음과 같다.
- Naver Developers 에 서비스 URL 과 Callback URL 을 등록한다.
- SDK 코드를 삽입한다.
- 네이버 로그인 컴포넌트를 삽입한다.
- 네이버 로그인 버튼 클릭시 컨트롤러에서 요청을 받아서 네이버에 토큰으로 사용자 정보를 받아서
이미 Artify 에 네이버로 가입된 경우는 로그인하고,
Artify 에 네이버로 가입한 적이 없으면 회원가입을 시킨다.
Naver Developers 에 등록
[NAVER Developers > Application > 애플리케이션 등록] 에 들어가 아래와 같이 입력한다.
등록하기 눌러도 반응이 없는데 그래도 등록 된 것이다.
Callback URL 을 백엔드 서버로 지정하면 백엔드에서 요청을 처리한다.
SDK (Software Development Kit) 준비
리액트의 public/index.html 에 head 태그 안에 다음 코드를 삽입해준다.
<script
type="text/javascript"
src="https://static.nid.naver.com/js/naveridlogin_js_sdk_2.0.0.js"
charset="utf-8"
></script>
아래 가이드 참고하여 작성하였다.
https://developers.naver.com/docs/login/devguide/devguide.md
코드 작성
AuthBtn
외부 로그인 버튼을 회원가입, 로그인 모달 둘다 사용해야 해서 상태를 부모 컴포넌트인 AuthBtn 에서 관리한다.
그리고 state 와 set 함수를 넘겨준다.
회원가입 또는 로그인 모달을 띄우기 위한 state 를 true 로 하면 externalLogin 상태도 true 가 되도록 한다.
아래부분 작성하려고 몇 시간을 헤맸다.
showExternalLogin={signupShow}
showExternalLogin={loginShow}
function AuthBtn(props) {
// 생략
const [showExternalLogin, setShowExternalLogin] = useState(true);
return (
// 생략
<SignupModal
signupShow={signupShow}
setSignupShow={setSignupShow}
setLoginShow={setLoginShow}
showExternalLogin={signupShow}
setShowExternalLogin={setShowExternalLogin}
/>
<LoginModal
currentUser={currentUser}
setCurrentUser={setCurrentUser}
handleShow={handleLoginShow}
loginShow={loginShow}
setLoginShow={setLoginShow}
setSignupShow={setSignupShow}
showExternalLogin={loginShow}
setShowExternalLogin={setShowExternalLogin}
/>
// 생략
)
}
LoginModal, SignupModal
네이버 로그인 버튼을 로그인 및 회원가입 모달의 원하는 곳에 위치시킨다.
function LoginModal(props) {
return (
// 생략
<div>{props.showExternalLogin && <ExternalLogin />}</div>
// 생략
)
}
function SignupModal(props) {
return (
// 생략
<div>{props.showExternalLogin && <ExternalLogin />}</div>
// 생략
)
}
ExternalLogin 컴포넌트
네이버 로그인 외에도 추가할 걸 대비해서 만든 컴포넌트이다.
function ExternalLogin(props) {
return (
<>
<NaverLogin />
</>
);
}
NaverLogin
id="naverIdLogin" 로 설정해야 네이버 로그인 버튼이 나온다.
아래처럼 clientId, callbackUrl 등을 세팅하고, state 는 자동으로 난수가 생성되어 전송된다.
네이버 로그인 버튼을 누르면 서비스 동의 상자가 나오고 동의하기를 누르면 요청을 보내고 응답을 받는다.
이 방법을 사용하면 응답 받을때 access_token 을 바로 받는다.
const NaverLogin = () => {
const { naver } = window;
const NAVER_CLIENT_ID = "************"; // 발급 받은 Client ID 입력
const NAVER_CALLBACK_URL = "http://localhost:3000/auth/naverlogin"; // 작성했던 Callback URL 입력
// 네이버 로그인 기능 및 버튼 구현
const naverLogin = new naver.LoginWithNaverId({
clientId: NAVER_CLIENT_ID, // Naver Developer 에 있는 Client ID
callbackUrl: NAVER_CALLBACK_URL, // 요청 보냈을때 네이버에서 응답해 줄 주소
isPopup: false, // 네이버 로그인 확인 창을 팝업으로 띄울지 여부
loginButton: {
color: "green", // green, white
type: 3, // 1: 작은버튼, 2: 중간버튼, 3: 큰 버튼
height: 50, // 크기는 높이로 결정한다.
},
});
useEffect(() => {
naverLogin.init();
}, []);
return (
<>
{/* 로그인 버튼 요청 URI
https://nid.naver.com/oauth2.0/authorize?response_type=token&client_id="************";&state=74075dc6-cfeb-40f9-87c5-d144e34a3983&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2FnaverLogin&version=js-2.0.0&svctype=1
응답
http://localhost:3000/auth/naverLogin#access_token=AAAAOJVd5J9VsZr4FoB************&state=74075dc6-cfeb-40f9-87c5-d144e34a3983&token_type=bearer&expires_in=3600 */}
<div id="naverIdLogin" />
</>
);
};
NaverLoginHandler (Callback 처리 Handler)
네이버에서 응답이 오면 그 응답을 처리할 handler 의 Route 를 먼저 지정한다.
function App() {
return (
<div>
<BrowserRouter>
<Navbars />
<Routes>
// 생략
<Route path="/auth/naverlogin" element={<NaverLoginHandler />} />
</Routes>
<Footer />
</BrowserRouter>
</div>
);
}
주소에서 # 뒤의 값을 가공해서 params 에 넣는다.
params 와 함께 백엔드 서버로 요청을 보낸다.
const NaverLoginHandler = () => {
// useHistory 훅을 사용해 history 객체를 가져옵니다. 이 객체를 사용하여 라우터 내에서 리다이렉션을 수행할 수 있습니다.
const navigate = useNavigate();
useEffect(() => {
const processNaverLogin = async () => {
// URL의 해시 부분에서 query parameter들을 추출합니다. 이 값들은 access_token, state, token_type, expires_in과 같은 인증 관련 정보를 포함합니다.
const queryParams = window.location.hash.substring(1).split("&");
const params = {};
// console.log(window.location); //Location {ancestorOrigins: DOMStringList, href: 'http://localhost:3000/auth/naverLogin#access_token…57-eb93287b5f70&token_type=bearer&expires_in=3600', origin: 'http://localhost:3000', protocol: 'http:', host: 'localhost:3000', …}
// console.log(window.location.hash); //#access_token=AAAAOJ2B7*************&state=4b53e1ff-4b37-44f4-b857-eb93287b5f70&token_type=bearer&expires_in=3600
// console.log(queryParams); //['access_token=AAAAOJ2B7*************', 'state=4b53e1ff-4b37-44f4-b857-eb93287b5f70', 'token_type=bearer', 'expires_in=3600']
queryParams.forEach((param) => {
const [key, value] = param.split("=");
params[key] = value;
});
try {
const response = await axios.post(
"http://localhost:8080/auth/naverlogin",
params
);
// console.log("서버에서 naverLogin 응답 옴!");
// console.log(response);
if (response.data.status === "failure") {
if (response.data.errorCode == "502") {
alert("Artify 계정으로 로그인 하세요");
}
}
window.location.href = "/"; // 인덱스 페이지로 이동
} catch (error) {
// console.error("서버에서 naverlogin 에러 옴!");
// console.error(error);
}
};
processNaverLogin();
}, []);
return <div>Processing Naver Login...</div>;
};
AuthController
서버에서 요청을 받아서 토큰 값을 분리하고 사용자 계정 정보를 요청할 String 을 만든다.
요청을 보내서 응답온 json 객체를 parsing 하고 Member 객체에 담을 준비를 한다.
외부계정 로그인 사용자는 비밀번호가 없기 때문에 난수 값을 생성해서 넣어준다.
이때, 해당 email 로 DB 에서 객체를 가져오고 객체가 없을때는 회원가입을 시킨다.
이미 가입되어 있으면 artify 에서 자체 가입한건지 확인해서 맞으면 에러 코드를 날린다.
그 후 세션에 유저 정보를 저장한다.
@RestController
@RequestMapping("/auth")
public class AuthController {
// 생략
@PostMapping("naverlogin")
public Object naverlogin(@RequestBody Map<String, String> params, HttpSession session) {
// log.info("params >>> " + params.toString()); //{access_token=AAAAOK3YZUQo0huTlz-hhCJuoC8c2oqBXuNgug8SJ9b9hKMAVsrDbQFrZ1ZEsW2pGT6hw3ouHoNIF2x1BYfjUcqtDWQ, state=4b53e1ff-4b37-44f4-b857-eb93287b5f70, token_type=bearer, expires_in=3600}
// log.info(params.get("access_token"));
String token = params.get("access_token"); // 네이버 로그인 접근 토큰
String header = "Bearer " + token; // Bearer 다음에 공백 추가
String apiURL = "https://openapi.naver.com/v1/nid/me";
try {
URL url = new URL(apiURL);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Authorization", header);
int responseCode = con.getResponseCode();
BufferedReader br;
if (responseCode == 200) { // 정상 호출
br = new BufferedReader(new InputStreamReader(con.getInputStream()));
} else { // 에러 발생
br = new BufferedReader(new InputStreamReader(con.getErrorStream()));
}
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = br.readLine()) != null) {
response.append(inputLine);
}
br.close();
// log.info(response.toString()); //{"resultcode":"00","message":"success","response":{"id":"eXA1mCGXExwuGQZo9uYmxlKnXI9LLqV5E_2lSaoakeE","nickname":"bit","profile_image":"https:\/\/ssl.pstatic.net\/static\/pwe\/address\/img_profile.png","gender":"F","email":"bit@naver.com","mobile":"010-0000-0000","mobile_e164":"+821000000000","birthday":"01-19"}}
String responseBody = response.toString();
JSONObject jsonObject = new JSONObject(responseBody);
JSONObject responseJson = jsonObject.getJSONObject("response");
String nickname = responseJson.getString("nickname");
String email = responseJson.getString("email");
String password = UUID.randomUUID().toString();
String link = email.contains("@naver.com") ? "naver" : "other";
Member oldMember = memberService.getByEmail(email);
// log.info("oldMember >>> " + oldMember);
if (oldMember == null) {
Member member = new Member();
member.setNickname(nickname);
member.setEmail(email);
member.setPassword(password);
member.setLink(link);
memberService.add(member);
}
Member user = memberService.getByEmail(email);
if (user != null && !user.getLink().equals("naver")) {
// log.info("artify 회원이 네이버 계정으로 접속 시도함!");
return new RestResult()
.setErrorCode(ErrorCode.rest.DUPLICATE_DATA)
.setStatus(RestStatus.FAILURE);
}
session.setAttribute("loginUser", user);
// log.info("세선에 user 정보 입력 >>> " + user);
return new RestResult()
.setStatus(RestStatus.SUCCESS);
} catch (Exception e) {
log.error("네이버 로그인 중 에러 발생! : " + e);
return new RestResult()
.setStatus(RestStatus.FAILURE);
}
}
}
MemberService
Service 객체에서는 기존에 가입한 회원가 닉네임이 중복되는지 체크한다.
중복되면 닉네임 끝에 +를 붙여 닉네임 중복을 방지한다.
회원은 닉네임에 -, _, . 만 사용 가능하므로 + 를 붙이면 중복은 없다.
@Service
public class DefaultMemberService implements MemberService {
@Override
public void add(Member member) {
Member OldMember = memberDao.findByNickname(member.getNickname());
if (OldMember != null) {
String signUpMemberNickname = member.getNickname();
if (signUpMemberNickname.equals(OldMember.getNickname())) {
member.setNickname(signUpMemberNickname + "+");
}
}
memberDao.insert(member);
}
// 생략
}
MemberDao.xml
insert 에 link 가 없었으므로 넣어준다.
<insert id="insert" parameterType="member"
useGeneratedKeys="true" keyProperty="no" keyColumn="member_no">
insert into aim_member(name, email, pw, pt, link)
values(#{nickname}, #{email}, sha2(#{password},256), 0, #{link})
</insert>
Member
vo 에 link 정보를 저장할 필드를 선언한다.
@Data
public class Member implements Serializable {
private static final long serialVersionUID = 1L;
// 생략
private String link;
}
참고
네이버ID 에서 가입한 사이트에 대한 철회를 신청할 수 있다.
철회를 신청하면 가입시 동의하기 창이 다시 나타난다.
'네이버클라우드 AIaaS 개발자 양성과정 1기 > 프로젝트' 카테고리의 다른 글
[프로젝트] SSE 방식으로 서버(SpringBoot)에서 클라이언트(React)로 데이터 보내기 (0) | 2023.04.19 |
---|---|
[비트캠프] 113일차(24주차1일) - React 파일 컴파일 (0) | 2023.04.17 |
[프로젝트] 네이버 메일 링크 클릭으로 인증 구현(SpringBoot) (1) | 2023.04.08 |
[비트캠프] 92일차(19주차5일) - DB 모델링 (0) | 2023.03.17 |