一、登錄機制
在項目中,我們可以大致得出一個登錄的過程,主要分為 登錄驗證、登錄保持、退出三個部分。登錄驗證是指客戶端提供用戶名和密碼,向服務器提出登錄請求,服務器判斷客戶端是否可以登錄並向客戶端確認。 登錄保持是指客戶端登錄后, 服務器能夠分辨出已登錄的客戶端,並為其持續提供登錄權限的服務器。退出是指客戶端主動退出登錄狀態。而登錄保持容易想到的方案是,客戶端登錄成功后, 服務器為其分配sessionId(token), 客戶端隨后每次請求資源時都帶上sessionId(token)。
注意:session與token的區別,可閱讀以下文章
- https://blog.csdn.net/mydistance/article/details/84545768
- https://www.cnblogs.com/moyand/p/9047978.html
1.1 登錄驗證
-
- 圖解
-
- 流程
- 客戶端向服務器 第一次 發起登錄請求(不傳輸用戶名和密碼);
- 服務器利用RSA算法產生一對serverRSAPublicKey和serverRSAPrivateKey,並保留serverRSAPrivateKey, 將serverRSAPublicKey發送給客戶端;
- 客戶端收到serverRSAPublicKey之后,同樣利用RSA算法產生一對clientRSAPublicKey與clientRSAPrivateKey,客戶端自己保留clientRSAPrivateKey,並用serverRSAPublicKey對登錄的數據以及clientRSAPublicKey進行加密,用加密后的數據 第二次 發出登錄請求;
- 服務器收到加密后的數據,利用保留的serverRSAPrivateKey對密文進行解密,(經過判斷,確定用戶可以登錄之后,生成一個加密字符串Token,)同時生成一對對稱的AES密鑰,並且利用密文中的clientRSAPublicKey對Token以及aesKey進行加密,將aesKey與Token全部返回給客戶端;
- 客戶端收到clientRSAPublicKey加密的aesKey與Token后,利用保留的clientRSAPrivateKey對其進行解密,並將其存儲在localStorage中,使客戶端每次發送請求headers都攜帶Token這一字段(基於Token的身份驗證是無狀態的,並且具有時效性);
- 編碼
- HTML(由於目前成功的接口只找到username與password這兩個字段的接口,為了方便講解,先調用一下)
<template> <div class="security-main" style="width: 348px;height: 340px;margin:20px"> <el-form :model="loginData" :rules="loginRules" ref="loginForm" style="margin-top:20px"> <el-form-item prop="username"> <el-input id="phoneNumber" v-model.trim="loginData.username" maxlength="11" placeholder="請輸入手機號" ></el-input> </el-form-item> <el-form-item prop="password"> <el-input id="verificationCode" style="width: 55%;float:left;" maxlength="6" v-model.trim="loginData.password" placeholder="請輸入驗證碼" ></el-input> <el-button type="primary" class="btn-verificationCode" style="width: 43%;float: right;padding: 12px 0px;" >獲取驗證碼</el-button> </el-form-item> <el-form-item> <el-button :disabled="loginBtnDisabled" type="primary" @click="submitForm('loginForm')"> <i class="el-icon-loading" v-if="isLogining"></i>登錄 </el-button> </el-form-item> </el-form> </div> </template> <script> import { loginData, loginRules } from "../js/login/data"; import { loginHandler } from "../js/login/bussiness"; const _loginData = _.cloneDeep(loginData); export default { name: "security", data() { return { loginData, //登錄的表單數據 loginRules, //登錄表單的校驗規則 loginBtnDisabled: false, //登錄button是否禁用 isLogining: false, //是否登錄中 serverRSAPublicKey: "", //服務端rsa公鑰 clientRSAPublicKey: "", //客戶端rsa公鑰 clientRSAPrivateKey: "" //客戶端rsa私鑰 }; }, components: {}, mounted() { // console.log(_loginData); }, methods: { submitForm(formName) { this.$refs[formName].validate(valid => { if (valid) { this.isLogining = true; this.loginBtnDisabled = true; loginHandler({ vue: this }); // this.$refs[formName].resetFields(); } else { return false; } }); } } }; </script> <style lang="scss"> .security-main { } </style> <style scoped lang="scss"> .security { width: 100%; height: 100%; &-main { } } </style>
- HTML(由於目前成功的接口只找到username與password這兩個字段的接口,為了方便講解,先調用一下)
- 流程
-
-
- 所涉及到的數據data(loginData里面的uuid相當於userId)
//登錄表單data export const loginData = { clientPublicKey: '', //客戶端生成的RSA公鑰base64后的字符串 不可為空 username: '', //手機號 不可為空 password: '', //手機驗證碼 uuid: '', //獲取服務端RSA公鑰時返回的uuid string 放到header中,key為uuid } //登錄表單校驗規則 export const loginRules = { username: [ { required: true, message: '請輸入手機號', trigger: 'blur' }, // { pattern: /^1(3[0-2]|4[5]|5[56]|7[6]|8[56])[0-9]{8}$|^1709[0-9]{7}$|^1(3[4-9]|4[7]|5[0-27-9]|7[8]|8[2-478])[0-9]{8}$|^1705[0-9]{7}$|^1(33|53|7[37]|8[019])[0-9]{8}$|^1700[0-9]{7}$|^1(99|98|66)[0-9]{8}$/, message: '手機號碼格式不正確', trigger: 'blur' } ], password: [ { required: true, message: '請輸入手機驗證碼', trigger: 'blur' }, // { pattern: /^\d{4,}$/, message: '手機驗證碼格式不正確', trigger: 'blur' } ], }
- JS文件中(利用node-rsa工具進行密鑰的處理)
import RSA from 'node-rsa' import { getServerRSAPublicKey, login } from '../../api/tuning' /** * [loginHandler 處理用戶登錄數據加密邏輯] * 1-----獲取RSA key * 2-----獲取AES key(通過登錄) * @param {[JSON]} config [配置] * @return {[type]} [description] */ export async function loginHandler(config) { const { vue } = config; try { //1、不傳輸登錄數據,獲取RSA公鑰 ----- 會返回一個serverPublicKey 與一個 uuid const rsaPromise = await getServerRSAPublicKey({}) //2、請求成功后 處理獲取服務端RSA公鑰 const getRSAKeyReturnCode = handleGetServerRSAPublicKey({ vue, promise: rsaPromise, dataKey: 'loginData' }) //4、處理好客戶端與服務器的rsa if (getRSAKeyReturnCode === 200) { //5、用服務端返回的rsa公鑰對登錄的數據(客戶端保留私鑰、把公鑰發送給服務器)進行加密 const rsaEncryptBody = RSAPublicKeyEncrypt(vue.serverRSAPublicKey, JSON.stringify(vue.loginData)) const loginConfig = { data: { encryptContent: rsaEncryptBody }, headers: { uuid: vue.loginData.uuid } } //6、服務端利用保留的rsa私鑰對加密的數據進行解密 並且在服務端生成aes對稱密鑰 // 並用獲取到的客戶端公鑰對aes密鑰以及客戶端需要的token進行加密 傳遞給客戶端 const loginPromise = await login(loginConfig) handleLoginData({ vue, promise: loginPromise, }) } } catch (error) { console.log(error) } finally { vue.isLogining = false vue.loginBtnDisabled = false; vue.$refs['loginForm'].resetFields(); } } /** * [handleGetServerRSAPublicKey 處理獲取服務端RSA公鑰] * @param {[JSON]} config [參數] * @return {[type]} [description] */ export function handleGetServerRSAPublicKey(config) { const { vue, promise, dataKey } = config if (promise.data.code === 200) { const { serverPublicKey, uuid } = promise.data.body; // 3、生成客戶端的 RSA 公鑰與私鑰 引用node-rsa工具 const clientRSAKeyPair = generateClientRSAKeyPair(); // console.log(clientRSAKeyPair.clientRSAPublicKey.replace(/\r|\n|\s/g, '').split('-----')) // console.log(clientRSAKeyPair.clientRSAPublicKey.split('-----')[2]) const clientRSAPublicKey = clientRSAKeyPair.clientRSAPublicKey.replace(/\r|\n/g, '').split('-----')[2] // console.log(clientRSAKeyPair.clientRSAPrivateKey.split('-----')) const clientRSAPrivateKey = clientRSAKeyPair.clientRSAPrivateKey.replace(/\r|\n/g, '') // console.log(clientRSAPrivateKey) vue[dataKey].clientPublicKey = clientRSAPublicKey vue[dataKey].uuid = uuid; vue.serverRSAPublicKey = '-----BEGIN PUBLIC KEY-----' + serverPublicKey + '-----END PUBLIC KEY-----' vue.clientRSAPublicKey = clientRSAPublicKey vue.clientRSAPrivateKey = clientRSAPrivateKey } return promise.data.code } /** * [generateClientRSAKeyPair 客戶端生成RSA公鑰私鑰對] * @return {[type]} [description] */ export function generateClientRSAKeyPair() { // 首先生成1024位密鑰 const NodeRSAKey = new RSA({ b: 1024 }) // 導出公鑰 const clientRSAPublicKey = NodeRSAKey.exportKey('public') // 導出私鑰 const clientRSAPrivateKey = NodeRSAKey.exportKey('pkcs8') return { clientRSAPublicKey, clientRSAPrivateKey, } } /** * [RSAPublicKeyEncrypt RSA公鑰加密] * @param {[String]} publicKey [公鑰] * @param {[String]} originalBody [要加密的明文字符串] * @return {[String]} [RSA公鑰加密結果(Base64字符串)] */ export function RSAPublicKeyEncrypt(publicKey, originalBody) { /*if (!SecurityUtils.currentRSAPublicKey || SecurityUtils.currentRSAPublicKey !== SecurityUtils.publicKey) { SecurityUtils.publicRSAInstance = new RSA(publicKey) }*/ const NodeRSAKey = new RSA(publicKey) NodeRSAKey.setOptions({ encryptionScheme: 'pkcs1', environment: 'browser' }) const encryptBase64 = NodeRSAKey.encrypt(originalBody, 'base64', 'utf8') return encryptBase64 } /** * [RSAPublicKeyDecrypt RSA私鑰解密] * @param {[String]} privateKey [私鑰] * @param {[String]} encryptBody [要解密的數據] * @return {[JSON]} [RSA私鑰解密結果(JSON)] */ export function RSAPrivateKeyDecrypt(privateKey, encryptBody) { /*if (!SecurityUtils.currentRSAPrivateKey || SecurityUtils.currentRSAPrivateKey !== SecurityUtils.privateKey) { SecurityUtils.privateRSAInstance = new RSA(privateKey) }*/ const NodeRSAKey = new RSA(privateKey) NodeRSAKey.setOptions({ encryptionScheme: 'pkcs1', environment: 'browser' }) const originalBody = NodeRSAKey.decrypt(encryptBody, 'utf8') return JSON.parse(originalBody) } /** * [handleLoginData 處理用戶登錄邏輯] * @param {[JSON]} config [參數] * @return {[type]} [description] */ export function handleLoginData(config) { const { vue, promise } = config if (promise.data.code === 200) { // 7、在客戶端使用保留的rsa私鑰對返回的數據進行解密 數據是accessToken 與aesKey promise.data.body = RSAPrivateKeyDecrypt(vue.clientRSAPrivateKey, promise.data.body); // 8、將這兩個字段存入localStorage中 const { aesKey, accessToken } = promise.data.body window.localStorage.setItem('aesKey', aesKey) window.localStorage.setItem('auth-token', accessToken); // 9、登錄成功后,接下來的代碼可以寫一些頁面的跳轉或者開發者項目的邏輯 // vue.$router.push({ name: '' }) } else if (promise.data.code === 205403) { const config = { vue, redirectUrl: promise.data.redirect } } else { vue.$message({ type: 'fail', message: promise.data.msg, duration: 1000, }) } return promise.data.code }
- 第一次發起登錄,獲取serverRSAPublicKey;
- api文件中(相當於發送axios請求)
import fetch from '../../../utils/fetch' import adrsConfig from '../config/adrs.config' import urlConfig from '../config/url.config' /** * [getServerRSAPublicKey 獲取服務端RSA公鑰] * @param {[JSON]} config [請求參數] * @return {[Promise]} [Promise] */ export function getServerRSAPublicKey(config) { const defaultConfig = { url: adrsConfig.IS_USE_RAP ? (adrsConfig.RAP_URL + urlConfig.GET_RSA_PUBLIC_KEY_URL) : (adrsConfig.USER_SERVICE_URL + urlConfig.GET_RSA_PUBLIC_KEY_URL), method: 'get', data: {}, } const mergeConfig = _.assign({}, defaultConfig, config) return fetch(mergeConfig) } /** * [login 用戶進行登錄 ] * @param {[JSON]} config [請求參數] * @return {[Promise]} [Promise] */ export function login(config) { const defaultConfig = { url: adrsConfig.IS_USE_RAP ? (adrsConfig.RAP_URL + urlConfig.SYS_LOGIN_URL) : (adrsConfig.USER_SERVICE_URL + urlConfig.SYS_LOGIN_URL), method: 'post', data: {}, } const mergeConfig = _.assign({}, defaultConfig, config) return fetch(mergeConfig) }
- 所涉及到的數據data(loginData里面的uuid相當於userId)
-
1.2 登錄保持
*** 注意:簽名指用私鑰加密的消息(只有擁有私鑰的用戶可以生成簽名)
在最原始的方案中,登錄保持僅僅靠服務器生成的sessionId,客戶端的請求中帶上sessionId, 如果服務器的redis中存在這個id,就認為請求來自相應的登錄客戶端。 但是
只要sessionId被截獲, 請求就可以為偽造,存在安全隱患;
引入token后,服務器將token和其它的一些變量(用戶數據,例如uuid),利用一些算法(散列算法、對稱加密或者非對稱加密)得到簽名后,將簽名和登錄的數據一並發送給客戶端,
;客戶端收到token之后,每次
發送請求,headers都攜帶了token,服務器收到token之后,再次利用相同的散列加密算法對數據在進行計算(服務端對token並不進行保存),生成新的token,如果生成的token與
攜帶的token一致, 就認為請求來自登錄的客戶端。如果不一致,則說明沒有登陸過,或者用戶的數據被人篡改了。
1.3 退出(用戶退出系統的原理 ----- 有以下兩種狀況)
- 服務端將對應的sessionId從redis隊列中刪除;
- Token具有時效性,或者用戶手動將其刪除;
二、對稱加密、非對稱加密、散列(哈希)算法
- 對稱加密
- AES
- DES
- 非對稱加密(加密密鑰與解密密鑰不相同,並且不可能從加密密鑰推導出解密密鑰,也叫公鑰加密算法)
- RSA
- 散列算法(簽名算法)
- MD5
三、遇到的問題
暫無