728x90

인터넷(Network)을 통해서 전송된 JSON 포맷 문자열을 Custom Class 로 역직렬화(Deserialization)해서 사용자 클래스로 변환하는 과정을 거쳐야 한다.
사용자 클래스를 네트웍을 통해 전송하기 위해서는 문자열로 변환을 해야 한다.
과거에는 XML 이라는 포멧으로 전송 했으나, 현재는 JSON 포멧 문자열로 전송한다.

Dart 언어에서 역직렬화(Deserialization)하는 과정은 
ㅇ JSON 포맷 String → Map<String, dynamic>
ㅇ Map<String, dynamic> → Custom Class
2단계 과정을 거친다.

Custom Class를 JSON 포멧 문자열로 직렬화(Serialization)하는 과정
ㅇ Custom Class → Map<String, dynamic>
ㅇ Map<String, dynamic> → JSON 포맷 String
2단계 과정을 거친다.

먼저 서버에 있는 JSON 포멧 데이터를 역직렬화하기 위한 Model Class 를 구현해야 한다.

구현 방법은 Model 클래스를 직접 구현하는 방법과 Code generation 라이브러리를 이용하는 방법이 있다.

1. 직접 구현 방법

- 모델 클래스 내부에 fromJson과 toJson을 정의한다.

class LoginResponse {
  final String status;
  final String message;
  final UserInfo? userinfo;
 
  const LoginResponse({
    required this.status,
    required this.message,
    this.userinfo,
  });
 
  factory LoginResponse.fromJson(Map<String, dynamic> json) {
    return LoginResponse(
      status: json['status'] as String,
      message: json['message'] as String,
      userinfo: json['userinfo'== null
          ? null
          : UserInfo.fromJson(json['userinfo'] as Map<String, dynamic>),
    );
  }
 
  Map<String, dynamic> toJson() {
    return {
      "status"this.status,
      "message"this.message,
      "userinfo"this.userinfo,
    };
  }
}
 
class UserInfo {
  final String userNM;
  final String mobileNO;
  final int profileImg;
 
  const UserInfo({
    required this.userNM,
    required this.mobileNO,
    required this.profileImg,
  });
 
  factory UserInfo.fromJson(Map<String, dynamic> json) {
    return UserInfo(
      userNM: json['userNM'] as String,
      mobileNO: json['mobileNO'] as String,
      profileImg: json['profileImg'] as int,
    );
  }
 
  Map<String, dynamic> toMap() {
    return {
      'userNM'this.userNM,
      'mobileNO'this.mobileNO,
      'profileImg'this.profileImg,
    };
  }
 
}

 

2. Code generation 라이브러리 이용방법

- 라이브러리 설치

flutter pub add json_annotation
flutter pub add -d build_runner json_serializable

 

- 모델 클래스 내부에 fromJson과 toJson을 아래와 같은 규칙으로 정의한다.

import 'package:json_annotation/json_annotation.dart';
 
part 'userinfo.g.dart';
 
@JsonSerializable()
class UserInfo {
  final String userNM;
  final String mobileNO;
  final int profileImg;
 
  const UserInfo({
    required this.userNM,
    required this.mobileNO,
    required this.profileImg,
  });
 
  factory UserInfo.fromJson(Map<String, dynamic> json) => _$UserInfoFromJson(json);
 
  Map<String, dynamic> toJson() => _$UserInfoToJson(this);
}
 

 

import 'package:json_annotation/json_annotation.dart';
import 'package:login_ex/user/model/userinfo.dart';
 
part 'login_response.g.dart';
 
@JsonSerializable()
class LoginResponse {
  final String status;
  final String message;
  final UserInfo? userinfo;
 
  const LoginResponse({
    required this.status,
    required this.message,
    this.userinfo,
  });
 
  factory LoginResponse.fromJson(Map<String, dynamic> json) => _$LoginResponseFromJson(json);
 
  Map<String, dynamic> toJson() => _$LoginResponseToJson(this);
}
 

 

와 같이 한 다음에 터미널 창에서 dart run build_runner build 를 하면 자동으로 추가된다.

 

 

  /// 네트워크 응답 문자열
  String jsonString = '{"name": "길동", "age": 33}';
 
  /// JSON 포맷 String -> Map<String, dynamic>
  Map<String, dynamic> jsonMap = jsonDecode(jsonString);
  print(jsonMap);
 
  /// Map<String, dynamic> -> Person
  Person person = Person.fromJson(jsonMap);
  print(person);

 

일반적인 JSON decoding에는 dart:convert의 jsonDecode() 메서드를 사용한다. 

이 방법은 간단하고 빠르게 decoding할 수 있다는 장점이 있지만 decoding된 Map<String, dynamic> 타입을 다시 적절한 Data Type으로 변환하는 부분을 수작업으로 진행해야 한다.

 

하지만 위와 같은 과정을 처리하지 않고,

서버로 데이터를 보내고 결과를 응답받는 과정을 손쉽게 처리하는 방법은 Retrofit 라이브러리를 이용하면 된다.

import 'package:dio/dio.dart' hide Headers;
import 'package:retrofit/retrofit.dart';
import 'package:login_ex/common/repository/retrofit_url.dart';
import 'package:login_ex/contact/model/contact_result.dart';
import 'package:login_ex/user/model/login_response.dart';
 
part 'rest_client.g.dart';
 
@RestApi(baseUrl: RetrofitURL.baseUrl)
abstract class RestClient {
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
 
