概要
例えば認証をつけたい場合に使えます
前回のアプリケーションをベースに変更していきます
環境
- macOS 11.7.4
- nodejs 19.6.0
- yarn 1.22.19
- quasar 2.0.0
- Python 3.10.9
- Flask 2.2.3
- Flask-CORS 3.0.10
- authlib 1.2.0
axios のリクエストを管理する (src/boot/axios.js)
バックエンドとやり取りする api をアプリケーション全体で管理するようにします
- vim src/boot/axios.js
import { boot } from 'quasar/wrappers'
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:5000',
withCredentials: true
})
export default boot(({ app }) => {
app.config.globalProperties.$axios = axios
app.config.globalProperties.$api = api
})
export { axios, api }
ルーティングの設定 (src/router/routes.js)
認証が必要なページと不要なページを設定します
認証が必要なページは meta.requiresAuth を設定します
- vim src/router/routes.js
const routes = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/IndexPage.vue'), meta: { requiresAuth: true } }
]
},
{
path: '/login',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/LoginPage.vue') }
]
},
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}
]
export default routes
beforeEach を router に追加する (src/router/index.js)
Quasar の beforeEach という機能を使うことですべてのページに対して共通して実施したいことを定義できます
先程各ページに付与した meta.requiresAuth が有効なページの場合に認証済みかどうかをバックエンドに聞きに行くような処理を追加します
バックエンド側にはあとで /verify
というエンドポイントを追加して認証が完了しているかチェックする機能を追加します
/verify
でエラーが返ってきた場合は Quasar のログインページを表示します
- vim src/router/index.js
import { route } from 'quasar/wrappers'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes'
import { api } from 'boot/axios'
export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE)
})
// ここを追加する
Router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// 認証が必要なページはバックエンドに問い合わせる
api.get('/verify')
.then((response) => {
console.log(response.data)
next()
})
.catch((error) => {
console.log(error)
next('/login')
})
} else {
// 認証が不要なページはそのまま表示する
next()
}
})
return Router
})
注意事項
本当は catch したところでバックエンドのログインページに飛ばしてあげてもいいのですがそうすると無限ループになってしまうのでそうならないように認証が不要なログインページを用意してあげる必要があります
ログイン用のページ (src/pages/LoginPage.vue)
このページには認証がありません
ログイン/ログアウトボタンとログインしたあとで取得できるユーザ情報を表示しています
- vim src/pages/LoginPage.vue
<template>
<div>
<h1>Login Test Page</h1>
<div>{{ data }}</div>
<button @click="login">Login</button>
<button @click="logout">Logout</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { api } from 'boot/axios'
const data = ref('nodata')
const fetchUser = async(value) => {
api.get('/user')
.then((response) => {
data.value = response.data
})
.catch((error) => {
console.log(error)
})
}
const login = async(value) => {
window.location.href = 'http://localhost:5000/login';
}
const logout = async(value) => {
api.get('/logout')
.then((response) => {
data.value = response.data
})
.catch((error) => {
console.log(error)
})
}
onMounted(() => {
fetchUser()
})
</script>
バックエンドの処理
基本は前回と同じですが verify メソッドを追加しています
verify ではセッション(クッキー)に値が設定されているかをチェックしています
-
vim app.py
from typing import Any, Optional
from flask import (Flask,
url_for,
session,
redirect)
from flask_cors import CORS
from authlib.integrations.flask_client import OAuth
app = Flask(__name__)
app.secret_key = "xxxx"
app.config["ONELOGIN_CLIENT_ID"] = "xxxx"
app.config["ONELOGIN_CLIENT_SECRET"] = "xxxx"
app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_HTTPONLY"] = True
CORS(app,
supports_credentials=True,
max_age=300)
oauth = OAuth(app)
oauth.register(
name="onelogin",
server_metadata_url="https://xxxxxx.onelogin.com/oidc/2/.well-known/openid-configuration",
client_kwargs={"scope": "openid email profile"}
)
def get_session() -> Optional[Any]:
return session.get("user")
def has_session() -> bool:
user = get_session()
if user is None:
return False
return True
@app.route("/user")
def user():
if not has_session():
return "Not logged in yet."
return f"Login successful. => {get_session()}"
@app.route("/verify")
def verify():
if not has_session():
return "Not logged in yet.", 400
return "Already logged in."
@app.route("/login")
def login():
redirect_uri = url_for("auth", _external=True)
return oauth.onelogin.authorize_redirect(redirect_uri)
@app.route("/callback")
def auth():
token = oauth.onelogin.authorize_access_token()
session["user"] = token["userinfo"]
return redirect("http://localhost:8080/#/login")
@app.route("/logout")
def logout():
session.pop("user", None)
return "Logout successful."
動作確認
- quasar dev
- pipenv run flask run
で localhost:8080/#/ にアクセスすると強制的にログインページに飛ばされるのが確認できます
そして OpenIDConnect でログイン後には localhost:8080/#/ に無事アクセスできることが確認できると思います
最後に
Quasar で全ページに対して行う共通処理を実現する方法を紹介しました
特定のページにのみ実施したい場合には meta 情報を使って beforeEach で判断するようなテクニックがあります
ログアウトするとアプリケーション内のセッションは消えますが OpenIDConnect を提供している Idp 側ではまだセッションが残っている場合があります
ログアウト時に Idp 側のセッションもログアウトにする方法があるので興味がある方は調べてみてください
バックエンドのレスポンスは flask.jsonify を使って JSON 形式にしたほうがいいかもしれません
返信削除