Intro

SSL Handler가 구현되어있는 최초 버전 (버전코드 1번)이 테스트 트랙에 올라가 있다는 이유로 구글플레이에서 삭제되었다.

당장 해당 트랙을 비활성화 한 뒤 업데이트를 시도해 보았지만 Cross App Scripting Vulnerability로 리젝되어 클라이언트의 요청이 지속되는 상황이었다.

Vulnerability APK Version(s) Past Due Date
Cross-app Scripting
Your app(s) are using a WebView that is vulnerable to cross-app scripting.
To address this issue, follow the steps in this Google Help Center article.
33 August 23, 2021

저 버전코드가 보이는가? 참고로 라이브 서비스중이었던 버전코드는 13이었다.

개빡친다

많고 많은 티스토리 블로그의 해결방법은 딱 두가지였는데,
내가 시도했던 방법은 이러하다.

AndroidMenifest 수정

많고 많은 티스토리 블로그들의 해결방법을 보면 아래 한 줄을 AndroidMenifest에 추가해줌으로써 해결할 수 있다고 쓰여있다.

<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" android:value="true" />

그러나 프로젝트에는 이미 해당 메타데이터가 추가되어있는 상태였다.

webview target url string to final

private final static String target_url = "https://블라블라";

...

    webView.loadUrl(target_url);

evaluateJavascript 보안

Scheme이 "javascript:" 인 것을 죄다 evaluateJavascript로 바꿔주고,
그마저도 사전에 약속된 스크립트가 아니면 실행하지 않도록 입구컷 시켰다.

구글한테 입구컷당했다.

FCM으로 넘어오는 url 검사

bundle.getString("url").contains(target_url)

위 코드가 true일 때만 해당 URL을 로드하도록 했다.

안된다.

android:exported="false" 설정

해당 속성을 웹뷰가 있는 Activity에 넣으면 된다고 한다.
근데.. 카카오톡 등 다른 앱 intent 사용은 안하시게요? 역시 패스....하려다가 혹시나 싶어 넣어봤다.

 

앱 실행이 안된다 ㅋㅋ

Unity Project 제거

해당 프로젝트는 Unity As A Library를 이용해 Unity Player Activity와 웹뷰가 상호 통신을 한다.
일단은.. 인텐트로 처리하니 빼보자..

***구글: 응~ 안돼~ 돌아가~

Camera / File Choose Handler 제거

카메라로 사진을 찍거나 파일선택기로 파일을 가져오는 것도 역시나 인텐트로 처리했으니 일단은 지워보았다.
다른 웹뷰 서비스들은 잘 올라가있는데 이게 맞을 리 없지.

죄다 지우고 함수 하나씩 살리기

개노가다 ON
버전코드가 33(지금은 40)까지 올라간 이유가 있다.

살리다보니 handleIntent 함수에서 딱 걸렸다.

혹시?

혹시나 싶어 FCM으로 넘어오는 url 검사 부분을 주석처리 해봤다.

와 Tlqkf

결론

구글새끼들은 loadUrl 메서드로 어떤 Url을 호출하는지에 관심이 없다.
그 Url이 final로 선언한 Url을 포함하고 있더라도..

단지 해당 Url이 final 인지 아닌지에만 관심이 있는 것 같다.

어휴..

해당부분은 이렇게 수정해 주고 업데이트를 올렸다.

 private final static String target_url = "https://블라블라";

 ...


 if(bundle.getString("url") != null) {
     final String target = target_url + bundle.getString("url");

     webView.loadUrl(target);
 }

fcm 데이터에 url 전체를 줬었는데 수정을 요청해서 url path만 주도록 바꿨다.

 

 

하........현타온다............

iOS 에서의 Javascript Interface Bridge는 단방향, JS -> Native로밖에 값을 전달하지 못한다.

따라서 js에 setValue() 등의 함수를 사용해야 한다..

그러나 window.webkit.messageHandler.foo.postMessage(args) 를 호출하고, 네이티브에서 evalJavascript로 setFooValue(bar) 함수를 호출해 밸류를 지정해준다 한들, 완성된 js코드를 보는 후임 개발자 입장에서 코드가 쉽게 눈에 들어오지 않을 것이고, 예쁘지도 않다. 또 리턴받아야 하는 함수가 여러 개인 경우 setValue() 함수를 여러번 생성해야 하며, Android JavascriptInterface Bridge와 다른 방식으로 사용해야 한다.

안드로이드만큼 편하지는 않겠지만, 그래도 기존 방식보다 편하면서 간결하고, 안드로이드와 따로 호출하지 않아도 될만한 코드를 고민해봤다.

 

메인 js에 객체 생성

