@delon/auth
Version:
Simple user authentication, support Json Web Token & Simple Token.
680 lines (657 loc) • 22 kB
JavaScript
import { DOCUMENT } from '@angular/common';
import * as i0 from '@angular/core';
import { InjectionToken, inject, Injectable, makeEnvironmentProviders } from '@angular/core';
import { Router } from '@angular/router';
import { Subject, BehaviorSubject, share, interval, map, filter, Observable } from 'rxjs';
import { AlainConfigService } from '@delon/util/config';
import { CookieService } from '@delon/util/browser';
import { HttpContextToken, HttpErrorResponse } from '@angular/common/http';
const AUTH_DEFAULT_CONFIG = {
store_key: `_token`,
token_invalid_redirect: true,
token_exp_offset: 10,
token_send_key: `token`,
token_send_template: '${token}',
token_send_place: 'header',
login_url: '/login',
refreshTime: 3000,
refreshOffset: 6000,
ignores: [/\/assets\//]
};
function mergeConfig(srv) {
return srv.merge('auth', AUTH_DEFAULT_CONFIG);
}
function DA_STORE_TOKEN_LOCAL_FACTORY() {
return new LocalStorageStore();
}
/**
* `localStorage` storage, **not lost after closing the browser**.
*
* ```ts
provideHttpClient(withInterceptors([...(environment.interceptorFns ?? []), authJWTInterceptor, defaultInterceptor])),
provideAuth(withLocalStorage()),
* ```
*/
class LocalStorageStore {
get(key) {
return JSON.parse(localStorage.getItem(key) || '{}') || {};
}
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
return true;
}
remove(key) {
localStorage.removeItem(key);
}
}
const DA_STORE_TOKEN = new InjectionToken('AUTH_STORE_TOKEN', {
providedIn: 'root',
factory: DA_STORE_TOKEN_LOCAL_FACTORY
});
function DA_SERVICE_TOKEN_FACTORY() {
return new TokenService();
}
/**
* 维护Token信息服务,[在线文档](https://ng-alain.com/auth)
*/
class TokenService {
store = inject(DA_STORE_TOKEN);
cogSrv = inject(AlainConfigService);
refresh$ = new Subject();
change$ = new BehaviorSubject(null);
interval$;
_referrer = {};
_options;
constructor() {
this._options = mergeConfig(this.cogSrv);
}
get refresh() {
this.builderRefresh();
return this.refresh$.pipe(share());
}
get login_url() {
return this._options.login_url;
}
get referrer() {
return this._referrer;
}
get options() {
return this._options;
}
set(data) {
const res = this.store.set(this._options.store_key, data);
this.change$.next(data);
return res;
}
get(type) {
const data = this.store.get(this._options.store_key);
return type ? Object.assign(new type(), data) : data;
}
clear(options = { onlyToken: false }) {
let data = null;
if (options.onlyToken === true) {
data = this.get();
data.token = ``;
this.set(data);
}
else {
this.store.remove(this._options.store_key);
}
this.change$.next(data);
}
change() {
return this.change$.pipe(share());
}
builderRefresh() {
const { refreshTime, refreshOffset } = this._options;
this.cleanRefresh();
this.interval$ = interval(refreshTime)
.pipe(map(() => {
const item = this.get();
const expired = item.expired || item.exp || 0;
if (expired <= 0) {
return null;
}
const curTime = new Date().valueOf() + refreshOffset;
return expired <= curTime ? item : null;
}), filter(v => v != null))
.subscribe(res => this.refresh$.next(res));
}
cleanRefresh() {
if (this.interval$ && !this.interval$.closed) {
this.interval$.unsubscribe();
}
}
ngOnDestroy() {
this.cleanRefresh();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: TokenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: TokenService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: TokenService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
const DA_SERVICE_TOKEN = new InjectionToken('DA_SERVICE_TOKEN', {
providedIn: 'root',
factory: DA_SERVICE_TOKEN_FACTORY
});
const OPENTYPE = '_delonAuthSocialType';
const HREFCALLBACK = '_delonAuthSocialCallbackByHref';
class SocialService {
tokenService = inject(DA_SERVICE_TOKEN);
doc = inject(DOCUMENT);
router = inject(Router);
_win = null;
_winTime;
observer;
/**
* 跳转至登录页,若为 `type=window` 时,返回值是 `Observable<ITokenModel>`
*
* @param url 获取授权地址
* @param callback 当 `type=href` 成功时的回调路由地址
* @param options.type 打开方式,默认 `window`
* @param options.windowFeatures 等同 `window.open` 的 `features` 参数值
*/
login(url, callback = '/', options = {}) {
options = {
type: 'window',
windowFeatures: 'location=yes,height=570,width=520,scrollbars=yes,status=yes',
...options
};
localStorage.setItem(OPENTYPE, options.type);
localStorage.setItem(HREFCALLBACK, callback);
if (options.type === 'href') {
this.doc.location.href = url;
return;
}
this._win = window.open(url, '_blank', options.windowFeatures);
this._winTime = setInterval(() => {
if (this._win && this._win.closed) {
this.ngOnDestroy();
let model = this.tokenService.get();
if (model && !model.token)
model = null;
// 触发变更通知
if (model) {
this.tokenService.set(model);
}
this.observer.next(model);
this.observer.complete();
}
}, 100);
return new Observable((observer) => {
this.observer = observer;
});
}
/**
* 授权成功后的回调处理
*
* @param rawData 指定回调认证信息,为空时从根据当前URL解析
*/
callback(rawData) {
// from uri
if (!rawData && this.router.url.indexOf('?') === -1) {
throw new Error(`url muse contain a ?`);
}
// parse
let data = { token: `` };
if (typeof rawData === 'string') {
const rightUrl = rawData.split('?')[1].split('#')[0];
data = this.router.parseUrl(`./?${rightUrl}`).queryParams;
}
else {
data = rawData;
}
if (!data || !data.token)
throw new Error(`invalide token data`);
this.tokenService.set(data);
const url = localStorage.getItem(HREFCALLBACK) || '/';
localStorage.removeItem(HREFCALLBACK);
const type = localStorage.getItem(OPENTYPE);
localStorage.removeItem(OPENTYPE);
if (type === 'window') {
window.close();
}
else {
this.router.navigateByUrl(url);
}
return data;
}
ngOnDestroy() {
clearInterval(this._winTime);
this._winTime = null;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: SocialService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: SocialService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: SocialService, decorators: [{
type: Injectable
}] });
/**
* 内存存储,关掉浏览器标签后**丢失**。
*/
class MemoryStore {
cache = {};
get(key) {
return this.cache[key] || {};
}
set(key, value) {
this.cache[key] = value;
return true;
}
remove(key) {
this.cache[key] = null;
}
}
/**
* `sessionStorage` storage, **lost after closing the browser**.
*
* ```ts
provideHttpClient(withInterceptors([...(environment.interceptorFns ?? []), authJWTInterceptor, defaultInterceptor])),
provideAuth(withSessionStorage()),
* ```
*/
class SessionStorageStore {
get(key) {
return JSON.parse(sessionStorage.getItem(key) || '{}') || {};
}
set(key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
return true;
}
remove(key) {
sessionStorage.removeItem(key);
}
}
/**
* `cookie` storage
*
* ```ts
provideHttpClient(withInterceptors([...(environment.interceptorFns ?? []), authJWTInterceptor, defaultInterceptor])),
provideAuth(withCookie()),
* ```
*/
class CookieStorageStore {
srv = inject(CookieService);
get(key) {
try {
return JSON.parse(this.srv.get(key) || '{}');
}
catch (ex) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
console.error(`CookieStorageStore: Invalid key-value format ${key}`, ex);
}
return {};
}
}
set(key, value) {
const expires = (value?.expired ?? 0) / 1e3;
this.srv.put(key, JSON.stringify(value ?? {}), { expires });
return true;
}
remove(key) {
this.srv.remove(key);
}
}
function CheckSimple(model) {
return model != null && typeof model.token === 'string' && model.token.length > 0;
}
function CheckJwt(model, offset) {
try {
return model != null && !!model.token && !model.isExpired(offset);
}
catch (err) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
console.warn(`${err.message}, jump to login_url`);
}
return false;
}
}
function ToLogin(options, url) {
const router = inject(Router);
const token = inject(DA_SERVICE_TOKEN);
const doc = inject(DOCUMENT);
token.referrer.url = url || router.url;
if (options.token_invalid_redirect === true) {
setTimeout(() => {
if (/^https?:\/\//g.test(options.login_url)) {
doc.location.href = options.login_url;
}
else {
router.navigate([options.login_url]);
}
});
}
}
function urlBase64Decode(str) {
let output = str.replace(/-/g, '+').replace(/_/g, '/');
switch (output.length % 4) {
case 0: {
break;
}
case 2: {
output += '==';
break;
}
case 3: {
output += '=';
break;
}
default: {
throw new Error(`'atob' failed: The string to be decoded is not correctly encoded.`);
}
}
return b64DecodeUnicode(output);
}
function b64decode(str) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let output = '';
str = String(str).replace(/=+$/, '');
for (
// initialize result and counters
let bc = 0, bs, buffer, idx = 0;
// get next character
(buffer = str.charAt(idx++));
// character found in table? initialize bit storage and add its ascii value;
~buffer &&
((bs = bc % 4 ? bs * 64 + buffer : buffer),
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4)
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
: 0) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf(buffer);
}
return output;
}
// https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
function b64DecodeUnicode(str) {
return decodeURIComponent(Array.prototype.map
.call(b64decode(str), (c) => {
return `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`;
})
.join(''));
}
class JWTTokenModel {
token;
expired;
/**
* 获取载荷信息
*/
get payload() {
const parts = (this.token || '').split('.');
if (parts.length !== 3)
throw new Error('JWT must have 3 parts');
const decoded = urlBase64Decode(parts[1]);
return JSON.parse(decoded);
}
/**
* 获取过期时间戳(单位:ms)
*/
get exp() {
const decoded = this.payload;
if (!Object.prototype.hasOwnProperty.call(decoded, 'exp'))
return null;
const date = new Date(0);
date.setUTCSeconds(decoded.exp);
return date.valueOf();
}
/**
* 检查Token是否过期,当`payload` 包含 `exp` 字段时有效,若无 `exp` 字段直接返回 `null`
*
* @param offsetSeconds 偏移量
*/
isExpired(offsetSeconds = 0) {
const exp = this.exp;
if (exp == null)
return null;
return !(exp > new Date().valueOf() + offsetSeconds * 1000);
}
}
class AuthJWTGuardService {
srv = inject(DA_SERVICE_TOKEN);
process(url) {
const cog = this.srv.options;
const res = CheckJwt(this.srv.get(JWTTokenModel), cog.token_exp_offset);
if (!res) {
ToLogin(cog, url);
}
return res;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AuthJWTGuardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AuthJWTGuardService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AuthJWTGuardService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* JWT 路由守卫, [ACL Document](https://ng-alain.com/auth/guard).
*
* ```ts
* data: {
* path: 'home',
* canActivate: [ authJWTCanActivate ],
* data: { guard: 'user1' }
* }
* ```
*/
const authJWTCanActivate = (_, state) => inject(AuthJWTGuardService).process(state.url);
/**
* JWT 路由守卫, [ACL Document](https://ng-alain.com/auth/guard).
*
* ```ts
* data: {
* path: 'home',
* canActivateChild: [ authJWTCanActivateChild ],
* data: { guard: 'user1' }
* }
* ```
*/
const authJWTCanActivateChild = (_, state) => inject(AuthJWTGuardService).process(state.url);
/**
* JWT 路由守卫, [ACL Document](https://ng-alain.com/auth/guard).
*
* ```ts
* data: {
* path: 'home',
* canMatch: [ authJWTCanMatch ],
* data: { guard: 'user1' }
* }
* ```
*/
const authJWTCanMatch = route => inject(AuthJWTGuardService).process(route.path);
/**
* Whether to allow anonymous login
*
* 是否允许匿名登录
*
* @example
* this.http.post(`login`, {
* name: 'cipchk', pwd: '123456'
* }, {
* context: new HttpContext().set(ALLOW_ANONYMOUS, true)
* })
*/
const ALLOW_ANONYMOUS = new HttpContextToken(() => false);
function isAnonymous(req, options) {
if (req.context.get(ALLOW_ANONYMOUS))
return true;
if (Array.isArray(options.ignores)) {
for (const item of options.ignores) {
if (item.test(req.url))
return true;
}
}
return false;
}
function throwErr(req, options) {
ToLogin(options);
// Interrupt Http request, so need to generate a new Observable
return new Observable((observer) => {
let statusText = '';
if (typeof ngDevMode === 'undefined' || ngDevMode) {
statusText = `来自 /auth 的拦截,所请求URL未授权,若是登录API可加入 new HttpContext().set(ALLOW_ANONYMOUS, true) 来表示忽略校验,更多方法请参考: https://ng-alain.com/auth/getting-started#AlainAuthConfig\nThe interception from @delon/auth, the requested URL is not authorized. If the login API can add new HttpContext().set(ALLOW_ANONYMOUS, true) to ignore the check, please refer to: https://ng-alain.com/auth/getting-started#AlainAuthConfig`;
}
const res = new HttpErrorResponse({
url: req.url,
headers: req.headers,
status: 401,
statusText
});
observer.error(res);
});
}
function newReq$1(req, model) {
return req.clone({
setHeaders: {
Authorization: `Bearer ${model.token}`
}
});
}
const authJWTInterceptor = (req, next) => {
const options = mergeConfig(inject(AlainConfigService));
if (isAnonymous(req, options))
return next(req);
const model = inject(DA_SERVICE_TOKEN).get(JWTTokenModel);
if (CheckJwt(model, options.token_exp_offset))
return next(newReq$1(req, model));
return throwErr(req, options);
};
class AuthSimpleGuardService {
srv = inject(DA_SERVICE_TOKEN);
process(url) {
const res = CheckSimple(this.srv.get());
if (!res) {
ToLogin(this.srv.options, url);
}
return res;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AuthSimpleGuardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AuthSimpleGuardService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: AuthSimpleGuardService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* Simple 路由守卫, [ACL Document](https://ng-alain.com/auth/guard).
*
* ```ts
* data: {
* path: 'home',
* canActivate: [ authSimpleCanActivate ],
* data: { guard: 'user1' }
* }
* ```
*/
const authSimpleCanActivate = (_, state) => inject(AuthSimpleGuardService).process(state.url);
/**
* Simple 路由守卫, [ACL Document](https://ng-alain.com/auth/guard).
*
* ```ts
* data: {
* path: 'home',
* canActivateChild: [ authSimpleCanActivateChild ],
* data: { guard: 'user1' }
* }
* ```
*/
const authSimpleCanActivateChild = (_, state) => inject(AuthSimpleGuardService).process(state.url);
/**
* Simple 路由守卫, [ACL Document](https://ng-alain.com/auth/guard).
*
* ```ts
* data: {
* path: 'home',
* canMatch: [ authSimpleCanMatch ],
* data: { guard: 'user1' }
* }
* ```
*/
const authSimpleCanMatch = route => inject(AuthSimpleGuardService).process(route.path);
function newReq(req, model, options) {
const { token_send_template, token_send_key } = options;
const token = token_send_template.replace(/\$\{([\w]+)\}/g, (_, g) => model[g]);
switch (options.token_send_place) {
case 'header':
// eslint-disable-next-line no-case-declarations
const obj = {};
obj[token_send_key] = token;
req = req.clone({
setHeaders: obj
});
break;
case 'body': {
const body = req.body || {};
body[token_send_key] = token;
req = req.clone({
body
});
break;
}
case 'url':
req = req.clone({
params: req.params.append(token_send_key, token)
});
break;
}
return req;
}
const authSimpleInterceptor = (req, next) => {
const options = mergeConfig(inject(AlainConfigService));
if (isAnonymous(req, options))
return next(req);
const model = inject(DA_SERVICE_TOKEN).get();
if (CheckSimple(model))
return next(newReq(req, model, options));
return throwErr(req, options);
};
class SimpleTokenModel {
token;
expired;
}
var AuthFeatureKind;
(function (AuthFeatureKind) {
AuthFeatureKind[AuthFeatureKind["Store"] = 0] = "Store";
})(AuthFeatureKind || (AuthFeatureKind = {}));
function makeAuthFeature(kind, providers) {
return {
ɵkind: kind,
ɵproviders: providers
};
}
/**
* Configures authentication process service to be available for injection.
*
* @see {@link withCookie}
* @see {@link withLocalStorage}
* @see {@link withSessionStorage}
*/
function provideAuth(store) {
return makeEnvironmentProviders([(store ?? withLocalStorage()).ɵproviders]);
}
/** `cookie` storage */
function withCookie() {
return makeAuthFeature(AuthFeatureKind.Store, [
{ provide: DA_STORE_TOKEN, useClass: CookieStorageStore, deps: [CookieService] }
]);
}
/** `localStorage` storage, **not lost after closing the browser**. */
function withLocalStorage() {
return makeAuthFeature(AuthFeatureKind.Store, [{ provide: DA_STORE_TOKEN, useClass: LocalStorageStore }]);
}
/** `sessionStorage` storage, **lost after closing the browser**. */
function withSessionStorage() {
return makeAuthFeature(AuthFeatureKind.Store, [{ provide: DA_STORE_TOKEN, useClass: SessionStorageStore }]);
}
/** Memory storage, **lost after closing the browser tab**. */
function withMemoryStorage() {
return makeAuthFeature(AuthFeatureKind.Store, [{ provide: DA_STORE_TOKEN, useClass: MemoryStore }]);
}
/**
* Generated bundle index. Do not edit.
*/
export { ALLOW_ANONYMOUS, AUTH_DEFAULT_CONFIG, AuthFeatureKind, AuthJWTGuardService, AuthSimpleGuardService, CookieStorageStore, DA_SERVICE_TOKEN, DA_SERVICE_TOKEN_FACTORY, DA_STORE_TOKEN, DA_STORE_TOKEN_LOCAL_FACTORY, JWTTokenModel, LocalStorageStore, MemoryStore, SessionStorageStore, SimpleTokenModel, SocialService, TokenService, authJWTCanActivate, authJWTCanActivateChild, authJWTCanMatch, authJWTInterceptor, authSimpleCanActivate, authSimpleCanActivateChild, authSimpleCanMatch, authSimpleInterceptor, isAnonymous, mergeConfig, provideAuth, throwErr, urlBase64Decode, withCookie, withLocalStorage, withMemoryStorage, withSessionStorage };
//# sourceMappingURL=auth.mjs.map