上節我們使用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