개인 프로젝트/프로젝트

SpringLegacyProject(MVC) Naver Login

Leo.K 2022. 7. 14. 22:14

국비지원 학원도 수료가 3주정도 남았다,, 굉장히 길다고 생각했던 6개월이 어느 순간에 휙하고 지나간 기분이다. 그래도 마지막까지 열심히하기 위해 오늘은 필자가 진행중인 최종프로젝트에 Naver 소셜 로그인을 진행한 부분을 소개하려고 한다. 

가장 먼저 네아로 API를 사용하기 위해 어플리케이션을 등록해야 하기 때문에 천천히 진행해보자. 

1. 어플리케이션 등록

https://developers.naver.com/main/

 

NAVER Developers

네이버 오픈 API들을 활용해 개발자들이 다양한 애플리케이션을 개발할 수 있도록 API 가이드와 SDK를 제공합니다. 제공중인 오픈 API에는 네이버 로그인, 검색, 단축URL, 캡차를 비롯 기계번역, 음

developers.naver.com

 

네이버 아이디로 로그인을 진행하고 등록 버튼을 누른다. 

 1-1. 애플리케이션 설정

사용할 애플리케이션의 이름을 지정한다. 자유롭게 원하는 이름을 작성해도 되고, 이왕이면 프로젝트명을 사용하는 것이 좋을 것 같다. 

사용 API는 '네이버 로그인'을 선택하고 네이버로부터 제공받을 정보를 선택한다. 필자는 기본적으로 이름과, 이메일 주소, 별명만 체크하고 진행했다.

API를 사용하는 환경을 설정한다. 모바일, PC 등이 있지만 필자는 웹서비스에서 사용하기 위해 PC웹을 선택했다. 

서비스 URL은 네이버 로그인을 사용할 주소를 작성한다. 특별한 도메인이 없는 독자가 많을 것이기 때문에, localhost를 사용하면 된다. 아래 이미지는 필자가 적은 예시이다.

tomcat의 기본 포트로 8080을 많이 사용하고 있지만, 필자는 서버의 포트를 사용하지 않는 9090포트로 바꾸었다는 점을 알고 넘어가자. 필자가 사용한 spring MVC 프로젝트의 contextpath명이 'teka'이다. 또한 테이블별로 폴더를 구분하기 위해 tekemember(회원 관리 기능이 정리된 폴더)의 loginForm에서 네이버 로그인을 사용할 것이기 때문에 서비스 URL을 위와 같이 작성했다. 

CallbackUrl은 로그인 폼에서 네이버 로그인을 누르면 팝업이 나타나면서 네이버로 로그인할 수 있는 익숙한 폼이 등장한다. 여기서 로그인을 진행하면 위에서 체크한 정보를 네이버에서 제공해주는데, 이 정보를 받을 URL을 지정해주는 것이다.

자세한 내용은 소스 코드를 보면서 설명하도록 하겠다. 여기까지 진행이 되었다면 애플리케이션은 설정이 끝났고, 제공되는 클라이언트 아이디와 클라이언트 시크릿은 외부에 공개되면 안되기 때문에 꽁꽁감춰두도록 하자.

 

[ 갑분 주의 사항 ]

네이버 로그인 API기능을 정리하다가 문득 뇌리를 스쳐가는 생각에 깃허브를 확인해보았는데 좌절을 머금지 않을 수 없었다. 바로 위에서 API키를 꽁꽁 감춰두라고 했던 필자가,,,, API Key가 존재하는 xml환경설정 파일을 그대로 푸시한 것이다.. 안그래도 public인 repository에 모든 사람이 볼 수 있도록 공유해버렸다,,,,ㅋㅋ 너무 당황한 마음에 급하게 원격에서 파일을 삭제하고,,, 커밋을 되돌리고,, 이런 저런 삽질을 몇시간동안 지속했다.

결론적으로 깃허브에 있는 모든 히스토리를 삭제하는 것에는 실패했지만, 네이버 어플리케이션을 삭제하고 새로 등록하였다. 하지만 앞으로도 적용해야 하기 때문에 API Key가 담긴 특정 파일을 이그노어 시켜줄 필요가 있었다. 