  @POST(RetrofitURL.mLogin)
  @FormUrlEncoded()
  Future<LoginResponse> userLogin(
      @Field() String keyword,
      @Field() String userID,
      @Field() String password,
      @Field() String uID,
      @Field() String mfoneNO,
      );
}

 

Retrofit 라이브러리 동작과정에 대한 설명은 Android 에서 설명한 게시글이 있으니 참고하면 도움된다.

로그인에 필요한 데이터를 POST 방식으로 전송하고, 결과를 LoginResponse 로 받는다.

 

riverpod 상태관리 패키지 활용 예제

- Dio 라이브러리와 Retrofit 을 처리하는 RestClient 를 보고 활용하면 된다.

- CustLogInterceptor는 필요하면 추가하지만 필요없는 경우에는 추가할 필요가 없다.

- 서버에서 로그인 정보가 일치하면 Session 정보를 생성하고 생성된 Session 정보를 쿠키로 저장하고 다른 URL 전송시 Cookie 정보를 실어서 보내도록 처리하는 로직을 CustLogInterceptor에 구현했다.

import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:login_ex/common/repository/custlog_interceptor.dart';
import 'package:login_ex/common/repository/rest_client.dart';
 
final secureStorageProvider =
    Provider<FlutterSecureStorage>((ref) => const FlutterSecureStorage());
 
final dioProvider = Provider<Dio>((ref) {
    final dio = Dio();
 
    final storage = ref.read(secureStorageProvider);
 
    dio.interceptors.add(LogInterceptor());
    dio.interceptors.add(
        CustLogInterceptor(storage: storage,),
    );
    return dio;
});
 
final restClientProvider = Provider<RestClient>((ref) {
    final dio = ref.read(dioProvider);
    final repository = RestClient(dio);
    return repository;
});

 

 

 

LoginView 처리는 View 와 ViewModel를 분리하여 구현한다.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:login_ex/common/view/base_view.dart';
import 'package:login_ex/user/view/login_view_model.dart';
 
class LoginView extends ConsumerWidget {
  String userid = '';
  String password = '';
 
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return BaseView(
      viewModelProvider: loginViewModelProvider,
      builder: (ref, viewModel, state) => DefaultLayout(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const SizedBox(height: 50.0),
              Image.asset(
                'assets/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: () => viewModel.login(context, userid, password),
                style: ElevatedButton.styleFrom(
                  backgroundColor: PRIMARY_COLOR,
                ),
                child: const Text(
                  '로그인',
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
 

 

final loginViewModelProvider =
    NotifierProvider.autoDispose<LoginViewModel, LoginViewState>(
        LoginViewModel.new);
 
class LoginViewModel extends BaseViewModel<LoginViewState> {
  @override
  LoginViewState build() {
    return LoginViewState(isBusy: false);
  }
 
  Future<void> login(
      BuildContext context, String userid, String password) async {
    if (userid.isNotEmpty && password.isNotEmpty) {
      state = state.copyWith(isBusy: true);
 
      final storage = ref.read(secureStorageProvider);
      final mobileNO = await storage.read(key: MNumber) ?? '';
      final deviceId = await storage.read(key: DeviceID) ?? '';
 
      LoginResponse result = await ref.read(restClientProvider).userLogin(
          Crypto.AES_encrypt(Crypto.URLkey()),
          Crypto.AES_encrypt(userid),
          Crypto.RSA_encrypt(password),
          deviceId,
          mobileNO);
 
      if (result.status.contains('success')) {
        Utils.showSnackBar(context, '로그인 성공');
 
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (_) => const ContactResultProviderPage(),
          ),
        );
        state = state.copyWith(isBusy: false);
      } else {
        Utils.showAlert(context, result.status, result.message);
        state = state.copyWith(isBusy: false);
      }
    } else {
      if (userid.isEmpty) {
        Utils.showSnackBar(context, '아이디를 입력하세요');
        state = state.copyWith(isBusy: false);
        return;
      }
      if (password.isEmpty) {
        Utils.showSnackBar(context, '비밀번호를 입력하세요');
        state = state.copyWith(isBusy: false);
        return;
      }
    }
 
  }
}
 

 

상속받은 BaseView 에 대한 코드는 제가 직접 구현한 것이 아니기 때문에 오픈할 수 없다.

riverpod 기반 BaseView 구현 코드를 얻고 싶은 분은 인프런 사이트 강좌 DevStory님의 "Flutter 앱 개발 실전" 강좌 수강을 추천한다. Flutter 에 대한 기본 개념을 확실하게 잡을 수 있고, 이를 바탕으로 응용하는데 도움이 될 것이다.

블로그 이미지

Link2Me

,

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

728x90

로그인 구현 함수를 별도로 구현하지 않고, Retrofit 라이브러를 활용하여 간단하게 로그인 하는 코드로 변경했다.

 

import 'package:dio/dio.dart' hide Headers;
import 'package:login_ex/contact/repository/retrofit_url.dart';
import 'package:login_ex/contact/model/contact_result.dart';
import 'package:login_ex/user/model/login_response.dart';
import 'package:retrofit/retrofit.dart';
 
part 'rest_client.g.dart';
 
@RestApi(baseUrl: RetrofitURL.baseUrl)
abstract class RestClient {
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
 
  @POST(RetrofitURL.mLogin)
  @FormUrlEncoded()
  Future<LoginResponse> userLogin(
      @Field() String keyword,
      @Field() String userID,
      @Field() String password,
      @Field() String uID,
      @Field() String mfoneNO,
      );
 
  @POST(RetrofitURL.contactData)
  @FormUrlEncoded()
  Future<ContactResult> postContactList(
      @Field() String keyword,
      @Field() String search,
      );
}

로그인 함수를 추가하고 터미널창에서 dart run build_runner build 를 다시 해서 rest_client.g.dart 파일을 업데이트 한다.

 

login_screen.dart 파일 코드

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:platform_device_id_v3/platform_device_id.dart';
import 'package:login_ex/common/repository/logging.dart';
import 'package:login_ex/common/res/data.dart';
import 'package:login_ex/common/utils/utils.dart';
import 'package:login_ex/contact/repository/rest_client.dart';
import 'package:login_ex/contact/view/contact_result_page.dart';
import 'package:login_ex/user/model/login_response.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/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 = '';
  late String _deviceId;
  late final RestClient restClient;
 
  @override
  void initState() {
    super.initState();
    getDeviceUniqueId();
   loginRestInit();
  }
 
  Future<void> loginRestInit() async {
    Dio dio = Dio();
    dio.interceptors.add(LogInterceptor());
    dio.interceptors.add(const CustLogInterceptor(storage: storage));
    restClient = RestClient(dio);
  }
 
  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);
 
                    LoginResponse result = await restClient.userLogin(
                        Crypto.AES_encrypt(Crypto.URLkey()),
                        Crypto.AES_encrypt(userid),
                        Crypto.RSA_encrypt(password),
                        _deviceId,
                        mobileNO!
                    );
                    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(
                  '로그인',
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
 
}

 

 

PHP Session 을 처리하는 핵심코드를 구현하느라고 하루종일 삽질을 엄청했다.

구글링을 해도 제대로 된 것을 찾기가 어려웠고, CookieJar 커스텀으로 해결하려고 했으나 실패했다.

인프런 유료강좌 "[코드팩토리][중급] Flutter 진짜 실전"  강좌에서 언급된 코드가 생각이 나서 Access_Token 처리 대신에 Cookie 를 실어보내는 코드로 구현해보면 되겠다 싶어서 테스트해봤더니 성공했다.

블로그 이미지

Link2Me

,
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

,
728x90

Flutter 에서 AES 256 으로 암호화하고 PHP에서 복호화하는 코드 클래스를 구현했다.

Java for Android 에서 활용하던 코드를 기반으로 Flutter에 맞게 변환 및 구현했다.

Flutter 와 PHP 간에 암호화 및 복호화 처리하는데 정상 동작됨을 확인했다.

 

AES_key는 32 byte 이고 IV는 16 byte 이다.

서로 다른 언어간에도 IV 값이 일치해야 한다.

import 'package:intl/intl.dart';
import 'package:encrypt/encrypt.dart' as enc;
 
// flutter pub add encrypt
// flutter pub add intl
// flutter pub get 하면 라이브러리 추가된다.
 
class CIpheR {
  static final String URLKEY = "jgsysyksr897213579";
  static final String AES_Key = "abcdefghijtuvwxyz1234klmnopqrs56";
 
  static String URLkey() {
    DateTime now = DateTime.now();
    DateFormat formatter = DateFormat('yyyyMMdd');
    String date = formatter.format(now);
    String keyword = '${URLKEY}${date}';
    return keyword;
  }
 
  static String AES_encrypt(String value) {
    final key = enc.Key.fromUtf8(AES_Key);
    final iv = enc.IV.allZerosOfLength(16);
 
    final encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
    final encrypted = encrypter.encrypt(value, iv: iv);
    return encrypted.base64;
  }
 
  static String AES_decrypt(String encrypted){
    final key = enc.Key.fromUtf8(AES_Key);
    final iv = enc.IV.allZerosOfLength(16);
 
    final encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
    enc.Encrypted enBase64 = enc.Encrypted.from64(encrypted);
    final decrypted = encrypter.decrypt(enBase64, iv: iv);
    return decrypted;
  }
}
 

 

PHP에서 str_repeat(chr(0), 16) 에 해당하는 Flutter IV 값을 찾아내느라고 애를 먹었다.

final iv = enc.IV.allZerosOfLength(16); // PHP ::: str_repeat(chr(0), 16)

 

PHP 암호화/복호화 클래스

 

<?php
class LoginClass extends DBController {
    // class 자식클래스 extends 부모클래스
    // override : 부모 클래스와 자식 클래스가 같은 메소드를 정의했을 경우 자식 클래스가 우선시된다.
 
    // ########################################################################
    // RSA 암호화 키
    function get_publickey() {
        // 경로 : 절대경로로 설정 필요
        $rsakeyfile = '/home/rsa/key/rsa_pub.pem';
        
        $fp = fopen($rsakeyfile,"r");
        $key = "";
        while(!feof($fp)) {
            $key .= fgets($fp,4096);
        } 
        fclose($fp);
 
        $key = preg_replace('/\r\n|\r|\n/','',$key);
        return $key;
    } 
 
    function get_privatekey() {
        // 경로 : 절대경로로 설정 필요
        $rsakeyfile = '/home/rsa/key/rsa_pri.pem';
        
        $fp = fopen($rsakeyfile,"r");
        $key = "";
        while(!feof($fp)) {
            $key .= fgets($fp,4096);
        } 
        fclose($fp);
 
        $key = preg_replace('/\r\n|\r|\n/','',$key);
        return $key;
    } 
 
    // RSA 복호화
    function rsa_decrypt($str){
        $private_key = "file:////home/rsa/key/rsa_pri.pem";
        $openssl_pk = openssl_get_privatekey($private_key);
        $_plaintext = base64_decode(explode(':::'$str)[0]);
        openssl_private_decrypt($_plaintext$return$openssl_pk);
        return $return;
    }
 
    // AES 암호화
    function AES_encrypt($plain_text){
        global $key;
        $encryptedMessage = openssl_encrypt($plain_text"aes-256-cbc"$keytrue,str_repeat(chr(0), 16));
        return base64_encode($encryptedMessage);
    }
 
    // AES 복호화
    function AES_decrypt($base64_text){
        global $key;
        $decryptedMessage = openssl_decrypt(base64_decode($base64_text), "aes-256-cbc"$keytrue, str_repeat(chr(0), 16));
        return $decryptedMessage;
    }
 
}
?>
 
<?php
class DBController {
    protected $db// 변수를 선언한 클래스와 상속받은 클래스에서 참조할 수 있다.
 
    // 생성자
    function __construct() {
        $this->db = $this->connectDB();
        // construct 메소드는 객체가 생성(인스턴스화)될 때 자동으로 실행되는 특수한 메소드다.
    }
 
    // 소멸자(destructor)
    function __destruct() {
        mysqli_close($this->connectDB());
    }
 
    private function connectDB() {
        require_once 'dbinfo.php';
        // MySQLi 객체지향 DB 연결
        $conn = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE);
        $conn->set_charset("utf8");
        return $conn// return database handler
    }
 
    public function putDbArray($sql) {
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        $result = $stmt->get_result();
        return $result;
    }
 
    public function getDbArray($sql) {
        $stmt = $this->db->prepare($sql);
        $stmt->execute();
        $result = $stmt->get_result();
        return $result;
    }
 
}
?>
 
<?php
// DB 정보 입력
define("DB_HOST""localhost");
define("DB_USER""andsample");
define("DB_PASSWORD""sample!#%");
define("DB_DATABASE""android");
 
/* 사용자 권한 등록
use mysql;
create user andsample@localhost identified by 'sample!#%';
grant all privileges on android.* to andsample@localhost;
flush privileges;
*/
?>

 

 

'Flutter 앱 > Network' 카테고리의 다른 글

Flutter Login with PHP Session #2 (Retrofit)  (0) 2023.12.24
Flutter Login with PHP Session #1  (0) 2023.12.24
ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
블로그 이미지

Link2Me

,
728x90

FutureBuilder 와 Dio 라이브러리를 이용하여 서버에 있는 데이터를 가져와서 화면에 보여주는 ListView 예제이다.

 

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:rest/restaurant/component/restaurant_card.dart';
 
import '../../common/const/data.dart';
 
class RestaurantScreen extends StatelessWidget {
  const RestaurantScreen({Key? key}) : super(key: key);
 
