Bootstrap

Malagu 框架的认证与授权【借鉴 Spring Security 和 aws iam 的设计】

前言

Malagu 是基于 TypeScript 的 Serverless First、可扩展和组件化的应用框架。

简介

Malagu Security 组件主要提供认证和授权相关功能。Security 借鉴了 Spring Security 和 aws 的 iam 的设计。

使用方法

Malagu 组件就是一个普通的 npm 包,可以通过以下命令安装:

yarn add @malagu/security

安装完后,就可以直接运行了。

默认配置

malagu:
  security:
    enabled: true
    usernameKey: username
    passwordKey: password
backend:
  malagu:
    security:
      contextKey: malagu:securityContext
      username: admin
      password: MzQ0NTg4ZTk2NzQyYWI1ODA1MDFlNDBjMzZhZDY4OWQ1Zjc5ZDYxYzc2MjQ1NWZk # raw password 123456
      passwordEncoder:
        secret: 123456
        encodeHashAsBase64: true
      basic:
        realm: realm
      loginPage: /login
      loginUrl: /login
      loginMethod: POST
      loginSuccessUrl: /
      logoutUrl: /logout
      logoutMethod: POST
      logoutSuccessUrl: /login

实现登录页面

在登录页面中,通过 POST 请求提交用户名(username)和密码(password)到 /login ,将会触发框架的认证流程,默认提供的用户名为:admin,密码为:123456,当然,您也可以实现 UserStore 接口提供您自己用户信息。当用户名和密码都匹配成功后,则认证成功,跳转到登录成功页面,默认是 / ,否则认证失败,跳转到登录页面 /login 。

自定义 UserStore

您可以通过查询数据库获取用户信息,以下为框架默认实现,返回固定的用户信息:

@Component(UserStore)
export class UserStoreImpl implements UserStore {

    @Value('malagu.security')
    protected readonly options: any;

    async load(username: string): Promise {
        if (this.options.username === username) {
            return {
                username,
                password: this.options.password,
                accountNonExpired: true,
                accountNonLocked: true,
                credentialsNonExpired: true,
                enabled: true,
                policies: [ {
                    type: PolicyType.El,
                    authorizeType: AuthorizeType.Pre,
                    el: 'true'
                } ]
            };
        }

        throw new UsernameNotFoundError(`Could not find: ${username}`);
    }
}

方法保护

默认对外的方法都会保护起来,当您没有登录通过 ajax 直接方法,将返回 401 状态码;当您没有登录通过浏览器访问页面,将返回 302 状态码,重定向到登录页面。

登录成功后,当您有权限访问该方法,则访问成功,当您没有权限访问该方法,则访问失败,返回 403 状态码。

匿名访问

方法上添加装饰器 @Anonymous ,可以让方法可以匿名访问。

@Get()
@Anonymous()
@Transactional({ readOnly: true })
list(): Promise {
  const repo = OrmContext.getRepository(User);
	return repo.find();
}

也可以添加到类上,让类的所有方法可以里面访问。

@Controller('users')
@Anonymous()
export class UserController {
  ...
}

授权

任何方法或者页面都需要显示授权才能访问,单单认证通过是不够的,可以授权给用户访问某些方法的权限,也可以授权给方法,允许哪类用户可以访问。框架会更加这些权限信息做权限验证,验证通过了才可以访问具体的方法和页面,后面会做详细的展开。

认证机制

说明:

  • 安全上下文中间件负责尝试从 Session 还原安全上下文内容

  • 所有的请求都会到达认证管理器,认证管理器会尝试匹配认证提供,如果该 请求没有匹配到认证提供者,则忽略,继续往下执行后面的中间件

  • 认证管理器可能匹配到多个认证提供者,只要其中存在一个是认证通过的,则表示认证通过

  • 框架默认的认证提供者会尝试从请求参数和 body - 中获取用户名和密码,通过用户名从用户存储器中加载用户信息,用户不存在,则抛出认证异常,认证失败,如果存在,则会继续校验密码是否正确,另外还有一些其他用户状态的校验,都通过了,则认证成功

  • 认证成功后,返回 Token,Token 中往往包含了用户基本信息,Token 会设置到安全上下文中,安全上下文中的内容会持久化到 Session 中

  • 您可以通过实现自己认证提供者,满足自己业务特殊的认证需求

  • 您可以通过属性配置自定义自己的登录页面地址、登录成功地址等等

  • 密码加密采用随机盐 + 秘钥的 Pbkdf2 的哈希算法,框架提供了一个默认秘钥,真实场景记得一定要改成您自己的秘钥。当然,您也可以自定义哈希算法

