『壹』 SpringCloud微服務實現數據許可權控制
前章講了如何進行用戶許可權驗證,它是微服務下統一資源訪問許可權的控制,就像一道牆保護著SpringCloud集群下的各個業務應用服務。而本章要講的是許可權控制的另一個層面數據許可權,意思是控制可訪問數據資源的數量。
舉個例子:
有一批業務員跟進全國的銷售訂單。他們被按城市進行劃分,一個業務員跟進3個城市的訂單,為了保護公司的業務數據不能被所有人都掌握,故每個業務員只能看到自己負責城市的訂單數據。所以從系統來講每個業務員都有訪問銷售訂單的功能,然後再需要配置每個業務員負責的城市,以此對訂單數據進行篩選。
要實現此功能有很多方法,如果系統中多個地方都需要類似的需求,那我們就可以將其提出來做成一個通用的功能。這里我介紹一個相對簡單的解決方案,以供參考。
一、整體架構數據許可權為作一個註解的形式掛在每一個需要數據許可權控制的Controller上,由於和具體的程序邏輯有關故有一定的入侵性,且需要資料庫配合使用。
二、實現流程瀏覽器傳帶查詢許可權范圍參數訪問Controller,如cities
POSThttp://127.0.0.1:8000/order/queryaccept:*/*Content-Type:application/jsontoken:1e2b2298-8274-4599-a26f-a799167cc82f{"cities":["cq","cd","bj"],"userName":"string"}通過註解攔截許可權范圍參數,並根據預授權范圍比較,回寫在授權范圍內的許可權范圍參數
cities=["cq","cd"]通過參數傳遞到DAO層,在SQL語句中拼裝出查詢條件,實現數據的過濾
select*fromorderwherecityin('cq','cd')三、實現步驟1.註解實現註解的完整代碼,請詳見源代碼
1)創建註解@Retention(value=RetentionPolicy.RUNTIME)@Target(value={ElementType.METHOD})@Documentedpublic@interfaceScopeAuth{Stringtoken()default"AUTH_TOKEN";Stringscope()default"";String[]scopes()default{};}此註解為運行時RetentionPolicy.RUNTIME作用在方法上ElementType.METHOD的
token:獲取識別唯一用戶的標識,與用戶數據許可權存儲有關
scope,scopes:預請求的數據許可權范圍
2)AOP實現註解publicclassScopeAuthAdvice{@Around("@annotation(scopeAuth)")publicObjectbefore(,ScopeAuthscopeAuth)throwsThrowable{//...省略過程//獲取tokenStringauthToken=getToken(args,scopeAuth.token(),methodSignature.getMethod());//回寫范圍參數setScope(scopeAuth.scope(),methodSignature,args,authToken);returnthisJoinPoint.proceed();}/***設置范圍*/privatevoidsetScope(Stringscope,,Object[]args,StringauthToken){//獲取請求范圍Set<String>requestScope=getRequestScope(args,scope,methodSignature.getMethod());ScopeAuthAdapteradapter=newScopeAuthAdapter(supplier);//已授權范圍Set<String>authorizedScope=adapter.identifyPermissionScope(authToken,requestScope);//回寫新范圍setRequestScope(args,scope,authorizedScope,methodSignature.getMethod());}/***回寫請求范圍*/privatevoidsetRequestScope(Object[]args,StringscopeName,Collection<String>scopeValues,Methodmethod){//解析SPEL表達式if(scopeName.indexOf(SPEL_FLAG)==0){ParseSPEL.setMethodValue(scopeName,scopeValues,method,args);}}}此為演示代碼省略了過程,主要功能為通過token拿到預先授權的數據范圍,再與本次請求的范圍做交集,最後回寫回原參數。
過程中用到了較多的SPEL表達式,用於計算表達式結果,具體請參考ParseSPEL文件
3)許可權范圍交集計算publicclassScopeAuthAdapter{;publicScopeAuthAdapter(AuthQuerySuppliersupplier){this.supplier=supplier;}/***驗證許可權范圍*@paramtoken*@paramrequestScope*@return*/publicSet<String>identifyPermissionScope(Stringtoken,Set<String>requestScope){Set<String>authorizeScope=supplier.queryScope(token);StringALL_SCOPE="AUTH_ALL";StringUSER_ALL="USER_ALL";if(authorizeScope==null){returnnull;}if(authorizeScope.contains(ALL_SCOPE)){//如果是全開放則返回請求范圍returnrequestScope;}if(requestScope==null){returnnull;}if(requestScope.contains(USER_ALL)){//所有授權的范圍returnauthorizeScope;}//移除不同的元素requestScope.retainAll(authorizeScope);returnrequestScope;}}此處為了方便設置,有兩個關鍵字范圍
AUTH_ALL:預設所有范圍,全開放的意思,為資料庫預先設置值,請求傳什麼值都通過
USER_ALL:請求所有授權的范圍,請求時傳此值則會以資料庫預設值為准
4)spring.factories自動導入類配置org.springframework.boot.autoconfigure.=fun.barryhome.cloud.annotation.ScopeAuthAdvice如果註解功能是單獨項目存在,在使用時有可能會存在找不到引入文件的問題,可通過此配置文件自動載入需要初始化的類
2.註解使用@ScopeAuth(scopes={"#orderDTO.cities"},token="#request.getHeader("X-User-Name")")@PostMapping(value="/query")publicStringquery(@RequestBodyOrderDTOorderDTO,HttpServletRequestrequest){returnArrays.toString(orderDTO.getCities());}在需要使用數據許可權的controller方法上增加@ScopeAuth註解
scopes={"#orderDTO.cities"}:表示取輸入參數orderDTO的cities值,這里是表達式必須加**#**
實際開發過程中,需要將**orderDTO.getCities()**帶入後續邏輯中,在DAO層將此拼裝在SQL中,以實現數據過濾功能
3.實現AuthStoreSupplierAuthStoreSupplier介面為數據許可權的存儲介面,與AuthQuerySupplier配合使用,可按實際情況實現
此介面為非必要介面,可由資料庫或Redis存儲(推薦),一般在登錄的同時保存在Redis中
4.實現AuthQuerySupplierAuthQuerySupplier介面為數據許可權查詢介面,可按存儲方法進行查詢,推薦使用Redis
@{@AutowiredprivateRedisTemplate<String,String>redisTemplate;/***查詢范圍*/@OverridepublicSet<String>queryScope(Stringkey){StringAUTH_USER_KEY="auth:logic:user:%s";StringredisKey=String.format(AUTH_USER_KEY,key);List<String>range=redisTemplate.opsForList().range(redisKey,0,-1);if(range!=null){returnnewHashSet<>(range);}else{returnnull;}}}在分布式結構里,也可將此實現提出到許可權模塊,採用遠程調用方式,進一步解耦
5.開啟數據許可權@EnableScopeAuth@EnableDiscoveryClient@{publicstaticvoidmain(String[]args){SpringApplication.run(OrderApplication.class,args);}}四、綜述至此數據許可權功能就實現了。在微伺服器架構中為了實現功能的復用,將註解的創建和AuthQuerySupplier的實現提取到公共模塊中,那麼在具體的使用模塊就簡單得多了。只需增加@ScopeAuth註解,配置好查詢方法就可以使用。
五、源代碼文中代碼由於篇幅原因有一定省略並不是完整邏輯,如有興趣請Fork源代碼gitee.com/hypier/barry-cloud/tree/master/cloud-auth-logic
『貳』 springboot啟動配置(springboot啟動配置文件載入順序)
SpringBoot的啟動過程及部分註解相比於以前繁瑣的基於Spring的Web應用,SpringBoot通過默認配置很多框架的方式,極大的簡化了項目的搭建以及開發流程。
一個簡單的SpringBoot應用只需要三步:
1.在pom.xml中引入所需要的依賴
2.在application.yml配置所需的數據源
3.在啟動類中加入@SpringBootApplication註解以及run方法
啟動流程
1.SpringApplication.run()啟動
2.新建SpringApplication實例,主要是初始化一些成員變數,參數列表等
prepareContext():
refreshContext()中refresh():
核心註解(部分)
@SpringBootAppliction啟動類
@Configuration+@EnableAutoConfiguration+@ComponentScan
@Configuration
允許在應用上下文中注冊其它的bean,可用@Component代替
@Configuration會為bean創建一個代理類,這個類會攔截所有被@Bean修飾的方法,從容器中返回所需要的單例對象;@Component不會創建代理類,會直接執行方法,每次返回一個新的對象
@EnableAutoConfiguration
啟用springboot自動裝配,該參數位於spring.factories中org.springframework.boot.autoconfigure.EnableAutoConfiguration
@ComponentScan
掃描被@Component(@Service,@Controller)註解的bean,註解默認會掃描該類所在的包下所有的類
@Autowired
自動導入對象到類中,被注入進的類被Spring容器管理Service-Controller
@Component
通用的註解,可標注任意類為Spring組件
@Repository持久層
@Service服務層
@Controller控制層
@Bean
用於告訴方法產生一個Bean對象,然後這個對象交給IOC容器管理。產生這個Bean對象的方法Spring只會調用一次,然後將這個Bean對象放在IOC容器中
二、springboot配置文件
1.配置文件
SpringBoot使用一個全局的配置文件
application.properties
application.yml
配置文件的作用:修改SpringBoot自動配置的默認值,SpringBoot在底層都給我們自動
配置好。有什麼配置項,可以移步官方文檔
配置文件一般放在src/main/resources目錄或者類路徑/confifig下,當然還有很多位置可
以放,它們會有不同優先順序,後面會講到。
YAML(YAMLAin'tMarkupLanguage)
簡單介紹
!--綁定配置文件處理器,配置文件進行綁定的時候就會有提示--
dependency
groupIdorg.springframework.boot/groupId
artifactIdspring-boot-configuration-processor/artifactId
optionaltrue/optional
/dependency
!--將應用打包成一個可執行Jar包,直接使用java-jarxxxx的命令來執行--
build
plugins
plugin
groupIdorg.springframework.boot/groupId
artifactIdspring-boot-maven-plugin/artifactId
/plugin
/plugins
/build以前的配置文件:大多是xml
.yml是YAML語言的文件,以數據為中心,比json、xml等更適合做配置文件
全局配置文件的可以對一些默認配置值進行修改
配置實例
xml:
yml:
2.YAML語法
基本語法
K:(空格)V標識一對鍵值對
以空格的縮進來控制層級關系
只要是左對齊的一列數據,都是同一層級的
屬性和值也是大小寫敏感
實例:
值的寫法
普通的值
k:v字面量直接來寫,字元串默認不用添加單引號
""雙引號不會轉義字元串裡面的特殊字元;
server
port8081/port
/server
server:
port:8081
server:
port:8081
path:/hello//冒號後面的空格不要拉下''單引號會轉義字元,特殊字元最終是一個普通的字元串
對象
普通寫法:
行內寫法
frends:{lastName:zhang,age:18}
Map
示例:
maps:{k1:v1,k2:v2}
數組
普通寫法:
pets://varonj={pets:['cat','pig','dog']}
-cat
-pig
-dog
行內寫法
pets:[cat,pig,dog]
配置文件獲取
將配置文件中的每一個值映射到此組件中
1.Persion
name:"wang qian"//輸出:wang換行qian
frends:
lastName:zhang
age:20packagecom.wrq.boot.bean;
@Component
@ConfigurationProperties(prefix="persion")
publicclassPersion{
privateStringname;
privateintage;
privatedoubleweight;
privatebooleanboss;
privateDatebirth;
privateMapString,Objectmaps;
privateListObjectlist;
privateDogdog;
此處,這個bean的getter、setter和tostring方法已經省略,千萬不能忽略!
}
@ConfifigurationProperties意思是:我們類裡面的屬性和配置文件中的屬性做綁定
不使用此註解,可以在bean的屬性添加@value()註解,如下:
@Component
//@ConfigurationProperties(prefix="persion")
publicclassPersion{
@value("${persion.name}")//$()讀取配置文件、環境變數中的值
privateStringname;
@value("#{11*2}")//#{SpEL}採用表達式
privateintage;
@value("true")//直接賦值
privatebooleanboos;
}
此處採用@ConfifigurationProperties的方式,@value()和@ConfifigurationProperties的
區別見下方表格。prefifix="persion"配置文件中那個下面的屬性來一一映射
@Component如果想要這個註解起作用,必須放到容器裡面
2.Dog
packagecom.wrq.boot.bean;
publicclassDog{//用作Persion中的屬性
privateStringname;
privateintage;
此處,這個bean的getter、setter和tostring方法已經省略,千萬不能忽略!
}
3.配置文件
方式一:application.yml
persion:
name:王大錘
age:18
weight:125
boss:false
birth:2018/5/5
maps:{k1:v1,k2:v2}
list:
-wangli
-wang
dog:
name:xiaogou
age:2
方式二:application.propertiespersion.name=王大錘
persion.age=18
persion.weight=125
persion.boss=false
persion.birth=2018/5/5
persion.maps.k1=v1
persion.maps.k2=v2
persion.dog.name=xiaogou
persion.dog.age=15
4.測試類:BootApplicationTests
packagecom.wrq.boot;
@RunWith(SpringRunner.class)
@SpringBootTest
{
@Autowired
Persionpersion;
@Test
publicvoidcontextLoads(){
System.out.print(persion);
}
}
5.運行BootApplicationTests方法
控制台列印:
application.yml的結果:
Persion{name='王大錘',age=18,weight=125.0,boss=false,birth=SatMay
0500:00:00CST2018,maps={k1=v1,k2=v2},list=[wangli,wang],
dog=Dog{name='xiaogou',age=2}}
application.properties的結果:
Persion{name='????????',age=18,weight=125.0,boss=false,birth=Sat
May0500:00:00CST2018,maps={k2=v2,k1=v1},list=[wangli,wang],
dog=Dog{name='xiaogou',age=15}}
把Bean中的屬性和配置文件綁定,通過yml文件和properties都可以做到,但是properties
文件出現亂碼。
properties中文讀取亂碼:File-Settings-FileEncodings最底部選utf-8、Tranparent打
上勾
註解比較
@value和@ConfifigurationProperties獲取值比較
名詞解釋:
鬆散綁定
last-name和lastName都可以獲取導致,則代表支持鬆散綁定
JSR303@Component
@ConfigurationProperties(prefix="persion")//如果使用的是@value注入值
時,無法使用校驗
@Validated//添加此註解
publicclassPersion{
@Email//配置文件書寫的屬性必須是郵箱格式,不符合報錯!
privateStringname;
}
復雜類型封裝
如果獲取配置文件中map的值時,@value是獲取不到值的
@value("${persion.maps}")//由於使用的是@value,無法獲取配置文件中的map
privateMapString,Objectmaps;
@PropertySource
@PropertySource:載入指定配置文件
@ConfifigurationProperties()默認是從全局配置文件中獲取值,也就是
application.properties這個文件中獲取值。
如果做的配置很多,全局的配置文件就會特別大,為了方便管理。我會創建不同的配置文
件定向管理不同的配置。
如創建persion.properties文件單獨存放persion需要的配置
@PropertySource就是用來導入創建的配置文件
示例:
1.persion.properties
同時把兩個全局的配置中關於Persion的配置都注釋掉persion.name=王弟弟
persion.age=18
persion.weight=125
persion.boss=false
persion.birth=2018/5/5
persion.maps.k1=v1
persion.maps.k2=v2
persion.dog.name=xiaogou
persion.dog.age=15
2.Persion
packagecom.wrq.boot.bean;
@Component
@PropertySource(value={"classpath:persion.properties"})
@ConfigurationProperties(prefix="persion")
publicclassPersion{
privateStringname;
privateintage;
privatedoubleweight;
privatebooleanboss;
privateDatebirth;
privateMapString,Objectmaps;
privateListObjectlist;
privateDogdog;
此處,這個bean的getter、setter和tostring方法已經省略,千萬不能忽略!
}
這樣運行測試類,控制台就可以列印persion.properties中的數據。
通過下面的註解,把類路徑下的persion.properties載入進來。並且把persion開頭的數
據進行綁定。
@PropertySource(value={"classpath:persion.properties"})@ConfifigurationProperties(prefifix="persion")
@ImportResource
@ImportResource:導入Spring的配置文件,讓配置文件生效。
示例:
1.com.wrq.boot.service
packagecom.wrq.boot.service;
/**
*Createdbywangqianon2019/1/12.
*/
publicclassHelloService{
}
2.resources目錄手動建立bean.xml
?xmlversion="1.0"encoding="UTF-8"?
beansxmlns=""
xmlns:xsi=""
xsi:schemaLocation="
"
beanid="helloService"class="com.wrq.boot.service.HelloService"
/bean
/beans
3.測試類
packagecom.wrq.boot;
@RunWith(SpringRunner.class)
@SpringBootTest
{
@Autowired
ApplicationContextioc;@Test
publicvoidtestConfig(){
booleanb=ioc.containsBean("helloService");
System.out.print(b);
}
}
試圖通過添加一個Spring的配置文件bean.xml來把HelloService注入進去。
運行測試類結果:false
結果表明IoC容器中並不包含HelloService,即:配置文件bean.xml沒有生效
解決方式
方式一:主程序中進行配置@ImportResouece註解
packagecom.wrq.boot;
@ImportResource(locations={"classpath:bean.xml"})//通過此配置是
bean.xml生效
@SpringBootApplication
publicclassBootApplication{
publicstaticvoidmain(String[]args){
//應用啟動起來
SpringApplication.run(BootApplication.class,args);
}
}
方法二:通過配置類實現,這種方式也是SpringBoot推薦的
1.com.wrq.boot.confifigpackagecom.wrq.boot.config;
/**
*Createdbywangqianon2019/1/12.
*/
@Configuration
publicclassMyConfig{
//將方法的返回值添加到容器之中,並且容器中這個組件的id就是方法名
@Bean
(){
System.out.print("通過@Bean給容器添加組件了..");
returnnewHelloService();
}
}
@Confifiguration標注這是一個配置類
通過@Bean註解,將方法的返回值添加到容器之中,並且容器中這個組件的id就是方
法名
2.把主程序類中@ImportResource()配置注釋掉
3.測試成功,添加了HelloService()組件
3.配置文件佔位符
隨機數
RandomValuePropertySource:配置文件中可以使用隨機數
${random.value}
${random.int}
${random.long}
${random.uuid}
${random.int(10)}
${random.int[1024,65536]}
屬性配置佔位符可以在配置文件中引用前面配置過的屬性(優先順序前面配置過的這里都能用)
${app.name:默認值}來指定找不到屬性時的默認值
persion.name=王弟弟${random.uuid}
persion.age=${random.int}
persion.dog.name=${persion.name}_dog
4.Profifile多環境支持
Profifile是Spring對不同環境提供不同配置功能的支持,可以通過激活、指定參數等方式
快速切換環境
1.多Profifile的方式
格式:application-{profifile}.properties/yml
application-dev.properties
application-prod.properties
默認採用application.properties配置文件,如果使用別的,需要激活:
1.application.properties中配置:
#激活application-dev.properties配置文件
spring.profiles.active=dev
2.application-dev.properties:
server.port=8082
3.運行BootApplication主程序:
2019-01-1220:46:09.345INFO14404---[main]
s.b.c.e.t.:Tomcatstartedonport(s):
8082(http)
2.多文檔塊的方式
除了上方多Profifile的方式來切換環境,也可以通過YAML多文檔塊的方式。示例:
ap