From 3cea27c228b26e39d82b5353e0929236403d7fa0 Mon Sep 17 00:00:00 2001
From: miniplugin <52336625+miniplugin@users.noreply.github.com>
Date: Tue, 20 Aug 2024 14:41:43 +0900
Subject: [PATCH] =?UTF-8?q?SNS=20=EA=B0=84=ED=8E=B8=20=EB=A1=9C=EA=B7=B8?=
=?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=EC=B6=94=EA=B0=80=20(#54)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* SNS 간편 로그인 기능추가 (naver, kakao)
---
.../com/jwt/EgovJwtTokenUtil.java | 1 +
.../com/sns/SnsLoginApiController.java | 298 ++++++++++++++++++
.../java/egovframework/com/sns/SnsUtils.java | 72 +++++
.../java/egovframework/com/sns/SnsVO.java | 52 +++
src/main/resources/application.properties | 9 +-
5 files changed, 431 insertions(+), 1 deletion(-)
create mode 100644 src/main/java/egovframework/com/sns/SnsLoginApiController.java
create mode 100644 src/main/java/egovframework/com/sns/SnsUtils.java
create mode 100644 src/main/java/egovframework/com/sns/SnsVO.java
diff --git a/src/main/java/egovframework/com/jwt/EgovJwtTokenUtil.java b/src/main/java/egovframework/com/jwt/EgovJwtTokenUtil.java
index 3707b6e..870d1fd 100644
--- a/src/main/java/egovframework/com/jwt/EgovJwtTokenUtil.java
+++ b/src/main/java/egovframework/com/jwt/EgovJwtTokenUtil.java
@@ -72,6 +72,7 @@ public class EgovJwtTokenUtil implements Serializable {
claims.put("orgnztId", loginVO.getOrgnztId());
claims.put("uniqId", loginVO.getUniqId());
claims.put("type", subject);
+ claims.put("groupNm", loginVO.getGroupNm());//권한그룹으로 시프링시큐리티 사용
log.debug("===>>> secret = " + SECRET_KEY);
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
diff --git a/src/main/java/egovframework/com/sns/SnsLoginApiController.java b/src/main/java/egovframework/com/sns/SnsLoginApiController.java
new file mode 100644
index 0000000..3f7efd9
--- /dev/null
+++ b/src/main/java/egovframework/com/sns/SnsLoginApiController.java
@@ -0,0 +1,298 @@
+package egovframework.com.sns;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.URLEncoder;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import egovframework.com.cmm.EgovMessageSource;
+import egovframework.com.cmm.LoginVO;
+import egovframework.com.cmm.service.EgovProperties;
+import egovframework.com.jwt.EgovJwtTokenUtil;
+import egovframework.com.sns.SnsVO.NaverProfileVO;
+import egovframework.com.sns.SnsVO.NaverResponseVO;
+import egovframework.com.sns.SnsVO.NaverTokenVO;
+import egovframework.com.sns.SnsVO.KakaoTokenVO;
+import egovframework.com.sns.SnsVO.KakaoProfileVO;
+import egovframework.com.sns.SnsVO.KakaoResponseVO;
+import egovframework.let.uat.uia.service.EgovLoginService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+/**
+ * Sns 로그인을 처리하는 컨트롤러 클래스
+ * @네이버로그인API명세 : https://developers.naver.com/docs/login/api/api.md
+ * @네이버회원프로필조회API명세 : https://developers.naver.com/docs/login/profile/profile.md
+ * @version 1.0
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ------- -------- ---------------------------
+ * 2024.08.15 김일국 최초 생성
+ *
+ *
+ */
+@Slf4j
+@RestController
+@Tag(name="SnsLoginApiController",description = "Sns 로그인 관련")
+public class SnsLoginApiController {
+
+ /** EgovLoginService */
+ @Resource(name = "loginService")
+ private EgovLoginService loginService;
+ /** EgovMessageSource */
+ @Resource(name = "egovMessageSource")
+ EgovMessageSource egovMessageSource;
+ /** JWT */
+ @Autowired
+ private EgovJwtTokenUtil jwtTokenUtil;
+ public static final String HEADER_STRING = "Authorization";
+ /** SNS */
+ public static final String NAVER_CLIENT_ID = EgovProperties.getProperty("Sns.naver.clientId");
+ public static final String NAVER_CLIENT_SECRET = EgovProperties.getProperty("Sns.naver.clientSecret");
+ public static final String NAVER_CALLBACK_URL = EgovProperties.getProperty("Sns.naver.callbackUrl");
+ public static final String KAKAO_CLIENT_ID = EgovProperties.getProperty("Sns.kakao.clientId");
+ public static final String KAKAO_CALLBACK_URL = EgovProperties.getProperty("Sns.kakao.callbackUrl");
+
+ /**
+ * 카카오API 로그인
+ * @param HttpServletResponse
+ * @return void
+ */
+ @Operation(
+ summary = "카카오API 로그인",
+ description = "카카오API 로그인 처리",
+ tags = {"SnsLoginApiController"}
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "인증 성공"),
+ @ApiResponse(responseCode = "401", description = "인증 실패")
+ })
+ @GetMapping("/login/kakao")
+ public void getKakaoAuthUrl(HttpServletResponse response) throws IOException {
+ String clientId = KAKAO_CLIENT_ID;//카카오 애플리케이션 클라이언트 아이디값";
+ String redirectURI = URLEncoder.encode(KAKAO_CALLBACK_URL, "UTF-8");
+ String apiURL = "https://kauth.kakao.com/oauth/authorize?response_type=code";
+ apiURL += "&client_id=" + clientId;
+ apiURL += "&redirect_uri=" + redirectURI;
+ log.debug("apiURL="+ apiURL);
+ response.sendRedirect(apiURL);
+ }
+
+ /**
+ * 카카오API 로그인 콜백
+ * @param HttpServletResponse, HttpServletRequest
+ * @return HashMap
+ */
+ @Operation(
+ summary = "카카오API 로그인 콜백",
+ description = "카카오API 로그인 콜백처리",
+ tags = {"SnsLoginApiController"}
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "인증 성공"),
+ @ApiResponse(responseCode = "401", description = "인증 실패")
+ })
+ @GetMapping("/login/kakao/callback")
+ public HashMap getKakaoAuthCallback(HttpServletResponse response, HttpServletRequest request) throws Exception {
+ HashMap resultMap = new HashMap();
+ //카카오 로그인 인증 시작
+ String clientId = KAKAO_CLIENT_ID;//애플리케이션 클라이언트 아이디값";
+ String code = request.getParameter("code");
+ String redirectURI = URLEncoder.encode(KAKAO_CALLBACK_URL, "UTF-8");
+ String json_string=""; //3번사용
+ String responseBody="";//2번사용
+ Map requestHeaders = new HashMap<>();
+ String apiURL;
+ apiURL = "https://kauth.kakao.com/oauth/token?grant_type=authorization_code&";
+ apiURL += "client_id=" + clientId;
+ apiURL += "&redirect_uri=" + redirectURI;
+ apiURL += "&code=" + code;
+ log.debug("apiURL="+ apiURL);
+ requestHeaders.put("Authorization", null);
+ requestHeaders.put("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
+ requestHeaders.put("X-Http-Method-Override", "GET");
+ responseBody = SnsUtils.getOpenApi(apiURL,requestHeaders);
+ log.debug("responseBody="+ responseBody);
+ json_string = responseBody;//토큰 값 변수에 저장
+ //카카오 로그인 인증 끝
+ //카카오 프로필 정보 가져오기 시작
+ ObjectMapper objectMapper = new ObjectMapper();
+ KakaoTokenVO jsonToken = objectMapper.readValue(json_string, KakaoTokenVO.class);
+ String token = jsonToken.getAccess_token(); // 카카오 로그인 접근 토큰;
+ String header = "Bearer " + token; // Bearer 다음에 공백 추가
+ String openApiURL = "https://kapi.kakao.com/v2/user/me";
+ requestHeaders.put("Authorization", header);
+ requestHeaders.put("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
+ requestHeaders.put("Authorization", header);
+ requestHeaders.put("X-Http-Method-Override", "POST");
+ responseBody = SnsUtils.getOpenApi(openApiURL,requestHeaders);
+ log.debug("responseBody="+ responseBody);
+ json_string = responseBody;//프로필 값 변수에 저장
+ KakaoResponseVO jsonResponse = objectMapper.readValue(json_string, KakaoResponseVO.class);
+ log.debug("jsonProfile="+ jsonResponse.getId() + jsonResponse.getProperties());
+ //카카오 프로필 정보 가져오기 끝
+ //로그인 권한 부여 시작
+ if(!jsonResponse.getId().isEmpty()) {
+ ObjectMapper mapper = new ObjectMapper();
+ KakaoProfileVO jsonProfile = mapper.convertValue(jsonResponse.getProperties(), KakaoProfileVO.class);
+ log.debug("jsonProfile="+ jsonProfile);
+ log.debug("jsonProfile.getName()="+ jsonProfile.getNickname());
+ LoginVO loginVO = new LoginVO();
+ loginVO.setName(jsonProfile.getNickname());
+ loginVO.setId(jsonResponse.getId());
+ loginVO.setUniqId(jsonResponse.getId());
+ loginVO.setUserSe("SNS");
+ loginVO.setGroupNm("ROLE_USER");
+ loginVO.setOrgnztId(jsonResponse.getId());
+ String jwtToken = jwtTokenUtil.generateToken(loginVO);
+ String username = jwtTokenUtil.getUserSeFromToken(jwtToken);
+ log.debug("Dec jwtToken username = "+username);
+ String groupnm = jwtTokenUtil.getInfoFromToken("groupNm", jwtToken);
+ log.debug("Dec jwtToken groupnm = "+groupnm);//생성한 토큰에서 스프링시큐리티용 그룹명값 출력
+ //서버사이드 권한 체크 통과를 위해 삽입
+ request.getSession().setAttribute("LoginVO", loginVO);
+ resultMap.put("resultVO", loginVO);
+ resultMap.put("jToken", jwtToken);
+ resultMap.put("resultCode", "200");
+ resultMap.put("resultMessage", "성공 !!!");
+ }else {
+ resultMap.put("resultVO", null);
+ resultMap.put("resultCode", "300");
+ resultMap.put("resultMessage", egovMessageSource.getMessage("fail.common.login"));
+ }
+ //로그인 권한 부여 끝
+ return resultMap;
+ }
+
+ /**
+ * 네이버API 로그인
+ * @param HttpServletResponse
+ * @return void
+ */
+ @Operation(
+ summary = "네이버API 로그인",
+ description = "네이버API 로그인 처리",
+ tags = {"SnsLoginApiController"}
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "인증 성공"),
+ @ApiResponse(responseCode = "401", description = "인증 실패")
+ })
+ @GetMapping("/login/naver")
+ public void getNaverAuthUrl(HttpServletResponse response) throws IOException {
+ String clientId = NAVER_CLIENT_ID;//네이버 애플리케이션 클라이언트 아이디값";
+ String redirectURI = URLEncoder.encode(NAVER_CALLBACK_URL, "UTF-8");
+ SecureRandom random = new SecureRandom();
+ String state = new BigInteger(130, random).toString();
+ String apiURL = "https://nid.naver.com/oauth2.0/authorize?response_type=code";
+ apiURL += "&client_id=" + clientId;
+ apiURL += "&redirect_uri=" + redirectURI;
+ apiURL += "&state=" + state;
+ log.debug("apiURL="+ apiURL);
+ response.sendRedirect(apiURL);
+ }
+
+ /**
+ * 네이버API 로그인 콜백
+ * @param HttpServletResponse, HttpServletRequest
+ * @return HashMap
+ */
+ @Operation(
+ summary = "네이버API 로그인 콜백",
+ description = "네이버API 로그인 콜백처리",
+ tags = {"SnsLoginApiController"}
+ )
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "인증 성공"),
+ @ApiResponse(responseCode = "401", description = "인증 실패")
+ })
+ @GetMapping("/login/naver/callback")
+ public HashMap getNaverAuthCallback(HttpServletResponse response, HttpServletRequest request) throws Exception {
+ HashMap resultMap = new HashMap();
+ //네이버 로그인 인증 시작
+ String clientId = NAVER_CLIENT_ID;//애플리케이션 클라이언트 아이디값";
+ String clientSecret = NAVER_CLIENT_SECRET;//애플리케이션 클라이언트 시크릿값";
+ String code = request.getParameter("code");
+ String state = request.getParameter("state");
+ String redirectURI = URLEncoder.encode(NAVER_CALLBACK_URL, "UTF-8");
+ String json_string=""; //3번사용
+ String responseBody="";//2번사용
+ Map requestHeaders = new HashMap<>();
+ String apiURL;
+ apiURL = "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&";
+ apiURL += "client_id=" + clientId;
+ apiURL += "&client_secret=" + clientSecret;
+ apiURL += "&redirect_uri=" + redirectURI;
+ apiURL += "&code=" + code;
+ apiURL += "&state=" + state;
+ log.debug("apiURL="+ apiURL);
+ requestHeaders.put("Authorization", null);
+ requestHeaders.put("X-Http-Method-Override", "GET");
+ responseBody = SnsUtils.getOpenApi(apiURL,requestHeaders);
+ log.debug("responseBody="+ responseBody);
+ json_string = responseBody;//토큰 값 변수에 저장
+ //네이버 로그인 인증 끝
+ //네이버 프로필 정보 가져오기 시작
+ ObjectMapper objectMapper = new ObjectMapper();
+ NaverTokenVO jsonToken = objectMapper.readValue(json_string, NaverTokenVO.class);
+ String token = jsonToken.getAccess_token(); // 네이버 로그인 접근 토큰;
+ String header = "Bearer " + token; // Bearer 다음에 공백 추가
+ String openApiURL = "https://openapi.naver.com/v1/nid/me";
+ requestHeaders.put("Authorization", header);
+ requestHeaders.put("X-Http-Method-Override", "GET");
+ responseBody = SnsUtils.getOpenApi(openApiURL,requestHeaders);
+ log.debug("responseBody="+ responseBody);
+ json_string = responseBody;//프로필 값 변수에 저장
+ NaverResponseVO jsonResponse = objectMapper.readValue(json_string, NaverResponseVO.class);
+ log.debug("jsonProfile="+ jsonResponse.getResultcode() + jsonResponse.getMessage());
+ //네이버 프로필 정보 가져오기 끝
+ //로그인 권한 부여 시작
+ if(jsonResponse.getResultcode().equals("00")) {
+ ObjectMapper mapper = new ObjectMapper();
+ NaverProfileVO jsonProfile = mapper.convertValue(jsonResponse.getResponse(), NaverProfileVO.class);
+ log.debug("jsonProfile="+ jsonProfile);
+ log.debug("jsonProfile.getName()="+ jsonProfile.getName());
+ LoginVO loginVO = new LoginVO();
+ loginVO.setName(jsonProfile.getName());
+ loginVO.setId(jsonProfile.getEmail());
+ loginVO.setUniqId(jsonProfile.getEmail());
+ loginVO.setUserSe("SNS");
+ loginVO.setGroupNm("ROLE_USER");
+ loginVO.setOrgnztId(jsonProfile.getEmail());
+ String jwtToken = jwtTokenUtil.generateToken(loginVO);
+ String username = jwtTokenUtil.getUserSeFromToken(jwtToken);
+ log.debug("Dec jwtToken username = "+username);
+ String groupnm = jwtTokenUtil.getInfoFromToken("groupNm", jwtToken);
+ log.debug("Dec jwtToken groupnm = "+groupnm);//생성한 토큰에서 스프링시큐리티용 그룹명값 출력
+ //서버사이드 권한 체크 통과를 위해 삽입
+ request.getSession().setAttribute("LoginVO", loginVO);
+ resultMap.put("resultVO", loginVO);
+ resultMap.put("jToken", jwtToken);
+ resultMap.put("resultCode", "200");
+ resultMap.put("resultMessage", "성공 !!!");
+ }else {
+ resultMap.put("resultVO", null);
+ resultMap.put("resultCode", "300");
+ resultMap.put("resultMessage", egovMessageSource.getMessage("fail.common.login"));
+ }
+ //로그인 권한 부여 끝
+ return resultMap;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/egovframework/com/sns/SnsUtils.java b/src/main/java/egovframework/com/sns/SnsUtils.java
new file mode 100644
index 0000000..02a2cc4
--- /dev/null
+++ b/src/main/java/egovframework/com/sns/SnsUtils.java
@@ -0,0 +1,72 @@
+package egovframework.com.sns;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+
+/**
+ * Sns 로그인을 처리하는 공통 메서드 클래스
+ * @version 1.0
+ *
+ *
+ * << 개정이력(Modification Information) >>
+ *
+ * 수정일 수정자 수정내용
+ * ------- -------- ---------------------------
+ * 2024.08.15 김일국 최초 생성
+ *
+ *
+ */
+public class SnsUtils {
+
+ //Open Api 데이터 가져오기(공통)
+ public static String getOpenApi(String openApiUrl, Map requestHeaders){
+ HttpURLConnection con = connect(openApiUrl);
+ try {
+ for(Map.Entry header :requestHeaders.entrySet()) {
+ con.setRequestProperty(header.getKey(), header.getValue());
+ }
+ int responseCode = con.getResponseCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
+ return readBody(con.getInputStream());
+ } else { // 에러 발생
+ return readBody(con.getErrorStream());
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("API 요청과 응답 실패", e);
+ } finally {
+ con.disconnect();
+ }
+ }
+
+ //외부 URL 커넥션 호출(공통)
+ private static HttpURLConnection connect(String apiUrl){
+ try {
+ URL url = new URL(apiUrl);
+ return (HttpURLConnection)url.openConnection();
+ } catch (MalformedURLException e) {
+ throw new RuntimeException("API URL이 잘못되었습니다. : " + apiUrl, e);
+ } catch (IOException e) {
+ throw new RuntimeException("연결이 실패했습니다. : " + apiUrl, e);
+ }
+ }
+ //외부 프로필 내용 출력(공통)
+ private static String readBody(InputStream body){
+ InputStreamReader streamReader = new InputStreamReader(body);
+ try (BufferedReader lineReader = new BufferedReader(streamReader)) {
+ StringBuilder responseBody = new StringBuilder();
+ String line;
+ while ((line = lineReader.readLine()) != null) {
+ responseBody.append(line);
+ }
+ return responseBody.toString();
+ } catch (IOException e) {
+ throw new RuntimeException("API 응답을 읽는데 실패했습니다.", e);
+ }
+ }
+}
diff --git a/src/main/java/egovframework/com/sns/SnsVO.java b/src/main/java/egovframework/com/sns/SnsVO.java
new file mode 100644
index 0000000..efe87a1
--- /dev/null
+++ b/src/main/java/egovframework/com/sns/SnsVO.java
@@ -0,0 +1,52 @@
+package egovframework.com.sns;
+
+import lombok.Getter;
+
+class SnsVO {
+ /**
+ * 네이버용 토큰, 응답, 프로필 변수 VO 시작
+ */
+ @Getter
+ static class NaverTokenVO {
+ private String access_token;
+ private String refresh_token;
+ private String token_type;
+ private String expires_in;
+ }
+ @Getter
+ static class NaverResponseVO {
+ private String resultcode;
+ private String message;
+ private Object response;
+ }
+ @Getter
+ static class NaverProfileVO {
+ private String id;
+ private String email;
+ private String name;
+ }
+ /**
+ * 카카오용 토큰, 응답, 프로필 변수 VO 끝
+ */
+ @Getter
+ static class KakaoTokenVO {
+ private String access_token;
+ private String refresh_token;
+ private String token_type;
+ private String expires_in;
+ private String scope;
+ private String refresh_token_expires_in;
+ }
+ @Getter
+ static class KakaoResponseVO {
+ private String id;
+ private String connected_at;
+ private Object properties;
+ private Object kakao_account;
+ }
+ @Getter
+ static class KakaoProfileVO {
+ private String id;
+ private String nickname;
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 96766e4..579cc92 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -110,4 +110,11 @@ springdoc.swagger-ui.operations-sorter=alpha
springdoc.swagger-ui.doc-expansion=none
springdoc.api-docs.path=/v3/api-docs
springdoc.api-docs.groups.enabled=true
-springdoc.cache.disabled=true
\ No newline at end of file
+springdoc.cache.disabled=true
+
+#Sns \uac04\ud3b8\ub85c\uadf8\uc778 \ubcc0\uc218
+Sns.naver.clientId=YOUR_CLIENT_ID
+Sns.naver.clientSecret=YOUR_CLIENT_SECRET
+Sns.naver.callbackUrl=http://localhost:3000/login/naver/callback
+Sns.kakao.clientId=YOUR_CLIENT_ID
+Sns.kakao.callbackUrl=http://localhost:3000/login/kakao/callback
\ No newline at end of file