はじめに
最近、あるプロジェクトを行う際、シングルサインオン、つまり、そのプロジェクトのログインページが、会社で共有されているログインページを利用することになり、その中でロジックが統一的に処理されます。最終的に実現するのは、ユーザーがログインした状態で会社のすべてのウェブサイトにアクセスするのに、ログインは1回で済むということです。
"シングルサインオン(SSO)は、企業のビジネス統合のための一般的なソリューションの1つであり、複数のアプリケーション間で使用されます。
"
この記事では、ログイン後のaccess_tokenとrefresh_tokenの管理方法について説明します。
要件
- プレシナリオ
http://..com/profilehttp://.com/login?app_id=project_name_id&redirect_url=http://..com/profileプロジェクトのページにログインする必要が入力し、SSOのログインプラットフォームにジャンプするためにログインしていない、この時点でログインURLのURLは、どのapp_idは、良いの合意定義の背景側である、redirect_urlは正常にコールバックアドレスを指定する権限が付与されます。http://..com/profile?code=XXXXXXアカウントのパスワードを正しく入力すると、アドレスバーに ?code=XXXXX というパラメータが表示され、入力したページにリダイレクトされます。/access_token/authenticate{ access_token: "xxxxxxx", refresh_token: "xxxxxxxx", expires_in: xxxxxxxx }すぐにコードの値を取得し、apiを要求するために行く { verify_code: code }パラメータを運ぶと、apiは、独自のapp_idとapp_secret 2つの固定値のパラメータを持っており、それを介してapiの権限を要求するために、要求の戻り値を取得するために成功し、クッキーにaccess_tokenとrefresh_tokenを保存し、この時点で、ユーザーが正常にログインしたとみなされます。トークンをクッキーに保存すると、この時点でユーザはログインに成功したとみなされます。標準のJWT形式のaccess_tokenは、認証トークンは、ユーザーのアイデンティティの認証として理解することができる、コールapiのアクセスのアプリケーションであり、ユーザーデータをパラメータに渡さなければならない変更、有効期限後2時間。つまり、最初の3つの手順の後、あなたはapiを使用するためにログインするユーザーを呼び出すことができます。しかし、あなたは何も操作しない場合は、静かに過去2時間にわたって、その後、これらのapiを要求するために行く、access_tokenの有効期限が切れることが報告され、呼び出しに失敗します。
{ access_token: "xxxxx", expires_in: xxxxx }2時間後にユーザーをログアウトさせることはできません。解決策は、有効期限が切れたaccess_tokenとrefresh_tokenを受け取り、2時間後にapiをリクエスト/リフレッシュすることです。refresh_tokenは有効期限内であれば、次回も新しいaccess_tokenと交換できますが、有効期限を過ぎると、本当の意味での有効期限内であっても、アカウントのパスワードを再入力してログインする必要があります。
会社のウェブサイトのログインの有効期限はわずか2時間ですが、1ヶ月にしたいアクティブなユーザーが再びログインしないことが多いので、ユーザーがログインするために再びアカウントのパスワードを入力することを避けるために、このようなニーズがあります。
なぜaccess_tokenを更新するためにrefresh_tokenが必要なのですか?まず、access_tokenは特定のユーザの権限と関連付けられます。ユーザの権限が変更された場合、access_tokenを新しい権限に関連付けるために更新する必要があります。refresh_tokenを用いれば、このような手間を省くことができます。また、クライアントはrefresh_tokenを用いて直接access_tokenを更新することができます。
ここまで言って、おそらく誰かが吐く、行のaccess_tokenを持つログインが、また、多くの問題を作るためにrefresh_tokenを追加するか、またはいくつかの企業のrefresh_tokenは、パッケージのバックエンドは、フロントエンドの処理を必要としません。しかし、フロントエンドのシナリオがあり、要件はシナリオに基づいています。
- 要件
access_tokenの有効期限が切れた場合、新しいaccess_tokenを要求するためにrefresh_tokenを使用し、フロントエンドはユーザに依存しない方法でaccess_tokenをリフレッシュする必要があります。インターフェイスを呼び出して新しい access_token を取得し、それからユーザリクエストを再投入しなければなりません。
複数のユーザー要求が同時に開始された場合、最初のユーザー要求は、インターフェイスが返されていないときに、ユーザー要求の残りの部分は、まだ複数の要求につながるトークンインターフェイスの要求をリフレッシュするために開始され、トークンを呼び出すインターフェイスをリフレッシュするには、これらの要求に対処する方法は、この記事の内容です。
アイデア
オプション1
リクエストインターセプターに書き込み、リクエストの前に、最初の要求が返されたフィールドexpires_inフィールドを使用して、access_tokenの有効期限が切れているかどうかを判断し、それが有効期限が切れている場合は、リクエストがハングアップします、最初にリクエストを続行する前にaccess_tokenをリフレッシュします。
- 利点: http リクエストを節約
- デメリット:現地時間の判定を行うため、現地時間が改ざんされると故障のリスクがあります。
オプション2
レスポンスインターセプターに記述して、return後のデータをインターセプトします。最初にユーザーリクエストを開始し、インターフェイスがaccess_tokenの有効期限が切れたと返したら、最初にaccess_tokenをリフレッシュして、もう一度リトライします。
- メリット:時間判断が不要
- 短所:httpリクエストを1つ多く消費します。
ここでは選択肢2を選びました。
実装
axios.interceptors.response.use()ここでは、リクエスト後のインターセプトを行う axios を使用しているので、axios のレスポンスインターセプターメソッドを使用します。
メソッド
- utils/auth.js
import Cookies from 'js-cookie'
const TOKEN_KEY = 'access_token'
const REGRESH_TOKEN_KEY = 'refresh_token'
export const getToken = () => Cookies.get(TOKEN_KEY)
export const setToken = (token, params = {}) => {
Cookies.set(TOKEN_KEY, token, params)
}
export const setRefreshToken = (token) => {
Cookies.set(REGRESH_TOKEN_KEY, token)
}
- request.js
import axios from 'axios'
import { getToken, setToken, getRefreshToken } from '@utils/auth'
// アクセスをリフレッシュする_token
const refreshToken = () => {
return instance.post('/auth/refresh', { refresh_token: getRefreshToken() }, true)
}
// axiosインスタンスを作成する
const instance = axios.create({
baseURL: process.env.GATSBY_API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
}
})
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
// token 期限切れまたは無効、ステータスコード401を返し、ここでロジックに対処する
return Promise.reject(error)
})
// リクエストヘッダへのアクセスを追加する_token
const setHeaderToken = (isNeedToken) => {
const accessToken = isNeedToken ? getToken() : null
if (isNeedToken) { // api リクエストは_token
if (!accessToken) {
console.log('アクセスしない_token ログインページに戻る')
}
instance.defaults.headers.common.Authorization = `Bearer ${accessToken}`
}
}
// いくつかのapiは、ユーザー認証を必要としないので、アクセスを運ばない。_tokenトークンを渡す必要がある場合は、3番目のパラメーターをtrueに設定する。
export const get = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken)
return instance({
method: 'get',
url,
params,
})
}
export const post = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken)
return instance({
method: 'post',
url,
data: params,
})
}
次に、request.js の axios 用のレスポンスインターセプターを変更します。
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
if (error.response.status === 401) {
const { config } = error
return refreshToken().then(res=> {
const { access_token } = res.data
setToken(access_token)
config.headers.Authorization = `Bearer ${access_token}`
return instance(config)
}).catch(err => {
console.log('申し訳ありません、ログインが無効になっています。もう一度ログインしてください!)
return Promise.reject(err)
})
}
return Promise.reject(error)
})
ユーザがリクエストを開始し、access_tokenが期限切れであるという結果が返された場合、ユーザはaccess_tokenをリフレッシュするインターフェイスをリクエストします。リクエストに成功した場合、ユーザーはそのインターフェイスに入り、設定をリセットし、access_tokenをリフレッシュして、元のリクエストを再度開始します。
しかし、refresh_tokenも期限切れの場合は、リクエストも401を返します。この時点でデバッグがrefreshToken()メソッドも同じインスタンスインスタンスの内部で使用されるため、それは、内部refreshToken()キャッチに関数が見つかりますインターセプタ401処理ロジックへの応答を繰り返しますが、関数自体は、access_tokenをリフレッシュすることですので、インターフェイスを除外する必要がある、ということです:
if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {}
上記のコードは、access_tokenが期限切れでない場合は通常のリターン、期限切れの場合はaxiosの内部操作でトークンをリフレッシュし、元のリクエストを再実行するという、無意味なaccess_tokenのリフレッシュを行うように実装されています。
最適化
複数回のトークン更新の防止
トークンの有効期限が切れている場合は、access_tokenインターフェイスをリフレッシュする要求も一定の時間間隔が返され、この時点で送信された他の要求がある場合、それは再びaccess_tokenインターフェイスをリフレッシュするために実行され、access_tokenをリフレッシュするために複数回になります。の現在の状態が access_token をリフレッシュしているかどうかを判断し、もしリフレッシュ状態であれば、そのインターフェイスを呼び出す他のリクエストを許可しなくなります。
let isRefreshing = false // トークンがリフレッシュされているかどうかのフラグを立てる
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {
const { config } = error
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res=> {
const { access_token } = res.data
setToken(access_token)
config.headers.Authorization = `Bearer ${access_token}`
return instance(config)
}).catch(err => {
console.log('申し訳ありません、ログインが無効になっています。もう一度ログインしてください!)
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
}
}
return Promise.reject(error)
})
同時に開始された複数のリクエストの処理
なぜなら、複数のリクエストが同時に開始された場合、トークンの有効期限が切れた場合、最初のリクエストはトークンのリフレッシュメソッドに進み、他のリクエストは論理的な処理を行わず、単に失敗を返し、最終的に最初のリクエストだけが実行されることになり、明らかに合理的ではないからです。
例えば、3つのリクエストが同時に開始された場合、最初のリクエストはトークンをリフレッシュするプロセスに入り、2番目と3番目のリクエストはトークンが更新されるまで保存され、その後リクエストが再度開始される必要があります。
ここでは、配列の要求を定義し、要求が待機している保存するために使用され、Promiseを返す限り、resolveメソッドを呼び出さないように、要求が待機状態になります、あなたは実際には、配列が関数に格納されていることを知ることができます;トークンの更新が完了するまで待って、関数の実行を介して配列のサイクルは、つまり、1つずつ要求を再送信するresolveを実行します。
let isRefreshing = false // トークンがリフレッシュされているかどうかのフラグを立てる
let requests = [] // 再送するリクエストの配列を格納する
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {
const { config } = error
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res=> {
const { access_token } = res.data
setToken(access_token)
config.headers.Authorization = `Bearer ${access_token}`
// token 配列を更新し、メソッドを再実行する。
requests.forEach((cb) => cb(access_token))
requests = [] // クリア
return instance(config)
}).catch(err => {
console.log('申し訳ありませんが、あなたのログインステータスはもう無効です。)
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
} else {
// resolveを実行しなかった Promiseを返す
return new Promise(resolve => {
// resolveを関数として格納し、リフレッシュされるのを待つ。
requests.push(token => {
config.headers.Authorization = `Bearer ${token}`
resolve(instance(config))
})
})
}
}
return Promise.reject(error)
})
最終的なrequest.jsのコード
import axios from 'axios'
import { getToken, setToken, getRefreshToken } from '@utils/auth'
// アクセスをリフレッシュする_token
const refreshToken = () => {
return instance.post('/auth/refresh', { refresh_token: getRefreshToken() }, true)
}
// axiosインスタンスを生成する
const instance = axios.create({
baseURL: process.env.GATSBY_API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
}
})
let isRefreshing = false // トークンがリフレッシュされているかどうかのフラグを立てる
let requests = [] // 再送するリクエストの配列を格納する
instance.interceptors.response.use(response => {
return response
}, error => {
if (!error.response) {
return Promise.reject(error)
}
if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {
const { config } = error
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res=> {
const { access_token } = res.data
setToken(access_token)
config.headers.Authorization = `Bearer ${access_token}`
// token 配列を更新し、メソッドを再実行する。
requests.forEach((cb) => cb(access_token))
requests = [] // クリア
return instance(config)
}).catch(err => {
console.log('申し訳ありませんが、あなたのログインステータスはもう無効です。)
return Promise.reject(err)
}).finally(() => {
isRefreshing = false
})
} else {
// resolveを実行しなかった Promiseを返す
return new Promise(resolve => {
// resolveを関数として格納し、リフレッシュされるのを待つ。
requests.push(token => {
config.headers.Authorization = `Bearer ${token}`
resolve(instance(config))
})
})
}
}
return Promise.reject(error)
})
// リクエストヘッダへのアクセスを追加する_token
const setHeaderToken = (isNeedToken) => {
const accessToken = isNeedToken ? getToken() : null
if (isNeedToken) { // api リクエストは_token
if (!accessToken) {
console.log('アクセスしない_token ログインページに戻る')
}
instance.defaults.headers.common.Authorization = `Bearer ${accessToken}`
}
}
// ユーザー認証を必要としないAPIもあるので、アクセスを運ぶ必要はない。_token第3パラメータを渡す必要がある場合は、trueを設定する。
export const get = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken)
return instance({
method: 'get',
url,
params,
})
}
export const post = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken)
return instance({
method: 'post',
url,
data: params,
})
}