프로젝트를 시작한 초기라면 처음에 gitignore파일을 작성하면 되기 때문에 상관없지만, 이미 깃이 추적하고 있는 파일을 이그노어 시키기 위해서는 아래의 과정을 거쳐야 한다. 

(1). git bash를 실행해서 특정 파일이 있는 위치로 경로를 이동한다.

(2).git rm - r --cached "특정 파일명.확장자"를 입력한다. 이를 입력하면 원격 ropository에 있는 내가 무시하고자하는 특정 파일을 삭제할 수 있다. 

(3). gitignore파일에 특정 파일을 무시하도록 절대경로를 써준다.(이 절대 경로는 로컬이 아닌 원격을 기준으로 작성!)

(4). 원격에서 특정 파일을 삭제한 정보가 staging에 있을 것이고, 변경된 이그노어 파일은 커밋을 대기중인데, 이 둘을 원격에 push한다. 

(5). 기존 원격에서 추적하던 특정 파일을 삭제하고, 이그노어를 추가함으로써, 앞으로도 깃이 추적을 무시하게 된다. 

https://nan-sso-gong.tistory.com/38

 

[Git] repository에서만 특정 파일 삭제 (.gitignore 사용)

1. 문제 git을 사용하다보면 개발폴더 내에 있는 파일이지만 공개적인 코드저장소엔 올리고 싶지 않은 파일이 있다. 예를 들자면, /upload 폴더의 내용이라던가 개발상 필요한 API코드가 들어간 파

nan-sso-gong.tistory.com

위는 필자가 참고한 사이트이니 한 번 방문해서 학습해보는 것도 좋을 것 같다. 

적당히 주의사항을 강렬하게 주었다고 생각한다,,, 개인정보 꼼꼼히 챙깁시다. maven디렉토리 환경설정 부터 이어서 가보자.

 

2. 프로젝트 환경 설정 

위의 두개의 라이브러리를 추가해준다. 기본적으로 spring에서는 googel소셜 로그인을 지원하는 라이브러리가 존재하지만, 네이버, 카카오 등 한국계 기업에 대한 로그인 API는 제공하지 않기 때문에 별도의 라이브러리를 추가해줘야 한다. 

root-context.xml파일에 다음의 환경설정 정보를 입력하여 bean객체를 생성해주자.

마지막으로 사용자가 사용할 Controller에서 사용할 수 있도록 DI를 진행한다.

 

이제 실제 소스코드를 보자. 조금 헷갈릴 수도 있지만, 필요성을 체크하기 위해 설명은 역순으로 진행하겠다. 

가장 먼저 Controller코드에다가 독자가 사용하고자 하는 로그인 폼에 네이버 로그인할 수 있는 URL을 작성해주어야 한다.

위의 코드만 봐도 처음보는 객체 및 메서드가 존재할텐데, 지금부터 소개하도록 하겠다. 먼저 util(com.ict.teka.login)폴더를 만들어서 이 패키지에 클래스를 생성해주어야 한다. 

소셜 로그인 생성자의 파라미터로 들어오는 SocialValue에 대한 코드를 한 번 보자. SocialLogin은 실질적으로 얻어온 데이터를 파싱하는 역할을 하기 때문에 가장 마지막에 보도록 하겠다.

단순하게 보면 초기에 설정한 bean파일에 대한 Vo정도로 생각해주면 될 것 같다. 위에서 root-context.xml파일에서 bean객체를 정의할 때, SocialValue를 정의하면서 서비스 종류(두개이상의 loginAPI를 사용하는 경우 구분하기 위함),  API의 아이디, 비밀번호, 콜백 url을 생성자를 통해 주입한 것을 확인할 수 있다. 이를 코드화 한 내용이 아래 생성자 부분의 코드이다. 

추가로 생성자에서 두개의 값을 더 받는다. 

api20Instance는 가장 처음에 추가한 라이브러리에서 제공하는 클래스로 이 클래스르 상속받는 Naver20API를 통해서 로그인 URL, 액세스 토큰, 인증 URL을 받아올 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.ict.teka.login;
 
