SpringCloud Alibaba 微服务实战十九 - 集成 RBAC 授权

本贴最后更新于 1735 天前,其中的信息可能已经渤澥桑田

概述

前面几篇文章我们一直是实现 SpringCloud 体系中的认证功能模块,验证当前登录用户的身份;本篇文章我们来讲 SpringCloud 体系中的授权功能,验证你是否能访问某些功能。

认证授权

很多同学分不清认证和授权,把他们当同一个概念来看待。其实他们是两个完全不同的概念,举个容易理解的例子:

你是张三,某知名论坛的版主。在你登录论坛的时候输入账号密码登录成功,这就证明了你是张三,这个过程叫做认证(authentication)。登录后系统判断你是版主,你可以给别人发表的帖子加亮、置顶,这个校验过程就是授权(authorization)。

简而言之,认证过程是告诉你你是谁,而授权过程是告诉你你能做什么?

在 SpringCloud 体系中实现授权一般使用以下两种方式:

  • 基于路径匹配器授权
    系统所有请求都会经过 Springcloud Gateway 网关,网关收到请求后判断当前用户是否拥有访问路径的权限,主要利用 ReactiveAuthorizationManager#check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) 方法进行校验。
    这种方法主要是基于用户拥有的资源路径进行考量。
  • 基于方法拦截
    使用这种方法在网关层不进行拦截,在需要进行权限校验的方法上加上 SpringSecurity 注解,判断当前用户是否有访问此方法的权限,当然也可以使用自定义注解或使用 AOP 进行拦截校验,这几种实现方式我们都统称为基于方法拦截。
    这种方法一般会基于用户拥有的资源标识进行考量。

接下来我们分别使用两种不同方式实现 SpringCloud 授权过程。

核心代码实现

不管是使用哪种方式我们都得先知道当前用户所拥有的角色资源,所以我们先利用 RBAC 模型建立一个简单的用户、角色、资源表结构并在项目中建立对应的 Service、Dao 层。

image.png
(资源表中建立了资源标识和请求路径两个字段,方便实现代码逻辑)

基于路径匹配器授权

  • 改造自定义 UserDetailService
    还记得我们原来自定义的 UserDetailService 吗,在 loadUserByUsername() 方法中需要返回 UserDetails 对象。
    之前我们返回的是固定的 'ADMIN' 角色,这里要改成从数据库中获取真实的角色,并将与角色对应的资源都放到 UserDetails 对象中。
@Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { //获取本地用户 SysUser sysUser = sysUserMapper.selectByUserName(userName); if(sysUser != null){ //获取当前用户的所有角色 List<SysRole> roleList = sysRoleService.listRolesByUserId(sysUser.getId()); sysUser.setRoles(roleList.stream().map(SysRole::getRoleCode).collect(Collectors.toList())); List<Integer> roleIds = roleList.stream().map(SysRole::getId).collect(Collectors.toList()); //获取所有角色的权限 List<SysPermission> permissionList = sysPermissionService.listPermissionsByRoles(roleIds); sysUser.setPermissions(permissionList.stream().map(SysPermission::getUrl).collect(Collectors.toList())); //构建oauth2的用户 return buildUserDetails(sysUser); }else{ throw new UsernameNotFoundException("用户["+userName+"]不存在"); } } /** * 构建oAuth2用户,将角色和权限赋值给用户,角色使用ROLE_作为前缀 * @param sysUser 系统用户 * @return UserDetails */ private UserDetails buildUserDetails(SysUser sysUser) { Set<String> authSet = new HashSet<>(); List<String> roles = sysUser.getRoles(); if(!CollectionUtils.isEmpty(roles)){ roles.forEach(item -> authSet.add(CloudConstant.ROLE_PREFIX + item)); authSet.addAll(sysUser.getPermissions()); } List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(authSet.toArray(new String[0])); return new User( sysUser.getUsername(), sysUser.getPassword(), authorityList ); }

注意这里是将 SysPermission::getUrl 放入用户对应权限中。

  • 改造 AccessManager 实现权限判断
@Autowired private AccessManager accessManager; @Bean SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{ ... http .httpBasic().disable() .csrf().disable() .authorizeExchange() .pathMatchers(HttpMethod.OPTIONS).permitAll() .anyExchange().access(accessManager) ... return http.build(); }

在原来网关配置中我们注入了自定义的 ReactiveAuthorizationManager 用于权限判断,我们需要实现根据请求路径与用户拥有的资源路径进行判断,若存在对应的资源访问路径则继续转发给后端服务,负责返回“没有权限访问”。

@Slf4j @Component public class AccessManager implements ReactiveAuthorizationManager<AuthorizationContext> { private Set<String> permitAll = new ConcurrentHashSet<>(); private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); public AccessManager (){ permitAll.add("/"); permitAll.add("/error"); permitAll.add("/favicon.ico"); //如果生产环境开启swagger调试 permitAll.add("/**/v2/api-docs/**"); permitAll.add("/**/swagger-resources/**"); permitAll.add("/webjars/**"); permitAll.add("/doc.html"); permitAll.add("/swagger-ui.html"); permitAll.add("/**/oauth/**"); } /** * 实现权限验证判断 */ @Override public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) { ServerWebExchange exchange = authorizationContext.getExchange(); //请求资源 String requestPath = exchange.getRequest().getURI().getPath(); // 是否直接放行 if (permitAll(requestPath)) { return Mono.just(new AuthorizationDecision(true)); } return authenticationMono.map(auth -> { return new AuthorizationDecision(checkAuthorities(auth, requestPath)); }).defaultIfEmpty(new AuthorizationDecision(false)); } /** * 校验是否属于静态资源 * @param requestPath 请求路径 * @return */ private boolean permitAll(String requestPath) { return permitAll.stream() .filter(r -> antPathMatcher.match(r, requestPath)).findFirst().isPresent(); } /** * 权限校验 * @author javadaily * @date 2020/8/4 16:47 * @param auth 用户权限 * @param requestPath 请求路径 * @return */ private boolean checkAuthorities(Authentication auth, String requestPath) { if(auth instanceof OAuth2Authentication){ Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); return authorities.stream() .map(GrantedAuthority::getAuthority) .filter(item -> !item.startsWith(CloudConstant.ROLE_PREFIX)) .anyMatch(permission -> antPathMatcher.match(permission, requestPath)); } return false; } }
  • 测试
    image.png
    查看当前用户拥有的所有权限
    image.png
    请求正常权限范围内资源
    image.png
    访问没有权限的资源

基于方法拦截实现

基于方法拦截实现在本文中是基于 SpringSecurity 内置标签 @PreAuthorize,然后通过实现自定义的校验方法 hasPrivilege()完成。再强调一遍实现方式多种多样,不一定非要采取本文的实现方式。

此方法下的代码逻辑需要写在资源服务器中,也就是提供具体业务服务的后端服务。由于每个后端服务都需要加入这些代码,所以建议抽取出公共的 starter 模块,各个资源服务器引用 starter 模块即可。

  • 改造 UserDetailService
    改造过程跟上面过程一样,只不过这里是需要将资源标识放入用户权限中。
sysUser.setPermissions( permissionList.stream() .map(SysPermission::getPermission) .collect(Collectors.toList()) );
  • 删除网关拦截配置
    由于不需要使用网关拦截,所以我们需要将 AccessManager 中的校验逻辑删除并全部返回 true。
  • 自定义方法校验逻辑
/** * 自定义权限校验 * @author http://www.javadaily.cn */ public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations { private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); public CustomMethodSecurityExpressionRoot(Authentication authentication) { super(authentication); } private Object filterObject; private Object returnObject; public boolean hasPrivilege(String permission){ Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); return authorities.stream() .map(GrantedAuthority::getAuthority) .filter(item -> !item.startsWith(CloudConstant.ROLE_PREFIX)) .anyMatch(x -> antPathMatcher.match(x, permission)); } ... }
  • 自定义方法拦截处理器
/** * @author http://www.javadaily.cn */ public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler { private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); @Override protected MethodSecurityExpressionOperations createSecurityExpressionRoot( Authentication authentication, MethodInvocation invocation) { CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication); root.setPermissionEvaluator(getPermissionEvaluator()); root.setTrustResolver(this.trustResolver); root.setRoleHierarchy(getRoleHierarchy()); return root; } }
  • 启用方法校验
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { @Override protected MethodSecurityExpressionHandler createExpressionHandler() { CustomMethodSecurityExpressionHandler expressionHandler = new CustomMethodSecurityExpressionHandler(); return expressionHandler; } }
  • 在需要权限校验的方法上加上注解
@ApiOperation("select接口") @GetMapping("/account/getByCode/{accountCode}") @PreAuthorize("hasPrivilege('queryAccount')") public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode){ log.info("get account detail,accountCode is :{}",accountCode); AccountDTO accountDTO = accountService.selectByCode(accountCode); return ResultData.success(accountDTO); }
  • 测试
    image.png
    通过 debug 可以看到这里获取到的用户权限是资源表中的资源标识。

小结

个人觉得在 SpringCloud 微服务架构中最复杂的一个模块就是用户的认证授权模块,本文通过两种实现方法解决了授权问题,解决你能做什么的问题。大家可以根据实际业务场景选择具体的实现方式,当然了个人还是建议使用第一种基于路径匹配器授权的方式,只需要在网关层进行拦截即可。

SpringCloud alibab 实战系列文章目前是第 21 篇,如果大家对之前的文章感兴趣可以移步至 http://javadaily.cn/tags/SpringCloud 查看。

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    948 引用 • 1460 回帖 • 1 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    143 引用 • 442 回帖
  • 微服务

    微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。

    96 引用 • 155 回帖 • 4 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • JssDream

    shiro 用着舒服trollface

    1 回复
  • jianzh5
    作者

    嗯嗯,都可以~看技术选型,我们也有项目用 shiro