Spring Cloud微服务网关Zuul过滤链和整合OAuth2+JWT入门实战

news2024/11/23 18:12:30

一、Spring Cloud Zuul 过滤链

1.1 工作原理

Zuul的核心逻辑是由一系列的Filter来实现的,他们能够在进行HTTP请求或者相应的时候执行相关操作。Zuul Filter的主要特性有一下几点:

  • Filter的类型:Filter的类型决定了它在Filter链中的执行顺序。路由动作发生前、路由动作发生时,路由动作发生后,也可能是路由过程发生异常时。
  • Filter的执行顺序:同一种类型的Filter可以通过filterOrder()方法来设定执行顺序
  • Filter的执行条件:Filter运行所需要的条件
  • Filter的执行效果:符合某个Filter执行条件,产生的执行效果

Zuul内部提供了一个动态读取、编译和运行这些Filter的机制。Filter之间不能直接通信,在请求线程中通过RequestContext来共享状态,它的内部是用ThreadLocal实现的。

上图描述了Zuul关于Filter的请求生命周期。

  • pre:在Zuul按照规则路由到下级服务之前执行。如果需要对请求进行预处理,比如鉴权、限流等,可在考虑在这类Filter中实现。
  • route:这类Filter是Zuul路由动作的执行者,是Http客户端构建和发送HTTP请求的地方。
  • post:这类Filter是在原服务返回结果或者异常信息发生后执行,如果需要对返回信息做一些处理,可以在此类Filter进行处理。
  • error:在整个生命周期内如果发生异常,则会进入error Filter,可以做全局异常处理

其中post Filter抛出错误分成两种情况:

1)在post Filter抛错之前,pre、route Filter没有抛错,此时会进入ZuulException的逻辑,打印堆栈信息,然后再返回status=500的Error信息

2)再post Filter跑错之前,pre、route Filter已有跑错,此时不会打印堆栈信息,直接返回status=500的error信息。

也就是说整个责任链中重点不只是post Filter,还可能是error Filter。

在实际项目中,需要子实现以上类型的Filter来对链路进行处理,根据业务的需求,选取对应生命周期的Filter来达到目的。每个Filter之间通过RequestContext(Zuul包中)类来进行通信,内部采用ThreadLocal保存每个请求的一些信息,包括请求路由,错误信息,HttpServletRequest,HttpServletResponse,这使得一些操作十分可靠,它害扩展了ConcurrentHashMap,目的是为了在处理过程中保存任何形式的信息。

1.2 Zuul中的原生Filter

Zuul Server通过@EnableZuulProxy开启之后,搭配Spring Boot Actuator,会多两个管控断点。

在配置文件中配置一下:

management:
  endpoints:
    web:
      exposure:
        include: 'routes,filters'
复制代码

1、/route:返回当前Zuul Server中已生成的映射规则,加上/details可查看明细。例如

每个路由的详细信息

2、/filters:返回当前Zuul Filter中已注册生效的Filter

从Filter的信息可以看到,所有已经注册生效的Filter的信息:Filter实现类的路径、Filter执行次序、是否被禁用、是否静态。而且很明显地可以看出Zuul内Filter的整个请求的生命流程,如下图:

Zuul中各内置的Filter:

名称类型次序描述
ServletDetectionFilterpre-3通过Spring Dispatcher检查请求是否通过
Servlet30WrapperFilterpre-2适配HttpServletRequest为Servlet30RequestWrapper对象
FormBodyWrapperFilterpre-1解析表单数据并为下游请求重新编码
DebugFiterpre1Debug路由表示
PreDecorationFilterpre5处理请求上下文共后续使用,设置下游相关信息头
RibbonRoutingFilterroute10使用Ribbon、Hystrix或者嵌入式HTTP客户端发送请求
SimpleHostRoutingFilterroute100使用Apache Httpclient转发请求
SendForwardFilterroute500使用Servlet转发请求
SendResponseFilterpost1000将代理请求的响应写入当前相应
SendErrorFiltererror0如果RequestContext.getThrowable()不为空,则转发到error.path配置的路径

