@holyhigh/juth2-client.js
Version:
A Juth2 client for javascript
354 lines (319 loc) • 9.54 kB
JavaScript
/* 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;