オンプレミスだけでなく、クラウドによりインターネットを介して様々なサービスやシステムが提供されています。これは、お互いを利用できる環境にあることであり、各々がサービスを定義する手段がREST APIです。
■ REST APIの概要
REST(Representational State Transfer) APIとは、HTTPプロトコルによるAPIを定義する手法の一つです。APIがレストフル(RESTful)であることとは、簡単に言うと、以下の点です。
①各処理をURLによって指定すること
②ステーレスであること
③作成・参照・更新・削除がHTTPメソッド(POST、GET、PUT、DELETE)と対応していること
④リクエストとレスポンスのデータがJSON形式であること
REST APIは、HTTPSによるサーバ認証と通信暗号化、トークン認証による呼び出し元の認証により、セキュリティを確保します。
・JSON
REST APIのリクエスト、レスポンスのデータは、JSON形式です。
JSON形式のデータをリクエストと、レスポンスのボディ部に設定することで、多様な形式のデータをやり取りすることを可能とします。
JSON形式は、XMLやHTMLのほど複雑性がなく、シンプルにタグ名とデータをセットとして扱うことができ、APIのパラメータや結果のデータセットを表現すること適しています。
以下にJSON形式のデータ例を示します。
{
"キー名(文字列)": "文字列です",
"キー名(数値)": 123,
"キー名(ブール)": true,
}
キー名と値を、コロン(:)でペアにし、それらをカンマ(,)で区切って並べます。それらを中括弧({})で囲んで、一つのデータ・セットとして表現します。
データ型は、文字列、数値、ブール(true/false)を表現できます。
タグを使ったデータ表現にはXMLがありますが、それをよりシンプルにしたものです。
・HTTPSによるサーバ認証
REST APIは、WebサーバのHTTP通信機能を使って、リクエストを受け取り、HTTPメソッド(POST、GET、PUT、DELETE)を判別し、レスポンスを返却します。
REST APIを提供するWebサーバでは、正当性を示すため、PKI(Public Key Infrastructure)によるサーバ証明書を使ったサーバ認証を実施する必要があります。
そのため、サーバ認証のためのRSAの鍵ペアの生成、公開鍵証明書の発行を実施し、Webサーバに設定する必要があります。鍵ペアの生成、公開鍵証明書の発行などについては、以下を参照ください。
・トークン認証(JSON Web Token)
REST APIの利用者を管理し、許可された人だけがREST APIを呼び出せるようにする仕組みが必要です。
その手段として、トークン認証を使います。
クライアント(呼び出し側)は、あらかじめ、各利用者に配布されるクライアントID、シークレットデータを使ってトークンを取得後、トークンを設定したREST APIのリクエストを送信します。
サーバ(REST APIサービス提供側)は、受け取ったトークンを検証した後、指定の処理を実行し、レスポンスを返却します。
トークンの形式には、JSON Web Token(JWT)が使われます。JWTは、RFC7519に準拠したもので、以下のようにBASE64エンコード文字列がドット(.)で連結された構造となっています。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY4MDI1MzM3NSwianRpIjoiNDA5ZmI5OWMtYTQ1MC00YTQyLThkMmItNmFhZmU2YzNjMTMxIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6IlUwMDAwMDAxIiwibmJmIjoxNjgwMjUzMzc1LCJleHAiOjE2ODAyNTQyNzV9.lzFjsZ6bsmnv_5NDbMlf3Xp1F3g7cG_9vh_3UQNxiRM
青字の部分は、ヘッダで、次のように、署名のアルゴリズムなどがJSON形式で記述されます。
{"alg":"HS256","typ":"JWT"}
オレンジの部分は、ペイロードで、次のように、署名ためのデータ本体がJSON形式で記述されます。
“jti”がランダムに生成されるユニーク値となります。
{"fresh":false,"iat":1680253375,"jti":"409fb99c-a450-4a42-8d2b-6aafe6c3c131","type":"access","sub":"U0000001","nbf":1680253375,"exp":1680254275}
赤字の部分は、署名データのバイナリそのものであり、トークン検証に使われます。
b'6c7a466a735a3662736d6e765f354e44624d6c66335870314633673763475f3976685f3355514e7869524d'
■ Python/FlaskによるREST APIの実装サンプル
Python/Flaskを使い、上記のような要件を満たしたREST APIの実装サンプルを示します。
Basic認証は、flask_httpauth、トークン(JWT)認証は、flask_jwt_extended、REST APIのHTTPメソッド分岐には、flask_restxを使っています。
また、Webサーバには、Flask内蔵のサーバ機能を使い、サーバの鍵ペアとサーバ証明書を設定し、起動します。(環境変数に、アプリ名=拡張子なしのファイル名を設定すること)
$ export FLASK_APP=test_restapi_server
$ flask run --cert server.crt --key server.key --port=50443
実装の概要を以下に示します。
①トークン(JWT)取得
クライアントは、HTTPSによってサーバに接続し、あらかじめ取得したクライアントIDとシークレットを設定したBasic認証により、トークンを取得する。
その際、Content-Typeを”application/x-www-form-urlencoded”として、ボディ部に”grant_type=client_credentials”を設定する。
(OAuth2仕様において、アプリケーション経由でのリソースへのアクセスを示す)
サーバは、Basic認証により、クライアントIDとシークレットからクライアントを特定し、トークン(JWT)を発行する。
②トークン(JWT)認証によるREST APIに呼び出し
クライアントは、取得したトークン(JWT)をヘッダに設定し、HTTPSによりサーバに接続し、
作成・参照・更新・削除に対応したHTTPメソッド(POST、GET、PUT、DELETE)を使って、
REST APIのリクエストを送信し、レスポンスを受け取る。
サーバは、リクエストを受信した際、まず、トークン(JWT)検証を実施し、正常であれば、
各処理を実行し、レスポンスを返却する。
・クライアントのサンプルソース
# -*- coding: utf-8 -*-
import requests
from requests.auth import HTTPBasicAuth
# クライアントIDとシークレット
g_client_id = 'user001'
g_client_secret = 'secret001'
#
# トークン(JWT)取得
#
# BASIC認証インスタンスを生成
auth=HTTPBasicAuth(g_client_id, g_client_secret)
#POST先URL
url = "https://127.0.0.1:50443/token"
# 'grant_type=client_credentials' をbodyに設定
data = 'grant_type=client_credentials'
# 'application/x-www-form-urlencoded'を'Content-Type'ヘッダーに設定
headers = {'Content-Type' : 'application/x-www-form-urlencoded'}
#POST送信(タイムアウト10秒)
response = requests.post(
url=url,
timeout=10,
headers=headers,
data = data,
auth=auth,
verify=False
)
#レスポンスデータ(JSON) を取得
res_data = response.json()
#アクセストークンを取得
access_token=res_data['access_token']
#
# POST送信
#
#REST APIのPOST先URL
url = "https://127.0.0.1:50443/restapi"
# アクセストークンをヘッダーに設定
authrization = 'Bearer '+access_token
headers = {'Content-Type' : 'application/json','Authorization' : authrization}
headers = {'Authorization' : authrization}
# リクエストデータ(JSON)
json_data={'id': 'ID_A001'}
# POST送信(タイムアウト10秒) ※独自証明書の場合は、verify=Falseを設定
response = requests.post(
url=url,
timeout=10,
headers=headers,
json=json_data,
verify=False
)
# レスポンスコードを取得
status_code = response.status_code
# 200 OKの場合、レスポンスデータ(JSON) を取得
if status_code == 200:
res_data = response.json()
print(res_data)
#
# GET送信
#
#REST APIのGET先URL
url = "https://127.0.0.1:50443/restapi"+"/ID_A001"
# アクセストークンをヘッダーに設定
authrization = 'Bearer '+access_token
headers = {'Content-Type' : 'application/json','Authorization' : authrization}
headers = {'Authorization' : authrization}
# GET送信(タイムアウト10秒) ※独自証明書の場合は、verify=Falseを設定
response = requests.get(
url=url,
timeout=10,
headers=headers,
verify=False
)
# レスポンスコードを取得
status_code = response.status_code
# 200 OKの場合、レスポンスデータ(JSON) を取得
if status_code == 200:
res_data = response.json()
print(res_data)
#
# PUT送信
#
# REST APIのPUT先URL
url = "https://127.0.0.1:50443/restapi"+"/ID_A001"
# アクセストークンをヘッダーに設定
authrization = 'Bearer '+access_token
headers = {'Content-Type' : 'application/json','Authorization' : authrization}
headers = {'Authorization' : authrization}
#リクエストデータ(JSON)
json_data={'id': 'ID_A001'}
#PUT送信(タイムアウト10秒) ※独自証明書の場合は、verify=Falseを設定
response = requests.put(
url=url,
timeout=10,
headers=headers,
json=json_data,
verify=False
)
#レスポンスコードを取得
status_code = response.status_code
# 200 OKの場合、レスポンスデータ(JSON) を取得
if status_code == 200:
res_data = response.json()
print(res_data)
#
# DELETE送信
#
#REST APIのGET先URL
url = "https://127.0.0.1:50443/restapi"+"/ID_A001"
# アクセストークンをヘッダーに設定
authrization = 'Bearer '+access_token
headers = {'Content-Type' : 'application/json','Authorization' : authrization}
headers = {'Authorization' : authrization}
#DELETE送信(タイムアウト10秒) ※独自証明書の場合は、verify=Falseを設定
response = requests.delete(
url=url,
timeout=10,
headers=headers,
verify=False
)
#レスポンスコードを取得
status_code = response.status_code
# 200 OKの場合、レスポンスデータ(JSON) を取得
if status_code == 200:
res_data = response.json()
print(res_data)
・サーバのサンプルソース
# -*- coding: utf-8 -*-
from werkzeug.security import generate_password_hash, check_password_hash
from flask_httpauth import HTTPBasicAuth
import json
from datetime import timedelta
from flask_jwt_extended import JWTManager, jwt_required, create_access_token
from flask_restx import Api, Resource
from flask import Flask, make_response, jsonify, request
# BASIC認証インスタンスを生成
auth = HTTPBasicAuth()
# BASIC認証用ユーザー情報
users_new = {
"user001": generate_password_hash("secret001"),
"user002": generate_password_hash("secret002")
}
# BASIC認証用パスワードチェック関数
@auth.verify_password
def verify_password(username, password):
if username in users_new and \
check_password_hash(users_new.get(username), password):
return username
# FLASKアプリのインスタンスの生成とJWTトークンの生成のパラメータの設定
def create_app():
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'SecretKey'
# 暗号化署名のアルゴリズム
app.config['JWT_ALGORITHM'] = 'HS256'
# 有効期限に対する余裕時間
app.config['JWT_LEEWAY'] = 0
app.config['JWT_EXPIRATION_DELTA'] = timedelta(seconds=300) # トークンの有効期間
app.config['JWT_NOT_BEFORE_DELTA'] = timedelta(seconds=0) # トークンの使用を開始する相対時間
return app
# FLASKアプリのインスタンスの生成
app = create_app()
# FLASK REST APIインスタンスの生成
api = Api(app)
# JWTインスタンスの生成
jwt = JWTManager(app)
#
# トークン取得エンドポイント
#
@app.route('/token', methods=['POST'])
@auth.login_required # BASIC認証
def index():
if request.method == 'POST':
grant_type = request.form['grant_type']
if grant_type == 'client_credentials':
access_token = create_access_token(identity=auth.username())
return make_response(jsonify({'access_token': access_token}), 200)
return make_response(jsonify({'message': 'bad request'}), 500)
#
# REST APIエンドポイント
#
@api.route('/restapi', '/restapi/<string:id>', methods=['POST','GET','PUT','DELETE'])
class TsetSecureController(Resource): # Resourceクラスを継承し、HTTPメソッド処理を定義
@jwt_required() # トークン(JWT)認証
def post(self): # POSTメソッド
json_data = request.json
print(json_data)
response_json_dic = {
'id': 'ID_A001',
'message': 'Create data by endpoint(POST).'
}
return make_response(jsonify(response_json_dic), 200)
def get(self, id=None): # GETメソッド
if id is not None:
response_json_dic = {
'id': 'ID_A001',
'message': 'Refer data by endpoint(GET).'
}
return make_response(jsonify(response_json_dic), 200)
else:
return make_response(jsonify({'message': 'bad id'}), 500)
def put(self, id=None): # PUTメソッド
if id is not None:
json_data = request.json
print(json_data)
response_json_dic = {
'id': 'ID_A001',
'message': 'Update data by endpoint(PUT).'
}
return make_response(jsonify(response_json_dic), 200)
else:
return make_response(jsonify({'message': 'bad id'}), 500)
def delete(self, id=None): # DELETEメソッド
if id is not None:
response_json_dic = {
'id': 'ID_A001',
'message': 'Delete data by endpoint(DELETE).'
}
return make_response(jsonify(response_json_dic), 200)
else:
return make_response(jsonify({'message': 'bad id'}), 500)
■ まとめ
REST APIを定義するために、モジュール間の依存性をなくし、独立性を保持することは重要なことです。これは、構造化設計でのモジュール化および単機能化に通じる考え方です。
システム構造、プログラム構造といったアーキテクチャを考える上では、システム間、プログラム間をできるだけ疎結合させ、密結合させない配慮が必要です。その結果、システムやプログラムの連結容易性と再利用性を高めることができます。REST APIは、これらを大きく拡張できる技術です。
ソフトウェア開発・システム開発業務/セキュリティ関連業務/ネットワーク関連業務/最新技術に関する業務など、「学習力×発想力×達成力×熱意」で技術開発の実現をサポート。お気軽にお問合せ下さい