# spring-cloud-alibaba-demo **Repository Path**: springzb/spring-cloud-alibaba-demo ## Basic Information - **Project Name**: spring-cloud-alibaba-demo - **Description**: spring-cloud-alibaba-demo项目,软件架构说明,此demo主要版信息: SpringBoot.2.3.12.RELEASE + SpringCloud Hoxton.SR12+ AlibabaCloud 2.2.7.RELEASE - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: develop - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 22 - **Forks**: 17 - **Created**: 2022-02-21 - **Last Updated**: 2025-02-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: SpringCloudAlibaba ## README # spring-cloud-alibaba-demo author superzheng --- 项目代码仓库地址:[https://gitee.com/springzb/spring-cloud-alibaba-demo](https://gitee.com/springzb/spring-cloud-alibaba-demo) ### 由于图床服务过期,详细文档请点击以下链接: 详细文档地址:[https://people.blog.csdn.net/article/details/123729595](https://people.blog.csdn.net/article/details/123729595) 此demo主要版信息: SpringBoot.2.3.12.RELEASE + SpringCloud Hoxton.SR12+ AlibabaCloud 2.2.7.RELEASE ## 一、**版本信息说明:** [https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明](https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明) | Spring Cloud Alibaba Version | Sentinel Version | Nacos Version | RocketMQ Version | Dubbo Version | Seata Version | | ---------------------------- | ---------------- | ------------- | ---------------- | ------------- | ------------- | | 2.2.7.RELEASE | 1.8.1 | 2.0.3 | 4.6.1 | 2.7.13 | 1.3.0 | | Spring Cloud Alibaba Version | Spring Cloud Version | Spring Boot Version | | ---------------------------- | ------------------------ | ------------------- | | 2.2.7.RELEASE | Spring Cloud Hoxton.SR12 | 2.3.12.RELEASE | ## 二、组件说明 SpringCloud - 全家桶+轻松嵌入第三方组件(Netflix 奈飞) - 官网:[https://spring.io/projects/spring-cloud](https://spring.io/projects/spring-cloud) - 配套 - 通信方式:http restful - 注册中心:eruka - 配置中心:config - 断路器:hystrix - 网关:zuul/gateway - 分布式追踪系统:sleuth+zipkin - Spring Alibaba Cloud - 全家桶+阿里生态多个组件组合+SpringCloud支持 - 官网 [https://spring.io/projects/spring-cloud-alibaba](https://spring.io/projects/spring-cloud-alibaba) - 配套 - 通信方式:http restful - 注册中心:nacos - 配置中心:nacos - 断路器:sentinel - 网关:gateway - 分布式追踪系统:sleuth+zipkin ## 三、新建聚合工程 ### 3.1父工程pom文件 ```Java 4.0.0 cn.mesmile spring-cloud-alibaba-demo 0.0.1-SNAPSHOT cloud-system cloud-common cloud-order spring-cloud-alibaba-demo springCloudAlibabaDemo pom UTF-8 UTF-8 1.8 1.8 1.8 1.18.20 2.3.12.RELEASE Hoxton.SR12 2.2.7.RELEASE 3.4.2 UTF-8 2.17.1 org.springframework.boot spring-boot-dependencies ${spring.boot.version} pom import org.springframework.cloud spring-cloud-dependencies ${spring.cloud.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring.cloud.alibaba} pom import org.springframework.boot spring-boot-maven-plugin true true ``` ## 四、搭建整合nacos docker-compose.yaml安装nacos ```Java version: '3' services: nacos: # 注意 2.0 不需要额外安装数据库了,docker run --name nacos-standalone -e MODE=standalone -d -p 8848:8848 -p 9848:9848 -p 9849:9849 nacos/nacos-server:2.0.3 image: 'nacos/nacos-server:2.0.3' restart: always container_name: nacos environment: # 启动模式 单机 MODE: 'standalone' # nacos默认端口号 NACOS_SERVER_PORT: 8848 # # 是否开启权限系统 # NACOS_AUTH_ENABLE: 'true' ports: - '8848:8848' - '9848:9848' - '9849:9849' #在当前目录打开终端,使用命令 docker-compose up -d 即可运行; #在当前目录打开终端,使用命令 docker-compose down 即可运行; ``` 安装完成后 [http://127.0.0.1:8848/nacos/](http://81.69.43.78:8848/nacos/) 默认用户名和密码都是 nacos 进入后台管理页面 ![](https://xxxxxx/blog/image_2.png) ## 五、 整合nacos 注册中心,新建cloud-system模块 cloud-system模块中pom.xml添加nacos依赖 ```Java com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` 配置文件中添加配置: ```Java # nacos 地址 spring cloud: nacos: discovery: server-addr: 127.0.0.1:8848 # 应用 application: # 应用名称 name: cloud-system-service ``` 启动类上加上注解开启 @EnableDiscoveryClient 服务注册 发现服务已经注册上来了: ![](https://xxxxxx/blog/image_3.png) ```Java @RequiredArgsConstructor @RequestMapping("/v1/nacos") @RestController public class NacosController { private final DiscoveryClient discoveryClient; @GetMapping("/get") public R get(){ List instances = discoveryClient.getInstances("cloud-system-service"); ServiceInstance serviceInstance = instances.get(0); // 获取到nacos注册中心上的服务ip以及端口 String host = serviceInstance.getHost(); int port = serviceInstance.getPort(); return R.data(host+":"+port); } } ``` 测试发送get请求获取信息: ```JSON { "code": 200, "success": true, "data": "10.113.229.45:8000", "msg": "操作成功" } ``` ![](https://xxxxxx/blog/image_4.png) ## 六、整合openFeign,新建 cloud-order 模块 cloud-system添加openFeign依赖: ```JSON org.springframework.cloud spring-cloud-starter-openfeign ``` cloud-system启动类上添加注解 @EnableFeignClients 开启openFeign cloud-order模块提供接口 ```Java @RequestMapping("/v1/order") @RequiredArgsConstructor @RestController public class OrderController { private final OrderService orderService; @GetMapping("/list") public R listOrder(){ List list = orderService.list(); return R.data(list); } } ``` cloud-system模块添加feign ```Java @FeignClient(value = "cloud-order-service") public interface OrderFeignClient { /** * 获取订单列表 * @return */ @GetMapping("/v1/order/list") R listOrder(); } ``` 通过cloud-system模块的【OrderFeignClient 对象】调用 cloud-order模块 ### 6.1 openFeign开启调试调试日志 openFeign的日志级别如下: - NONE:默认的,不显示任何日志; - BASIC:仅记录请求方法、URL、响应状态码及执行时间; - HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息; - FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。 **第一步**配置类中配置日志隔离级别: ```Java @Configuration public class CloudOpenFeignConfig { /** * 日志级别定义 * import feign.Logger; * @return */ @Profile("dev") @Bean Logger.Level feginLoggerLevel(){ return Logger.Level.FULL; } } ``` **第二步** 在配置信息中指定包下的日志级别 ```YAML logging: level: # 全局日志级别 root: info # 局部包的日志级别, cn.mesmile.system.feign是openFeign接口所在的包名 cn.mesmile.system.feign: debug ``` 调用openFeign测试是否开启日志结果如下: ```Java 2022-02-23 14:40:48.580 DEBUG 23620 --- [nio-8000-exec-3] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] <--- HTTP/1.1 200 (2ms) 2022-02-23 14:40:48.581 DEBUG 23620 --- [nio-8000-exec-3] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] connection: keep-alive 2022-02-23 14:40:48.581 DEBUG 23620 --- [nio-8000-exec-3] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] content-type: application/json 2022-02-23 14:40:48.581 DEBUG 23620 --- [nio-8000-exec-3] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] date: Wed, 23 Feb 2022 06:40:48 GMT 2022-02-23 14:40:48.581 DEBUG 23620 --- [nio-8000-exec-3] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] keep-alive: timeout=60 2022-02-23 14:40:48.581 DEBUG 23620 --- [nio-8000-exec-3] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] transfer-encoding: chunked 2022-02-23 14:40:48.581 DEBUG 23620 --- [nio-8000-exec-3] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] 2022-02-23 14:40:48.581 DEBUG 23620 --- [nio-8000-exec-3] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] {"code":200,"success":true,"data":[],"msg":"操作成功"} 2022-02-23 14:40:48.581 DEBUG 23620 --- [nio-8000-exec-3] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] <--- END HTTP (58-byte body) ``` ### 6.2 替换默认的httpclient Feign在默认情况下使用的是JDK原生的**URLConnection**发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection 在生产环境中,通常不使用默认的http client,通常有如下两种选择: - 使用**ApacheHttpClient** - 使用**OkHttp** 这里演示替换为 **ApacheHttpClient** 在openFeign接口服务的pom文件maven添加如下依赖 ```XML org.apache.httpcomponents httpclient io.github.openfeign feign-httpclient ``` org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration ![](https://xxxxxx/blog/image_5.png "") 上述红色框中的生成条件,其中的`@ConditionalOnClass(ApacheHttpClient.class)`,必须要有`ApacheHttpClient`这个类才会生效,并且`feign.httpclient.enabled`这个配置要设置为`true` 应此需要添加配置: ```YAML # 开启httpclient,默认值为 true feign: httpclient: enabled: true ``` 验证是否替换成功: `feign.SynchronousMethodHandler#executeAndDecode()`这个方法中可以清楚的看出调用哪个client ![](https://xxxxxx/blog/image_6.png "") ### 6.3 开启通讯优化gzip 添加配置: ```YAML feign: ## 开启压缩 compression: request: enabled: true ## 开启压缩的阈值,单位字节,默认2048,即是2k,这里为了演示效果设置成1字节 min-request-size: 1 mime-types: text/xml,application/xml,application/json response: enabled: true ``` 验证已经开启 gzip : Accept-Encoding: gzip ```XML 2022-02-23 15:35:42.647 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] ---> GET http://cloud-order-service/v1/order/list HTTP/1.1 2022-02-23 15:35:42.647 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] Accept-Encoding: gzip 2022-02-23 15:35:42.647 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] Accept-Encoding: deflate 2022-02-23 15:35:42.647 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] ---> END HTTP (0-byte body) 2022-02-23 15:35:42.727 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] <--- HTTP/1.1 200 (80ms) 2022-02-23 15:35:42.727 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] connection: keep-alive 2022-02-23 15:35:42.727 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] content-type: application/json 2022-02-23 15:35:42.727 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] date: Wed, 23 Feb 2022 07:35:42 GMT 2022-02-23 15:35:42.727 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] keep-alive: timeout=60 2022-02-23 15:35:42.727 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] transfer-encoding: chunked 2022-02-23 15:35:42.727 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] 2022-02-23 15:35:42.727 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] {"code":200,"success":true,"data":[{"id":1495409546527555585,"outTradeNo":"234234534","state":1,"createTime":"2022-02-20 09:53:41","totalFee":1003,"videoId":12443225,"videoTitle":"spring专题","videoImg":"https://www.baidu.com","userId":3242342342}],"msg":"操作成功"} 2022-02-23 15:35:42.727 DEBUG 22184 --- [nio-8000-exec-5] c.mesmile.system.feign.OrderFeignClient : [OrderFeignClient#listOrder] <--- END HTTP (272-byte body) ``` ## 七、整合sentinel 流量卫兵 sentinel是面向分布式服务框架的轻量级流量控制框架,主要以流量为切入点,从流量控制,熔断降级,系统负载保护等多个维度来维护系统的稳定性. 官网 [https://github.com/alibaba/Sentinel/wiki](https://github.com/alibaba/Sentinel/wiki) 下载编译好的后台管理系统包: [https://github.com/alibaba/Sentinel/releases](https://github.com/alibaba/Sentinel/releases) 这里使用的是 1.8.1 版本号: ### sentinel安装包 [sentinel-dashboard-1.8.1.jar](file/sentinel-dashboard-1.8.1.jar) 启动命令: ```Java # 注意:启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。 # 其中 -Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080。 # 从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel。 # 可以参考 鉴权模块文档 配置用户名和密码。 # 启动成功后的后台地址 http://127.0.0.1:8080/ java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.1.jar ``` 启动后台jar包后,访问http://127.0.0.1:8080/ 用户名和密码都是sentinel ![](https://xxxxxx/blog/image_7.png "") 项目中添加maven依赖 ```Java com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` 项目中添加sentinel配置: ```YAML # sentinel 配置 spring: cloud: sentinel: transport: dashboard: 127.0.0.1:8080 port: 9900 #dashboard: 8080 控制台端口 #port: 9900 本地启的端口,随机选个不能被占用的,与dashboard进行数据交互,会在应用对应的机器上 #启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互, 若被占用,则开始+1一次扫描 ``` ### 7.1 **Sentinel自定义全局异常降级** 在cloud-system中添加配置类,完成自定义降级异常 ```Java @Component public class CloudSentinelBlockHandler implements BlockExceptionHandler { /* FlowException //限流异常 DegradeException //降级异常 ParamFlowException //参数限流异常 SystemBlockException //系统负载异常 AuthorityException //授权异常 */ /** * V2.1.0 到 V2.2.0后,sentinel 里面依赖进行改动,且不向下兼容 * @param request * @param response * @param e * @throws Exception */ @Override public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { /* request.getRequestURI() = /v1/user/list request.getRemoteUser() = null request.getContextPath() = request.getMethod() = GET */ // 降级业务 Map backMap=new HashMap<>(); if (e instanceof FlowException){ backMap.put("code",-1); backMap.put("msg","限流-异常啦"); }else if (e instanceof DegradeException){ backMap.put("code",-2); backMap.put("msg","降级-异常啦"); }else if (e instanceof ParamFlowException){ backMap.put("code",-3); backMap.put("msg","热点-异常啦"); }else if (e instanceof SystemBlockException){ backMap.put("code",-4); backMap.put("msg","系统规则-异常啦"); }else if (e instanceof AuthorityException){ backMap.put("code",-5); backMap.put("msg","认证-异常啦"); } backMap.put("success",false); // 设置返回json数据 response.setStatus(200); response.setHeader("content-Type","application/json;charset=UTF-8"); response.getWriter().write(JSONObject.toJSONString(backMap)); } } ``` cloud-system中写好接口: ```Java @RequiredArgsConstructor @RequestMapping("/v1/user") @RestController public class UserController { private final UserService userService; @GetMapping("/list") public R listUser(){ List list = userService.list(); return R.data(list); } } ``` ![](https://xxxxxx/blog/image_8.png "") 浏览器快速请求接口/v1/user/list验证自定义:**Sentinel自定义异常降级** ![](https://xxxxxx/blog/image_9.png "") 验证成功 ### 7.2 Sentinel整合OpenFeign cloud-system中添加配置: ```YAML # 开启 sentinel 对feign 的支持,一旦出错就会进入兜底数据 feign: sentinel: enabled: true ``` cloud-system中定义feign接口请求 cloud-order ```Java @FeignClient(value = "cloud-order-service",fallback = OrderFeignClientFallback.class) public interface OrderFeignClient { /** * 获取订单列表 * @return */ @GetMapping("/v1/order/list") R listOrder(); } ``` 新建兜底类 OrderFeignClientFallback: ```Java @Component public class OrderFeignClientFallback implements OrderFeignClient { @Override public R listOrder() { // 请求另外服务出错就会进入到这里的兜底方法 return R.fail("获取数据失败了,这是兜底数据哦。。。"); } } ``` cloud-system中编写测试接口 ```Java @RequiredArgsConstructor @RequestMapping("/v1/user") @RestController public class UserController { private final OrderFeignClient orderFeignClient; @GetMapping("/listOrder") public R listOrder(){ R r = orderFeignClient.listOrder(); return r; } } ``` 请求接口:/v1/user/listOrder 测试只开启cloud-system 关闭cloud-order: 测试结果,进入兜底类,Sentinel整合OpenFeign 成功 ![](https://xxxxxx/blog/image_10.png "") **单独设置:** ![](https://xxxxxx/blog/image_11.png "") 熔断降级规则(DegradeRule)包含下面几个重要的属性: | Field | 说明 | 默认值 | | ------------------ | ------------------------------------------------------------ | ---------- | | resource | 资源名,即规则的作用对象 | | | grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 | | count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | | | timeWindow | 熔断时长,单位为 s | | | minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 | | statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms | | slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) | | ### 7.3 sentinel配置持久化 配置持久化到nacos ```Java com.alibaba.csp sentinel-datasource-nacos ``` ![](https://xxxxxx/blog/image_12.png "") Data ID为 spring应用的名称: ![](https://xxxxxx/blog/image_13.png "") ```JSON [ { "resource": "/findById", "limitApp":"default", "grade":1, "count":1, "strategy":0, "controlBehavior":0, "clusterMode":false } ] ``` **naocs配置解读:** ```Java resource:资源名称 limitApp:来源应用 grade:阀值类型 【0---线程数,1---QPS】 count:单机阀值 strategy:流控模式,【0---直接,1---关联,2---链路】 controlBehavior:流控效果,【0---快速失败,1---warmUp,2---排队等待】 clusterMode:是否集群 ``` - 此时如果是Nacos集群,每个节点务必要配置到同一个数据库上。并且保证每个 节点都可用。如果有的节点宕掉了可能会导致配置持久化失败。 - 部署在nacos上的配置文件的名字并没有太多要求,只需要跟微服务项目中yml文件中配置的dataId一致即可。 ## 八、网关springcloudgateway **路由(route)** 路由是网关中最基础的部分, 路由信息包括一个ID、一个目的URI、一组**断言**工厂、一组**Filter**组成。如果**断言**为真, 则说明请求的URL和配置的路由匹配 **断言(predicates)** Java 8中的断言函数, Spring Cloud Gateway中的断言函数类型是Spring 5.0框架中的Server Web Exchange断言函数允许开发者去定义匹配Httprequest中的任何信息, 比如请求头和参数等 **过滤器(Filter)** Spring Cloud Gateway中的iter分为Gateway Fil r和Global Filter。Filter可以对请求和响应进行处理 - API Gateway,是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能 提供路由请求、鉴权、监控、缓存、限流等功能 - 统一接入 - 智能路由 - AB测试、灰度测试 - 负载均衡、容灾处理 - 日志埋点(类似Nignx日志) - 流量监控 - 限流处理 - 服务降级 - 安全防护 - 鉴权处理 - 监控 - 机器网络隔离 这里选用 springcloud gateway 网关 springcloud gateway: Spring公司专门开发的网关 新增maven依赖 ```XML org.springframework.cloud spring-cloud-starter-gateway ``` **源码中已经配置了【gateway-adapter】的配置**,无需再次配置 ![](https://xxxxxx/blog/image_14.png "") 添加配置: ```YAML spring: # nacos 地址 cloud: nacos: discovery: server-addr: 81.69.43.78:8848 gateway: routes: #数组形式 - id: system-service #路由唯一标识 #uri: http://127.0.0.1:8000 #想要转发到的地址 uri: lb://cloud-system-service # 从nacos获取名称转发,lb是负载均衡轮训策略 predicates: #断言 配置哪个路径才转发,不配置Path则不用添加前缀 - Path=/system-service/** filters: #过滤器,请求在传递过程中通过过滤器修改 - StripPrefix=1 #去掉第一层前缀 # 请求 http://localhost:8002/system-service/v1/user/list 会转发到 http://cloud-system-service/v1/user/list discovery: locator: #开启网关拉取nacos的服务 若不配置 routes 规则,则默认通过 服务名 开头去调用 # 注意这里配置后在请求开头添加服务名后,路由和断路器会失效 enabled: true ``` ** # 请求 http://localhost:8002/system-service/v1/user/list 会转发到 http://cloud-system-service/v1/user/list
discovery:
locator:****
#开启网关拉取nacos的服务,则可以通过 服务名 开头去调用
# 注意这里配置后在【请求路径添加服务名后】,路由和断路器会失效
enabled: true** ### **内置断言工厂:** ![](https://xxxxxx/blog/image_15.png "") ![](https://xxxxxx/blog/image_16.png "") ```JSON spring: # nacos 地址 cloud: nacos: discovery: server-addr: 81.69.43.78:8848 gateway: routes: #数组形式 - id: system-service #路由唯一标识 #uri: http://127.0.0.1:8000 #想要转发到的地址 uri: lb://cloud-system-service # 从nacos获取名称转发,lb是负载均衡轮训策略 predicates: #断言 配置哪个路径才转发 - Query=name,test|test2|test3 #http://localhost:8002/v1/user/list?name=test - After=2021-03-18T17:32:58.129+08:00[Asia/Shanghai] # 运行网关拉取nacos服务 discovery: locator: #开启网关拉取nacos的服务,则可以通过 服务名 开头去调用 # 注意这里配置后在【请求路径添加服务名后】,路由和断路器会失效 enabled: true ``` ### 自定义断言工厂 自定义路由断言工厂需要继承AbstractRoutePredicateFactory类, 重写apply方法的逻辑。在apply方法中可以通过exchange.getRequest() 拿到ServerHttpRequest对象, 从而可以获取到请求的参数、请求方式、请求头 等信息。 1、必须spring组件bean 2.类必须加上RoutePredicateFactory作为结尾 3.必须继承AbstractRoutePredicateFactory 4.必须声明静态内部类声明属性来接收配置文件中对应的断言的信息 5.需要结合shortcutFieldOrder进行绑定 6.通过apply进行逻辑判断true就是匹配成功false匹配失败 ```Java package cn.mesmile.gateway.predicate; import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.server.ServerWebExchange; import javax.validation.constraints.NotEmpty; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.Predicate; /** * @author zb * @Description * 配置 * predicates: #断言 配置哪个路径才转发 * - Query=name,test|test2|test3 #http://localhost:8002/v1/user/list?name=test * - CheckParam=test */ @Component public class CheckParamRoutePredicateFactory extends AbstractRoutePredicateFactory { /** * Param key. */ public static final String PARAM_KEY = "param"; // /** // * Regexp key. // */ // public static final String REGEXP_KEY = "regexp"; public CheckParamRoutePredicateFactory() { super(CheckParamRoutePredicateFactory.Config.class); } @Override public List shortcutFieldOrder() { return Arrays.asList(PARAM_KEY); } @Override public Predicate apply(CheckParamRoutePredicateFactory.Config config) { return new GatewayPredicate() { @Override public boolean test(ServerWebExchange exchange) { String param = config.getParam(); if (Objects.equals("test", param)){ return true; } // if (!StringUtils.hasText(config.regexp)) { // // check existence of header // return exchange.getRequest().getQueryParams() // .containsKey(config.param); // } // // List values = exchange.getRequest().getQueryParams() // .get(config.param); // if (values == null) { // return false; // } // for (String value : values) { // if (value != null && value.matches(config.regexp)) { // return true; // } // } return false; } }; } @Validated public static class Config { @NotEmpty private String param; // private String regexp; public String getParam() { return param; } public CheckParamRoutePredicateFactory.Config setParam(String param) { this.param = param; return this; } // public String getRegexp() { // return regexp; // } // // public CheckParamRoutePredicateFactory.Config setRegexp(String regexp) { // this.regexp = regexp; // return this; // } } } ``` ### 内置过滤器 ![](https://xxxxxx/blog/image_17.png "") ### 自定义过滤器 ```Java package cn.mesmile.gateway.filter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.Arrays; import java.util.List; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*; /** * @author zb * @Description * 配置 * filters: #过滤器,请求在传递过程中通过过滤器修改 * - CheckParam=1 */ @Component public class CheckParamGatewayFilterFactory extends AbstractGatewayFilterFactory { /** * Prefix key. */ public static final String PREFIX_KEY = "prefix"; private static final Log log = LogFactory .getLog(CheckParamGatewayFilterFactory.class); public CheckParamGatewayFilterFactory() { super(CheckParamGatewayFilterFactory.Config.class); } @Override public List shortcutFieldOrder() { return Arrays.asList(PREFIX_KEY); } @Override public GatewayFilter apply(CheckParamGatewayFilterFactory.Config config) { return new GatewayFilter() { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String prefix = config.getPrefix(); return chain.filter(exchange); // boolean alreadyPrefixed = exchange // .getAttributeOrDefault(GATEWAY_ALREADY_PREFIXED_ATTR, false); // if (alreadyPrefixed) { // return chain.filter(exchange); // } // exchange.getAttributes().put(GATEWAY_ALREADY_PREFIXED_ATTR, true); // // ServerHttpRequest req = exchange.getRequest(); // addOriginalRequestUrl(exchange, req.getURI()); // String newPath = config.prefix + req.getURI().getRawPath(); // // ServerHttpRequest request = req.mutate().path(newPath).build(); // // exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI()); // // if (log.isTraceEnabled()) { // log.trace("Prefixed URI with: " + config.prefix + " -> " // + request.getURI()); // } // // return chain.filter(exchange.mutate().request(request).build()); } // @Override // public String toString() { // return filterToStringCreator(CheckParamGatewayFilterFactory.this) // .append("prefix", config.getPrefix()).toString(); // } }; } public static class Config { private String prefix; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } } } ``` ### cloud-gateway模块自定义【全局】的过滤器 ```Java @Component public class GlobalApiFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 写业务逻辑 String token = exchange.getRequest().getHeaders().getFirst("Authorization"); // 根据业务鉴权 // if (StringUtils.isEmpty(token)) { // exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // Mono voidMono = exchange.getResponse().setComplete(); // return voidMono; // } // 继续往下执行 Mono filter = chain.filter(exchange); return filter; } @Override public int getOrder() { // 数字越小,优先级越高 return 0; } } ``` **框架自带的过滤器** ![](https://xxxxxx/blog/image_18.png "") ### 记录网关请求日志 ![](https://xxxxxx/blog/image_19.png "") ![](https://xxxxxx/blog/image_20.png "") ### gateway跨越配置(cors gateway) **方式一:配置文件的方式配置** ![](https://xxxxxx/blog/image_21.png "") *号表示所有 ![](https://xxxxxx/blog/image_22.png "") **方式二:java代码的方式配置** ![](https://xxxxxx/blog/image_23.png "") ![](https://xxxxxx/blog/image_24.png "") ![](https://xxxxxx/blog/image_25.png "") ### gateway整合sentinel 导入依赖 ```XML com.alibaba.cloud spring-cloud-starter-alibaba-sentinel com.alibaba.cloud spring-cloud-alibaba-sentinel-gateway ``` 添加配置: ```YAML spring: cloud: # sentinel 配置 sentinel: transport: dashboard: 127.0.0.1:8080 port: 9904 #dashboard: 8080 控制台端口 #port: 9904 本地启的端口,随机选个不能被占用的,与dashboard进行数据交互,会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互, 若被占用,则开始+1一次扫描 ``` ![](https://xxxxxx/blog/image_26.png "") ![](https://xxxxxx/blog/image_27.png "") ### gateway全局sentinel限流异常返回 ```Java @Component public class InitBlockHandle { @PostConstruct public void init(){ BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @Override public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable e) { // 降级业务 Map backMap=new HashMap<>(); if (e instanceof FlowException){ backMap.put("code",-1); backMap.put("msg","限流-异常啦"); }else if (e instanceof DegradeException){ backMap.put("code",-2); backMap.put("msg","降级-异常啦"); }else if (e instanceof ParamFlowException){ backMap.put("code",-3); backMap.put("msg","热点-异常啦"); }else if (e instanceof SystemBlockException){ backMap.put("code",-4); backMap.put("msg","系统规则-异常啦"); }else if (e instanceof AuthorityException){ backMap.put("code",-5); backMap.put("msg","认证-异常啦"); } backMap.put("success",false); return ServerResponse.status(HttpStatus.OK) .header("content-Type","application/json;charset=UTF-8") .body(BodyInserters.fromValue(backMap)); // 设置返回json数据 // response.setStatus(200); // response.setHeader("content-Type","application/json;charset=UTF-8"); // response.getWriter().write(JSONObject.toJSONString(backMap)); } }; GatewayCallbackManager.setBlockHandler(blockRequestHandler); } } ``` ### gateway api 分组流控规则 ![](https://xxxxxx/blog/image_28.png "") ## 九、链路追踪 ### Sleuth链路追踪系统 - 分布式应用架构虽然满足了应用横向扩展的需求,但是运维和诊断的过程变得越来越复杂,例如会遇到接口诊断困难、应用性能诊断复杂、架构分析复杂等难题,传统的监控工具并无法满足,分布式链路系统由此诞生 - 核心:将一次请求分布式调用,使用GPS定位串起来,记录每个调用的耗时、性能等日志,并通过可视化工具展示出来 Sleuth和zipking(内部使用的鹰眼) ```Java org.springframework.cloud spring-cloud-starter-sleuth ``` 调用网关【gateway-service】转发到【system-service】再到【order-service】 localhost:8002/system-service/v1/user/listOrder ![](https://xxxxxx/blog/image_29.png "") ![](https://xxxxxx/blog/image_30.png "") ![](https://xxxxxx/blog/image_31.png "") ```Java 第一个值,spring.application.name的值 第二个值,0faa27ad8177e6be,sleuth生成的一个ID,叫【Trace ID】,用来标识一条请求链路,一条请求链路中包含一个Trace ID,多个Span ID 第三个值,852ef4cfcdecabf3、【spanid】基本的工作单元,获取元数据,如发送一个http 第四个值:false,是否要将该信息输出到【zipkin 可视化 服务中来收集和展示】。 ``` ### **zipkin可视化 收集链路追踪**信息 - sleuth收集跟踪信息通过http请求发送给zipkin server - zipkin server进行跟踪信息的存储以及提供Rest API即可 - Zipkin UI调用其API接口进行数据展示默认存储是内存,可也用mysql 或者elasticsearch等存储 - 官网 - [https://zipkin.io/](https://zipkin.io/) - [https://zipkin.io/pages/quickstart.html](https://zipkin.io/pages/quickstart.html) - 大规模分布式系统的APM工具(Application Performance Management),基于Google Dapper的基础实现,和sleuth结合可以提供可视化web界面分析调用链路耗时情况 - 同类产品 - 鹰眼(EagleEye) - CAT - twitter开源zipkin,结合sleuth - Pinpoint,运用JavaAgent字节码增强技术 ```Java docker run -d -p 9411:9411 openzipkin/zipkin:2.23.0 ``` - 访问入口:[http://127.0.0.1:9411/zipkin/](http://127.0.0.1:9411/zipkin/) ```Java org.springframework.cloud spring-cloud-starter-zipkin ``` 配置 ```Java spring: zipkin: base-url: http://127.0.0.1:9411/ #zipkin地址 discovery-client-enabled: false #不用开启服务发现 ​ sleuth: sampler: probability: 1.0 #采样百分比 ``` 默认为0.1,即10%,这里配置1,是记录全部的sleuth信息,是为了收集到更多的数据(仅供测试用)。 在分布式系统中,过于频繁的采样会影响系统性能,所以这里配置需要采用一个合适的值。 [http://127.0.0.1:9411/zipkin/](http://127.0.0.1:9411/zipkin/) ![](https://xxxxxx/blog/image_32.png "") ![](https://xxxxxx/blog/image_33.png "") ### **zipkin数据持久化** - 服务重启会导致链路追踪系统数据丢失,数据是存储在内存中的 - 持久化配置:mysql或者elasticsearch - 创建数据库表SQL脚本 - 启动命令 **方式一:持久化到mysql数据库** ```Java java -jar zipkin-server-2.12.9-exec.jar \ --STORAGE_TYPE=mysql \ --MYSQL_HOST=127.0.0.1 \ --MYSQL_TCP_PORT=3306 \ --MYSQL_DB=zipkin_log \ --MYSQL_USER=root \ --MYSQL_PASS=root ``` 数据库脚本 ```Java CREATE TABLE IF NOT EXISTS zipkin_spans ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL, `id` BIGINT NOT NULL, `name` VARCHAR(255) NOT NULL, `parent_id` BIGINT, `debug` BIT(1), `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL', `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query' ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`) COMMENT 'ignore insert on duplicate'; ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`) COMMENT 'for joining with zipkin_annotations'; ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds'; ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames'; ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range'; CREATE TABLE IF NOT EXISTS zipkin_annotations ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id', `span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id', `a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1', `a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB', `a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation', `a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp', `endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address', `endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null' ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds'; ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames'; ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces'; ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces'; CREATE TABLE IF NOT EXISTS zipkin_dependencies ( `day` DATE NOT NULL, `parent` VARCHAR(255) NOT NULL, `child` VARCHAR(255) NOT NULL, `call_count` BIGINT ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`); ``` **方式二:持久化到elasticsearch** ```Java java -jar zipkin-server-2.12.9-exec.jar \ --STORAGE_TYPE=elasticsearch \ --ES-HOST=localhost:9200 ``` 注:测试发现使用最新版的elasticsearch-7.9.1,会有以下异常 ```Java java.lang.IllegalStateException: response for update-template failed: {"error":{"root_cause":[{"type":"invalid_index_template_exception","reason":"index_template [zipkin:span_template]invalid, cause [Validation Failed: 1: index_pattern [zipkin:span-*] must not contain a ':';]"}],"type":"invalid_index_template_exception","reason":"index_template [zipkin:span_template] invalid, cause [Validation Failed: 1: index_pattern [zipkin:span-*] must not contain a ':';]"},"status":400} ``` 解决方案:降低elasticsearch版本,使用elasticsearch-6.8.4数据可以正常持久化。 ## 十、配置中心Nacos - 现在微服务存在的问题 - 配置文件增多,不好维护 - 修改配置文件需要重新发布 - 统一管理配置, 快速切换各个环境的配置 - 相关产品: - 百度的disconf 地址:[https://github.com/knightliao/disconf](https://github.com/knightliao/disconf) - 阿里的diamand 地址:[https://github.com/takeseem/diamond](https://github.com/takeseem/diamond) - springcloud的configs-server: 地址:[http://cloud.spring.io/spring-cloud-config/](http://cloud.spring.io/spring-cloud-config/) - 阿里的Nacos:既可以当服务治理,又可以当配置中心,Nacos = Eureka + Config - 官方文档 - [https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config](https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config) 添加依赖: ```Java com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` ![](https://xxxxxx/blog/image_34.png "") **注意配置文件优先级** - **不能使用原先的application.yml, 需要使用bootstrap.yml作为配置文件** - **配置读取优先级 bootstrap.yml > application.yml** 以网关服务(cloud-gateway-service)为例: bootstrap.yml ```YAML spring: profiles: # 使用 dev 分支配置 active: dev application: # 应用名称 name: cloud-gateway-service cloud: nacos: config: server-addr: 81.69.43.78:8848 #Nacos配置中心地址 file-extension: yaml #文件拓展格式 server: port: 8002 ``` 将原来application.yml 注释,将原来application-dev.yml 拷贝到nacos中,将原来代码中的application-dev.yml注释 ![](https://xxxxxx/blog/image_35.png "") ![](https://xxxxxx/blog/image_36.png "") ![](https://xxxxxx/blog/image_37.png "") **nacos中导出的配置文件:** [nacos_config_export_20220301224344.zip](file/nacos_config_export_20220301224344.zip) **nacos动态刷新配置:** 我们修改了配置,程序不能自动更新,动态刷新就可以解决这个问题 @RefreshScope 动态刷新 在nacos中配置 useLocalCache 的值,然后再代码中动态获取 ```Java @RestController @RequestMapping("/config") @RefreshScope public class ConfigController { @Value("${useLocalCache:false}") private boolean useLocalCache; @RequestMapping("/get") public boolean get() { return useLocalCache; } } ``` ## ~~十一、hystrix熔断降级(停止更新维护)~~ - 使用命令模式将所有对外部服务(或依赖关系)的调用包装再HystrixCommand或HystrixObservableCommand对象中,并将该对象放在单独的线程中执行。 - 每个依赖都维护一个线程池(或信号量),线程池被耗尽则拒绝请求(而不是让请求排队)。 - 记录请求成功,失败,超时和线程拒绝。 - 服务错误百分比超过阀值,熔断器开关自动打开,一段时间内停止对该服务的所有请求。 - 请求失败,被拒绝,超时或熔断时执行降级逻辑。 - 近实时地讲课指标和配置的修改。 - 引入jar包 ```XML org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` **@EnableHystrix //在启动类上添加@EnableHystrix注解开启Hystrix的熔断器功能。** ![](https://xxxxxx/blog/image_38.png "") 配置开启 feign对hystirx的支持: ```YAML feign: hystrix: # feign熔断器开关 enabled: true ``` ### 方法一:单独设置接口服务降级 ![](https://xxxxxx/blog/image_39.png "") ### 方法二:统一设置接口服务降级(推荐) OrderFeignClient 客户端 ```Java /** * @author zb * @Description * * 服务降级-》进而熔断-》恢复调用链路 * 服务降级 fallback */ @FeignClient(value = "cloud-order-service",fallback = OrderFeignClientFallback.class) public interface OrderFeignClient { /** * 获取订单列表 * @return */ @GetMapping("/v1/order/list") R listOrder(); } ``` OrderFeignClientFallback兜底类 ```Java /** * @author zb * @Description */ @Component public class OrderFeignClientFallback implements OrderFeignClient { @Override public R listOrder() { // 请求另外服务出错就会进入到这里的兜底方法 return R.fail("获取数据失败了,这是兜底数据哦。。。"); } } ``` ### 设置服务熔断(顺序:服务降级—> 服务熔断 —> 恢复调用链路) ![](https://xxxxxx/blog/image_40.png "") 开启熔断器,在10s内10次请求里面有6次失败的话 **配置****参数配置出处** ![](https://xxxxxx/blog/image_41.png "") **熔断器的三个状态:关闭(正常状态)、打开(断开状态)、半打开(半打开状态)** 例如: 开启熔断器,在10s内10次请求里面有6次失败的话(依据配置),熔断器从【关闭】变为【打开】 过了一段时间后熔断器从【打开】变为【半打开】,尝试让请求通过,若请求没有超过配置限度熔断器则从【半打开】变为【关闭】 ![](https://xxxxxx/blog/image_42.png "") ## 十二、Skywalking链路追踪 sky walking是一个国产开源框架, 2015年由吴晟开源, 2017年加入Apache孵化器。sky walking是分布式系统的应用程序性能监视工具, 专为微服务、云原 生架构和基于容器(Docker、K8s、Mesos) 架构而设计。它是一款优秀的APM(Application Performance Management) 工具, 包括了分布式追踪、性能 指标分析、应用和服务依赖分析等。 官网:http://skywalking.apache.org/ 下载:http://skywalking.apache.org/downloads/ Git hub:https://aithub.com/apache/skywalking 文档:https://skywalking.apache.org/docs/main/v8.4.0/readme 中文文档:https://skyapm.github.io/document-cn-translation-of-skywalking/ 版本:v8.3.0升级到v8.4.0 ![](https://xxxxxx/blog/image_43.png "") 下载:http://skywalking.apache.org/downloads/ ### skywalking**安装包** [apache-skywalking-apm-es7-8.5.0.tar.gz](file/apache-skywalking-apm-es7-8.5.0.tar.gz) ![](https://xxxxxx/blog/image_44.png "") ![](https://xxxxxx/blog/image_45.png "") ![](https://xxxxxx/blog/image_46.png "") **默认采用H2内存数据库** ![](https://xxxxxx/blog/image_47.png "") ![](https://xxxxxx/blog/image_48.png "") ![](https://xxxxxx/blog/image_49.png "") ![](https://xxxxxx/blog/image_50.png "") ![](https://xxxxxx/blog/image_51.png "") ```Java -javaagent:C:\Users\SuperZheng\Desktop\apache-skywalking-apm-bin-es7\agent\skywalking-agent.jar -DSW_AGENT_NAME=GatewayApplication -DSW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800 ``` ![](https://xxxxxx/blog/image_52.png "") ![](https://xxxxxx/blog/image_53.png "") ### 持久化 ![](https://xxxxxx/blog/image_54.png "") mysql驱动: ![](https://xxxxxx/blog/image_55.png "") 将驱动复制到安装包: ![](https://xxxxxx/blog/image_56.png "") ![](https://xxxxxx/blog/image_57.png "") ### 自定义业务Skywalking链路追踪 ```Java org.apache.skywalking apm-toolkit-trace 8.5.0 ``` ![](https://xxxxxx/blog/image_58.png "") ![](https://xxxxxx/blog/image_59.png "") ![](https://xxxxxx/blog/image_60.png "") ### 性能剖析 ![](https://xxxxxx/blog/image_61.png "") 请求5次,每个10ms采样一次 ![](https://xxxxxx/blog/image_62.png "") ### ~~**分析出了springboot第一次接口访问慢的问题**~~(暂时无效) SpringBoot的接口第一次访问都很慢,通过日志可以发现,dispatcherServlet不是一开始就加载的,有访问才开始加载的,即懒加载。 ```Java 2019-01-25 15:23:46.264 INFO 1452 --- [nio-8080-exec-1] Initializing Spring FrameworkServlet 'dispatcherServlet' 2019-01-25 15:23:46.265 INFO 1452 --- [nio-8080-exec-1] FrameworkServlet 'dispatcherServlet': initialization started 2019-01-25 15:23:46.395 INFO 1452 --- [nio-8080-exec-1] FrameworkServlet 'dispatcherServlet': initialization completed in 130 ms ``` 这样对于我们来说是一个问题。 在SpringBoot的配置文件中添加以下配置即可: ```Java spring: mvc: servlet: # 当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个servlet load-on-startup: 1 ``` ### 日志记录 引入插件依赖 ```Java org.apache.skywalking apm-toolkit-logback-1.x 8.5.0 ``` ![](https://xxxxxx/blog/image_63.png "") ```Java # 假如skywalking没有部署在本地,就需要配置以下内容到 agent/config/agent.config plugin.toolkit.log.grpc.reporter.server_host=${SW_GRPC_LOG_SERVER_HOST:127.0.0.1} plugin.toolkit.log.grpc.reporter.server_port=${SW_GRPC_LOG_SERVER_PORT:11800} plugin.toolkit.log.grpc.reporter.max_message_size=${SW_GRPC_LOG_MAX_MESSAGE_SIZE:10485760} plugin.toolkit.log.grpc.reporter.upstream_timeout=${SW_GRPC_LOG_GRPC_UPSTREAM_TIMEOUT:30} ``` **文档地址** https://[github.com/apache/skywalking/blob/v8.5.0/docs/en/setup/service-agent/java-agent/Application-toolkit-logback-1.x.md](http://github.com/apache/skywalking/blob/v8.5.0/docs/en/setup/service-agent/java-agent/Application-toolkit-logback-1.x.md) ![](https://xxxxxx/blog/image_64.png "") **加上[%!t(MISSING)id]这个就为traceId** logback-spring.xml ```XML -%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p})[%tid] %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx} %d{yyyy-MM-dd HH:mm:ss.SSS} [%tid] [%thread] %-5level %logger{36} -%msg%n ``` ![](https://xxxxxx/blog/image_65.png "") ### 告警规则 **文档地址** [https://github.com/apache/skywalking/blob/v8.5.0/docs/en/setup/backend/backend-alarm.md](https://github.com/apache/skywalking/blob/v8.5.0/docs/en/setup/backend/backend-alarm.md) ![](https://xxxxxx/blog/image_66.png "") #### 回调告警信息,当发送告警信息的时候,skywalking发送告警信息到其他地方 ![](https://xxxxxx/blog/image_67.png "") ![](https://xxxxxx/blog/image_68.png "") ![](https://xxxxxx/blog/image_69.png "") ### Skywalking高可用部署 ![](https://xxxxxx/blog/image_70.png "") ![](https://xxxxxx/blog/image_71.png "") ![](https://xxxxxx/blog/image_72.png "") ![](https://xxxxxx/blog/image_73.png "") ![](https://xxxxxx/blog/image_74.png "") ## 十三、分布式事务解决方案seata **版本说明:** [https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明](https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明) **官方文档****:** [https://seata.io/zh-cn/docs/overview/what-is-seata.html](https://seata.io/zh-cn/docs/overview/what-is-seata.html) Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了** AT、TCC、SAGA 和 XA **事务模式,为用户打造一站式的分布式解决方案。**seata推荐 AT 模式** ![](https://xxxxxx/blog/image_75.png "") #### TC (Transaction Coordinator) - 事务协调者(单独部署服务) 维护全局和分支事务的状态,驱动全局事务提交或回滚。 #### TM (Transaction Manager) - 事务管理器 定义全局事务的范围:开始全局事务、提交或回滚全局事务。 #### RM (Resource Manager) - 资源管理器 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。 常见分布式事务解决方案 1、seata阿里分布式事务框架 2、消息队列 3、saga 4、XA **他们有一个共同点,都是两阶段(2PC)**。“两阶段“是指完成整个分布式事务,划分成两个步骤完成。 这四种常见的分布式事务解决方案, 分别对应着分布式事务的四种模式:**AT、TCC、Saga、XA**; 四种分布式事务模式,都有各自的理论基础,分别在不同的时间被提出;每种模式都有它的适用场景,同样每个模式也都诞生有各自的代表产品;而这些代表产品, 可能就是我们常见的(全局事务、基于可靠消息、最大努力通知、TCC) 。 ### 写隔离 - 一阶段本地事务提交前,需要确保先拿到 **全局锁** 。 - 拿不到 **全局锁** ,不能提交本地事务。 - 拿 **全局锁** 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。 ![](https://xxxxxx/blog/image_76.png "") ![](https://xxxxxx/blog/image_77.png "") ### 读隔离 ![](https://xxxxxx/blog/image_78.png "") ### AT模式 ![](https://xxxxxx/blog/image_79.png "") ![](https://xxxxxx/blog/image_80.png "") ### TCC模式 ![](https://xxxxxx/blog/image_81.png "") AT 模式([参考链接 TBD](https://seata.io/zh-cn/docs/overview/what-is-seata.html))基于 **支持本地 ACID 事务** 的 **关系型数据库**: - 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。 - 二阶段 commit 行为:马上成功结束,**自动** 异步批量清理回滚日志。 - 二阶段 rollback 行为:通过回滚日志,**自动** 生成补偿操作,完成数据回滚。 相应的,TCC 模式,不依赖于底层数据资源的事务支持: - 一阶段 prepare 行为:调用 **自定义** 的 prepare 逻辑。 - 二阶段 commit 行为:调用 **自定义** 的 commit 逻辑。 - 二阶段 rollback 行为:调用 **自定义** 的 rollback 逻辑。 所谓 TCC 模式,是指支持把 **自定义** 的分支事务纳入到全局事务的管理中。 *国内主要的开源TCC分布式事务框架包括 框架名称 Github地址 star数量* *tcc-transaction *[*https://github.com/changmingxie/tcc-transaction*](https://github.com/changmingxie/tcc-transaction)* 2446 * *Hmily *[*https://github.com/yu199195/hmily*](https://github.com/yu199195/hmily)* 1381 * *ByteTCC *[*https://github.com/liuyangming/ByteTCC*](https://github.com/liuyangming/ByteTCC)* 1300 * *EasyTransaction *[*https://github.com/QNJR-GROUP/EasyTransaction*](https://github.com/QNJR-GROUP/EasyTransaction)* 904* ![](https://xxxxxx/blog/image_82.png "") ### 2PC的问题 1.同步阻塞参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行,倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去 2.单点在2PC中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的 全部状态信息(例如已等待Prepare响应的时长等) , 所以也无法顺利处理上一个事务 3.数据不一致Commit事务过程中Commit请求/Rollback请求可能因为协调者宕机或协调者与参与者网络问题丢失, 那么就导致了部分参与者没有收到Commit/Rollback请求, 而其他参与者则正常收到执行了Commit/Rollback操作, 没有收到请求的参与者则继续阻塞。这时, 参与者之间的数据就不再一致了当参与者执行Commit/Rollback后会向协调者发送Ack, 然而协调者不论是否收到所有的参与者的Ack, 该事务也不会再有其他补救措施了, 协调者能做的也就是等待超时后像事务发起者返回一个*我不确定该事务是否成功 4, 环境可靠性依赖协调者Prepare请求发出后, 等待响应, 然而如果有参与者宕机或与协调者之间的网络中断, 都会导致协调者无法收到所有参与者的响应, 那么在2PC中,协调者会等待一定时间,然后超时后,会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的,这种机制对网络问题常见的现实环境来说太苛刻了 ### 可靠事务的解决方案最终一致(消息队列) ![](https://xxxxxx/blog/image_83.png "") ![](https://xxxxxx/blog/image_84.png "") ### 部署Seata(Server端) Seata分TC、TM和RM三个角色,**TC(Server端)**为单独服务端部署,**TM和RM(Client端)**由业务系统集成。 [https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html](https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html) 资源目录 [https://github.com/seata/seata/tree/1.3.0/script](https://github.com/seata/seata/tree/1.3.0/script) - client > 存放client端sql脚本 (包含 undo_log表) ,参数配置 - config-center > 各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件 - server > server端数据库脚本 (包含 lock_table、branch_table 与 global_table) 及各个容器配置 ### 启动并安装Server Server端存储模式(store.mode)现有file、db、redis三种(后续将引入raft,mongodb), file模式无需改动,直接启动即可 注: file模式为单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高; db**(db version 5.7+)**模式为高可用模式,全局事务会话信息通过db共享,相应性能差些; redis模式Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置. **下载地址:** [https://github.com/seata/seata/releases](https://github.com/seata/seata/releases) **已经配置好的服务端包:** [seata-1.3.0-server.zip](file/seata-1.3.0-server.zip) **服务端表:** ```Java -- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS `global_table` ( `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(96), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; ``` 启动包: seata-->conf-->file.conf,修改store.mode="db或者redis" ![](https://xxxxxx/blog/image_85.png "") 启动包: seata-->conf-->file.conf,修改store.db或store.redis相关属性。 ![](https://xxxxxx/blog/image_86.png "") **修改注册中心** ![](https://xxxxxx/blog/image_87.png "") **修改配置中心** ![](https://xxxxxx/blog/image_88.png "") ![](https://xxxxxx/blog/image_89.png "") ![](https://xxxxxx/blog/image_90.png "") ![](https://xxxxxx/blog/image_91.png "") ![](https://xxxxxx/blog/image_92.png "") ![](https://xxxxxx/blog/image_93.png "") ![](https://xxxxxx/blog/image_94.png "") **将配置推送到nacos** **script文件夹是从源码拉下来的** ![](https://xxxxxx/blog/image_95.png "") ![](https://xxxxxx/blog/image_96.png "") ```Java sh nacos-config.sh -h 81.69.43.78 -p 8848 -g SEATA_GROUP ``` ![](https://xxxxxx/blog/image_97.png "") **检查已经成功推送到nacos** ![](https://xxxxxx/blog/image_98.png "") **需要config.txt的数据源改为db 然后通过nacos-config.sh注册到配置中心** ![](https://xxxxxx/blog/image_99.png "") ### 启动Seata Server ![](https://xxxxxx/blog/image_100.png "") ![](https://xxxxxx/blog/image_101.png "") ![](https://xxxxxx/blog/image_102.png "") ![](https://xxxxxx/blog/image_103.png "") ### 客户端整合seata 涉及到分布式事务的服务引入依赖: ```Java com.alibaba.cloud spring-cloud-starter-alibaba-seata ``` 【**各个微服务】对应的数据库添加 undo_log 表** ```SQL -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table'; ``` 涉及到分布式事务的客户端配置: ```YAML # 分布式事务的分组 和 配置文件中要一致 spring: cloud: alibaba: seata: tx-service-group: chengdu # 分布式事务seata相关配置 seata: registry: # 配置 seata的注册中心,告诉 seata client 怎么去访问 seata server(TC) type: nacos nacos: server-addr: 81.69.43.78:8848 # nacos地址 application: seata-server # seata server的服务名 username: nacos password: nacos group: SEATA_GROUP ``` ### seata遇到的异常错误 ### seata 1.4.x版本在MySQL8.0下DATETIME类型转换错误的问题 ```Java com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` ``` ![](https://xxxxxx/blog/image_104.png "") 客户端引入jar包 ```Java com.esotericsoftware kryo 4.0.2 de.javakaffee kryo-serializers 0.44 ``` ### seata测试结果 ![](https://xxxxxx/blog/image_105.png "") ![](https://xxxxxx/blog/image_106.png "") | 服务端 | feign端 | 服务端是否手动抛出异常 | 服务端全局事务 | feign全局事务 | 是否全部都回滚 | | ------ | ------- | ---------------------- | -------------- | ------------------ | ------------------------- | | 正确 | 出错 | 是 | 是 | 否 | 回滚 | | 正确 | 出错 | 否 | 是 | 是 | 不回滚 | | 错误 | 正确 | 是 | 是 | 是 | 回滚 | | 正确 | 出错 | 否 | 是 | 否(单体事务注解) | feign端回滚,服务端不回滚 | **综上所述:** **服务端必须检查 feign 是否调用正确,若feign调用失败则抛出异常**,让两边都回滚 ## 十四、springsecurity安全认证授权 引入依赖 ```Java org.springframework.boot spring-boot-starter-security ``` 第一次引入依赖后,所有的接口访问都需要认证 会默认跳转到登录接口http://ip:port/login ![](https://xxxxxx/blog/image_107.png "") 默认退出登录接口 http://ip:port/logout ### 登录校验流程 ![](https://xxxxxx/blog/image_108.png "") ### springSecurity完整流程 ![](https://xxxxxx/blog/image_109.png "") **UsernamePasswordAuthenticationFilter**:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的**认证**工作主要有它负责。 **ExceptionTranslationFilter**:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException **FilterSecuritylnterceptor**:负责**权限校验**的过滤器 ![](https://xxxxxx/blog/image_110.png "") ![](https://xxxxxx/blog/image_111.png "") ### 认证流程 ![](https://xxxxxx/blog/image_112.png "") 自定义UserDetailService从数据库中通过登录账号查询密码,返回后与Authentication对象里面的密码对比 ![](https://xxxxxx/blog/image_113.png "") ![](https://xxxxxx/blog/image_114.png "") **登录** ①自定义登录接口 调用ProviderManager的方法进行认证如果认证通过生成jwt 把用户信息存入redis中 ②自定义UserDetailsService 在这个实现列中去查询数据 **校验** ①定义Jwt认证过滤器 ②获取token ③解析token获取其中的userid ④从redis中获取用户信息 ⑤存入SecurityContextHolder(定义过滤器存入这里面) ```Java org.springframework.boot spring-boot-starter-data-redis com.alibaba fastjson 1.2.78 com.auth0 java-jwt 3.16.0 ``` ### redis使用fastjson序列化 ```Java import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.serializer.SerializerFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** * @author zb * @date 2022/3/10 0:25 * @Description Redis 使用 FastJson 序列化 */ public class FastJsonRedisSerializer implements RedisSerializer { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private Class tClass; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } public FastJsonRedisSerializer(Class tClass){ super(); this.tClass = tClass; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } try { return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } catch (Exception e) { throw new SerializationException("Could not serialize: " + e.getMessage(), e); } } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length <= 0){ return null; } String str = new String(bytes, DEFAULT_CHARSET); try { return JSON.parseObject(str, tClass); } catch (Exception e) { throw new SerializationException("Could not deserialize: " + e.getMessage(), e); } } protected JavaType getJavaType(Class tClass){ return TypeFactory.defaultInstance().constructType(tClass); } } ``` ```Java import cn.mesmile.system.serializer.FastJsonRedisSerializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author zb * @date 2022/3/10 0:35 * @Description fastjson序列化 * KryoRedisSerializer的压缩率和速度最优,fastJson次之,默认的则最差 */ @Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory){ RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); // 使用 StringRedisSerializer 来序列化和反序列化 Redis 的key template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(fastJsonRedisSerializer); // hash的key 也使用 StringRedisSerializer 来序列化和反序列化 Redis 的key template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(fastJsonRedisSerializer); template.afterPropertiesSet(); return template; } ``` ### 自定义类实现UserDetailsService ```Java @RequiredArgsConstructor @Service public class UserDetailsServiceImpl implements UserDetailsService { private final UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 通过账号查询用户 LambdaQueryWrapper eq = Wrappers.lambdaQuery() .eq(User::getUsername, username); User user = userService.getOne(eq); if (user == null){ throw new ServiceException("用户名或密码错误"); } if (!user.getEnabled()){ throw new ServiceException("用户已禁用,请联系管理员"); } // todo 通过用户查询权限 return new UserLogin(user); } } ``` 自定类实现 UserDetails ```Java @NoArgsConstructor @AllArgsConstructor @Data public class UserLogin implements UserDetails { private User user; /** * 返回用户的授权信息 * @return */ @Override public Collection getAuthorities() { return null; } /** * 返回用户密码 * @return */ @Override public String getPassword() { return user.getPassword(); } /** * 返回用户名 * @return */ @Override public String getUsername() { return user.getUsername(); } /** * 判断账号是否 没过期 * @return */ @Override public boolean isAccountNonExpired() { return true; } /** * 判断账号是否 没锁定 * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 证书是否 没过期 * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 * @return */ @Override public boolean isEnabled() { return true; } } ``` springsecurity有一个默认的密码编码,若使用明文密码则需要在数据库密码前加上{noop} ![](https://xxxxxx/blog/image_115.png "") ### 密码加密存储 实际项目中我们不会把密码明文存储在数据库中。 **默认使用的Password Encoder要求数据库中的密码格式为:{id} password。**它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中, SpringSecurity就会使用该PasswordEncoder来进行密码校验。 我们可以定义一个SpringSecurity的配置类, SpringSecurity要求这个配置类要继承WebSecurityConf gurerAdapter。 ```Java @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } } ``` ![](https://xxxxxx/blog/image_116.png "") ## Spring Security认证 接下我们需要自定义登陆接口, 然后让SpringSecurity对这个接口放行, 让用户访问这个接口的时候不用登录也能访问。 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证, 所以需要在SecurityConfig中配置把 AuthenticationManager注入容器 认证成功的话要生成一个jwt, 放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户, 我们需要把用户信息存入redis, 可以把用户id作为key。 **登录** ①自定义登录接口 调用ProviderManager的方法进行认证如果认证通过生成jwt 把用户信息存入redis中 ②自定义UserDetailsService 在这个实现列中去查询数据 ![](https://xxxxxx/blog/image_117.png "") ### 自定义登录接口 ```Java @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } // 自定义认证管理,自定义 认证管理 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 配置 放行接口 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // super.configure(http); http // 前后端分离 关闭 csrf .csrf().disable() // 前后端分离 session不管用 不通过 Session 获取 SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() // 除了上面的请求,其他请求都需要鉴权认证 .anyRequest().authenticated(); // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前 // http //.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } } ``` 登录service方法 ```Java @Override public String login(User user) { String principal = user.getUsername(); String credentials = user.getPassword(); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(principal, credentials); // 用户认证 AuthenticationManager authenticate 进行用户认证 Authentication authenticate = authenticationManager.authenticate(authenticationToken); if (authenticate == null){ throw new ServiceException("登录失败"); } // UserLogin 实现了 UserDetails 接口 // 认证前 principal 为username ,认证后 principal 为 UserDetails 的实现类 UserLogin userLogin = (UserLogin) authenticate.getPrincipal(); // user = userLogin.getUser(); // 使用username生成一个jwt 存入返回结果 String token = JwtUtil.sign(user.getUsername()); // 把完整用户信息存入 redis userid作为key cloudRedisUtil.set(user.getUsername(),userLogin); return token; } ``` ### 定义认证过滤器 JwtAuthenticationTokenFilter ```Java @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private final CloudRedisUtil cloudRedisUtil; public JwtAuthenticationTokenFilter(CloudRedisUtil cloudRedisUtil) { this.cloudRedisUtil = cloudRedisUtil; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 解析token String token = request.getHeader("Authorization"); // 如果为空放行 if (StringUtils.isBlank(token)) { filterChain.doFilter(request, response); return; } boolean tokenExpired = JwtUtil.isTokenExpired(token); if (tokenExpired){ filterChain.doFilter(request, response); return; } String username = JwtUtil.getClaim(token, "username"); // redis中获取用户 UserLogin userLogin = (UserLogin) cloudRedisUtil.get(username); if (userLogin == null){ filterChain.doFilter(request, response); return; // throw new RuntimeException("用户未登录"); } SecurityContext securityContext = SecurityContextHolder.getContext(); // todo 查询权限放入 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userLogin, null, null); // 将认证信息存入 SecurityContextHolder securityContext.setAuthentication(authenticationToken); // 放行 filterChain.doFilter(request, response); } } ``` ### 配置认证过滤器 **// 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前** ![](https://xxxxxx/blog/image_118.png "") ```Java @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) { this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter; } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 配置 放行接口 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // super.configure(http); http // 前后端分离 关闭 csrf .csrf().disable() // 前后端分离 session不管用 不通过 Session 获取 SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() // 除了上面的请求,其他请求都需要鉴权认证 .anyRequest().authenticated(); // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前 http .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } } ``` ### 自定义退出登录 我们只需要定义一个登陆接口, 然后获取SecurityContextHolder中的认证信息, 删除redis中对应的数据即可。 自定义退出登录方法 ```Java @Override public boolean logout() { // 调用注销接口的时候需要携带token // 从 SecurityContextHolder 请求中获取认证信息,然后再获取username SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); if (authentication == null){ return false; } // 认证之后 principal 里面是 UserDetails 的子类 // 未认证的时候 principal 里面是 username (登录账号) Object principal = authentication.getPrincipal(); // UserLogin 实现了 UserDetails 接口 UserLogin userLogin = (UserLogin) principal; User user = userLogin.getUser(); String username = user.getUsername(); // 删除redis中的token return cloudRedisUtil.delete(username); } ``` ## Spring Security授权权限 在Spring Security中, 会使用默认的**FilterSecurityInterceptor**来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication, 然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。 所以我们在项目中只需要把当前登录用户的**权限信息也存入Authentication** ```Java SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); ``` ### 基于注解的方式限制访问资源权限 ![](https://xxxxxx/blog/image_119.png "") 开启全局基于注解方式的权限认证: ![](https://xxxxxx/blog/image_120.png "") 权限判断注解@PreAuthorize ```Java // 判断用户是否有 test 这个权限,必须具有 test 这个权限才可以访问 @PreAuthorize("hasAuthority('test')") @GetMapping("/list") public R listUser(){ List list = userService.listUser(); return R.data(list); } ``` ### 其他方式进行权限认证 前面都是使用@PreAuthorize注解, 然后在在其中使用的是hasAuthority方法进行校验。Spring Security还为我们提供了其它方法 例如:hasAnyAuthority, hasRole, hasAnyRole, 等。 这里我们先不急着去介绍这些方法, 我们先去理解hasAuthority的原理, 然后再去学习其他方法你就更容易理解, 而不是死记硬背区别。并且我们也可以选择定义校验方法,实现我们自己的校验逻辑。 hasAuthority方法实际是执行到了**SecurityExpressionRoot**的**hasAuthority**, 大家只要断点调试既可知道它内部的校验原理。 它内部其实是调用**authentication**的**getAuthorities**方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。 ![](https://xxxxxx/blog/image_121.png "") hasAnyAuthority方法可以传入多个权限, 只有用户有其中任意一个权限都可以访问对应资源。 ```Java @PreAuthorize("hasAnyAuthority('title:list','title:list2')") @GetMapping("/list") public R listUser(){ List list = userService.listUser(); return R.data(list); } ``` hasRole要求有对应的角色才可以访问, 但是它**内部**会把我们传入的参数拼接上**ROLE_**后再去比较。所以这种情况下要用用户对应的权限也要有**ROLE_**这个前缀才可以。数据库中代码角色的权限要加上 **ROLE_** 代码里面不用添加,以下情况就能访问: ![](https://xxxxxx/blog/image_122.png "") ```Java @PreAuthorize("hasAnyRole('title:list2')") @GetMapping("/list") public R listUser(){ List list = userService.listUser(); return R.data(list); } ``` hasAnyRole有任意的角色就可以访问。它内部也会把我们传入的参数拼接上ROLE_后再去比较。所以这种情况下要用用户对应的权限也要有ROLE_这个前缀才可以。 ### 自定义权限校验方法 **自定义权限校验方法** CloudSecurityExpression ```Java /** * @author zb * @Description 自定义权限校验方法 */ @Component("cloudSecurityExpression") public class CloudSecurityExpression { public final boolean hasAuthority(String authority){ return hasAnyAuthority(authority); } private boolean hasAnyAuthority(String... authoritys) { // 获取拥有的认证信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); UserLogin userLogin = (UserLogin) authentication.getPrincipal(); Set permissionSet = userLogin.getPermissionSet(); for (String authority : authoritys) { if (permissionSet.contains(authority)) { return true; } } return false; } public final boolean hasRole(String role) { return hasAnyRole(role); } private boolean hasAnyRole(String... roles) { // 获取拥有的认证信息 return hasAnyAuthority(roles); } } ``` 使用自定义权限校验 ```Java @PreAuthorize("@cloudSecurityExpression.hasAuthority('title:list2')") @GetMapping("/list") public R listUser(){ List list = userService.listUser(); return R.data(list); } ``` ### 基于配置的权限控制 ```Java /** * @author zb * @Description 配置 springSecurity密码加密的方式 * @EnableGlobalMethodSecurity(prePostEnabled = true) 开启基于注解的权限授权控制 */ @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; private final AccessDeniedHandlerImpl accessDeniedHandler; private final AuthenticationEntryPointImpl authenticationEntryPoint; public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, AccessDeniedHandlerImpl accessDeniedHandler, AuthenticationEntryPointImpl authenticationEntryPoint) { this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter; this.accessDeniedHandler = accessDeniedHandler; this.authenticationEntryPoint = authenticationEntryPoint; } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 自定义认证管理 登录、退出 * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 配置 放行接口 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】 // super.configure(http); http // 前后端分离 关闭 csrf .csrf().disable() // 前后端分离 session不管用 不通过 Session 获取 SecurityContext 这里关闭 session 功能 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问,当携带token的时候不能访问 // .permitAll() 表示 登录和未登录 都可以访问 .antMatchers("/user/login").anonymous() // 除了上面的请求, .authenticated() 任意用户都可以认证后访问 .anyRequest().authenticated(); // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前 http .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 将自定义的 认证 和 授权 异常处理加入到配置 http.exceptionHandling() .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint); // 开启springSecurity允许跨域 http.cors(); } } ``` ### 查询到用户的权限信息放入 UserLogin UserDetailsServiceImpl ```Java @RequiredArgsConstructor @Service public class UserDetailsServiceImpl implements UserDetailsService { private final UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 通过账号查询用户 LambdaQueryWrapper eq = Wrappers.lambdaQuery() .eq(User::getUsername, username); User user = userService.getOne(eq); if (user == null){ throw new ServiceException("用户名或密码错误"); } if (!user.getEnabled()){ throw new ServiceException("用户已禁用,请联系管理员"); } // todo 通过用户从数据库中查询权限 List permissionList = new ArrayList<>(Arrays.asList("test", "admin")); return new UserLogin(user, permissionList); } } ``` ### UserLogin接收转换权限 ```Java @NoArgsConstructor @Data public class UserLogin implements UserDetails { /** * 用户 */ private User user; /** * 权限集合 */ private List permissionList; public UserLogin(User user, List permissionList){ this.user = user; this.permissionList = permissionList; } /** * 此变量无需序列化 */ @JSONField(serialize = false) private Set grantedAuthorities; /** * 返回用户的授权信息 * @return */ @Override public Collection getAuthorities() { if (grantedAuthorities == null){ grantedAuthorities = permissionList.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); } return grantedAuthorities; } /** * 返回用户密码 * @return */ @Override public String getPassword() { return user.getPassword(); } /** * 返回用户名 * @return */ @Override public String getUsername() { return user.getUsername(); } /** * 判断账号是否 没过期 * @return */ @Override public boolean isAccountNonExpired() { return true; } /** * 判断账号是否 没锁定 * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 证书是否 没过期 * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 * @return */ @Override public boolean isEnabled() { return true; } } ``` ### JwtAuthenticationTokenFilter 过滤器放入权限 ```Java @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private final CloudRedisUtil cloudRedisUtil; public JwtAuthenticationTokenFilter(CloudRedisUtil cloudRedisUtil) { this.cloudRedisUtil = cloudRedisUtil; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 解析token String token = request.getHeader("Authorization"); // 如果为空放行 if (StringUtils.isBlank(token)) { filterChain.doFilter(request, response); return; } boolean tokenExpired = JwtUtil.isTokenExpired(token); if (tokenExpired){ filterChain.doFilter(request, response); return; } String username = JwtUtil.getClaim(token, "username"); // redis中获取用户 UserLogin userLogin = (UserLogin) cloudRedisUtil.get(username); if (userLogin == null){ filterChain.doFilter(request, response); return; // throw new RuntimeException("用户未登录"); } SecurityContext securityContext = SecurityContextHolder.getContext(); // todo 查询权限放入 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userLogin, null, userLogin.getAuthorities()); // 将认证信息存入 SecurityContextHolder securityContext.setAuthentication(authenticationToken); // 放行 filterChain.doFilter(request, response); } } ``` ### RBAC权限模型 RBAC权限模型(Role-Based AccessControl) 即:**基于角色的权限控制**。这是目前最常被开发者使用也是相对易用、通用权限模型。 **用户** 多对多 **角色** 多对多 **权限** ## 自定义失败处理 我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json, 这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道Spring Security的异常处理机制。 在Spring Security中, 如果我们在认证或者授权的过程中出现了异常会被**ExceptionTranslationFilter**捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。 如果是**认证过程**中出现的异常会被封装成**AuthenticationException**然后调用**AuthenticationEntryPoint**对象的方法去进行异常处理。 如果是**授权过程**中出现的异常会被封装成**AccessDeniedException** 然后调用**AccessDeniedHandler**对象的方法去进行异常处理。 所以如果我们需要自定义异常处理, 我们只需要自定义**AuthenticationEntryPoint**和**AccessDeniedHandler**然后配置给SpringSecurity即可。 ### 自定义 **认证异常** 拦截处理 ```Java @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { R r = R.fail(ResultCode.CLIENT_UN_AUTHORIZED, "请求未认证"); String result = JSONObject.toJSONString(r); WebUtil.renderString(response, result); } } ``` ### 自定义** 授权异常** 拦截处理 ```Java @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException { R r = R.fail(ResultCode.REQ_REJECT, "请求未授权"); String result = JSONObject.toJSONString(r); WebUtil.renderString(response, result); } } ``` ```Java public static String renderString(HttpServletResponse response,String text) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().print(text); } catch (IOException e) { e.printStackTrace(); } return ""; } ``` ### 加入到security配置中 ```Java @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; private final AccessDeniedHandlerImpl accessDeniedHandler; private final AuthenticationEntryPointImpl authenticationEntryPoint; public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, AccessDeniedHandlerImpl accessDeniedHandler, AuthenticationEntryPointImpl authenticationEntryPoint) { this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter; this.accessDeniedHandler = accessDeniedHandler; this.authenticationEntryPoint = authenticationEntryPoint; } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 自定义认证管理 登录、退出 * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 配置 放行接口 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】 // super.configure(http); http // 前后端分离 关闭 csrf .csrf().disable() // 前后端分离 session不管用 不通过 Session 获取 SecurityContext 这里关闭 session 功能 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问,当携带token的时候不能访问 // .permitAll() 表示 登录和未登录 都可以访问 .antMatchers("/user/login").anonymous() // 除了上面的请求, .authenticated() 任意用户都可以认证后访问 .anyRequest().authenticated(); // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前 http .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 将自定义的 认证 和 授权 异常处理加入到配置 http.exceptionHandling() .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint); } } ``` ## 配置支持跨域请求 ```Java /** * @author zb * @Description 配置支持跨域请求 */ @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 设置允许跨域请求的域名 .allowedOrigins("*") // 设置是否允许cookie .allowCredentials(true) // 设置允许请求方式 .allowedMethods("GET","POST","DELETE","PUT") // 设置允许的 header 属性 .allowedHeaders("*") // 设置跨域允许时间 .maxAge(3600); } } ``` **开启springSecurity允许跨域** // 开启springSecurity允许跨域
http.cors(); ```Java @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; private final AccessDeniedHandlerImpl accessDeniedHandler; private final AuthenticationEntryPointImpl authenticationEntryPoint; public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, AccessDeniedHandlerImpl accessDeniedHandler, AuthenticationEntryPointImpl authenticationEntryPoint) { this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter; this.accessDeniedHandler = accessDeniedHandler; this.authenticationEntryPoint = authenticationEntryPoint; } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 自定义认证管理 登录、退出 * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 配置 放行接口 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】 // super.configure(http); http // 前后端分离 关闭 csrf .csrf().disable() // 前后端分离 session不管用 不通过 Session 获取 SecurityContext 这里关闭 session 功能 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问,当携带token的时候不能访问 // .permitAll() 表示 登录和未登录 都可以访问 .antMatchers("/user/login").anonymous() // 除了上面的请求, .authenticated() 任意用户都可以认证后访问 .anyRequest().authenticated(); // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前 http .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 将自定义的 认证 和 授权 异常处理加入到配置 http.exceptionHandling() .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint); // 开启springSecurity允许跨域 http.cors(); } } ``` ## CSRF攻击 **前后端【不分离】的项目,通常包验证信息存放在cookie中,导致如下图的攻击:** ![](https://xxxxxx/blog/image_123.png "") **前后端分离的信息,利用每次请求头中 带上 token 信息,天然的防范的csrf 攻击** SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token, 前端发起请求的时候需要携带这个csrf_token, 后端会有过滤器进行校验, 如果没有携带或者是伪造的就不允许访问 可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在**前后端分离的项目中我们的认证信息其实是token, 而token并不是存储中cookie中, 并且需要前端代码去把token设置到请求头中才可以, 所以CSRF攻击也就不用担心了** 因此可以关闭csrf,不然框架会检查 csrf_token // 前后端分离 关闭 csrf http.csrf().disable() ```Java @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 配置 放行接口 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】 // super.configure(http); http // 前后端分离 关闭 csrf .csrf().disable() // 前后端分离 session不管用 不通过 Session 获取 SecurityContext 这里关闭 session 功能 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问,当携带token的时候不能访问 // .permitAll() 表示 登录和未登录 都可以访问 .antMatchers("/user/login").anonymous() // 除了上面的请求, .authenticated() 任意用户都可以认证后访问 .anyRequest().authenticated(); // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前 http .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 将自定义的 认证 和 授权 异常处理加入到配置 http.exceptionHandling() .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint); // 开启springSecurity允许跨域 http.cors(); } } ``` ## 登录成功处理器(一般没使用) 实际上在**UsernamePasswordAuthenticationFilter**进行登录认证的时候, 如果登录成功了是会调用**AuthenticationSuccessHandler**的方法进行认证成功后的处理的。**AuthenticationSuccessHandler**就是登录成功处理器。我们也可以自己去自定义成功处理器进行成功后的相应处理。 ![](https://xxxxxx/blog/image_124.png "") 自定义类实现登录成功拦截器 AuthenticationSuccessHandler ```Java /** * @author zb * @date 2022/3/11 15:50 * @Description 自定原生的登录成功处理器 * 注意:配置了 * * @Bean * @Override * public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();} * * 后就不会走 UsernamePasswordAuthenticationFilter 过滤器,就不会再调用登录成功拦截器了 AuthenticationSuccessHandler */ @Component public class CloudSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Object principal = authentication.getPrincipal(); System.out.println(">>>>>>>>>>>>>>>> 登录成功:"+ principal.toString()); } } ``` 配置自定义登录成功拦截器 ```Java @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CloudSuccessHandler cloudSuccessHandler; /** * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】 // super.configure(http); // 配置登录成功,登录成功拦截器 http.formLogin().successHandler(cloudSuccessHandler); http.authorizeRequests().anyRequest().authenticated(); } } ``` ![](https://xxxxxx/blog/image_125.png "") ## 登录失败处理器(一般不用) 实际上在**UsernamePasswordAuthenticationFilter**进行登录认证的时候, 如果登录失败了是会调用**AuthenticationFailureHandler** 的方法进行认证失败后的处理的。**AuthenticationFailureHandler** 就是登录失败处理器。我们也可以自己去自定义失败处理器进行失败后的相应处理。 **注意注释内容** ```Java /** * @author zb * 自定原生的登录失败处理器 * 注意:配置了 * * @Bean * @Override * public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();} * * 后就不会走 UsernamePasswordAuthenticationFilter 过滤器,就不会再调用登录成功拦截器了 AuthenticationSuccessHandler */ */ public class CloudAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println("-----------进入自定义登录失败拦截器------------"); } } ``` 配置自定义登录成功拦截器 ```Java ​```Java @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CloudAuthenticationFailureHandler failureHandler; /** * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】 // super.configure(http); // 配置登录失败,登录失败拦截器 http.formLogin().failureHandler(failureHandler); http.authorizeRequests().anyRequest().authenticated(); } } ``` ``` ## 退出登录成功处理器 自定义退出登录成功拦截器 ​```Java /** * @author zb * @Description 自定原生的退出登录成功处理器 * 注意:配置了 * * @Bean * @Override * public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();} * * 后就不会走 LogoutFilter 过滤器,就不会再调用退出登录成功拦截器了 LogoutSuccessHandler */ @Component public class CloudLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("------------进入到 退出登录成功了-------------"); } } ``` 配置自定义退出登录拦截器 ```Java @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CloudLogoutSuccessHandler cloudLogoutSuccessHandler; /** * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】 // super.configure(http); http.logout().logoutSuccessHandler(cloudLogoutSuccessHandler); } } ``` ![](https://xxxxxx/blog/image_126.png "") ## 自动登录(前后端不分离的情况) ![](https://xxxxxx/blog/image_127.png "") ![](https://xxxxxx/blog/image_128.png "") ![](https://xxxxxx/blog/image_129.png "") ![](https://xxxxxx/blog/image_130.png "") ![](https://xxxxxx/blog/image_131.png "") ## SpringCloud权限认证 用户分配角色 角色分配权限 ![](https://xxxxxx/blog/image_132.png "") ![](https://xxxxxx/blog/image_133.png "") ![](https://xxxxxx/blog/image_134.png "") [https://www.bilibili.com/video/BV15a411A7kP?p=28](https://www.bilibili.com/video/BV15a411A7kP?p=28)