본문 바로가기
Study/Knowledge

[Flutter] 사용자 인증방식의 종류

개발을 하다보면 다양한 방식의 사용자 인증방식이 있다는걸 알 수 있습니다.

서버가 사용자를 인증하는 방식은 어떻게 진화해왔고, 지금 우리가 주목하는 JWT, OAuth방식에 대해서 자세히 알아보겠습니다

 

 

1. 인증 방식의 진화

웹 인증방식은 초기에는 기본적으로 HTTP 인증을 사용했지만, 보안적인 취약점이 존재하고 비효율적이라는 문제점이 있었습니다.

그리하여 점차적으로 보안성을 강화하고, 효율적으로 인증을 처리할 수 있는 새로운 인증 방식들이 개발되어 왔습니다.

 

먼저, 기존의 HTTP 인증 방식에서는 인증 정보가 평문으로 전송되어 보안상의 취약점이 존재했습니다.

이를 보완하기 위해 HTTPS 프로토콜을 사용하는 SSL/TLS 인증 방식이 개발되었고, 현재 많은 웹 사이트에서 사용되고 있습니다.

또한, 기존의 인증 방식은 사용자가 매번 인증 정보를 입력해야하는 불편함이 있었습니다.

 

이를 해결하기 위해 쿠키(Cookie) 기반의 인증 방식이 개발되었고, 현재 대부분의 웹 사이트에서 사용되고 있습니다.

쿠키 기반 인증 방식에서는 사용자의 인증 정보를 웹 브라우저에 저장하고, 서버 측에서는 해당 쿠키를 확인하여 인증을 처리합니다.

 

최근에는 JWT(Json Web Token) OAuth2.0 인증 방식 각광받고 있습니다.

JWT 클라이언트와 서버 간에 토큰을 주고받는 방식으로 인증을 처리합니다.

이를 통해 불필요한 인증 정보 입력의 불편함을 해소할  있습니다.

또한, OAuth2.0 인증 방식은 특정 서비스에 대한 권한을 부여하고 이를 이용하여 인증을 처리하는 방식입니다.

이를 통해 보안성을 강화하고, 효율적인 인증을 처리할  있습니다.

 

 

2.  웹 사용자 인증 방식 종류

웹에서 사용하는 사용자 인증 방식은 다양합니다. 대표적으로 다음과 같은 방식들이 있습니다.

1. Basic Authentication

  • 가장 간단한 인증 방식으로, 사용자의 아이디와 비밀번호를 base64로 인코딩하여 HTTP 요청 헤더에 추가합니다.
  • 인코딩된 값이 외부에 노출되기 때문에 보안성이 매우 낮습니다.

2. Digest Authentication

  • Basic Authentication의 보안성 문제를 해결하기 위한 방식으로, 클라이언트가 요청을 보낼 때 미리 정해진 알고리즘에 따라 서버가 생성한 nonce 값과 함께 인증 요청을 보내야 합니다.
  • nonce 값을 사용하기 때문에 Basic Authentication보다는 보안성이 높지만, 암호화된 비밀번호를 서버에서 저장하고 있어야 하므로 서버 부담이 높습니다.

3. Session Cookies Authentication

  • 세션 쿠키 방식은 사용자 인증 정보를 쿠키라는 작은 데이터 파일에 저장하여 인증을 유지하는 방식입니다.
  • 세션 쿠키 방식은 클라이언트와 서버 사이에서 상태 정보를 유지하기 위해 쿠키를 사용합니다.
  • 세션 쿠키 방식은 사용자 인증 정보가 서버에 저장되기 때문에 보안 측면에서 안전합니다. 
  • 서버에서 사용자 정보를 저장하고 관리해야 하기 때문에 서버 자원을 많이 사용할 수 있습니다.
  • 클라이언트에서 쿠키를 저장하고 관리하기 때문에 쿠키가 탈취될 경우 보안에 취약할 수 있습니다.
  • 이러한 문제점들을 보완하기 위해 JWT 토큰 방식이 등장하게 되었습니다.