上表为使用@EnableZuulProxy之后安装的Filter,当使用@EnableZuulServer将会缺少PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter。这些原生的Filter可以关掉,例如:在配置文件里面配置zuul.SendErrorFilter.error.disable=true

1.3 多过滤器组成过滤链

在实际中我们不仅是只定义一个过滤器,而是多个过滤器组成过滤链来完成工作,除了Zuul的其他网关也是有这个功能

要在Zuul中自定义Filter子需要继承ZuulFilter即可。它是个抽象类,主要实现的几个方法:

  • String filterType():使用返回值定义Filter的类型,有pre、route、post、error
  • int filterOrder():使用返回值设置Filter的执行顺序
  • boolean shouldFilter():使用返回值设置Filter是否执行,即所定义Filter的开关
  • Object run():Filter里面的核心执行逻辑便需要写在该方法里面

自定义一个前置过滤器,如下:

public class CustomPreFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        LOG.info("This is custom pre filter...");
        return null;
    }
}
复制代码

FirstPreFilter注入到Spring Bean容器

@Configuration
public class ZuulFilterConfig {

    @Bean
    public CustomPreFilter customPreFilter() {
        return new CustomPreFilter();
    }
}
复制代码

然后启动分别启动eurekazuulservice-a,访问http://localhost:88/servicea/add?a=1&b=2。观察网关的日志输出

INFO 20260 --- [ XNIO-1 task-1] c.m.better.zuul.filter.CustomPreFilter : This is custom pre filter...

到这可以看到定义一个Zuul过滤器其实很简单,对于微服务网关来说不仅是Zuul,其他的微服务网关也是,很大部分的开发工作都是开发各种过滤器来达到我们目的。现在来实现一个简单的参数校验功能:

FirstPreFilter:

public class FirstPreFilter extends ZuulFilter {
    private Logger log = LoggerFactory.getLogger(FirstPreFilter.class);

    @Override
    public String filterType() {
        // 自定义的过滤器类型为前置过滤器
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 自定义过滤器的执行次序
        return 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        log.info("first pre filter...");
        // 拿到请求上下文
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 拿到HttpServletRequest
        HttpServletRequest request = requestContext.getRequest();
        // 获取传入的参数值
        String a = request.getParameter("a");
        if (StringUtils.isBlank(a)) {
            // 禁止路由,也就是不允许访问下游服务
            requestContext.setSendZuulResponse(false);
            // 设置响应结果,供PostFilter使用,参数是字符串,序列化一下返回对象也行。
            ObjectMapper mapper = new ObjectMapper();
            Map<String, Object> map = new HashMap<>();
            map.put("code", -1);
            map.put("msg", "参数a不能为空");
            String result = null;
            try {
                result = mapper.writeValueAsString(map);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            requestContext.setResponseBody(result);
            // parameter-check-success保存于上下文,作为同类型下游Filter的执行开关
            requestContext.set("parameter-check-success", false);
            return null;
        }
        // 设置避免报空
        requestContext.set("parameter-check-success", true);
        return null;
    }
}
复制代码

SecondPreFilter:

public class SecondPreFilter extends ZuulFilter {
    private Logger log = LoggerFactory.getLogger(SecondPreFilter.class);

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 3;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 参数a是否检验成功,不成功那就没必要继续执行下去
        return (boolean) requestContext.get("parameter-check-success");
    }

    @Override
    public Object run() throws ZuulException {
        log.info("second pre filter...");
        // 拿到请求上下文
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 拿到HttpServletRequest
        HttpServletRequest request = requestContext.getRequest();
        // 获取传入的参数值
        String b = request.getParameter("b");
        if (StringUtils.isBlank(b)) {
            // 禁止路由,也就是不允许访问下游服务
            requestContext.setSendZuulResponse(false);
            // 设置响应结果,供PostFilter使用,参数是字符串,序列化一下返回对象也行。
            ObjectMapper mapper = new ObjectMapper();
            Map<String, Object> map = new HashMap<>();
            map.put("code", -1);
            map.put("msg", "参数b不能为空");
            String result = null;
            try {
                result = mapper.writeValueAsString(map);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            requestContext.setResponseBody(result);
            // parameter-check-success保存于上下文,作为同类型下游Filter的执行开关
            requestContext.set("parameter-check-success", false);
            return null;
        }
        return null;
    }
}
复制代码

CustomPostFilter:

public class CustomPostFilter extends ZuulFilter {

