티스토리 OpenApi, 웹뷰 없이 계속 토큰 받아오기

2019.05.12 14:51안드로이드 개발/라이브러리 소개

안녕하세요, 아이엔입니다. 오늘은 제가 블로그 플래너를 개발하면서, 그 중에서도 티스토리 지원을 추가하면서 티스토리 OpenApi  사용에 대해서 알게 되었습니다. 네이버의 경우 '네이버 아이디로 로그인'이라는 OpenApi의 리소스를 이미 제공하여 Handler도 이미 구현이 다 되어 있기 때문에 비교적 쉬운데, 티스토리 로그인은 진짜 처음에는 대충 웹뷰로 하려다가 계속 오류가 나서 결국 오늘 제출한 업데이트에서 해결을 할 수 있었습니다.

오랜 제 삽질의 결과물이기 때문에, 이 글을 보시는 여러분들은 아마 삽질하지 않을 수 있을 것입니다. 그럼에도 불구하고 원하는 대로 되지 않으신다면 댓글로 달아주시면 감사하겠습니다.

HttpsURLConnection, WebView, CookieManager를 사용합니다. 최소 안드로이드 버전은 API 21, 롤리팝입니다. 게시글에서는 androidx를 사용하고 있습니다. 또한 Kotlin 언어를 사용한 강좌입니다. 이 점 숙지해두시기 바랍니다.


티스토리 OpenApi 신청하기

 

 

TISTORY

나를 표현하는 블로그를 만들어보세요.

www.tistory.com

위 링크에 접속해서 칸을 기입하고 등록합니다. callBack은 서비스에서 토큰을 받아오거나 로그아웃을 하고 그 다음 Redirect가 되는 링크인 것 같습니다. 아무거나 해도 상관없지만, 기억해두셔야 합니다. 서비스 Url은 그냥 개인 블로그 적으셔도 되는데, 저는 현재 서비스 중인 앱이기 때문에 Google Play 앱 링크를 적어 두었습니다. App ID와 Redirect Url을 따로 메모해 두세요.


리소스 다운로드 받기

먼저 관련 리소스를 다운받아 주세요. 직접 만든 버튼 이미지인데, 제가 개발한 블로그 플래너에도 같은 이미지를 넣었습니다. 한글, 영어 버전이 있으니 필요한 대로 다운받아 주세요. 파일은 직접 압축하였습니다. 

ienlab_img_tistory_kor.zip
0.02MB
ienlab_img_tistory_eng.zip
0.02MB

다운받으셨다면, Android 모드에서 app\res\drawable에 넣습니다. Locale 설정을 하셔도 좋습니다 (drawable-ko). 이제 본격적으로 코드를 봅시다.


AndroidManifest.xml에서 권한 설정하기

WebView와 HttpsURLConnection은 둘 다 인터넷 (Internet) 권한을 필요로 합니다. 때문에 매니페스트 파일에서 이 권한을 설정해주어야만 해요. 다음과 같이 설정합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package={본인 패키지명. 아마 적혀 있을 것이므로 변경하지 마세요}>

    <uses-permission android:name="android.permission.INTERNET" />
 
 ...

설정하셨으면 닫아주세요.

build.gradle (Module: app)에서 CardView implementation 해주기
dependencies {
	...
	implementation 'androidx.cardview:cardview:1.0.0'
    ...
}	

프로젝트를 생성하면서 다른 건 이미 되어 있을 거라고 생각합니다. CardView는 따로 import해주어야 하기 때문에 여기서 해 줍니다. '...'은 붙여넣지 마세요. implementation ~만 복붙하시면 됩니다.


activity_main에서 로그인/로그아웃 기능을 수행할 버튼 만들기

전체적으로 이 버튼이 하는 기능은 이렇습니다.  로그인 상태를 나타내는 Boolean 변수 isLogin이 false라면 -> 최초 로그인 시에는 WebView를 띄워서 티스토리 아이디와 비밀번호를 입력받고 (웹사이트가 다 해서 우리가 굳이 구현할 필요는 없음), 로그인이 성공하면 토큰이 포함된 URL로 이동하기 때문에 그 URL을 split해서 토큰을 SharedPreference에 저장합니다. 그리고 isLogin을 true로 바꾸고, 이 버튼의 이미지를 로그아웃 이미지로 변경합니다.

