前情提要
上次,在《使用 IdentityServer 保护 Vue 前端 - Jeff Tian的文章 - 知乎 》中记录了直接在 Vue 项目里,不使用任何状态管理包,直接使用 oidc-client js 来对接 Duende IdentityServer,并且使用了 OAuth 2.0 的隐式许可模式。
今天,继续记录一下在 Vue 项目中对接 Duende IdentityServer (或者任何的其他的 OAuth 2.0 服务器。这次,我们使用 vuex 状态管理包,以及对应的 vuex-oidc 来对接 Duende IdentityServer,并且尝试一下 OAuth 2.0 的授权码许可模式。
步骤 0: 授权服务器端的配置工作
首先,需要在 Duende IdentityServer 中为该 Vue 项目配置好客户端。需要注意的是,不能直接复用在《使用 IdentityServer 保护 Vue 前端 - Jeff Tian的文章 - 知乎 》中使用的那个客户端。那个客户端已经配置了隐式许可。
尽管在授权服务器端可以直接为客户端添加一个授权码许可,并保存成功,然而,在实际对接过程中会碰到麻烦,即在从前端页面跳转到 Duende IdentityServer 页面后,会得到一个 invalid request 页面。
所以,需要为这个 Vue 项目专门配置一个使用授权码许可模式的客户端。
步骤 1: Vue 项目添加相关的依赖
这一次,我们不直接使用 oidc-client js,而是使用 oidc-client-ts 这个使用 TypeScript 的包,并且引入 vuex 和 vuex-oidc:
"oidc-client-ts": "^2.0.6",
"vuex": "^3.0.1",
"vuex-oidc": "^4.0.0"
步骤 2: 为 Vue 项目添加多环境支持
如果有多个对接环境,可以再引入 dotenv,并且在项目中可以添加多个不同的 .env 文件。比如用 .env.local 和 .env.live 来分别存储本地和线上环境的配置,那么,可以同步为这两个环境准备两套不同的启动命令,用来加载对应的配置:
"serve:local": "dotenv -e .env.local vue-cli-service serve",
"serve:live": "dotenv -e .env.live vue-cli-service serve",
步骤 3: 配置 OIDC 信息
以 env.local 为例:
VUE_APP_BASE_API=http://localhost:3000
VUE_APP_OIDC_CONFIG={
"accessTokenExpiringNotificationTime":30,
"authority":"https://id6.azurewebsites.net/",
"clientId":"xxx",
"clientSecret": "yyy",
"redirectUri":"http://localhost:3000/oidc-callback",
"responseType":"code",
"scope":"openid email profile","automaticSilentRenew":true,
"automaticSilentSignin":false,
"silentRedirectUri":"http://localhost:3000/silent-renew-oidc.html"
}
为了从项目中读取配置好的 OIDC 信息,添加一个 src/oidc/oidc_config.js
文件:
export const oidcSettings = JSON.parse(process.env.VUE_APP_OIDC_CONFIG)
步骤 4: 添加 silent renew 页面
前文《使用 IdentityServer 保护 Vue 前端 - Jeff Tian的文章 - 知乎 》中使用了拷贝 oidc-client js 的方式,这一次我们不用这么做了。添加一个 src/oidc/silent-renew.js
文件:
import 'core-js/fn/promise'
import { vuexOidcProcessSilentSignInCallback } from 'vuex-oidc'
import { oidcSettings } from './oidc_config'
vuexOidcProcessSilentSignInCallback(oidcSettings)
步骤 5: main.js、App.vue 等文件的改造
上一次,我们没有使用状态管理工具,于是引入了一个 security js,并且通过 globalMethods 的方式注入了 OIDC 相关的方法,这一次,不再需要 security js,在实例化 Vue 时,也不再传递 data 和 methods,而是传递 store(因为用了 vuex 状态管理工具):
...
import App from './App.vue'
import store from './oidc/store'
import router from "@/router";
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
其中,src/oidc/store.js
文件如下:
import Vue from 'vue'
import Vuex from 'vuex'
import { vuexOidcCreateStoreModule } from 'vuex-oidc'
import { oidcSettings } from './oidc_config'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
oidcStore: vuexOidcCreateStoreModule(
oidcSettings,
{
namespaced: true,
dispatchEventsOnWindow: true
},
// Optional OIDC event listeners
{
userLoaded: (user) => console.log('OIDC user is loaded:', user),
userUnloaded: () => console.log('OIDC user is unloaded'),
accessTokenExpiring: () => console.log('Access token will expire'),
accessTokenExpired: () => console.log('Access token did expire'),
silentRenewError: () => console.log('OIDC user is unloaded'),
userSignedOut: () => console.log('OIDC user is signed out'),
oidcError: (payload) => console.log('OIDC error', payload),
automaticSilentRenewError: (payload) => console.log('OIDC automaticSilentRenewError', payload)
}
)
}
})
这一次,我们使用了 vuex 做状态管理,在实例化 Vue 时,可以将 oidc 相关的方法直接映射过去(src/App.vue
):
import {mapGetters} from "vuex";
export default {
name: 'App',
computed: {
...mapGetters('oidcStore', [
'oidcAccessToken',
'oidcIsAuthenticated',
'oidcAuthenticationIsChecked',
'oidcUser',
'oidcIdToken',
'oidcIdTokenExp'
]),
userDisplay: function () {
return this.oidcUser?.email ?? 'User'
}
},
components: {
},
data() {
...
步骤 6: 改造 router
上一次,我们给私有路由添加了 meta 属性,并用 requiresAuth 来标记需要登录。这一次,我们使用 vuex-oidc 提供的 vuexOidcCreateRouterMiddleware 来达到同样的效果。src/router/index.js
:
import Vue from 'vue'
import OidcCallback from "@/views/OidcCallback.vue";
import Router from "vue-router";
import {vuexOidcCreateRouterMiddleware} from "vuex-oidc";
import store from "@/oidc/store";
Vue.use(Router)
onst router = new Router({
mode: 'history',
base: '/ars-notification-dashboard',
routes: [
{
path: '/',
},
{
path: '/private',
name: 'private page',
component: resolve => require(['@/pages/private.vue'], resolve)
},
{
path: '/oidc-callback',
name: 'oidcCallback',
component: OidcCallback,
meta: {
isPublic: true
}
}
]
});
router.beforeEach(vuexOidcCreateRouterMiddleware(store, 'oidcStore'))
export default router;
注意,这一次我们首先假定所有页面都需要登录,而对于登录回调页面,通过 meta 中的 isPublic 来标记允许匿名访问。对于登录回调页面,其代码如下。src/views/OidcCallback.vue
:
<template>
<div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'OidcCallback',
methods: {
...mapActions('oidcStore', [
'oidcSignInCallback'
])
},
created () {
this.oidcSignInCallback()
.then((redirectPath) => {
this.$router.push(redirectPath)
})
.catch((err) => {
console.error(err)
this.$router.push('/signin-oidc-error') // Handle errors any way you want
})
}
}
</script>
步骤 7: 给 API 请求添加认证头
这一步其实是与前文《使用 IdentityServer 保护 Vue 前端 - Jeff Tian的文章 - 知乎 》一致的,只是取 token 的写法略有调整。src/api/request.js
:
import store from '@/oidc/store'
service.interceptors.request.use(config => {
const accessToken = store.state.oidcStore.access_token
if(accessToken){
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config
})