中文社區(qū)中很少檢索到關(guān)于SAP Business Technology Platform(BTP) UAA 相關(guān)的文章淆院,筆者也想就此機(jī)會(huì)梳理一下關(guān)于這方面的知識迂烁,會(huì)整理一系列的文章介紹如何使用UAA奄侠。大部分內(nèi)容將會(huì)是對blog.sap中相關(guān)文章的翻譯和整合惠勒,如有任何錯(cuò)誤之處姚建,請指正。
什么是UAA?
UAA 的全稱是User account and authentication烹棉,其實(shí)又有不同的UAA定義,下面是簡單的介紹:
CFUAA, 它代表的Cloud Foundry User Account and Authentication, 是一個(gè)CF上的開源項(xiàng)目怯疤。
Platform UAA, 它是部署在BTP Cloud Foundry平臺上的一個(gè)基礎(chǔ)服務(wù)(a requirement to get Cloud Foundry running)浆洗,可以管理Platform Users(Users that administrate the Cloud Foundry account and its security),去授權(quán)管理各種BTP平臺相關(guān)的權(quán)限集峦。
XSUAA的全稱是eXtened Services for UAA, 它是SAP開發(fā)的基于CFUAA的擴(kuò)展伏社,在CFUAA上增加了service broker, multitenancy等功能,是BTP平臺管理Business User認(rèn)證和授權(quán)的服務(wù)組件塔淤。開發(fā)人員在BTP中創(chuàng)建的Authorization and Trust Management Service就是XSUAA Service, 后文中提到的UAA也特指XSUAA摘昌。
此處有一個(gè)重要的概念還是需要解釋清楚,認(rèn)證和授權(quán)是兩個(gè)相對獨(dú)立的過程:
認(rèn)證(Authentication) :通過用戶名(ID, email etc.) + 密碼(password, security certificate, token etc.) 確認(rèn)該用戶是個(gè)合法用戶高蜂。
授權(quán)(Authorization) :基于一個(gè)合法用戶授予此賬號相應(yīng)的權(quán)限聪黎,如可以瀏覽訂單,但是不能創(chuàng)建訂單等妨马。
UAA本身并不做用戶認(rèn)證挺举,換句話說UAA并沒有校驗(yàn)用戶名密碼的功能,用戶輸入的用戶名和密碼登錄信息會(huì)被轉(zhuǎn)發(fā)到 Identity Provider (IdP) 去做驗(yàn)證烘跺,這個(gè)Identity Provider才是真正校驗(yàn)用戶是否合法的組件湘纵,我們在BTP上可能對這個(gè)過程無感,是因?yàn)锽TP上的賬號關(guān)聯(lián)到了SAP ID Service作為Default Identity Provider滤淳,關(guān)于該內(nèi)容后續(xù)文章會(huì)再探討梧喷。
在很多博客中接著介紹Approuter詳細(xì)分析UAA的認(rèn)證和授權(quán)的過程,筆者想把該部分內(nèi)容留在讀者有一定基礎(chǔ)概念和練習(xí)之后再解釋脖咐,從個(gè)人學(xué)習(xí)的經(jīng)驗(yàn)上來說如果過多新的概念混雜在一起可能理解起來會(huì)有困難铺敌。
如何在BTP中創(chuàng)建UAA?
通常我們有兩種方式創(chuàng)建UAA Service屁擅,第一種是使用BTP Cockpit創(chuàng)建偿凭,另一種是使用CF CLI來創(chuàng)建,此處我們用CF CLI來創(chuàng)建一個(gè)新的UAA Service派歌,
- 在本地創(chuàng)建一個(gè)新的文件夾 BasicPractice弯囊,也可以起其他名字,新建一個(gè)文件 xs-security.json, 復(fù)制下方代碼:
{
"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"
]
}
]
}
其實(shí)這個(gè)文件就是UAA的配置文件胶果,有幾個(gè)比較重要的property:
xsappname
代表了這個(gè)UAA Service的名字匾嘱,它在當(dāng)前BTP Subaccount下會(huì)是唯一的,在xs-security文件中可以使用$XSAPPNAME
去引用它早抠。
scopes
表示了當(dāng)前的UAA中定義的范圍霎烙,在application中程序會(huì)去檢查當(dāng)前用戶的JWT中是否包含某個(gè)scope。 通常會(huì)mapping到CRUD的操作,如創(chuàng)建悬垃,更新游昼,刪除。我們將會(huì)看到如何使用它盗忱。
role-templates
中定義了role酱床,role可以用來把一個(gè)或者多個(gè)scope組合在一起,當(dāng)用戶擁有某個(gè)role時(shí)趟佃,該用戶也擁有了此role下面所有的scope。嚴(yán)格意義上說role是role-template的實(shí)例昧捷,在大多數(shù)情況下可以認(rèn)為在xs-security.json中role和role-template是等價(jià)的闲昭。
role-collections
可以用來把一個(gè)或者多個(gè)role組合在一起,當(dāng)真正去分配用戶權(quán)限時(shí)靡挥,是把某個(gè)role-collection 分配給某個(gè)用戶序矩,該用戶擁有此role-collection下面的所有role。
基于xs-security.json配置文件跋破,我們可以創(chuàng)建UAA Service簸淀,在根目錄下運(yùn)行如下命令(確保已經(jīng)CF登錄BTP):
cf cs xsuaa application MyFirstUAA -c xs-security.json
打開BTP Cockpit,可以發(fā)現(xiàn)成功創(chuàng)建的UAA Service Instance并且binding到了之前創(chuàng)建的UAA Service毒返。
創(chuàng)建應(yīng)用
目前SAP更推薦使用CAP在BTP上開發(fā)App租幕,為了使用方便CAP已經(jīng)為開發(fā)者把權(quán)限管理封裝成注解的形式。此處筆者采用普通的express的方式暴露出REST endpoint拧簸,是為了更清楚的展現(xiàn)我們可以在程序運(yùn)行時(shí)可以獲得的權(quán)限相關(guān)信息劲绪。
仍然在當(dāng)前目錄(BasicPractic)下創(chuàng)建文件package.json
, 復(fù)制以下內(nèi)容:
{
"main": "server.js",
"dependencies": {
"@sap/xsenv": "latest",
"@sap/xssec": "latest",
"express": "^4.16.3",
"passport": "^0.5.1"
}
}
創(chuàng)建新文件server.js
,復(fù)制以下內(nèi)容:
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...')
})
創(chuàng)建新文件manifest.yml
盆赤,復(fù)制以下內(nèi)容:
---
applications:
- name: providerapp
memory: 128M
buildpacks:
- nodejs_buildpack
services:
- MyFirstUAA
random-route: true
我們在server.js
中暴露出3個(gè)endpoint, 分別是/
, Display
, Update
, 其中/
并沒有檢查任何scope信息贾富,在 Display
handler中我們檢查了DisplayScope
, 在 Update
handler中我們檢查了UpdateScope
.
分別運(yùn)行
npm install
cf push
回到BTP Cockpit,在CF space下我們可以找到剛剛創(chuàng)建的App牺六,點(diǎn)擊Application Routes颤枪,訪問/
的地址,不幸的是我們會(huì)得到 Unauthorized 的返回值淑际,是因?yàn)槲覀內(nèi)鄙倭薐WT畏纲,在server.js
中app.use(passport.authenticate('JWT', { session: false }))
會(huì)去檢查請求中是不是包含了JWT,如果沒有就會(huì)返回401 Unauthorized.
如何獲得JWT?
關(guān)于什么是JWT庸追,它有什么作用會(huì)在后續(xù)文章中介紹霍骄,此處就不贅述了。當(dāng)前注重在如何獲取JWT并把它帶著一起訪問REST endpoint淡溯。
- 打開BTP Cockpit, 找到之前創(chuàng)建的UAA Service读整,點(diǎn)擊 View Credential,找到clientid(讀者可以發(fā)現(xiàn)此處的clientid和
xs-security.js
中定義的xsappname
很像咱娶,該值才是真正運(yùn)行時(shí)的xsappname
)米间,clientsecret强品,url,并記錄下來屈糊。
image.png - 打開Postman, 新建一個(gè)Request的榛,在Authorization tab中選擇OAuth 2.0, Grant Type選擇
Password Credentials
, Access Token URL 中填入前一步的url + /oauth/token
, 填入相應(yīng)的Client ID和Client Secret, 還有登錄BTP的郵箱和密碼。
image.png -
點(diǎn)擊 Get New Access Token, 成功得到Token后點(diǎn)擊Use Token逻锐,
image.png -
在Request URL中夫晌,輸入Endpoint地址,使用GET method昧诱,點(diǎn)擊Send晓淀,可以成功得到200的status code。
image.png - 將URL地址上再加上
/Display
嘗試一下盏档,發(fā)現(xiàn)我們只能得到403的status code凶掰,原因是缺乏需要的DisplayScode
scope,
image.png
我們在這里稍微停一下蜈亩,去仔細(xì)地看一下JWT里面到底包含了什么信息懦窘, 將Token從Headers里面地Authorization key 復(fù)制出來(不要復(fù)制Bearer單詞),打開 https://jwt.io/稚配,粘貼Token畅涂,Decode之后可以發(fā)現(xiàn)該Token中包含很多信息,用戶名药有,UAA Service信息等等毅戈,當(dāng)然scope也在其中,但是目前只包含了openid這個(gè)scope愤惰,并沒有我們新建的DisplayScope
和UpdateScope
苇经。
給用戶賦予Role Collection
在此處就不詳細(xì)記錄如何在BTP上給用戶賦權(quán)的操作了,請不清楚的讀者自行g(shù)oogle一下宦言。
筆者給自己的賬號assign了UserViewerRoleCollection
role collection扇单,它包含了DisplayScope
scope
現(xiàn)在我們再重復(fù)一下上一個(gè)章節(jié)步驟2-5,發(fā)現(xiàn)已經(jīng)可以成功訪問
/Display
奠旺,在 https://jwt.io/ 的中測試蜘澜,新的Token解析完包含了 MyFirstUAA!t77228.DisplayScope
scope。有興趣的讀者可以自行嘗試一下訪問
/Update
响疚,并解決403的問題鄙信。
小結(jié)
本篇博客主要簡略介紹了什么是UAA,如何創(chuàng)建一個(gè)簡單的BTP UAA Service忿晕,如何在express服務(wù)器端校驗(yàn)權(quán)限装诡,如何手動(dòng)獲取JWT并解析。
可以發(fā)現(xiàn)用戶權(quán)限信息都是包含在JWT中,下一篇博客將會(huì)介紹當(dāng)用戶訪問SAP BTP上的Fiori/UI5應(yīng)用時(shí)鸦采,是如何獲取JWT的宾巍。
先挖個(gè)坑激勵(lì)自己寫下去,后續(xù)還會(huì)有app2app的授權(quán)渔伯,跨uaa instance的授權(quán)等顶霞。
再次說明內(nèi)容并非原創(chuàng),是對blog.sap上一些文章的翻譯和整理锣吼,方便不習(xí)慣英文閱讀的讀者选浑。
引用:
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(強(qiáng)烈推薦)