手把手教大家在 gRPC 中使用 JWT 完成身份校验

news2024/10/5 15:34:41

文章目录

    • 1. JWT 介绍
      • 1.1 无状态登录
        • 1.1.1 什么是有状态
        • 1.1.2 什么是无状态
      • 1.2 如何实现无状态
      • 1.3 JWT
        • 1.3.1 简介
        • 1.3.2 JWT数据格式
        • 1.3.3 JWT 交互流程
        • 1.3.4 JWT 存在的问题
    • 2. 实践
      • 2.1 项目创建
      • 2.2 grpc_api
      • 2.3 grpc_server
      • 2.4 grpc_client
    • 3. 小结

上篇文章松哥和小伙伴们聊了在 gRPC 中如何使用拦截器,这些拦截器有服务端拦截器也有客户端拦截器,这些拦截器的一个重要使用场景,就是可以进行身份的校验。当客户端发起请求的时候,服务端通过拦截器进行身份校验,就知道这个请求是谁发起的了。今天松哥就来通过一个具体的案例,来和小伙伴们演示一下 gRPC 如何结合 JWT 进行身份校验。

1. JWT 介绍

1.1 无状态登录

1.1.1 什么是有状态

有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 Tomcat 中的 Session。例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:

  • 服务端保存大量数据,增加服务端压力
  • 服务端保存用户状态,不支持集群化部署

1.1.2 什么是无状态

微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:

  • 服务端不保存任何客户端请求者信息
  • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

那么这种无状态性有哪些好处呢?

  • 客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
  • 服务端的集群和状态对客户端透明
  • 服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)
  • 减小服务端存储压力

1.2 如何实现无状态

无状态登录的流程:

  • 首先客户端发送账户名/密码到服务端进行认证
  • 认证通过后,服务端将用户信息加密并且编码成一个 token,返回给客户端
  • 以后客户端每次发送请求,都需要携带认证的 token
  • 服务端对客户端发送来的 token 进行解密,判断是否有效,并且获取用户登录信息

1.3 JWT

1.3.1 简介

JWT,全称是 Json Web Token, 是一种 JSON 风格的轻量级的授权和身份认证规范,可实现无状态、分布式的 Web 应用授权:

JWT 作为一种规范,并没有和某一种语言绑定在一起,常用的 Java 实现是 GitHub 上的开源项目 jjwt,地址如下:https://github.com/jwtk/jjwt

1.3.2 JWT数据格式

JWT 包含三部分数据:

  • Header:头部,通常头部有两部分信息:

    • 声明类型,这里是JWT
    • 加密算法,自定义

我们会对头部进行 Base64Url 编码(可解码),得到第一部分数据。

  • Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:

    • iss (issuer):表示签发人
    • exp (expiration time):表示token过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号

这部分也会采用 Base64Url 编码,得到第二部分数据。

  • Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过 Header 中配置的加密算法生成。用于验证整个数据完整和可靠性。

生成的数据格式如下图:

注意,这里的数据通过 . 隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展示方便而已。

1.3.3 JWT 交互流程

流程图:

步骤翻译:

  1. 应用程序或客户端向授权服务器请求授权
  2. 获取到授权后,授权服务器会向应用程序返回访问令牌
  3. 应用程序使用访问令牌来访问受保护资源(如 API)

因为 JWT 签发的 token 中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了 RESTful 的无状态规范。

1.3.4 JWT 存在的问题

说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如下:

  1. 续签问题,这是被很多人诟病的问题之一,传统的 cookie+session 的方案天然的支持续签,但是 jwt 由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入 redis,虽然可以解决问题,但是 jwt 也变得不伦不类了。
  2. 注销问题,由于服务端不再保存用户信息,所以一般可以通过修改 secret 来实现注销,服务端 secret 修改后,已经颁发的未过期的 token 就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。
  3. 密码重置,密码重置后,原本的 token 依然可以访问系统,这时候也需要强制修改 secret。
  4. 基于第 2 点和第 3 点,一般建议不同用户取不同 secret。