    private static final Logger LOG = LoggerFactory.getLogger(CustomPostFilter.class);

    @Override
    public String filterType() {
        return POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        System.out.println("这是PostFilter!");
        // 从RequestContext获取上下文
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 处理返回中文乱码
        requestContext.getResponse().setCharacterEncoding("UTF-8");
        // 获取上下文中保存的responseBody
        String responseBody = requestContext.getResponseBody();
        // 如果responseBody不为空,则说明流程有异常发生
        if (null != responseBody) {
            //设定返回状态码
            requestContext.setResponseStatusCode(500);
            //替换响应报文
            requestContext.setResponseBody(responseBody);
        }
        return null;
    }
}
复制代码

这整个小功能实现下来,体验到了Zuul中过滤器的执行顺序,以及通过RequestContext来获取HttpServletRequest得到请求信息。

二、Spring Cloud Zuul整合OAuth2+JWT入门实战

作为一个微服务网关,一般我们会在网关上进行鉴权,对于网关后面众多的无状态服务常用的授权和认证便是基于OAuth2。

2.1 什么是OAuth2和JWT

OAuth2是OAuth协议的第二个版本,是对授权认证比较成熟地面向资源的授权协议,在业界中广泛应用。出了定义了常用的用户名密码登录之后,还可以使用第三方一个用登录。例如在某些网站上可以使用QQ、微信、Github等进行登录。其主要流程如下:

至于JWT则是一种使用JSON格式来规约Token和Session的协议。因为传统的认证方式中会产生一个凭证,比如Session会话是保存在服务端,然后依赖于Cookie返回给客户端,Session是有状态的。但是对于众多的微服务来说又是无状态,便诞生像JWT这样的解决方案。

JWT通常有三部分组成:

  • Header:头部,指定JWT使用的签名算法
  • Payload:载荷,包含一些自定义或非自定义的认证信息
  • Signature:签名,将头部和载荷用.连接之后,使用头部的签名算法生成的签名信息并拼接到末尾

OAuth2 + JWT 就是服务端使用OAuth2的方式进行认证,然后颁发一个Token,而这个Token使用JWT。客户端拿着这个Token,便可以访问系统,一般我们会给这个Token设置一个有效期,因为服务端并不会保存这个Token。OAuth2的实现有很多,这里使用Spring社区的基于Spring Security实现的OAuth2

2.2 Zuul + OAuth2 + JWT 入门实操

2.2.1 修改cloud-zuul-gateway

在Zuul网关中我们需要对接口的请求进行保护,判断是否登录鉴权。如果未登录需要重定向到登录页面,登录成功由认证服务器颁发JWT Token;把JWT Token放到请求头传递到下游服务器。

引入Maven依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
复制代码

配置文件:

  • 首先定义了service-a服务的路由规则
  • 注册中心Eureka的地址
  • 验证授权端点:http://localhost:7788/uaa/oauth/authorize
  • Token的颁发端点:http://localhost:7788/uaa/oauth/token
  • 默认是使用HS256加密算法,密钥是hahaha。加密算法的话建议使用安全性更高的非堆成加密
server:
  port: 88
spring:
  application:
    name: zuul-gateway
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8671}/eureka/
  instance:
    prefer-ip-address: true
