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