概要
LINE Messaging APIを使用するにはチャネルアクセストークン(チャネルシークレットとは別物)が必要になる
チャネルアクセストークンには4種類あるみたいだが、今回はステートレスチャネルアクセストークンの取得と使用を行う。ステートレスチャネルアクセストークンの特徴と利点は以下が考えられる
- トークンの取り消しはできないが、有効期限が15分と短いのでトークンの漏洩のリスクを抑えられる
- 発行数が無制限のため、トークンの使いまわしなどを考えずに都度発行できる
- APIの発行頻度がそんなにないケースでは特に有用と思われる
その他詳細は以下を参照
事前準備
LINEビジネスアカウントの作成とLINE Messaging APIの有効化は済ませているものとする
クイックスタートのアサーション署名キーのキーペアを生成で説明されている手順を以下で説明する
アサーション署名キーのキーペアを生成する
- uvをインストール
- ターミナルで
uvx --with jwcrypto python3
を実行してipythonを起動。以下をペーストして実行する
from jwcrypto import jwk
import json
key = jwk.JWK.generate(kty='RSA', alg='RS256', use='sig', size=2048)
private_key = key.export_private()
public_key = key.export_public()
print("=== private key ===\n"+json.dumps(json.loads(private_key),indent=2))
print("=== public key ===\n"+json.dumps(json.loads(public_key),indent=2))
- 秘密鍵(
=== private key ===
以下の出力)の内容をコピーして任意のファイルに保存する- 後でRustで参照する
Line Developersコンソールで公開鍵を登録
- 公開鍵(
=== public key ===
以下の出力)の内容をコピペする - アサーション署名キーに紐づくID (
kid
)は、後でRustで参照する
コード
ほぼ同じコードをGitHubにも上げている
以下の順序で処理する
- JWTの生成
- (JWTを使って)ステートレスチャネルアクセストークンを取得
- メッセージの送信
JWTの生成やメッセージの送信に必要なものはすべて環境変数から取得している
use chrono::{Duration, Utc};
use jsonwebkey::JsonWebKey;
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use line_bot_messaging_api::LineClient;
use line_bot_messaging_api::message::{LineMessageText, LineMessagesBuilder};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs::File;
use std::io::prelude::*;
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Claims {
iss: String,
sub: String,
aud: String,
exp: usize,
// 有効期限は15分で固定のため、ペイロード(Claims)にtoken_expは不要
}
impl Claims {
pub fn new(line_channel_id: &str) -> Self {
// expは30分以内に失効するように指定する
let exp = Utc::now() + Duration::minutes(1);
Self {
iss: line_channel_id.to_string(),
sub: line_channel_id.to_string(),
exp: exp.timestamp() as usize,
aud: "https://api.line.me/".to_string(),
}
}
}
#[tokio::main]
async fn main() {
// Get environment variables
let line_private_jwk_path = env::var("LINE_PRIVATE_JWK_PATH")
.expect("environment variable LINE_PRIVATE_JWK_PATH is not set");
let line_kid = env::var("LINE_KID").expect("environment variable LINE_KID is not set");
let line_channel_id =
env::var("LINE_CHANNEL_ID").expect("environment variable LINE_CHANNEL_ID is not set");
let line_user_id =
env::var("LINE_USER_ID").expect("environment variable LINE_USER_ID is not set");
// === JWTの生成 ===
// Read private JWK
let mut f = File::open(&line_private_jwk_path)
.expect(format!("file not found: {}", line_private_jwk_path).as_str());
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("failed to read file");
let jwk: JsonWebKey = contents.parse().expect("failed to parse JWK");
let key = EncodingKey::from_rsa_pem(jwk.key.to_pem().as_bytes()).expect("failed to create key");
// Create JWT header
let header = Header {
alg: Algorithm::RS256,
kid: Some(line_kid.to_string()),
// typはデフォルトで"JWT"のため未指定
..Default::default()
};
// Create JWT payload (claims)
let claims = Claims::new(&line_channel_id);
// Generate JWT
let token = jsonwebtoken::encode(&header, &claims, &key).expect("failed to encode JWT");
// === ステートレスチャネルアクセストークンを取得 ===
// Get line channel access token
let access_token_client = Client::new();
let response = access_token_client
.post("https://api.line.me/oauth2/v3/token")
.header("Content-Type", "application/x-www-form-urlencoded")
.query(&[
("grant_type", "client_credentials"),
(
"client_assertion_type",
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
),
("client_assertion", &token),
])
.send()
.await
.expect("failed to request for line channel access token");
let line_channel_access_token = response
.json::<serde_json::Value>()
.await
.expect("failed to parse line channel access token to json");
// 成功すれば以下のようなレスポンスが返ってくる
// {
// "token_type": "Bearer",
// "access_token": "ey....",
// "expires_in": 900
// }
let line_channel_access_token = line_channel_access_token["access_token"]
.as_str()
.expect("failed to get line channel access token from json");
// === メッセージの送信 ===
// Send message request
let message_client = LineClient::new(&line_channel_access_token);
let message = LineMessageText::new("Hello, world!");
let mut builder = LineMessagesBuilder::new();
builder.append(message);
message_client
.message_send_push(&builder.to_push_request(&line_user_id))
.await
.expect("failed to send message");
println!("message sent");
}
ヘッダー(Header)やペイロード(Claims)は、それぞれ以下を参照している
- ヘッダー
- ペイロード
また、POSTリクエストで指定しているヘッダーとクエリ文字列は以下を参照している
依存しているクレートは以下の通り
[package]
name = "line_stateless_channel_access_token_and_messaging_api"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = "0.4.41"
jsonwebkey = { version = "0.3.5", features = ["jwt-convert"] }
jsonwebtoken = "9.3.1"
line-bot-messaging-api = "0.1.12"
reqwest = "0.12.23"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros"] }
主要なクレートの用途は以下の様になる
クレート | 用途 |
---|---|
jsonwebkey | JWTの署名に使用する秘密鍵の読み込み |
jsonwebtoken | JWTの生成 |
serde |
jsonwebtoken での処理に必要 |
reqwest | チャネルアクセストークンの取得 |
serde_json | チャネルアクセストークンの取得 |
line-bot-messaging-api | チャネルアクセストークンを使用したLINE Messaging APIの使用 |
実行
実行に際し以下が必要になるため調べておく。いずれもLine Developersコンソールから取得可能(チャネルのチャネル基本設定
から確認できる)
- アサーション署名キーに紐づくID (
kid
) - チャネルID
- ユーザーID
その上で必要な環境変数を指定する
export LINE_USER_ID='ユーザーID'
export LINE_KID='kid'
export LINE_CHANNEL_ID='チャネルID'
export LINE_PRIVATE_JWK_PATH='保存した秘密鍵への絶対パス'
以下コマンドでメッセージを送信する
cargo run
成功すれば、標準出力で以下が出力されると同時に、LINEにHello, world!
とメッセージが飛んでくる
message sent
LINEのスクショの例
注意
-
jsonwebkeyクレートのexampleでは
to_encoding_key()
が使われている。これを使えば、jsonwebkey→jsonwebtokenのオブジェクト変換が容易にできると思われるが、対応しているjsonwebtokenのバージョンが古いので使えない (おそらく互換性がない)-
jsonwebtoken::encode()
の部分で以下のようなエラーがでるerror[E0308]: mismatched types ... note: two different versions of crate `jsonwebtoken` are being used; two types coming from two different versions of the same crate are different types even if they look the same
-
- 今回はステートレスチャネルアクセストークンを発行するので、JWTのペイロード(Claims)内に
token_exp
は不要となる
参考