728x90

플러터에서 로그인은 시도하는 코드를 살펴보자.

 

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($Rarray("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
블로그 이미지

Link2Me

,