로그아웃 이미지로 변경하면? 로그아웃 기능을 수행하게 됩니다. isLogin의 변수가 true니까 로그아웃할 준비를 하겠죠? 로그아웃은 웹뷰를 아예 띄울 필요가 없습니다. HttpsURLConnection으로 로그아웃을 하면 됩니다.

먼저 버튼을 UI에서 만드는 것부터 시작해 봅시다.

<layout\activity_main.xml>

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.cardview.widget.CardView
        android:id="@+id/btn_LoginTistory"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:layout_centerInParent="true"
        android:foreground="?android:attr/selectableItemBackground"
        android:clickable="true"
        app:cardElevation="@dimen/cardview_elevation"
        app:contentPadding="@dimen/cardview_padding"
        app:cardCornerRadius="@dimen/cardview_radius"
        app:cardMaxElevation="@dimen/cardview_elevation" >
        <ImageView
            android:id="@+id/img_LoginTistory"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:scaleType="centerCrop"
            android:src="@drawable/img_tistory_login" />
    </androidx.cardview.widget.CardView>

</RelativeLayout>

참고로 dimen 사이즈는 다음과 같습니다.

<values\dimens.xml>

<resources>
    <dimen name="cardview_radius">5dp</dimen>
    <dimen name="cardview_elevation">5dp</dimen>
    <dimen name="cardview_padding">10dp</dimen>
</resources>

이러면 아마 UI 상에서 버튼은 띄워졌을 것입니다. 이제 버튼의 기능을 구현하러 갑시다.


btn_loginTistory의 OnClickListener 붙이기

코틀린 Kotlin에서는 findViewById를 하지 않아도 UI의 객체를 불러올 수 있다는 게 너무 좋습니다. MainActivity로 이동하여 티스토리 로그인 버튼의 기능을 붙여 봅시다.

전역 변수로 String 변수 TISTORY_REDIRECT_ID, TISTORY_REDIRECT_URL, LOGIN_INTENT를 만들어 주세요. 그리고 TISTORY_REDIRECT_ID에는 아까 발급했던 OpenApi의 App ID, TISTORY_REDIRECT_URL에는 Callback Url을 대입해주세요. LOGIN_INTENT는 웹뷰에서 로그인이 완료됐을 때 Broadcast로 받을 때의 인텐트입니다. "loginIntent"로 대입해 주세요. 그리고 현재 로그인했는지 알기 위한 Boolean 변수 isLogin을 생성해주세요. 일단 기본값은 false로 지정해 줍시다. 그러면 현재 전역 변수에는 String 변수 2개와 Boolean 변수 1개가 생겼습니다.

onCreate함수에 들어가서 먼저 로그인 시 접속할 URL 변수를 만들어 줍니다. 저는 이름을 tistoryURL로 했습니다. 코드를 참고해서 넣어 주세요. 그 다음, SharedPreferences를 두 개 생성해 주세요. 이는 현재 로그인을 한 상태인지 안 한 상태인지 저장하기 위해서, 그리고 티스토리 OAuth 관련 값을 저장하기 위해서입니다. 그리고 isLogin은 이전 데이터를 불러와야 합니다. 따라서 sharedPreferences에서 "isLogin"의 boolean값을 받아오도록 합시다. 만약 그 값이 없으면 로그인을 안 한 거니까 기본값은 false입니다.

var sharedPreferences = applicationContext.getSharedPreferences("pref", Context.MODE_PRIVATE)
var tistoryPref = applicationContext.getSharedPreferences("TistoryOAuthLoginPreferenceData", Context.MODE_PRIVATE)
var tistoryURL = URL(
            "https://www.tistory.com/oauth/authorize?" +
                    "client_id=$TISTORY_CLIENT_ID" +
                    "&redirect_uri=$TISTORY_REDIRECT_URL" +
                    "&response_type=token"
        )
isLogin = sharedPreferences.getBoolean("isLogin", false)

만약 불러온 isLogin이 true라면 현재 로그인을 해 놓은 상태라는 거겠죠. 따라서 로그아웃을 해야 합니다. 그럼 그것에 맞게 이미지도 바꿔주어야겠죠? 다음과 같은 구문을 추가합시다.

if (isLogin) img_LoginTistory.setImageDrawable(ContextCompat.getDrawable(applicationContext, R.drawable.img_tistory_logout))

