1. 概述
本文接 《Apollo 源码解析 —— OpenAPI 认证与授权(一)之认证》 ,侧重在授权部分。和 Portal 的授权一样:
具体每个 URL 的权限校验,通过在对应的方法上,添加
@PreAuthorize
方法注解,配合具体的方法参数,一起校验功能 + 数据级的权限校验。
2. 权限模型
和 Portal 使用相同的权限模型,差别在于 UserRole 换成了 ConsumerRole 。所以,关系如下图:
2.1 ConsumerRole
ConsumerRole 表,Consumer 与角色的关联表,对应实体 com.ctrip.framework.apollo.openapi.entity.ConsumerRole
,代码如下:
@Entity
@Table(name = "ConsumerRole")
@SQLDelete(sql = "Update ConsumerRole set isDeleted = 1 where id = ?")
@Where(clause = "isDeleted = 0")
public class ConsumerRole extends BaseEntity {
/**
* Consumer 编号 {@link Consumer#id}
*/
@Column(name = "ConsumerId", nullable = false)
private long consumerId;
/**
* Role 编号 {@link com.ctrip.framework.apollo.portal.entity.po.Role#id}
*/
@Column(name = "RoleId", nullable = false)
private long roleId;
}
3. ConsumerService
com.ctrip.framework.apollo.openapi.service.ConsumerService
,提供 Consumer、ConsumerToken、ConsumerAudit、ConsumerRole 相关的 Service 逻辑。
3.1 createConsumerRole
#createConsumerRole(consumerId, roleId, operator)
方法,创建 Consumer 对象。代码如下:
ConsumerRole createConsumerRole(Long consumerId, Long roleId, String operator) {
ConsumerRole consumerRole = new ConsumerRole();
consumerRole.setConsumerId(consumerId);
consumerRole.setRoleId(roleId);
consumerRole.setDataChangeCreatedBy(operator);
consumerRole.setDataChangeLastModifiedBy(operator);
return consumerRole;
}
3.2 assignAppRoleToConsumer
#assignAppRoleToConsumer(token, appId)
方法,授权 App 的 Role 给 Consumer 。代码如下:
@Transactional
public ConsumerRole assignAppRoleToConsumer(String token, String appId) {
// 校验 Token 是否有对应的 Consumer 。若不存在,抛出 BadRequestException 异常
Long consumerId = getConsumerIdByToken(token);
if (consumerId == null) {
throw new BadRequestException("Token is Illegal");
}
// 获得 App 对应的 Role 对象
Role masterRole = rolePermissionService.findRoleByRoleName(RoleUtils.buildAppMasterRoleName(appId));
if (masterRole == null) {
throw new BadRequestException("App's role does not exist. Please check whether app has created.");
}
// 获得 Consumer 对应的 ConsumerRole 对象。若已存在,返回 ConsumerRole 对象
long roleId = masterRole.getId();
ConsumerRole managedModifyRole = consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, roleId);
if (managedModifyRole != null) {
return managedModifyRole;
}
// 创建 Consumer 对应的 ConsumerRole 对象
String operator = userInfoHolder.getUser().getUserId();
ConsumerRole consumerRole = createConsumerRole(consumerId, roleId, operator);
// 保存 Consumer 对应的 ConsumerRole 对象
return consumerRoleRepository.save(consumerRole);
}
3.3 assignNamespaceRoleToConsumer
#assignNamespaceRoleToConsumer(token, appId, namespaceName)
方法,授权 Namespace 的 Role 给 Consumer 。对吗如下:
@Transactional
public List<ConsumerRole> assignNamespaceRoleToConsumer(String token, String appId, String namespaceName) {
// 校验 Token 是否有对应的 Consumer 。若不存在,抛出 BadRequestException 异常
Long consumerId = getConsumerIdByToken(token);
if (consumerId == null) {
throw new BadRequestException("Token is Illegal");
}
// 获得 Namespace 对应的 Role 们。若有任一不存在,抛出 BadRequestException 异常
Role namespaceModifyRole = rolePermissionService.findRoleByRoleName(RoleUtils.buildModifyNamespaceRoleName(appId, namespaceName));
Role namespaceReleaseRole = rolePermissionService.findRoleByRoleName(RoleUtils.buildReleaseNamespaceRoleName(appId, namespaceName));
if (namespaceModifyRole == null || namespaceReleaseRole == null) {
throw new BadRequestException("Namespace's role does not exist. Please check whether namespace has created.");
}
long namespaceModifyRoleId = namespaceModifyRole.getId();
long namespaceReleaseRoleId = namespaceReleaseRole.getId();
// 获得 Consumer 对应的 ConsumerRole 们。若都存在,返回 ConsumerRole 数组
ConsumerRole managedModifyRole = consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, namespaceModifyRoleId);
ConsumerRole managedReleaseRole = consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, namespaceReleaseRoleId);
if (managedModifyRole != null && managedReleaseRole != null) {
return Arrays.asList(managedModifyRole, managedReleaseRole);
}
// 创建 Consumer 对应的 ConsumerRole 们
String operator = userInfoHolder.getUser().getUserId();
ConsumerRole namespaceModifyConsumerRole = createConsumerRole(consumerId, namespaceModifyRoleId, operator);
ConsumerRole namespaceReleaseConsumerRole = createConsumerRole(consumerId, namespaceReleaseRoleId, operator);
// 保存 Consumer 对应的 ConsumerRole 们到数据库中
ConsumerRole createdModifyConsumerRole = consumerRoleRepository.save(namespaceModifyConsumerRole);
ConsumerRole createdReleaseConsumerRole = consumerRoleRepository.save(namespaceReleaseConsumerRole);
// 返回 ConsumerRole 数组
return Arrays.asList(createdModifyConsumerRole, createdReleaseConsumerRole);
}
4. ConsumerRolePermissionService
com.ctrip.framework.apollo.openapi.service.ConsumerRolePermissionService
,ConsumerRole 权限校验 Service 。代码如下:
@Service
public class ConsumerRolePermissionService {
@Autowired
private PermissionRepository permissionRepository;
@Autowired
private ConsumerRoleRepository consumerRoleRepository;
@Autowired
private RolePermissionRepository rolePermissionRepository;
/**
* Check whether user has the permission
*/
public boolean consumerHasPermission(long consumerId, String permissionType, String targetId) {
// 获得 Permission 对象
Permission permission = permissionRepository.findTopByPermissionTypeAndTargetId(permissionType, targetId);
// 若 Permission 不存在,返回 false
if (permission == null) {
return false;
}
// 获得 ConsumerRole 数组
List<ConsumerRole> consumerRoles = consumerRoleRepository.findByConsumerId(consumerId);
// 若数组为空,返回 false
if (CollectionUtils.isEmpty(consumerRoles)) {
return false;
}
// 获得 RolePermission 数组
Set<Long> roleIds = consumerRoles.stream().map(ConsumerRole::getRoleId).collect(Collectors.toSet());
List<RolePermission> rolePermissions = rolePermissionRepository.findByRoleIdIn(roleIds);
// 若数组为空,返回 false
if (CollectionUtils.isEmpty(rolePermissions)) {
return false;
}
// 判断是否有对应的 RolePermission 。若有,则返回 true 【有权限】
for (RolePermission rolePermission : rolePermissions) {
if (rolePermission.getPermissionId() == permission.getId()) {
return true;
}
}
return false;
}
}
- 和
DefaultRolePermissionService#userHasPermission(userId, permissionType, targetId)
方法,基本类似。
5. ConsumerPermissionValidator
ConsumerPermissionValidator 和 PermissionValidator 基本类似。
com.ctrip.framework.apollo.openapi.auth.ConsumerPermissionValidator
,Consumer 权限校验器。代码如下:
@Component
public class ConsumerPermissionValidator {
@Autowired
private ConsumerRolePermissionService permissionService;
@Autowired
private ConsumerAuthUtil consumerAuthUtil;
// ========== Namespace 级别 ==========
public boolean hasModifyNamespacePermission(HttpServletRequest request, String appId, String namespaceName) {
if (hasCreateNamespacePermission(request, appId)) {
return true;
}
return permissionService.consumerHasPermission(consumerAuthUtil.retrieveConsumerId(request),
PermissionType.MODIFY_NAMESPACE,
RoleUtils.buildNamespaceTargetId(appId, namespaceName));
}
public boolean hasReleaseNamespacePermission(HttpServletRequest request, String appId, String namespaceName) {
if (hasCreateNamespacePermission(request, appId)) {
return true;
}
return permissionService.consumerHasPermission(consumerAuthUtil.retrieveConsumerId(request),
PermissionType.RELEASE_NAMESPACE,
RoleUtils.buildNamespaceTargetId(appId, namespaceName));
}
// ========== App 级别 ==========
public boolean hasCreateNamespacePermission(HttpServletRequest request, String appId) {
return permissionService.consumerHasPermission(consumerAuthUtil.retrieveConsumerId(request),
PermissionType.CREATE_NAMESPACE,
appId);
}
}
在每个需要校验权限的方法上,添加 @PreAuthorize
注解,并在 value
属性上写 EL 表达式,调用 PermissionValidator 的校验方法。例如:
- 创建 Namespace 的方法,添加了
@PreAuthorize(value = "@consumerPermissionValidator.hasCreateNamespacePermission(#request, #appId)")
。 - 发布 Namespace 的方法,添加了
@PreAuthorize(value = "@consumerPermissionValidator.hasReleaseNamespacePermission(#request, #appId, #namespaceName)")
。
通过这样的方式,达到功能 + 数据级的权限控制。
6. ConsumerController
在 apollo-portal
项目中,com.ctrip.framework.apollo.portal.controller.ConsumerController
,提供 Consumer、ConsumerToken、ConsumerAudit 相关的 API 。
在创建第三方应用的界面中,点击【提交】按钮,调用授权 Consumer 的 API 。
代码如下:
@PreAuthorize(value = "@permissionValidator.isSuperAdmin()")
@RequestMapping(value = "/consumers/{token}/assign-role", method = RequestMethod.POST)
public List<ConsumerRole> assignNamespaceRoleToConsumer(@PathVariable String token, @RequestParam String type, @RequestBody NamespaceDTO namespace) {
String appId = namespace.getAppId();
String namespaceName = namespace.getNamespaceName();
// 校验 appId 非空。若为空,抛出 BadRequestException 异常
if (StringUtils.isEmpty(appId)) {
throw new BadRequestException("Params(AppId) can not be empty.");
}
// 授权 App 的 Role 给 Consumer
if (Objects.equals("AppRole", type)) {
return Collections.singletonList(consumerService.assignAppRoleToConsumer(token, appId));
// 授权 Namespace 的 Role 给 Consumer
} else {
if (StringUtils.isEmpty(namespaceName)) {
throw new BadRequestException("Params(NamespaceName) can not be empty.");
}
return consumerService.assignNamespaceRoleToConsumer(token, appId, namespaceName);
}
}
OpenAPI 在 v1/controller
中,实现了自己的 API ,共享调用 Portal 中的 Service 。如下图所示:
- 🙂 具体的 Controller 实现,胖友自己查看噢,笔者就不分享啦。