본문 바로가기

JAVA/JAVA-Project

[Spring Framework] OAuth2.0 카카오톡 로그인/회원가입 (Spring Security 적용)

병원 테마의 회원제 게시판 사이트를 만드는 개인 프로젝트를 할 때, OAuth 2.0을 이용한 로그인을 구현해보고 싶었다. 네이버, 구글, 페이스북, 카카오 등 여러 곳 중 카카오로 도전을 했었다.
카카오톡 로그인에 대한 정보가 Spring Boot는 많았지만 Security가 적용된 Framework에 대해서는 찾기 힘들어서 프로젝트 기간 내에 구현하지 못 했었는데, 이번에 이전 최종 팀플 때 했던 네이버 로그인과 비교해가며 구현해 냈다.

개발 환경

  • STS 3.9.11
  • Tomcat 9.0

 


 

🍍 1. 초반 설정

  • 입력 후 생성을 하고 요약정보를 누르면 발급된 앱키를 확인 할 수 있다.
  • 플랫폼 메뉴에서 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);
    }
}