写点什么

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

用户头像
木香丘
关注
发布于: 2020 年 07 月 14 日
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: passwordbackend:  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<User> { if (this.options.username === username) { return { username, password: this.options.password, accountNonExpired: true, accountNonLocked: true, credentialsNonExpired: true, enabled: true, policies: [ <ElPolicy>{ 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<User[]> {  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<Authentication>;    support(): Promise<boolean>;}
复制代码

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

async support(): Promise<boolean> {	  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<SecurityMetadata>;}
复制代码


访问决策管理器

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

export interface AccessDecisionManager {    decide(securityMetadata: SecurityMetadata): Promise<void>;}
复制代码


访问决策投票器

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

export interface AccessDecisionVoter {    vote(securityMetadata: SecurityMetadata): Promise<number>;    support(securityMetadata: SecurityMetadata): Promise<boolean>;    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<void>}
复制代码


集中式认证与授权

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

集中式认证


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


集中式授权


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


相关链接


发布于: 2020 年 07 月 14 日阅读数: 95
用户头像

木香丘

关注

Serverless 爱好者 2019.01.18 加入

Serverless 原生框架 Malagu 的作者。杭州细胞矩阵科技有限公司创始人。从事 Serverless 相关工作两年多。开源爱好者。

评论

发布
暂无评论
Malagu 框架的认证与授权【借鉴 Spring Security 和 aws iam 的设计】