카카오 로그인 환경설정
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
카카오 개발자 센터에서 애플리케이션 추가
- 플랫폼 등록 - WEB 플랫폼 등록 - http://localhost:8080 등록
- 카카오 로그인 - 활성화
- Redirect URI - http://localhost:8080/auth/kakao/callback 등록
- 동의항목 - 프로필, 이메일
카카오 로그인 OAuth2.0 개념
무한개의 홈페이지가 있을 때, 회원가입 시 각 사이트마다 따로 가입을 해줘야 한다. 실제 나라는 존재는 1명이지만 수많은 사이트에 회원가입하면서 내 개인 정보가 엄청 많이 노출되게 되는 것이다. 내 개인정보를 지키기 위해서는 수많은 사이트에 노출되어 있는 것 보다 한 곳에서 관리되는 게 훨씬 안전하다.
네이버, 카카오 등과 같은 대형 포털 사이트에서 개인정보를 관리해서 로그인 처리를 해준다. 네이버 아이디나 카카오 아이디만 있으면 여러 사이트에 개인정보를 노출하지 않고도 즉, 회원가입하지 않아도 로그인이 가능해진다. 이렇게 되면 네이버, 카카오에서만 확실하게 보안이 유지되면 개인정보는 절대 안전하다. 만에 하나 네이버, 카카오가 털리더라도 한 곳에서 관리되고 있기 때문에 네이버, 카카오 개인정보만 변경해주면 모든 사이트에 대해 안전해진다.
지금까지 만든 LEEBOOK 사이트에서도 회원가입을 하지 않고, 네이버, 카카오 아이디로 로그인할 수 있도록 하면 인증 처리에 대한 수고를 덜 수 있다.
하지만 만약 쇼핑몰이라면 네이버, 카카오에서 id, username, password 정보만 있는데 주소정보가 필수이므로 결국 쇼핑몰의 회원 정보(주소)와 네이버, 카카오의 회원정보를 연동하는 서비스를 구축해야 한다. 혹은 회원의 등급이 있을 경우 또 따로 각자 사이트에서 관리되기 때문에 연동하는 서비스가 필요하다.
그래서 네이버, 카카오는 딱 인증의 처리만 한다고 생각하면 된다. 각 사이트만의 독자적인 회원 정보가 필요하면 따로 관리해서 처리해야 한다.
OAuth란?
Open Auth를 뜻하는 말로 인증 처리를 대신해주는 서비스이다.
기존의 로그인 방식
OAuth 적용
OAuth 로그인의 용도
- 첫 번째 : 인증 처리 목적
- 두 번째 : 인증 처리 후 Access Token을 부여받아서 자원 서버에 접근할 수 있는 권한 취득 목적
스프링에서 공식적으로 제공해주는 OAuth 주체는 Facebook과 google이다. OAuth-Client 라이브러리에서 Facebook, google을 통한 인증, 권한 처리가 정말 쉽게 이뤄진다. 스프링 프로젝트를 만들 때 OAuth-Client를 체크해서 만들면 되지만 지금은 직접 구축해볼 것이다.
스프링에서 네이버, 카카오를 쓰려면 따로 연동이 필요한데 스프링 프로젝트를 만들 때 OAuth Resource Server를 체크해주면 쉽게 만들 수 있지만 지금은 노가다 코드로 익혀보자!!
카카오 로그인 엑세스 토큰 받기
카카오 로그인 버튼
카카오 로그인 버튼 리소스 다운로드 : https://developers.kakao.com/tool/resource/login
src/resource/static/image에 다운받은 리소스 붙여넣기
인증 코드 받기 - Request
GET /oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code HTTP/1.1
Host: kauth.kakao.com
최종 로그인 요청 주소 (GET)
https://kauth.kakao.com/oauth/authorize?client_id=2aef492ccc8fd9af1b188a17a89a355a&redirect_uri=http://localhost:8000/auth/kakao/callback&response_type=code
로그인 버튼 생성 후 적용
loginform.jsp
<a href="https://kauth.kakao.com/oauth/authorize?client_id=2aef492ccc8fd9af1b188a17a89a355a&redirect_uri=http://localhost:8000/auth/kakao/callback&response_type=code"><img height="38px" src="/image/kakao_login_button.png"/></a>
카카오 로그인을 눌러보면 로그인 페이지가 뜨고 실제 카카오 계정으로 로그인 하면 다음과 같은 주소가 반환된다.
http://localhost:8000/auth/kakao/callback?code=E4TnXJUAKUOypJmtkCssvrIGtJ3UdAI_XW7Zih7LN4LwRDkk8OjoU5kJ-hkaYpeQ_ZUmugo9dVoAAAGCu81cVg
http://localhost:8000/auth/kakao/callback 주소로 인가 코드 code=E4TnXJUAKUOypJmtkCssvrIGtJ3UdAI_XW7Zih7LN4LwRDkk8OjoU5kJ-hkaYpeQ_ZUmugo9dVoAAAGCu81cVg를 정상적으로 받았다는 뜻이다. 즉, 로그인 인증 정상 처리!!
UserController.java
@GetMapping("/auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수
return "로그인 성공! 인증 코드 : "+code;
}
사용자 토큰 받기
이 인증된 인증 코드를 통해 엑세스 토큰을 부여받을 것이다. 카카오 리소스 서버에 등록된 내 개인정보를 응답받기 위해서는 엑세스 토큰이 필요하기 때문이다.
사실 카카오 로그인이 완료된 것은 아니지만 인증 코드는 받았기 때문에 인증된 사용자로 로그인 처리를 해줄 수도 있긴 하지만 그 사람의 정보를 알려면 엑세스 토큰으로 카카오 리소스 서버에 접근애야 하기 때문에 의미가 없다.
최종 토큰 발급 요청 주소 (POST) - http body로 데이터 전달 (필수 4가지 데이터 담아서)
- 요청 주소 : https://kauth.kakao.com/oauth/token
- 헤더 값
application/x-www-form-urlencoded;charset=utf-8 (key=value 형태) - 바디값
grant_type=authorization_code
client_id=2aef492ccc8fd9af1b188a17a89a355a
redirect_uri=http://localhost:8000/auth/kakao/callback
code={지금 알 수 없는 동적인 값}
@GetMapping("/auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수
// POST 방식으로 key=value 타입의 데이터를 요청 (카카오쪽으로) - 라이브러리 사용
// Retrofit2 - 안드로이드에서 많이 사용
// OkHttp
// RestTemplate
RestTemplate rt = new RestTemplate();
// HttpHeader 오브젝트 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // key-value 형태라고 알려주기
// HttpBody 오브젝트 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", "2aef492ccc8fd9af1b188a17a89a355a");
params.add("redirect_uri", "http://localhost:8000/auth/kakao/callback");
params.add("code", code); // 동적인 값
// 좋은 방법은 아니지만 이해를 위해 날코딩!! 원래는 변수를 만들어 놓고 사용하는 게 좋다.
// HttpHeader와 HttpBody 하나의 오브젝트로
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
new HttpEntity<>(params, headers);
// 실제로 Http 요청
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token", // 토큰 요청 주소
HttpMethod.POST, // 요청 방식
kakaoTokenRequest, // 요청 데이터
String.class // 응답은 String으로 받을 것
);
return "카카오 토큰 요청 완료: "+response;
}
사용자 토큰 값이 잘 반환되는 것을 볼 수 있다.
사용자 정보 가져오기
이제 이 사용자 토큰을 이용해 개인정보를 요청해보자. 이를 위해 토큰을 오브젝트에 담을 것이다.
model/OAuthToken.java
package com.cos.blog.model;
import lombok.Data;
@Data
public class OAuthToken {
private String access_token;
private String token_type;
private String refresh_token;
private int expires_in;
private String scope;
private int refresh_token_expires_in;
}
UserController.java
@GetMapping("/auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수
// POST 방식으로 key=value 타입의 데이터를 요청 (카카오쪽으로) - 라이브러리 사용
// Retrofit2 - 안드로이드에서 많이 사용
// OkHttp
// RestTemplate
RestTemplate rt = new RestTemplate();
// HttpHeader 오브젝트 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // key-value 형태라고 알려주기
// HttpBody 오브젝트 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", "2aef492ccc8fd9af1b188a17a89a355a");
params.add("redirect_uri", "http://localhost:8000/auth/kakao/callback");
params.add("code", code); // 동적인 값
// 좋은 방법은 아니지만 이해를 위해 날코딩!! 원래는 변수를 만들어 놓고 사용하는 게 좋다.
// HttpHeader와 HttpBody 하나의 오브젝트로
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
new HttpEntity<>(params, headers);
// 실제로 Http 요청 - Post 방식으로
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token", // 토큰 요청 주소
HttpMethod.POST, // 요청 방식
kakaoTokenRequest, // 요청 데이터
String.class // 응답은 String으로 받을 것
);
// 라이브러리 Gson, Json Simple, ObjectMapper
ObjectMapper objectMapper = new ObjectMapper();
OAuthToken oauthToken = null;
try {
oauthToken = objectMapper.readValue(response.getBody(), OAuthToken.class);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
System.out.println("카카오 엑세스 토큰 : " + oauthToken.getAccess_token());
return "";
}
이제 담은 오브젝트로 사용자 개인정보를 요청해보자
토큰을 통한 사용자 정보 조회 (GET, POST 모두 지원하지만 POST로)
- 요청 주소 : https://kapi.kakao.com/v2/user/me
- 헤더 값
Authorization: Bearer ${ACCESS_TOKEN}
Content-type: application/x-www-form-urlencoded;charset=utf-8
UserController.java
@GetMapping("/auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수
...
RestTemplate rt2 = new RestTemplate();
// HttpHeader 오브젝트 생성
HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer "+oauthToken.getAccess_token());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HttpHeader와 HttpBody 하나의 오브젝트로
HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest2 =
new HttpEntity<>(headers2);
// 실제로 요청
ResponseEntity<String> response2 = rt2.exchange(
"https://kapi.kakao.com/v2/user/me", // 토큰 요청 주소
HttpMethod.POST, // 요청 방식
kakaoProfileRequest2, // 요청 데이터
String.class // 응답은 String으로 받을 것
);
return response2.getBody();
}
반환값도 자바 오브젝트로 받으려면 다음 사이트를 이용하면 편하다.
https://www.jsonschema2pojo.org/
KakaoProfile.java
package com.cos.blog.model;
import lombok.Data;
@Data
public class KakaoProfile {
public Long id;
public String connectedAt;
public Properties properties;
public KakaoAccount kakaoAccount;
class Properties {
public String nickname;
public String thumbnailImageUrl;
public String profileImageUrl;
}
class KakaoAccount {
public Boolean profileNicknameNeedsAgreement;
public Profile profile;
public Boolean hasEmail;
public Boolean emailNeedsAgreement;
public Boolean isEmailValid;
public Boolean isEmailVerified;
public String email;
class Profile {
public String nickname;
public String thumbnailImageUrl;
public String profileImageUrl;
}
}
}
KakaoProfile을 자바 오브젝트로 받아오고 블로그 서버 통합 아이디, 이메일, 패스워드 등 회원정보 설정
UserConroller.java
@GetMapping("/auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수
...
ObjectMapper objectMapper2 = new ObjectMapper();
KakaoProfile kakaoProfile = null;
try {
kakaoProfile = objectMapper2.readValue(response2.getBody(), KakaoProfile.class);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
System.out.println("카카오 아이디 : "+kakaoProfile.getId());
System.out.println("카카오 이메일 : "+kakaoProfile.getKakao_account().getEmail());
// 중복방지를 위해 길더라도 아래처럼!
System.out.println("블로그서버 유저네임 :"+kakaoProfile.getKakao_account().getEmail()+"_"+kakaoProfile.getId());
System.out.println("블로그서버 이메일 :"+kakaoProfile.getKakao_account().getEmail());
UUID garbagePassword = UUID.randomUUID(); // 어짜피 안쓰니까 임시 패스워드 (Garbage)
System.out.println("블로그서버 패스워드 :"+garbagePassword);
return response2.getBody();
}
이 정보들 말고 추가 정보가 필요하다면 따로 회원정보 구성창을 만들어서 추가적으로 기입을 받아야 한다.
설정한 정보들을 이용해 회원을 조회한 후 비가입자면 강제 회원가입을 시키고 가입자면 강제 로그인을 시킬 것이다.
UserController.java
User kakaoUser = User.builder()
.username(kakaoProfile.getKakao_account().getEmail()+"_"+kakaoProfile.getId())
.password(garbagePassword.toString()) // 알아서 인코딩 될 예정
.email(kakaoProfile.getKakao_account().getEmail())
.build();
// 가입자 혹은 비가입자 체크해서 처리
User originUser = userService.회원찾기(kakaoUser.getUsername());
// 비가입자의 경우 강제 회원가입
if(originUser.getUsername() == null) {
userService.회원가입(kakaoUser);
}
// 가입자의 경우 강제 로그인 처리
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(kakaoUser.getUsername(), kakaoUser.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
UserService.java
@Transactional(readOnly=true)
public User 회원찾기(String username) {
User user = userRepository.findByUsername(username).orElseGet(()->{
return new User();
});
return user;
}
회원가입은 되지만 에러가 발생한다..
비밀번호를 UUID로 임시 설정 해줬었는데 UUID는 매번 랜덤으로 바뀌는 값이라서 비밀번호가 계속 바뀌면 로그인이 안되기 때문에 Bad credentials 에러가 뜨는 것이다. 비밀번호를 특정 값으로 고정시켜서 강제 회원가입, 로그인 시켜줘야 한다.
application.yml
cos:
key: ******
UserConroller.java
@Controller
public class UserController {
@Value("${cos.key}")
private String cosKey;
...
@GetMapping("/auth/kakao/callback")
public String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수
...
// UUID라서 로그인할 때마다 패스워드가 바뀌니까 로그인 에러 생긴 것
// UUID garbagePassword = UUID.randomUUID(); // 어짜피 안쓰니까 임시 패스워드 (Garbage)
System.out.println("블로그서버 패스워드 :"+cosKey);
User kakaoUser = User.builder()
.username(kakaoProfile.getKakao_account().getEmail()+"_"+kakaoProfile.getId())
.password(cosKey) // 알아서 인코딩 될 예정
.email(kakaoProfile.getKakao_account().getEmail())
.build();
// 가입자 혹은 비가입자 체크해서 처리
User originUser = userService.회원찾기(kakaoUser.getUsername());
// 비가입자의 경우 강제 회원가입
if(originUser.getUsername() == null) {
userService.회원가입(kakaoUser);
}
// 가입자의 경우 강제 로그인 처리
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(kakaoUser.getUsername(), cosKey));
SecurityContextHolder.getContext().setAuthentication(authentication);
// kakaoCallback의 @ResponseBody 지워줘야 한다! 그래야 뷰 리졸버를 호출해서 파일을 찾아갈 것이다.
return "rediret:/";
}
}
잘 작동하지만 문제는 강제로 특정 값으로 설정한 비밀번호 값을 변경할 때 생긴다. 비밀번호가 바뀌었기 때문에 카카오 로그인을 재시도 했을 경우 로그인이 막힌다. 그래서 OAuth로 로그인을 시도한 경우에는 비밀번호 수정을 막아야 한다.
카카오 로그인인지 아닌지 구분해야 한다. 이를 위해 User 모델에 oauth 필드를 하나 추가해준다.
User.java
private String oauth; // kakao, google, ...
UserController.java
User kakaoUser = User.builder()
.username(kakaoProfile.getKakao_account().getEmail()+"_"+kakaoProfile.getId())
.password(cosKey) // 알아서 인코딩 될 예정
.email(kakaoProfile.getKakao_account().getEmail())
.oauth("kakao")
.build();
UpdateFrom.jsp
<c:if test="${empty principal.user.oauth}">
<div class="form-group">
<label for="password">Password:</label>
<input type="password" class="form-control" placeholder="Enter password" id="password">
</div>
</c:if>
<div class="form-group">
<label for="email">Email</label> <input type="email" value="${principal.user.email}" class="form-control" placeholder="Enter email" id="email" readonly>
</div>
이렇게만 하면 포스트맨을 통해 포스트 공격을 할 수도 있으니 서버쪽에서도 막아줘야 한다.
UserService.java
// Validate 체크
if(persistance.getOauth() == null || persistance.getOauth().equals("")) {
String rawPassword = user.getPassword();
String encPassword = encoder.encode(rawPassword); // 암호화
persistance.setPassword(encPassword);
persistance.setEmail(user.getEmail());
}
출처 : https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9
'Spring > Blog 만들기 with SpringBoot' 카테고리의 다른 글
에러 수정 (0) | 2022.09.06 |
---|---|
댓글 시스템 (0) | 2022.09.06 |
회원 수정 (0) | 2022.07.25 |
게시판 (0) | 2022.07.25 |
비밀번호 해쉬 & 시큐리티 로그인 (0) | 2022.07.24 |