认证提供者

认证管理器会将真正的认证任务委派给认证提供者,您也可以自定义认证提供者,只需要实现接口 AuthenticationProvider,并以 AuthenticationProvider 接口为 id 注入到 IoC 容器即可。

export interface AuthenticationProvider {
    readonly priority: number;
    authenticate(): Promise;
    support(): Promise;
}

support 方法往往是匹配当前请求的路由是否为我们指定的即可。示例如下:

async support(): Promise {
	  return !!await this.requestMatcher.match(this.options.loginUrl, this.options.loginMethod);
}

授权机制

说明:

  • 授权是通过 AOP 机制实现

  • 安全元信息包含资源、操作、授信主体、授权类型和策略

  • 通过安全元信息可以分别获得资源权限策略和授信主体权限策略,然后更加策略类型匹配对应的策略解释器来解析策略

  • 一般情况下,一种类型的策略对应一个策略解释器,可以根据业务需要定义自己的策略语法规则和策略解释器

  • 策略可以所属授信主体,也是所属资源,策略可以在代码写死,也可以配置文件配置,还可以存储在数据库当中

  • 授信主体一般是系统用户,也可以是其他逻辑上的授信主体

  • 授权方式包括前置授权和后置授权

安全元信息

访问决策管理器基于安全元信息和策略来进行权限判断。

export interface SecurityMetadata {
    authorizeType: AuthorizeType;
    action: string;
    resource: string;
    principal: any;
    policies: Policy[];
}

安全元信息下文

安全元信息源会基于安全元信息上下文获得安全元信息。

export interface SecurityMetadataContext {
}

export interface MethodSecurityMetadataContext extends SecurityMetadataContext {
    authorizeType: AuthorizeType
    method: string;
    args: any[];
    target: any;
    returnValue?: any

}

安全元信息源

基于安全元信息上下文获得安全元信息。

export interface SecurityMetadataSource {
    load(context: SecurityMetadataContext): Promise;
}

访问决策管理器

基于安全元信息进行访问决策。

export interface AccessDecisionManager {
    decide(securityMetadata: SecurityMetadata): Promise;
}

访问决策投票器

访问决策管理器会把真正的决策任务委派给访问决策投票器。您也可以自定义认证提供者,只需要实现接口 AccessDecisionVoter,并以 AccessDecisionVoter 接口为 id 注入到 IoC 容器即可。

export interface AccessDecisionVoter {
    vote(securityMetadata: SecurityMetadata): Promise;
    support(securityMetadata: SecurityMetadata): Promise;
    readonly priority: number;
}

权限策略

策略(Policy)是对权限的描述,策略语法可以是任意形式,只要有对应的策略解释器来解释就好。策略可以分配给授信主体,也可以分配给资源。当然,也可以根据业务需要将策略分配给角色,角色可以再分配给某个用户,这样用户就拥有了角色的策略,换而言之,用户拥有了角色的权限。

云平台的策略

企业可以在云平台上创建很多资源,企业如何安全的管理这些资源,这就是云平台的策略需要考虑的问题,云平台的策略是这样来描述权限:谁在什么条件下能对哪些资源的哪些操作进行处理。

在 Malagu 框架中可以很方便地实现这样的效果,您只需要定义一个策略语法来表达:谁在什么条件下能对哪些资源的哪些操作进行处理。然后在实现一个对应的策略解释器即可。

