@mamba-le/auth
Version:
366 lines (365 loc) • 11 kB
text/typescript
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'
}
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
*/
protected _AccessToken = undefined;
/**
* 最后一次更新 值 HydrateisStopped 未完成前 存储 用于对比变化 default 表示默认无值
* @protected
* @memberof AuthController
*/
protected lastValue = 'default';
/**
* 提供外部访问的 AccessToken
* @readonly
* @memberof AuthController
*/
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
*/
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
*/
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
*/
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;
}