前章講了如何進(jìn)行用戶權(quán)限驗證《SpringCloud下的用戶鑒權(quán)方案》了讨,它是微服務(wù)下統(tǒng)一資源訪問權(quán)限的控制楔敌,就像一道墻保護(hù)著SpringCloud
集群下的各個業(yè)務(wù)應(yīng)用服務(wù)贫奠。而本章要講的是權(quán)限控制的另一個層面數(shù)據(jù)權(quán)限
温数,意思是控制可訪問數(shù)據(jù)資源的數(shù)量。
舉個例子:
有一批業(yè)務(wù)員跟進(jìn)全國的銷售訂單也祠。他們被按城市進(jìn)行劃分乒躺,一個業(yè)務(wù)員跟進(jìn)3個城市的訂單,為了保護(hù)公司的業(yè)務(wù)數(shù)據(jù)不能被所有人都掌握扭屁,故每個業(yè)務(wù)員只能看到自己負(fù)責(zé)城市的訂單數(shù)據(jù)算谈。所以從系統(tǒng)來講每個業(yè)務(wù)員都有訪問銷售訂單的功能,然后再需要配置每個業(yè)務(wù)員負(fù)責(zé)的城市疯搅,以此對訂單數(shù)據(jù)進(jìn)行篩選濒生。
要實現(xiàn)此功能有很多方法,如果系統(tǒng)中多個地方都需要類似的需求幔欧,那我們就可以將其提出來做成一個通用的功能罪治。這里我介紹一個相對簡單的解決方案,以供參考礁蔗。
一觉义、 整體架構(gòu)
數(shù)據(jù)權(quán)限
為作一個注解
的形式掛在每一個需要數(shù)據(jù)權(quán)限控制的Controller上,由于和具體的程序邏輯有關(guān)故有一定的入侵性浴井,且需要數(shù)據(jù)庫配合使用晒骇。
二、 實現(xiàn)流程
- 瀏覽器傳
帶查詢權(quán)限范圍參數(shù)
訪問Controller磺浙,如cities
POST http://127.0.0.1:8000/order/query
accept: */*
Content-Type: application/json
token: 1e2b2298-8274-4599-a26f-a799167cc82f
{"cities":["cq","cd","bj"],"userName":"string"}
-
通過注解攔截權(quán)限范圍參數(shù)洪囤,并根據(jù)預(yù)授權(quán)范圍比較,回寫在授權(quán)范圍內(nèi)的權(quán)限范圍參數(shù)
cities = ["cq","cd"]
-
通過參數(shù)傳遞到DAO層,在SQL語句中拼裝出查詢條件,實現(xiàn)數(shù)據(jù)的過濾
select * from order where city in ('cq','cd')
三击吱、 實現(xiàn)步驟
1. 注解實現(xiàn)
注解的完整代碼叁幢,請詳見源代碼
1)創(chuàng)建注解
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface ScopeAuth {
String token() default "AUTH_TOKEN";
String scope() default "";
String[] scopes() default {};
}
此注解為運行時RetentionPolicy.RUNTIME
作用在方法上ElementType.METHOD
的
token
:獲取識別唯一用戶的標(biāo)識袒炉,與用戶數(shù)據(jù)權(quán)限存儲有關(guān)
scope
,scopes
:預(yù)請求的數(shù)據(jù)權(quán)限范圍
2) AOP實現(xiàn)注解
public class ScopeAuthAdvice {
@Around("@annotation(scopeAuth)")
public Object before(ProceedingJoinPoint thisJoinPoint, ScopeAuth scopeAuth) throws Throwable {
// ... 省略過程
// 獲取token
String authToken = getToken(args, scopeAuth.token(), methodSignature.getMethod());
// 回寫范圍參數(shù)
setScope(scopeAuth.scope(), methodSignature, args, authToken);
return thisJoinPoint.proceed();
}
/**
* 設(shè)置范圍
*/
private void setScope(String scope, MethodSignature methodSignature, Object[] args, String authToken) {
// 獲取請求范圍
Set<String> requestScope = getRequestScope(args, scope, methodSignature.getMethod());
ScopeAuthAdapter adapter = new ScopeAuthAdapter(supplier);
// 已授權(quán)范圍
Set<String> authorizedScope = adapter.identifyPermissionScope(authToken, requestScope);
// 回寫新范圍
setRequestScope(args, scope, authorizedScope, methodSignature.getMethod());
}
/**
* 回寫請求范圍
*/
private void setRequestScope(Object[] args, String scopeName, Collection<String> scopeValues, Method method) {
// 解析 SPEL 表達(dá)式
if (scopeName.indexOf(SPEL_FLAG) == 0) {
ParseSPEL.setMethodValue(scopeName, scopeValues, method, args);
}
}
}
此為演示代碼省略了過程,主要功能為通過token拿到預(yù)先授權(quán)的數(shù)據(jù)范圍,再與本次請求的范圍做交集府怯,最后回寫回原參數(shù)。
過程中用到了較多的SPEL表達(dá)式
防楷,用于計算表達(dá)式結(jié)果牺丙,具體請參考ParseSPEL文件
3)權(quán)限范圍交集計算
public class ScopeAuthAdapter {
private final AuthQuerySupplier supplier;
public ScopeAuthAdapter(AuthQuerySupplier supplier) {
this.supplier = supplier;
}
/**
* 驗證權(quán)限范圍
* @param token
* @param requestScope
* @return
*/
public Set<String> identifyPermissionScope(String token, Set<String> requestScope) {
Set<String> authorizeScope = supplier.queryScope(token);
String ALL_SCOPE = "AUTH_ALL";
String USER_ALL = "USER_ALL";
if (authorizeScope == null) {
return null;
}
if (authorizeScope.contains(ALL_SCOPE)) {
// 如果是全開放則返回請求范圍
return requestScope;
}
if (requestScope == null) {
return null;
}
if (requestScope.contains(USER_ALL)){
// 所有授權(quán)的范圍
return authorizeScope;
}
// 移除不同的元素
requestScope.retainAll(authorizeScope);
return requestScope;
}
}
此處為了方便設(shè)置,有兩個關(guān)鍵字范圍
-
AUTH_ALL
:預(yù)設(shè)所有范圍复局,全開放的意思赘被,為數(shù)據(jù)庫預(yù)先設(shè)置值是整,請求傳什么值都通過 -
USER_ALL
:請求所有授權(quán)的范圍,請求時傳此值則會以數(shù)據(jù)庫預(yù)設(shè)值為準(zhǔn)
4) spring.factories自動導(dǎo)入類配置
org.springframework.boot.autoconfigure.AutoConfigurationImportSelector=\
fun.barryhome.cloud.annotation.ScopeAuthAdvice
如果注解功能是單獨項目存在民假,在使用時有可能會存在找不到引入文件的問題,可通過此配置文件自動載入需要初始化的類
2. 注解使用
@ScopeAuth(scopes = {"#orderDTO.cities"}, token = "#request.getHeader(\"X-User-Name\")")
@PostMapping(value = "/query")
public String query(@RequestBody OrderDTO orderDTO, HttpServletRequest request) {
return Arrays.toString(orderDTO.getCities());
}
在需要使用數(shù)據(jù)權(quán)限的controller方法上增加@ScopeAuth
注解
scopes = {"#orderDTO.cities"}
:表示取輸入?yún)?shù)orderDTO的cities值龙优,這里是表達(dá)式必須加#
實際開發(fā)過程中羊异,需要將orderDTO.getCities()帶入后續(xù)邏輯中,在DAO層將此拼裝在SQL中彤断,以實現(xiàn)數(shù)據(jù)過濾功能
3. 實現(xiàn)AuthStoreSupplier
AuthStoreSupplier
接口為數(shù)據(jù)權(quán)限的存儲接口野舶,與AuthQuerySupplier配合使用,可按實際情況實現(xiàn)
此接口為非必要接口宰衙,可由數(shù)據(jù)庫或Redis存儲(推薦)平道,一般在登錄的同時保存在Redis
中
4. 實現(xiàn)AuthQuerySupplier
AuthQuerySupplier
接口為數(shù)據(jù)權(quán)限查詢接口,可按存儲方法進(jìn)行查詢供炼,推薦使用Redis
@Component
public class RedisAuthQuerySupplier implements AuthQuerySupplier {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 查詢范圍
*/
@Override
public Set<String> queryScope(String key) {
String AUTH_USER_KEY = "auth:logic:user:%s";
String redisKey = String.format(AUTH_USER_KEY, key);
List<String> range = redisTemplate.opsForList().range(redisKey, 0, -1);
if (range != null) {
return new HashSet<>(range);
} else {
return null;
}
}
}
在分布式結(jié)構(gòu)里一屋,也可將此實現(xiàn)提出到權(quán)限模塊,采用遠(yuǎn)程調(diào)用方式袋哼,進(jìn)一步解耦
5. 開啟數(shù)據(jù)權(quán)限
@EnableScopeAuth
@EnableDiscoveryClient
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
四冀墨、 綜述
至此數(shù)據(jù)權(quán)限功能就實現(xiàn)了。在微服務(wù)器架構(gòu)中為了實現(xiàn)功能的復(fù)用涛贯,將注解的創(chuàng)建和AuthQuerySupplier的實現(xiàn)提取到公共模塊中诽嘉,那么在具體的使用模塊就簡單得多了。只需增加@ScopeAuth
注解弟翘,配置好查詢方法就可以使用虫腋。
五、源代碼
文中代碼由于篇幅原因有一定省略并不是完整邏輯稀余,如有興趣請F(tuán)ork源代碼
https://gitee.com/hypier/barry-cloud/tree/master/cloud-auth-logic