var nativeObj = {
    coordinate:{
        getFromNative: function(val){
            if(nativeObj.nativeType.val == "Android"){
                this.latitude = window.android.get_latitude();
                this.longitude = window.android.get_longitude();
            }else if(nativeObj.nativeType.val == "iOS"){
                window.webkit.messageHandler.getCoordinate.postMessage("");
            }else{
                alert('앱에서만 이용이 가능한 기능입니다.');
            }
        },
        latitude:0,
        longitude:0
    },
    userToken: {
        getFromNative: function(){

        },
        val:""
    },
    userState: false,
    nativeType: {
        get: function(){
            var agent = navigator.userAgent.toLowerCase();
            is_android = agent.indexOf('app_android') > 0;
            is_ios = agent.indexOf('app_ios') > 0;
            if(is_android)
                this.val = "Android";
            else if(is_ios)
                this.val = "iOS";
            else
                this.val = "Web";
        },
        val: "Web"
    }
};

 

AndroidBridge.java

    @JavascriptInterface
    public double get_latitude(){
        GpsTracker gpsTracker = new GpsTracker(mainActivity);
        return gpsTracker.getLatitude();
    }

    @JavascriptInterface
    public double get_longitude(){
        GpsTracker gpsTracker = new GpsTracker(mainActivity);
        return gpsTracker.getLongitude();
    }

 

ViewController.m

- (void)userContentController:(WKUserContentController*)userContentController
  didReceiveScriptMessage:(WKScriptMessage*)message {
    if([message.name isEqualToString:@"getCoordinate"]) {
      [self getCurrentLocation];
      if(latitude != nil && longitude != nil){
          NSString *javaScript = [NSString stringWithFormat:@"window.nativeObj.coordinate.latitude = %@; window.nativeObj.coordinate.longitude = %@;", latitude, longitude];
          [self.wkWebView evaluateJavaScript:javaScript completionHandler:nil];
      }
   }
}

 

 

함수 호출시

window.nativeObj.coordinate.getFromNative();

 

 

물론 객체를 사용하지 않거나, 객체를 사용하더라도 콜백 등을 구현해 이보다 간결하게 작성할 수 있겠지만.. 
네이티브 함수 호출이 그다지 많지 않을 것 같아서 이렇게만 해야겠다.

'Mobile > iOS' 카테고리의 다른 글

[Objective C] Javascript와 WKWebView 통신 시 리턴값 받기  (0) 2021.05.28

카톡로그인 인텐트를 아래와 같이 처리해줬다.

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
  if (url.startsWith("intent:")) {
    try {
        Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
        Intent existPackage = getPackageManager().getLaunchIntentForPackage(intent.getPackage());
        if (existPackage!=null) {
        	startActivity(intent);
        } else {
        	Intent marketIntent = new Intent(Intent.ACTION_VIEW);
        	marketIntent.setData(Uri.parse("market://details?id=" + intent.getPackage()));
        }
      return true;
    } catch (Exception e) {
    	e.printStackTrace();
    }
  } 
  else {
  	view.loadUrl(url);
  }
  return true;
}

실사용에서는 참 편하지만 테스트할 때 만큼은 정말 불편하다.

특히 여러대의 테스트기기로 로그인을 해야하는데..
현재 개발중인 서비스는 카톡로그인 외의 로그인방법이 존재하지 않는다.

 

방법이 없을까 고민하다가 Extra를 자세히 보니 브라우저에서 로드되는 URL이 있어서 아래와 같이 처리해줬다.

  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
    Log.d("MainIntent URL", url);
    if (url.startsWith("intent:")) {
    	try {
    		Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
    		Intent existPackage = getPackageManager().getLaunchIntentForPackage(intent.getPackage());
    		if (existPackage!=null) {
    			startActivity(intent);
    		} else {
    			Intent marketIntent = new Intent(Intent.ACTION_VIEW);
    			marketIntent.setData(Uri.parse("market://details?id=" + intent.getPackage()));
    		}
    		return true;
    	} catch (Exception e) {
        	try{
            	Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);

                if(intent.getAction().contains("kakao")){
                	view.loadUrl(intent.getStringExtra("browser_fallback_url"));
                }
                else {
                    Intent marketIntent = new Intent(Intent.ACTION_VIEW);
                    marketIntent.setData(Uri.parse("market://details?id=" + intent.getPackage()));
                }
            }
            catch (Exception ex){
                ex.printStackTrace();
            }
      	}
    } 
    else {
      view.loadUrl(url);
    }
    return true;
  }

 

이제 카톡이 깔려있지 않아도 카카오 계정으로 사용이 가능하다.

 

MainActivity.java

    private long backKeyPressedTime = 0;
    private Toast toast;
    
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
        	// 웹뷰 History상 이전 페이지가 있을 경우
            if(mWebView.canGoBack()){
                mWebView.goBack(); // 뒤로가기
                return true;
            }
            // 없을 경우 앱 종료 전 Toast로 물어보기
            else{
            	// 토스트메세지 출력
                if (System.currentTimeMillis() > backKeyPressedTime + 2000) {
                    backKeyPressedTime = System.currentTimeMillis();
                    toast = Toast.makeText(this, "뒤로가기 버튼을 한번 더 누르시면 종료됩니다.", Toast.LENGTH_SHORT);
                    toast.show();
                    return true;
                }
                // 토스트메세지가 있는 상태에서 뒤로가기를 한번 더 누르면 앱 종료
                else if (System.currentTimeMillis() <= backKeyPressedTime + 2000) {
                    finish();
                    toast.cancel();
                }
            }
        }
        return super.onKeyDown(keyCode, event);
    }

 