4. JWT(Json Web Token) Authentication

  • 서버가 사용자에게 토큰을 발급하고, 이 토큰을 이용해 인증하는 방식입니다.
  • 토큰은 서버에서 발급하고 유효기간이 설정될 수 있으며, 유효기간이 지나면 다시 발급받아야 합니다.
  • 토큰은 클라이언트 측에서 저장하고, 인증 요청시 요청 헤더에 토큰 값을 넣어 전송합니다.

5. OAuth (Open Authentication)

  • 사용자 인증을 위한 규격 중 하나로, 서드파티 애플리케이션에서 로그인 기능을 제공하는 경우 사용됩니다.
  • OAuth를 사용하면 사용자 정보를 제공하는 서버에서는 사용자의 아이디와 비밀번호를 알 필요가 없으며, 사용자 역시 애플리케이션의 계정 정보를 제공하지 않아도 됩니다.

 

각 인증 방식은 서로 다른 보안 수준과 사용 방식을 가지고 있습니다.

따라서 애플리케이션의 보안 요구사항과 사용자 편의성을 고려하여 적절한 인증 방식을 선택해야 합니다.

 

지금부터 각각의 인증방식에 대해서 상세히 알아보겠습니다.

 

 

3. 인증방식의 특징

1)  Basic Authentication

Basic Authentication은 사용자 인증을 위한 가장 간단한 인증 방식 중 하나이며 현재는 거의 사용하지 않습니다.

이 방식은 사용자 ID와 비밀번호를 인코딩한 문자열을 HTTP 요청 헤더의 Authorization 필드에 담아 서버에 전송하는 방식으로 이루어집니다.

인증 과정은 다음과 같이 이루어집니다.

  1. 클라이언트가 서버에 요청을 보냅니다.
  2. 서버는 클라이언트의 요청에 대해 401 Unauthorized 응답을 보냅니다.
  3. 클라이언트는 인증 정보를 담아 서버에 재요청을 보냅니다.
  4. 서버는 Authorization 헤더에서 인증 정보를 추출합니다.
  5. 서버는 추출한 인증 정보와 서버에 저장된 사용자 정보를 비교합니다.
  6. 인증이 성공하면 요청에 대한 적절한 응답을 보냅니다.

헤더에 Authorization 필드를 추가하기 위해 사용자 ID와 비밀번호를 Base64로 인코딩한 문자열을 만들어 헤더에 추가합니다.

이렇게 하면 서버는 Authorization 헤더에서 인증 정보를 추출하고, 클라이언트의 요청이 유효한지를 확인할 수 있습니다.

 

Basic Authentication과 현재 사용하는 JWT와 차이점

Basic Authentication과 JWT 인증 방식은 각각 다른 동작 방식과 사용 용도를 가지고 있습니다.

Basic Authentication은 사용자가 서버에 요청을 보낼 때마다 인증 정보를 함께 보내는 방식입니다. 이때 인증 정보는 사용자의 아이디와 비밀번호를 base64로 인코딩한 문자열입니다. 서버는 이 인증 정보를 받아서 디코딩하고, 사용자의 아이디와 비밀번호를 검증합니다. 이 방식은 간단하고 구현하기 쉽지만, 인증 정보가 요청 헤더에 평문으로 노출되기 때문에 보안에 취약합니다.

반면 JWT 인증 방식은 사용자가 인증에 성공하면 서버에서 JWT 토큰을 발급하고, 이 토큰을 사용자에게 전달합니다. 사용자는 이 토큰을 요청 헤더에 실어서 서버에 요청을 보냅니다. 서버는 이 토큰을 검증하여 사용자를 인증합니다. 이 방식은 Basic Authentication에 비해 보안성이 높고, 유효 기간을 설정할 수 있기 때문에 보안 상태를 유지할 수 있습니다. 또한, JWT 토큰에는 사용자의 정보를 넣을 수 있기 때문에, 로그인 이후에도 계속해서 사용자의 정보를 유지할 수 있습니다.

