https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
지난 번엔 spring MVC로 했고 이번엔 spring boot로 해본다.
https://developers.kakao.com/tool/resource/login
축약형 middle 사이즈로 카카오 버튼 이미지 다운.
loginForm에 카카오 로그인 버튼 추가
GET /oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code HTTP/1.1
Host: kauth.kakao.com
kakao 로그인 요청은 이러한 형식을 따라야 한다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ include file="../layout/header.jsp"%>
<div class="container">
<form action="/auth/loginProc" method="post">
<div class="form-group">
<label for="uname">Username:</label> <input type="text" class="form-control" id="username" placeholder="아이디를 입력해주세요." name="username" required>
</div>
<div class="form-group">
<label for="pwd">Password:</label> <input type="password" class="form-control" id="password" placeholder="비밀번호를 입력해주세요." name="password" required>
</div>
<div class="form-group form-check">
<label class="form-check-label"> <input class="form-check-input" type="checkbox" name="remember" required> 로그인 상태 유지
</label>
</div>
<button id="btn-login" class="btn btn-primary">로그인</button>
<a href="https://kauth.kakao.com/oauth/authorize?client_id=b8fcc2cb4e694b98986d5501c3e20dc0&redirect_uri=http://localhost:8282/auth/kakao/callback&response_type=code"><img src="/image/kakao_login_medium.png" height="38px"></a>
</form>
</div>
<%@ include file="../layout/footer.jsp"%>
저 주소로 요청을 보내면 코드를 응답해준다.
카카오 디벨로퍼에 나와있는 설명대로 인증 코드 요청, 코드를 가지고 사용자 토큰 요청, 토큰을 가지고 사용자 정보 가져오기를 구현.
package com.pure.blog.controller;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pure.blog.model.OAuthToken;
// 인증이 되지 않은 사용자들이 출입할 수 있는 /auth/** 경로 허용
// 주소가 "/"일 경우 index.jsp 허용
// static에 있는 /js/**, /css/**, /image/** 등등 허용.
@Controller
public class UserController {
@GetMapping("/auth/joinForm")
public String joinForm() {
return "user/joinForm";
}
@GetMapping("/auth/loginForm")
public String loginForm() {
return "user/loginForm";
}
@GetMapping("/user/updateForm")
public String updateForm() {
return "user/updateForm";
}
@GetMapping("/auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) {
String rest_api_key = "b8fcc2cb4e694b98986d5501c3e20dc0";
String redirect_uri = "http://localhost:8282/auth/kakao/callback";
//POST방식으로 key = value 형태의 데이터를 요청 (x-www-form-urlencoded;charset=utf-8 방식)
RestTemplate rt = new RestTemplate();
//Http Header 오브젝트 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
//Http Body 오브젝트 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", rest_api_key);
params.add("redirect_uri", redirect_uri);
params.add("code", code);
//HttpHeader와 HttpBody를 하나의 오브젝트에 합치기
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
new HttpEntity<MultiValueMap<String,String>>(params, headers);
//POST방식으로 http 요청하고 String타입으로 응답받아서 response 변수에 저장
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token", //요청주소
HttpMethod.POST, //요청방식
kakaoTokenRequest, //request entity (http header랑 body 합친것)
String.class //응답받을 타입
);
//JSON에 담기 위해 ObjectMapper 라이브러리 사용
ObjectMapper om = new ObjectMapper();
OAuthToken oauthToken = null;
try {
//String 상태인 response.getBody를 ObjectMapper를 이용해 파싱하여 OAuthToken의 각 변수에 값을 넣어줌.
oauthToken = om.readValue(response.getBody(), OAuthToken.class);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
System.out.println("카카오 액세스 토큰: "+oauthToken.getAccess_token());
//사용자 토큰으로 사용자 정보 받기
//POST방식으로 key = value 형태의 데이터를 요청 (x-www-form-urlencoded;charset=utf-8 방식)
RestTemplate rt2 = new RestTemplate();
//Http Header 오브젝트 생성
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);
//POST방식으로 http 요청하고 String타입으로 응답받아서 response 변수에 저장
ResponseEntity<String> response2 = rt2.exchange(
"https://kapi.kakao.com/v2/user/me", //요청주소
HttpMethod.POST, //요청방식
kakaoProfileRequest2, //request entity (http header랑 body 합친것)
String.class //응답받을 타입
);
return response2.getBody();
}
}
여기까지 하고 실행시켜서 페이지를 확인해보면 문자열 형태로 사용자 정보 가져오기 응답을 받을 수 있다.
이를 model에 담아야 하기 때문에
https://www.jsonschema2pojo.org/
여기에 바디의 내용을 넣어서 편리하게 만들 수 있다.
model 패키지에 KakaoProfile.java를 만든다.
package com.pure.blog.model;
import lombok.Data;
@Data
public class KakaoProfile {
public Integer id;
public String connected_at;
public Properties properties;
public Kakao_account kakao_account;
@Data
public class Properties {
public String nickname;
public String profile_image;
public String thumbnail_image;
}
@Data
public class Kakao_account {
public Boolean profile_nickname_needs_agreement;
public Boolean profile_image_needs_agreement;
public Profile profile;
public Boolean has_email;
public Boolean email_needs_agreement;
public Boolean is_email_valid;
public Boolean is_email_verified;
public String email;
@Data
public class Profile {
public String nickname;
public String thumbnail_image_url;
public String profile_image_url;
public Boolean is_default_image;
}
}
}
User 오브젝트도 oauth를 표시해주기 위해 수정.
package com.pure.blog.model;
import java.sql.Timestamp;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicInsert;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
//@DynamicInsert //null인 필드는 제외하고 insert 해줌.
@Entity //User 클래스가 MySQL에 자동으로 테이블로 생성됨.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id //PK
@GeneratedValue(strategy = GenerationType.IDENTITY) //프로젝트에서 연결된 DB의 넘버링 전략을 따라간다.
private int id; //시퀀스, auto_increment
@Column(nullable = false, length = 30, unique = true)
private String username; //아이디
@Column(nullable = false, length = 100) //추후 해쉬(비밀번호 암호화)를 위함.
private String password;
@Column(nullable = false, length = 50)
private String email;
//@ColumnDefault("user")
//DB는 RoleType이라는 타입이 없음.
@Enumerated(EnumType.STRING)
private RoleType role; // USER, ADMIN 두개만 들어갈 수 있도록 설정.
private String oauth; //kakao, google 등
@CreationTimestamp //시간이 자동으로 입력됨
private Timestamp createDate;
}
application.yml에 키 값 추가
server:
port: 8282
servlet:
context-path: /
encoding:
charset: UTF-8
enabled: true
force: true
spring:
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Seoul
username: pure
password: pure1234
# ddel-auto: create로 하면 서버 재시작마다 비어 있는 새 테이블을 생성함.
jpa:
open-in-view: true
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
use-new-id-generator-mappings: false
show-sql: true
properties:
hibernate.format_sql: true
jackson:
serialization:
fail-on-empty-beans: false
pure:
key: pure1234
저 키가 카카오 로그인시 비밀번호가 될 것임. (외부에 노출되면 안됨.)
회원정보수정에서 카카오 계정은 비밀번호 수정이 불가능하게 만들었음.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ include file="../layout/header.jsp"%>
<div class="container">
<form class="was-validated">
<input type="hidden" value="${principal.user.id }" id="id">
<div class="form-group">
<label for="uname">Username:</label> <input type="text" class="form-control" id="username" placeholder="아이디를 입력해주세요." name="username" value="${principal.user.username }" readonly>
</div>
<c:if test="${empty principal.user.oauth }">
<div class="form-group">
<label for="pwd">Password:</label> <input type="password" class="form-control" id="password" placeholder="비밀번호를 입력해주세요." name="password" required>
<div class="valid-feedback"></div>
<div class="invalid-feedback">수정할 비밀번호를 입력해주세요.</div>
</div>
</c:if>
<div class="form-group">
<label for="uname">Email:</label> <input type="text" class="form-control" id="email" placeholder="이메일 주소를 입력해주세요." name="email" value="${principal.user.email }">
<div class="valid-feedback"></div>
<div class="invalid-feedback">수정할 이메일을 입력해 주세요</div>
</div>
</form>
<button id="btn-update" class="btn btn-primary">회원정보 수정 완료</button>
</div>
<script src="/js/user.js"></script>
<%@ include file="../layout/footer.jsp"%>
UserService의 회원정보수정 메서드도 수정.
package com.pure.blog.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pure.blog.model.RoleType;
import com.pure.blog.model.User;
import com.pure.blog.repository.UserRepository;
//component-scan으로 Bean에 등록. (IoC)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder encoder;
@Transactional(readOnly = true)
public User 회원찾기(String username) {
User user = userRepository.findByUsername(username).orElseGet(() -> {
return new User();
}) ;
return user;
}
@Transactional //내부 기능이 모두 정상작동 하면 commit, 하나라도 실패하면 rollback
public int 회원가입(User user) {
try {
String rawPassword = user.getPassword();
String encPassword = encoder.encode(rawPassword);
user.setPassword(encPassword);
user.setRole(RoleType.USER);
userRepository.save(user);
return 1;
} catch (Exception e) {
e.printStackTrace();
System.out.println("UserService: 회원가입() : " + e.getMessage());
}
return -1;
}
@Transactional
public void 회원정보수정(User user) {
// 수정시에는 User 오브젝트를 영속화시켜서 영속화된 User 오브젝트를 수정해야 함.
// 따라서 select문을 통해 User 오브젝트를 영속화시켜야 함.
// 영속화된 오브젝트를 수정하면 자동으로 DB에 업데이트 되는 특성을 이용하여 회원정보수정.
User persistance = userRepository.findById(user.getId())
.orElseThrow(() -> {
return new IllegalArgumentException("회원 찾기 실패");
});
//카카오 사용자가 아닐경우에만 수정 가능.
if(persistance.getOauth() == null || persistance.getOauth().equals("")) {
String rawPassword = user.getPassword();
String encPassword = encoder.encode(rawPassword);
persistance.setPassword(encPassword);
persistance.setEmail(user.getEmail());
}
// 회원수정 함수 종료 == 서비스 종료 == 트랜잭션 종료 --> 자동 커밋 (더티체킹)
}
}
UserController 카카오 로그인 추가
package com.pure.blog.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pure.blog.model.KakaoProfile;
import com.pure.blog.model.OAuthToken;
import com.pure.blog.model.User;
import com.pure.blog.service.UserService;
// 인증이 되지 않은 사용자들이 출입할 수 있는 /auth/** 경로 허용
// 주소가 "/"일 경우 index.jsp 허용
// static에 있는 /js/**, /css/**, /image/** 등등 허용.
@Controller
public class UserController {
@Value("${pure.key}")
private String pureKey;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@GetMapping("/auth/joinForm")
public String joinForm() {
return "user/joinForm";
}
@GetMapping("/auth/loginForm")
public String loginForm() {
return "user/loginForm";
}
@GetMapping("/user/updateForm")
public String updateForm() {
return "user/updateForm";
}
@GetMapping("/auth/kakao/callback")
public String kakaoCallback(String code) {
String rest_api_key = "b8fcc2cb4e694b98986d5501c3e20dc0";
String redirect_uri = "http://localhost:8282/auth/kakao/callback";
//POST방식으로 key = value 형태의 데이터를 요청 (x-www-form-urlencoded;charset=utf-8 방식)
RestTemplate rt = new RestTemplate();
//Http Header 오브젝트 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
//Http Body 오브젝트 생성
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", rest_api_key);
params.add("redirect_uri", redirect_uri);
params.add("code", code);
//HttpHeader와 HttpBody를 하나의 오브젝트에 합치기
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
new HttpEntity<MultiValueMap<String,String>>(params, headers);
//POST방식으로 http 요청하고 String타입으로 응답받아서 response 변수에 저장
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token", //요청주소
HttpMethod.POST, //요청방식
kakaoTokenRequest, //request entity (http header랑 body 합친것)
String.class //응답받을 타입
);
//JSON에 담기 위해 ObjectMapper 라이브러리 사용
ObjectMapper om = new ObjectMapper();
OAuthToken oauthToken = null;
try {
//String 상태인 response.getBody를 ObjectMapper를 이용해 파싱하여 OAuthToken의 각 변수에 값을 넣어줌.
oauthToken = om.readValue(response.getBody(), OAuthToken.class);
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
System.out.println("카카오 액세스 토큰: "+oauthToken.getAccess_token());
//사용자 토큰으로 사용자 정보 받기
//POST방식으로 key = value 형태의 데이터를 요청 (x-www-form-urlencoded;charset=utf-8 방식)
RestTemplate rt2 = new RestTemplate();
//Http Header 오브젝트 생성
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);
//POST방식으로 http 요청하고 String타입으로 응답받아서 response 변수에 저장
ResponseEntity<String> response2 = rt2.exchange(
"https://kapi.kakao.com/v2/user/me", //요청주소
HttpMethod.POST, //요청방식
kakaoProfileRequest2, //request entity (http header랑 body 합친것)
String.class //응답받을 타입
);
ObjectMapper om2 = new ObjectMapper();
KakaoProfile kakaoProfile = null;
try {
kakaoProfile = om2.readValue(response2.getBody(), KakaoProfile.class);
} catch (JsonMappingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JsonProcessingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("카카오 아이디: "+kakaoProfile.getId());
System.out.println("카카오 이메일: "+kakaoProfile.getKakao_account().getEmail());
System.out.println("카카오로그인시 블로그서버 유저네임: "+kakaoProfile.getProperties().getNickname()+"_kakao");
System.out.println("카카오로그인시 블로그서버 이메일: "+kakaoProfile.getKakao_account().getEmail());
System.out.println("카카오로그인시 블로그서버 패스워드: "+pureKey);
User kakaoUser = User.builder()
.username(kakaoProfile.getProperties().getNickname()+"_kakao")
.password(pureKey)
.email(kakaoProfile.getKakao_account().getEmail())
.oauth("kakao")
.build();
//기가입, 미가입 분기 처리
User originUser = userService.회원찾기(kakaoUser.getUsername());
if(originUser.getUsername() == null) { //미가입자인 경우 바로 회원가입
System.out.println("미가입자 자동 회원가입 진행");
userService.회원가입(kakaoUser);
}
System.out.println("자동 로그인");
//세션 로그인 강제
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(kakaoUser.getUsername(), kakaoUser.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
return "redirect:/";
}
}
'취업 준비 > Spring boot' 카테고리의 다른 글
39. 댓글 DB에서 가져와서 출력 + 무한참조 방지 (0) | 2022.01.31 |
---|---|
38. 댓글 화면 디자인 (0) | 2022.01.31 |
36. 카카오 로그인 준비 (0) | 2022.01.29 |
35. 회원 정보 수정 (0) | 2022.01.28 |
34. 글 수정 (0) | 2022.01.28 |