建网站方案,网站开发项目总结范文,潍坊知名网站建设哪家好,石家庄网站建设电商文章目录1. JWT 介绍1.1 无状态登录1.1.1 什么是有状态1.1.2 什么是无状态1.2 如何实现无状态1.3 JWT1.3.1 简介1.3.2 JWT数据格式1.3.3 JWT 交互流程1.3.4 JWT 存在的问题2. 实践2.1 项目创建2.2 grpc_api2.3 grpc_server2.4 grpc_client3. 小结上篇文章松哥和小伙伴们聊了在 …
文章目录1. JWT 介绍1.1 无状态登录1.1.1 什么是有状态1.1.2 什么是无状态1.2 如何实现无状态1.3 JWT1.3.1 简介1.3.2 JWT数据格式1.3.3 JWT 交互流程1.3.4 JWT 存在的问题2. 实践2.1 项目创建2.2 grpc_api2.3 grpc_server2.4 grpc_client3. 小结上篇文章松哥和小伙伴们聊了在 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 交互流程
流程图 步骤翻译
应用程序或客户端向授权服务器请求授权获取到授权后授权服务器会向应用程序返回访问令牌应用程序使用访问令牌来访问受保护资源如 API
因为 JWT 签发的 token 中已经包含了用户的身份信息并且每次请求都会携带这样服务的就无需保存用户信息甚至无需去数据库查询这样就完全符合了 RESTful 的无状态规范。
1.3.4 JWT 存在的问题
说了这么多JWT 也不是天衣无缝由客户端维护登录状态带来的一些问题在这里依然存在举例如下
续签问题这是被很多人诟病的问题之一传统的 cookiesession 的方案天然的支持续签但是 jwt 由于服务端不保存用户状态因此很难完美解决续签问题如果引入 redis虽然可以解决问题但是 jwt 也变得不伦不类了。注销问题由于服务端不再保存用户信息所以一般可以通过修改 secret 来实现注销服务端 secret 修改后已经颁发的未过期的 token 就会认证失败进而实现注销不过毕竟没有传统的注销方便。密码重置密码重置后原本的 token 依然可以访问系统这时候也需要强制修改 secret。基于第 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 用来放服务端的代码我这里服务端主要提供了两个接口
登录接口登录成功之后返回 JWT 字符串。hello 接口客户端拿着 JWT 字符串来访问 hello 接口。
grpc_client 则是我的客户端代码。
2.2 grpc_api
我将 protocol buffers 和一些依赖都放在 grpc_api 模块中因为将来我的 grpc_server 和 grpc_client 都将依赖 grpc_api。
我们来看下这里需要的依赖和插件
dependenciesdependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt-api/artifactIdversion0.11.5/version/dependencydependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt-impl/artifactIdversion0.11.5/versionscoperuntime/scope/dependencydependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt-jackson/artifactIdversion0.11.5/versionscoperuntime/scope/dependencydependencygroupIdio.grpc/groupIdartifactIdgrpc-netty-shaded/artifactIdversion1.52.1/version/dependencydependencygroupIdio.grpc/groupIdartifactIdgrpc-protobuf/artifactIdversion1.52.1/version/dependencydependencygroupIdio.grpc/groupIdartifactIdgrpc-stub/artifactIdversion1.52.1/version/dependencydependencygroupIdorg.apache.tomcat/groupIdartifactIdannotations-api/artifactIdversion6.0.53/versionscopeprovided/scope/dependency
/dependencies
buildextensionsextensiongroupIdkr.motd.maven/groupIdartifactIdos-maven-plugin/artifactIdversion1.6.2/version/extension/extensionspluginsplugingroupIdorg.xolstice.maven.plugins/groupIdartifactIdprotobuf-maven-plugin/artifactIdversion0.6.1/versionconfigurationprotocArtifactcom.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}/protocArtifactpluginIdgrpc-java/pluginIdpluginArtifactio.grpc:protoc-gen-grpc-java:1.51.0:exe:${os.detected.classifier}/pluginArtifact/configurationexecutionsexecutiongoalsgoalcompile/goalgoalcompile-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.KeyString AUTH_CLIENT_ID Context.key(clientId);String AUTH_HEADER Authorization;String AUTH_TOKEN_TYPE Bearer;
}这里的每个常量我都给大家解释下
JWT_KEY这个是生成 JWT 字符串以及进行 JWT 字符串校验的密钥。AUTH_CLIENT_ID这个是客户端的 ID即客户端发送来的请求携带了 JWT 字符串通过 JWT 字符串确认了用户身份就存在这个变量中。AUTH_HEADER这个是携带 JWT 字符串的请求头的 KEY。AUTH_TOKEN_TYPE这个是携带 JWT 字符串的请求头的参数前缀通过这个可以确认参数的类型常见取值有 Bearer 和 Basic。
如此我们的 gRPC_api 就定义好了。
2.3 grpc_server
接下来我们来定义 gRPC_server。
首先来定义登录服务
public class LoginServiceImpl extends LoginServiceGrpc.LoginServiceImplBase {Overridepublic void login(LoginBody request, StreamObserverLoginResponse 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 {Overridepublic void sayHello(StringValue request, StreamObserverStringValue 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);Overridepublic ReqT, RespT ServerCall.ListenerReqT interceptCall(ServerCallReqT, RespT serverCall, Metadata metadata, ServerCallHandlerReqT, 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 {JwsClaims 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.ListenerReqT() {};}
}这段代码逻辑应该好理解
首先从 Metadata 中提取出当前请求所携带的 JWT 字符串相当于从请求头中提取出来。如果第一步提取到的值为 null 或者这个值不是以指定字符 Bearer 开始的说明这个令牌是一个非法令牌设置对应的响应 status 即可。如果令牌都没有问题的话接下来就进行令牌的校验校验失败则设置相应的 status 即可。校验成功的话我们就会获取到一个 Jws 对象从这个对象中我们可以提取出来用户名并存入到 Context 中将来我们在 HelloServiceImpl 中就可以获取到这里的用户名了。最后登录成功的话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 StreamObserverLoginResponse() {Overridepublic void onNext(LoginResponse loginResponse) {System.out.println(loginResponse.getToken() loginResponse.getToken());}Overridepublic void onError(Throwable throwable) {}Overridepublic 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;}Overridepublic 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));}});}Overridepublic 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 StreamObserverStringValue() {Overridepublic void onNext(StringValue stringValue) {System.out.println(stringValue.getValue() stringValue.getValue());}Overridepublic void onError(Throwable throwable) {System.out.println(throwable.getMessage() throwable.getMessage());}Overridepublic void onCompleted() {countDownLatch.countDown();}});countDownLatch.await();}
}这里的登录令牌就是前面调用 login 方法时获取到的令牌。
好啦大功告成。
3. 小结
上面的登录与校验只是松哥给小伙伴们展示的一个具体案例而已在此案例基础之上我们还可以扩展出来更多写法但是万变不离其宗其他玩法就需要小伙伴们自行探索啦