따라서, Basic Authentication은 간단하고 빠르게 구현할 수 있지만 보안성이 떨어지며, JWT는 보안성이 높고, 사용자의 정보를 유지할 수 있지만 구현이 복잡합니다. 

 

2) 세션 쿠키 인증

세션 쿠키 방식은 사용자 인증 정보를 쿠키라는 작은 데이터 파일에 저장하여 인증을 유지하는 방식입니다. 세션 쿠키 방식은 클라이언트와 서버 사이에서 상태 정보를 유지하기 위해 쿠키를 사용합니다. 즉, 로그인 인증 후 서버에서 클라이언트에게 쿠키를 발급하고, 클라이언트는 이 쿠키를 저장하고 이후 요청에서 이 쿠키를 함께 서버에 보내 인증을 유지합니다.

세션 쿠키 방식의 동작 과정은 다음과 같습니다.

  1. 클라이언트가 서버에 로그인 요청을 보냅니다.
  2. 서버는 로그인 정보를 확인하고, 인증이 성공하면 클라이언트에게 쿠키를 발급합니다.
  3. 클라이언트는 이후 요청에서 쿠키를 함께 서버에 보내게 됩니다.
  4. 서버는 클라이언트가 보낸 쿠키를 확인하고, 인증 정보를 유지합니다.
  5. 클라이언트가 로그아웃하면 서버에서 세션 정보를 삭제하고, 클라이언트에서도 쿠키를 삭제합니다.

 

세션 쿠키 방식은 사용자 인증 정보가 서버에 저장되기 때문에 보안 측면에서 안전합니다.

하지만 서버에서 사용자 정보를 저장하고 관리해야 하기 때문에 서버 자원을 많이 사용할 수 있습니다.

또한, 클라이언트에서 쿠키를 저장하고 관리하기 때문에 쿠키가 탈취될 경우 보안에 취약할 수 있습니다.

이러한 문제점들을 보완하기 위해 JWT 토큰 방식이 등장하게 되었습니다.

 

3) JWT 토큰 방식 ❤️

JWT(Json Web Token)는 JSON 형식의 데이터를 안전하게 전송하기 위한 토큰 기반의 인증 방식입니다.

JWT는 세 가지 부분으로 구성되어 있습니다.

Header, Payload, Signature로 구성되어 있으며 각 부분은 점(.)으로 구분됩니다.

  • Header : 토큰의 타입과 암호화 알고리즘 정보가 들어있습니다.
  • Payload : 토큰에 담을 정보(claim)가 포함되어 있습니다. 클레임은 이름과 값의 쌍으로 이루어져 있으며 등록된 클레임, 공개 클레임, 비공개 클레임으로 구성됩니다.
  • Signature : 토큰의 유효성 검증을 위한 서명입니다. Header와 Payload를 합친 후, 비밀키를 이용해 해싱된 값을 포함합니다.

 

JWT는 서버와 클라이언트 간의 통신 시 토큰을 사용하여 인증을 수행합니다.

사용자가 로그인을 하면 서버에서 JWT 토큰을 발급하고, 이후에 클라이언트는 발급된 토큰을 이용하여 인증을 수행합니다.

클라이언트는 HTTP 요청 헤더나 URL 매개변수 등에 JWT 토큰을 포함시켜 서버로 전송합니다.

서버는 전송된 JWT 토큰을 검증하여 인증을 수행합니다.

 

JWT의 장점

  • Stateless : 서버에서 세션을 관리하지 않으므로, 서버에서 별도의 상태를 유지하지 않아도 됩니다.
  • 확장성 : 클라이언트가 다양한 서버에 접속할 수 있으므로, 서비스의 확장성이 높아집니다.
  • 보안성 : 토큰이 서명되어 있기 때문에, 클라이언트에서 조작하더라도 검증이 불가능합니다.
  • 유연성 : 클라이언트에서 토큰을 검증하므로, 서버가 다양한 인증 방식을 지원할 수 있습니다.

 

구현 예제 코드

JWT는 Base64URL로 인코딩된 헤더, 페이로드, 서명 세 부분으로 구성됩니다.

