中文社区中很少检索到关于SAP Business Technology Platform(BTP) UAA 相关的文章,笔者也想就此机会梳理一下关于这方面的知识,会整理一系列的文章介绍如何使用UAA。大部分内容将会是对blog.sap中相关文章的翻译和整合,如有任何错误之处,请指正。
什么是UAA?
UAA 的全称是User account and authentication,其实又有不同的UAA定义,下面是简单的介绍:
CFUAA, 它代表的Cloud Foundry User Account and Authentication, 是一个CF上的开源项目。
Platform UAA, 它是部署在BTP Cloud Foundry平台上的一个基础服务(a requirement to get Cloud Foundry running),可以管理Platform Users(Users that administrate the Cloud Foundry account and its security),去授权管理各种BTP平台相关的权限。
XSUAA的全称是eXtened Services for UAA, 它是SAP开发的基于CFUAA的扩展,在CFUAA上增加了service broker, multitenancy等功能,是BTP平台管理Business User认证和授权的服务组件。开发人员在BTP中创建的Authorization and Trust Management Service就是XSUAA Service, 后文中提到的UAA也特指XSUAA。
此处有一个重要的概念还是需要解释清楚,认证和授权是两个相对独立的过程:
认证(Authentication) :通过用户名(ID, email etc.) + 密码(password, security certificate, token etc.) 确认该用户是个合法用户。
授权(Authorization) :基于一个合法用户授予此账号相应的权限,如可以浏览订单,但是不能创建订单等。
UAA本身并不做用户认证,换句话说UAA并没有校验用户名密码的功能,用户输入的用户名和密码登录信息会被转发到 Identity Provider (IdP) 去做验证,这个Identity Provider才是真正校验用户是否合法的组件,我们在BTP上可能对这个过程无感,是因为BTP上的账号关联到了SAP ID Service作为Default Identity Provider,关于该内容后续文章会再探讨。
在很多博客中接着介绍Approuter详细分析UAA的认证和授权的过程,笔者想把该部分内容留在读者有一定基础概念和练习之后再解释,从个人学习的经验上来说如果过多新的概念混杂在一起可能理解起来会有困难。
如何在BTP中创建UAA?
通常我们有两种方式创建UAA Service,第一种是使用BTP Cockpit创建,另一种是使用CF CLI来创建,此处我们用CF CLI来创建一个新的UAA Service,
- 在本地创建一个新的文件夹 BasicPractice,也可以起其他名字,新建一个文件 xs-security.json, 复制下方代码:
{
"xsappname" : "MyFirstUAA",
"tenant-mode" : "dedicated",
"scopes": [
{
"name": "$XSAPPNAME.DisplayScope"
},
{
"name": "$XSAPPNAME.UpdateScope"
}
],
"role-templates": [
{
"name" : "ViewerRoleTemplate",
"scope-references" : ["$XSAPPNAME.DisplayScope"]
},
{
"name" : "ManagerRoleTemplate",
"scope-references" : [
"$XSAPPNAME.DisplayScope",
"$XSAPPNAME.UpdateScope"]
}
],
"role-collections": [
{
"name": "UserViewerRoleCollection",
"description": "User Viewer Role Collection",
"role-template-references": [
"$XSAPPNAME.ViewerRoleTemplate"
]
},
{
"name": "UserManagerRoleCollection",
"description": "User Manager Role Collection",
"role-template-references": [
"$XSAPPNAME.ViewerRoleTemplate",
"$XSAPPNAME.ManagerRoleTemplate"
]
}
]
}
其实这个文件就是UAA的配置文件,有几个比较重要的property:
xsappname
代表了这个UAA Service的名字,它在当前BTP Subaccount下会是唯一的,在xs-security文件中可以使用$XSAPPNAME
去引用它。
scopes
表示了当前的UAA中定义的范围,在application中程序会去检查当前用户的JWT中是否包含某个scope。 通常会mapping到CRUD的操作,如创建,更新,删除。我们将会看到如何使用它。
role-templates
中定义了role,role可以用来把一个或者多个scope组合在一起,当用户拥有某个role时,该用户也拥有了此role下面所有的scope。严格意义上说role是role-template的实例,在大多数情况下可以认为在xs-security.json中role和role-template是等价的。
role-collections
可以用来把一个或者多个role组合在一起,当真正去分配用户权限时,是把某个role-collection 分配给某个用户,该用户拥有此role-collection下面的所有role。
基于xs-security.json配置文件,我们可以创建UAA Service,在根目录下运行如下命令(确保已经CF登录BTP):
cf cs xsuaa application MyFirstUAA -c xs-security.json
打开BTP Cockpit,可以发现成功创建的UAA Service Instance并且binding到了之前创建的UAA Service。
创建应用
目前SAP更推荐使用CAP在BTP上开发App,为了使用方便CAP已经为开发者把权限管理封装成注解的形式。此处笔者采用普通的express的方式暴露出REST endpoint,是为了更清楚的展现我们可以在程序运行时可以获得的权限相关信息。
仍然在当前目录(BasicPractic)下创建文件package.json
, 复制以下内容:
{
"main": "server.js",
"dependencies": {
"@sap/xsenv": "latest",
"@sap/xssec": "latest",
"express": "^4.16.3",
"passport": "^0.5.1"
}
}
创建新文件server.js
,复制以下内容:
const express = require('express');
const passport = require('passport');
const xsenv = require('@sap/xsenv');
const JWTStrategy = require('@sap/xssec').JWTStrategy;
//configure passport
const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});
const xsuaaCredentials = xsuaaService.myXsuaa;
const jwtStrategy = new JWTStrategy(xsuaaCredentials)
// configure express server with authentication middleware
passport.use(jwtStrategy);
const app = express();
// Middleware to read JWT sent by client
function jwtLogger(req, res, next) {
console.log('===> Decoding auth header' )
const jwtToken = readJwt(req)
if(jwtToken){
console.log('===> JWT: audiences: ' + jwtToken.aud);
console.log('===> JWT: scopes: ' + jwtToken.scope);
console.log('===> JWT: client_id: ' + jwtToken.client_id);
}
next()
}
app.use(jwtLogger)
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
app.get('/', function(req, res){
console.log('===> Endpoint has been reached. No authorization check')
res.send('The endpoint was properly called, everything works fine');
});
// app endpoint with authorization check
app.get('/Display', function(req, res){
console.log('===> Endpoint has been reached. Now checking authorization')
const MY_SCOPE = xsuaaCredentials.xsappname + '.DisplayScope'// scope name copied from xs-security.json
if(req.authInfo.checkScope(MY_SCOPE)){
res.send('The endpoint was properly called, role available, delivering data');
}else{
const jwtToken = readJwt(req)
const availableScopes = jwtToken jwtToken.scope : {}
return res.status(403).json({
error: 'Unauthorized',
message: `Missing required scope: <DisplayScope>. Available scopes: ${availableScopes}`
});
}
});
app.get('/Update', function(req, res){
console.log('===> Endpoint has been reached. Now checking authorization')
const MY_SCOPE = xsuaaCredentials.xsappname + '.UpdateScope'// scope name copied from xs-security.json
if(req.authInfo.checkScope(MY_SCOPE)){
res.send('The endpoint was properly called, role available, updating data');
}else{
const jwtToken = readJwt(req)
const availableScopes = jwtToken jwtToken.scope : {}
return res.status(403).json({
error: 'Unauthorized',
message: `Missing required role: <UpdateScope>. Available scopes: ${availableScopes}`
});
}
});
const readJwt = function(req){
const authHeader = req.headers.authorization;
if (authHeader){
const theJwtToken = authHeader.substring(7);
if(theJwtToken){
const jwtBase64Encoded = theJwtToken.split('.')[1];
if(jwtBase64Encoded){
const jwtDecoded = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii');
return JSON.parse(jwtDecoded);
}
}
}
}
// start server
app.listen(process.env.PORT || 8080, () => {
console.log('Server running...')
})
创建新文件manifest.yml
,复制以下内容:
---
applications:
- name: providerapp
memory: 128M
buildpacks:
- nodejs_buildpack
services:
- MyFirstUAA
random-route: true
我们在server.js
中暴露出3个endpoint, 分别是/
, Display
, Update
, 其中/
并没有检查任何scope信息,在 Display
handler中我们检查了DisplayScope
, 在 Update
handler中我们检查了UpdateScope
.
分别运行
npm install
cf push
回到BTP Cockpit,在CF space下我们可以找到刚刚创建的App,点击Application Routes,访问/
的地址,不幸的是我们会得到 Unauthorized 的返回值,是因为我们缺少了JWT,在server.js
中app.use(passport.authenticate('JWT', { session: false }))
会去检查请求中是不是包含了JWT,如果没有就会返回401 Unauthorized.
如何获得JWT?
关于什么是JWT,它有什么作用会在后续文章中介绍,此处就不赘述了。当前注重在如何获取JWT并把它带着一起访问REST endpoint。
- 打开BTP Cockpit, 找到之前创建的UAA Service,点击 View Credential,找到clientid(读者可以发现此处的clientid和
xs-security.js
中定义的xsappname
很像,该值才是真正运行时的xsappname
),clientsecret,url,并记录下来。
- 打开Postman, 新建一个Request,在Authorization tab中选择OAuth 2.0, Grant Type选择
Password Credentials
, Access Token URL 中填入前一步的url + /oauth/token
, 填入相应的Client ID和Client Secret, 还有登录BTP的邮箱和密码。
-
点击 Get New Access Token, 成功得到Token后点击Use Token,
-
在Request URL中,输入Endpoint地址,使用GET method,点击Send,可以成功得到200的status code。
- 将URL地址上再加上
/Display
尝试一下,发现我们只能得到403的status code,原因是缺乏需要的DisplayScode
scope,
我们在这里稍微停一下,去仔细地看一下JWT里面到底包含了什么信息, 将Token从Headers里面地Authorization key 复制出来(不要复制Bearer单词),打开 https://jwt.io/,粘贴Token,Decode之后可以发现该Token中包含很多信息,用户名,UAA Service信息等等,当然scope也在其中,但是目前只包含了openid这个scope,并没有我们新建的DisplayScope
和UpdateScope
。
给用户赋予Role Collection
在此处就不详细记录如何在BTP上给用户赋权的操作了,请不清楚的读者自行google一下。
笔者给自己的账号assign了UserViewerRoleCollection
role collection,它包含了DisplayScope
scope
现在我们再重复一下上一个章节步骤2-5,发现已经可以成功访问
/Display
,在 https://jwt.io/ 的中测试,新的Token解析完包含了 MyFirstUAA!t77228.DisplayScope
scope。有兴趣的读者可以自行尝试一下访问
/Update
,并解决403的问题。
小结
本篇博客主要简略介绍了什么是UAA,如何创建一个简单的BTP UAA Service,如何在express服务器端校验权限,如何手动获取JWT并解析。
可以发现用户权限信息都是包含在JWT中,下一篇博客将会介绍当用户访问SAP BTP上的Fiori/UI5应用时,是如何获取JWT的。
先挖个坑激励自己写下去,后续还会有app2app的授权,跨uaa instance的授权等。
再次说明内容并非原创,是对blog.sap上一些文章的翻译和整理,方便不习惯英文阅读的读者。
引用:
https://blogs.sap.com/2019/01/07/uaa-xsuaa-platform-uaa-cfuaa-what-is-it-all-about/
https://blogs.sap.com/2020/08/20/demystifying-xsuaa-in-sap-cloud-foundry/
https://blogs.sap.com/2020/06/02/how-to-call-protected-app-from-external-app-as-external-user-with-scope/#samplecode
https://people.sap.com/carlos.roggan(强烈推荐)