본문 바로가기

Flutter

[플러터] PASS 인증 InAppWebView로 실행하기

웹뷰 형태로 
PASS 인증을 할 수 없을까?


PASS InAppWebView로 연결

 

 

안녕하세요. 앱개발지식나눔입니다.

오늘은 PASS를 앱에서 직접 연결하는 것이 아닌 웹앱 페이지 속에서 PASS를 연동하는 방법에 대해서 설명할 예정입니다. 

 

 

1. InAppWebView URL: https://inappwebview.dev/docs/in-app-webview/basic-usage/
2. PASS 기술 문의: https://www.dreamsecurity.com/customer/contact.php
3. PASS 개발자 센터: https://developers.passlogin.com/docs/develop/web 
4. 플러터 PASS 패키지(앱-PASS 직접 연결): https://pub.dev/packages/iamport_flutter/changelog

 

 

글에 들어가기 앞서서(필독)

 

PASS를 연결하는 방법은 두 가지가 있습니다. 앱에서 직접 PASS를 실행하는 방법과 웹앱을 통해서 웹사이트를 들어간 다음 거기서 PASS를 실행하는 케이스입니다.

 

첫 번째 방법은 위에 있는 4번 링크를 따라가 주세요. 쉽게 연동하여 사용할 수 있을 것입니다. 하지만 개인마다 사정으로 꼭 웹페이지를 들어간 다음 거기서 PASS를 실행해야 하는 케이스가 있습니다. 오늘은 이것에 대해서 설명할 예정입니다.

 

일단 플러터에서 웹페이지를 실행하는 방법은 여러 가지가 있습니다.

대표적으로 InAppWebView 패키지, WebView 패키지, url_launcher 패키지가 있습니다. 저는 자바스크립트 채널이 필요가 없다면 InAppWebView 패키지 중에서 ChromeSafariBrowserurl_launcher를 추천합니다. 크롬, 사파리 브라우저를 사용하기 때문에 개발자가 일일이 조건을 지정할 필요가 없습니다. 그 말인즉슨 PASS 연동을 위해서 따로 설정해야 할 것이 없다는 의미입니다. 난이도도 쉽고 개발 코드도 적습니다.

 

자세한 내용은 아래 링크를 참고해주세요.

 

 

[Flutter] - [플러터] 대표 웹뷰 패키지 소개 및 장단점

 

[플러터] 대표 웹뷰 패키지 소개 및 장단점

내 상황에 맞는 앱뷰는 무엇일까? 안녕하세요. 앱개발지식나눔입니다. 플러터로 서비스를 개발하시다보면 가끔 인앱 웹뷰를 사용해야할 때가 있습니다. 예를들어 저는 플러터로 홈페이지를 웹

app-developement-sharing-forum.tistory.com

 

 

아마 자바스크립트 채널도 써야 하면서 꼭 InAppWebView 패키지 중에서 일반 InAppWebView 클래스만 사용해야겠다는 사람만 남았을 거라 생각이 듭니다. 참! WebView 패키지는 새창 열기가 불가능해서 PASS 사용에 적합한 패키지가 아닙니다.

 

그리고 이 글을 읽기 전에 아래에 있는 글을 먼저 읽고 와주세요. PASS 인증의 구조를 이해해야 PASS를 InAppWebView로 구현할 수 있습니다.

 

 

[기타] - PASS 인증 연결 시 필요한 것

 

PASS 인증 연결시 필요한 것

패키지 말고 직접 PASS 인증 창을 띄우고 싶은데 방법이 없을까? 알아두면 좋은 사이트 1. 기술 고객센터: https://www.dreamsecurity.com/customer/contact.php 2. PASS 개발 가이드: https://developers.passlo..

app-developement-sharing-forum.tistory.com

 

그리고 InAppWebView 새로운 창을 여는 방법에 대해서도 알고 있어야 합니다. 따라서 아래 글도 읽고 이 글을 읽는 것을 추천드립니다.

 

[Flutter] - [플러터] InAppWebView에서 새 창 여는 법

 

[플러터] InAppWebView에서 새 창 여는 법