Header는 JWT가 어떤 알고리즘으로 서명되었는지를 알려주는 부분입니다. 일반적으로 HMAC SHA256 또는 RSA 알고리즘을 사용합니다.

Payload는 인증 정보를 포함합니다. 사용자 ID, 이름, 이메일, 권한 등을 포함할 수 있으며, 이 정보를 기반으로 서버에서 사용자 인증 및 권한 부여를 진행합니다.

Signature은 JWT의 무결성을 보장하기 위한 부분입니다. 헤더와 페이로드를 암호화한 값을 서명으로 사용하며, 이 서명값을 검증하여 JWT의 유효성을 판단합니다.

아래는 JWT 토큰 방식을 이용한 사용자 인증의 예제 코드입니다. 

  1. 클라이언트에서 로그인 정보를 입력받아 서버로 전송합니다.
  2. 서버는 입력받은 로그인 정보를 검증하고, 로그인이 성공하면 JWT 토큰을 발급합니다.
  3. 클라이언트는 발급받은 JWT 토큰을 저장합니다.
  4. 클라이언트는 API를 요청할 때마다 HTTP 헤더에 JWT 토큰을 포함시켜 요청합니다.
  5. 서버는 JWT 토큰을 검증하고, 토큰이 유효하면 요청을 처리합니다.
import 'dart:convert';
import 'package:http/http.dart' as http;

// 로그인 정보를 입력받아 JWT 토큰을 발급하는 함수
Future authenticate(String username, String password) async {
  // 로그인 정보를 서버에 전송
  var response = await http.post(
    Uri.parse('<https://example.com/authenticate>'),
    body: {
      'username': username,
      'password': password,
    },
  );

  if (response.statusCode == 200) {
    // JWT 토큰 추출
    var jsonResponse = jsonDecode(response.body);
    return jsonResponse['access_token'];
  } else {
    throw Exception('Failed to authenticate');
  }
}

// API 요청을 보낼 때 JWT 토큰을 포함시키는 함수
Future fetchApi(String jwtToken) async {
  var response = await http.get(
    Uri.parse('<https://example.com/api>'),
    headers: {
      'Authorization': 'Bearer $jwtToken',
    },
  );

  if (response.statusCode == 200) {
    // API 응답 처리
  } else {
    throw Exception('Failed to fetch API');
  }
}

위의 예제 코드에서 authenticate 함수는 사용자의 로그인 정보를 입력받아 서버에 전송하고, 서버에서 발급된 JWT 토큰을 반환합니다.

fetchApi 함수는 API 요청을 보낼 때 JWT 토큰을 포함시켜 요청하고, 서버에서 응답이 오면 해당 응답을 처리합니다.

이 예제 코드에서 JWT 토큰은 HTTP 요청의 Authorization 헤더에 "Bearer" 접두어와 함께 포함되어 전송됩니다.

 

JWT와 OAuth 인증방식의 차이점

JWT와 OAuth는 둘 다 웹 애플리케이션에서 사용되는 인증 방식입니다.

하지만 두 방식은 서로 다른 목적을 가지고 있으며,

구현 방식도 다릅니다.

JWT (JSON Web Token)은 인증을 위한 토큰 기반의 방식으로, 정보를 안전하게 전달하기 위한 방법입니다. JWT는 JSON 포맷으로 인코딩되어 있으며, 토큰 내에 필요한 정보를 모두 담고 있어 토큰 검증을 위한 별도의 데이터베이스나 서버 저장소가 필요하지 않습니다. JWT는 토큰 내에 해당 유저의 권한 정보를 포함하고 있어, 인증과 권한 부여를 모두 수행할 수 있습니다.

반면에 OAuth (Open Authorization)는 인증을 위한 프로토콜로, 서드파티 애플리케이션에서 사용자 인증을 관리하는 데 사용됩니다. OAuth는 사용자의 인증 정보를 제공하는 대신, API를 호출할 수 있는 권한을 부여합니다. 이를 통해 사용자의 정보를 안전하게 보호하면서, 다른 서드파티 애플리케이션에서 사용자의 리소스에 접근할 수 있도록 합니다.

