多租户
如果系统需要支持多租户,那么最好事先定义好多租户的存储部署方式,Abp提供了几种方式,根据需要选择,每一个用户身份认证与权限验证都需要完全的隔离
这里设计的权限数据全部存储在缓存中,每个租户单独建立缓存Key,见权限系统服务章节介绍。
用户accesstoken
accesstoken的定义就不多的介绍了,Abp其实就是直接使用微软IdentityModel这套组件,并且zero项目还直接使用的微软的用户角色相关管理,这里面性能存在一定的问题,而且使用相对比较复杂,还不利于扩展,其实功能就那么一些,完全没有必要用这一套用户角色管理。
其实这里要做的核心扩展,就是自定义Claim信息,accesstoken信息其实是存在Claim集合里面的,我们可以自定义Claim信息,用户登录的时候,调用组件提供的接口,创建用户访问的accesstoken信息,我这里扩展了RoleIds集合信息和UserName信息,UserName在系统很多地方都可能用到,而RoleIds集合信息用于做权限控制。
实现代码
public class AuthTokenProvider: IAuthTokenProvider { private readonly TokenAuthConfiguration _configuration; public AuthTokenProvider(TokenAuthConfiguration configuration) { _configuration = configuration; } public AuthenticateResultModel Authenticate(LoginResultModel loginResultModel) { List<Claim> claims = new List<Claim>(); claims.Add(new Claim(AbpClaimTypes.UserId, loginResultModel.UserId.ToString())); if (loginResultModel.TenantId.HasValue) { claims.Add(new Claim(AbpClaimTypes.TenantId, loginResultModel.TenantId.ToString())); } claims.Add(new Claim(AbpClaimTypes.RoleIds, loginResultModel.RoleIds)); // 自定义RoleIds和UserName申明 claims.Add(new Claim(AbpClaimTypes.UserName, loginResultModel.UserName)); var accessToken = CreateAccessToken(claims); return new AuthenticateResultModel { AccessToken = accessToken, EncryptedAccessToken = GetEncrpyedAccessToken(accessToken), ExpireInSeconds = (int)_configuration.Expiration.TotalSeconds, UserId = loginResultModel.UserId }; } private string CreateAccessToken(IEnumerable<Claim> claims, TimeSpan? expiration = null) { var now = DateTime.UtcNow; var jwtSecurityToken = new JwtSecurityToken( issuer: _configuration.Issuer, audience: _configuration.Audience, claims: claims, notBefore: now, expires: now.Add(expiration ?? _configuration.Expiration), signingCredentials: _configuration.SigningCredentials ); return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); } private static List<Claim> CreateJwtClaims(List<Claim> claims) { var nameIdClaim = claims.First(c => c.Type == ClaimTypes.NameIdentifier); // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims. claims.AddRange(new[] { //new Claim(JwtRegisteredClaimNames.Sub, nameIdClaim.Value), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.Now.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) }); return claims; } private string GetEncrpyedAccessToken(string accessToken) { return SimpleStringCipher.Instance.Encrypt(accessToken, CoreModule.DefaultPassPhrase); } }
Ng-alain前端页面Token
ng-alain前端页面也有权限管理,主要是delon项目里面的权限管理,核心其实也是在Token的管理,ITokenService,定义了管理Token的接口,ITokenModel定义了token和一个自定义存储的数据字典,在路由拦截的时候,添加了对Token是否为空的验证,我们在登录的时候,对token进行赋值,退出登录清空token。
默认Token是存储在LocalStorage里面的,根据需要,可以改为SessionStorage。
用户登录
/** * 登录结果回调 * @param authenticateResult 登录结果 */ private processAuthenticateResult(authenticateResult: AuthenticateResultModel) { if (authenticateResult.accessToken) { // 设置用户Token信息 this.tokenService.set({ token: authenticateResult.accessToken, userId: authenticateResult.userId , expireInSeconds: authenticateResult.expireInSeconds,encryptedAccessToken:authenticateResult.encryptedAccessToken }); // 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响 this.startupSrv.load().then(() => { let url = this.tokenService.referrer.url || '/'; if (url.includes('/passport')) url = '/'; this.router.navigateByUrl(url); }); } else { this.error = "用户名或密码错误!"; return; } }
退出登录
logout() { this.tokenService.clear(); this.signalRService.CloseSignalr(); this.router.navigateByUrl(this.tokenService.login_url); }
ng-alain权限管理
export interface ITokenModel { [key: string]: any; token: string; } export interface AuthReferrer { url?: string; } export interface ITokenService { set(data: ITokenModel): boolean; /** * 获取Token,形式包括: * - `get()` 获取 Simple Token * - `get<JWTTokenModel>(JWTTokenModel)` 获取 JWT Token */ get(type?: any): ITokenModel; /** * 获取Token,形式包括: * - `get()` 获取 Simple Token * - `get<JWTTokenModel>(JWTTokenModel)` 获取 JWT Token */ get<T extends ITokenModel>(type?: any): T; clear(): void; change(): Observable<ITokenModel>; /** 获取登录地址 */ readonly login_url: string; /** 获取授权失败前路由信息 */ readonly referrer?: AuthReferrer; }
http拦截
由于Abp后端需要验证accesstoken,abp对错误信息和请求结果信息进行了封装,因此,需要替换ng-alain的http拦截器,自定义拦截器。
自定义拦截器主要是对每一个http请求,从ITokenService里面获取token,添加到请求Header里面;以及对答复内容,提取答复内容和显示错误信息。
添加请求Token
protected addAuthorizationHeaders(headers: HttpHeaders): HttpHeaders { let authorizationHeaders = headers ? headers.getAll('Authorization') : null; if (!authorizationHeaders) { authorizationHeaders = []; } if (!this.itemExists(authorizationHeaders, (item: string) => item.indexOf('Bearer ') == 0)) { let token = (this.injector.get(DA_SERVICE_TOKEN) as ITokenService).get(); // let token = this._tokenService.getToken(); if (headers && token) { headers = headers.set('Authorization', 'Bearer ' + token.token); } } return headers; }
答复内容参照abp提供的前端框架代码即可
Ng-alain前端权限验证
也是delon项目里面的acl功能模块,只需要在用户登录成功的时候,把用户能够访问的操作Code集合传递给acl,以及在页面使用的地方,定义权限控制的Code即可
// ACL:设置权限为全量 // this.aclService.setFull(true); this.aclService.setAbility(res.operate);
控件控制
<button (click)="add()" acl [acl-ability]="'adminuser-add'" nz-button nzType="primary">新建</button>
列表按钮控制
{ text: '删除', type: 'del', acl:{ ability:['adminuser-delete']}, click: (item: any) => { this.http.delete(`api/services/app/SEC_AdminUser/Delete?Id=${item.id}`).subscribe(() => this.st.reload()); } },
路由控制
{ path: 'operate', loadChildren: './sec-operate/sec-operate.module#SecOperateModule', canActivate: [ACLGuard], data: { guard: <ACLType>{ability: ['operate-mgt']} }}, // 操作管理