왜 웹뷰에서 버튼을 눌렀을 때 흰색 화면만 보일까? 안녕하세요. 앱개밸자지식나눔입니다. 오늘은 플러터 패키지인 InAppWebView에서 새로운 창(탭)을 여는 법에 대해서 알아보겠습니다. 그전에 저

app-developement-sharing-forum.tistory.com

 

 

그러면 이제 진짜로 시작해볼까요??

 

암호화 키인 req_info를 크롤링하기

 

위 글에서 PASS 인증을 위해선 4가지가 필수라고 했습니다. PASS URL, req_info, rtn_url, cpid 중에서 하나라도 틀리면 PASS가 열리지 않습니다. 따라서 첫 번째 해야 할 것은 실시간으로 변하는 req_info를 크롤링하여 변수에 저장해야 합니다. 

 

그러면 크롤링 순서를 알아보겠습니다.

 

  1. 해당 페이지 html 코드를 모두 크롤링하기
  2. 끌고 온 html 코드 중에서 req_info 부분만 파싱 하기
  3. 파싱 한 값을 변수(res)에 저장하기

 

req_info를 얻기 위해서는 해당 페이지 html 코드를 모두 끌고 오는 부분은 InAppWebView에서 onloadStop 메서드에서 실행됩니다. onloadstop은 웹 페이지 로딩이 끝나면 실행되는 메소드입니다. 꼭 이 메소드에서 html 파싱을 진행해야 합니다. 그럼 파싱 하는 html 코드를 살펴봅시다.

 

 

import 'dart:async';

import 'dart:convert';
import 'dart:developer';

import 'package:html/dom.dart' as Document;
import 'dart:io';
import 'package:html/parser.dart' as parser;
import 'package:cp949/cp949.dart' as cp949;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';





onLoadStop: (controller, url) async {
  ///PASS에 들어갈시 req_info 암호화 키를 추출하는 부분.

  List<Document.Element> keywordElements = await extractHTMLSource(controller);

  var link;
  for (var element in keywordElements) {
    link = element.querySelector('input[name=req_info]');

    res = link.outerHtml.split("value=").last.split(">")[0];

    res = res.replaceAll("\"", "");
    res = Uri.decodeFull(res);
  }

  pullToRefreshController.endRefreshing();
  setState(() {
    this.url = url.toString();
    urlController.text = this.url;
  });
},

 

해당 페이지 중에서 HTML body만 뽑는 extractHTMLSource 함수는 아래와 같이 구성되어있습니다.

 

Future<List> extractHTMLSource(var controller)async{
  var html = await controller.evaluateJavascript(
      source:
      "window.document.getElementsByTagName('html')[0].outerHTML;");

  Document.Document document = parser.parse(html);

  List<Document.Element> keywordElements =
  document.querySelectorAll('body');
  return keywordElements;
}

 

 

왜 이렇게 파싱을 했을까요? 그 대답은 해당 페이지 html 코드에 있습니다.

 

   <body>
        
        <form name="form1" method=post>
          <article class="article-index">

            <input type=hidden name=req_info value=test_secret_key_abc12345==>
            <input type=hidden name=rtn_url value=https://www.testpage.com>
            <input type=hidden name=cpid value=testcompany>

              <button type="button" onclick="openNewWindow(a, 'b', 'c', 'd', '', 'testcode11', 'app')" name="adult" class="button-blue-big color-white font-weight-bold font-size-normal-more mx-auto button-0">인증</button>
                              
              
                           
            </article>
            <html>
  
</html>
        </form>
    </body>

 

해당 req_info, rtn_url, cpid는 body - form 구조 안에 있습니다.(인증 후 리다이렉트 주소 - rtn_url, PASS 인증 회사 고유 아이디 - cpid는 고정값이니 생략하겠습니다.) 따라서 파싱을 할 때 먼저 extractHTMLSource에서 body만 파싱 했습니다. 그리고 querySelector로 input[name=req_info] 만 파싱했습니다. 그러면 'value=test_secret_key_abc12345==>'만 남는데 이것도 String 클래스의 메서드인 replaceAll로 모두 제거하여 알맹이 req_info만 남습니다.

 

 

