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 接口实现同样的效果。