spritekuang

导航

企业级工作流解决方案(十二)--集成Abp和ng-alain--用户身份认证与权限验证

多租户

  如果系统需要支持多租户,那么最好事先定义好多租户的存储部署方式,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']}
              }}, // 操作管理

 

posted on 2019-05-06 12:13  spritekuang  阅读(1056)  评论(0编辑  收藏  举报