이 알맹이 req_info를 담은 res 변수는 encodingRequestURL 함수로 인해 rtn_url, cpid와 같이 동봉되어 새로운 창에서 POST 됩니다. 

 

var outputAsUint8List =
    encodingRequestURL(requestURL: res);

List encodingRequestURL({String requestURL}) {
  var stringTemp =
      'req_info=$requestURL&rtn_url=https://www.test_redirectionpage.com&cpid=testcompany';
  stringTemp = stringTemp.replaceAll('+', '%2b');
  var result = encodingUINT(data: stringTemp);
  return result;
}

initialUrlRequest: URLRequest(
  url: Uri.parse(
      passURL),
  method: 'POST',
  headers: {
    'Content-Type':
        'application/x-www-form-urlencoded'
  },
  body: outputAsUint8List,
),

 

여기서 중요한 점은 stringTemp 변수의 형태입니다. 원래는 req_info에 '+'가 있었지만 PASS 측에서 이것을 공백으로 받아들였습니다. 그 이유는 우리는 암호화 방식이 유니코드 형식이지만 PASS 쪽에서는 EUC-KR으로 디코딩해서 생긴 결과였습니다. 따라서 '+'가 연산자 오버라이딩되지 않게 '+'의 유니코드인 '$%2b'로 살짝 바꾸어서 동봉하였습니다. 

 

그리고 body에 encodingRequestURL로 형식화한 req_info, rtn_url, cpid를 함께 넣었습니다. 마지막으로 PASS로 Post 할 때 파일 전송 형식인 'x-www-form-urlencoded'로 헤더를 넣었습니다. 꼭 이렇게 형식을 작성해야 합니다.

 

결론

 

정리하면 순서는 다음과 같습니다.

 

1. onLoadStop에서 해당 html 코드를 크롤링한다.
2. 크롤링 함수를 통해 새로고침마다 변하는 req_info의 값을 파싱 한다.
3. onCreateWindow 메서드에 showDialog를 구성한다.
4. 구성된 showDialog에 새로운 InAppWebView 클래스를 넣는다.
5. 새 창 InAppWebView의 initialURLRequest에 url과 body에 rtn_url, cpid, req_info를 같이 넣어 POST 한다. 

 

 

전체 소스코드

 

import 'dart:async';

import 'dart:convert';
import 'dart:developer';

