[Flutter] URI Scheme 사용하는 앱 개발

목적

  • Universal Link 를 통해 인자를 받는 webview를 사용하는 앱 개발

개발환경

  • 개발언어 : Flutter, Kotlin
  • 타겟 디바이스 : Android Pixel 3a API 29(Android 10.0) Emulator

' + Additional

  • 개발언어 : Flutter, Swift
  • 타겟 디바이스 : iPhone SE2 (iOS 13.7) Emulator

개요

  1. 웹뷰만 실행하는 앱
  2. 딥링크를 통해 인자를 받는 앱
  3. 2의 인자를 받아 1의 웹뷰를 실행하는 앱

1. 웹뷰만 실행하는 앱

  1. flutter_webview: ^0.3.19+9 pubspec.yaml에 추가

     dependencies:
       webview_flutter: ^0.3.19+9
       flutter:
         sdk: flutter
    
       # The following adds the Cupertino Icons font to your application.
       # Use with the CupertinoIcons class for iOS style icons.
       cupertino_icons: ^0.1.2

    빈 프로젝트에서 웹뷰 띄우기(default 경로 지정해서 자동으로 열리게 설정)

    1. main.dart 작성

      import 'package:flutter/material.dart';
      import 'package:webview_flutter/webview_flutter.dart';
      
      void main() => runApp(MyApp());
      
      class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
       return MaterialApp(
         title: 'Webview Demo',
         theme: ThemeData(
           primarySwatch: Colors.blue,
         ),
         home: MyHomePage(),
       );
      }
      }
      
      class MyHomePage extends StatefulWidget {
      MyHomePage({Key key}) : super(key: key);
      
      @override
      _MyHomePageState createState() => _MyHomePageState();
      }
      
      class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
       return Scaffold(
         body: SafeArea(
           child:
             WebView(
               initialUrl: 'https://flutter.dev',
               javascriptMode: JavascriptMode.unrestricted,
           )
         ),
       );
      }
      }

      예전에 실습할 때에는 인터넷 접근권한이 필요했었던걸로 기억하는데 이번엔 묻지 않았다.

    2. http 접근 허용을 위해 /android/app/src/main/AndroidManifest.xml 에 다음 내용 추가

      (미설정시 https만 사용가능)

      <application
           android:name="io.flutter.app.FlutterApplication"
           android:label="flutterdemo"
           android:icon="@mipmap/ic_launcher"
           android:usesCleartextTraffic="true"> # << 추가할 라인

실행결과 ( https://flutter.dev/ )

 

2. 딥링크로 인자를 받아 표시하는 앱

공식 문서 : https://developer.android.com/training/app-links/deep-linking?hl=ko

    • 스키마는 "bob"를 사용
    • host는 "deeplink.flutter.dev" 사용

      AndroidManifest.xml에 <intent-filter>에 사용할 딥링크 등록

AndroidManifest.xml

  딥링크로 앱을 시작하기 위한 핸들러를 등록 이때, 앱이 이미 실행중일 수도 있고 아닐 수도 있다.

안드로이드에서는 onNewIntent 메소드를 오버라이드한다.

 

 

2. Platform Channel

 

  flutter는 ios, android 모두 지원하기 때문에 플랫폼 별로 채널을 구분하여 핸들러 요청을 처리한다.

안드로이드에서는 Incoming Intent를 onCreate 메소드에서 캐치하고 MethodChannel을 생성하고 딥링크를 통해 전달된 URI를 전송한다.

 

private val CHANNEL = "bob.deeplink.flutter.dev/channel"
private var startString: String? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    GeneratedPluginRegistrant.registerWith(flutterEngine)

    MethodChannel(flutterEngine.dartExecutor, CHANNEL).setMethodCallHandler { call, result ->
        if (call.method == "initialLink") {
            if (startString != null) {
                result.success(startString)
            }
        }
    }
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val intent = getIntent()
    startString = intent.data?.toString()
}

[MainActivity.kt]

 

 

3. 실행 1 : 딥링크 인자 없이 실행시켰을 때

실행 2 : 딥링크로 앱이 실행되었을때

딥링크로 실행된 앱에서 정상적으로 인자가 전달됨을 확인

