From b60d296fb7ed58559e04593914aa63d5f66692f4 Mon Sep 17 00:00:00 2001 From: JSYOO Date: Fri, 13 Aug 2021 15:48:40 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=B6=80=EB=B6=84=20js=ED=98=95?= =?UTF-8?q?=ED=83=9C=EC=9D=98=20sns=EC=9D=B8=EC=A6=9D=EC=97=90=EC=84=9C=20?= =?UTF-8?q?java=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 102 ++++--- .../nlib/cmm/snslogin/GoogleOAuthRequest.java | 90 +++++++ .../cmm/snslogin/GoogleOAuthResponse.java | 49 ++++ .../nlib/cmm/snslogin/KakaoController.java | 81 ++++++ .../java/nlib/cmm/snslogin/NaverLoginApi.java | 29 ++ .../java/nlib/cmm/snslogin/NaverLoginBO.java | 102 +++++++ .../nlib/security/SpringSecurityConfig.java | 4 +- .../java/nlib/user/web/LoginController.java | 249 ++++++++++++++---- .../java/nlib/user/web/MemberController.java | 225 +++++++++++++--- .../egovframework/spring/context-common.xml | 3 +- .../WEB-INF/jsp/nlib/login/loginForm.jsp | 162 +----------- .../WEB-INF/jsp/nlib/member/snsCertForm.jsp | 161 +---------- .../jsp/nlib/popup/naverLoginResultPopup.jsp | 50 ---- .../jsp/nlib/popup/naverSignResultPopup.jsp | 56 ---- .../webapp/images/nlib/sign-in/g-normal.png | Bin 529 -> 0 bytes .../images/nlib/sns/google_login_btn.png | Bin 0 -> 6734 bytes .../images/nlib/sns/kakao_login_btn.png | Bin 0 -> 6598 bytes .../images/nlib/sns/naver_login_btn.png | Bin 0 -> 2925 bytes 18 files changed, 818 insertions(+), 545 deletions(-) create mode 100644 src/main/java/nlib/cmm/snslogin/GoogleOAuthRequest.java create mode 100644 src/main/java/nlib/cmm/snslogin/GoogleOAuthResponse.java create mode 100644 src/main/java/nlib/cmm/snslogin/KakaoController.java create mode 100644 src/main/java/nlib/cmm/snslogin/NaverLoginApi.java create mode 100644 src/main/java/nlib/cmm/snslogin/NaverLoginBO.java delete mode 100644 src/main/webapp/WEB-INF/jsp/nlib/popup/naverLoginResultPopup.jsp delete mode 100644 src/main/webapp/WEB-INF/jsp/nlib/popup/naverSignResultPopup.jsp delete mode 100644 src/main/webapp/images/nlib/sign-in/g-normal.png create mode 100644 src/main/webapp/images/nlib/sns/google_login_btn.png create mode 100644 src/main/webapp/images/nlib/sns/kakao_login_btn.png create mode 100644 src/main/webapp/images/nlib/sns/naver_login_btn.png diff --git a/pom.xml b/pom.xml index bdf6c4f5..c877eb56 100644 --- a/pom.xml +++ b/pom.xml @@ -125,22 +125,22 @@ egovframework.rte.fdl.crypto ${egovframework.rte.version} - - - org.apache.poi - poi - 5.0.0 - - - org.apache.poi - poi-ooxml - 3.9 - - - commons-httpclient - commons-httpclient - 3.1 - + + + org.apache.poi + poi + 5.0.0 + + + org.apache.poi + poi-ooxml + 3.9 + + + commons-httpclient + commons-httpclient + 3.1 + @@ -191,7 +191,29 @@ jackson-dataformat-xml 2.9.0 - + + + com.github.scribejava + scribejava-core + 2.8.1 + + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + + org.apache.httpcomponents + httpcore + 4.4.6 + + + org.apache.httpcomponents + httpclient + 4.5.3 + com.google.zxing @@ -266,29 +288,29 @@ commons-dbcp 1.4 - - org.lazyluke - log4jdbc-remix - 0.2.7 - - - org.projectlombok - lombok - 1.18.20 - provided - - - - - com.google.code.gson - gson - 2.8.7 - - - org.json - json - 20201115 - + + org.lazyluke + log4jdbc-remix + 0.2.7 + + + org.projectlombok + lombok + 1.18.20 + provided + + + + + com.google.code.gson + gson + 2.8.7 + + + org.json + json + 20201115 + diff --git a/src/main/java/nlib/cmm/snslogin/GoogleOAuthRequest.java b/src/main/java/nlib/cmm/snslogin/GoogleOAuthRequest.java new file mode 100644 index 00000000..9e7d6eb2 --- /dev/null +++ b/src/main/java/nlib/cmm/snslogin/GoogleOAuthRequest.java @@ -0,0 +1,90 @@ +package nlib.cmm.snslogin; + +public class GoogleOAuthRequest { + private String redirectUri; + private String clientId; + private String clientSecret; + private String code; + private String responseType; + private String scope; + private String accessType; + private String grantType; + private String state; + private String includeGrantedScopes; + private String loginHint; + private String prompt; + + public String getRedirectUri() { + return redirectUri; + } + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + public String getClientId() { + return clientId; + } + public void setClientId(String clientId) { + this.clientId = clientId; + } + public String getClientSecret() { + return clientSecret; + } + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + public String getCode() { + return code; + } + public void setCode(String code) { + this.code = code; + } + public String getResponseType() { + return responseType; + } + public void setResponseType(String responseType) { + this.responseType = responseType; + } + public String getScope() { + return scope; + } + public void setScope(String scope) { + this.scope = scope; + } + public String getAccessType() { + return accessType; + } + public void setAccessType(String accessType) { + this.accessType = accessType; + } + public String getGrantType() { + return grantType; + } + public void setGrantType(String grantType) { + this.grantType = grantType; + } + public String getState() { + return state; + } + public void setState(String state) { + this.state = state; + } + public String getIncludeGrantedScopes() { + return includeGrantedScopes; + } + public void setIncludeGrantedScopes(String includeGrantedScopes) { + this.includeGrantedScopes = includeGrantedScopes; + } + public String getLoginHint() { + return loginHint; + } + public void setLoginHint(String loginHint) { + this.loginHint = loginHint; + } + public String getPrompt() { + return prompt; + } + public void setPrompt(String prompt) { + this.prompt = prompt; + } + +} \ No newline at end of file diff --git a/src/main/java/nlib/cmm/snslogin/GoogleOAuthResponse.java b/src/main/java/nlib/cmm/snslogin/GoogleOAuthResponse.java new file mode 100644 index 00000000..6adb25b0 --- /dev/null +++ b/src/main/java/nlib/cmm/snslogin/GoogleOAuthResponse.java @@ -0,0 +1,49 @@ +package nlib.cmm.snslogin; + + +public class GoogleOAuthResponse { + private String accessToken; + private String expiresIn; + private String refreshToken; + private String scope; + private String tokenType; + private String idToken; + + public String getAccessToken() { + return accessToken; + } + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + public String getExpiresIn() { + return expiresIn; + } + public void setExpiresIn(String expiresIn) { + this.expiresIn = expiresIn; + } + public String getRefreshToken() { + return refreshToken; + } + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + public String getScope() { + return scope; + } + public void setScope(String scope) { + this.scope = scope; + } + public String getTokenType() { + return tokenType; + } + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + public String getIdToken() { + return idToken; + } + public void setIdToken(String idToken) { + this.idToken = idToken; + } + +} \ No newline at end of file diff --git a/src/main/java/nlib/cmm/snslogin/KakaoController.java b/src/main/java/nlib/cmm/snslogin/KakaoController.java new file mode 100644 index 00000000..55732d1b --- /dev/null +++ b/src/main/java/nlib/cmm/snslogin/KakaoController.java @@ -0,0 +1,81 @@ +package nlib.cmm.snslogin; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.springframework.stereotype.Controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +@Controller +public class KakaoController { + + private final static String K_CLIENT_ID = "e74d25210853313dd1fead4c6c1f06ec"; + + public static String getAuthorizationUrl(HttpSession session,String K_REDIRECT_URI) { + String kakaoUrl = "https://kauth.kakao.com/oauth/authorize?" + "client_id=" + K_CLIENT_ID + "&redirect_uri=" + + K_REDIRECT_URI + "&response_type=code"; + return kakaoUrl; + } + + public static JsonNode getAccessToken(String autorize_code,String K_REDIRECT_URI) { + final String RequestUrl = "https://kauth.kakao.com/oauth/token"; + final List postParams = new ArrayList(); + postParams.add(new BasicNameValuePair("grant_type", "authorization_code")); + postParams.add(new BasicNameValuePair("client_id", K_CLIENT_ID)); // REST API KEY + postParams.add(new BasicNameValuePair("redirect_uri",K_REDIRECT_URI)); + // 리다이렉트 URI + postParams.add(new BasicNameValuePair("code",autorize_code)); // 로그인 과정중 얻은 code 값 + final HttpClient client = HttpClientBuilder.create().build(); + final HttpPost post = new HttpPost(RequestUrl); + JsonNode returnNode = null; + try { + post.setEntity(new UrlEncodedFormEntity(postParams)); + final HttpResponse response = client.execute(post); + // JSON 형태 반환값 처리 + ObjectMapper mapper = new ObjectMapper(); + returnNode = mapper.readTree(response.getEntity().getContent()); + } catch (UnsupportedEncodingException e){ + e.printStackTrace(); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); } + finally { + // clear resources + } return returnNode; + } + + public static JsonNode getKakaoUserInfo(JsonNode accessToken) { + final String RequestUrl = "https://kapi.kakao.com/v2/user/me"; + final HttpClient client = HttpClientBuilder.create().build(); + final HttpPost post = new HttpPost(RequestUrl); + // add header + post.addHeader("Authorization", "Bearer " + accessToken); + JsonNode returnNode = null; + try { + final HttpResponse response = client.execute(post); + // JSON 형태 반환값 처리 + ObjectMapper mapper = new ObjectMapper(); + returnNode = mapper.readTree(response.getEntity().getContent()); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + // clear resources + } return returnNode; + } +} \ No newline at end of file diff --git a/src/main/java/nlib/cmm/snslogin/NaverLoginApi.java b/src/main/java/nlib/cmm/snslogin/NaverLoginApi.java new file mode 100644 index 00000000..992e3d8e --- /dev/null +++ b/src/main/java/nlib/cmm/snslogin/NaverLoginApi.java @@ -0,0 +1,29 @@ +package nlib.cmm.snslogin; + +import com.github.scribejava.core.builder.api.DefaultApi20; + +public class NaverLoginApi extends DefaultApi20{ + + protected NaverLoginApi(){ + } + + private static class InstanceHolder{ + private static final NaverLoginApi INSTANCE = new NaverLoginApi(); + } + + + public static NaverLoginApi instance(){ + return InstanceHolder.INSTANCE; + } + + @Override + public String getAccessTokenEndpoint() { + return "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code"; + } + + @Override + protected String getAuthorizationBaseUrl() { + return "https://nid.naver.com/oauth2.0/authorize"; + } + +} diff --git a/src/main/java/nlib/cmm/snslogin/NaverLoginBO.java b/src/main/java/nlib/cmm/snslogin/NaverLoginBO.java new file mode 100644 index 00000000..126ab0d9 --- /dev/null +++ b/src/main/java/nlib/cmm/snslogin/NaverLoginBO.java @@ -0,0 +1,102 @@ +package nlib.cmm.snslogin; + +import java.io.IOException; +import java.util.UUID; + +import javax.servlet.http.HttpSession; + +import org.springframework.util.StringUtils; + +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; + +public class NaverLoginBO { + + /* 인증 요청문을 구성하는 파라미터 */ + //client_id: 애플리케이션 등록 후 발급받은 클라이언트 아이디 + //response_type: 인증 과정에 대한 구분값. code로 값이 고정돼 있습니다. + //redirect_uri: 네이버 로그인 인증의 결과를 전달받을 콜백 URL(URL 인코딩). 애플리케이션을 등록할 때 Callback URL에 설정한 정보입니다. + //state: 애플리케이션이 생성한 상태 토큰 + private final static String CLIENT_ID = "jefkoYhSfrQ3TtZz5mTp"; + private final static String CLIENT_SECRET = "E2qAGaLEaG"; + private static String REDIRECT_URI = ""; + private final static String SESSION_STATE = "oauth_state"; + /* 프로필 조회 API URL */ + private final static String PROFILE_API_URL = "https://openapi.naver.com/v1/nid/me"; + + public void setRedirect_url(String REDIRECT_URI) { + this.REDIRECT_URI = REDIRECT_URI; + } + /* 네이버 아이디로 인증 URL 생성 Method */ + public String getAuthorizationUrl(HttpSession session) { + + /* 세션 유효성 검증을 위하여 난수를 생성 */ + String state = generateRandomString(); + /* 생성한 난수 값을 session에 저장 */ + setSession(session,state); + System.out.println(getSession(session)); + + /* Scribe에서 제공하는 인증 URL 생성 기능을 이용하여 네아로 인증 URL 생성 */ + OAuth20Service oauthService = new ServiceBuilder() + .apiKey(CLIENT_ID) + .apiSecret(CLIENT_SECRET) + .callback(REDIRECT_URI) + .state(state) //앞서 생성한 난수값을 인증 URL생성시 사용함 + .build(NaverLoginApi.instance()); + + return oauthService.getAuthorizationUrl(); + } + /* 네이버아이디로 Callback 처리 및 AccessToken 획득 Method */ + public OAuth2AccessToken getAccessToken(HttpSession session, String code, String state) throws IOException{ + + /* Callback으로 전달받은 세선검증용 난수값과 세션에 저장되어있는 값이 일치하는지 확인 */ + String sessionState = getSession(session); + if(StringUtils.pathEquals(sessionState, state)){ + + OAuth20Service oauthService = new ServiceBuilder() + .apiKey(CLIENT_ID) + .apiSecret(CLIENT_SECRET) + .callback(REDIRECT_URI) + .state(state) + .build(NaverLoginApi.instance()); + + /* Scribe에서 제공하는 AccessToken 획득 기능으로 네아로 Access Token을 획득 */ + OAuth2AccessToken accessToken = oauthService.getAccessToken(code); + return accessToken; + } + return null; + } + + /* 세션 유효성 검증을 위한 난수 생성기 */ + private String generateRandomString() { + return UUID.randomUUID().toString(); + } + + /* http session에 데이터 저장 */ + private void setSession(HttpSession session,String state){ + session.setAttribute(SESSION_STATE, state); + } + + /* http session에서 데이터 가져오기 */ + private String getSession(HttpSession session){ + return (String) session.getAttribute(SESSION_STATE); + } + /* Access Token을 이용하여 네이버 사용자 프로필 API를 호출 */ + public String getUserProfile(OAuth2AccessToken oauthToken) throws IOException{ + + OAuth20Service oauthService =new ServiceBuilder() + .apiKey(CLIENT_ID) + .apiSecret(CLIENT_SECRET) + .callback(REDIRECT_URI).build(NaverLoginApi.instance()); + + OAuthRequest request = new OAuthRequest(Verb.GET, PROFILE_API_URL, oauthService); + oauthService.signRequest(oauthToken, request); + Response response = request.send(); + return response.getBody(); + } + +} diff --git a/src/main/java/nlib/security/SpringSecurityConfig.java b/src/main/java/nlib/security/SpringSecurityConfig.java index 9591c3e3..731b2acb 100644 --- a/src/main/java/nlib/security/SpringSecurityConfig.java +++ b/src/main/java/nlib/security/SpringSecurityConfig.java @@ -58,8 +58,6 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { .antMatchers("/inform/**") .antMatchers("/alert/**") .antMatchers("/code/**") - .antMatchers("/test/**") - .antMatchers("/login/**") ; } @@ -69,6 +67,8 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { .antMatchers("/login/login*.do").permitAll() .antMatchers("/login/logout*.do").permitAll() .antMatchers("/login/naver*.do").permitAll() + .antMatchers("/login/google*.do").permitAll() + .antMatchers("/login/kakao*.do").permitAll() .antMatchers("/member/*.do").permitAll() .antMatchers("/member/*.ajax").permitAll() .antMatchers("/userInfo/*.do").permitAll() diff --git a/src/main/java/nlib/user/web/LoginController.java b/src/main/java/nlib/user/web/LoginController.java index b4114df6..19dfa406 100644 --- a/src/main/java/nlib/user/web/LoginController.java +++ b/src/main/java/nlib/user/web/LoginController.java @@ -1,39 +1,66 @@ package nlib.user.web; +import java.io.IOException; +import java.net.URI; +import java.util.Map; + import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.ui.ModelMap; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.util.UriComponentsBuilder; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.github.scribejava.core.model.OAuth2AccessToken; + +import nlib.cmm.snslogin.GoogleOAuthResponse; +import nlib.cmm.snslogin.KakaoController; +import nlib.cmm.snslogin.NaverLoginBO; import nlib.user.service.LoginService; import nlib.util.StringUtil; /** *
- * @Class Name : LoginController.java
+ * @Class Name : LoginController.java
  * 
- * @Description : 스프링 시큐리티를 이용한 로그인 처리 컨트롤러 
+ * @Description : 스프링 시큐리티를 이용한 로그인 처리 컨트롤러 
  * 
  * 
- * @프로젝트명: 지방문화원 통합자료관리시스템 구축사업 (2021)
+ * @프로젝트명: 지방문화원 통합자료관리시스템 구축사업 (2021)
  *
  * 
* - * @ ------------ -------- --------------------------- - * @ 수정일 수정자 수정내용 - * @ ------------ -------- --------------------------- - * @ 2021. 7. 9. KNKIM 최초 생성 + * @ ------------ -------- --------------------------- @ 수정일 수정자 수정내용 @ + * ------------ -------- --------------------------- @ 2021. 7. 9. KNKIM 최초 생성 * * * @author 이씨플라자 * DIGITALSHIP KNKIM @@ -42,16 +69,24 @@ import nlib.util.StringUtil; * */ @Controller -public class LoginController -{ +public class LoginController { private static final Logger log = LoggerFactory.getLogger(LoginController.class); - - + + /* NaverLoginBO */ + private NaverLoginBO naverLoginBO; + private String apiResult = null; + + @Autowired + private void setNaverLoginBO(NaverLoginBO naverLoginBO) { + this.naverLoginBO = naverLoginBO; + } + @Resource(name = "loginService") private LoginService loginService; - + + /** - * 로그인 정보 입력 화면을 출력한다. + * 로그인 정보 입력 화면을 출력한다. * * @param req * @param error @@ -60,25 +95,141 @@ public class LoginController * @return */ @RequestMapping("/login/loginForm.do") - public String loginForm( - HttpServletRequest req - , @RequestParam(required=false) String error - , @RequestParam(required=false) String logout - , ModelMap model) { - - if(error != null) { - model.addAttribute("error" , String.format("ID와 비밀번호를 확인하여 주시기 바랍니다. %s", error)); + public String loginForm(HttpServletRequest req, @RequestParam(required = false) String error, + @RequestParam(required = false) String logout, Model model, HttpSession session) { + if (error != null) { + model.addAttribute("error", String.format("ID와 비밀번호를 확인하여 주시기 바랍니다. %s", error)); } - if(logout != null) { - model.addAttribute("logout" , String.format("로그아웃하였습니다 %s", logout)); + if (logout != null) { + model.addAttribute("logout", String.format("로그아웃하였습니다 %s", logout)); } + // 네이버 로그인 URL 생성 + /* 네이버아이디로 인증 URL을 생성하기 위하여 naverLoginBO클래스의 getAuthorizationUrl메소드 호출 */ + naverLoginBO.setRedirect_url("http://nlib.nculture.org/nlib/login/naverCallback.do"); + String naverAuthUrl = naverLoginBO.getAuthorizationUrl(session); + // 구글 로그인 URL 생성 + String googleUrl = "https://accounts.google.com/o/oauth2/v2/auth?" + + "client_id=879126511006-jro7bld7b2epl3n5mkksp0p24k2inbpu.apps.googleusercontent.com" + + "&redirect_uri=http://nlib.nculture.org/nlib/login/googleCallback.do" + + "&response_type=code" + + "&scope=email%20profile%20openid" + + "&access_type=offline"; + + // 카카오 로그인 URL 생성 + String k_redirect_url="http://nlib.nculture.org/nlib/login/kakaoCallback.do"; + String kakaoUrl = KakaoController.getAuthorizationUrl(session,k_redirect_url); + + model.addAttribute("naverUrl", naverAuthUrl); + model.addAttribute("googleUrl", googleUrl); + model.addAttribute("kakaoUrl", kakaoUrl); return "nlib/login/loginForm"; } + // 네이버 로그인 성공시 callback호출 메소드 + @RequestMapping(value = "/login/naverCallback.do", method = { RequestMethod.GET, RequestMethod.POST }) + public String naverCallback(Model model, @RequestParam String code, @RequestParam String state, HttpSession session) + throws IOException, ParseException { + OAuth2AccessToken oauthToken; + oauthToken = naverLoginBO.getAccessToken(session, code, state); + // 로그인 사용자 정보를 읽어온다. + apiResult = naverLoginBO.getUserProfile(oauthToken); + JSONParser parser = new JSONParser(); + Object obj = parser.parse(apiResult); + JSONObject jsonObj = (JSONObject) obj; + JSONObject response_obj = (JSONObject) jsonObj.get("response"); + String email = (String) response_obj.get("email"); + model.addAttribute("result", apiResult); + + /* 네이버 로그인 성공 페이지 View 호출 */ + return "naverSuccess"; + } + + // 구글 로그인 콜백 + @RequestMapping(value = "/login/googleCallback.do") + public String googleCallback(@RequestParam(value = "code") String authCode, HttpSession session, Model model, + HttpServletRequest request) throws Exception { + + String code = request.getParameter("code"); + HttpHeaders headers = new HttpHeaders(); + RestTemplate restTemplate = new RestTemplate(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("code", code); + parameters.add("client_id", "879126511006-jro7bld7b2epl3n5mkksp0p24k2inbpu.apps.googleusercontent.com"); + parameters.add("client_secret", "F6T-ZV9jibLhKtcugxPUtBlN"); + parameters.add("redirect_uri", "http://nlib.nculture.org/nlib/login/googleCallback.do"); + parameters.add("grant_type", "authorization_code"); + + HttpEntity> rest_request = new HttpEntity<>(parameters,headers); + + URI uri = URI.create("https://www.googleapis.com/oauth2/v4/token"); + + ResponseEntity resultEntity; + resultEntity = restTemplate.postForEntity(uri, rest_request, String.class); + // JSON 파싱을 위한 기본값 세팅 + // 요청시 파라미터는 스네이크 케이스로 세팅되므로 Object mapper에 미리 설정해준다. + ObjectMapper mapper = new ObjectMapper(); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + mapper.setSerializationInclusion(Include.NON_NULL); + // Token Request + GoogleOAuthResponse result = mapper.readValue(resultEntity.getBody(), new TypeReference() { + }); + + // ID Token만 추출 (사용자의 정보는 jwt로 인코딩 되어있다) + String jwtToken = result.getIdToken(); + String requestUrl = UriComponentsBuilder.fromHttpUrl("https://oauth2.googleapis.com/tokeninfo") + .queryParam("id_token", jwtToken).toUriString(); + + String resultJson = restTemplate.getForObject(requestUrl, String.class); + + Map userInfo = mapper.readValue(resultJson, new TypeReference>() { + }); + System.out.println(userInfo.get("email")); + model.addAttribute("token", result.getAccessToken()); + + return "redirect:/"; + } + @RequestMapping(value = "/login/kakaoCallback.do") + public ModelAndView kakaoCallback(@RequestParam("code") String code, HttpServletRequest request, HttpServletResponse response, HttpSession session) + throws Exception { + ModelAndView mav = new ModelAndView(); + // 결과값을 node에 담아줌 + String k_redirect_url="http://nlib.nculture.org/nlib/login/kakaoCallback.do"; + JsonNode node = KakaoController.getAccessToken(code,k_redirect_url); + // accessToken에 사용자의 로그인한 모든 정보가 들어있음 + JsonNode accessToken = node.get("access_token"); + // 사용자의 정보 + JsonNode userInfo = KakaoController.getKakaoUserInfo(accessToken); + String kemail = null; + String kname = null; + String kgender = null; + String kbirthday = null; + String kage = null; + String kimage = null; + // 유저정보 카카오에서 가져오기Get properties + JsonNode properties = userInfo.path("properties"); + JsonNode kakao_account = userInfo.path("kakao_account"); + kemail = kakao_account.path("email").asText(); + kname = properties.path("nickname").asText(); + kimage = properties.path("profile_image").asText(); + kgender = kakao_account.path("gender").asText(); + kbirthday = kakao_account.path("birthday").asText(); + kage = kakao_account.path("age_range").asText(); + session.setAttribute("kemail", kemail); + session.setAttribute("kname", kname); + session.setAttribute("kimage", kimage); + session.setAttribute("kgender", kgender); + session.setAttribute("kbirthday", kbirthday); + session.setAttribute("kage", kage); + mav.setViewName("main"); + return mav; + }// end kakaoLogin() + + /** - * 로그인을 처리한다. - * (스프링 시큐리티 로그인 수행) + * 로그인을 처리한다. (스프링 시큐리티 로그인 수행) * * @param req * @param res @@ -88,51 +239,35 @@ public class LoginController * @return */ @RequestMapping("/login/login.do") - public String login( - HttpServletRequest req - , HttpServletResponse res - , @RequestParam(required=false) String username - , @RequestParam(required=false) String password - , ModelMap model) { + public String login(HttpServletRequest req, HttpServletResponse res, + @RequestParam(required = false) String username, @RequestParam(required = false) String password, + ModelMap model) { String redirectUrl = loginService.login(req, username, password); - - // 정상적으로 로그인된 경우 - if(StringUtil.isEmpty(redirectUrl)) { - RequestCache cache = new HttpSessionRequestCache(); - SavedRequest savedRequest = cache.getRequest(req, res); - if(savedRequest == null) redirectUrl = "/"; - else redirectUrl = savedRequest.getRedirectUrl(); + + // 정상적으로 로그인된 경우 + if (StringUtil.isEmpty(redirectUrl)) { + RequestCache cache = new HttpSessionRequestCache(); + SavedRequest savedRequest = cache.getRequest(req, res); + if (savedRequest == null) + redirectUrl = "/"; + else + redirectUrl = savedRequest.getRedirectUrl(); } log.debug("login.do > RequestCache : url=" + redirectUrl); - - return "redirect:" + redirectUrl; + + return "redirect:" + redirectUrl; } - - /** - * 로그아웃 후의 처리를 담당한다. - * 스프링 시큐리티에서 로그아웃이 수행된 후, 호출된다. + * 로그아웃 후의 처리를 담당한다. 스프링 시큐리티에서 로그아웃이 수행된 후, 호출된다. * * @param req * @return */ @RequestMapping("/login/logout.do") - public String logout(HttpServletRequest req) { + public String logout(HttpServletRequest req) { return null; } - - /** - * 네이버 로그인 - * @param session - * @return - * @throws Exception - */ - @RequestMapping(value="/login/naverLoginResultPopup.do") - public String naverLoginResult(HttpSession session) throws Exception{ - - return "nlib/popup/naverLoginResultPopup"; - } } \ No newline at end of file diff --git a/src/main/java/nlib/user/web/MemberController.java b/src/main/java/nlib/user/web/MemberController.java index 05d68f1b..32498a1f 100644 --- a/src/main/java/nlib/user/web/MemberController.java +++ b/src/main/java/nlib/user/web/MemberController.java @@ -2,6 +2,7 @@ package nlib.user.web; import java.io.IOException; +import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -10,6 +11,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Random; import javax.annotation.Resource; @@ -17,14 +19,40 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +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.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.ui.ModelMap; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.servlet.support.RequestContextUtils; +import org.springframework.web.util.UriComponentsBuilder; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.github.scribejava.core.model.OAuth2AccessToken; + +import nlib.cmm.snslogin.GoogleOAuthResponse; +import nlib.cmm.snslogin.KakaoController; +import nlib.cmm.snslogin.NaverLoginBO; import nlib.info.service.InformService; import nlib.restful.service.DataApiReqVO; import nlib.restful.service.DataApiResVO; @@ -53,6 +81,7 @@ import nlib.user.service.NlibLoginVO; * @version 1.0 * */ + @Controller public class MemberController { @Resource(name="memberService") @@ -61,6 +90,15 @@ public class MemberController { @Resource(name="informService") private InformService informService; + /* NaverLoginBO */ + private NaverLoginBO naverLoginBO; + private String apiResult = null; + + @Autowired + private void setNaverLoginBO(NaverLoginBO naverLoginBO) { + this.naverLoginBO = naverLoginBO; + } + /* 이메일 메시지 및 템플릿 정보 */ //템플릿 파일경로 @Value("#{properties['mailing.sender.membership.template']}") @@ -74,34 +112,12 @@ public class MemberController { @Value("#{properties['mailing.sender.membership.name']}") private String senderName; - public ModelMap selectMemberJoiningInfo(HttpServletRequest req) { - return null; - } - public ModelMap certificateMember(HttpServletRequest req) { return null; } - - public ModelMap insertMemberInfo(HttpServletRequest req) { - return null; - } - public ModelMap sendEmailForMemberJoining(HttpServletRequest req) { return null; } - - public ModelMap searchIdForm(HttpServletRequest req) { - return null; - } - - public ModelMap searchId(HttpServletRequest req) { - return null; - } - - public ModelMap initPassword(HttpServletRequest req) { - return null; - } - public ModelMap changePassword(HttpServletRequest req) { return null; } @@ -122,28 +138,46 @@ public class MemberController { return null; } - @RequestMapping(value="/member/naverSignResultPopup.do") - public String naverSignResult(HttpSession session) throws Exception{ - return "nlib/popup/naverSignResultPopup"; - } - @RequestMapping(value="/member/selectMemberJoiningInfo.do") public String selectMemberJoiningInfo(HttpServletRequest req,ModelMap model) { - DataApiReqVO reqVO = new DataApiReqVO(); - DataApiResVO resVO = new DataApiResVO(); - resVO = informService.selectTermsInfo(reqVO); - System.out.println(resVO.getInfo().get("terms")); - model.addAttribute("result",resVO); return "/nlib/member/selectMemberJoiningInfo"; } @RequestMapping(value="/member/snsCertForm.do") - public String snsCertForm(HttpSession session) throws Exception{ + public String snsCertForm(HttpSession session, Model model) throws Exception{ + // 네이버 로그인 URL 생성 + /* 네이버아이디로 인증 URL을 생성하기 위하여 naverLoginBO클래스의 getAuthorizationUrl메소드 호출 */ + naverLoginBO.setRedirect_url("http://nlib.nculture.org/nlib/member/naverCallback.do"); + String naverAuthUrl = naverLoginBO.getAuthorizationUrl(session); + + // 구글 로그인 URL 생성 + String googleUrl = "https://accounts.google.com/o/oauth2/v2/auth?" + + "client_id=879126511006-jro7bld7b2epl3n5mkksp0p24k2inbpu.apps.googleusercontent.com" + + "&redirect_uri=http://nlib.nculture.org/nlib/member/googleCallback.do" + + "&response_type=code" + + "&scope=email%20profile%20openid" + + "&access_type=offline"; + + // 카카오 로그인 URL 생성 + String k_redirect_url="http://nlib.nculture.org/nlib/member/kakaoCallback.do"; + String kakaoUrl = KakaoController.getAuthorizationUrl(session,k_redirect_url); + + model.addAttribute("naverUrl", naverAuthUrl); + model.addAttribute("googleUrl", googleUrl); + model.addAttribute("kakaoUrl", kakaoUrl); return "nlib/member/snsCertForm"; } @RequestMapping(value="/member/insertMemberInfoForm.do") - public String insertMemberInfoForm(NlibLoginVO vo,HttpSession session,ModelMap model ) throws Exception{ + public String insertMemberInfoForm(HttpServletRequest request,NlibLoginVO vo,HttpSession session,ModelMap model ) throws Exception{ + Map flashMap =RequestContextUtils.getInputFlashMap(request); + + if(flashMap != null) + { + vo.setId(String.valueOf(flashMap.get("email"))); + }else { + return "forward:/member/selectMemberJoiningInfo.do"; + } model.addAttribute("loginVO",vo); return "nlib/member/insertMemberInfoForm"; } @@ -286,4 +320,127 @@ public class MemberController { System.out.println(request.getAttribute("initPasswordPhoneNum")); return "nlib/member/initPassword"; } + + /** + * 회원가입 SNS 네이버인증 + * @param model + * @param code + * @param state + * @param session + * @return + * @throws IOException + * @throws ParseException + */ + @RequestMapping(value = "/member/naverCallback.do", method = { RequestMethod.GET, RequestMethod.POST }) + public String naverCallback(Model model, @RequestParam String code, @RequestParam String state, HttpSession session,RedirectAttributes rttr) + throws IOException, ParseException { + OAuth2AccessToken oauthToken; + oauthToken = naverLoginBO.getAccessToken(session, code, state); + // 로그인 사용자 정보를 읽어온다. + apiResult = naverLoginBO.getUserProfile(oauthToken); + JSONParser parser = new JSONParser(); + Object obj = parser.parse(apiResult); + JSONObject jsonObj = (JSONObject) obj; + JSONObject response_obj = (JSONObject) jsonObj.get("response"); + String email = (String) response_obj.get("email"); + rttr.addFlashAttribute("email",email); + /* 네이버 로그인 성공 페이지 View 호출 */ + return "redirect:/member/insertMemberInfoForm.do"; + } + + /** + * 회원가입 SNS 구글인증 + * @exception Exception + */ + @RequestMapping(value = "/member/googleCallback.do") + public String googleCallback(@RequestParam(value = "code") String authCode, HttpSession session, Model model, + HttpServletRequest request,RedirectAttributes rttr) throws Exception { + + String code = request.getParameter("code"); + HttpHeaders headers = new HttpHeaders(); + RestTemplate restTemplate = new RestTemplate(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("code", code); + parameters.add("client_id", "879126511006-jro7bld7b2epl3n5mkksp0p24k2inbpu.apps.googleusercontent.com"); + parameters.add("client_secret", "F6T-ZV9jibLhKtcugxPUtBlN"); + parameters.add("redirect_uri", "http://nlib.nculture.org/nlib/member/googleCallback.do"); + parameters.add("grant_type", "authorization_code"); + + HttpEntity> rest_request = new HttpEntity<>(parameters,headers); + + URI uri = URI.create("https://www.googleapis.com/oauth2/v4/token"); + + ResponseEntity resultEntity; + resultEntity = restTemplate.postForEntity(uri, rest_request, String.class); + // JSON 파싱을 위한 기본값 세팅 + // 요청시 파라미터는 스네이크 케이스로 세팅되므로 Object mapper에 미리 설정해준다. + ObjectMapper mapper = new ObjectMapper(); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + mapper.setSerializationInclusion(Include.NON_NULL); + // Token Request + GoogleOAuthResponse result = mapper.readValue(resultEntity.getBody(), new TypeReference() { + }); + + // ID Token만 추출 (사용자의 정보는 jwt로 인코딩 되어있다) + String jwtToken = result.getIdToken(); + String requestUrl = UriComponentsBuilder.fromHttpUrl("https://oauth2.googleapis.com/tokeninfo") + .queryParam("id_token", jwtToken).toUriString(); + + String resultJson = restTemplate.getForObject(requestUrl, String.class); + + Map userInfo = mapper.readValue(resultJson, new TypeReference>() { + }); + + rttr.addFlashAttribute("email",userInfo.get("email")); + return "redirect:/member/insertMemberInfoForm.do"; + } + + /** + * 회원가입 SNS 카카오인증 + * @param code + * @param request + * @param response + * @param session + * @return + * @throws Exception + */ + @RequestMapping(value = "/member/kakaoCallback.do") + public String kakaoCallback(@RequestParam("code") String code, HttpServletRequest request, HttpServletResponse response, HttpSession session + ,RedirectAttributes rttr) + throws Exception { + ModelAndView mav = new ModelAndView(); + // 결과값을 node에 담아줌 + String k_redirect_url="http://nlib.nculture.org/nlib/member/kakaoCallback.do"; + JsonNode node = KakaoController.getAccessToken(code,k_redirect_url); + // accessToken에 사용자의 로그인한 모든 정보가 들어있음 + JsonNode accessToken = node.get("access_token"); + // 사용자의 정보 + JsonNode userInfo = KakaoController.getKakaoUserInfo(accessToken); + String kemail = null; + String kname = null; + String kgender = null; + String kbirthday = null; + String kage = null; + String kimage = null; + // 유저정보 카카오에서 가져오기Get properties + JsonNode properties = userInfo.path("properties"); + JsonNode kakao_account = userInfo.path("kakao_account"); + kemail = kakao_account.path("email").asText(); + kname = properties.path("nickname").asText(); + kimage = properties.path("profile_image").asText(); + kgender = kakao_account.path("gender").asText(); + kbirthday = kakao_account.path("birthday").asText(); + kage = kakao_account.path("age_range").asText(); + session.setAttribute("kemail", kemail); + session.setAttribute("kname", kname); + session.setAttribute("kimage", kimage); + session.setAttribute("kgender", kgender); + session.setAttribute("kbirthday", kbirthday); + session.setAttribute("kage", kage); + mav.setViewName("main"); + rttr.addFlashAttribute("email",kemail); + return "redirect:/member/insertMemberInfoForm.do"; + }// end kakaoLogin() } \ No newline at end of file diff --git a/src/main/resources/egovframework/spring/context-common.xml b/src/main/resources/egovframework/spring/context-common.xml index 2a5cd8aa..8ab98143 100644 --- a/src/main/resources/egovframework/spring/context-common.xml +++ b/src/main/resources/egovframework/spring/context-common.xml @@ -65,7 +65,8 @@ - + + diff --git a/src/main/webapp/WEB-INF/jsp/nlib/login/loginForm.jsp b/src/main/webapp/WEB-INF/jsp/nlib/login/loginForm.jsp index 0c7222d9..5cfc396e 100644 --- a/src/main/webapp/WEB-INF/jsp/nlib/login/loginForm.jsp +++ b/src/main/webapp/WEB-INF/jsp/nlib/login/loginForm.jsp @@ -34,20 +34,11 @@ LoginTest - - - + - -
- - - - - - - - - - -
-
-
- -
- - - - - +
+ + + +
+ +
+
+ + +
\ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/nlib/member/snsCertForm.jsp b/src/main/webapp/WEB-INF/jsp/nlib/member/snsCertForm.jsp index b1d6a349..365d29b2 100644 --- a/src/main/webapp/WEB-INF/jsp/nlib/member/snsCertForm.jsp +++ b/src/main/webapp/WEB-INF/jsp/nlib/member/snsCertForm.jsp @@ -56,166 +56,17 @@ -LoginTest - - - - -SNS 인증
-
-
- - - - - -
- - - - - - -
- - - - - +SNS 인증 +
+ +
+ +
diff --git a/src/main/webapp/WEB-INF/jsp/nlib/popup/naverLoginResultPopup.jsp b/src/main/webapp/WEB-INF/jsp/nlib/popup/naverLoginResultPopup.jsp deleted file mode 100644 index cc7807ad..00000000 --- a/src/main/webapp/WEB-INF/jsp/nlib/popup/naverLoginResultPopup.jsp +++ /dev/null @@ -1,50 +0,0 @@ -<% -/** - *
- * @Class Name : naverLoginResultPopup.jsp
- * 
- * @Description : 로그인시 naver로그인 페이지
- * 
- * 
- * @프로젝트명: 지방문화원 통합자료관리시스템 구축사업 (2021)
- *
- * 
- * - * @ ------------ -------- --------------------------- - * @ 수정일 수정자 수정내용 - * @ ------------ -------- --------------------------- - * @ 2021. 7. 12. JSYOO 최초 생성 - * - * - * @author 이씨플라자 * DIGITALSHIP JSYOO - * @since 2021. 7. 12. - * @version 1.0 - * - */ - %> -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> - - - - - - - - diff --git a/src/main/webapp/WEB-INF/jsp/nlib/popup/naverSignResultPopup.jsp b/src/main/webapp/WEB-INF/jsp/nlib/popup/naverSignResultPopup.jsp deleted file mode 100644 index 9f009a7c..00000000 --- a/src/main/webapp/WEB-INF/jsp/nlib/popup/naverSignResultPopup.jsp +++ /dev/null @@ -1,56 +0,0 @@ -<% -/** - *
- * @Class Name : naverSignResultPopup.jsp
- * 
- * @Description : 회원가입시 naver로그인 페이지
- * 
- * 
- * @프로젝트명: 지방문화원 통합자료관리시스템 구축사업 (2021)
- *
- * 
- * - * @ ------------ -------- --------------------------- - * @ 수정일 수정자 수정내용 - * @ ------------ -------- --------------------------- - * @ 2021. 7. 12. JSYOO 최초 생성 - * - * - * @author 이씨플라자 * DIGITALSHIP JSYOO - * @since 2021. 7. 12. - * @version 1.0 - * - */ - %> -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> - - - - - - - - diff --git a/src/main/webapp/images/nlib/sign-in/g-normal.png b/src/main/webapp/images/nlib/sign-in/g-normal.png deleted file mode 100644 index 79e972c74817aabefa3ec68f78e2268c8752ee67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 529 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 literal 0 HcmV?d00001 diff --git a/src/main/webapp/images/nlib/sns/kakao_login_btn.png b/src/main/webapp/images/nlib/sns/kakao_login_btn.png new file mode 100644 index 0000000000000000000000000000000000000000..e2c7b0354fbbfbed5a632027bc7fee15486e0446 GIT binary patch literal 6598 zcma)h1yoeg*YBl~9=bsq>5>pcx6Q`+8A7C`yQHOCy1(mt zZ>_h!wf^6FZ_QnE=iGhw+57A`d(ZDgywp^}$Dzgn003V_SzZSKQ0c(mPS}{>H^#y^ zA$UW#lhKd?fSOp`TT2XZ%xJBwqX7WEYyc4a0RXPSN5NYF;K>gFJFfviA{78ATr-=r zB|!qFm70<~@bK@G(^8xW0F;d?@-nZy=k^x@ymilVv5pSK#Z?l83t2TBa{0a`!#={2 z1ZCV9oAlh4`{_OPMeC*VG#hp$8X`Jw-d&_Q@3bHX{Gb;b2eiIj4H~2q?L~$V1uP7i z4|>KS$thzgBgs!47KgqEB^8Mm@;M~9C$KFIpIoLU#VsAure_{rwcteN1%LebQQ6U| znd^nTynJa$ZYVhRXdjUQM@_G{7@3%u%=kTsAdq-oN{N53QsB{nqyk*dMY56ry^l~b z@S)C7^X{odF&A%czSoVl*CnDAIAl+0=by#u;_=8 z{EKyuneq(dY=7*c4V?n)CP4PX9z)||D)!@j{$NR|o!tJtPjl&hqWN}m&JQQF&s;~N zTOTr_@n}Iw#>5IgGK*U8G0)!hR#be4*7BZ>GzHH=gOZ~TQl8vo+u5Ng=u!%U3tW{U zx~}Y)HF_+)7g+lBd?8zFwCUZxiNvkO@Z%92Kv@W-3nM*68JshpgLezbiw2-p1h*X< zRrF6h_wc^p+Y3rf$@g&p>7;Cvp0miHkg4_Uk+IR#sfUp_lg+}QCve8mO?pWzT8wu8 zej}s{nOfe$Pt-mc1BG*_caMmFd7rVd?!X|C6jIv81oeH)#H31Dra~7fy?XJ8!Ww`; zAltD2Z~ul9fC9%b6k^ox?#Sj{;)^h;ldrf{InpZHz*g`d>2s$o!0mj?j&BXjp;a@^$_e zw{smm9p)ePC5AEf+z>^8A>1?KmYnxd%a%_qx0M&>zK-{@cpkAQmu-Wt|QoLLcl;jwZ&# zT>dfS`^sRx#<|1F<5WHJf)fmZu~(|$2=i?G;TA8d`>5o|sx){T?rlb7??3Xu_fAB!b~xH$A^p>uSCqY^GX@~G*V0?hei6;y65CL@hzV31 z-*OC0JDchub!9Wg@c@or&~~epZ&$}Glh5JCs%ay+FMdqTb$Oq2va^gP@`o@h6TKEr zt6p>Q^bXkY|E+Tct=x?Np}$~8?SI=jm!FOi>-3D)5cw1fsJ1D^EiWhRxfu~D+&rF| zoOiW2n^-;>%y>Tf7ARC57^)CbCazCc<1%R($_Q;HZBSrIr*`@SYS}Uur6x z=OZfmEPONjpgnBUA{sXe+JjV0LQh9zxaBzi6qnX&z~p!LEiQsO-9ayvs4F;l-#hrG zSQxm-Ihs(9RRoHCl}fKSwJ+Rmq#AA&6EC|cX&#ICUi&ODkG@9Vz@;O@#cu6KyRa~= zVBWU@xY`ENweOZd!-U)6g_U`L^}v4Jil|mf{?$=nK^n1rzToh?p6cCYQ&WM93U34Ub*DM| z4n2bzrX~^`QCA?qBG_c{YtJ9vvkBy5tq{pf#hc?2lne#7TFO& zfo~BiMq1Ja!DqWdaH(vJG zpQVY7zO$K*=uUX<(Hr>zDxDcwc`m0c#6Ba2Siwd`T{Le#L6G6T(FpFEAUmXH{p8^Y zs3kWk`VY1SYzSb4uSDx>8l+fG@O9Ks?!YWSJ4f<)<}}*p@badhz+;V%H&be1RSPX# z_|gHl%_TpB=XpNs2Kt0yk*!O4@9syxF7F35@m_7v&A)~%ef3Ou;yOboSb4unmAU=& z^|+$R(I%mE0-Sxy*J{k)+;~IIGS{VZT0NWGr(p<8CP*yLci*coC9ztEzJ@$C+jHf! zZ@YGOav$FHPpOm*&w!y|ShGipbK<8B23WX;c>A($?PTP4cQ&_ym~e2-nUZF(3rACa z$|P?CTXlKu_WRf-*D<5ulfML=NC_3&QP{cmp+6R(VDxFfKxWiVzM~q)@8* zP9c5=COwwZ1;()3pkDFO7?&=L;&1^ub~G*O?zZ+f5%Q_c2`;C1FXxyZx_RBb&aTHp z0>(?DrH>7dvU9^n&tV&^>=Xm*fFvx{nwLst*u1FQR zAg!`8iQ_sGtYnr{feM=Pa%8+^1~pz8jYYrJ*2v2-K7y$^XW99_aU%S+e8<9UnS_y1 zf1J7-wN?j~aF)Vk?J0T<=^S2N#Kf*U@0W4Sg+hIYo&wn&*O29O? zN5^=B<{VYp-IqPMdHJO2x&~T9D<(EVETXeJ!g^w6FE%lqRcq%W@g!yE{WX(55}Nl` zQnhTF*_4D0JufhJ)ruk0&P2?owm7SRkj^;;Pkb4x+j8qADazTI%7az`vWB1$`C8Ei z&4)QHohC+w;A~;hkavUOU_eJI_UUc3@Qfh&F6_g+L{i3XOcS@1f?o|6&&kR!t|1l; zsXVglw3!B%%L9=HlUMjpV~V=s6_Za5gpHEVX2kSKV1Tz|rVzc>I$1uSVBNv24+0RegNUwSS?Z&psfMk4Xj^I%VNhE5lz(fM$${FvVF@~Gr&>0YM(ExEFu zo{g^8E)8Z=LD8QW$qws29Rt^+y?DjlxW~n%eII#DCXsxEleWTRj7uX?J<39~#M*gZ z{L?tjK6YN&qP6$TvRU0=&B_HnZP<4$`Ao4c7JOIDOr!s9U7jo(%uS_%tO^*wiGWsUYq;1@o~t}{vn9l zm=fH$haEVnpLmGvt(A*}o?6%RmXz(_h3{BAI1|68-Psdzu`96dnpEY2s zcW^tLBQ2LB#7#8y6_JhD-{{dcNGTq4dc!Mf=F(XI$j2v)y-cA{F+Kg+CBZ$SDm${T zW-x0Z0zReO9+D0W1+Kh7Ttn6R{S57m8$Pe zs+jl)r7Gxmoj1qd*<|XmF10Fk?HDpc9}{ort@XlD#df2SNSz1hIm7a}@cDrjdzojS z&88zZ9}*a*UcW%fb^ofWUpv8_&ui*X(_+}NqIM#e+w!S5hSQ_Zw3W=-n=(gBW7B2u z&*hm%6ul`Y6ER^nIpRt$ydkNh4-GJu=2x^Xz?l8IA1HZ#WI5TyBgfNgV7}7ca3I#L zPY?qdFx8&ob^o{JQ%(xJHG4f`aiU-mN2WJP^7YCjF@wvKJcrHWrG4teU>x%-+d_K{ z>%MYstlP}rfz@8y|HKPY=f|GcWBYPnO9z~GwI9jfw-Pa1WSYxEj)WGr?~}uCQI~49 z7Ip+pAAG<%GUL#eYb{H_P)Fd0=XYL&#Sr^+hy_w$lAj%h@_Inm3{=~4S~TF#HTlNH zYEY0F9BijH_r}K=ob&M)NI!$A+V%a6qwVzV?cFdBzw(+LrW#{TPL42^klOQ)P`!l3 z<>w23aR@8FKY6-9mkaJjUiPYkG4sl74&yEOMzVqP%7!A<)HCV0IJ6h)CQZX#dEk z|EoGpiJ@0Ho<_1_@*r;5XhD!1?B$L#q)AI!zks%;8qYFY8?=hA@mGLWtJy6G&6YII zl;Y@lo;}rz8e8pW&OkI@ew|uYe1{Lee)AoVKy3JdRr+$~AWpzV^EA7VIR=fQ%&Yjr zQ#%WiAg~4WHN`wVr95(I5pRSJhf@$DwxOj#VKccClk?Sn(?7;;jfXf?l6<3CgkK^& zBSHJH52TBgvTe&@0Rx=#v9N92k?LXsHuqIHLECU_bE$6i(JC5I*o?4yg7HISRIWAs z9syOXl)u;p524?=?dsCBrnbnozMiLIbDIj2NzGo;Tifx&-xlG12XAE%9ihxI`jdv_ zU|erf)GNHwtY4_Y5xZ-zS+!y}b$&#%)T?(lUwQl1PaXDWu1?(A*w3~=rc%C?gH^T% zU5)0z+!IIoUYopG%8UpIVW89A4f#Y=fmn+Mug7>G(G< z0hSGQ@@xae^_cI5UPk!n)l~vh=8MAOGu%rqeL55e$QyfgczmOf_Nz1;8AQ`Xt&MnvcJJsieWoo!sfT ze{|*>Z{kny-1Z$Mq4yaJPm;PP3T0!ky=QTv(?$9D`>kvQ#V&qNS*w69*}I=2$<8q4 zA}M`dCz-A;&Asbh`b?Pw%S3^S3A%A3RIn4dj8)pHae||P8Eg&KDmG2LWmp>h!1t18 zY$z@Vdqb&%M^x{?+A`B>Eo)dsCyDQKwDXPGsP3ynCu4z~l(UIA^UewRuUWRwwRJLI zig$)mrv{zBQmUkZ+EZ;jmGoXx@-8W+#e&ky7^7kkW>3TSV3J<~tFbU<;j3m@1tL%{ z_T;MGpJ~pJh6R6}aWkbh&mI9u*8+5|wz*0p5@(hW>n}`M=c!H?*;za}PIs7ZSnUI! zSXK7eirIadb&Xj+T&VsX{qE+7LPKgd7ptXcXKbeM6$q;NDdGlxy(YQcqo8;_e9p{o zg70QgIMR*T7-dbuqzPW5q46-^NsgyCx$?djvk6 zcMhkO|NaoA!(sC2WomCZDI|c`I&Vkx1~bKW?+jV6WUC+oD@wLD8KZi#HSTjl`m+> z^>QY_Lb@=ejDfSTj7AxSF)aH*XWb5auI$Azl$rNx*sx`nKA%`(5-KD^u+ae-S;Zp! z@eR_IzY0=~Hs?ycgEqUEkoeC}syq}W?UVCT{^m0GE`=e+eTeXx0Na@qjN8B$+Skdt z6zJG@Rr;hHb`NL(HZfivOxOhTE3HwgNDquPrNO#;;v(KwV<;?FN97?i07&NrKE2&V z+wy59@T=Nc(pHNi_rf?t^fcLsc1``B&6;Ue$>1CjmIz1GnlEKyq|5`gK3ivKHdIGg z@-`Yzg2gq$r`O=ayI~E}aN$}=LLYXhHq0i8QH4phsL*oAl#v}tW2<}4B$SW)#(LF7 zOkrizam1{wf-qu5d*gxR zKJvY_KkSuu)_r?t?l@A`Y8M71*NiYN9cg25etaYDgA;Yx{HLHPro*OlG6@;@glg3 zdu`~M=4xaY-EXxJ5Uo??q?7o!+dRB@)NBsH3`K3T2+jla%cIpOyjJ_C8?1e)dMrU0 zrc3r$I6=17kDz-&hGOGr{tR317N+;_n;Sjasr*-d5vcRI%Oxaj;et>d6TnF=;p>Zc zS!JQsjXczNp^0D!!)P)5+DLNJkBrYgjDep0gJE@kg%K#AEC#ZZY!16xj~1Gx8vl{e z&ShVm1m{ro1=9f?<#J5WuDKk9fRo;TTd+t4*|xa<-+17Ego*zXTW-J>QFJ>zlK8NL z6|`?)+3wezJNoK5IKe2yf9u52oEij_!c;uX2)>&;D%^}%a_{d4%}iGOUvN~cLUdIW zC-FSD!(#>OjxfVSgf9lv@@GL4FWj`ME-zp!4yO%OjsWj5z^TuuKb3Yl1gN!KUvfeq+L9cqOP-A_%zxk@WdM2GPGX$G z!y#dI=eX3IOC?qH=UiG;^~WET7jG1otYtu2J!l_4~!^Q2vln$KK0tgP`4eP6LP8Li5BW(Y`WaH7Pg`Ppv1)uv*L%d zY`?1i1?;abj{SrCOG9}6H?;qseD>c@1a75|8L(oe7H5RPQw>%R1w#)TOAlKK>-VS+wDEE4v;B9t0;nix%2&$1hW!^mN0syd literal 0 HcmV?d00001 diff --git a/src/main/webapp/images/nlib/sns/naver_login_btn.png b/src/main/webapp/images/nlib/sns/naver_login_btn.png new file mode 100644 index 0000000000000000000000000000000000000000..bcdfe7e967c697b316880f86d58db4dc43e278ca GIT binary patch literal 2925 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 WLdRTc^%M5M0AO$9WL=CvCjA>9%%}$d literal 0 HcmV?d00001