微服务之间的通讯安全(三)-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); } }
@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; } }
/** * 网关资源服务器配置 * * @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在访问订单服务的时候,订单服务可以访问库存服务,这样的话,就越权了。这个场景怎么解决呢?可以在每个微服务上都去调用安全中心判断权限,和网关上做法差不多,但是不建议这么做。
项目源码:https://github.com/caofanqi/study-security/tree/dev-jwt-permission