当然,为了解决 JWT 存在的问题,也可以将 JWT 结合 Redis 来用,服务端生成的 JWT 字符串存入到 Redis 中并设置过期时间,每次校验的时候,先看 Redis 中是否存在该 JWT 字符串,如果存在就进行后续的校验。但是这种方式有点不伦不类(又成了有状态了)。

2. 实践

我们来看下 gRPC 如何结合 JWT。

2.1 项目创建

首先我先给大家看下我的项目结构:

├── grpc_api
│   ├── pom.xml
│   └── src
├── grpc_client
│   ├── pom.xml
│   └── src
├── grpc_server
│   ├── pom.xml
│   └── src
└── pom.xml

还是跟之前文章中的一样,三个模块,grpc_api 用来存放一些公共的代码。

grpc_server 用来放服务端的代码,我这里服务端主要提供了两个接口:

  1. 登录接口,登录成功之后返回 JWT 字符串。
  2. hello 接口,客户端拿着 JWT 字符串来访问 hello 接口。

grpc_client 则是我的客户端代码。

2.2 grpc_api

我将 protocol buffers 和一些依赖都放在 grpc_api 模块中,因为将来我的 grpc_server 和 grpc_client 都将依赖 grpc_api。

我们来看下这里需要的依赖和插件:

<dependencies>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-netty-shaded</artifactId>
        <version>1.52.1</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf</artifactId>
        <version>1.52.1</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-stub</artifactId>
        <version>1.52.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>annotations-api</artifactId>
        <version>6.0.53</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
<build>
    <extensions>
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.6.2</version>
        </extension>
    </extensions>
    <plugins>
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version>
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}</protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.51.0:exe:${os.detected.classifier}</pluginArtifact>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

这里的依赖和插件松哥在本系列的第一篇文章中都已经介绍过了,唯一不同的是,这里引入了 JWT 插件,JWT 我使用了比较流行的 JJWT 这个工具。JJWT 松哥在之前的文章和视频中也都有介绍过,这里就不再啰嗦了。

先来看看我的 Protocol Buffers 文件:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "org.javaboy.grpc.api";
option java_outer_classname = "LoginProto";
import "google/protobuf/wrappers.proto";

package login;

service LoginService {
  rpc login (LoginBody) returns (LoginResponse);
}

service HelloService{
  rpc sayHello(google.protobuf.StringValue) returns (google.protobuf.StringValue);
}

message LoginBody {
  string username = 1;
  string password = 2;
}

message LoginResponse {
  string token = 1;
}

经过前面几篇文章的介绍,这里我就不多说啦,就是定义了两个服务:

  • LoginService:这个登录服务,传入用户名密码,返回登录成功之后的令牌。
  • HelloService:这个就是一个打招呼的服务,传入字符串,返回也是字符串。

定义完成之后,生成对应的代码即可。

接下来再定义一个常量类供 grpc_server 和 grcp_client 使用,如下:

public interface AuthConstant {
    SecretKey JWT_KEY = Keys.hmacShaKeyFor("hello_javaboy_hello_javaboy_hello_javaboy_hello_javaboy_".getBytes());
    Context.Key<String> AUTH_CLIENT_ID = Context.key("clientId");
    String AUTH_HEADER = "Authorization";
    String AUTH_TOKEN_TYPE = "Bearer";
}

这里的每个常量我都给大家解释下:

  1. JWT_KEY:这个是生成 JWT 字符串以及进行 JWT 字符串校验的密钥。
  2. AUTH_CLIENT_ID:这个是客户端的 ID,即客户端发送来的请求携带了 JWT 字符串,通过 JWT 字符串确认了用户身份,就存在这个变量中。
  3. AUTH_HEADER:这个是携带 JWT 字符串的请求头的 KEY。
  4. AUTH_TOKEN_TYPE:这个是携带 JWT 字符串的请求头的参数前缀,通过这个可以确认参数的类型,常见取值有 Bearer 和 Basic。

如此,我们的 gRPC_api 就定义好了。

2.3 grpc_server

接下来我们来定义 gRPC_server。

首先来定义登录服务:

