UNPKG

@holyhigh/juth2-client.js

Version:

A Juth2 client for javascript

354 lines (319 loc) 9.54 kB
/* eslint-disable max-len */ import _ from '@holyhigh/func.js'; import Log from './log'; import KJUR from 'jsrsasign'; import axios from 'axios'; const SESSION_KEY_JWT = 'Juth2_JWT'; const SESSION_GLOPTS_KEY = 'Juth2_client_opts'; const JWT_STR_KEY = Symbol('func.js'); const JWT_OBJ_KEY = Symbol('juth2'); /** * juth2客户端,用于浏览器中Juth2-Server的通信 * 提供鉴权、登入、登出、api调用等服务 * * @param {Object} opts 请求选项 * @author holyhigh https://gitee.com/holyhigh2 */ const juth2 = {}; // /////////////// XHR juth2._axios = axios.create(); juth2._axios.interceptors.request.use(function(config) { if (!juth2._inited) { return Promise.reject({message: Log.getTitle()+': The client has not been initialized', status: -1}); } const jwt = sessionStorage.getItem(SESSION_KEY_JWT); if (!jwt && config.url != '/juth2/authorize') { return Promise.reject({message: Log.getTitle()+': Not signed request', status: -2}); } return config; }, function(error) { return Promise.reject( error); }); juth2._axios.interceptors.response.use(function(response) { return response; }, function(error) { const resp = error.response;// xhr errors const msg = resp?resp.data.message:error.message; const statusCode = resp?resp.status:0; if ( (statusCode === 403 && msg.includes('token')) || statusCode === 401 && msg.includes('out') ) { juth2[JWT_STR_KEY] = undefined; sessionStorage.removeItem(SESSION_KEY_JWT); } return Promise.reject(msg, statusCode, error); }); // /////////////// API _.assign(juth2, { getRequestHeader() { return this._axios.defaults.headers.common; }, async request(opts) { return await this._axios.request(opts); }, async get(url, opts) { opts = opts || {}; opts.method = 'GET'; opts.url = url; return await this.request(opts); }, async post(url, data, opts) { opts = opts || {}; opts.method = 'POST'; opts.url = url; opts.data = data; return await this.request(opts); }, /** * 初始化auth客户端,并在回调里获取登录状态 * * @param {string} clientId 后台获取 * @param {string} scope client对应的scope。多个scope通过空格分割,可以少于后台注册scope * @param {string} domain 验证服务器域名 * @param {string} origin 自己的服务器域名 * @param {string} secret 密钥,用于请求签名 * @param {string} [credentials=false] 是否启用credentials * @param {boolean} [daemon=false] 是否开启守护模式。守护模式会自动请求刷新token * @param {boolean} [log=false] 是否开启日志 * @param {Function} [onRefresh] 刷新时调用,可以在该事件中获取服务器响应的headers信息(AuthenticationHeaderObject) */ init({ clientId, scope, domain, origin, secret = '', credentials = false, daemon = false, log = false, onRefresh, }) { this.clientId = clientId; this.domain = domain; this.origin = origin; this.scope = scope; this.user = {}; this.secret = secret; this.daemon = daemon;// 守护进程 this.credentials = credentials; this.log = log; this.onRefresh = onRefresh; const opts = JSON.stringify({ clientId, scope, origin, domain, secret, credentials, daemon, log, }); if (opts != sessionStorage.getItem(SESSION_GLOPTS_KEY)) { // 保存初始化配置 sessionStorage.setItem(SESSION_GLOPTS_KEY, opts); this._inited = false; } if (!this._inited) { init(this); } }, /** * 通过用户名和密码方式进行token授权 * @param {*} username 用户名 * @param {*} password 密码 * @param {Object} extra 登陆时的额外验证信息 * @return {Promise} 返回promise对象 */ async signIn(username, password, extra) { try { const jwtToken = getJWTToken({ sub: 'authorize', iss: this.origin, aud: this.domain, client_id: this.clientId, grant_type: 'password', scope: this.scope, username, password, }, this.secret); const rs = await this.post( `/juth2/authorize`, extra, { headers: {// 设置JWT头 'Authorization': 'Bearer ' + jwtToken, }, withCredentials: this.credentials || false, }, ); const jwt = rs.headers['authorization']; if (!_.isEmpty(jwt)) { saveJWT(this, jwt.replace('Bearer ', '')); } return rs; } catch (e) { // printAxiosError(e, 'sign in error'); throw e; } }, /** * 登出操作 * @return {Promise} 返回promise对象 */ async signOut() { const rs = await this.get(`/juth2/sign-out`); this[JWT_STR_KEY] = undefined; sessionStorage.removeItem(SESSION_KEY_JWT); Log.info('Signed out'); return rs; }, /** * 是否已经登录 * 该方法只适用于在页面使用系统账号登录的场景 * @return {boolean} 是否已经登录 */ isAuthenticated() { return !!this[JWT_STR_KEY]; }, /** * 刷新授权token,如果开启了deamon模式会自动刷新 * @throws 如果没有刷新token权限会报错 */ async refreshToken() { const jwt = this[JWT_OBJ_KEY]; const jwtToken = getJWTToken({ sub: 'authorize', iss: this.origin, aud: this.domain, client_id: this.clientId, grant_type: 'refresh_token', scope: this.scope, at_hash: jwt.payloadObj.at_hash, refresh_token: jwt.payloadObj.refresh_token, }, this.secret); let rs; try { rs = await this.post( `/juth2/refresh-token`, null, { headers: {// 设置JWT头 'Authorization': 'Bearer ' + jwtToken, }, withCredentials: this.credentials || false, }, ); const newJwt = rs.headers['authorization']; saveJWT(this, newJwt.replace('Bearer ', ''), true); } catch (e) { printAxiosError(e, 'refresh token error', this); throw e; } return rs; }, }); /** * 初始化juth2 * @param {*} juth2 */ function init(juth2) { Log.info('Starting init...'); const opts = _.fval(sessionStorage.getItem(SESSION_GLOPTS_KEY)||'{}'); if (_.size(opts) > 0) { _.assign(juth2, opts); if (opts.daemon) { Log.info('Starting deamon mode...'); _deamonToken(juth2); } // 初始化axios参数 juth2._axios.defaults.baseURL = opts.domain; juth2._inited = true; } // jwt const jwt = sessionStorage.getItem(SESSION_KEY_JWT); if (!jwt) return; saveJWT(juth2, jwt); } // /////////////// INIT init(juth2); // 定时更新token // eslint-disable-next-line require-jsdoc async function _deamonToken(client) { try { const jwtInfo = client[JWT_OBJ_KEY]; if (jwtInfo) { const diffSec = ( new Date(jwtInfo.payloadObj.exp*1000).getTime() - Date.now() )/1000; if (diffSec < 60) {// 1分钟刷新 Log.info('starting to refresh token...'); await client.refreshToken(); } setTimeout(async function() { await _deamonToken(client); }, 30000); } else { setTimeout(async function() { await _deamonToken(client); }, 30000); } } catch (e) { throw e; } } // eslint-disable-next-line require-jsdoc function saveJWT(juth2, jwtStr, isRefresh) { juth2[JWT_STR_KEY] = jwtStr; const jwtInfo = KJUR.jws.JWS.parse(jwtStr); juth2[JWT_OBJ_KEY] = jwtInfo; // 保存防止刷新 sessionStorage.setItem(SESSION_KEY_JWT, jwtStr); // 更新 axios实例 juth2._axios.defaults.headers.common['Authorization'] = 'Bearer '+jwtStr; const title = isRefresh?'refresh token':'login'; Log.info( `${title} success at `+ new Date(jwtInfo.payloadObj.iat*1000).toTimeString()+ '. The Token will expired at '+ new Date(jwtInfo.payloadObj.exp*1000).toTimeString(), ); if (juth2.onRefresh) {// 回写最新鉴权信息 juth2.onRefresh({'Authorization': 'Bearer '+jwtStr}); } } /** * 输出Juth2错误 * @param {*} e * @param {*} desc */ function printAxiosError(e, desc) { const resp = e.response; const msg = resp?resp.data.message:e.message; Log.error(`${desc} `+msg, _.get(resp, 'status')); } // 已有claim https://www.iana.org/assignments/jwt/jwt.xhtml /** * 创建JWT token * @param {*} payload sub/iss/aud/client_id 必填,其他验证字段根据grant type填写 * @param {string} secret * @return {string} jwt token */ function getJWTToken(payload, secret) { payload.iat = Date.now()/1000 >> 0; payload.exp = (Date.now()+5*60*1000)/1000 >> 0; payload.state = 'juth2'+Date.now(); let token = ''; try { // https://kjur.github.io/jsrsasign/api/symbols/KJUR.jws.JWS.html#.sign // https://github.com/kjur/jsrsasign/wiki/Tutorial-for-JWT-generation token = KJUR.jws.JWS.sign( null, {'alg': 'HS256', 'typ': 'JWT'}, payload, secret, ); } catch (e) {} return token; } export default juth2;