그리고 btn_loginTistory에 OnClickListener을 붙여 줍시다. 그리고 크게 두 가지 케이스로 나눠줍시다. if (!isLogin)과 else로. !isLogin으로 하는 이유는 로그인 / 로그아웃 순으로 배치하는게 그냥 마음 편해서 그랬습니다.

<if (!sharedPreferences.getBoolean("isLogin", false))>

이 작업은 네트워크를 사용하기 때문에 메인 Thread에서 사용하면 NetworkOnMainThreadException이 발생하게 되니까, if문 내에서 새로운 Thread를 생성해 줍니다. 

Thread 내에서, cookieManager와 cookie를 생성해 줄 겁니다. 다음과 같이 코드를 작성할 수 있어요.

Thread {
    val cookieManager = CookieManager.getInstance()
    val cookie = cookieManager.getCookie(tistoryURL.host)
}.start()

이제 tistoryURL의 연결을 열고, webView의 cookie를 적용시켜 주어야 합니다. 그렇지 않으면 항상 웹뷰를 띄워야지만 로그인을 할 수밖에 없어요. Thread 내에 이런 코드를 추가합니다.

val connection = tistoryURL.openConnection() as HttpsURLConnection
connection.setRequestProperty("Cookie", cookie)
connection.connect()

그러면 해당 링크에 접속이 됩니다. 만약 로그인이 되어 있으면 "popup"이라는 글자가 링크에 있지 않은, 링크로 리다이렉트됩니다. 그러면 자동으로 로그인이 될 건데, 만약 "popup"이라는 링크로 연결이 된다면 WebView를 띄워서 직접 사용자가 로그인할 수 있도록 해야 합니다. 따라서 다음과 같이 if문을 작성해 줍니다. 

try {
    var inputStream = connection.inputStream
    inputStream.close()
    if (connection.url.toString().contains("popup")) {
    	startActivity(Intent(this@MainActivity, TistoryLoginWeb::class.java))
    }  else if ("access_token" in connection.url.toString()) {
    	var urls = connection.url.toString().split("=", "&")
		tistoryPref.edit().putString("ACCESS_TOKEN", urls[1]).apply()
        sharedPreferences.edit().putBoolean("isLogin", true).apply()
        img_LoginTistory.setImageDrawable(ContextCompat.getDrawable(applicationContext, R.drawable.img_tistory_logout))
    }
} catch (e: java.lang.Exception) {
	startActivity(Intent(this@MainActivity, TistoryLoginWeb::class.java))
}

드디어 토큰을 얻어내고 tistoryPref에 그 값을 저장하고 있습니다. urls을 =, &로 나누면 3개 정도의 뭉텅이가 생기고 그 중 2번째가 토큰입니다. 그래서 urls[1]을 집어넣습니다. 만약 오류가 나면 TistoryLoginWeb으로 이동하기도 합니다. 못 보던 클래스가 있어요. 그리고 로그인을 했으니 isLogin에 true를 넣어 주고, 버튼 이미지를 바꿉니다. TistoryLoginWeb은 WebView를 포함하는 액티비티 클래스입니다. 조금 있다가 생성하기로 합시다. 그러면 여기서 로그인은 끝입니다. 이제 로그아웃 버튼을 구현하러 갑시다.

<else>

로그아웃은 링크가 하나 더 필요합니다. 이전에 제가 삽질을 할 때는 지금 필요로 하는 링크 하나만 사용해서 웹뷰의 쿠키를 불러오려고 해도 로그인 정보가 넘어오지 않았는데, 링크 두 개로 구조를 짜게 되면 하나는 로그인의 쿠키를 받아오기 위한 용도, 하나는 로그아웃을 하기 위한 용도로 쉽게 사용할 수 있게 됩니다. else문 내에 구문을 추가합니다. 현재 혹시 없었던 쿠키가 생겼을 수도 있으니까 함수에서 선언하지 않고 if, else 수준에서 변수를 선언하고 있습니다. 

이 친구도 네트워크 작업이기 때문에 Thread 내에서 작업해주어야 합니다. AsyncTask로 해도 되긴 합니다만 코드가 너무 길어지고 오래 걸리는 작업도 아니기 때문에 그냥 Thread로 했다고 생각하시면 됩니다. 