public class LoginServiceImpl extends LoginServiceGrpc.LoginServiceImplBase {
    @Override
    public void login(LoginBody request, StreamObserver<LoginResponse> responseObserver) {
        String username = request.getUsername();
        String password = request.getPassword();
        if ("javaboy".equals(username) && "123".equals(password)) {
            System.out.println("login success");
            //登录成功
            String jwtToken = Jwts.builder().setSubject(username).signWith(AuthConstant.JWT_KEY).compact();
            responseObserver.onNext(LoginResponse.newBuilder().setToken(jwtToken).build());
            responseObserver.onCompleted();
        }else{
            System.out.println("login error");
            //登录失败
            responseObserver.onNext(LoginResponse.newBuilder().setToken("login error").build());
            responseObserver.onCompleted();
        }
    }
}

省事起见,我这里没有连接数据库,用户名和密码固定为 javaboy 和 123。

登录成功之后,就生成一个 JWT 字符串返回。

登录失败,就返回一个 login error 字符串。

再来看我们的 HelloService 服务,如下:

public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
    @Override
    public void sayHello(StringValue request, StreamObserver<StringValue> responseObserver) {
        String clientId = AuthConstant.AUTH_CLIENT_ID.get();
        responseObserver.onNext(StringValue.newBuilder().setValue(clientId + " say hello:" + request.getValue()).build());
        responseObserver.onCompleted();
    }
}

这个服务就更简单了,不啰嗦。唯一值得说的是 AuthConstant.AUTH_CLIENT_ID.get(); 表示获取当前访问用户的 ID,这个用户 ID 是在拦截器中存入进来的。

最后,我们来看服务端比较重要的拦截器,我们要在拦截器中从请求头中获取到 JWT 令牌并解析,如下:

public class AuthInterceptor implements ServerInterceptor {
    private JwtParser parser = Jwts.parser().setSigningKey(AuthConstant.JWT_KEY);

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata, ServerCallHandler<ReqT, RespT> serverCallHandler) {
        String authorization = metadata.get(Metadata.Key.of(AuthConstant.AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER));
        Status status = Status.OK;
        if (authorization == null) {
            status = Status.UNAUTHENTICATED.withDescription("miss authentication token");
        } else if (!authorization.startsWith(AuthConstant.AUTH_TOKEN_TYPE)) {
            status = Status.UNAUTHENTICATED.withDescription("unknown token type");
        } else {
            Jws<Claims> claims = null;
            String token = authorization.substring(AuthConstant.AUTH_TOKEN_TYPE.length()).trim();
            try {
                claims = parser.parseClaimsJws(token);
            } catch (JwtException e) {
                status = Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e);
            }
            if (claims != null) {
                Context ctx = Context.current()
                        .withValue(AuthConstant.AUTH_CLIENT_ID, claims.getBody().getSubject());
                return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler);
            }
        }
        serverCall.close(status, new Metadata());
        return new ServerCall.Listener<ReqT>() {
        };
    }
}

这段代码逻辑应该好理解:

  1. 首先从 Metadata 中提取出当前请求所携带的 JWT 字符串(相当于从请求头中提取出来)。
  2. 如果第一步提取到的值为 null 或者这个值不是以指定字符 Bearer 开始的,说明这个令牌是一个非法令牌,设置对应的响应 status 即可。
  3. 如果令牌都没有问题的话,接下来就进行令牌的校验,校验失败,则设置相应的 status 即可。
  4. 校验成功的话,我们就会获取到一个 Jws 对象,从这个对象中我们可以提取出来用户名,并存入到 Context 中,将来我们在 HelloServiceImpl 中就可以获取到这里的用户名了。
  5. 最后,登录成功的话,Contexts.interceptCall 方法构建监听器并返回;登录失败,则构建一个空的监听器返回。

最后,我们再来看看启动服务端:

public class LoginServer {
    Server server;

    public static void main(String[] args) throws IOException, InterruptedException {
        LoginServer server = new LoginServer();
        server.start();
        server.blockUntilShutdown();
    }