  Future<List> pagenateRestaurant() async {
    final dio = Dio();
    final accessToken = await storage.read(key: ACCESS_TOKEN_KEY);
    //print('accessToken read ::: ${accessToken}');
 
    final resp = await dio.get(
      'http://$realIp/restaurant',
      options: Options(headers: {
        'authorization''Bearer $accessToken',
      }),
    );
 
    return resp.data['data'];
  }
 
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: FutureBuilder<List>(
            future: pagenateRestaurant(),
            builder: (context, AsyncSnapshot<List> snapshot) {
              //print(snapshot.error);
              //print(snapshot.data);
              if (!snapshot.hasData) {
                return Container();
              }
 
              return ListView.separated(
                itemCount: snapshot.data!.length,
                itemBuilder: (_, index) {
                  final item = snapshot.data![index];
                  return RestaurantCard(
                    image: Image.network(
                      'http://$realIp${item['thumbUrl']}',
                      fit: BoxFit.cover,
                    ),
                    name: item['name'],
                    tags: List<String>.from(item['tags']),
                    ratingsCount: item['ratingsCount'],
                    deliveryTime: item['deliveryTime'],
                    deliveryFee: item['deliveryFee'],
                    ratings: item['ratings'],
                  );
                },
                separatorBuilder: (_, index) {
                  return SizedBox(height: 16.0);
                },
              );
            },
          ),
        ),
      ),
    );
  }
}
 

 

 

위 코드를 아래와 같이 Model 화하여 수정한 코드이다.

 
enum RestaurantPriceRange {
  expensive,
  medium,
  cheap,
}
 
class RestaurantModel {
  final String id;
  final String name;
  final String thumbUrl;
  final List<String> tags;
  final RestaurantPriceRange priceRange;
  final double ratings;
  final int ratingsCount;
  final int deliveryTime;
  final int deliveryFee;
 
  RestaurantModel({
    required this.id,
    required this.name,
    required this.thumbUrl,
    required this.tags,
    required this.priceRange,
    required this.ratings,
    required this.ratingsCount,
    required this.deliveryTime,
    required this.deliveryFee,
  });
}
 

 

 

 
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:rest/restaurant/component/restaurant_card.dart';
import 'package:rest/restaurant/model/restaurant_model.dart';
 
import '../../common/const/data.dart';
 
