병원 테마의 회원제 게시판 사이트를 만드는 개인 프로젝트를 할 때, OAuth 2.0을 이용한 로그인을 구현해보고 싶었다. 네이버, 구글, 페이스북, 카카오 등 여러 곳 중 카카오로 도전을 했었다.
카카오톡 로그인에 대한 정보가 Spring Boot는 많았지만 Security가 적용된 Framework에 대해서는 찾기 힘들어서 프로젝트 기간 내에 구현하지 못 했었는데, 이번에 이전 최종 팀플 때 했던 네이버 로그인과 비교해가며 구현해 냈다.
개발 환경
- STS 3.9.11
- Tomcat 9.0
🍍 1. 초반 설정
- 카카오 개발자센터에서 애플리케이션 등록을 한다.
developers.kakao.com/console/app
- 입력 후 생성을 하고 요약정보를 누르면 발급된 앱키를 확인 할 수 있다.
- 플랫폼 메뉴에서 Web을 추가한다. 로컬pc가 개발 서버이기 때문에.. 운영 서버에 배포하면 그에 맞는 도메인을 입력해줘야 한다.
- 카카오 로그인을 ON으로 활성화 시키고, Redirect URI를 설정한다. (제일 중요)
로그인 버튼을 눌러서 앱키를 넘겨 주면 코드값을 받을 주소를 입력하는 곳
🍏 2. jsp 설정 (customLogin.jsp)
- 로그인 페이지에 카카오 로그인 버튼을 넣고, 카카오 문서에 나와 있는 대로 스크립트 코드도 넣어 준다.
<div class="col-lg-12 text-center mt-3">
<button class="btn btn-block waves-effect waves-light btn-rounded btn-outline-info mb-3">로그인하기</button>
<img alt="카카오로그인" src="${pageContext.request.contextPath}/resources/assets/img/kakao_login_medium_wide.png" onclick="loginWithKakao()">
</div>
<!-- 카카오 로그인 -->
<script type="text/javascript" src="https://developers.kakao.com/sdk/js/kakao.min.js" charset="utf-8"></script>
<script type="text/javascript">
$(document).ready(function(){
Kakao.init('script앱키 입력');
Kakao.isInitialized();
});
function loginWithKakao() {
Kakao.Auth.authorize({
redirectUri: 'http://localhost:8080/kakao_callback'
}); // 등록한 리다이렉트uri 입력
}
</script>
- 카카오 로그인을 누르면 실행이 되면서 서버에서 코드값을 받아서 넘어 오고, 이 코드값으로 토큰값을 받아와야 한다. 토큰값을 받는 건 REST API를 이용해야 한다.
🍎 3. KakaoService.java & KakaoController.java
서비스와 컨트롤러를 만들기 위한 코드는 https://hdhdeveloper.tistory.com/47를 참고했다.
KakaoService.java
package site.levinni.service;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.extern.log4j.Log4j;
@Service
@Log4j
public class KakaoService {
public String getReturnAccessToken(String code) {
String access_token = "";
String refresh_token = "";
String reqURL = "https://kauth.kakao.com/oauth/token";
try {
URL url = new URL(reqURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//HttpURLConnection 설정 값 셋팅
conn.setRequestMethod("POST");
conn.setDoOutput(true);
// buffer 스트림 객체 값 셋팅 후 요청
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
StringBuilder sb = new StringBuilder();
sb.append("grant_type=authorization_code");
sb.append("&client_id=REST API 앱키 입력"); //앱 KEY VALUE
sb.append("&redirect_uri=http://localhost:8080/kakao_callback"); // 앱 CALLBACK 경로
sb.append("&code=" + code);
bw.write(sb.toString());
bw.flush();
// RETURN 값 result 변수에 저장
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String br_line = "";
String result = "";
while ((br_line = br.readLine()) != null) {
result += br_line;
}
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(result);
// 토큰 값 저장 및 리턴
access_token = element.getAsJsonObject().get("access_token").getAsString();
refresh_token = element.getAsJsonObject().get("refresh_token").getAsString();
br.close();
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
return access_token;
}
public Map<String,Object> getUserInfo(String access_token) {
Map<String,Object> resultMap =new HashMap<>();
String reqURL = "https://kapi.kakao.com/v2/user/me";
try {
URL url = new URL(reqURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
//요청에 필요한 Header에 포함될 내용
conn.setRequestProperty("Authorization", "Bearer " + access_token);
int responseCode = conn.getResponseCode();
System.out.println("responseCode : " + responseCode);
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String br_line = "";
String result = "";
while ((br_line = br.readLine()) != null) {
result += br_line;
}
System.out.println("response:" + result);
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(result);
log.warn("element:: " + element);
JsonObject properties = element.getAsJsonObject().get("properties").getAsJsonObject();
JsonObject kakao_account = element.getAsJsonObject().get("kakao_account").getAsJsonObject();
log.warn("id:: "+element.getAsJsonObject().get("id").getAsString());
String id = element.getAsJsonObject().get("id").getAsString();
String nickname = properties.getAsJsonObject().get("nickname").getAsString();
String email = kakao_account.getAsJsonObject().get("email").getAsString();
log.warn("email:: " + email);
resultMap.put("nickname", nickname);
resultMap.put("id", id);
resultMap.put("email", email);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return resultMap;
}
}
🔶 id로 넘어오는 값을 vo의 snsId로 이용해서 회원 가입이 된 이용자면 로그인 처리를, 그렇지 않으면 회원가입을 시키도록 할 것이다.
🔶 카카오로 받아오는 정보에 닉네임(이름)과 이메일을 db에 저장할 것이라 변수에 담았다.
KakaoController.java
package site.levinni.controller;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j;
import site.levinni.domain.MemberVO;
import site.levinni.security.domain.CustomUser;
import site.levinni.service.KakaoService;
import site.levinni.service.MemberService;
@Controller
@Log4j
@AllArgsConstructor
public class KakaoController {
private KakaoService kakaoService;
private MemberService memberService;
@RequestMapping(value = "/kakao_callback", method = RequestMethod.GET)
public String redirectkakao(@RequestParam String code, HttpSession session) throws IOException {
System.out.println("code:: " + code);
// 접속토큰 get
String kakaoToken = kakaoService.getReturnAccessToken(code);
// 접속자 정보 get
Map<String, Object> result = kakaoService.getUserInfo(kakaoToken);
log.info("result:: " + result);
String snsId = (String) result.get("id");
String userName = (String) result.get("nickname");
String email = (String) result.get("email");
String userpw = snsId;
// 분기
MemberVO memberVO = new MemberVO();
// 일치하는 snsId 없을 시 회원가입
System.out.println(memberService.kakaoLogin(snsId));
if (memberService.kakaoLogin(snsId) == null) {
log.warn("카카오로 회원가입");
memberVO.setUserid(email);
memberVO.setUserpw(userpw);
memberVO.setUserName(userName);
memberVO.setSnsId(snsId);
memberVO.setEmail(email);
memberService.kakaoJoin(memberVO);
}
// 일치하는 snsId가 있으면 멤버객체에 담음.
log.warn("카카오로 로그인");
String userid = memberService.findUserIdBy2(snsId);
MemberVO vo = memberService.findByUserId(userid);
log.warn("member:: " + vo);
/*Security Authentication에 붙이는 과정*/
CustomUser user = new CustomUser(vo);
log.warn("user : " + user);
List<GrantedAuthority> roles = CustomUser.getList(vo);
Authentication auth = new UsernamePasswordAuthenticationToken(user, null, roles);
log.warn("auth : " + auth);
SecurityContextHolder.getContext().setAuthentication(auth);
/* 로그아웃 처리 시, 사용할 토큰 값 */
session.setAttribute("kakaoToken", kakaoToken);
return "redirect:/";
}
}
🔶 OAuth로 받아온 사용자 정보를 시큐리티에 접목하는 게 어려웠었는데 auth를 직접 조작할 수 있었다. 참고: http://yoonbumtae.com/?p=1841
🍄 그 외
관련 된 클래스들이다.
MemberVO.java
package site.levinni.domain;
import java.util.Date;
import java.util.List;
import lombok.Data;
@Data
public class MemberVO {
private String userid;
private String userpw;
private String userName;
private Date regDate;
private String email;
private String tel;
private List<AuthVO> authList;
private String snsId;
}
MemberMapper.java
package site.levinni.mapper;
import org.apache.ibatis.annotations.Select;
import site.levinni.domain.MemberVO;
public interface MemberMapper {
MemberVO read(String userid); // 회원 정보 조회
void register(MemberVO vo); // 회원 가입
void authorize (MemberVO memberVO); // 회원 권한
void modify(MemberVO vo); // 회원 정보 수정
void remove(MemberVO vo); // 회원 탈퇴
int checkPw(String userid, String userpw); // 수정 및 삭제를 위한 비밀번호 체크
/* 카카오 로그인 */
// 카카오 회원가입
void kakaoInsert(MemberVO memberVO);
//snsId로 회원정보얻기
@Select("SELECT USERID, USERNAME, EMAIL, TEL FROM TBL_MEMBER WHERE SNSID = #{snsId}")
MemberVO kakaoSelect(String snsId);
//snsId로 회원 아이디찾기
@Select("SELECT USERID FROM TBL_MEMBER WHERE SNSID = #{snsId}")
String findUserIdBy2(String snsId);
//회원아이디로 권한찾기
@Select("SELECT AUTH FROM TBL_MEMBER_AUTH WHERE USERID = #{userid}")
String findAuthBy(String userid);
}
MemberMapper.xml 일부
<insert id="kakaoInsert">
INSERT INTO TBL_MEMBER (USERID, USERPW, USERNAME, EMAIL, SNSID) VALUES
(#{userid}, #{userpw}, #{userName}, #{email}, #{snsId})
</insert>
MemberServiceImpl.java 일부
package site.levinni.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.log4j.Log4j;
import site.levinni.domain.MemberVO;
import site.levinni.mapper.MemberMapper;
@Service
@Log4j
public class MemberServiceImpl implements MemberService{
@Autowired
private MemberMapper mapper;
@Autowired @Qualifier("BCryptPasswordEncoder")
private PasswordEncoder encoder;
/* 카카오 로그인 */
@Override
public void kakaoJoin(MemberVO memberVO) {
mapper.kakaoInsert(memberVO);
String userid = memberVO.getUserid();
log.info("userid:: " + userid);
mapper.authorize(memberVO);
}
@Override
public MemberVO kakaoLogin(String snsId) {
log.info("snsId:: " + snsId);
return mapper.kakaoSelect(snsId);
}
@Override
public String findAuthBy(String userid) {
log.info("userid:: " + userid);
return mapper.findAuthBy(userid);
}
@Override
public String findUserIdBy2(String snsId) {
log.info("snsId:: " + snsId);
return mapper.findUserIdBy2(snsId);
}
@Override
public MemberVO findByUserId(String userid) {
// TODO Auto-generated method stub
return mapper.read(userid);
}
}
'JAVA > JAVA-Project' 카테고리의 다른 글
[스프링] 독서실 관리 프로그램- 관리자 페이지에서의 좌석 등록 1 21. 05. 31. (0) | 2021.05.31 |
---|---|
테이블 3개 조인하기. (관리자-좌석관리에서 좌석 배치도에 정보 입력 하기 위함) 21. 05. 29. (0) | 2021.05.29 |
[스프링] 독서실 관리 프로그램- 좌석 변경 21. 05. 27. (0) | 2021.05.27 |
[스프링] 독서실 관리 프로그램- 이용 기간 연장하기 21. 06. 02. 수정 (0) | 2021.05.26 |
[스프링] 독서실 관리 프로그램 1인 1좌석 자바스크립트 21. 05. 26. (0) | 2021.05.26 |