如果您不需要定义像云平台那样复杂的策略,您完全可以更加您自己的业务需要定义您自己的策略语法。

默认策略

Malagu 框架提供给了一个默认策略:EL 表达式策略,当 EL 表达式计算结果为 true,表示允许,否则表示拒绝。用户可以在方法上通过相关的权限装饰器定义策略,也可以通过组件属性定义策略。

EL 表达式策略

Malagu 框架提供给了一个默认策略:EL 表达式策略,当 EL 表达式计算结果为 true,表示允许,否则表示拒绝。用户可以在方法上通过相关的权限装饰器配置策略,也可以通过组件属性配置策略。

定义

export interface Policy {
    type: PolicyType
    authorizeType: AuthorizeType;
}

export interface ElPolicy extends Policy {
    context: any;
    el: string;
}

权限装饰器

为了用户方便地配置 EL 表达式策略,框架提供了一些列权限装饰器 @Authorize 、 @PreAuthorize 、 @PostAuthorize 、 @Anonymous 和 @Authenticated 。

其中,@PreAuthorize 、 @PostAuthorize、 @Anonymous 和 @Authenticated是对 @Authorize 的扩展。

@Authorize

装饰器的配置属性:

export interface AuthorizeOption {
    el: string;
    authorizeType: AuthorizeType; // 授权类型包括前置授权和后置授权
}

@PreAuthorize

扩展 @Authorize 装饰器,等价于将 @Authorize 的 authorizeType 属性固定为前置授权。具体实现如下:

export const PreAuthorize = function (el: string) {
    return Authorize({ el, authorizeType: AuthorizeType.Pre });
};

@PostAuthorize

扩展 @Authorize 装饰器,等价于将 @Authorize 的 authorizeType 属性固定为后置授权。具体实现如下:

export const PostAuthorize = function (el: string) {
    return Authorize({ el, authorizeType: AuthorizeType.Post });
};

@Anonymous

扩展 @Authorize 装饰器,等价于将 @Authorize 的 el 属性固定为 true 和 authorizeType 属性固定为前置授权 。具体实现如下:

export const Anonymous = function (): any {
    return Authorize({ el: 'true', authorizeType: AuthorizeType.Pre });
};

@Authenticated

扩展 @Authorize 装饰器,等价于将 @Authorize 的 el 属性固定为 authenticated 和 authorizeType 属性固定为前置授权 。具体实现如下:

export const Authenticated = function () {
    return Authorize({ el: 'authenticated', authorizeType: AuthorizeType.Pre });
};

其中,authenticated 是内置的 EL 上下文变量,当为 true 时,表示当前登录用户认证通过;当为 false 时,表认证失败。

EL 表达式上下文

我们往往需要一些变量或者方法参与到 EL 表达式的计算中,框架通过 EL 表达式上下文来提供这些变量或者方法,默认提供的 EL 表达式上下文如下:

export interface ElContext {
    authorizeType: AuthorizeType
    method: string;
    args: any[];
    target: any;
    returnValue?: any;
    policies: Policy[];
    credentials: any;
    details?: any;
    principal: any;
    authenticated: boolean;
}

扩展 EL 表达式上下文

框架提供了一个扩展接口 SecurityExpressionContextHandler 用于扩展 EL 表达式上下文。

export interface SecurityExpressionContextHandler {
    handle(context: any): Promise
}

集中式认证与授权

Malagu 默认提供的单体式认证与授权,认证与授权过程都发生在本地应用中,如何将认证与授权进行集中式授权呢?框架设计初期就考虑这个问题。

集中式认证

我们只需要实现一个自己的认证提供者就可以了,将认证委托给远端统一认证服务即可。如果在非 Malagu 框架体系的应用,可以通过 rest 接口或者 Rpc 接口实现同样的效果。

集中式授权

我们只需要实现一个自己的认证管理器就可以了,将授权委托给远端统一授权服务务即可。如果在非 Malagu 框架体系的应用,可以通过 rest 接口或者 Rpc 接口实现同样的效果。

相关链接