class RestaurantScreen extends StatelessWidget {
  const RestaurantScreen({Key? key}) : super(key: key);
 
  Future<List> pagenateRestaurant() async {
    final dio = Dio();
    final accessToken = await storage.read(key: ACCESS_TOKEN_KEY);
    //print('accessToken read ::: ${accessToken}');
 
    final resp = await dio.get(
      'http://$realIp/restaurant',
      options: Options(headers: {
        'authorization''Bearer $accessToken',
      }),
    );
 
    return resp.data['data'];
  }
 
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: FutureBuilder<List>(
            future: pagenateRestaurant(),
            builder: (context, AsyncSnapshot<List> snapshot) {
              //print(snapshot.error);
              //print(snapshot.data);
              if (!snapshot.hasData) {
                return Container();
              }
 
              return ListView.separated(
                itemCount: snapshot.data!.length,
                itemBuilder: (_, index) {
                  final item = snapshot.data![index];
                  final pItem = RestaurantModel(
                    id: item['id'],
                    name: item['name'],
                    thumbUrl: 'http://$realIp${item['thumbUrl']}',
                    tags: List<String>.from(item['tags']),
                    priceRange: RestaurantPriceRange.values.firstWhere(
                      (e) => e.name == item['priceRange'],
                    ),
                    ratings: item['ratings'],
                    ratingsCount: item['ratingsCount'],
                    deliveryTime: item['deliveryTime'],
                    deliveryFee: item['deliveryFee'],
                  );
 
                  return RestaurantCard(
                    image: Image.network(
                      pItem.thumbUrl,
                      fit: BoxFit.cover,
                    ),
                    name: pItem.name,
                    tags: pItem.tags,
                    ratingsCount: pItem.ratingsCount,
                    deliveryTime: pItem.deliveryTime,
                    deliveryFee: pItem.deliveryFee,
                    ratings: pItem.ratings,
                  );
                },
                separatorBuilder: (_, index) {
                  return SizedBox(height: 16.0);
                },
              );
            },
          ),
        ),
      ),
    );
  }
}
 

 

 

'Flutter 앱 > Network' 카테고리의 다른 글

Flutter Login with PHP Session #1  (0) 2023.12.24
Flutter AES256 with PHP  (0) 2023.12.19
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
블로그 이미지

Link2Me

,
728x90

구글 검색하면 kakao Login 또는 Firebase 기반 로그인 기능 구현에 대한 예제들이 검색된다.

이 예제는 기업/클라우드 서버와의 통신을 전제로 한 로그인 구현 기본 사항이다.

아직 더 해결해야 할 사항들로는 RSA 암호화 통신, 세션기반 처리 기능 구현이다.

더 고민하고 테스트해서 해결되면 다른 게시글에 추가 작성할 예정이다.

 

Flutter 에서 로그인 처리를 이해하는데 한걸음 더 나아간 예제라고 보면 된다.

로그인에 대한 이해를 하기 위해서는 서버에서 응답하는 메시지의 형태가 어떤지 알아야 한다.

 

1. 서버의 PHP 코드 예제

- 로그인 핵심 코드는 loginClass 에 정의되어 있다.

- 여기서는 JSON encode 처리 메시지에 대한 이해 관점으로 전체적인 흐름을 이해하면 된다.

더보기

 

<?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']);
        $password = $c->rsa_decrypt($_POST['password']);
 
        // 동일 휴대폰 번호로 로그인하면 phoneSE 업데이트 처리
        $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;
}
?>

 

 

2. JSON 메시지 형태

- PHP 서버 코드가 제공하는 JSON 메시지

{
  "status": "로그인 에러",
  "message": "로그인 정보가 일치하지 않습니다",
  "userinfo": null
}
 
{
  "status": "단말 불일치",
  "message": "등록 단말 정보가 일치하지 않습니다. 관리자에게 문의하시기 바랍니다.",
  "userinfo": null
}
 
{
  "status": "success",
  "message": "",
  "userinfo": {
    "userNM": "강감찬",
    "mobileNO": "01092010852",
    "profileImg": 1
  }
}

- 로그인이 성공했을 때 제공되는 JSON 메시지

- 로그인이 실패했을 때 제공되는 JSON 메시지

로 구분해서 UserResult 클래스를 만들도록 서버의 PHP 코드를 고려했다.

- 메시지를 보면 userinfo 가 null 일 수도 있고, 값을 반환할 수도 있다는 점을 확인하고

  UserResult 클래스 구현시 이 사항을 반드시 고려해야 한다.

 

3. UserResult 클래스 구현

- Java 에서 구현하는 클래스로 접근해서 엄청난 삽질과 어려움을 겪었다.

- Flutter 에서 JSON 메시지 파싱처리 개념을 제대로 이해해야 문제없이 구현할 수 있다.

  반드시 factory 생성자를 구현해야 한다.

- factory 생성자 : 새로운 인스턴스를 생성하지 않는 생성자를 구현할 때 사용한다.

  . 기존에 이미 생성된 인스턴스가 있다면 return 하여 재사용한다.

  . 하나의 클래스에서 하나의 인스턴스만 생성한다.(싱글톤 패턴)

  . 서브 클래스 인스턴스를 리턴할 때 사용할 수 있다.

  . factory constructor 에서는 this에 접근할 수 없다.

class UserResult {
  final String status;
  final String message;
  final UserInfo? userinfo;
 
  UserResult(
      {required this.status, required this.message, required this.userinfo});
 
  // 반드시 정의 해주어야 서버에서 전달받은 데이터를 처리하더라.
  factory UserResult.fromJson(Map<String, dynamic> parsedJson) {
    return UserResult(
      status: parsedJson['status'],
      message: parsedJson['message'],
      userinfo: parsedJson['userinfo'== null
          ? null
          : UserInfo.fromJson(parsedJson['userinfo']),
      // UserInfo 가 null 일 수도 있고, 값이 들어있을 수도 있는 걸 삼항연산자로 처리
    );
  }
}
 
class UserInfo {
  final String userNM;
  final String mobileNO;
  final int profileImg;
 
  UserInfo({
    required this.userNM,
    required this.mobileNO,
    required this.profileImg,
  });
 
  factory UserInfo.fromJson(Map<String, dynamic> parsedJson) {
    return UserInfo(
      userNM: parsedJson['userNM'],
      mobileNO: parsedJson['mobileNO'],
      profileImg: parsedJson['profileImg'],
    );
  }
 
