使用 IdentityServer 保护 Vue 前端

共 6648字,需浏览 14分钟

 ·

2022-12-18 14:05

前情提要

使用 IdentityServer 保护 Web 应用(AntD Pro 前端 + SpringBoot 后端)》中记录了使用 IdentityServer 保护前后端的过程,其中的前端工程是以 UMI Js 为例。今天,再来记录一下使用 IdentityServer 保护 Vue 前端的过程,和 UMI Js 项目使用 umi plugin 的方式不同,本文没有使用 Vue 相关的插件,而是直接使用了 oidc-client js。

另外,我对 Vue 这个框架非常不熟,在 vue-router 这里稍微卡住了一段时间,后来瞎试居然又成功了。针对这个问题,我还去 StackOverflow 上问了,但并没有收到有效的回复:https://stackoverflow.com/questions/74769607/how-to-access-vues-methods-from-navigation-guard

准备工作

首先,需要在 IdentityServer 服务器端注册该 Vue 前端应用,仍然以代码写死这个客户端为例:

new Client            {                ClientId = "vue-client",                ClientSecrets = { new Secret("vue-client".Sha256()) },                ClientName = "vue client",                AllowedGrantTypes = GrantTypes.Implicit,                AllowAccessTokensViaBrowser = true,                RequireClientSecret = false,                RequirePkce = true,
RedirectUris = { "http://localhost:8080/callback", "http://localhost:8080/static/silent-renew.html", }, AllowedCorsOrigins = { "http://localhost:8080" }, AllowedScopes = { "openid", "profile", "email" }, AllowOfflineAccess = true, AccessTokenLifetime = 90, AbsoluteRefreshTokenLifetime = 0, RefreshTokenUsage = TokenUsage.OneTimeOnly, RefreshTokenExpiration = TokenExpiration.Sliding, UpdateAccessTokenClaimsOnRefresh = true, RequireConsent = false, };

在 Vue 工程里安装 oidc-client

yarn add oidc-client

在 Vue 里配置 IdentityServer 服务器信息

在项目里添加一个 src/security/security.js文件:

import Oidc from 'oidc-client'
function getIdPUrl() { return "https://id6.azurewebsites.net";}
Oidc.Log.logger = console;Oidc.Log.level = Oidc.Log.DEBUG;
const mgr = new Oidc.UserManager({ authority: getIdPUrl(), client_id: 'vue-client', redirect_uri: window.location.origin + '/callback', response_type: 'id_token token', scope: 'openid profile email', post_logout_redirect_uri: window.location.origin + '/logout', userStore: new Oidc.WebStorageStateStore({store: window.localStorage}), automaticSilentRenew: true, silent_redirect_uri: window.location.origin + '/silent-renew.html', accessTokenExpiringNotificationTime: 10,})
export default mgr

在 main.js 里注入登录相关的数据和方法

数据

不借助任何状态管理包,直接将相关的数据添加到 Vue 的 app 对象上:

import mgr from "@/security/security";
const globalData = { isAuthenticated: false, user: '', mgr: mgr}


方法

const globalMethods = {    async authenticate(returnPath) {        console.log('authenticate')        const user = await this.$root.getUser();        if (user) {            this.isAuthenticated = true;            this.user = user        } else {            await this.$root.signIn(returnPath)        }    },    async getUser() {        try {            return await this.mgr.getUser();        } catch (err) {            console.error(err);        }    },    signIn(returnPath) {        returnPath ? this.mgr.signinRedirect({state: returnPath}) : this.mgr.signinRedirect();    }}

修改 Vue 的实例化代码

new Vue({  router,  data: globalData,  methods: globalMethods,  render: h => h(App),}).$mount('#app')

修改 router

src/router/index.js中,给需要登录的路由添加 meta 字段:

Vue.use(VueRouter)
const router = new VueRouter({ { path: '/private', name: 'private page', component: resolve => require(['@/pages/private.vue'], resolve), meta: { requiresAuth: true } }});
export default router


接着,正如在配置中体现出来的,需要一个回调页面来接收登录后的授权信息,这可以通过添加一个 src/views/CallbackPage.vue 文件来实现:

<template>  <div>    <p>Sign-in in progress... 正在登录中……</p>  </div></template>
<script>export default { async created() { try { const result = await this.$root.mgr.signinRedirectCallback(); const returnUrl = result.state ?? '/'; await this.$router.push({path: returnUrl}) }catch(e){ await this.$router.push({name: 'Unauthorized'}) } }}</script>


然后,需要在路由里配置好这个回调页面:

import CallbackPage from "@/views/CallbackPage.vue";

Vue.use(VueRouter)
const router = new VueRouter({ routes: { path: '/private', name: 'private page', component: resolve => require(['@/pages/private.vue'], resolve), meta: { requiresAuth: true } }, { path: '/callback', name: 'callback', component: CallbackPage }});
export default router


同时,在这个 router 里添加一个所谓的“全局前置守卫”(https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E5%85%A8%E5%B1%80%E5%89%8D%E7%BD%AE%E5%AE%88%E5%8D%AB),注意就是这里,我碰到了问题,并且在 StackOverflow 上提了这个问题。在需要调用前面定义的认证方法时,不能使用 router.app.authenticate,而要使用 router.apps[1].authenticate,这是我通过 inspect router发现的:

...
router.beforeEach(async function (to, from, next) { let app = router.app.$data || {isAuthenticated: false} if(app.isAuthenticated) { next() } else if (to.matched.some(record => record.meta.requiresAuth)) { router.apps[1].authenticate(to.path).then(()=>{ next() }) }else { next() }})
export default router


到了这一步,应用就可以跑起来了,在访问 /private 时,浏览器会跳转到 IdentityServer 服务器的登录页面,在登录完成后再跳转回来。

添加 silent-renew.html

注意 security.js,我们启用了 automaticSilentRenew,并且配置了 silent_redirect_uri的路径为 silent-renew.html。它是一个独立的引用了 oidc-client js 的 html 文件,不依赖 Vue,这样方便移植到任何前端项目。

oidc-client.min.js

首先,将我们安装好的 oidc-client 包下的 node_modules/oidc-client/dist/oidc-client.min.js文件,复制粘贴到 public/static目录下。


然后,在这个目录下添加 public/static/silent-renew.html文件。


<!DOCTYPE html><html><head>    <title>Silent Renew Token</title></head><body><script src='oidc-client.min.js'></script><script>    console.log('renewing tokens');    new Oidc.UserManager({userStore: new Oidc.WebStorageStateStore({ store: window.localStorage })})        .signinSilentCallback();</script></body></html>


给 API 请求添加认证头

最后,给 API 请求添加上认证头。前提是,后端接口也使用同样的 IdentityServer 来保护(如果是 SpringBoot 项目,可以参考《[使用 IdentityServer 保护 Web 应用(AntD Pro 前端 + SpringBoot 后端) - Jeff Tian的文章 - 知乎](https://zhuanlan.zhihu.com/p/533197284) 》);否则,如果 API 是公开的,就不需要这一步了。

对于使用 axios 的 API 客户端,可以利用其 request interceptors,来统一添加这个认证头,比如:

import router from '../router'import Vue from "vue";
const v = new Vue({router})
const service = axios.create({ // 公共接口--这里注意后面会讲 baseURL: process.env.BASE_API, // 超时时间 单位是ms,这里设置了3s的超时时间 timeout: 20 * 1000});
service.interceptors.request.use(config => { const user = v.$root.user; if(user) { const authToken = user.access_token; if(authToken){ config.headers.Authorization = `Bearer ${authToken}`; } }
return config;}, Promise.reject)
export default service


浏览 56
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报