깃허브에서 전체 코드 보기 – https://github.com/ayaysir/spring-boot-security-example-1

보다 개선된 네이버 아이디로 로그인 – 스프링 부트(Spring Boot): 구글 로그인 연동 (스프링 부트 스타터의 oauth2-client) 이용 + 네이버 아이디로 로그인

 

이전글의 두 상황을 결합하여 네이버 아이디로 로그인(이하 네아로)을 스프링 시큐리티와 연결하는 예제입니다.

 

외부 소셜 로그인을 구현할 때 다음 상황이 있습니다.

  1. 기존에 사용자 계정이 존재하고, 네아로를 기존 로그인 체계로 연결
  2. 기존 사용자 계정과 연결하지 않고 네이버 아이디를 단독으로 사용

여기서는 1번을 다룹니다.

 

네아로 연결 시 구현 내용 및 주의사항들이 있습니다.

  1. 네아로 연동이 안되어 있다면, 네아로 연동하는 창을 띄운다.
    – 이 때, 로그인이 되어 있다면 기존 아이디를 네아로와 연동할 것인지 확인 여부를 물음
  2. 네아로 연동 안되어 있고 로그인이 되어 있지 않다면, 로그인 창(+회원가입 링크)으로 리다이렉트
    – 기존에 회원아이디로 로그인했다면 네아로 연동 과정을 계속 진행
    – 회원가입이 되지 않은 상태이며 이 회원 가입링크를 통해 가입한 경우 가입 완료하자마자 네아로 연동 과정 진행
  3. 네아로 연동이 되어 있다면 연동 정보를 통해 로그인 처리
    – 현재 로그인한 계정과 네아로 연결된 로그인 계정이 다른 경우, 현재 계정을 로그아웃하고 그 연결된 계정으로 재로그인

오늘은 1번을 구현하도록 하겠습니다. 3번은 지난 글에서 구현하였습니다.

 

1. 데이터베이스 구조

외부 로그인 연동 테이블(users_oauth)

username은 외래키로 회원 테이블의 키와 연결됩니다. provider는 제공사 이름으로, “naver“, “google” 등이 입력됩니다. unique_id에는 회원을 구분하는 고유값이 입력됩니다.

 

2. 컨트롤러, DAO 등 작성

// 로그인 컨트롤러: 전체 내용은 지난번 글 참고

@Autowired SimpleUserDAO sud;

/**
 * 콜백 페이지 컨트롤러
 * @param session
 * @param request
 * @param model
 * @return
 * @throws IOException
 * @throws ParseException
 */
@SuppressWarnings("unchecked")
@RequestMapping("/naver/callback1")
public String naverCallback1(HttpSession session, HttpServletRequest request, Model model) throws IOException, ParseException {

  String code = request.getParameter("code");
  String state = request.getParameter("state");
  String redirectURI = URLEncoder.encode(REDIRECT_URI, "UTF-8");

  UrlBuilder ub = new UrlBuilder("https://nid.naver.com/oauth2.0/token");
  ub
    .add("grant_type", "authorization_code")
    .add("client_id", CLIENT_ID)
    .add("client_secret", CLI_SECRET)
    .add("redirect_uri", redirectURI)
    .add("code", code)
    .add("state", state);
  System.out.println(ub);

  String apiURL = ub.toString();

  String res = requestToServer(apiURL);

  if(res != null && !res.equals("")) {
    Map<String, Object> parsedJson = new JSONParser(res).parseObject();
    if(parsedJson.get("access_token") != null) {
      
      // 
      String infoStr = getProfileFromNaver(parsedJson.get("access_token").toString());
      Map<String, Object> infoMap = new JSONParser(infoStr).parseObject();
      if(infoMap.get("message").equals("success")) {
        Map<String, Object> infoResp = (Map<String, Object>) infoMap.get("response");
        String uniqueId = infoResp.get("id").toString();
        System.out.println(uniqueId);
        List<Map<String, String>> infoOAuth = sud.getOAuthInfoByProviderAndUniqueId("naver", uniqueId);
        if(infoOAuth.size() == 1) {
          System.out.println(infoOAuth);
          // 네아로 연동이 되어 있다면 연동 정보를 통해 로그인 처리
          // - 현재 로그인한 계정과 네아로 연결된 로그인 계정이 다른 경우, 현재 계정을 로그아웃하고 그 연결된 계정으로 재로그인
          loginWithoutForm(infoOAuth.get(0).get("username"));
          model.addAttribute("isConnectedToNaver", true);
        } else {
          System.out.println("네이버 연동 정보 없음");
          // 로그인이 되어 있다면 기존 아이디를 네아로와 연동할 것인지 확인 여부를 물음
          model.addAttribute("isConnectedToNaver", false);
          model.addAttribute("uniqueIdOfNaver", uniqueId);
        }
      }

      session.setAttribute("currentNaverUser", res);
      session.setAttribute("currentAT", parsedJson.get("access_token"));
      session.setAttribute("currentRT", parsedJson.get("refresh_token"));

      model.addAttribute("res", res);
    } else {
      model.addAttribute("res", "Login failed!");
    }
    System.out.println(parsedJson);
  } else {
    model.addAttribute("res", "Login failed!");
  }
  return "test-naver-callback";
}