간단하게 비유하자면, JWT는 주민등록증과 같은 개인 식별 정보를 담은 신분증이라면, OAuth는 운전면허증과 같은 권한 부여를 받은 증명서라고 볼 수 있습니다.

또한, JWT는 대부분의 경우 서버 측에서 사용되며, OAuth는 클라이언트 측에서 사용됩니다. 또한, JWT는 토큰 내에 정보가 모두 담겨 있으므로, 검증에 필요한 추가적인 요청이 없습니다. 반면에 OAuth는 권한을 부여하기 위해 별도의 요청이 필요합니다.

 

4) Oauth 인증

OAuth는 Open Authorization의 약어로, 사용자의 인증 정보를 제공하기 위한 오픈 스탠다드 프로토콜입니다. OAuth를 사용하면 사용자는 자신의 인증 정보를 제3자에게 공개하지 않고 안전하게 서비스를 이용할 수 있습니다. 예를 들어, 사용자가 Facebook 로그인을 한 후, 다른 서비스에서 Facebook 계정을 사용해 로그인할 때, Facebook이 OAuth를 사용하여 사용자의 인증 정보를 안전하게 공유할 수 있습니다. 이러한 방식으로 OAuth는 보안과 편의성을 모두 충족시킬 수 있습니다.

 

 

4. 구현 예제코드

Client 측 인증 요청 예시코드입니다. http 라이브러리를 기반으로 작성되었습니다.

HTTP 기본 인증 (Basic Authentication)

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

Future fetchSecureDataWithBasicAuth() async {
  final username = 'your-username';
  final password = 'your-password';
  final url = Uri.parse('<https://example.com/secure-data>');

  final response = await http.get(
    url,
    headers: {
      'Authorization': 'Basic ' + base64Encode(utf8.encode('$username:$password')),
    },
  );

  if (response.statusCode == 200) {
    // 성공적으로 데이터를 받아온 경우
    print(response.body);
  } else {
    // 에러 처리
    print('Error: ${response.statusCode}');
  }
}

 

JWT (JSON Web Token) 인증

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

Future fetchSecureDataWithJWT() async {
  final token = 'your-jwt-token';
  final url = Uri.parse('<https://example.com/secure-data>');

  final response = await http.get(
    url,
    headers: {
      'Authorization': 'Bearer $token',
    },
  );

  if (response.statusCode == 200) {
    // 성공적으로 데이터를 받아온 경우
    print(response.body);
  } else {
    // 에러 처리
    print('Error: ${response.statusCode}');
  }
}

 

OAuth2 인증

import 'package:flutter_web_auth/flutter_web_auth.dart';
import 'package:http/http.dart' as http;

Future authenticateWithOAuth2() async {
  final clientId = 'your-client-id';
  final clientSecret = 'your-client-secret';
  final redirectUri = 'your-redirect-uri';
  final authorizeUrl = '<https://example.com/oauth2/authorize>';
  final tokenUrl = '<https://example.com/oauth2/token>';

  final authUrl = Uri.parse('$authorizeUrl?'
      'client_id=$clientId&'
      'redirect_uri=$redirectUri&'
      'response_type=code');

  final result = await FlutterWebAuth.authenticate(
    url: authUrl.toString(),
    callbackUrlScheme: redirectUri,
  );

  final code = Uri.parse(result).queryParameters['code'];

  final response = await http.post(
    Uri.parse(tokenUrl),
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': 'Basic ${base64Encode(utf8.encode('$clientId:$clientSecret'))}',
    },
    body: {
      'grant_type': 'authorization_code',
      'code': code,
      'redirect_uri': redirectUri,
    },
  );

  if (response.statusCode == 200) {
    final accessToken = jsonDecode(response.body)['access_token'];
    // API 호출에 대한 인증 정보를 포함한 access token을 사용하여 API 호출
  } else {
    // 에러 처리
    print('Error: ${response.statusCode}');
  }
}
`