  Map<String, dynamic> toJson() => {
        "userNM": userNM,
        "mobileNO": mobileNO,
        "profileImg": profileImg,
      };
}
 

 

// android/app/src/main/AndroidManifest.xml 에 퍼미션 추가 
 
<uses-permission android:name="android.permission.INTERNET"/>
 
// pubspec.yaml 파일 
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  http: ^0.13.4
  dio: ^4.0.6
  logging: ^1.0.2
  get: ^4.6.5
  awesome_dialog: ^2.2.1
  validators: ^3.0.0
  shared_preferences: ^2.0.15
  platform_device_id: ^1.0.1
  mobile_number: ^1.0.4
  cached_network_image: ^3.2.1
  fluttertoast: ^8.0.9
  intl: ^0.17.0
  encrypt: ^5.0.1
  json_annotation: ^4.6.0
 

 

4. DIO 라이브러리를 이용한 Login Class 정의

import 'dart:convert';
 
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:login_ex01/model/user_result.dart';
 
import '../api/api.dart';
import '../api/logging.dart';
 
abstract class ILogin {
  Future<UserResult> login(String keyword, String userID, String password,
      String uID, String mfoneNO);
}
 
class LoginService extends ILogin {
  @override
  Future<UserResult> login(String keyword, String userID, String password,
      String uID, String mfoneNO) async {
    BaseOptions options = BaseOptions(
      baseUrl: Api.baseUrl,
      connectTimeout: 5000,
      receiveTimeout: 3000,
    );
    Dio dio = Dio(options);
    dio.interceptors.add(Logging());
 
    FormData formData = FormData.fromMap({
      "keyword": keyword,
      "userID": userID,
      "password": password,
      "uID": uID,
      "mfoneNO": mfoneNO
    });
 
    final response = await dio.post(Api.mLogin, data: formData);
      final Map<String, dynamic> body = response.data;      
      print("${body}"); // 로그 분석 목적
 
      UserResult result = UserResult.fromJson(body);
      return result;
    } else {
      return UserResult(status: "fail", message: "fail", userinfo: null);
    }
  }
}

 

5. 로그인 구현 예제

- 로그인 UI 구성이 포함되어 있으며, 로그인시 전달될 파라미터로 폰의 고유한 ID (UUID), 폰의 휴대폰번호 수집 로직, 서버와 주고받을 keyword 일치 여부를 통한 통신의 기본 체크 사항이 포함되어 있다.

- AES 암호화/복호화 메소드는 다른 게시글을 참조하면 된다.

- 로그인 성공하면 다른 UI 로의 이동은 포함하지 않았다.

  GetX 를 이용한 로그인/로그아웃 처리 로직을 구현 시 처리할 예정이다.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
 
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:login_ex01/model/user_result.dart';
import 'package:platform_device_id/platform_device_id.dart';
import 'package:mobile_number/mobile_number.dart';
 
import '../api/api_cipher.dart';
import '../service/login_service.dart';
import '../utils/utils.dart';
 
class LoginView extends StatefulWidget {
  const LoginView({Key? key}) : super(key: key);
 
  @override
  State<LoginView> createState() => _LoginViewState();
}
 
class _LoginViewState extends State<LoginView> {
  final _formKey = GlobalKey<FormState>(); // 이 코드는 학습해야 함.
 
  final ILogin _login = LoginService();
  final _userIDController = TextEditingController();
  final _passwordController = TextEditingController();
 
  late String _deviceId;
 
  String _mobileNumber = '';
  List<SimCard> _simCard = <SimCard>[];
 
  late String _userID;
  late String _password;
 
  var userIDFocusNode;
  var passwdFocusNode;
 
