微服务之间的通讯安全(三)-JWT优化之权限控制

上节我们使用JWT优化了认证机制,通过令牌可以解析出当前用户是谁,并且这个令牌可以在网关到微服务,微服务和微服务之间传递,现在我们来看一下权限的控制

1、简单的ACL控制

最简单的情况就是ACL(访问控制列表),能干什么都在scope里面,但是scope是针对客户端应用的,无法控制各个用户可以做什么,可以使用用户里的authorities来进行判断。我们只需要在程序里判断一下,访问这个方法有没有相应的权限就可以了。可以在springsecurity中进行配置,也可以使用注解,这里我们使用注解。

1.1、添加@EnableGlobalMethodSecurity注解并开启prePostEnabled

/**
 * 订单微服务
 *
 * @author caofanqi
 * @date 2020/1/31 14:22
 */
@EnableResourceServer
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OrderApiApplication {


    public static void main(String[] args) {
        SpringApplication.run(OrderApiApplication.class, args);
    }

    /**
     * 将OAuth2RestTemplate声明为spring bean,OAuth2ProtectedResourceDetails,OAuth2ClientContext springboot会自动帮我们注入
     */
    @Bean
    public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
        return new OAuth2RestTemplate(resource, context);
    }

}

1.2、在方法上可以使用@PreAuthorize注解进行方法前权限校验,下面的创建订单方法,需要令牌具有fly权限并且用户的角色是ROLE_ADMIN。

    @PostMapping
    @PreAuthorize("#oauth2.hasScope('write') and hasRole('ROLE_ADMIN')")
    public OrderDTO create(@RequestBody OrderDTO orderDTO, @AuthenticationPrincipal String username) {
        log.info("username is :{}", username);
        PriceDTO price = oAuth2RestTemplate.getForObject("http://127.0.0.1:9070/prices/" + orderDTO.getProductId(), PriceDTO.class);
        log.info("price is : {}", price.getPrice());
        return orderDTO;
    }

1.3、启动各项目进行测试

  1.3.1、使用scope为read,write,并且用户的authorities里有ROLE_ADMIN,可以正常访问。

  1.3.2、将@PreAuthorize("#oauth2.hasScope('write') and hasRole('ROLE_ADMIN')") 修改为@PreAuthorize("#oauth2.hasScope('fly') and hasRole('ROLE_ADMIN')")或@PreAuthorize("#oauth2.hasScope('write') and hasRole('ROLE_USER')"),继续使用该令牌进行访问,均没有权限。

2、在网关上做复杂的权限控制

  上面直接在方法上控制权限,适用于比较简单的场景,只有几个角色,就可以实现权限控制。但是没有办法应付复杂的场景,比如说用户的角色权限是动态变化的,上面的方式就无法进行控制了。下面我们来看一下,要实时的去做授权怎么来做。

一般,我们对于复杂的权限控制,都是会有一个权限服务的,和订单服务、库存服务一样都是一个微服务;前端会有相关的页面可以对用户的角色权限实时进行修改,配置到数据库中,在同步到redis中,权限系统可以提供一个接口,用来来判断当前请求是否有相应的权限。请求发到网关经过校验令牌之后,网关去校验该请求的权限,有权限就转发,没有权限就返回。我们把认证服务器和权限服务整个一块称为安全中心。

2.1、在网关上写一个权限控制服务,可以去安全中心进行权限判断。这里我们就不去实现权限服务了,使用随机数替代。

/**
 * 权限控制
 * @author caofanqi
 * @date 2020/2/9 14:48
 */
public interface PermissionService {

    /**
     * 判断当前请求是否有权限
     * @param request 请求
     * @param authentication 认证相关信息
     * @return boolean
     */
    boolean hasPermission(HttpServletRequest request, Authentication authentication);

}

/**
 * 权限控制实现类
 *
 * @author caofanqi
 * @date 2020/2/9 14:51
 */
@Slf4j
@Service
public class PermissionServiceImpl implements PermissionService {

    /**
     * 在这里可以去安全中心,获取该请求是否具有相应的权限
     *
     * @param request        请求
     * @param authentication 认证相关信息
     */
    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {

        //这里我们就不写具体的权限判断了,采用随机数模拟,百分之50的机率可以访问
        log.info("request uri : {}", request.getRequestURI());
        log.info("authentication : {}", ReflectionToStringBuilder.toString(authentication));
        boolean  hasPermission =  RandomUtils.nextInt() % 2 == 0;
        log.info("hasPermission is :{}",hasPermission);
        return hasPermission;
    }

}

2.2、将我们的服务添加到评估上下文中,使其可以被解析。在表达式中写permissionService时可以找到该bean。

/**
 * 将权限服务表达式添加到评估上下文中
 *
 * @author caofanqi
 * @date 2020/2/9 14:58
 */
@Component
public class GatewayWebSecurityExpressionHandler extends OAuth2WebSecurityExpressionHandler {

    @Resource
    private PermissionService permissionService;

    @Override
    protected StandardEvaluationContext createEvaluationContextInternal(Authentication authentication, FilterInvocation invocation) {
        StandardEvaluationContext sc = super.createEvaluationContextInternal(authentication, invocation);
        sc.setVariable("permissionService",permissionService);
        return sc;
    }
    
}

2.3、在网关资源服务器配置类中进行配置

/**
 * 网关资源服务器配置
 *
 * @author caofanqi
 * @date 2020/2/8 22:30
 */
@Configuration
@EnableResourceServer
public class GatewayResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Resource
    private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId("gateway")
                //表达式处理器
                .expressionHandler(gatewayWebSecurityExpressionHandler);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                //放过申请令牌的请求不需要身份认证
                .antMatchers("/token/**").permitAll()
                //其他所有请求是否有权限,要通过permissionService的hasPermission方法进行判断
                .anyRequest().access("#permissionService.hasPermission(request,authentication)");
    }

}

2.4、启动各项目进行创建订单测试

  可以正常访问时,gateway控制台打印

  没有权限访问403时,gateway控制台打印

   这说明权限控制已经是通过我们的PermissionService来控制的了。

3、微服务之间的权限控制

  如果权限控制都在网关上管理,那么有可能就会出现,用户A可以访问订单服务,但是不能访问库存服务。但是用户A在访问订单服务的时候,订单服务可以访问库存服务,这样的话,就越权了。这个场景怎么解决呢?可以在每个微服务上都去调用安全中心判断权限,和网关上做法差不多,但是不建议这么做。

  推荐的做法是,细粒度的权限,每个请求可不可以访问在网关上做,可以覆盖95%-99%的权限。到了后面的微服务与微服务之间,我们只需要做粗粒度的黑白名单控制即可。比如所我们有一个跟钱相关的结算服务,可以控制只能订单服务来调用,其他服务不可以调用。后面学习sentinel进行控制。

 项目源码:https://github.com/caofanqi/study-security/tree/dev-jwt-permission

 

原文地址:https://www.cnblogs.com/caofanqi/p/12288634.html