플러터에서 로그인은 시도하는 코드를 살펴보자.
Android APP 개발시에는 서버 세션을 처리하기 위해서 CookieJar를 Custom 으로 implements 하여 해결을 했다.
Flutter 에서 이 방법으로 해결 시도해보다가 실패하여 포기했다.
아래 주석처리한 CustLogInterceptor 파일을 활용하여 PHP Session 을 FlutterSecureStorage 에 저장하고 Load하여 보내는 방법으로 해결했다.
dio.interceptors.add(CookieManager(cookieJar)); 코드는 사용되지 않으므로 삭제하면 된다.
최근에는 2차 인증은 기본으로 적용하고 있다. PHP Session 생성을 하는 위치가 매우 중요하다.
1차 인증 로그인 이후 2차 인증(Google OTP, SMS OTP 등)을 성공하고 난 이후에 PHP Session 을 생성해야 한다.
아래 코드는 1차 인증 후 PHP Session 을 생성하는 예제로 구현했다.
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:login_ex/contact/repository/retrofit_url.dart';
import 'package:login_ex/user/model/login_response.dart';
import 'package:login_ex/user/model/user_request.dart';
abstract class UserRepository {
Future<LoginResponse> login(UserRequest req);
}
class LoginService extends UserRepository {
@override
Future<LoginResponse> login(UserRequest req) async {
BaseOptions options = BaseOptions(
baseUrl: RetrofitURL.baseUrl,
);
Dio dio = Dio(options);
final cookieJar = CookieJar();
dio.interceptors.add(CookieManager(cookieJar));
dio.interceptors.add(LogInterceptor());
//dio.interceptors.add(CustLogInterceptor(storage: storage));
FormData formData = FormData.fromMap({
"keyword": req.keyword,
"userID": req.userID,
"password": req.password,
"uID": req.uID,
"mfoneNO": req.mfoneNO
});
final response = await dio.post(RetrofitURL.mLogin, data: formData);
if (response.statusCode == 200) {
LoginResponse result = LoginResponse.fromJson(response.data);
return result;
} else {
return const LoginResponse(status: "fail", message: "fail", userinfo: null);
}
}
}
|
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:login_ex/common/res/data.dart';
import 'package:login_ex/common/utils/utils.dart';
import 'package:login_ex/contact/view/contact_result_page.dart';
import 'package:login_ex/user/model/login_response.dart';
import 'package:login_ex/user/model/user_request.dart';
import 'package:platform_device_id_v3/platform_device_id.dart';
import 'package:login_ex/common/component/custom_text_form_field.dart';
import 'package:login_ex/common/res/colors.dart';
import 'package:login_ex/common/view/default_layout.dart';
import 'package:login_ex/user/repository/login_service.dart';
import 'package:login_ex/user/repository/crypto_api.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
String userid = '';
String password = '';
final UserRepository loginRepository = LoginService();
late String _deviceId;
@override
void initState() {
super.initState();
getDeviceUniqueId();
}
Future<void> getDeviceUniqueId() async {
String? deviceId;
try {
deviceId = await PlatformDeviceId.getDeviceId;
} on PlatformException {
deviceId = 'Failed to get deviceId.';
}
if (!mounted) return;
setState(() {
_deviceId = deviceId!;
print("deviceId -> $_deviceId");
});
}
@override
Widget build(BuildContext context) {
return DefaultLayout(
child: SafeArea(
top: true,
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 50.0),
Image.asset(
'asset/img/logo/logo.png',
width: MediaQuery.of(context).size.width ,
),
const SizedBox(height: 16.0),
CustomTextFormField(
hintText: '아이디를 입력하세요',
obscureText: false,
onChanged: (String value) {
userid = value.trim();
},
),
const SizedBox(height: 16.0),
CustomTextFormField(
hintText: '비밀번호를 입력하세요',
obscureText: true,
onChanged: (String value) {
password = value.trim();
},
),
const SizedBox(height: 16.0),
ElevatedButton(
onPressed: () async {
if(userid.isNotEmpty && password.isNotEmpty){
final mobileNO = await storage.read(key: MNumber);
UserRequest req = UserRequest(
keyword: Crypto.AES_encrypt(Crypto.URLkey()),
userID: Crypto.AES_encrypt(userid),
password: Crypto.RSA_encrypt(password),
uID: _deviceId,
mfoneNO: mobileNO!,
);
LoginResponse result = await loginRepository.login(req);
if(result.status.contains('success')){
Utils.showSnackBar(context, '로그인 성공');
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const ContactResultPage(),
),
);
} else {
Utils.showAlert(context, result.status, result.message);
}
} else {
if(userid.isEmpty) {
Utils.showSnackBar(context, '아이디를 입력하세요');
return;
}
if(password.isEmpty){
Utils.showSnackBar(context, '비밀번호를 입력하세요');
return;
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: PRIMARY_COLOR,
),
child: const Text(
'로그인',
),
),
],
),
),
),
);
}
}
|
로그인을 위한 코드와 로그인 화면은 위와 같다.
로그인을 시도하고 로그인은 성공하고 난 이후의 로그를 살펴보자.
위의 코드에서 일부러 loginChk.php 에서 로그인 성공이 되더라도 APP에서 서버의 Session 값을 저장하지 않았다.
로그를 보면 loginChk.php 파일에서 set-cookie 값이 넘어온 값과 ContactList.php 에서 넘어온 set-cookie 값이 서로 다르다는 걸 확인할 수 있다.
APP에서는 서버에서 전달받은 set-cookie 값을 저장해서 이 값을 header 정보에 다시 실어보내야 서버에서 생성한 Session 일치 여부를 확인할 수 있다.
loginChk.php 파일은 어떻게 구성되어 있는지 살펴보자.
- 서버와 APP간에 키 정보가 있는지 확인하고, 키가 일치해야만 다음 단계로 진행된다.
- 어떤 userID로 로그인을 시도하는지 모르도록 AES256 암호화/복호화 처리를 했다.
- 비밀번호는 반드시 RSA 암호화/복호화를 해야 보안검증에 통과된다. (기업 사용 용도)
- 아래 예시코드는 완벽한 예제는 아니다.
개발 테스트용으로 구현한 코드이기 때문에 필요한 사항만 넣었다.
- APP에서 폰 번호를 획득하여, 서버 DB에 있는 인사정보 휴대폰번호와 불일치하는 경우에는
사용하지 못하도록 처리하는 서버 함수를 구현하면 된다.
<?php
// 파일을 직접 실행하는 비정상적 동작을 방지 하기 위한 목적
if(isset($_POST) && $_SERVER['REQUEST_METHOD'] == "POST"){
@extract($_POST); // $_POST['loginID'] 라고 쓰지 않고, $loginID 라고 써도 인식되게 함
if(isset($userID) && !empty($userID) && isset($password) && !empty($password)) {
require_once 'config/config.php';
require_once 'phpclass/dbconnect.php';
require_once 'phpclass/loginClass.php';
$c = new LoginClass();
header("Cache-Control: no-cache, must-revalidate");
header("Content-type: application/json; charset=UTF-8");
//키워드 확인
if(!isset($_POST['keyword'])){
$result = array(
'status' => "key fail",
'message' => "서버 KEY 정보가 없습니다.",
'userinfo' => null
);
echo json_encode($result);
exit;
}
$keyword=$c->AES_decrypt($_POST['keyword']);
//키워드 일치 확인
if(strcmp($keyword,$mykey)<>0){
$result = array(
'status' => "key fail",
'message' => "서버와 단말의 KEY가 일치하지 않습니다.",
'userinfo' => null
);
echo json_encode($result);
exit;
}
$userID = $c->AES_decrypt($_POST['userID']); // userID AES256 복호화
$password = $c->rsa_decrypt($_POST['password']); // 비밀번호 RSA 복호화
// 동일 번호로 로그인하면 phoneSE 업데이트 처리(2022.5.30)
$c->LoginUserPhoneChk($userID,$password,$uID,$mfoneNO);
$rs = $c->LoginUserChk($userID,$password,$uID);
if($rs > 0){
$user = $c->getUser($userID, $password);
if ($user != false) {
$c->regUserPhone($userID,$mfoneNO);
if(!isset($_SESSION)) {
session_start();
}
$_SESSION['userID'] = $user['userID'];
$_SESSION['userNM'] = $user['userNM'];
$_SESSION['admin'] = $user['admin'];
$row = array("userNM"=>$user['userNM'],"mobileNO"=>$user['mobileNO'],"profileImg"=>$user['idx']);
$status = "success";
$message = "";
$userinfo = $row;
} else {
$status = "로그인 에러";
$message = "다시 한번 시도하시기 바랍니다.";
$userinfo = null;
}
} else if($rs === -1){
$status = "단말 불일치";
$message = '등록 단말 정보가 일치하지 않습니다. 관리자에게 문의하시기 바랍니다.';
$userinfo = null;
} else {
$status = "로그인 에러";
$message = '로그인 정보가 일치하지 않습니다';
$userinfo = null;
}
$result = array(
'status' => $status,
'message' => $message,
'userinfo' => $userinfo
);
echo json_encode($result);
}
} else { // 비정상적인 접속인 경우
echo 0; // loginChk.php 파일을 직접 실행할 경우에는 화면에 0을 찍어준다.
exit;
}
?>
|
ContactList.php 파일 코드
- 세션 정보가 일치하는지 여부를 체크하는 로직이 추가되어 있다.
- 세부 설명은 아래 코드에 포함된 주석 정보를 보면 이해될 것이다.
<?php
if(!isset($_SESSION)) {
session_start();
}
// 파일을 직접 실행하면 동작되지 않도록 하기 위해서
if(isset($_POST) && $_SERVER['REQUEST_METHOD'] == "POST"){
@extract($_POST); // POST 전송으로 전달받은 값 처리
require_once 'config/config.php';
require_once 'phpclass/dbconnect.php';
require_once 'phpclass/loginClass.php';
$c = new LoginClass();
header("Cache-Control: no-cache, must-revalidate");
header("Content-type: application/json");
// 로그인 이후 생성된 SESSION 정보가 불일치하면 아래코드에서 걸려 이 정보를 전송한다.
if(!(isset($_SESSION['userID']) && !empty($_SESSION['userID']))){
$result = array(
'status' => "Session 불일치",
'message' => "세션 정보가 일치하지 않습니다.",
'addrinfo' => null
);
echo json_encode($result);
exit;
}
//키워드 확인
if(!isset($_POST['keyword'])){
$result = array(
'status' => "key fail",
'message' => "서버 키 정보가 없습니다.",
'addrinfo' => null
);
echo json_encode($result);
exit;
}
$keyword=$c->AES_decrypt($_POST['keyword']);
//키워드 일치 확인
if(strcmp($keyword,$mykey)<>0){
$result = array(
'status' => "key fail",
'message' => "서버와 단말의 KEY가 일치하지 않습니다.",
'addrinfo' => null
);
echo json_encode($result);
exit;
}
$sql = "select idx,userNM,mobileNO,telNO,photo from Person "; // 화면에 출력할 칼럼 발췌
if(!empty($search)) {
$sql .= "where userNM LIKE '%".$search."%' or mobileNO LIKE '%".$search."%'";
}
$R = array(); // 결과 담을 변수 생성
$result = $c->putDbArray($sql);
while($row = $result->fetch_assoc()) {
$photo_path = './photos/'.$row['photo'];
if(file_exists($photo_path)) { // 실제 사진 이미지 파일이 존재하는 여부 확인
$row['photo'];
} else {
$row['photo']=null;
}
$row['checkBoxState'] = false;
// 중요한 정보는 AES256 암호화해서 전송해야 하지만 개발용이기 때문에 평문 형태로 전송 처리
array_push($R, array("idx"=>$row['idx'],"userNM"=>$row['userNM'],"mobileNO"=>$row['mobileNO'],
"telNO"=>$row['telNO'],"photo"=>$row['photo'],"checkBoxState"=>$row['checkBoxState']));
}
$status = "success";
$message = "";
$addrinfo = $R; // 전체 ArrayList 데이터
$result = array(
'status' => $status,
'message' => $message,
'addrinfo' => $addrinfo
);
echo json_encode($result);
}
?>
|
여기까지 1단계 과정을 적어둔다.
'Flutter 앱 > Network' 카테고리의 다른 글
Flutter Login with PHP #3 (플러터 세션 처리 코드) (0) | 2023.12.27 |
---|---|
Flutter Login with PHP Session #2 (Retrofit) (0) | 2023.12.24 |
Flutter AES256 with PHP (0) | 2023.12.19 |
ListView.separated 예제 (0) | 2023.11.20 |
Flutter Login Example (0) | 2022.07.25 |