일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- PDA
- GDI
- phpmailer
- EUC-KR
- self-signed ssl
- php
- ClickOnce
- 기념일관리
- docker
- 크래시로그
- protobuf-c
- C#
- API
- Font
- 와이브로
- 데이터 전달
- net
- JavaScript
- crashlog
- MFC
- 블루투스 헤드셋
- .net
- plcrashreporter
- 한 번만 실행
- 자바스크립트
- Antialiasing
- C/C++
- M8200
- VS2008
- 설치제거
- Today
- Total
~☆~ 우하하!!~ 개발블로그
[SpringBoot] Naver 로그인 구현 (1차 시도) 본문
일단, 제목에 1차 시도라고 굳이 밝히고 있는 이유는 Naver 계정을 이용한 로그인 구현이 완료될 수 없는 지경임을 확인했기 때문이다.
이번 포스트에서는 Naver 계정으로 로그인하는 기본 절차만 확인하고, 그 결과에 따라 회원정보 테이블 구조를 변경하는 작업을 수행한 후에 완료지을 예정이다.
네이버 계정 로그인을 위한 네이버 개발자센터 설정내용은 https://iwoohaha.tistory.com/336 을 참고하자.
로그인 폼에 네이버 계정 로그인을 위한 버튼을 추가한다. 별다른 설명없이 로그인폼에 배치한 구글 로그인 버튼의 스타일을 변경해봤는데, https://iwoohaha.tistory.com/333 에서 설명하는 css scanner 확장 프로그램의 도움을 받았다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.tyhmeleaf.org">
<head>
<meta charset="UTF-8">
<!-- 모바일에서의 적절한 반응형 동작을 위해 -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- -->
<title>Diary</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
>
<!-- -->
<!-- head 태그에 다음의 코드를 삽입 -->
<style type="text/css">
html,
body {
height: 100%;
}
.form-signin {
max-width: 330px;
padding: 1rem;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
div {
margin: 0px;
padding: 0px;
}
.css-1n7nx3r {
margin-top: 24px;
}
.css-1n7nx3r::before {
color: rgb(94, 108, 132);
content: attr(data-i18n-continue);
display: block;
font-size: 14px;
line-height: 16px;
margin-bottom: 16px;
font-weight: 600;
text-align: center;
}
.css-1vymulm:not(:last-child) {
margin-bottom: 8px;
}
button {
font-family: inherit;
}
.css-1bthe7p {
-webkit-box-align: baseline;
align-items: baseline;
box-sizing: border-box;
display: inline-flex;
font-size: inherit;
font-style: normal;
font-family: inherit;
max-width: 100%;
position: relative;
text-align: center;
text-decoration: none;
transition: background 0.1s ease-out, box-shadow 0.15s cubic-bezier(0.47, 0.03, 0.49, 1.38);
white-space: nowrap;
cursor: pointer;
padding: 0px 10px;
vertical-align: middle;
width: 100%;
-webkit-box-pack: center;
justify-content: center;
box-shadow: none;
font-weight: bold;
border: 1px solid rgb(193, 199, 208);
border-radius: 3px;
color: var(--ds-text, #42526E) !important;
height: 40px !important;
line-height: 40px !important;
background: rgb(255, 255, 255) !important;
}
.css-1bthe7p:visited {
background: var(--ds-background-neutral, rgba(9, 30, 66, 0.04));
color: var(--ds-text, #42526E) !important;
}
.css-1bthe7p:hover {
background: var(--ds-background-neutral-hovered, rgba(9, 30, 66, 0.08));
text-decoration: inherit;
transition-duration: 0s, 0.15s;
color: var(--ds-text, #42526E) !important;
}
.css-1bthe7p:active {
background: var(--ds-background-neutral-pressed, rgba(179, 212, 255, 0.6));
transition-duration: 0s, 0s;
color: var(--ds-text, #0052CC) !important;
}
.css-1bthe7p:focus {
outline: 2px solid var(--ds-border-focused, #2684FF);
outline-offset: 2px;
}
.css-1bthe7p:focus-visible {
outline: 2px solid var(--ds-border-focused, #2684FF);
outline-offset: 2px;
}
@media screen and (forced-colors: active), screen and (-ms-high-contrast: active){
.css-1bthe7p:focus-visible {
outline: solid 1px;
}
}
.css-1bthe7p span {
-webkit-box-pack: center;
justify-content: center;
display: flex !important;
}
.css-1bthe7p span {
-webkit-box-flex: unset;
flex-grow: unset;
}
.css-1ti50tg {
opacity: 1;
transition: opacity 0.3s;
display: flex;
margin: 0px 2px;
-webkit-box-flex: 0;
flex-grow: 0;
flex-shrink: 0;
align-self: center;
font-size: 0px;
line-height: 0;
user-select: none;
margin-inline-start: var(--ds-space-negative-025, -2px);
}
img {
margin: 0px;
padding: 0px;
}
img {
border: 0px;
}
.css-1bthe7p img {
height: 24px;
width: 24px;
margin-right: 6px;
}
.css-178ag6o {
opacity: 1;
transition: opacity 0.3s;
margin: 0px 2px;
-webkit-box-flex: 1;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media screen and (forced-colors: active), screen and (-ms-high-contrast: active){
.css-1bthe7p:focus-visible {
outline: solid 1px;
}
}
@media screen and (forced-colors: active), screen and (-ms-high-contrast: active){
.css-1bthe7p:focus-visible {
outline: solid 1px;
}
}
@media screen and (forced-colors: active), screen and (-ms-high-contrast: active){
.css-1bthe7p:focus-visible {
outline: solid 1px;
}
}
</style>
</head>
<body>
<main class="form-signin w-100 m-auto">
<th:block th:if="${error}">
<div th:if="${message}" class="alert alert-danger" role="alert" th:text="${message}"></div>
</th:block>
<!-- <form th:action="@{/process_login}" method="POST">-->
<form th:action="@{/signin}" method="POST">
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
<div class="form-floating">
<input type="email" class="form-control" id="floatingInput" name="email" placeholder="name@example.com">
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="floatingPassword" name="password" placeholder="Password">
<label for="floatingPassword">Password</label>
</div>
<!-- <div class="form-check text-start my-3">-->
<!-- <input class="form-check-input" type="checkbox" value="yes" name="remember-me" id="flexCheckDefault">-->
<!-- <label class="form-check-label" for="flexCheckDefault">-->
<!-- Remember me-->
<!-- </label>-->
<!-- </div>-->
<div class="d-grid gap-2">
<button class="btn btn-primary w-100 py-2" type="submit">Sign in</button>
<a href="/signup">
<button class="btn btn-success w-100 py-2" type="button">Sign up</button>
</a>
<div data-i18n-or="또는" data-i18n-continue="또는 다음을 사용하여 계속하기" class="google-login social-login css-1n7nx3r" data-testid="social-login-wrapper" style="">
<div data-testid="social-login-button-row" class="css-1vymulm">
<div class="css-1vymulm">
<a href="/oauth2/authorization/google">
<button id="google-auth-button" class="css-1bthe7p" tabindex="0" type="button" style="">
<span class="css-1ti50tg">
<img src="https://id-frontend.prod-east.frontend.public.atl-paas.net/assets/google-logo.5867462c.svg" alt="">
</span>
<span class="css-178ag6o">Google</span>
</button>
</a>
</div>
<div class="css-1vymulm">
<a href="/oauth2/authorization/naver">
<button id="naver-auth-button" class="css-1bthe7p" tabindex="0" type="button">
<span class="css-1ti50tg">
<img src="/images/naverlogo.png" alt="Naver Logo">
</span>
<span class="css-178ag6o">Naver</span>
</button>
</a>
</div>
</div>
</div>
</div>
<p class="mt-5 mb-3 text-body-secondary">© 2017–2024</p>
</form>
</main>
<!-- Popper -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
crossorigin="anonymous"></script>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"></script>
</body>
</html>
위 코드로 구현된 로그인폼 화면의 모습은 아래와 같다.
네이버 로그인 버튼은 구글 로그인 버튼의 스타일을 최대한 따랐고, 네이버 로고 이미지는 내가 직접 작업해서 /src/main/resources/static/images 경로에 넣어두었다.
위 경로에 저장된 이미지 파일의 액세스를 위해서 SecurityConfig.java 의 filterChain 함수에 허용 디렉토리 목록에 /images/** 를 추가한 것도 밝혀둔다.
새로 추가한 네이버 로그인 버튼의 URL 은 구글 버튼의 URL 과 비슷하게 /oauth2/authorization/naver 이다.
이번에는 application.yml 에 네이버 로그인 기능에 필요한 설정값을 추가한다.
spring:
security:
oauth2:
client:
registration:
naver:
client-id: NFgdRo9T7HjogFEdUqSu
client-secret: X7EDZmWMCx
scope:
- name
- email
client-name: Naver
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/naver
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response
google:
client-id: 528898101025-895po5vpn68pdkmuqj4aq314h2h0eob4.apps.googleusercontent.com
client-secret: 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
scope:
- profile
- email
# - openid
...
google 로그인 설정과는 다르게 provider 관련 설정값도 추가해야 한다는 것을 잊지 말자.
여기까지 완료하였으면 CustomOAuth2UserService 의 loadUser 함수에 BreakPoint 를 걸어놓고 디버그로 실행시켜보자.
네이버에 로그인된 상태가 아니라면 네이버 로그인 버튼을 클릭했을 때 네이버 로그인 화면이 표시된다.
로그인을 하면 아래와 같이 동의 화면이 표시된다.
전체 동의하기에 체크를 하고 동의하기 버튼을 클릭하면 loadUser 에 걸어두었던 BreakPoint 에 걸리게 된다.
굳이 여기에 BreakPoint 를 걸어둔 이유는 구글 로그인의 경우와는 다른 데이터 구조를 확인하기 위함이다.
oAuthUser 는 정보 제공자인 naver 가 제공해준 정보인데, 아래에서 볼 수 있는 것처럼 response KEY 를 통해서 id, profile_image, name 등의 값을 구할 수 있는 구조이다.
구글 로그인시 구글이 제공하는 정보 구조와 네이버 로그인시 네이버가 제공하는 정보 구조가 다르므로 provider 값에 따른 분기처리가 필요한 상황이다.
그런데, diary 프로그램의 회원구조는 email 주소가 PK 로 정의되어 있는데, 네이버는 네이버 계정 이메일 주소를 제공해주지 않으므로 회원정보 관리 구조에 대한 대대적인 개편이 필요하게 되었다.
'SpringBoot' 카테고리의 다른 글
[SpringBoot] Google 로그인시 회원가입 로직 추가 (0) | 2024.11.18 |
---|---|
[연재] SpringBoot diary - jwt 로그인으로 변경, 로그아웃까지 수정 (0) | 2024.11.15 |
[연재] SpringBoot diary - Spring Security remember-me, logout 처리 재정리 (0) | 2024.11.15 |
[연재] SpringBoot diary - 역할(ROLE) 관리 기능 추가 (0) | 2024.11.15 |
[연재] SpringBoot diary - RestController 에서 ResponseEntity 를 리턴하자. (0) | 2024.11.15 |