else {
    Thread {
        var logoutURL = URL("https://www.tistory.com/auth/logout?redirectUrl=$TISTORY_REDIRECT_URL")
        val cookieManager = CookieManager.getInstance()
        val cookie = cookieManager.getCookie(tistoryURL.host)
        val connection = logoutURL.openConnection() as HttpsURLConnection

        connection.setRequestProperty("Cookie", cookie)
        connection.connect()
        try {
            var inputStream = connection.inputStream
            inputStream.close()
            sharedPreferences.edit().putBoolean("isLogin", false).apply()
            img_LoginTistory.setImageDrawable(ContextCompat.getDrawable(applicationContext, R.drawable.img_tistory_login))
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
        }
    }.start()
}

그러면 setOnClickListener 쪽의 작업은 끝이 났습니다. 최초 로그인 또는 로그아웃한 상태에서 로그인할 때 띄워야 하는 웹뷰를 작업해 봅시다.


TistoryLoginWeb 액티비티 생성

먼저 UI부터 작업합시다. 딱히 작업할 것도 없는데, 그냥 WebView 하나만 끼얹으면 됩니다. 우리가 브라우저를 만드는 건 아니므로.. 

<layout\activity_tistory_login_web.xml>

<WebView
    android:id="@+id/webview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

그냥 대충 만들면 되는데, 대신 id는 꼭 생성해줘야 합니다. 안 그러면 주소 지정 등을 할 수 없어요. 그리고 코틀린 클래스로 이동합니다.

<TistoryLoginWeb.kt>

아까와 같이 전역 변수로 TISTORY_CLIENT_ID, TISTORY_REDIRECT_URL, LOGIN_INTENT (="loginIntent")을 선언해 줍니다. 클래스 수준 변수에요. 그리고 onCreate(bundle) 함수 내에서는 tistoryPref, sharedPreferences, tistoryURL을 선언해 주세요. 아까와 똑같은 내용.

var sharedPreferences = applicationContext.getSharedPreferences("pref", Context.MODE_PRIVATE)
var tistoryPref = applicationContext.getSharedPreferences("TistoryOAuthLoginPreferenceData", Context.MODE_PRIVATE)

var tistoryURL =
            "https://www.tistory.com/oauth/authorize?" +
            "client_id=$TISTORY_CLIENT_ID" +
            "&redirect_uri=$TISTORY_REDIRECT_URL" +
            "&response_type=token"

이제 WebView 설정만 하면 됩니다. 

webview.settings.javaScriptEnabled = true
webview.webViewClient = object: WebViewClient() {
    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        super.onPageStarted(view, url, favicon)
        if ("access_token" in url!!) {
            sharedPreferences.edit().putBoolean("isLogin", true).apply()
            var urls = url.split("=", "&")
            tistoryPref.edit().putString("ACCESS_TOKEN", urls[1]).apply()
            LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(Intent(LOGIN_INTENT))
            finish()
        }
    }
}
webview.loadUrl(tistoryURL)

여기서 LOGIN_INTENT라는 인텐트를 보내게 됩니다. 이 인텐트는 다시 MainActivity에서 받아 버튼의 속성을 수정하게 됩니다. 로그아웃 이미지로 변경시켜야 합니다. setOnClickListener 만든 밑에 다음과 같은 구문을 추가합시다.


MainActivity에서 LOGIN_INTENT 받기
LocalBroadcastManager.getInstance(applicationContext).registerReceiver(object: BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
    	img_LoginTistory.setImageDrawable(ContextCompat.getDrawable(applicationContext, R.drawable.img_tistory_logout))
    }
}, IntentFilter(LOGIN_INTENT))

이렇게 하면 완료입니다. 


전체적인 코드는 GitHub에 올려놓도록 하겠습니다. 질문이 있으시면 댓글로 남겨 주세요. 페이스북 댓글로 바꿔 두어서 이제 댓글을 달아도 알림을 받으실 수 있습니다. 원체 티스토리는 글 알림이 안 오는 시스템이여서요!

이 링크는 TistoryOAuth 예제입니다. 참고하시면 좋겠습니다. 감사합니다.

 

ericanorhee/TistoryOAuthEx

Contribute to ericanorhee/TistoryOAuthEx development by creating an account on GitHub.

github.com