import com.github.scribejava.core.builder.api.DefaultApi20;
 
public class SocialValue implements SocialUrl{
    private String service;    //naver or google이 들어온다.
    private String clientId;
    private String clientSecret;
    private String redirectUrl;
    private DefaultApi20 api20Instance; //NaverAPI20내부에 정의된 인스턴스를 가져오기 위해 상위 클래스인 DefaultAPI20타입으로 설정한다. 이 인스턴스가 있어야만 SnsUrl에서 naverurl을 가져올 수 있다!!
    private String profileUrl;            //각 소셜에서 프로필을 받아올 URL을 저장
    
    private boolean isNaver;
 
    public SocialValue(String service, String clientId, String clientSecret, String redirectUrl) {
        super();
        this.service = service;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.redirectUrl = redirectUrl;
        this.isNaver  = this.service.equals("naver");
        
        if(isNaver) {
            this.api20Instance = NaverAPI20.getInstance();
            this.profileUrl    = NAVER_PROFILE_URL;
        }
    }
    
    
    public boolean isNaver() {
        return isNaver;
    }
    
    public void setNaver(boolean isNaver) {
        this.isNaver = isNaver;
    }
    
    public boolean isGoogle() {
        return isGoogle;
    }
    
    public void setGoogle(boolean isGoogle) {
        this.isGoogle = isGoogle;
    }
    
    public DefaultApi20 getApi20Instance() {
        return api20Instance;
    }
    public void setApi20Instance(DefaultApi20 api20Instance) {
        this.api20Instance = api20Instance;
    }
    public String getProfileUrl() {
        return profileUrl;
    }
    public void setProfileUrl(String profileUrl) {
        this.profileUrl = profileUrl;
    }
    public String getService() {
        return service;
    }
    public void setService(String service) {
        this.service = service;
    }
    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 getRedirectUrl() {
        return redirectUrl;
    }
    public void setRedirectUrl(String redirectUrl) {
        this.redirectUrl = redirectUrl;
    }
}
 
cs

 

NaverAPI20(싱글톤 구조)

라이브러리에서 제공하는 추상클래스 DefaultApi20을 상속받으면 재정의해야 하는 두 개의 추상 메소드가 있는데 아래의 코드를 보면 알겠지만, 네이버에서 프로필을 읽어오기 위한 액세스토큰을 발급받는 메서드, 인증을 진해할 url을 받아올 수 있는데, 이는 SocialUrl이라는 인터페이스에 상수로 정의되어 있기 때문에 사용만 하면 된다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.ict.teka.login;
 
import com.github.scribejava.core.builder.api.DefaultApi20;
 
public class NaverAPI20 extends DefaultApi20 implements SocialUrl{
    //싱글톤
    private static NaverAPI20 _instance;
    private NaverAPI20() {
        
    }
    
    public static NaverAPI20 getInstance() {
        if(_instance == null) {
            _instance = new NaverAPI20();
        }
        return _instance;
    }
    
    
    @Override
    public String getAccessTokenEndpoint() {
        // TODO Auto-generated method stub
        return NAVER_ACCESS_TOKEN;
    }
    @Override
    protected String getAuthorizationBaseUrl() {
        // TODO Auto-generated method stub
        return NAVER_AUTH;
    }
 
}
 
cs

 

SocialUrl

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.ict.teka.login;
 
public interface SocialUrl {
    //토큰을 받아오는 end-point
    static final String NAVER_ACCESS_TOKEN = "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code";
    //인증을 받는 url
    static final String NAVER_AUTH = "https://nid.naver.com/oauth2.0/authorize";
    
    //액세스 토큰을 가지고 프로필을 가지러 가는 url
    static final String NAVER_PROFILE_URL = "https://openapi.naver.com/v1/nid/me";
 
}
 