최종 결과물 

Activity에서 처리하는 경우

activity_main.xml

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <WebView
            android:layout_width="match_parent"
            android:layout_height="match_parent"

            android:id="@+id/mWebview"/>
    </LinearLayout>

MainActivity.java

    WebView mWebView;
    WebSettings mWebSettings;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebView = (WebView) findViewById(R.id.mWebview); // 웹뷰 선언
        mWebSettings = mWebView.getSettings();

        mWebView.setWebChromeClient(new WebChromeClient()); // 크롬클라이언트 사용
        mWebView.setWebViewClient(new WebViewClientClass()); // 웹클라이언트 설정 (아래)
        mWebSettings.setJavaScriptEnabled(true); // 자바스크립트 허용
        mWebSettings.setSupportMultipleWindows(false); // 여러 창 또는 탭 열리는 것 비허용
        mWebSettings.setLoadWithOverviewMode(true); // 페이지 내에서만 이동하게끔
        mWebSettings.setUseWideViewPort(true); // 페이지를 웹뷰 width에 맞춤
        mWebSettings.setSupportZoom(false); // 확대 비활성화
        mWebSettings.setBuiltInZoomControls(false); // 확대 비활성화
        mWebSettings.setCacheMode(mWebSettings.LOAD_NO_CACHE); // 캐시 사용안함 (매번 새로 로딩)
        mWebSettings.setDomStorageEnabled(true); // 로컬스토리지 사용 허용

        mWebView.loadUrl("접속할 URL");
    }

    // 웹클라이언트 세부 설정
    private class WebViewClientClass extends WebViewClient {
        // SSL 인증서 무시
        @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
            handler.proceed();
        }

        // 페이지 내에서만 url 이동하게끔 만듬
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }
    }

AndroidManifest에서 처리하는 경우

AndroidManifest.xml

    <application
        ...
        android:usesCleartextTraffic="true">
        ...

이전 포스팅 : 안드로이드 프래그먼트 활용하기

 

안드로이드 프래그먼트 활용하기

MainActivity.java public class MainActivity extends AppCompatActivity { private FragmentManager fragmentManager; private Fragment_Main fragment_main; private Fragment_MyPage fragment_my_page; privat..

jyspw.tistory.com

 

사전 작업
1. res 폴더 우클릭 -> New -> Android Resources Directory -> menu 폴더 생성
2. res/drawable -> New -> Vector Asset -> 사용할 Clipart 이미지 생성

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private FragmentManager fragmentManager;
    private Fragment_Main fragment_main;
    private Fragment_MyPage fragment_my_page;
    private FragmentTransaction fragmentTransaction;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        fragmentManager = getSupportFragmentManager();

        fragment_main = new Fragment_Main();
        fragment_my_page = new Fragment_MyPage();

        fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.frameLayout, fragment_my_page).commitNowAllowingStateLoss();
 
 	// 이벤트 리스너 등록
        BottomNavigationView bottomNavigationView = findViewById(R.id.navBar);
        bottomNavigationView.setOnNavigationItemSelectedListener(new MenuClickEventListener()); 
    }
    class MenuClickEventListener implements BottomNavigationView.OnNavigationItemSelectedListener{
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            fragmentTransaction = fragmentManager.beginTransaction();

            switch(item.getItemId())
            {
                case R.id.btn_main:
                    fragmentTransaction.replace(R.id.frameLayout, fragment_main).commitNowAllowingStateLoss();
                    break;
                case R.id.btn_my_page:
                    fragmentTransaction.replace(R.id.frameLayout, fragment_my_page).commitNowAllowingStateLoss();
                    break;
            }
            return true;
        }
    }

    /* 
    // 이전 포스팅 코드
    public void MenuClickHandler(View v){
        fragmentManager = getSupportFragmentManager();
        fragmentTransaction = fragmentManager.beginTransaction();

        switch (v.getId()){
            case R.id.btn_main:
                fragmentTransaction.replace(R.id.frameLayout, fragment_main).commitNowAllowingStateLoss();
                break;
            case R.id.btn_my_page:
                fragmentTransaction.replace(R.id.frameLayout, fragment_my_page).commitNowAllowingStateLoss();
                break;
        }
    }*/
}

 

res/menu/nav.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/btn_main"
        android:icon="@drawable/ic_baseline_home_24"
        android:title="Main" />
    <item
        android:id="@+id/btn_my_page"
        android:icon="@android:drawable/ic_menu_manage"
        android:title="My Page" />
</menu>

 

activity_main.xml

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

    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/navBar"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </FrameLayout>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/navBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="parent"
        app:menu="@menu/nav" />

</androidx.constraintlayout.widget.ConstraintLayout>

최종 결과물

+ Recent posts