聊聊網(wǎng)關(guān)Restful接口攔截
本文轉(zhuǎn)載自微信公眾號「JAVA日知錄」,作者單一色調(diào)。轉(zhuǎn)載本文請聯(lián)系JAVA日知錄公眾號。
前言
之前在 集成RBAC授權(quán) 的文章中提到了SpringCloud可以「基于路徑匹配器授權(quán)」在網(wǎng)關(guān)層進行用戶權(quán)限校驗,這種方式的實現(xiàn)原理是Springcloud Gateway接受到請求后根據(jù) ReactiveAuthorizationManager#check(Mono
具體實現(xiàn)方式在上面文章中有闡述,如果有不清楚的可以再次查閱。文章地址:
http://javadaily.cn/articles/2020/08/07/1596772909329.html
不過之前的實現(xiàn)方式有個問題,就是不支持restful風(fēng)格的url路徑。
例如一個微服務(wù)有如下API
- GET /v1/pb/user
- POST /v1/pb/user
- PUT /v1/pb/user
這樣在網(wǎng)關(guān)通過 request.getURI().getPath()方法獲取到用戶請求路徑的時候都是同一個地址,給一個用戶授予 /v1/pb/user權(quán)限后他就擁有了 GET、PUT、POST三種不同權(quán)限,很顯然這樣不能滿足精細權(quán)限控制。本章內(nèi)容我們就來解決這個Restful接口攔截的問題,使其能支持精細化的權(quán)限控制。
場景演示
我們看下實際的案例,演示下這種場景。在 account-service模塊下增加一個博客用戶管理功能,有如下的接口方法:
接口URL | HTTP方法 | 接口說明 |
---|---|---|
/blog/user | POST | 保存用戶 |
/blog/user/{id} | GET | 查詢用戶 |
/blog/user/{id} | DELETE | 刪除用戶 |
/blog/user/{id} | PUT | 更新用戶信息 |
然后我們在 sys_permission表中添加2個用戶權(quán)限,再將其授予給用戶角色
在網(wǎng)關(guān)層的校驗方法中可以看到已經(jīng)增加了2個權(quán)限
由于DELETE 和 PUT對應(yīng)的權(quán)限路徑都是 /blog/user/{id},這樣就是當給用戶授予了查詢權(quán)限后此用戶也擁有了刪除和更新的權(quán)限。
解決方案
看到這里大部分同學(xué)應(yīng)該想到了,要想實現(xiàn)Restful風(fēng)格的精細化權(quán)限管理單單通過URL路徑是不行的,需要搭配Method一起使用。
最關(guān)鍵的點就是「需要給權(quán)限表加上方法字段,然后在網(wǎng)關(guān)校驗的時候即判斷請求路徑又匹配請求方法?!?實現(xiàn)步驟如下:
修改權(quán)限表,新增方法字段
- 在loadUserByUsername()方法構(gòu)建用戶權(quán)限的時候?qū)?quán)限對應(yīng)的Method也拼接在權(quán)限上,關(guān)鍵代碼如下:
- @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());
- //獲取所有角色的權(quán)限
- List<SysPermission> permissionList = sysPermissionService.listPermissionsByRoles(roleIds);
- //拼接method
- List<String> permissionUrlList = permissionList.stream()
- .map(item -> "["+item.getMethod()+"]"+item.getUrl())
- .collect(Collectors.toList());
- sysUser.setPermissions(permissionUrlList);
- //構(gòu)建oauth2的用戶
- return buildUserDetails(sysUser);
- }else{
- throw new UsernameNotFoundException("用戶["+userName+"]不存在");
- }
- }
通過上面的代碼構(gòu)建的用戶權(quán)限如下:
- [GET]/account-service/blog/user/{id}
- [POST]/account-service/blog/user
可以通過代碼調(diào)試查看:
- 權(quán)限校驗方法AccessManager#check(),校驗[MEHOTD]RequestPath 格式
@Override
- @Override
- public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) {
- ServerWebExchange exchange = authorizationContext.getExchange();
- ServerHttpRequest request = exchange.getRequest();
- //請求資源
- String requestPath = request.getURI().getPath();
- //拼接method
- String methodPath = "["+request.getMethod()+"]" + requestPath;
- // 1. 對應(yīng)跨域的預(yù)檢請求直接放行
- if(request.getMethod() == HttpMethod.OPTIONS){
- return Mono.just(new AuthorizationDecision(true));
- }
- // 是否直接放行
- if (permitAll(requestPath)) {
- return Mono.just(new AuthorizationDecision(true));
- }
- return authenticationMono.map(auth -> new AuthorizationDecision(checkAuthorities(auth, methodPath)))
- .defaultIfEmpty(new AuthorizationDecision(false));
- }
校驗方法 checkAuthorities():
- private boolean checkAuthorities(Authentication auth, String requestPath) {
- if(auth instanceof OAuth2Authentication){
- OAuth2Authentication authentication = (OAuth2Authentication) auth;
- String clientId = authentication.getOAuth2Request().getClientId();
- log.info("clientId is {}",clientId);
- //用戶的權(quán)限集合
- Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
- return authorities.stream()
- .map(GrantedAuthority::getAuthority)
- //ROLE_開頭的為角色,需要過濾掉
- .filter(item -> !item.startsWith(CloudConstant.ROLE_PREFIX))
- .anyMatch(permission -> ANT_PATH_MATCHER.match(permission, requestPath));
- }
- return true;
- }
- 這樣當請求Delete方法時就會提示沒有權(quán)限
這里還有另外一種方案,實現(xiàn)的原理跟上面差不多,只簡單提一下。
首先還是得在權(quán)限表中新增METHOD字段,這是必須的。
然后項目中使用的權(quán)限類是 SimpleGrantedAuthority,這個只能存儲一個權(quán)限字段,我們可以自定義一個權(quán)限實體類,讓其可以存儲url 和 method。
- @Data
- public class MethodGrantedAuthority implements GrantedAuthority {
- private String method;
- private String url;
- public MethodGrantedAuthority(String method, String url){
- this.method = method;
- this.url = url;
- }
- @Override
- public String getAuthority() {
- return "["+method+"]" + url;
- }
- }
在 UserDetailServiceImpl中構(gòu)建用戶權(quán)限時使用自定義的 MethodGrantedAuthority
網(wǎng)關(guān)層校驗的方法還是需要跟上面一樣,既校驗Method 又 校驗 URL。