『壹』 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