2023-02-27
原文作者:Java日知录 原文地址:https://javadaily.cn

前面文章咱们对比过网关授权与微服务授权的区别,文章也提到了,如果要实现微服务授权,一般会构建一个独立的资源服务器配置模块,否则每个后端业务都需要进行资源服务器的配置,那本节内容我们就来完成此功能。

由于间隔时间较久,建议先阅读下面两篇相关文章回顾一下。

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

SpringCloud Alibaba微服务实战二十八 - 网关授权VS微服务授权

话不多说,我们直接开始代码改造。

认证服务器改造

首先我们需要改造认证服务器,需要认证服务器在构建用户权限的时候使用的是权限标识字段。对于代码而言只需要 UserDetailServiceImpl#loadUserByUsername()中修改即可。

    @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);
    
      //基于方法拦截.只需放入用户权限标识即可
      List<String> permissionMethodList = permissionList.stream()
        .map(SysPermission::getPermission)
        .collect(Collectors.toList());
      sysUser.setPermissions(permissionMethodList);
      //构建oauth2的用户
      return buildUserDetails(sysUser);
    
     }else{
      throw  new UsernameNotFoundException("用户["+userName+"]不存在");
     }
    }

网关改造

网关服务器不再需要进行用户权限校验,所以我们需要将相关校验逻辑全部删除。

    @Configuration
    public class SecurityConfig {
        @Bean
        SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{
    
            http
       .httpBasic().disable()
       .csrf().disable();
    
            return http.build();
        }
    }

独立资源服务器配置模块

完成了上面两步后就到了最重要的步骤了,需要建立一个独立的资源服务器配置模块,用于其他模块引用。

首先我们得建立一个单独的资源服务模块 cloud-component-security-starter ,如下为改造后的代码结构图。

202302272302383101.png

然后,要让一个普通后端服务成为资源服务器,需要有一个配置类继承 ResourceServerConfigurerAdapter并进行相关配置,那在我们独立的资源服务器模块我们首先得创建一个这样的配置类,这个比较简单,只需从之前的模块中拷贝一份出来。

    public class CloudResourceServerConfigure extends ResourceServerConfigurerAdapter {
        private CustomAccessDeniedHandler accessDeniedHandler;
        private CustomAuthenticationEntryPoint exceptionEntryPoint;
    
        private TokenStore tokenStore;
    
        @Value("${security.oauth2.resource.id}")
        private String resourceId ;
    
        @Autowired(required = false)
        public void setAccessDeniedHandler(CustomAccessDeniedHandler accessDeniedHandler) {
            this.accessDeniedHandler = accessDeniedHandler;
        }
    
        @Autowired(required = false)
        public void setExceptionEntryPoint(CustomAuthenticationEntryPoint exceptionEntryPoint) {
            this.exceptionEntryPoint = exceptionEntryPoint;
        }
    
        @Autowired(required = false)
        public void setTokenStore(TokenStore tokenStore) {
            this.tokenStore = tokenStore;
        }
    
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                    .antMatchers(
                            "/v2/api-docs/**",
                            "/swagger-resources/**",
                            "/swagger-ui.html",
                            "/webjars/**"
                    ).permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .csrf().disable();
        }
    
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
            UserAuthenticationConverter userTokenConverter = new CustomUserAuthenticationConverter();
            accessTokenConverter.setUserTokenConverter(userTokenConverter);
    
            if (exceptionEntryPoint != null) {
                resources.authenticationEntryPoint(exceptionEntryPoint);
            }
            if (accessDeniedHandler != null) {
                resources.accessDeniedHandler(accessDeniedHandler);
            }
    
            resources.resourceId(resourceId).tokenStore(tokenStore);
        }
      
    }

现在有了资源服务器配置,那其他模块如何引入这个配置类呢?

这里我们可以借助SpringBoot的Enable模块驱动能力,通过@EnableXXX注解导入配置类。

我们创建一个自定义注解类 EnableCloudResourceServer,其他模块通过 @EnableCloudResourceServer注解即可导入资源服务器配置

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @EnableResourceServer //开启资源服务器
    @Import({CloudResourceServerConfigure.class, TokenStoreConfigure.class})
    public @interface EnableCloudResourceServer {
    
    }

最后我们知道微服务授权是基于方法拦截,基于方法拦截我们就需要开启 @EnableGlobalMethodSecurity,并且需要将我们自定义的权限注解功能迁移过来。所以我们再创建一个配置类用于配置上述功能。

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class CloudSecurityAutoConfigure extends GlobalMethodSecurityConfiguration {
    
        @Bean
        @ConditionalOnMissingBean(name = "accessDeniedHandler")
        public CustomAccessDeniedHandler accessDeniedHandler() {
            return new CustomAccessDeniedHandler();
        }
    
        @Bean
        @ConditionalOnMissingBean(name = "authenticationEntryPoint")
        public CustomAuthenticationEntryPoint authenticationEntryPoint() {
            return new CustomAuthenticationEntryPoint();
        }
    
        @Override
        protected MethodSecurityExpressionHandler createExpressionHandler() {
            return new CustomMethodSecurityExpressionHandler();
        }
    
    }

经过上面的改造,一个独立的资源服务器创建成功了,现在剩下的就是对微服务的改造。

微服务改造

  • 在maven中删除原oauth2.0的相关配置,引入自定义cloud-component-security-starter
    <dependency>
     <groupId>com.jianzh5.cloud</groupId>
     <artifactId>cloud-component-security-starter</artifactId>
    </dependency>
  • 删除所有资源服务器相关代码(此过程略)

  • 修改主启动类,通过@EnableCloudResourceServer引入资源服务器配置

    @EnableDiscoveryClient
    @SpringCloudApplication
    @EnableCloudResourceServer
    public class AccountServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(AccountServiceApplication.class, args);
        }
    }
  • 在需要拦截的Controller方法中添加自定义权限拦截注解@PreAuthorize("hasPrivilege('queryAccount')")当然也可以使用SpringSecurity原生注解 @PreAuthorize("hasAuthority('queryAccount')") ,两者作用一样。
    @GetMapping("/account/getByCode/{accountCode}")
    @PreAuthorize("hasPrivilege('queryAccount')")
    //@PreAuthorize("hasAuthority('queryAccount')")
    public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode){
     AccountDTO accountDTO = accountService.selectByCode(accountCode);
     return ResultData.success(accountDTO);
    }

测试

我们访问一个没有权限的方法会出现如下错误提示,表明独立资源服务器成功配置

202302272302391822.png

    
    {
      "status": 500,
      "message": "不允许访问",
      "data": null,
      "success": false,
      "timestamp": 1619052359563
    }

提示:@PreAuthorize 注解的异常,抛出AccessDeniedException异常,不会被accessDeniedHandler捕获,而是会被全局异常捕获。

如果需要自定义 @PreAuthorize错误异常,可以通过全局的 @RestControllerAdvice进行异常拦截

拦截后的自定义异常如下:

202302272302398593.png

    {
      "status": 2003,
      "message": "没有权限访问该资源",
      "data": null,
      "success": false,
      "timestamp": 1619052359563
    }
阅读全文