import 'package:html/dom.dart' as Document;
import 'dart:io';
import 'package:html/parser.dart' as parser;
import 'package:cp949/cp949.dart' as cp949;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();

  if (Platform.isAndroid) {
    await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
  }

  runApp(MaterialApp(
    home: MyApp(),
  ));
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final GlobalKey webViewKey = GlobalKey();

  InAppWebViewController webViewController;

  PullToRefreshController pullToRefreshController;
  String url = "";
  double progress = 0;
  final urlController = TextEditingController();
  String res;
  InAppWebViewGroupOptions options;

  String id;
  String URL;
  String passURL;
  String afterPASSRedirectURL;
  @override
  void initState() {
    super.initState();

    id = 'testcode11';
  URL = 'https://www.mainURL.com;
    passURL = 'https://dev.mobile-ok.com/popup/common/hscert.jsp';
    afterPASSRedirectURL = 'https://www.redirectionURL.com;
    options = InAppWebViewGroupOptions(
        crossPlatform: InAppWebViewOptions(
          javaScriptEnabled: true,
          javaScriptCanOpenWindowsAutomatically: true,
          useShouldOverrideUrlLoading: true,
          mediaPlaybackRequiresUserGesture: false,
        ),
        android: AndroidInAppWebViewOptions(
          useHybridComposition: true,
        ),
        ios: IOSInAppWebViewOptions(
          allowsInlineMediaPlayback: true,
        ));

    pullToRefreshController = PullToRefreshController(
      options: PullToRefreshOptions(
        color: Colors.blue,
      ),
      onRefresh: () async {
        if (Platform.isAndroid) {
          webViewController?.reload();
        } else if (Platform.isIOS) {
          webViewController?.loadUrl(
              urlRequest: URLRequest(url: await webViewController?.getUrl()));
        }
      },
    );
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          appBar: AppBar(title: const Text("일반 InAppWebView 테스트 코드")),
          body: SafeArea(
              child: Column(children: <Widget>[

            Expanded(
              child: Stack(
                children: [
                  InAppWebView(
                    key: webViewKey,
                    initialUrlRequest: URLRequest(
                        url: Uri.parse(
                 URL)),
                    initialOptions: options,
                    pullToRefreshController: pullToRefreshController,
                    
                    onLoadStart: (controller, url) async {
                      setState(() {
                        this.url = url.toString();
                        urlController.text = this.url;
                      });
                    },
                    onCreateWindow: (controller, action) {
                      var outputAsUint8List =
                          encodingRequestURL(requestURL: res);

                      return showDialog(
                          barrierDismissible: true,
                          context: context,
                          builder: (context) {
                            return InAppWebView(
                              initialOptions: options,
                              pullToRefreshController: pullToRefreshController,
                              onWebViewCreated: (controller) async {
                                print(await controller.getProgress());
                              },
                              onConsoleMessage: (controller, message) {
                                print("message is:" + message.message);
                              },



                              
                              initialUrlRequest: URLRequest(
                                url: Uri.parse(
                                    passURL),
                                method: 'POST',
                                headers: {
                                  'Content-Type':
                                      'application/x-www-form-urlencoded'
                                },
                                body: outputAsUint8List,
                              ),
                            );
                          });
                    },
                    androidOnPermissionRequest:
                        (controller, origin, resources) async {
                      return PermissionRequestResponse(
                          resources: resources,
                          action: PermissionRequestResponseAction.GRANT);
                    },
                    shouldOverrideUrlLoading:
                        (controller, navigationAction) async {
                      var uri = navigationAction.request.url;

                      if (![
                        "http",
                        "https",
                        "file",
                        "chrome",
                        "data",
                        "javascript",
                        "about"
                      ].contains(uri.scheme)) {
                        if (await canLaunch(url)) {
                          // Launch the App
                          await launch(
                            url,
                          );
                          // and cancel the request
                          return NavigationActionPolicy.CANCEL;
                        }
                      }

                      return NavigationActionPolicy.ALLOW;
                    },
                    onLoadStop: (controller, url) async {
                      ///PASS에 들어갈시 req_info 암호화 키를 추출하는 부분.

                      List<Document.Element> keywordElements = await extractHTMLSource(controller);

                      var link;
                      for (var element in keywordElements) {
                        link = element.querySelector('input[name=req_info]');

                        res = link.outerHtml.split("value=").last.split(">")[0];

                        res = res.replaceAll("\"", "");
                        res = Uri.decodeFull(res);
                      }

                      pullToRefreshController.endRefreshing();
                      setState(() {
                        this.url = url.toString();
                        urlController.text = this.url;
                      });
                    },
                    onLoadError: (controller, url, code, message) {
                      pullToRefreshController.endRefreshing();
                    },

                  ),

                ],
              ),
            ),

          ]))),
    );
  }
}

List encodingRequestURL({String requestURL}) {
  var stringTemp =
      'req_info=$requestURL&rtn_url=https://www.redirectionPageURL.php&cpid=passID';
  stringTemp = stringTemp.replaceAll('+', '%2b');
  var result = encodingUINT(data: stringTemp);
  return result;
}



List encodingUINT({String data}) {
  var temp = Uint8List.fromList(utf8.encode(data));
  String s = new String.fromCharCodes(temp);

  var outputAsUint8List = new Uint8List.fromList(s.codeUnits);
  return outputAsUint8List;
}

Future<List> extractHTMLSource(var controller)async{
  var html = await controller.evaluateJavascript(
      source:
      "window.document.getElementsByTagName('html')[0].outerHTML;");

  Document.Document document = parser.parse(html);

  List<Document.Element> keywordElements =
  document.querySelectorAll('body');
  return keywordElements;
}

 

이상 앱개발지식나눔이였습니다.