    public void start() throws IOException {
        int port = 50051;
        server = ServerBuilder.forPort(port)
                .addService(new LoginServiceImpl())
                .addService(ServerInterceptors.intercept(new HelloServiceImpl(), new AuthInterceptor()))
                .build()
                .start();
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            LoginServer.this.stop();
        }));
    }

    private void stop() {
        if (server != null) {
            server.shutdown();
        }
    }

    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }
}

这个跟之前的相比就多加了一个 Service,添加 HelloServiceImpl 服务的时候,多加了一个拦截器,换言之,登录的时候,请求是不会被这个认证拦截器拦截的。

好啦,这样我们的 grpc_server 就开发完成了。

2.4 grpc_client

接下来我们来看 grpc_client。

先来看登录:

public class LoginClient {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();
        LoginServiceGrpc.LoginServiceStub stub = LoginServiceGrpc.newStub(channel);
        login(stub);
    }

    private static void login(LoginServiceGrpc.LoginServiceStub stub) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        stub.login(LoginBody.newBuilder().setUsername("javaboy").setPassword("123").build(), new StreamObserver<LoginResponse>() {
            @Override
            public void onNext(LoginResponse loginResponse) {
                System.out.println("loginResponse.getToken() = " + loginResponse.getToken());
            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onCompleted() {
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
    }
}

这个方法直接调用就行了,看过前面几篇 gRPC 文章的话,这里都很好理解。

再来看 hello 接口的调用,这个接口调用需要携带 JWT 字符串,而携带 JWT 字符串,则需要我们构建一个 CallCredentials 对象,如下:

public class JwtCredential extends CallCredentials {
    private String subject;

    public JwtCredential(String subject) {
        this.subject = subject;
    }

    @Override
    public void applyRequestMetadata(RequestInfo requestInfo, Executor executor, MetadataApplier metadataApplier) {
        executor.execute(() -> {
            try {
                Metadata headers = new Metadata();
                headers.put(Metadata.Key.of(AuthConstant.AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER),
                        String.format("%s %s", AuthConstant.AUTH_TOKEN_TYPE, subject));
                metadataApplier.apply(headers);
            } catch (Throwable e) {
                metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e));
            }
        });
    }

    @Override
    public void thisUsesUnstableApi() {

    }
}

这里就是将请求的 JWT 令牌放入到请求头中即可。

最后来看看调用:

