因公司後臺按照業務劃分,不同的業務需要有不同的後臺,越來越多的時候每次登入後臺都要重新輸入賬號密碼實在是不方便,所以需要實現一個 SSO 單點登入,網上翻閱了一些 SSO 的實現方案,有如下幾個實現方案:
- 基於父級域名實現跨域 Cookie
- 基於 LocalStorage 跨域
- 基於自己搭建認證中心
本篇文章選用了搭建認證中心該方案,該實現效果和其他 SSO 單點登入一樣,有如下特點:
跨域名的單點登入(一個站點登入即所有站點都登入)
跨域名的單點退出(一個站點退出即所有站點都退出)
實時的賬戶資訊同步(當 A 站點的 A 賬戶切換了 B 賬戶重新登入後,訪問其他站點也會切換至 B 賬戶)
涉及技術點
Redis(採用 Redis 儲存使用者的 Token 實現多站點統一 Token)
JWT(採用 JWT 實現使用者的 Token 加密)
後臺實現
passport 後臺
passport 後臺登入需要實現的方法
- login:賬號登入(供使用者登入使用)
- authTokenLogin:授權碼登入(供其他站點自動登入使用)
- getAuthToken:獲取授權 token
- logout:退出登入
- Middleware 中介軟體校驗
賬號登入
// TODO:: 基礎邏輯,驗證賬號密碼業務邏輯...
// 呼叫 jwt 生成token
$token = JWT::enToken($admin->id);
// 將使用者token存入 redis
Redis::set("adminUserToken:{$admin->id}", $token);
return $this->succeed([
'token' => $token
]);
賬號登入的通用邏輯為使用 jwt 生成 token,然後將使用者 token 存入 Redis。
授權碼登入
// 獲取到前端傳來的授權token,並解密出使用者ID
$adminId = CommonSupport::authcode($request->input("authToken"));
// TODO:: 驗證業務邏輯...
// 獲取使用者當前的登入token
$redisToken = Redis::get("adminUserToken:{$adminId}");
return $this->succeed([
'token' => $redisToken
]);
授權碼登入的使用場景為當訪問其他站點時,若本地 token 已過期或本地沒有 token,則重定向至 passport 後臺,passport 後臺判斷本地為已登入狀態時會向介面索要 authToken,並帶上 authToken 重定向至其他站點,其他站點獲取到 url 上有 authToken 時,則會用 authToken 進行登入,然後下發使用者當前的 token,並儲存,完成了登入流程。
獲取授權 token
// 獲取當前已登入的使用者ID
$adminId = Context::get("currentAdmin")['id'];
// 加密獲取授權Token
$authToken = CommonSupport::authcode($adminId, "ENCODE");
return $this->succeed([
"authToken" => $authToken
]);
退出登入
// 獲取當前已登入的使用者ID
$adminId = Context::get("currentAdmin")['id'];
// 將該使用者 Token 從 redis 中刪除
Redis::del("adminUserToken:{$adminId}");
return $this->succeed();
中介軟體校驗
public function checkToken(string $token)
{
// 解密 JWT token,驗證 token 是否有效,若解密失敗則報錯
$jwt = JWT::deToken($token);
if (!$jwt) {
throw new ApiException(10001, "使用者驗證失敗");
}
$userId = (int)$jwt->data;
// 從 redis 中獲取使用者token
$redisToken = Redis::get("adminUserToken:{$userId}");
// 如果使用者token不存在redis或者和redis中的不相等,則報錯
if ($redisToken != $token) {
throw new ApiException(10001, "使用者驗證失敗");
}
// 判斷管理員是否存在
$admin = Admin::find($userId);
if (!$admin) {
throw new ApiException(10001, "使用者驗證失敗");
}
return $this->succeed($admin->toArray());
}
業務後臺
業務後臺的實現就變得簡單,只需要在中介軟體中呼叫 passport 後臺的 checkToken 方法,具體實現可使用 Http 方式請求,或者 Rpc 呼叫。
前端實現
前端技術採用的是 Vue2.0,使用 vue-element-admin實現
passport 前端
permission.js 檔案
permission.js 檔案在每次重新整理頁面時都會進入該頁面,在該頁面通過獲取 url 上特定的引數來完成特定的動作
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// 設定頁面標題
document.title = getPageTitle(to.meta.title)
// 全域性獲取url上的請求引數
const query = to.query
// 獲取本地token
const hasToken = getToken()
// 如果登入了
if (hasToken) {
// 如果有場景值
if (query.scene) {
// 如果場景值是退出登入,並且有重定向地址,則跳轉到登入介面並銷燬本地token
if (query.scene === 'logout' && query.redirectUri) {
const redirectUri = encodeURIComponent(query.redirectUri)
await store.dispatch('user/logout')
next(`/login?redirectUri=${redirectUri}`)
NProgress.done()
return
}
}
// 如果有重定向地址
if (query.redirectUri) {
// 則獲取授權token
const authToken = await getAuthToken()
const redirectUri = query.redirectUri
// 跳轉到重定向地址,把授權token帶過去
window.location = redirectUri + '?token=' + authToken.data.authToken
NProgress.done()
return
}
// 如果沒登入
} else {
// 如果有場景值
if (query.scene) {
// 如果場景值是退出登入,並且有重定向地址,則跳轉到登入介面
if (query.scene === 'logout' && query.redirectUri) {
next(`/login?redirectUri=${encodeURIComponent(query.redirectUri)}`)
NProgress.done()
return
}
}
// 如果有企業微信回撥回來的code
if (query.code) {
// 呼叫介面使用微信授權碼登入
const tokenData = await workWechatCodeLogin({
code: query.code
})
// 將 token 存入本地
const token = tokenData.data.token
setToken(token)
// 如果有重定向地址
if (query.state) {
window.location.href = process.env.VUE_APP_CURRENT_URL +
'/home?redirectUri=' + encodeURIComponent(Base64.decode(query.state))
} else {
window.location.href = process.env.VUE_APP_CURRENT_URL
}
}
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
if (query.redirectUri) {
next(`/login?redirectUri=${query.redirectUri}`)
} else {
next(`/login?redirect=${to.path}`)
}
NProgress.done()
}
}
})
request.js
該檔案為網路請求基礎檔案,該檔案中需要修改當介面返回登入失效的時候,直接跳轉至登入頁。
response => {
const res = response.data
// if the custom code is not 200, it is judged as an error.
if (res.code !== 200) {
// 如果介面返回 10001 代表登入失效,則重定向至登入頁
if (res.code === 10001) {
// 先刪除token
removeToken()
// 然後跳轉到登入頁
window.location = process.env.VUE_APP_CURRENT_URL + '/login'
return
}
Message({
message: res.msg || 'Error',
type: 'error',
duration: 5 * 1000
})
return Promise.reject(new Error(res.msg || 'Error'))
} else {
return res
}
},
業務前端
permission.js
//TODO:: 先判斷如果是沒登入的話
// 判斷是否有從 passport 後臺重定向回來並帶著token
if (query.token) {
// 根據授權token去請求登入token
const tokenData = await authTokenLogin({
authToken: query.token,
platform_id: process.env.VUE_APP_CURRENT_PLATFORM_ID
})
// 將token存入本地並重新整理當前頁面
const token = tokenData.data.token
setToken(token)
window.location = process.env.VUE_APP_CURRENT_HOME_URL
location.reload()
// 如果沒有 授權token引數
} else {
// 則跳轉到 passport 登入頁面並帶著當前後臺的地址作為 redirectUri 引數
// other pages that do not have permission to access are redirected to the login page.
const redirectUri = encodeURIComponent(process.env.VUE_APP_CURRENT_HOME_URL)
window.location = process.env.VUE_APP_PASSPORT_WEB_LOGIN_URL + '?redirectUri=' + redirectUri
NProgress.done()
}
request.js
if (res.code !== 200) {
// 如果介面返回 10001 代表登入失效,則重定向至登入頁
if (res.code === 10001) {
// 先刪除token
removeToken()
// 然後帶著當前地址作為 redirectUri 跳轉到 SSO 登入頁
const redirectUri = encodeURIComponent(process.env.VUE_APP_CURRENT_HOME_URL)
window.location = process.env.VUE_APP_PASSPORT_WEB_LOGIN_URL + '?scene=logout&redirectUri=' + redirectUri
}
Message({
message: res.msg || 'Error',
type: 'error',
duration: 5 * 1000
})
return Promise.reject(new Error(res.msg || 'Error'))
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結