SAP Business Technology Platform(BTP) UAA服务(1) - 创建和使用

中文社区中很少检索到关于SAP Business Technology Platform(BTP) UAA 相关的文章,笔者也想就此机会梳理一下关于这方面的知识,会整理一系列的文章介绍如何使用UAA。大部分内容将会是对blog.sap中相关文章的翻译和整合,如有任何错误之处,请指正。

什么是UAA?

UAA 的全称是User account and authentication,其实又有不同的UAA定义,下面是简单的介绍:

  1. CFUAA, 它代表的Cloud Foundry User Account and Authentication, 是一个CF上的开源项目。

  2. 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平台相关的权限。

  3. 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,

  1. 在本地创建一个新的文件夹 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。

image.png

创建应用

目前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.jsapp.use(passport.authenticate('JWT', { session: false }))会去检查请求中是不是包含了JWT,如果没有就会返回401 Unauthorized.

image.png

如何获得JWT?

关于什么是JWT,它有什么作用会在后续文章中介绍,此处就不赘述了。当前注重在如何获取JWT并把它带着一起访问REST endpoint。

  1. 打开BTP Cockpit, 找到之前创建的UAA Service,点击 View Credential,找到clientid(读者可以发现此处的clientid和xs-security.js中定义的xsappname很像,该值才是真正运行时的xsappname),clientsecret,url,并记录下来。
    image.png
  2. 打开Postman, 新建一个Request,在Authorization tab中选择OAuth 2.0, Grant Type选择 Password Credentials, Access Token URL 中填入前一步的 url + /oauth/token, 填入相应的Client ID和Client Secret, 还有登录BTP的邮箱和密码。
    image.png
  3. 点击 Get New Access Token, 成功得到Token后点击Use Token,


    image.png
  4. 在Request URL中,输入Endpoint地址,使用GET method,点击Send,可以成功得到200的status code。


    image.png
  5. 将URL地址上再加上/Display尝试一下,发现我们只能得到403的status code,原因是缺乏需要的DisplayScode scope,
    image.png

我们在这里稍微停一下,去仔细地看一下JWT里面到底包含了什么信息, 将Token从Headers里面地Authorization key 复制出来(不要复制Bearer单词),打开 https://jwt.io/,粘贴Token,Decode之后可以发现该Token中包含很多信息,用户名,UAA Service信息等等,当然scope也在其中,但是目前只包含了openid这个scope,并没有我们新建的DisplayScopeUpdateScope

给用户赋予Role Collection

在此处就不详细记录如何在BTP上给用户赋权的操作了,请不清楚的读者自行google一下。
笔者给自己的账号assign了UserViewerRoleCollection role collection,它包含了DisplayScope scope

image.png

现在我们再重复一下上一个章节步骤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(强烈推荐)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358

推荐阅读更多精彩内容