  @override
  void initState() {
    super.initState();
    getDeviceUniqueId();
    MobileNumber.listenPhonePermission((isPermissionGranted) {
      if (isPermissionGranted) {
        initMobileNumberState();
      } else {}
    });
 
    initMobileNumberState();
  }
 
  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");
    });
  }
 
  Future<void> initMobileNumberState() async {
    if (!await MobileNumber.hasPhonePermission) {
      await MobileNumber.requestPhonePermission;
      return;
    }
    String mobileNumber = '';
    try {
      mobileNumber = (await MobileNumber.mobileNumber)!;
      _simCard = (await MobileNumber.getSimCards)!;
    } on PlatformException catch (e) {
      debugPrint("Failed to get mobile number because of '${e.message}'");
    }
 
    if (!mounted) return;
 
    setState(() {
      _mobileNumber = mobileNumber;
      _mobileNumber = Utils.getPhoneNumber(_mobileNumber);
      print("mobileNumber -> $_mobileNumber");
    });
  }
 
  Widget fillCards() {
    List<Widget> widgets = _simCard
        .map((SimCard sim) => Text(
            'Sim Card Number: (${sim.countryPhonePrefix}) - ${sim.number}\nCarrier Name: ${sim.carrierName}\nCountry Iso: ${sim.countryIso}\nDisplay Name: ${sim.displayName}\nSim Slot Index: ${sim.slotIndex}\n\n'))
        .toList();
    return Column(children: widgets);
  }
 
  onSubmit() async {
    if (_userIDController.text.trim().isEmpty ||
        _passwordController.text.trim().isEmpty) {
      if (_userIDController.text.trim().isEmpty) {
        Utils.showSnackBar(context, '아이디를 입력하세요');
        userIDFocusNode.requestFocus();
        return;
      }
 
      if (_passwordController.text.trim().isEmpty) {
        Utils.showSnackBar(context, '비밀번호를 입력하세요');
        passwdFocusNode.requestFocus();
        return;
      }
    } else {
      String keyword = CIpheR.AES_encrypt(CIpheR.URLkey());
      String passwd = _passwordController.text.trim();
     // passwd = CIpheR.rsaEncrypt(passwd);
 
      UserResult result = await _login.login(
          keyword,
          CIpheR.AES_encrypt(_userIDController.text.trim()),
          passwd,
          _deviceId,
          _mobileNumber);
 
      if (result.status.contains("success")) {
        Utils.showSnackBar(context, '로그인 성공');
      } else {
        Utils.showAlert(context, result.status, result.message);
      }
    }
  }
 
  @override
  void dispose() {
    userIDFocusNode.dispose();
    passwdFocusNode.dispose();
    super.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Login Page'),
      ),
      body: Center(
        child: SingleChildScrollView(
          child: Container(
            padding: const EdgeInsets.all(16.0),
            child: Form(
              key: _formKey,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  Container(
                    padding: const EdgeInsets.fromLTRB(1010100),
                    child: TextFormField(
                      controller: _userIDController,
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        labelText: 'UserID',
                      ),
                      focusNode: userIDFocusNode,
                      textInputAction: TextInputAction.next,
                    ),
                  ),
                  SizedBox(
                    height: 30,
                  ),
                  Container(
                    padding: const EdgeInsets.fromLTRB(1010100),
                    child: TextFormField(
                      controller: _passwordController,
                      obscureText: true,
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        labelText: '비밀번호',
                      ),
                      focusNode: passwdFocusNode,
                      onEditingComplete: () async {},
                    ),
                  ),
                  SizedBox(
                    height: 30,
                  ),
                  Container(
                    height: 50,
                    padding: const EdgeInsets.fromLTRB(500500),
                    child: ElevatedButton(
                        child: Text(
                          'Login',
                          style: TextStyle(fontSize: 20.0),
                        ),
                        onPressed: onSubmit),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}
 

 

6. API 정의

class Api {
  static const baseUrl = "https://www.abc.com";
  static const mLogin = "/androidSample/loginChk.php";
 
  static const api_key = ""// 서버와 통신하기 위한 AES 암호화 키
  static const String URLKEY = ''// 서버 인증 수단 체크 keyword
 
  // 완벽 통신 위해서는 RSA 암호화 통신 및 Session 통신 처리해야 한다.
}

- 보안을 고려한 통신 처리 로직 구현에 많은 시간을 할애할 수 밖에 없다.

아직 해결해야 할 사항이 많지만 Flutter 기본 로그인 처리에 대한 이해를 하고 기록해 둔다.

'Flutter 앱 > Network' 카테고리의 다른 글

Flutter AES256 with PHP  (0) 2023.12.19
ListView.separated 예제  (0) 2023.11.20
Session vs JWT  (0) 2022.07.22
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
Flutter DIO 라이브러리 예제2  (0) 2022.07.02
블로그 이미지

Link2Me

,

Session vs JWT

Flutter 앱/Network 2022. 7. 22. 20:49
728x90

Session_Based Authentication

- 세션을 이용하면 서버 Side 에 로그인된 사용자의 모든 정보를 저장한다.

- 동일 아이디 다중 접속시 기존 접속 강제 로그아웃 기능을 직접 구현해야 한다.

 

Token-Based Authentication

- 토큰 기반 인증은 클라이언트 Side에 저장되기 때문에 별도의 DB없이 로그인 구현이 가능하다.

- 서버는 토큰의 유효성 여부만 체크하므로 서버의 부담이 적다.

- 로그인된 사용자 통제가 어렵고, 강제 로그아웃 기능은 사용할 수 없다.

- 해커에게 토큰을 탈취당하면, 해당 토큰을 무효화시킬 대응이 쉽지 않다.

- JWT(JSON Web Token)는 속성 정보 (Claim)를 JSON 데이터 구조로 표현한 토큰으로 RFC 7519 표준이다.

- JWT는 서버와 클라이언트 간 정보를 주고 받을 때 Http 리퀘스트 헤더에 JSON 토큰을 넣은 후 서버는 별도의 인증 과정없이 헤더에 포함되어 있는 JWT 정보를 통해 인증한다.

- JWT는 세 파트로 나누어지며, 각 파트는 점로 구분하여 xxxxx.yyyyy.zzzzz 이런식으로 표현된다. 순서대로 헤더 (Header), 페이로드 (Payload), 서명 (Sinature)로 구성한다.

 

일반적으로 JWT 를 사용하면 아래와 같은 순서로 진행된다.

1. 클라이언트 사용자가 아이디, 패스워드를 통해 웹 서비스 인증

2. 서버에서 서명된(signed) JWT 를 생성하여 클라이언트에 응답으로 돌려주기

3. 클라이언트가 서버에 데이터를 추가적으로 요구할 때 JWT를 HTTP Header 에 첨부한다.

4. 서버에서 클라이언트로부터 전달받은 JWT를 검증한다.

 

 

 

'Flutter 앱 > Network' 카테고리의 다른 글

ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
Flutter DIO 라이브러리 예제2  (0) 2022.07.02
Flutter DIO 라이브러리 예제 1  (0) 2022.06.29
블로그 이미지

Link2Me

,
728x90

UI 구현 코드는 다른 게시글에 올릴 예정이므로 여기서는 언급하지 않는다.

Flutter Login 처리하는 로직 구현을 했는데 완벽하지는 않다.

아직 배우는 중이라 기록해두고 나중에 제대로 수정하련다.

import 'dart:convert';
 
import 'package:dio/dio.dart';
import 'package:login_ex01/model/user_result.dart';
 
import '../api/api.dart';
import '../model/user_model.dart';
import '../utils/utils.dart';
 
class LoginService extends ILogin {
  @override
  Future<UserResult> login(String keyword, String userID, String password,
      String uID, String mfoneNO) async {
    BaseOptions options = BaseOptions(
      baseUrl: Api.baseUrl,
      connectTimeout: 3000,
      receiveTimeout: 3000,
    );
    Dio dio = Dio(options);
 
    var formData = FormData.fromMap({
      "keyword": keyword,
      "userID": userID,
      "password": password,
      "uID": uID,
      "mfoneNO": mfoneNO
    });
 
    Response response;
 
    response = await dio.post(Api.mLogin, data: formData);
    if (response.statusCode == 200) {
      final body = json.decode(response.data);
      print("${body}");
      return body;
    } else {
      return UserResult(status: "fail", message: "fail", userinfo: null);
    }
  }
}
 
abstract class ILogin {
  Future<UserResult> login(String keyword, String userID, String password,
      String uID, String mfoneNO);
}
 

 

 

class UserResult {
  final String status;
  final String message;
  final UserInfo? userinfo;
 
  UserResult({
    required this.status,
    required this.message,
    required this.userinfo
  });
}
 
class UserInfo {
  final String userNM;
  final String mobileNO;
  final String profileImg;
 
  UserInfo({
    required this.userNM,
    required this.mobileNO,
    required this.profileImg,
  });
 
  factory UserInfo.fromJson(Map<String, dynamic> parsedJson) {
    return UserInfo(
      userNM: parsedJson['userNM'],
      mobileNO: parsedJson['mobileNO'],
      profileImg: parsedJson['profileImg'],
    );
  }
 
  Map<String, dynamic> toJson() =>
      {
        "userNM": userNM,
        "mobileNO": mobileNO,
        "profileImg": profileImg,
      };
 
}
 

 

 

import 'package:flutter/material.dart';
 
class Utils {
  static Future<void> showAlert(BuildContext context, String title, String message) async {
    return showDialog<void>(
      context: context,
      barrierDismissible: false// user must tap button!
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text(title),
          content: SingleChildScrollView(
            child: ListBody(
              children: <Widget>[
                Text(message),
              ],
            ),
          ),
          actions: <Widget>[
            TextButton(
              child: const Text('OK'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
 
  static String getPhoneNumber(String phoneNumber) {
    if (phoneNumber.startsWith("82+82")) {
      phoneNumber = phoneNumber.replaceAll("82+82""0");
    }
    return phoneNumber.replaceAll("-""");
  }
}
 

 

 

'Flutter 앱 > Network' 카테고리의 다른 글

ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
Flutter DIO 라이브러리 예제2  (0) 2022.07.02
Flutter DIO 라이브러리 예제 1  (0) 2022.06.29
블로그 이미지

Link2Me

,
728x90

DIO 라이브러리 interceptors 사용 시 Null Safety 전후 변동사항

initializeInterceptors(){
  _dio.interceptors.add(InterceptorsWrapper(
    onError: (error){
      print(error.message);
    },
    onRequest: (request){
      print("${request.method} ${request.path}");
    },
    onResponse: (response){
      print(response.data);
    }
  ));
}
 
// ------ Null Safety 이후 -----------------------------
 
initializeInterceptors() {
  _dio.interceptors.add(InterceptorsWrapper(
    onRequest: (request, handler) async {
      print("${request.method} ${request.path}");
    },
    onResponse: (response, handler) async {
      print(response.data);
    },
    onError: (error, handler) async {
      print(error.message);
    },
  ));
}

 

pubspec.yaml 수정사항 : Null Safety 기준 : 2.12.0

- flutter pub add http 및 flutter pub add dio , flutter pub add json_serializable 을 각각 터미널 창에서 실행

- flutter pub get 를 하면 아래 dependencies 에 최신 버전이 추가된다.

environment:
  sdk: ">=2.17.5 <3.0.0"
 
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  http: ^0.13.4
  dio: ^4.0.6
  json_serializable: ^6.2.0
 
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  flutter_lints: ^2.0.0
  build_runner: ^2.1.11
 
flutter:
 
  uses-material-design: true
  assets:
    - images/photo_base.png
 

 

User 클래스 생성하는 방법이 아래와 같은 방식으로 구글에서 많이 검색된다.

이전 게시글에서 직접 User Class 를 정의한 것과 동일하므로 비교해보는 것도 ....

user.dart 파일

import 'package:json_annotation/json_annotation.dart';
 
part 'user.g.dart';
// 아래와 같이 Class 를 정의하고 터미널 창에서
// flutter pub run build_runner build
// flutter pub get
 
@JsonSerializable()
class User {
  int idx;
  @JsonKey(name : "userNM")
  String userNM;
  @JsonKey(name : "mobileNO")
  String mobileNO;
  @JsonKey(name : "telNO")
  String telNO;
 
  String photo;
 
  User(
      {required this.idx,
        required this.userNM,
        required this.mobileNO,
        required this.telNO,
        required this.photo});
 
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
 
}

 

자동 생성된 user.g.dart 파일

// GENERATED CODE - DO NOT MODIFY BY HAND
 
part of 'user.dart';
 
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
 
User _$UserFromJson(Map<String, dynamic> json) => User(
      idx: json['idx'] as int,
      userNM: json['userNM'] as String,
      mobileNO: json['mobileNO'] as String,
      telNO: json['telNO'] as String,
      photo: json['photo'] as String,
    );
 
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
      'idx': instance.idx,
      'userNM': instance.userNM,
      'mobileNO': instance.mobileNO,
      'telNO': instance.telNO,
      'photo': instance.photo,
    };
 

 

http_service.dart 파일

import 'package:dio/dio.dart';
 
class DioService {
  late final Dio _dio;
  final baseUrl = 'https://www.abc.com';
 
  DioService(){
    BaseOptions options = BaseOptions(
      baseUrl: baseUrl,
      connectTimeout: 3000,
      receiveTimeout: 3000,
    );
 
    _dio = Dio(options);
    initializeInterceptors();
  }
 
  Future<Response> getRequest(String endPoint) async {
    Response response;
 
    try {
      response = await _dio.get(endPoint);
    } on DioError catch (e) {
      print(e.message);
      throw Exception(e.message);
    }
 
    return response;
  }
 
  initializeInterceptors() {
    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (request, handler) async {
        print("${request.method} ${request.path}");
        return handler.next(request);
      },
      onResponse: (response, handler) async {
        print(response.headers);
        print(response.data);
        return handler.next(response);
      },
      onError: (error, handler) async {
        print(error.message);
        return handler.next(error);
      },
    ));
  }
}

 

main.dart 파일

import 'package:dio_sampe2/network/http_service.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'model/user.dart';
 
void main() {
  runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'JSON Data Parsing Demo'),
    );
  }
}
 
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
 
  final String title;
 
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
 
class _MyHomePageState extends State<MyHomePage> {
  bool isLoading = false;
  late List<User> users;
  late DioService dio;
 
  Future getListUser() async {
    Response response;
    try {
      isLoading = true;
      response = await dio.getRequest("/androidSample/getData_Flutter2.php");
      isLoading = false;
 
      if (response.statusCode == 200) {
        setState(() {
          users = response.data.map<User>((json) {
            return User.fromJson(json);
          }).toList();
        });
      } else {
        print("There is some problem status code not 200");
      }
    } on Exception catch (e) {
      isLoading = false;
      print(e);
    }
  }
 
  @override
  void initState() {
    dio = DioService();
    getListUser();
    super.initState();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: isLoading
          ? Center(child: CircularProgressIndicator())
          : users != null
              ? ListView.builder(
                  itemCount: users.length,
                  itemBuilder: (context, index) {
                    final user = users[index];
 
                    return ListTile(
                      title: Text(user.userNM),
                      leading: user.photo.length == 0
                          ? Image.asset('images/photo_base.png')
                          : Image.network(
                              'https://www.abc.com/androidSample/photos/${user.photo}'),
                      subtitle: Text(user.mobileNO),
                    );
                  },
                )
              : Center(
                  child: Text("No User Object"),
                ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {
              setState(() {
                //getServerDataWithHttp();
              });
            },
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
          SizedBox(
            width: 10.0,
            height: 10.0,
          ),
          FloatingActionButton(
            onPressed: () {
              setState(() {
                dio = DioService();
                getListUser();
              });
            },
            tooltip: 'Increment',
            child: const Icon(Icons.remove),
          ),
        ],
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
 
 

 

서버에 있는 List 자료를 가져와서 ListView 에 보여주는 코드 구현의 기본 샘플이 작성되었다.

 

https://github.com/themaaz32/dio_and_json 는 Null Safety 이전 코드인데, 참고하면 도움된다.

 

 

 

 

'Flutter 앱 > Network' 카테고리의 다른 글

ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
Flutter DIO 라이브러리 예제 1  (0) 2022.06.29
블로그 이미지

Link2Me

,
728x90

구글링이나 GitHub 샘플코드를 받아서 보면 Null Safety 이전과 이후의 버전에 따라 동작이 되기도 하고 안되기도 하더라.

라이브러리 버전을 최신버전으로 변경해보면 에러가 발생하는 것이 http, dio 부분에서 걸리는 거 같아서

dio 라이브러리에 대한 이해 부족으로 소스코드 변경에 시간 낭비를 하는 거 같아서 학습해 두고자 한다.

 

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  http: ^0.13.4  # 0.12.2 null safety 이전, 최신 버전 0.13.4
  dio: ^4.0.6   # 3.0.10 null safety 이전, 최신 버전 4.0.6

 

main.dart 파일 import 사항

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:dio/dio.dart';

 

android/app/src/main/AndroidManifest.xml 파일 추가 사항

- 추가 안해도 개발 모드에서 잘 동작하는 걸 확인했다.

<uses-permission android:name="android.permission.INTERNET" />

 

서버에서 데이터 가져오고 파싱처리하는 코드

class _MyHomePageState extends State<MyHomePage> {
  Future getServerDataWithHttp() async {
    try {
      var jsonString = await http.get(
          Uri.parse('https://www.abc.com/androidSample/getData_Flutter.php'));
      if (jsonString.statusCode == 200) {
        print(jsonString);
        var resp = jsonDecode(jsonString.body);
        print(resp);
        UserResult userResult = UserResult.fromJson(resp);
        for (User user in userResult.result) {
          print(user.userNM);
        }
      }
    } catch (e) {
      print('error : ${e}');
    }
  }
 
  Future getServerDataWithDio() async {
    String url = 'https://www.abc.com/androidSample/getData_Flutter.php';
    BaseOptions options = BaseOptions(
      baseUrl: 'https://www.abc.com',
      connectTimeout: 3000,
      receiveTimeout: 3000,
    );
    Dio dio = Dio(options);
    try {
      Response resp = await dio.get(
        "/androidSample/getData_Flutter.php",
        //queryParameters: {"search": "dio"},
      );
      print("Response:");
      print("Status: ${resp.statusCode}");
      print("Header:\n${resp.headers}");
      print("Data:\n${resp.data}");
 
      UserResult userResult = UserResult.fromJson(resp.data);
      for (User user in userResult.result) {
        print(user.userNM);
      }
 
    } catch (e) {
      print("Exception: $e");
    }
 
 
  }
 
  @override
  void initState() {
    super.initState();
    getServerDataWithHttp();
    getServerDataWithDio();
  }
 
  @override
  Widget build(BuildContext context) {  }

 

 

서버에서 가져온 JSON 데이터 구조

{result: [
    {idx: 1, userNM: 개발자, mobileNO: 01000010001, telNO: 0234560001, photo: 1.jpg}, 
    {idx: 2, userNM: 이정은, mobileNO: 01001230001, telNO: , photo: 2.jpg}, 
    {idx: 3, userNM: 김홍길, mobileNO: 01001230002, telNO: , photo: }
  ]
}

 

 

 

class UserResult {
  final List<User> result;
 
  UserResult({required this.result});
 
  factory UserResult.fromJson(Map<String, dynamic> parsedJson) {
 
    var list = parsedJson['result'] as List;
    List<User> usersList = list.map((i) => User.fromJson(i)).toList();
 
    return UserResult(
      result: usersList,
    );
  }
}
 
class User {
  final int idx;
  final String userNM;
  final String mobileNO;
  final String telNO;
  final String photo;
 
  User(
      {required this.idx,
      required this.userNM,
      required this.mobileNO,
      required this.telNO,
      required this.photo});
 
  factory User.fromJson(Map<String, dynamic> parsedJson) {
    return User(
      idx: parsedJson['idx'],
      userNM: parsedJson['userNM'],
      mobileNO: parsedJson['mobileNO'],
      telNO: parsedJson['telNO'],
      photo: parsedJson['photo'],
    );
  }
 
  Map<String, dynamic> toJson() => {
    "idx": idx,
    "userNM": userNM,
    "moboileNO": mobileNO,
    "telNO": telNO,
    "photo": photo,
  };
}
 

factory는 싱글톤 패턴을 사용할 때 쓰는 예약어이다.

dart 공식문서에 새로운 인스턴스를 생성하지 않는 생성자를 구현할 때 factory 키워드를 사용하라고 명시되어 있다.

factory 의 특징

- 이전에 이미 생성된 인스턴스가 있다면 원래 값을 return하여 재사용한다.

- 하나의 클래스에서 하나의 인스턴스만 사용한다.

- 서브 클래스를 리턴할 때 사용할 수 있다.

- factory 생성자에서는 this에 접근할 수 없다.

 

 

 

서버에서 넘겨주는 데이터 형식을 아래와 같이 JSON 배열 데이터로 변경했다.

[
 {idx: 1, userNM: 개발자, mobileNO: 01000010001, telNO: 0234560001, photo: 1.jpg}, 
 {idx: 2, userNM: 이정은, mobileNO: 01001230001, telNO: , photo: 2.jpg}, 
 {idx: 3, userNM: 김홍길, mobileNO: 01001230002, telNO: , photo: }
]

 

DIO를 이용해 데이터를 변환하는 로직

Future getServerDataWithDio() async {
  BaseOptions options = BaseOptions(
    baseUrl: 'https://www.abc.com',
    connectTimeout: 3000,
    receiveTimeout: 3000,
  );
  Dio dio = Dio(options);
  try {
    Response resp = await dio.get(
      "/androidSample/getData_Flutter.php",
      //queryParameters: {"search": "dio"},
    );
    print("Response:");
    print("Status: ${resp.statusCode}");
    print("Header:\n${resp.headers}");
    print("Data:\n${resp.data}");
 
    List<User> users = resp.data.map<User>((parsedJson) {
      return User.fromJson(parsedJson);
    }).toList();
 
    for (User user in users) {
      print(user.userNM);
    }
 
  } catch (e) {
    print("Exception: $e");
  }
}

 

 

POST 방식으로 데이터 통신을 할 경우 코드 예제이다.

formData 변수 사용법으로 사용하니까 제대로 동작되더라.

Future getServerDataWithDio() async {
  BaseOptions options = BaseOptions(
    baseUrl: 'https://www.abc.com',
    connectTimeout: 3000,
    receiveTimeout: 3000,
  );
  Dio dio = Dio(options);
 
  var formData = FormData.fromMap({
    "search""이정은",
  });
 
  try {
    Response resp = await dio.post(
      "/androidSample/putData_Flutter.php",
      data: formData,
    );
    // print("Response:");
    // print("Status: ${resp.statusCode}");
    // print("Header:\n${resp.headers}");
    print("Data:\n${resp.data}");
 
    // UserResult userResult = UserResult.fromJson(resp.data);
    List<User> users = UserResult.fromJson(resp.data).result;
    for (User user in users) {
      print(user.userNM);
    }
  } catch (e) {
    print("Exception: $e");
  }
}
 

 

이해를 돕기 위해 서버 코드에 사용한 PHP 코드를 첨부한다.

<?php
if(!isset($_SESSION)) {
    session_start();
}
 
//ini_set("display_startup_errors", 1);
//ini_set("display_errors", 1);
//error_reporting(E_ALL);
 
// 파일을 직접 실행하면 동작되지 않도록 하기 위해서
if(isset($_POST) && $_SERVER['REQUEST_METHOD'== "POST"){
    @extract($_POST); // POST 전송으로 전달받은 값 처리
 
    require_once 'phpclass/dbconnect.php';
    require_once 'phpclass/loginClass.php';
    $c = new LoginClass();
    $column ="idx,userNM,mobileNO,telNO,photo";
    $sql = "select idx,userNM,mobileNO,telNO,photo from Person";
    if(!empty($search)) {
        $where = "userNM LIKE '%".$search."%' or mobileNO LIKE '%".$search."%'";
    } else {
        $where = "";
    }
 
    if(strlen($where) > 0){
        $sql .= " where ".$where;
    }
 
    $R = array(); // 결과 담을 변수 생성
    $result = $c->putDbArray($sql);
    while($row = $result->fetch_assoc()) {
        if($row['photo'== NULL) {
            $row['photo'= "";
        } else {
            $path = "./photos/".$row['photo'];
            if(!file_exists($path)) {
                $row['photo'= "";
            }
        }
        array_push($R, $row);
    }
    header("Cache-Control: no-cache, must-revalidate");
    header("Content-type: application/json; charset=UTF-8");
 
    echo json_encode(array('result'=>$R)); //배열-문자열등을 json형식의 '문자열'로 변환
}
?>

 

추가적으로 계속 유사 예제로 테스트하고 적어둘 예정이다.

 

'Flutter 앱 > Network' 카테고리의 다른 글

ListView.separated 예제  (0) 2023.11.20
Flutter Login Example  (0) 2022.07.25
Session vs JWT  (0) 2022.07.22
Flutter Login 로직 구현 예제 (오류 포함)  (0) 2022.07.22
Flutter DIO 라이브러리 예제2  (0) 2022.07.02
블로그 이미지

Link2Me

,