[내멋대로 만드는 Kimstagram] 12. OAuth2를 활용한 페이스북 로그인
이제 슬슬 외부에서 제공하는 API를 활용하는 법을 알아야 할 것 같아서 페이스북 로그인을 구현해보려고 한다.
사실 조금 찾아보니까 OAuth2를 활용하면 굉장히 간단하게 구현할 수 있는 것 같은데... ㅋㅋㅋㅋ 크게 도움이 될 진 모르겠지만....
https://developers.facebook.com/
Meta for Developers
개발자를 위한 필요한 조치 대시보드 도입 이번 달부터 developers.facebook.com에서 앱을 관리하는 몇몇 앱 관리자를 대상으로 필요한 조치 대시보드를 공개합니다. 데이터 사용 확인을 위한 데이터
developers.facebook.com
우선 위 링크로 들어가면 Meta(페이스북)의 개발자 센터가 나온다.
빠르게 로그인해준 후, 앱을 만들어 주었다.
그리고 이제 내 앱으로 들어가서 기본 설정으로 들어가면, 나의 앱ID와 시크릿 코드를 확인할 수 있다.
그럼 이제 이 앱ID와 시크릿 코드를 application.properties에 입력해준다.
spring.security.oauth2.client.registration.facebook.client-id=(앱ID)
spring.security.oauth2.client.registration.facebook.client-secret=(시크릿 코드)
spring.security.oauth2.client.registration.facebook.scope=public_profile, email
SecurityConfig를 수정하여 Spring Security에게 oauth2를 사용하고 있음을 알려주자.
http.authorizeRequests()
.antMatchers("/checkToken").access(("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')"))
.antMatchers("/**", "/index", "/auth/**", "/login", "/join", "/js/**", "/dynamicImage/**", "/image/**", "/favicon.ico").permitAll()
.anyRequest().authenticated()
.and().oauth2Login()
.and().exceptionHandling().authenticationEntryPoint(((request, response, authException) -> response.sendRedirect("/")));
그리고 페이스북 로그인 버튼의 url을 "/oauth2/authorization/facebook"로 설정해준다.
<!-- 페이스북 로그인 버튼 -->
<a href="/oauth2/authorization/facebook"><img src="/image/login/log_fc.png" id="facebookButton" onmousedown="index.facebookClick()"/></a>
됐다. 이제 저 버튼을 클릭하면 바로 페이스북 로그인 페이지로 이어져서 로그인할 수 있게 된다.
그렇다면 로그인 후 응답이 어디로 오는 걸까? 그걸 서버에서 받아야 유저 정보를 DB에 등록하던가 하는데...
바로 DefaultOAuth2UserService 의 loadUser() 메서드이다!
이걸 오버라이드해서 구현해주면, 페이스북 로그인 응답으로 오는 데이터를 다룰 수 있게 된다.
package com.kimdev.kimstagram.service;
import com.kimdev.kimstagram.Repository.AccountRepository;
import com.kimdev.kimstagram.controller.api.auth.PrincipalDetail;
import com.kimdev.kimstagram.model.Account;
import com.kimdev.kimstagram.model.RoleType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class OAuth2Service extends DefaultOAuth2UserService {
@Autowired
AccountRepository accountRepository;
@Autowired
private BCryptPasswordEncoder encoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId(); // facebook
String providerId = oAuth2User.getAttribute("id"); // Facebook의 고유 id를 가져옴
Account account = accountRepository.findByProviderId(providerId);
if(account == null) { // 해당 유저가 없을 경우
String username = provider+"_"+providerId; // facebook_12345678 양식
String password = encoder.encode(UUID.randomUUID().toString());
String email = oAuth2User.getAttribute("email");
String name = oAuth2User.getAttribute("name");
account = new Account();
account.setUsername(username);
account.setPassword(password);
account.setEmail(email);
account.setName(name);
account.setUse_profile_img(0);
account.setRole(RoleType.ROLE_USER);
accountRepository.save(account);
}
return new PrincipalDetail(account, oAuth2User.getAttributes());
}
}
기존의 유저도 PrincipalDetail을 반환했으니, 페이스북으로 로그인하는 유저도 PrincipalDetail을 반환하도록 했다.
그래서 PrincipalDetail 역시 아래와 같이 수정해 주었다.
package com.kimdev.kimstagram.controller.api.auth;
import com.kimdev.kimstagram.model.Account;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
@Getter
public class PrincipalDetail implements UserDetails, OAuth2User {
private Account account; // 로그인을 시도하는 Account
private Map<String, Object> attributes; // 페이스북 로그인이라면 이것도 들어옴
public PrincipalDetail(Account account) {
this.account = account;
}
public PrincipalDetail(Account account, Map<String, Object> attributes) {
this.account = account;
this.attributes = attributes;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>();
collectors.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return String.valueOf(account.getRole());
}
});
return collectors;
};
@Override
public String getPassword() {
return account.getPassword();
}
@Override
public String getUsername() {
return account.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getName() {
return null;
}
}
여튼 이러면 페이스북 로그인 자체는 끝난다.
그런데... 아마 로그인을 해도 /index로 넘어가지 않을 것이다.
왜냐? 기존에는 로그인 성공 시 Jwt 토큰을 받아서 js에서 /index로 리다이렉트 해주었는데,
페이스북 로그인에서는 우리가 적용한 이 js의 통제를 받지 않으니 토큰도 못 받고 인덱스로 리다이렉팅도 안 되는 것...
그럼 즉, OAuth2의 로그인이 성공했을 경우를 처리해주는 메서드를 구현해서 적용해주면 된다.
SpringConfig에서 아래와 같이 oauth2Login()이 성공하면 OAuth2Service의 loginSuccess()를 타게 해준다.
http.authorizeRequests()
.antMatchers("/checkToken").access(("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')"))
.antMatchers("/**", "/index", "/auth/**", "/login", "/join", "/js/**", "/dynamicImage/**", "/image/**", "/favicon.ico").permitAll()
.anyRequest().authenticated()
.and().oauth2Login()
.successHandler(((request, response, authentication) -> {
oAuth2Service.loginSuccess(request, response, authentication);
}))
...
loginSuccess()는 다음과 같이 만들었다.
public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
PrincipalDetail principalDetail = (PrincipalDetail) authentication.getPrincipal();
Account principal = principalDetail.getAccount();
int principalId = principal.getId();
String username = principal.getUsername();
if (username.startsWith("facebook")) {
response.sendRedirect("/facebook/facebookSetUsername");
} else {
String AccessToken = makeAccessToken(principalId, username);
String RefreshToken = makeRefreshToken(principalId, username);
response.sendRedirect("/facebookAuth?acesstoken="+AccessToken+"&refreshtoken="+RefreshToken);
}
}
public String makeAccessToken(int principalId, String username) {
// Hash 암호 방식으로 JWT토큰 생성 (RSA방식은 아님)
String jwtToken = JWT.create()
.withSubject("JWT_TOKEN")
.withExpiresAt(new Date(System.currentTimeMillis() + (60000*10))) // 토큰의 만료 시간 (10분)
.withClaim("id", principalId) // 비공개 클레임인데 그냥 값 암거나 넣으면 될 듯?
.withClaim("username", username)
.sign(Algorithm.HMAC512("kimdevAuth")); // 토큰에 붙일 고유한 Secret 값
return "Bearer " + jwtToken;
}
public String makeRefreshToken(int principalId, String username) {
Account account = accountRepository.findById(principalId).get();
// 리프레시 토큰 생성
String refreshToken = JWT.create()
.withSubject("JWT_TOKEN")
.withExpiresAt(new Date(System.currentTimeMillis() + (60000*60 * 24))) // 24시간 동안 유효
.withClaim("refresh", "refresh")
.sign(Algorithm.HMAC512("kimdevRefresh"));
securityService.saveRefreshToken(refreshToken, account);
return "Bearer " + refreshToken;
}
받은 principalId와 username으로 토큰들을 만들어서 쿼리스트링으로 반환한다.
그 전에, username이 "facebook"으로 시작한다면 그 유저네임은 "facebook_1234567" 이런 형식이라는 건데, 이건 보기 흉하므로 수정할 수 있도록 "/facebookSetUsername"으로 이동하게 해 주었다.
여기서는 아래와 같이 자신의 Username을 수정할 수 있다.
facebookSetUsername.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<head>
<%@ include file="../../layout/indexHeader.jsp"%>
<style>
#button {
width: 100px;
height: 30px;
text-align: center;
padding-top: 1px;
background-color: #0095f6;
border: 1px solid #ededed;
color: white;
cursor: pointer;
}
#button:hover {
background-color: #1877f2;
}
#button:active {
background-color: #4cb5f9;
}
</style>
</head>
<body>
<div style="width: 100%; height: 50%; display: flex; flex-direction: column; justify-content: center; align-items: center">
<div style="font-size: 26px; font-weight: bold">
아이디 설정
</div>
<div style="margin-bottom: 20px">
kimstagram에서 사용할 아이디를 입력해 주세요.
</div>
<input id="input" style="width: 300px; margin-bottom: 10px">
<div id="button" onclick="setUsername('${oriusername}')">
확인
</div>
<div id="using" style="display: none; color: #ed4956; margin-top: 30px">
이미 사용 중인 아이디입니다.
</div>
</div>
</body>
<script src="/js/facebookSetUsername.js"></script>
facebookSetUsername.js
function setUsername(oriusername) {
let data = {
oriusername: oriusername,
newusername: document.getElementById('input').value
};
$.ajax({
type: "GET",
url: "/setUsername",
data: data,
contentType: "application/json; charset=utf-8"
}).done(function(resp) {
if (resp === 0) { // 이미 해당 유저네임이 존재
document.getElementById('input').value = "";
document.getElementById('using').style.display = "block";
} else {
alert("아이디 설정이 완료되었습니다.\r\n로그인을 다시 진행해 주세요.");
location.href = "/";
}
});
}
뭐 이렇게 간단하게 작성했다.
그러면 진짜 로그인이 완료되고, 유저네임까지 제대로 설정했다면?!
액세스 토큰과 리프레시 토큰을 들고 /facebookAuth로 이동해서, LocalStorage에 토큰들을 저장한 후 /index로 이동하면 된다!
@GetMapping("/facebook/facebookAuth")
public String facebookAuth(Model model, @RequestParam String acesstoken, @RequestParam String refreshtoken) {
model.addAttribute("acess", acesstoken);
model.addAttribute("refresh", refreshtoken);
return "facebook/facebookAuth";
}
이렇게 컨트롤러에서 토큰들을 받아서 Model에 담아 facebookAuth.jsp로 보낸 뒤
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<head>
<%@ include file="../../layout/indexHeader.jsp"%>
</head>
<body>
<div style="width: 100%; height: 50%; display: flex; flex-direction: column; justify-content: center; align-items: center">
<div style="font-size: 26px">
잠시만 기다려 주세요...
</div>
<script>
localStorage.setItem('Authorization', '${access}')
localStorage.setItem('Refresh-Token', '${refresh}')
location.href = "/index";
</script>
</div>
</body>
<script src="/js/facebookSetUsername.js"></script>
facebookAuth.jsp에서는 이렇게 Model에 담긴 토큰을 받아 LocalStorage에 저장한 후 /index로 리다이렉팅 해주면 된다.
이렇게 짜면
이런 폼이 뜨면서 LocalStorage에 토큰들을 저장하게 되고...
저장이 완료된다면?!!
이렇게 인덱스 페이지로 리다이렉팅 된다 ㅎㅎㅎ
어찌저찌... 힘들게 구현 완료한 것 같다...
어떻게 OAuth2를 쓰고도 이렇게 힘들게 할 수 있지?? ㅋㅋㅋ
Jwt토큰 발급하는 과정이 항상 힘든 것 같다. 특히 서버에서 만든 토큰을 클라이언트로 넘기는 방법을 고안하는 게 제일 힘든 듯... ㅋㅋㅋㅋ
뭐 여튼 성공했으니~~ 이만 쉬러 가야지 ㅎㅎ
OAuth 2.0 + JWT + Spring Security로 회원 기능 개발하기 - 앱등록과 OAuth 2.0 기능구현
OAuth 2.0 로그인 기능을 구현하기 위해서는 꽤나 많은 작업이 필요합니다.이번 파트에서는 네이버, 카카오, 구글에 앱을 등록하고 OAuth 2.0을 본격적으로 사용할 수 있는 준비를 해보겠습니다.구글
velog.io
[Spring Security] 6. OAuth2 Google Login ②
Spring security 공부
velog.io
[Spring Boot] OAuth 2.0 로그인 (카카오, 네이버, 페이스북 로그인)
[Spring Boot] OAuth 2.0 로그인 (구글 로그인)에서 구글 로그인과 회원가입에 대해 정리했었음 구글 뿐 아니라 카카오, 네이버, 페이스북 로그인에 대해서 정리 과정이 거의 비슷하긴 하지만 조금씩
chb2005.tistory.com