cs

 지금까지 따라온 플로우를 한 번 복기해보자. 

  1. 프로젝트가 실행되면서 bean객체를 생성하는데, 이때 SocialValue클래스에 API에 대한 정보를 생성자 주입함으로써 생성이 된다.
  2. SocialValue의 생성자에서 DefaultApi20을 상속받는 NaverAPI20클래스의 객체가 생성되는데 싱글톤 구조로 생성된다. 이 객체를 통해서 SocialUrl에 정의된 상수에 접근하여 사용할 수 있다.
  3. 컨트롤러에서 socialLogin을 생성할 때, 파라미터로 생성되어 있는 socialValue값(API키에 대한 설정 값)을 받는다.
  4. socialValue에 정의된 NaverAPI20 객체를 builder패턴을 사용하여 빌드한다. 빌드한 이 객체를 통해서 accessToken을 받고, 네이버에서 제공하는 프로필을 받아볼 수 있다. 

이제 마지막이다. 자세한 내용은 주석을 참고하면서 이해해보자. 

SocialLogin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.ict.teka.login;
 
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
 
import vo.TekaMemberVo;
 
public class SocialLogin {
    private OAuth20Service oauthService;
    private SocialValue social;
    
    public SocialLogin(SocialValue social) {
        this.oauthService = new ServiceBuilder(social.getClientId())
                .apiSecret(social.getClientSecret())
                .callback(social.getRedirectUrl())
                .scope("profile")
                .build(social.getApi20Instance());
        
        this.social = social;
    }
    
    //인증을 위한 URL을 받아온다.
    public Object getNaverAuthURL() {
        // TODO Auto-generated method stub
        return this.oauthService.getAuthorizationUrl();
    }
    
    //액세스 토큰을 받아서 프로필을 받아오는 과정
    public TekaMemberVo getUserProfile(String code) throws Exception {
        // TODO Auto-generated method stub
        //액세스 토큰을 받아오기
        OAuth2AccessToken accessToken = oauthService.getAccessToken(code);
        
        //소셜에 요청하기 위한 request생성
        OAuthRequest request = new OAuthRequest(Verb.GET, this.social.getProfileUrl());
        oauthService.signRequest(accessToken, request);
        
        //요청을 처리한 응답을 가져온다.
        Response response = oauthService.execute(request);
        
        //json형태로 응답이 온다.
        //return parseJson(response.getBody());
        return parseJson(response.getBody());
    }
    
    //프로필 정보가 json형태로 오는데, 이를 파싱하는 코드
    private TekaMemberVo parseJson(String body) throws Exception {
        // TODO Auto-generated method stub
        
        TekaMemberVo user = new TekaMemberVo();
        
        
        //json - > object 매핑
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode = mapper.readTree(body);
    
        JsonNode resNode = rootNode.get("response");
        user.setM_naverId(resNode.get("id").asText());
        user.setM_email(resNode.get("email").asText());
        user.setM_nickname(uniToKor(resNode.get("name").asText()));
        return user;
    }
    
    //id는 암호화, email은 그대로, name은 유니코드로 인코딩되어서 들어오는데,
    //유니코드로 인코딩된 문자를 utf-8로 복호화하는 과정이다.
    public String uniToKor(String uni){
        StringBuffer result = new StringBuffer();
        
        for(int i=0; i<uni.length(); i++){
            if(uni.charAt(i) == '\\' &&  uni.charAt(i+1== 'u'){    
                Character c = (char)Integer.parseInt(uni.substring(i+2, i+6), 16);
                result.append(c);
                i+=5;
            }else{
                result.append(uni.charAt(i));
            }
        }
        return result.toString();
    }
}
 
cs

소셜로그인 클래스에서 인가된 사용자에게 code를 발급하는데, 이 code값을 사용해서 액세스토큰을 발급받고, 이 액세스 토큰을 사용해서 사용자의 프로필을 읽어온다. 이제 컨트롤러에서는 SocialLogin에서 수신하고 파싱한 프로필정보를 DB에 저장하고 가공하여 사용하기만 하면 된다.

 

전체 소스코드는 필자의 깃허브 주소로 들어가면 확인할 수 있다.

https://github.com/yhn032/TEKASpring

 

GitHub - yhn032/TEKASpring: 기술 면접 학습 플래시 카드 웹 서비스

기술 면접 학습 플래시 카드 웹 서비스. Contribute to yhn032/TEKASpring development by creating an account on GitHub.

github.com