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

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

概述


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


认证授权

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

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

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

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

  • 基于路径匹配器授权
    系统所有请求都会经过Springcloud Gateway 网关,网关收到请求后判断当前用户是否拥有访问路径的权限,主要利用 ReactiveAuthorizationManager#check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) 方法进行校验。

    这种方法主要是基于用户拥有的资源路径进行考量。

  • 基于方法拦截
    使用这种方法在网关层不进行拦截,在需要进行权限校验的方法上加上SpringSecurity注解,判断当前用户是否有访问此方法的权限,当然也可以使用自定义注解或使用AOP进行拦截校验,这几种实现方式我们都统称为基于方法拦截。

    这种方法一般会基于用户拥有的资源标识进行考量。

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


核心代码实现

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

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


基于路径匹配器授权

  • 改造自定义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 http://www.javadaily.cn
     * @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;
    }
}
  • 测试
  •          查看当前用户拥有的所有权限          请求正常权限范围内资源    访问没有权限的资源

基于方法拦截实现

基于方法拦截实现在本文中是基于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);
}
  • 测试
  • 通过debug可以看到这里获取到的用户权限是资源表中的资源标识。

小结


个人觉得在SpringCloud微服务架构中最复杂的一个模块就是用户的认证授权模块,本文通过两种实现方法解决了授权问题,解决你能做什么的问题。


大家可以根据实际业务场景选择具体的实现方式,当然了个人还是建议使用第一种基于路径匹配器授权的方式,只需要在网关层进行拦截即可。

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


如果本文对你有帮助,别忘记给我个三连:点赞,转发,评论咱们下期见!

收藏 等于白嫖点赞 才是真情!

目录
相关文章
|
8月前
|
数据可视化 Java BI
将 Spring 微服务与 BI 工具集成:最佳实践
本文探讨了 Spring 微服务与商业智能(BI)工具集成的潜力与实践。随着微服务架构和数据分析需求的增长,Spring Boot 和 Spring Cloud 提供了构建可扩展、弹性服务的框架,而 BI 工具则增强了数据可视化与实时分析能力。文章介绍了 Spring 微服务的核心概念、BI 工具在企业中的作用,并深入分析了两者集成带来的优势,如实时数据处理、个性化报告、数据聚合与安全保障。同时,文中还总结了集成过程中的最佳实践,包括事件驱动架构、集中配置管理、数据安全控制、模块化设计与持续优化策略,旨在帮助企业构建高效、智能的数据驱动系统。
411 1
将 Spring 微服务与 BI 工具集成:最佳实践
|
9月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1367 3
|
7月前
|
Cloud Native Serverless API
微服务架构实战指南:从单体应用到云原生的蜕变之路
🌟蒋星熠Jaxonic,代码为舟的星际旅人。深耕微服务架构,擅以DDD拆分服务、构建高可用通信与治理体系。分享从单体到云原生的实战经验,探索技术演进的无限可能。
微服务架构实战指南:从单体应用到云原生的蜕变之路
|
11月前
|
缓存 负载均衡 监控
微服务架构下的电商API接口设计:策略、方法与实战案例
本文探讨了微服务架构下的电商API接口设计,旨在打造高效、灵活与可扩展的电商系统。通过服务拆分(如商品、订单、支付等模块)和标准化设计(RESTful或GraphQL风格),确保接口一致性与易用性。同时,采用缓存策略、负载均衡及限流技术优化性能,并借助Prometheus等工具实现监控与日志管理。微服务架构的优势在于支持敏捷开发、高并发处理和独立部署,满足电商业务快速迭代需求。未来,电商API设计将向智能化与安全化方向发展。
564 102
|
7月前
|
监控 Cloud Native Java
Spring Boot 3.x 微服务架构实战指南
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Spring Boot 3.x与微服务架构,探索云原生、性能优化与高可用系统设计。以代码为笔,在二进制星河中谱写极客诗篇。关注我,共赴技术星辰大海!(238字)
1269 2
Spring Boot 3.x 微服务架构实战指南
|
9月前
|
负载均衡 监控 Java
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
在微服务架构中,高可用与稳定性至关重要。本文详解熔断、限流与负载均衡三大关键技术,结合API网关与Hystrix-Go实战,帮助构建健壮、弹性的微服务系统。
891 1
微服务稳定性三板斧:熔断、限流与负载均衡全面解析(附 Hystrix-Go 实战代码)
|
11月前
|
NoSQL Java 微服务
2025 年最新 Java 面试从基础到微服务实战指南全解析
《Java面试实战指南:高并发与微服务架构解析》 本文针对Java开发者提供2025版面试技术要点,涵盖高并发电商系统设计、微服务架构实现及性能优化方案。核心内容包括:1)基于Spring Cloud和云原生技术的系统架构设计;2)JWT认证、Seata分布式事务等核心模块代码实现;3)数据库查询优化与高并发处理方案,响应时间从500ms优化至80ms;4)微服务调用可靠性保障方案。文章通过实战案例展现Java最新技术栈(Java 17/Spring Boot 3.2)的应用.
933 9
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
865 6
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
453 1
|
安全 应用服务中间件 API
微服务分布式系统架构之zookeeper与dubbo-2
微服务分布式系统架构之zookeeper与dubbo-2