사용한 딥링크(bob://deeplink.flutter.dev/?url=http://naver.com&token=a123)

사용한 딥링크와 앱에서 실행한 결과

3. 딥링크의 인자를 받아 웹뷰를 실행시키는 앱

딥링크 uri의 파라미터를 파싱한 후 webview의 initial page로 연결

Flutter는 uri를 파싱해주는 Uri 클래스를 기본 내장한다.

 

 

url, token 인자를 파싱하여 구분한다.

4. 파싱한 인자를 Webview의 url로 전달하여 시작하기

view의 상태가 변할 경우 Statefulwidget사용, 상태의 변화가 없을 경우 StatelessWidget 사용.

예제의 경우 단순히 텍스트만 표시해주는 위젯이기 때문에 StatelessWidget을 사용하였으나 Webview로 변경하기 위해서는 StatefulWidget으로 변경해 주어야 동작한다.

StatefulWidget의 경우 위젯의 생명주기를 가지게 되며, OnCreate, OnChange 등 상태의 변화에 따라 앱의 동작을 지정해줄 수 있다.

이 상태를 페이지 뿐만 아니라 앱 전체의 범위에서 사용 가능하게 해줄 수 있도록 변수를 저장 관리해주는 기술 패턴이 두 가지가 있다.

  1. BLoC(Business Logic Component)
    • Flutter의 상태관리를 제어하기 위해 Google에서 개발
    • 각 UI객체들은 BLoC객체를 구독 중
  2. Provider()
    • 데이터의 생산과 소비로 구분
    • 관심사의 분리 - 클래스가 하나의 역할을 하도록 구현
    • 데이터의 공유 - 여러 페이지에서 하나의 데이터 공유 가능
    • 간결한 코드 - BLoC에 비해 코드가 간결해진다.

중규모 프로젝트에서는 Provider패턴, 대규모 프로젝트에서는 BLoC를 권장하고 있다.

예제 코드는 BLoC패턴을 이용하여 작성되었다.

 

import 'dart:async';
import 'package:flutter/services.dart';

abstract class Bloc {
  void dispose();
}

class DeepLinkBloc extends Bloc {

  //Event Channel creation
  static const stream = const EventChannel('bob.deeplink.flutter.dev/events');
  //Method channel creation
  static const platform = const MethodChannel('bob.deeplink.flutter.dev/channel');

  StreamController<String> _stateController = StreamController();
  Stream<String> get state => _stateController.stream;
  Sink<String> get stateSink => _stateController.sink;

  //Adding the listener into contructor
  DeepLinkBloc() {
    startUri().then(_onRedirected);
    stream.receiveBroadcastStream().listen((d) => _onRedirected(d));
  }

  _onRedirected(String uri) {
    // Here can be any uri analysis, checking tokens etc, if it’s necessary
    // Throw deep link URI into the BloC's stream
		// 원래 이부분에 URI 파서코드를 작성해야 하지만 Widget으로 데이터가 전달되지 않아 UI 부분에서 파싱코드를 작성하였다.
    var parseduri = Uri.parse(uri);

    stateSink.add(uri);
  }
  
  @override
  void dispose() {
    _stateController.close();
  }
  
  Future<String> startUri() async {
    try {
      return platform.invokeMethod('initialLink');
    } on PlatformException catch (e) {
      return "Failed to Invoke: '${e.message}'.";
    }
  }
}

[bloc.dart]

 

class _MyPocWidgetState extends State<PocWidget> {
  var initurl, inittoken;
  @override
  Widget build(BuildContext context) {
    DeepLinkBloc _bloc = Provider.of<DeepLinkBloc>(context);
    return StreamBuilder<String>(
      stream: _bloc.state,
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return Container(
              child: Center(
                  child: Text('No deep link was used  ',
                      style: Theme.of(context).textTheme.title)));
        } else {
          log('data: ${snapshot.data}');
          Uri initialUri = Uri.parse(snapshot.data);
          final params = initialUri?.queryParametersAll?.entries?.toList();
          var cUrl = params[0].value;
          var cInittoken = params[1].value;

          initialUri.queryParameters.forEach((i, k){
            if(i == 'url') {
              initurl = k.toString(); // 인자 중 url이 key값인 인자를 url로 저장
            }
            else if(i == 'token'){
              inittoken = k.toString();
            }
          });
          return Scaffold(
            body: SafeArea(
                child:
                WebView(
                  initialUrl: initurl, //전달받은 url로 webview 시작
                  javascriptMode: JavascriptMode.unrestricted,
                )
            ),
          );
      /*
					인자를 제대로 전달받았는지 앱에서 Text로 출력하는 위젯
          return Container(
              child: Center(
                  child: Padding(
                      padding: EdgeInsets.all(20.0),
                      child: Text('Redirected: ${snapshot.data}\nurl: ${initurl}\ntoken: ${inittoken}',
                          style: Theme.of(context).textTheme.title))));
*/

        }
      },
    );
  }

}

[poc.dart]

 

5. 실행

(bob://deeplink.flutter.dev/?url=http://naver.com&token=a123)


추가적으로, iOS에서도 동일하게 동작한다.

ios에서는 안드로이드의 AndroidManifest.xml처럼 Info.plist의 내용을 읽어서 앱 시작을 초기화한다. CFBundleURLSchemes의 값으로 URI 스키마로 사용할 구문('bob')을 등록한다.

 

<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleURLName</key>
			<string>deeplink.flutter.dev</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>bob</string>
			</array>
		</dict>
	</array>

일반 실행 / URI Scheme와 함께 실행한 앱

 

6. 결론

다음은 URI스키마를 통해 실행된 앱에서 전달받은 인자들을 콘솔에서 로그로 출력해 보았다.

MapEntity로 입력 받아서 딕셔너리 형태로 파싱하여 인자를 선택해 사용할 수 있도록 했다.

로그를 보면 키값으로 url과 token이 입력 된 것을 알 수 있다. 우리가 연구할 취약점은 이러한 인자의 값들을 제대로 검증하지 않은 채 다른 서버로 연결하도록 하거나, 민감정보를 노출시키게 하는 xss코드를 통해서 트리거할 수 있을 것으로 예상한다.

 


Referneces

  1. "Deep Links and Flutter applications. How to handle them properly." https://medium.com/flutter-community/deep-links-and-flutter-applications-how-to-handle-them-properly-8c9865af9283
  2. "Flutter 앱에 Webview 추가하기" https://clein8.tistory.com/entry/Flutter-앱에-웹뷰Webview-추가하기-webviewflutter
  3. https://github.com/DenisovAV/deep_links_flutter
  4. https://developer.android.com/reference/android/net/Uri.html

작성한 모든 코드는 https://github.com/colfax0483/uri-app 에 업로드하였다.

반응형