/**
 * 네이버 계정을 users_oauth 테이블에 할당
 * @param session
 * @param auth
 * @param model
 * @param uniqueId
 * @return
 */
@PostMapping("/oauth/assign/naver")
public String addRowToOAuthTableForNaver(HttpSession session, Authentication auth, Model model, String uniqueId) {
  String username = auth.getName();
  String provider = "naver";
  List<Map<String, String>> infoOAuth = sud.getOAuthInfoByProviderAndUniqueId(provider, uniqueId);
  int resultCode = 0;
  if(infoOAuth.size() == 0) {
    Map<String, String> aRow = new HashMap<>();
    aRow.put("username", username);
    aRow.put("provider", provider);
    aRow.put("unique_id", uniqueId);
    resultCode = sud.insertAnUserOAuth(aRow);
    if(resultCode <= 0) {
      session.setAttribute("currentNaverUser", null);
    }
    model.addAttribute("task", "assign-naver");
    model.addAttribute("resultCode", resultCode);
  }
  return "redirect:/";
}

연동이 되어있지 않은 상황이라면 isConnectedToNaver와 uniqueIdOfNaver 정보를 Model 속성에 담아 뷰 페이지로 넘깁니다. 밑의 뷰 페이지의 test-naver-callback.html에 연결되는 내용이 있습니다.  addRowToOAuthTableForNaver 메소드에서는 이미 연동된 네이버 아이디인지 여부를 검색하고 연동이 안되어있으면 연동 정보 레코드를 추가하는 작업을 진행합니다. 사실 그 전에 이미 연동 여부를 naverCallback1 메소드에서 이미 체크한 상태이므로 여기서 다시 검사하지 않아도 상관은 없습니다.

 

 

SimpleUserDAO: 전체 내용은 지난 글 참고

/**
 * 
 * @param aRow
 * @return
 */
public int insertAnUserOAuth(Map<String, String> aRow) {
  String sql = "insert into users_oauth"
      + "(seq, username, provider, unique_id, reg_date, last_date) "
      + "values(0, ?, ?, ?, sysdate(), sysdate())";
  
  return jt.update(sql, 
      aRow.get("username"),
      aRow.get("provider"),
      aRow.get("unique_id")
    );
  
}

users_oauth 테이블에 연동 정보를 추가하는 부분입니다.

 

// 메인 컨트롤러의 일부	

@Autowired SimpleUserDAO sud;

@RequestMapping("/")
public String home(Model model, HttpSession session) throws Exception {

  Object currentAuth = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  UserDetails principal = null;

  if(!(currentAuth instanceof String)) {
    principal = (UserDetails) currentAuth;
    List<Map<String, ?>> userInfo = sud.getUserInfo(principal.getUsername());
    String food = (String) userInfo.get(0).get("food");
    model.addAttribute("food", food);
    LoginController lc = new LoginController();

    // 메인 화면에 네이버 연동 버튼 추가
    List<Map<String, String>> infoOAuth = sud.getOAuthInfoByProviderAndUsername("naver", principal.getUsername());
    if(infoOAuth.size() == 0) {
      String apiURL = lc.getNaverOAuthURI(session);
      model.addAttribute("naverApiURL", apiURL);
    }
  }

  return "home";	
}

연동이 안되어 있다면 메인화면에 연동하기 버튼을 추가합니다. 이미 연동된 상태라면 해당 부분은 나타나지 않습니다.

 

3. 뷰 페이지(Thymeleaf) 작성

home.html (메인 페이지)
......
<body>
  ......
  
    
  <div class="row" th:if="${naverApiURL ne null}">
    <div class="col-12">
      <h3>현재 계정을 네이버와 연동하시겠습니까? 이 부분은 연동이 되지 않았을 때에만 나타납니다.</h3>
          <a th:href="${naverApiURL}"><img height="50" src="http://static.nid.naver.com/oauth/small_g_in.PNG" /></a>
    </div>
  </div>

  ......

  

</body>

 

test-naver-callback.html (네이버 콜백 페이지)
......

<body>
  ......
  
  <th:block sec:authorize="isAuthenticated()">
    <p th:text="${isConnectedToNaver}"></p>
    <div th:unless="${isConnectedToNaver}">
      <h5>현재 아이디를 네이버 아이디와 연동하시겠습니까?</h5>
      <a>[NO]</a> 
      <form th:action="@{/oauth/assign/naver}" method="POST">
        <input type="hidden" th:value="${uniqueIdOfNaver}" name="uniqueId">
        <button>[YES]</button>
      </form>
    </div>
    <div th:if="${isConnectedToNaver}">
      <h5>현재 아이디 연동이 되어 있습니다.</h5>
    </div>
  </th:block>
</body>

 

4. 테스트

처음에는 테이블이 비어있습니다. (연동 전)

 

guest1 아이디로 로그인하면 네이버 연동 여부를 묻는 부분이 나타납니다.

 

빨간 네모박스로 연동 여부를 물어봅니다.

 

[YES] 버튼을 누르면 연동 작업이 진행됩니다. 테이블에 연동 정보가 추가되었습니다.

네이버 계정과 연동되었으므로 아까 그 부분은 더 이상 나오지 않습니다.

 

guest1로 로그인하는 장면입니다.

 

Spring GIF - Find & Share on GIPHY

 

문의 | 코멘트 또는 yoonbumtae@gmail.com




0개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다