zuul:
  routes:
    service-a:
      path: /servicea/**
      serviceId: service-a
security:
  oauth2:
    client:
      access-token-uri: http://localhost:7788/uaa/oauth/token #令牌端点
      user-authorization-uri: http://localhost:7788/uaa/oauth/authorize #授权端点
      client-id: zuul-gateway #OAuth2客户端ID
      client-secret: my-secret #OAuth2客户端密钥
    resource:
      jwt:
        key-value: hahaha #使用对称加密方式,默认算法为HS256
复制代码

WebSecurity的配置:主要是声明

@Configuration
@Order(101)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login", "/servicea/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf()
                .disable();
    }
}

复制代码

在启动类上添加@EnableOAuth2Sso注解

@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
@EnableOAuth2Sso
public class ZuulServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }
}
复制代码

2.2.2 编写认证服务器cloud-auth-server

创建cloud-auth-server来基于OAuth2 实现我们的认证服务器。依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-zuul-practice-intermediate</artifactId>
        <groupId>com.msr.better</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-auth-server</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
复制代码

配置文件application.yml

spring:
  application:
    name: cloud-auth-server
server:
  port: 7788
  servlet:
    contextPath: /uaa
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8671}/eureka/
  instance:
    prefer-ip-address: true
复制代码

认证服务器配置:继承AuthorizationServerConfigurerAdapter编写认证授权服务器配置。主要是指定clientId、密钥、以及权限定义和作用域声明,指定JwtTokenStore,类似的实现Spring Security还有RedisTokenStore等。

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                .inMemory()
                .withClient("zuul-gateway")
                .secret("my-secret")
                .scopes("write", "read").autoApprove(true)
                .authorities("WRIGTH_READ", "WRIGTH_WRITE")
                .authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code")
                .redirectUris("http://localhost:88/login");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .tokenStore(jwtTokenStore())
                .tokenEnhancer(jwtTokenConverter())
                .authenticationManager(authenticationManager);
    }

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtTokenConverter());
    }

    @Bean
    protected JwtAccessTokenConverter jwtTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("hahaha");
        return converter;
    }
}
复制代码

Web Security 相关配置:声明guest用户,密码为guest,拥有READ权限。admin用户,密码为admin,拥有READ、WRITE权限。

AuthenticationManager是认证管理器,需要注入到Spring容器中。passwordEncoder()声明密码的加密方式,在Spring Security中要求需要对密码进行加密,因此需要向Spring容器中注入。但是这里使用了内存的方式存放用户信息,而且密码是原值保存,所以使用NoOpPasswordEncoder,即不做加密处理。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }

    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("guest").password("guest").authorities("READ")
                .and()
                .withUser("admin").password("admin").authorities("READ", "WRITE");
    }
}
复制代码

认证服务器启动类:

@SpringBootApplication
@EnableDiscoveryClient
public class AuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}
复制代码

2.2.3 cloud-service-a服务整合资源服务器

service-a的编写相对简单,在Spring Security OAuth2中,每个服务都是一个资源服务器,拥有者该服务的资源。

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
复制代码

配置文件:

server:
  port: 8080
spring:
  application:
    name: service-a
eureka:
  client:
    serviceUrl:
      defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8671}/eureka/
  instance:
    prefer-ip-address: true
复制代码

编写资源服务器:

@Configuration
public class ServiceAResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .authorizeRequests()
                .antMatchers("/**").authenticated()
                .antMatchers(HttpMethod.GET, "/servicea/test")
                .hasAuthority("WRIGHT_READ");
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("WRIGHT")
                .tokenStore(jwtTokenStore());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();
        tokenConverter.setSigningKey("hahaha");
        return tokenConverter;
    }

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
}
复制代码

编写ClientController:

@RestController
@RequestMapping
public class ClientController {

    @GetMapping("/test")
    public String test(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("================header================");
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            System.out.println(key + ": " + request.getHeader(key));
        }
        System.out.println("================header================");
        return "hello word!";
    }
}
复制代码

servicea的启动类:启用资源服务器@EnableResourceServer

@SpringBootApplication
@EnableDiscoveryClient
@EnableResourceServer
public class ServiceAApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceAApplication.class, args);
    }
}
复制代码

2.2.4 测试

先启动注册中心Eureka、然后启动Zuul网关、serivce-a、auth-server。

请求访问:

http://localhost:88/service/test

OAuth2 + JWT 实战小总结

这里关于Zuul整合OAuth2 + JWT 的介绍就到这,后面会写一篇详细的Spring Security实现的OAuth2文章。本文这里用到的认证服务器和资源服务器是较为早期的写法了,前年Spring Security开了一个新项目专门来编写认证服务器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/432319.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决Ubunt20.04安装Sogou输入法失败进不去桌面 及 中文输入法安装

目录解决Ubunt20.04安装Sogou输入法失败进不去桌面中文输入法安装解决wps无法输入中文解决Ubunt20.04安装Sogou输入法失败进不去桌面 问题&#xff1a; Ubuntu20.04 安装了 fcitx 和 sogou 输入法&#xff1b;键盘输入法系统由 IBus 改成了 fcitx&#xff1b;重启后可以出现登…

文心一言 VS chatgpt (11)-- 算法导论2.3 7题 3.1 4~5题

四、2 ^ (n1)O(2 ^ n)成立吗? 2^(2n)O(2 ^ n)成立吗? 文心一言&#xff1a; chatgpt&#xff1a; 第一个式子不成立&#xff0c;第二个式子成立。 2 ^ (n1) O(2 ^ n) 该式不成立。按照大O符号的定义&#xff0c;如果存在正常数c和n0&#xff0c;使得n>n0时&#xf…

旋转矩阵与欧拉角

其他相关的内容网上很多&#xff0c;这里就简单记录一下不同欧拉角分解顺序时&#xff0c;对应的角度怎么计算 #include <opencv2/opencv.hpp> #include <iostream>using namespace cv; using namespace std; #define PI acos(-1)void getEulerAngles(Mat& ro…

Spring项目整合Minio分布式的对象存储系统

文章目录 安装Docker安装Docker-compose安装 SpringBoot集成引入依赖初始化客户端存储桶的CRUD存储桶的文件操作存储桶生命周期配置 安装 Docker安装 docker run -p 9000:9000 -p 9090:9090 \--nethost \--name minio \-d --restartalways \-e "MINIO_ACCESS_KEYadmin&q…

01-Linux-磁盘分区与目录配置

1. 主引导纪录和磁盘分区表 1.1 MBR分区表 启动引导程序记录区与分区表都放在磁盘的第一个扇区&#xff08;512B&#xff09; 由于分区表仅占 64B&#xff0c;因此最多能有四组记录区&#xff0c;每组记录区记录了该区段的起始与结束的柱面号码。 缺点如下&#xff1a; 操作…

设计模式-创建型模式之工厂方法模式(Factory Method Pattern)

3.工厂方法模式(Factory Method Pattern)3.1. 模式动机现在对该系统进行修改&#xff0c;不再设计一个按钮工厂类来统一负责所有产品的创建&#xff0c;而是将具体按钮的创建过程交给专门的工厂子类去完成&#xff0c;我们先定义一个抽象的按钮工厂类&#xff0c;再定义具体的工…

vue项目导入excel成功后下载导入结果(后端返回的list数组)

需求&#xff1a; 点击批量导入按钮&#xff0c;弹出弹窗。 下载模板如图二 上传后&#xff0c;如果有错误&#xff0c;会弹出提示&#xff0c;如图三 点击查看失败原因&#xff0c;会自动下载失败的excel如图四。 请求参数和返回结果 1. vue项目导出表格功能实现步骤 np…

十七、市场活动明细:添加备注

功能需求 用户在市场活动明细页面,输入备注内容,点击"保存"按钮,完成添加市场活动备注的功能. *备注内容不能为空 *添加成功之后,清空输入框,刷新备注列表 *添加失败,提示信息,输入框不清空,列表也不刷新 功能分析 流程图 代码实现 一、ActivityRemarkMapper 1.Ac…

笔记-Samba服务器的安装与配置

引言 代码编写我们是在Windows系统下的VS Code来编辑&#xff0c;但是代码在虚拟机的Ubuntu系统中&#xff0c;所以我们要先实现如何将在Ubuntu下的项目映射到Windows系统中&#xff0c;这时我们可以使用到samba服务器。 一、安装samba服务器 sudo apt-get install samba我这里…

【瑞吉外卖】003 -- 后台退出功能开发

本文章为对 黑马程序员Java项目实战《瑞吉外卖》的学习记录 目录 一、需求分析 二、代码开发 三、功能测试 四、分析后台页面构成和效果展示 1、Vue & Element 2、iframe 一、需求分析 前端页面分析&#xff1a; 前端页面&#xff0c;点击事件 点击事件 logout()&#x…

使用vscode写UML图

文章目录 环境配置关键字多图注释Title多行title图注头部或尾部添加注释多行header/footer放大率类图接口抽象类枚举 类型关系泛化关系&#xff0c;箭头指向父类实现关系&#xff0c;箭头指向接口依赖关系&#xff0c;箭头指向被依赖关系关联关系&#xff0c;指向被拥有者可以双…

QML控件--DelayButton

文章目录 一、控件基本信息二、控件使用三、属性四、信号 一、控件基本信息 Import Statement&#xff1a;import QtQuick.Controls 2.14 Since&#xff1a;Qt 5.9 Inherits&#xff1a;AbstractButton 二、控件使用 DelayButton是一个延时按钮&#xff0c;需要长按才能触发&…

WPF教程(二)--Application WPF程序启动方式

1.Application介绍 WPF与WinForm一样有一个 Application对象来进行一些全局的行为和操作&#xff0c;并且每个 Domain &#xff08;应用程序域&#xff09;中仅且只有一个 Application 实例存在。和 WinForm 不同的是WPF Application默认由两部分组成 : App.xaml 和 App.xaml.…

SpringBoot单元测试断言 assertions

断言 断言&#xff08;assertions&#xff09;是测试方法中的核心部分&#xff0c;用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别&#xff1a; 1、简单断言 2、数组断言 通过 …

类型转换-空间配置器

文章目录 类型转换C语言的类型转换隐式类型转换&#xff1a;强制类型的转换&#xff1a;C类型转换缺点 C强制类型转换static_castreinterpret_castconst_castdynamic_cast explicit 空间配置器(STL专用内存池)SGI-STL空间配置器实现原理一级空间配置器二级空间配置器内存池SGI-…

stm32启动过程(以F1为例)

文章目录 STM32启动模式STM32启动过程&#xff08;以内部flash启动为例&#xff09;启动文件功能启动文件常用汇编指令及说明 STM32启动模式 M3/M4/M7等内核复位后&#xff0c;做的第一件事&#xff1a; 从地址 0x0000 0000 处取出 堆栈指针 MSP 的初始值&#xff0c;该值为栈…

接踵而至,昆仑万维天工大语言模型发布

目录 天工大语言模型对标GPT3.5对话能力多模态应用同行对比后言 天工大语言模型 国产ChatGPT再次迎来新成员&#xff0c;4月17日下午&#xff0c;昆仑万维正式发布千亿级大语言模型“天工”&#xff0c;同时宣布即日起启动邀请测试&#xff0c;并注册了chatgpt.cn作为域名。天…

认真复习c语言1

最近好好写总结了&#xff08;不能在偷懒了&#xff09;&#xff1a;这一次绝对认真&#xff0c;写总结写了三个多小时&#xff0c;学了一个小时左右TAT&#xff1b; 今天来复习一下c语言的多文件操作&#xff1a; 经过我这几天的摸索&#xff0c;我算是明白了&#xff1a; …

Typora (windows/MacOs版) 你未曾见过的Markdown编辑器

简介 Typora的设计理念非常人性化&#xff0c;与传统编辑器相比&#xff0c;更注重用户的视觉效果和阅读体验。它的编辑界面非常简洁&#xff0c;用户可以就近编辑&#xff0c;随时查看效果&#xff0c;避免频繁地转换预览模式&#xff0c;提高了工作效率和灵活性。 此外&…

d2l解码编码器与seq2seq

seq2seq难死了&#xff0c;卡了好久&#xff0c;好不容易有些头绪了。。。 目录 1.编码器与解码器 1.1原理 1.2实现 2.seq2seq 2.1构造编码器 2.2构造解码器 repeat与cat探索 总结nn.rnn\GRU\LSTM输入输出 看一下解码器的输出 2.3损失计算 2.4训练 2.5预测 2.6预…