2024. 3. 17. 20:28ㆍ모바일어플개발/Flutter
안녕하세요~ totally 개발자입니다.
이 포스팅에서는 보통 결제할 때 카드 번호, 비밀번호 등 사용할 수 있는 보안 키패드 적용하는 방법에 대해서 알아보도록 하겠습니다.
패키지는 https://pub.dev/packages/flutter_secure_keyboard
flutter_secure_keyboard를 사용하면 됩니다.
flutter_secure_keyboard | Flutter package
Mobile secure keyboard to prevent KeyLogger attack and screen capture.
pub.dev
이 보안 키패드를 적용하면 KeyLogger 공격, 스크린 캡쳐 등 여러 가지 위험을 방지할 수 있게 됩니다.
바로 실습해보도록 하겠습니다.
Step 1: pubspec.yaml에 flutter_secure_keyboard: ^3.0.0을 추가하고 flutter pub get해서 반영해줍니다.

Step 2: main.dart에 아래 import 구문을 추가해줍니다.
import 'package:flutter_secure_keyboard/flutter_secure_keyboard.dart';

Step 3: 아래 화면처럼 TestView 클래스를 만들어주시고 아래 빨간색 박스처럼 필요한 변수들을 선언해줍니다. (공식 문서대로 진행했습니다) 여기에서 final _secureKeyboardController는 보안 키패드를 위한 키패드 컨트롤러 변수이고 _passwordEditor, _pinCodeEditor은 입력 받은 텍스트를 위한 컨트롤러 변수이며, _passwordTextFieldFocusNode와 _pinCodeTextFieldFocusNode는 초점을 조정할 수 있도록 해주는 변수입니다.

Step 4: Scaffold 위젯을 WithSecureKeyboard 위젯으로 감싸주고 controller에 _secureKeyboardController를 전달합니다.

Step 5: 문자 및 숫자 비밀번호를 위한 보안 키패드 위젯입니다. 아래에서 아셔야 하는 부분은 enableInteractiveSelection: false와 obscureText: true인데 Creates a [FormField] that contains a [TextField].
enableInteractiveSelection의 목적은 아래와 같습니다.
/// Whether to enable user interface affordances for changing the
/// text selection.
///
/// For example, setting this to true will enable features such as
/// long-pressing the TextField to select text and show the
/// cut/copy/paste menu, and tapping to move the text caret.
///
/// When this is false, the text selection cannot be adjusted by
/// the user, text cannot be copied, and the user cannot paste into
/// the text field from the clipboard.
///
/// Defaults to true.
요약하면 enableInteractiveSelection은 true의 경우에 사용자가 입력 글자를 길게 누르는 경우, 복사할 수 있는 기능, 텍스트 범위 선택 등을 가능하도록 해주고, false라면 복사 및 선택 등을 허용하지 않는 것입니다. 즉 보안 키패드의 목적을 위해서 false로 설정해야 합니다.
obscureText: true는 obscure가 불분명한, 보기 어렵게 하다 등의 뜻이므로 obscureText: true는 텍스트를 *으로 치환해서 표시하는 것입니다.

Step 6: 숫자 키패드를 위한 핀 코드 TextField 위젯도 아래처럼 만들어줍니다. (맨 아래에 전체 코드 첨부하였습니다) 여기서 꼭 변경하실 부분은 아래 104번째 라인에 있는 type: SecureKeyboardType.NUMERIC입니다.

Step 7: 그 후 dispose 메소드를 넣어줍니다. dispose는 해당 navigation를 벗어날 때(해당 화면이 종료될 때) 호출됩니다.

Step 8: 아래 Column의 children에 위에서 선언했던 위젯들을 넣어줍니다. 중간에 공백을 위한 SizedBox도 넣어줍니다.

Step 9: 실행한 모습입니다. password쪽 textfield를 누르면 아래처럼 나옵니다.

Step 10: PinCode쪽 textfield를 누르면 아래처럼 나옵니다.

[전체 소스 코드]
import 'package:flutter_secure_keyboard/flutter_secure_keyboard.dart'; | |
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Test App', | |
home: const TestView(), | |
debugShowCheckedModeBanner: false, | |
theme: ThemeData( | |
useMaterial3: true, | |
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), | |
), | |
); | |
} | |
} | |
class TestView extends StatefulWidget { | |
const TestView({super.key}); | |
@override | |
State<TestView> createState() => _TestViewState(); | |
} | |
class _TestViewState extends State<TestView> { | |
final _secureKeyboardController = SecureKeyboardController(); | |
final _passwordEditor = TextEditingController(); | |
final _passwordTextFieldFocusNode = FocusNode(); | |
final _pinCodeEditor = TextEditingController(); | |
final _pinCodeTextFieldFocusNode = FocusNode(); | |
@override | |
Widget build(BuildContext context) { | |
return WithSecureKeyboard( | |
controller: _secureKeyboardController, | |
child: Scaffold( | |
appBar: AppBar( | |
title: const Text("Secure Keyboard Test"), | |
centerTitle: true, | |
), | |
body: SingleChildScrollView( | |
child: Column( | |
children: [ | |
_buildPasswordTextField(), | |
const SizedBox(height: 15.0), | |
_buildPinCodeTextField(), | |
], | |
), | |
), | |
), | |
); | |
} | |
Widget _buildPasswordTextField() { | |
return Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
const Text('Password'), | |
TextFormField( | |
controller: _passwordEditor, | |
focusNode: _passwordTextFieldFocusNode, | |
enableInteractiveSelection: false, | |
obscureText: true, | |
onTap: () { | |
_secureKeyboardController.show( | |
type: SecureKeyboardType.ALPHA_NUMERIC, | |
focusNode: _passwordTextFieldFocusNode, | |
initText: _passwordEditor.text, | |
hintText: 'password', | |
onCharCodesChanged: (List<int> charCodes) { | |
_passwordEditor.text = String.fromCharCodes(charCodes); | |
}, | |
); | |
}, | |
), | |
], | |
); | |
} | |
Widget _buildPinCodeTextField() { | |
return Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
const Text('PinCode'), | |
TextFormField( | |
controller: _pinCodeEditor, | |
focusNode: _pinCodeTextFieldFocusNode, | |
enableInteractiveSelection: false, | |
obscureText: true, | |
onTap: () { | |
_secureKeyboardController.show( | |
type: SecureKeyboardType.NUMERIC, | |
focusNode: _pinCodeTextFieldFocusNode, | |
initText: _pinCodeEditor.text, | |
hintText: 'pinCode', | |
onDoneKeyPressed: (List<int> charCodes) { | |
_pinCodeEditor.text = String.fromCharCodes(charCodes); | |
}, | |
); | |
}, | |
), | |
], | |
); | |
} | |
@override | |
void dispose() { | |
super.dispose(); | |
_secureKeyboardController.dispose(); | |
_passwordEditor.dispose(); | |
_pinCodeEditor.dispose(); | |
} | |
} |