UNPKG

@mamba-le/auth

Version:
366 lines (365 loc) 11 kB
import dayjs from 'dayjs'; import jsCookie from 'js-cookie'; import jwtDecode from 'jwt-decode'; import lodash from 'lodash'; import { BindAll } from 'lodash-decorators'; import { action, computed, observable } from 'mobx'; import { persist } from 'mobx-persist'; import { lastValueFrom, Subject } from 'rxjs'; import { AuthOptions } from './options'; export interface IAuthOptions { /** * 持久化 Key * @type {Array<string>} * @memberof IAuthOptions */ StorageKey?: string /** * Cookie Key * @type {Array<string>} * @memberof IAuthOptions */ CookieKey?: Array<string> /** * Cookie 域名 * @type {Array<string>} * @memberof IAuthOptions */ CookieDomain?: Array<string> /** * Token 类型 * @type {('JWT' | 'Other')} * @memberof IPortalAuthOptions */ TokenType?: 'JWT' | 'Other' } @BindAll() export class AuthController { constructor(options: IAuthOptions = {}) { this.resetConfig(options) this.createHydrate() } /** * 持久化初始化完成 Subject * @type {Promise<any>} * @memberof ControllerUser */ protected readonly HydrateSubject = new Subject<Boolean>(); /** * 持久化初始化完成 Promise * @type {Promise<any>} * @memberof ControllerUser */ get HydrateAsync(): Promise<this> { return lastValueFrom(this.HydrateSubject, { defaultValue: undefined }); } /** * 异步 HydrateSubject 已经完成 * @readonly * @memberof PortalAuthController */ get HydrateisStopped() { return this.HydrateSubject.isStopped } readonly options: IAuthOptions = { StorageKey: 'mamba-auth', CookieKey: ['mamba-auth'], CookieDomain: ['.lenovo.com.cn', '.lenovo.com'], TokenType: 'JWT' } get JsCookie() { return jsCookie } get JWTDecode() { return jwtDecode } get IsJWT() { return lodash.eq(this.options.TokenType, 'JWT') } /** * 存储 key * @readonly * @memberof AuthController */ get StorageKey() { return `_Auth_${this.options.StorageKey}` } /** * CookieKey * @readonly * @memberof AuthController */ get CookieKey() { if (AuthOptions.browser) { return lodash.concat([], this.options.CookieKey) } return [] } /** * Cookie 站点信息 * @readonly * @memberof AuthController */ get CookieDomain() { if (AuthOptions.browser) { return lodash.concat([`.${window.location.hostname}`], this.options.CookieDomain) } return [] } /** * 所有的 Cookie * @readonly * @memberof AuthController */ get AllCookieKeys() { if (AuthOptions.browser) { try { const aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/); for (let nIdx = 0; nIdx < aKeys.length; nIdx++) { aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]); } return aKeys; } catch (error) { return [] } } } /** * 重置配置 * @param options */ resetConfig(options: IAuthOptions = {}) { try { AuthOptions.writeCheck() lodash.merge(this.options, lodash.pick(options, ['StorageKey', 'CookieKey', 'CookieDomain'])) } catch (error) { AuthOptions.log('error', error) } } /** * AccessToken * @protected * @memberof AuthController */ @observable protected _AccessToken = undefined; /** * 最后一次更新 值 HydrateisStopped 未完成前 存储 用于对比变化 default 表示默认无值 * @protected * @memberof AuthController */ protected lastValue = 'default'; /** * 提供外部访问的 AccessToken * @readonly * @memberof AuthController */ @computed get AccessToken() { const AccessToken = this.getAccessToken() const Decoded = this.getDecoded(AccessToken); if (Decoded && Decoded.overdue) { AuthOptions.log('Warning 已过期 不返回', Decoded, this) return undefined } return AccessToken } /** * 解析后的 JWT * @readonly * @type {JWTDecoded} * @memberof AuthController */ @computed get JwtDecoded() { return this.getDecoded() } /** * Cookie 中存储的值 * @readonly * @private * @memberof AuthController */ private get CookieAccessToken() { // 微信小程序中不使用 cookie if (AuthOptions.isWeappBowser) { return undefined } if (AuthOptions.browser) { try { return lodash.head(lodash.compact(lodash.map(this.CookieKey, key => jsCookie.get(key)))) } catch (error) { AuthOptions.log('error', this, error) return undefined } } } /** * 获取 JWT 解析后数据 * @returns */ getDecoded(AccessToken = this.getAccessToken()): JWTDecoded { try { if (!AccessToken || !this.IsJWT) { return { } as JWTDecoded } const Decode: JWTDecoded = jwtDecode(AccessToken); return lodash.assign<JWTDecoded, Partial<JWTDecoded>>(Decode, { expFormat: dayjs(Decode.exp * 1000).format('YYYY-MM-DD HH:mm:ss'), iatFormat: dayjs(Decode.iat * 1000).format('YYYY-MM-DD HH:mm:ss'), // 是否过期 overdue: dayjs(Decode.exp * 1000).isBefore(dayjs()) }) } catch (error) { // AuthOptions.log('error', 'Decoded', this, error) return { overdue: true } as JWTDecoded } } /** * 获取 AccessToken * @returns */ getAccessToken() { return lodash.head(lodash.compact([this.CookieAccessToken, this._AccessToken])) } /** * 保存 Token * @param _AccessToken * @returns */ @action onSaveAccessToken(_AccessToken: string = undefined, setCookie = false) { try { AuthOptions.writeCheck(); AuthOptions.trace('Save AccessToken', _AccessToken, setCookie) if (_AccessToken && this.IsJWT) { const Decoded = this.getDecoded(_AccessToken); AuthOptions.log('AccessToken Decoded', Decoded) // if (!Decoded.aud) { // throw 'AccessToken 非法' // } if (Decoded.overdue) { throw 'AccessToken 已过期' } } if (_AccessToken && lodash.eq(this.getAccessToken(), _AccessToken)) { throw 'AccessToken 已存在 【 Cookie 存在将不在写入 】' } if (!this.HydrateisStopped) { this.lastValue = _AccessToken; } this._AccessToken = _AccessToken; if (setCookie && AuthOptions.browser) { lodash.map(this.CookieDomain, domain => { lodash.map(this.CookieKey, key => { AuthOptions.log('jsCookie set', key, _AccessToken, domain) jsCookie.set(key, _AccessToken) jsCookie.set(key, _AccessToken, { domain }) }) }) } } catch (error) { AuthOptions.log('AccessToken Error', error, this) } } /** * 清理所有的登录信息 * @return {*} * @memberof AuthController */ @action onClear() { try { AuthOptions.writeCheck() this._AccessToken = undefined; if (!this.HydrateisStopped) { this.lastValue = undefined; } this.onRemove() lodash.map(this.CookieDomain, domain => this.onRemove({ domain })) // return AuthOptions.LocalForage.clear(); } catch (error) { AuthOptions.log('error', error) } } private onRemove(options?) { if (AuthOptions.browser) { try { AuthOptions.writeCheck() lodash.map(this.CookieKey, key => jsCookie.remove(key, options)) lodash.map(this.AllCookieKeys, key => jsCookie.remove(key, options)) } catch (error) { AuthOptions.log('error', error) } } } /** * 创建持久化存储 * @memberof BaseModel */ protected async createHydrate() { try { if (!AuthOptions.browser) { throw '非浏览器环境 不进行 Hydrate' } // 微信小程序中不使用 cookie if (AuthOptions.isWeappBowser) { this.onRemove() lodash.map(this.CookieDomain, domain => this.onRemove({ domain })) } const Hydrate = AuthOptions.createHydrate() persist({ _AccessToken: true })(this); AuthOptions.log(`Storage ${this.StorageKey}`, this) await Hydrate(this.StorageKey, this); if (!lodash.eq(this.lastValue, 'default') && !lodash.isEqual(this._AccessToken, this.lastValue)) { AuthOptions.log(`Storage ${this.StorageKey} LastValue`, this, this.lastValue) this.onSaveAccessToken(this.lastValue); } // jwt 校验正确性 if (this.IsJWT) { const Decoded = this.getDecoded(); // 过期清理 或者无效 if (Decoded.overdue) { this.onClear() } } // const CookieAccessToken = lodash.find(this.CookieAccessToken, lodash.identity); // if (!this._AccessToken && CookieAccessToken) { // this.onSaveAccessToken(CookieAccessToken) // } this.HydrateSubject.next(true) this.HydrateSubject.complete() } catch (error) { if (AuthOptions.browser) { AuthOptions.log('error', error) } this.HydrateSubject.next(false) this.HydrateSubject.complete() } } } /** * @docs https://www.jianshu.com/p/d1644e281250 */ export interface JWTDecoded { /** @desc 用户 */ aud: string; /** @desc 主题 */ sub: string; /** @desc 身份 */ identityId: string; /** @desc 发行人 */ iss: string; /** @desc 过期时间 */ exp: number; expFormat: string; /** @desc 颁发时间 */ iat: number; iatFormat: string; /** @desc 用户名 */ username: string; /** @desc 过期 */ overdue: boolean; }