public class LoginClient {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();
        LoginServiceGrpc.LoginServiceStub stub = LoginServiceGrpc.newStub(channel);
        sayHello(channel);
    }

    private static void sayHello(ManagedChannel channel) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        HelloServiceGrpc.HelloServiceStub helloServiceStub = HelloServiceGrpc.newStub(channel);
        helloServiceStub
                .withCallCredentials(new JwtCredential("eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJqYXZhYm95In0.IMMp7oh1dl_trUn7sn8qiv9GtO-COQyCGDz_Yy8VI4fIqUcRfwQddP45IoxNovxL"))
                .sayHello(StringValue.newBuilder().setValue("wangwu").build(), new StreamObserver<StringValue>() {
            @Override
            public void onNext(StringValue stringValue) {
                System.out.println("stringValue.getValue() = " + stringValue.getValue());
            }

            @Override
            public void onError(Throwable throwable) {
                System.out.println("throwable.getMessage() = " + throwable.getMessage());
            }

            @Override
            public void onCompleted() {
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
    }
}

这里的登录令牌就是前面调用 login 方法时获取到的令牌。

好啦,大功告成。

3. 小结

上面的登录与校验只是松哥给小伙伴们展示的一个具体案例而已,在此案例基础之上,我们还可以扩展出来更多写法,但是万变不离其宗,其他玩法就需要小伙伴们自行探索啦~

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

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

相关文章

Docker 如何配置镜像加速

Docker 镜像加速 国内从 DockerHub 拉取镜像有时会遇到困难&#xff0c;此时可以配置镜像加速器。Docker 官方和国内很多云服务商都提供了国内加速器服务&#xff0c;例如&#xff1a; 科大镜像&#xff1a;https://docker.mirrors.ustc.edu.cn/网易&#xff1a;https://hub-…

生态流量数据采集传输协议定制开发(嵌入式水资源SZY206协议以及VC++ POST数据发送)

水电站生态流量在线监测&#xff0c;流量数据采集传输,水资源遥测终端机程序。 背景&#xff1a;现场使用SCJ-LL01多普勒超声波流量计采集生态下泄流量&#xff0c;使用太阳能供电系统&#xff0c;使用SCJ-RTU01遥测终端机进行数据采集&#xff0c;设备采用4G通讯&#xff0c;…

基于MATLAB开发AUTOSAR软件应用层模块-part23.SR interface通信介绍(接收的数据错误时应该如何处理)

在软件SWC之间的AUTOSAR SR通信中,当COM报告接收SWC的数据接收错误时,运行时环境(RTE)触发DataReceiveErrorEvent。该事件可以指示发送方SWC未能在AliveTimeout限制内回复,或者发送方SWC发送了无效数据。 接下来我们就讲解下怎么实现无效数据的接收和判断 还是三步走,建模…

Ubuntu 上 Let‘s Encrypt 生成泛域名证书

安装生成工具certbot&#xff1a; apt install certbot 查看安装在哪&#xff1a; which certbot 使用certbot&#xff08;位置在 /usr/bin/certbot&#xff09;生成证书&#xff1a; /usr/bin/certbot certonly -d *.xxx.com --manual --preferred-challenges dns --ser…

一种全新的图像滤波理论的实验(二)

一、前言 2021年12月31日&#xff0c;我发布了基于加权概率模型的图像滤波算法的第一个实验&#xff0c;当时有两个关键问题没有解决&#xff1a; 1、出现了大面积的黑色区域&#xff0c;最近考虑把这个算法实际应用在图像和视频的压缩领域&#xff0c;于是通过对程序的分析&a…

【论文简述】GMFlow: Learning Optical Flow via Global Matching(CVPR 2022)

一、论文简述 1. 第一作者&#xff1a;Haofei Xu 2. 发表年份&#xff1a;2022 3. 发表期刊&#xff1a;CVPR oral 4. 关键词&#xff1a;光流、代价体、Transformers、全局匹配、注意力机制 5. 探索动机&#xff1a;过去几年中具有代表性的光流学习框架的核心估计方式没有…

Java文件IO及其案例分析

目录 1. 文件概述 1.1 狭义和广义上的文件 1.2 文件的路径 1.3 文件的类型 2. 针对文件系统的操作 3. 针对文件内容的操作&#xff08;文件的读和写&#xff09; 3.1 IO流对象 3.2 文件的读操作&#xff08;字节流&#xff09; 3.3 文件的写操作&#xff08;字节流&#…

内存取证常见例题思路方法-volatility (没有最全 只有更全)

目录 1.从内存文件中获取到用户hacker 的密码并且破解密码&#xff0c;将破解后的密码作为 Flag值提交; 2.获取当前系统的主机名&#xff0c;将主机名作为Flag值提交; 3.获取当前系统浏览器搜索过的关键词&#xff0c;作为Flag提交; 4.获取当前内存文件的 ip地址 5.当前系…

pycharm和navigator打开时出现报错,无法正常打开

1、navigator打开时出现提示&#xff1a; 原因是&#xff1a;python.exe有多个任务在占用。 解决办法&#xff1a; &#xff08;1&#xff09;打开cmd &#xff08;2&#xff09;输入&#xff1a;tasklist | findstr “pythonw” &#xff08;3&#xff09;有几个线程就kill几个…

qt qchart学习

Qt Charts主要由QChartView、QChart、QLegend图例、坐标轴(由QAbstractAxis子类实现)、**数据源(由QAbstractSeries子类实现)**等组成使用QChart的前期准备1. Qt5.9及以上版本&#xff1b;2. .pro文件中添加QT charts3. 在使用QChart的各个控件之前&#xff0c;引用头文件并必…

【前缀和】和为k的子数组 路径总和 III

文章目录和为k的子数组路径总和 III和为k的子数组 动态规划算法&#xff08;超时&#xff09; class Solution { public:int subarraySum(vector<int>& nums, int k) {int n nums.size();vector<vector<int>> dp(n, vector<int>(n, 0));int ans …

b2b b2c o2o分布式电子商务平台源码 mybatis+spring cloud

鸿鹄云商大型企业分布式互联网电子商务平台&#xff0c;推出PC微信APP云服务的云商平台系统&#xff0c;其中包括B2B、B2C、C2C、O2O、新零售、直播电商等子平台。 分布式、微服务、云架构电子商务平台 java b2b2c o2o 技术解决方案 开发语言&#xff1a; java、j2ee 数据库&am…

注解@Transactional 原理和常见的坑

这篇文章&#xff0c;会先讲述 Transactional 的 4 种不生效的 Case&#xff0c;然后再通过源码解读&#xff0c;分析 Transactional 的执行原理&#xff0c;以及部分 Case 不生效的真正原因1 项目准备下面是 DB 数据和 DB 操作接口&#xff1a;uidunameusex1张三女2陈恒男3楼仔…

百万医疗险是什么

一、百万医疗险是什么 从名字可以看出&#xff0c;这是一款医疗险。因为保额高&#xff0c;最高能报销百万&#xff0c;所以叫百万医疗险。 二、百万医疗险有什么用 可以报销被保险人因意外伤害和疾病导致的医疗费用 三、如何挑选 虽然高达几百万的保额&#xff0c;但保额却并非…

cas单点登录-自定义登录界面 / 自定义主题风格(三)

cas单点登录-自定义登录界面 / 自定义主题风格&#xff08;三&#xff09; 在前面的文章中&#xff0c;介绍了使用cas实现SSO单点登录&#xff0c;静态登录&#xff0c;使用mysql数据库登录。但是在登录时都是跳转到了同一个登录界面。 假设现在我有两个子站点app1.com&#x…

【Docker】初识Dcoker以及镜像操作(一)

目录 1.初识Docker 1.1.什么是Docker 1.1.1.应用部署的环境问题 1.1.2.Docker解决依赖兼容问题 1.1.3.Docker解决操作系统环境差异 1.1.4.小结 1.2.Docker和虚拟机的区别 1.3.Docker架构 1.3.1.镜像和容器 1.3.2.DockerHub 1.3.3.Docker架构 1.3.4.小结 1.4.安装D…

Three.js铅笔手绘效果实现

在这个教程中&#xff0c;我们将学习如何使用 Three.js 后处理创建铅笔手绘效果。 我们将完成创建自定义后处理渲染通道、在 WebGL 中实现边缘检测、将法线缓冲区重新渲染到渲染目标以及使用生成和导入的纹理调整最终结果的步骤。 这就是最终结果的样子&#xff0c;让我们开始…

NLP学习笔记(九) 分词(上)

大家好&#xff0c;我是半虹&#xff0c;这篇文章来讲分词算法 1 概述 分词是自然语言处理领域中的基础任务&#xff0c;是文本预处理的重要步骤 简单来说&#xff0c;就是将文本段落分解为基本语言单位&#xff0c;亦可称之为词元 ( token\text{token}token ) 按照粒度的不…

day50【代码随想录】动态规划之不同的子序列、两个字符串的删除操作、编辑距离

文章目录前言一、不同的子序列&#xff08;力扣115&#xff09;【hard】二、两个字符串的删除操作&#xff08;力扣583&#xff09;思路一思路二三、编辑距离&#xff08;力扣72&#xff09;【hard】前言 1、不同的子序列 2、两个字符串的删除操作 3、编辑距离 一、不同的子序…

网页扫描图像并以pdf格式上传到服务器端

本文描述如何通过网页驱动扫描仪、高拍仪等图像扫描设备进行图像扫描&#xff0c;扫描结果经编辑修改后以pdf压缩格式上传到后台java程序中进行服务器端落盘保存。图像扫描上传如文字描述顺序所介绍&#xff0c;先要驱动扫描设备工作&#xff0c;进行纸张数据的光学扫描操作形成…