管理侧操作指南
创建实例
菜单路径
产品与服务→能力中枢服务→统一身份管理→购买统一身份管理
操作步骤
1.登录浪潮云服务控制台。
2.点击“产品与服务”选择“能力中枢服务”,再点击统一身份管理,进入服务概览界面。
3.两种路径可以创建统一身份管理:
1)在概览页面上,点击“开通统一身份管理”,创建实例。
2)在左侧导航栏,点击“服务管理”→“开通统一身份管理”,创建实例。
4.完成基础配置:
进入开通页,完成开通3实例相关选项配置。
- 根据业务需求选择“版本”,版本分为基础版和专业版
- 基础版用户数为500
- 专业版可选择用户数为 1000、2000、3000、4000、5000、6000、7000、8000、9000、10000共10个规格
- 填写“实例名称”。
应用管理
应用列表
应用列表展示企业构建的应用,包括自建应用和IDaaS内置集成应用。管理员可对已添加的应用进行状态修改、授权、编辑、删除及API配置等操作。
添加应用
目前支持添加OAuth应用,OAuth是一个开放的资源授权协议,应用可以通过OAuth获取到令牌access_token,并携带令牌来服务端请求用户资源,应用可以使用OAuth应用模板来实现统一身份管理。
OAuth 应用只实现了 SP(Server Provider, 业务系统方) 发起的单点登录流程。完善应用基本信息,点击提交按钮即可完成新增应用。
授权
应用授权可以将应用授权给用户或组织机构,也可将用户授权给某些应用。
更多
用户管理
用户类型
用户通过绑定不同的用户类型以此区分不同的用户群。 管理员可对用户类型进行新增、编辑、查看、删除及修改状态等操作。
新建用户类型
查看用户类型
编辑用户类型
删除用户类型
机构管理
管理员在当前页面对用户、组、组织机构进行管理,左侧为组织机构信息,右侧显示该组织机构下的用户、组信息。切换页面左侧的组织机构,右侧组织详情中展示不同组织的用户、用户组、组织机构。点击相应的操作会实现修改、同步、删除等操作。
新增
选中左侧组织机构,点击用户模块下的创建用户,即可在当前组织机构下创建用户,填写创建用户信息。
扩展属性通过组织机构右上角的数据字典创建,扩展属性通过数据字典动态查询而来。
数据字典
数据字典列表展示已添加的数据字典信息,如该字段分类设置为用户,该字段则在组织机构用户模块下创建用户中的扩展属性展示。组和组织机构同用户设置。
数据字典中添加的字段会在对应的分类下的扩展属性中展示,字段类型支持下拉框、文本框、文本域,并支持字段必填、字段唯一、字段状态启用/停用。
注意,启用状态下无法删除字段,需要先关闭启用状态再进行字段的删除。
用户列表
用户账号列表展示所有已添加的用户信息,负责对每个用户进行禁用、离职等操作,也可以查看用户详情信息,包括状态、属性、所属组织机构等。
离职账号列表展示所有已离职的账号信息,用户账号离职后自动存放到该列表中,通过账号后方的返聘即可将该用户恢复为正常账号状态。
授权管理
应用授权
应用授权主体用户时选中左侧应用,在用户模块下勾选要授权的用户(支持多选用户),点击下方授权即可完成用户授权,反之点击取消授权即可完成用户的取消授权,组织机构同用户。
主体授权应用时选中左侧用户模块下的用户,勾选右侧应用列表(支持应用多选),点击下方授权即可完成一个用户授权多个应用的操作,反之点击取消授权完成取消授权操作,组织机构同用户。
权限系统
权限系统是基于RBAC(Role-Based Access Control,基于角色的访问控制)的权限模型。既可以管理 IDaaS 自身资源权限,也可以管理第三方应用的二级菜单功能按钮等资源权限;当授予某个 IDaaS 用户开发者权限,在未来将可以创建出新的自定义权限系统,以支持应用的三级授权。
对系统的权限进行统一管理,功能包括角色管理、资源管理、授权管理。
新增权限系统
输入系统名称及系统ID(系统唯一标识, 不能重复),点击确定按钮即完成权限系统的新增。
授权管理
主体授权页左侧展示的是主体,分为用户、用户组、组织机构,页面右侧展示的是权限资源。页面右侧选中的选项是当前主体已被赋予的权限资源,未选中的表示未赋予的权限资源。点击选中未被赋予的权限,接着点击授权按钮便可为当前主体添加权限资源。点击已被赋予的权限然后点击取消授权按钮便可为当前主体解除该权限资源。
页面左侧展示的是主体,右侧展示的是当前选中主体已被赋予的角色列表,点击列表多选按钮然后点击取消授权便可取消该角色授权。切换下拉框为未授权就可以查看当前主体可被赋予的角色列表,选中角色列表选项,点击授权按钮便可为当前主体添加角色授权。
页面左侧展示的资源和角色,右侧展示的是主体,分为用户、用户组、组织机构。与上面主体资源相反,是通过资源来选择有哪些主体能够访问。
角色管理
IDaaS 的权限系统支持角色授权模型(RBAC)。角色可以关联到一系列指定权限上,拥有角色的账号则即可拥有所有对应的权限。管理员可以在这里为指定权限系统的角色进行新增、删除、编辑、关联权限等管理操作。
点击列表中授权到主体按钮,跳转到授权管理页面,页面左侧展示当前角色名称的筛选结果,点击左侧角色名称,页面右侧展示当前角色可访问的主体。
资源管理
无论是某个功能、菜单、按钮的查看权、使用权,还是某些特定数据的访问权限,都属于一种资源(Resource)。资源支持嵌套树形结构。管理员在这里可以新增、导入、删除、编辑资源,也可以为某资源关联到角色使用。
点击新增父级资源按钮,填写资源标识和资源名称,点击确认按钮完成父级资源新增。新增完成后,新添加的资源会在列表左侧树状结构根目录下。
点击新增子级资源,新增子级资源弹窗中父级就变成了列表中的路径名。新增完成后,该资源就会出现在页面左侧树状结构中,并且列表路径名和新增资源就形成的父子关系。
点击页面右上角的数据权限模型按钮,进入到数据权限模型列表页面。数据权限模型 是 IDaaS 实现数据权限管理中的重要概念,代表着对指定类型的数据细分为哪些操作权限,在创建数据权限时,需要指定所属的数据权限模型,在授权时,新的数据权限中即可选择某些操作进行授权。
数据模型中包含了针对当前模型可以授权的所有操作。 例如,为一个协同办公应用,可以新建“项目”数据模型,在这里设置可授权操作为“创建项目”、“查看普通项目信息”、“查看核心项目信息”、“删除项目”等。
系统详情
审计
同步记录
API同步展示针对不同应用、主体和请求类型的同步结果,管理员可以对同步记录进行删除操作,也可对同步记录发起重新同步的操作。
管理员通过消息同步可以查看消息同步的历史记录,可以对消息同步进行删除和发起重新同步的操作。
操作日志
操作日志记录了用户进行的数十种关键操作,管理员和用户侧的操作,都可以在这里找到对应的操作日志,以对某次改变提供充分的溯源证据。
根据操作日志的操作模块类型及操作类型的不同,可以选择一个时间段的柱状图或者饼图。
进出日志
进/出日志专门直观地查询某个账户的登录、退出日志。管理员也可以在操作日志中通过筛选日志类型进行搜索。
高级
会话管理
会话管理可以查看所有当前活动的会话列表,并且可以强制注销某个用户的会话。会话是否有效以登录令牌为准,如果没有手动退出,在令牌过期前(默认 2 小时),会话仍然有效。注销后,该用户凭证即刻失效,之后的所有操作都需要重新认证。一个账户在网页端只会有一个会话存在。移动端和网页端是两个会话范围,可以同时存在。
用户侧操作指南
用户登录
用户访问https://console1.cloud.inspur.com/iuser?realm={主账号} ,进入用户侧登录页面,如图6.1.1。
输入管理侧创建的用户名和密码后点击登录按钮,即可进入用户侧首页,如图6.1.2。在用户侧首页可以查看当前登录用户有权限访问的应用列表,包括WEB应用、移动应用和PC应用。
在用户侧首页如果未找到需要的应用,可以主动申请某应用的访问权限,点击申请应用访问权限按钮,选择要访问的应用、使用时长、申请原因即可发送申请(如图6.1.3),管理员审核通过后及可获取该应用的使用权限。
应用管理
进入应用管理菜单,可以查看已有权限的应用列表,并能查询指定应用的用户操作日志,如图6.2.1。
设置
我的账号
从设置菜单进入我的账号菜单,可以对当前账号进行安全设置以及个人信息维护。安全设置包括自主修改密码、绑定邮箱、绑定手机号等操作,如图6.3.1。
Oauth2.0应用接入示例
配置后台服务
打开前端项目中environments文件下的environment.ts,在Environment中添加如下代码配置:
static application: Application = {
backServiceGetTokenAPI: 'http://{IDaaS用户侧服务地址}:8092/token?realm={当前应用的realm}',
backServiceAPI: '{IDaaS用户侧服务地址}', //例如http://10.110.26.169
clientId: '{当前应用的clientId}',
realm: '{当前应用的realm}'
};
interface Application {
backServiceAPI: string;
backServiceGetTokenAPI: string;
clientId: string;
用户登录
在app.component.ts文件中引入依赖及定义必要变量
import { NzModalService } from 'ng-zorro-antd/modal';
import { SettingServiceService } from '../app/common/service/setting-service/setting-service.service';
import { CookieService } from '../app/common/constant/cookie.service';
userInfo = '';
refreshToken = '';
accessToken = '';
accountName = '';
clientId = '{当前应用id}';
redirectUri = '当前应用首页地址'; // 登录后跳转地址
private updataTokenTime = 3600; // 1h
private loginValidTime = 60; // 1h token
private timeout = 300000; // ms 5分钟
在ngOnInit()函数中加入login()函数。
ngOnInit(): void {
this.login();
}
在login()函数中,首先检查是否存在code,若code为空,则页面跳至登录页进行登录,从登录成功后的url中解析出code,并通过该code获得用户id和有效token;若code不为空,且accessToken为空,则进行code获取token操作。
login(): void {
const url = this.getRedirectURL();
const localUrl = window.location.href;
if (localUrl.indexOf('code=') < 0) {
window.location.href = url;
}
if (localUrl.indexOf('code=') >= 0 && this.accessToken === '') {
const params = localUrl.slice(localUrl.indexOf('code=')).split('&')[0];
const code = params.split('=')[1];
let href = window.location.href.split('&state')[0];
href = href.split('#state')[0];
this.getToken(this.getTokenUrl() + '&client_id=', code).subscribe(res => {
if (res.data) {
this.tokenCallback(res);
this.setLoginVaildCookie();
this.loopUpdateToken();
} else {
// 用户无权限
this.modal.error({
nzTitle: res.message,
nzContent: res.message,
nzClosable: false,
nzMaskStyle: {'background-color': 'rgba(0,0,0,0.9)'},
nzOnOk: () => {
window.location.href = url;
}
});
}
});
}
}
getToken(address: string, code: string): Observable<any> {
const url = address + this.clientId + `&redirect_uri=` + this.redirectUri + `&code=${code}`;
return this.http.post(url, JSON.stringify({})).pipe();
}
getTokenUrl(): string {
return Environment.application.backServiceGetTokenAPI;
}
getUserInfo(userId: string): void {
this.settingService.getUserInfo(userId).subscribe(res => {
if (res.code === '200') {
this.accountName = res.data.accountName || '';
}
});
}
getRedirectURL(): string {
const state = this.createUUID();
const nonce = this.createUUID();
const url = 'https://{keycloak服务地址} /auth/realms/{当前应用的realm}/protocol/openid-connect/auth'
+ '?client_id=' + encodeURIComponent(this.clientId)
+ '&redirect_uri=' + encodeURIComponent(this.redirectUri)
+ '&state=' + encodeURIComponent(state)
+ '&nonce=' + encodeURIComponent(nonce)
+ '&response_mode=fragment&response_type=code&scope=openid';
return url;
}
createUUID(): string {
const s: Array<string> = [];
const hexDigits = '0123456789abcdef';
for (let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = '4';
s[19] = hexDigits.substr(0, 1);
s[8] = s[13] = s[18] = s[23] = '-';
const uuid = s.join('');
return uuid;
}
tokenCallback()函数用于解析token,获得登录用户的信息,用于其他接口访问使用。
tokenCallback(res: any): void {
this.refreshToken = res.data.refresh_token;
this.accessToken = res.data.access_token;
// 解析token,获取用户信息
this.userInfo = window.atob(this.accessToken.split('.')[1]);
sessionStorage.setItem('inspur_token', this.accessToken);
sessionStorage.setItem('refresh_token', this.refreshToken);
sessionStorage.setItem('userId', JSON.parse(this.userInfo).sub);
sessionStorage.setItem('sessionId', res.data.session_state);
this.tokenParsed = this.decodeToken(res.data.access_token);
const timeLocal = new Date().getTime();
if (timeLocal) {
this.timeSkew = Math.floor(timeLocal / 1000) - this.tokenParsed.iat;
}
this.getUserInfo(JSON.parse(this.userInfo).sub);
}
decodeToken(str: string) {
str = str.split('.')[1];
if (!str) { return {}; }
str = str.replace('/-/g', '+');
str = str.replace('/_/g', '/');
switch (str.length % 4) {
case 0:
break;
case 2:
str += '==';
break;
case 3:
str += '=';
break;
default:
throw new Error('Invalid token');
}
str = (str + '===').slice(0, str.length + (str.length % 4));
str = str.replace(/-/g, '+').replace(/_/g, '/');
str = decodeURIComponent(escape(atob(str)));
str = JSON.parse(str);
return str;
}
setLoginVaildCookie()函数在cookie中设置登录标志位,有效期为1小时。
setLoginVaildCookie(): void {
let hosts = window.location.hostname.split('.'), host;
if (hosts.length >= 2) {
host = hosts[hosts.length - 2] + '.' + hosts[hosts.length - 1];
}
this.cookieService.setCookie('iuserisLoginValid', true, this.loginValidTime, host);
}
loopUpdateToken()函数进行登陆有效性检查,每五分钟检查一次,若登陆过期,则页面退回到登录页;若登录仍在有效期内,则执行updateToken()函数刷新token。
loopUpdateToken(): void {
this.time = setInterval(() => {
if (!this.cookieService.getCookie('iuserisLoginValid')) {
this.logout();
} else if (!this.isTokenExpired(this.timeout / 1000, this.refreshToken)) {
// isLoginValid有效且refreshtoken有效
this.updateToken(this.updataTokenTime).subscribe();
} else {
// isLoginValid有效, refreshtoken
clearInterval(this.time);
window.location.href = this.redirectUri;
}
}, this.timeout);
}
isTokenExpired(minValidity: number, token = this.tokenParsed) {
if (!this.tokenParsed || !this.refreshToken) {
throw 'Not authenticated';
}
if (this.timeSkew == null) {
console.info('Unable to determine if token is expired as timeskew is not set');
return true;
}
let expiresIn = token['exp'] - Math.ceil(new Date().getTime() / 1000) + this.timeSkew;
if (minValidity) {
expiresIn -= minValidity;
}
return expiresIn < 0;
}
updateToken()函数用于刷新token有效期。
updateToken(minValidity?: number): Observable<any> {
return Observable.create((observer: Observer<any>) => {
if (!this.refreshToken) {
return observableThrowError('err');
}
minValidity = minValidity || 5;
let refreshToken = false;
if (minValidity === -1) {
refreshToken = true;
} else if (!this.tokenParsed || this.isTokenExpired(minValidity)) {
refreshToken = true;
}
if (!refreshToken) {
return observableThrowError('err');
} else {
const token = sessionStorage.getItem('refresh_token') || '';
if (this.refreshQueue) {
this.refreshQueue = false;
this.refreshtoken(this.clientId, this.realm, token).subscribe(res => {
if (res.code === '200') {
this.tokenCallback(res);
this.refreshQueue = true;
} else {
this.refreshQueue = true;
}
});
}
}
});
}
refreshtoken(clientId: string, realm: string, token: string): Observable<any> {
const url = Environment.application.backServiceAPI + `/iuser/v1/token/refresh?client_id=${clientId}&realm=${realm}&refresh_token=${token}`;
return this.http.get(url).pipe();
}
用户登出
signOut()函数用于用户登出,用户登出时将清除本地储存的token、userId信息,页面跳回至登录页。
// 退出登录
signOut(): void {
const sessionId = sessionStorage.getItem('sessionId') || '';
this.SignOutMethod(sessionId).subscribe(res => {
if (res.code === '200') {
sessionStorage.clear();
window.location.href = this.redirectUri;
} else {
this.msg.error(res.message);
}
});
}
// 退出登录接口
SignOutMethod(sessionId: string): Observable<any> {
const url = Environment.application.backServiceAPI + `/iuser/v1/userside/logout/${sessionId}?realm=${Environment.application.realm}`;
return this.http.delete(url).pipe();
}