在之前的文章中,松哥和小伙伴们聊了 gRPC+JWT 进行认证,这也是我们常用的认证方式之一,考虑到文章内容的完整性,今天松哥再来和小伙伴们聊一聊在 gRPC 中通过 HttpBasic 进行认证,HttpBasic 认证有一些天然的缺陷,这个在接下来的文章中松哥也会和大家进行分析。
HTTP Basic authentication 中文译作 HTTP 基本认证,在这种认证方式中,将用户的登录用户名/密码经过 Base64 编码之后,放在请求头的 Authorization 字段中,从而完成用户身份的 认证。
这是一种在 RFC7235(https://tools.ietf.org/html/rfc7235) 规范中定义的认证方式,当客户端发起一个请求之后,服务端可以针对该请求返回一个质询信息,然后客户端再?供用户的凭 证信息。具体的质询与应答流程如图所示:

由上图可以看出,客户端的用户名和密码只是简单做了一个 Base64 转码,然后放到请求头中就传输到服务端了。
我们在日常的开发中,其实也很少见到这种认证方式,有的读者可能在一些老旧路由器中见过这种认证方式;另外,在一些非公开访问的 Web 应用中,可能也会见到这种认证方式。为什么很少见到这种认证方式的应用场景呢?主要还是安全问题。
HTTP 基本认证没有对传输的凭证信息进行加密,仅仅只是进行了 Base64 编码,这就造成了很大的安全隐患,所以如果用到了 HTTP 基本认证,一般都是结合 HTTPS 一起使用;同 时,一旦使用 HTTP 基本认证成功后,由于令牌缺乏有效期,除非用户重启浏览器或者修改密码,否则没有办法退出登录。
gRPC 并没有为 Http Basic 认证提供专门的 API,如果我们需要在 gRPC 中进行 Http Basic 认证,需要自己手工处理。
不过相信小伙伴们看了上面的流程图之后,对于手工处理 gRPC+Http Basic 也没啥压力。
首先我们先来看客户端的代码:
public class HttpBasicCredential extends CallCredentials {
private String username;
private String password;
public HttpBasicCredential(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public void applyRequestMetadata(RequestInfo requestInfo, Executor executor, MetadataApplier metadataApplier) {
executor.execute(() -> {
try {
String token = new String(Base64.getEncoder().encode((username + ":" + password).getBytes()));
Metadata headers = new Metadata();
headers.put(Metadata.Key.of(AuthConstant.AUTH_HEADER, Metadata.ASCII_STRING_MARSHALLER),
String.format("%s %s", AuthConstant.AUTH_TOKEN_TYPE, token));
metadataApplier.apply(headers);
} catch (Throwable e) {
metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e));
}
});
}
@Override
public void thisUsesUnstableApi() {
}
}这块就是纯手工操作,技术原理跟我们之前讲的 JWT+gRPC 没有任何差别,基本上是一模一样的,所以我就不啰嗦了。
来看下前端请求该如何发起:
public class LoginClient {
public static void main(String[] args) throws InterruptedException, SSLException {
File certFile = Paths.get( "certs", "ca.crt").toFile();
SslContext sslContext = GrpcSslContexts.forClient().trustManager(certFile).build();
ManagedChannel channel = NettyChannelBuilder.forAddress("local.javaboy.org", 50051)
.useTransportSecurity()
.sslContext(sslContext)
.build();
LoginServiceGrpc.LoginServiceStub stub = LoginServiceGrpc.newStub(channel).withDeadline(Deadline.after(3, TimeUnit.SECONDS));
sayHello(channel);
}
private static void sayHello(ManagedChannel channel) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
HelloServiceGrpc.HelloServiceStub helloServiceStub = HelloServiceGrpc.newStub(channel);
helloServiceStub
.withCallCredentials(new HttpBasicCredential("javaboy", "123"))
.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();
}
}通过 withCallCredentials 方法,在客户端发起请求的时候,把这段认证信息携带上。
再来看看服务端的处理。
服务端通过一个拦截器来统一处理,从请求头中提取出来认证信息并解析判断,逻辑如下:
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 {
try {
String token = authorization.substring(AuthConstant.AUTH_TOKEN_TYPE.length()).trim();
String[] split = new String(Base64.getDecoder().decode(token)).split(":");
String username = split[0];
String password = split[1];
if ("javaboy".equals(username) && "123".equals(password)) {
Context ctx = Context.current()
.withValue(AuthConstant.AUTH_CLIENT_ID, username);
return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler);
}
} catch (JwtException e) {
status = Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e);
}
}
serverCall.close(status, new Metadata());
return new ServerCall.Listener<ReqT>() {
};
}
}服务端的启动代码如下:
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;
File certFile = Paths.get( "certs", "server.crt").toFile();
File keyFile = Paths.get("certs", "server.pem").toFile();
server = ServerBuilder.forPort(port)
.addService(ServerInterceptors.intercept(new HelloServiceImpl(), new AuthInterceptor()))
.useTransportSecurity(certFile,keyFile)
.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();
}
}
}小伙伴们看下,就是用了下这个拦截器而已。
最后,在业务代码中,也可以直接访问到刚刚认证成功的用户名:
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();
}
}好啦,大功告成。
和之前的 JWT 相比,Http Basic 认证的缺点还是非常明显的,但是从认证流程来说,感觉两者差别不大,只是创建令牌和解析令牌的方式不同而已。
感兴趣的小伙伴可以尝试一下哦。
本文松哥只贴出来了一些关键代码,完整的代码小伙伴们可以从 GitHub 上下载:https://github.com/lenve/javaboy-code-samples
我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
question的一些答案关于redirect_to让我想到了其他一些问题。基本上,我正在使用Rails2.1编写博客应用程序。我一直在尝试自己完成大部分工作(因为我对Rails有所了解),但在需要时会引用Internet上的教程和引用资料。我设法让一个简单的博客正常运行,然后我尝试添加评论。靠我自己,我设法让它进入了可以从script/console添加评论的阶段,但我无法让表单正常工作。我遵循的其中一个教程建议在帖子Controller中创建一个“评论”操作,以添加评论。我的问题是:这是“标准”方式吗?我的另一个问题的答案之一似乎暗示应该有一个CommentsController参
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
我基本上来自Java背景并且努力理解Ruby中的模运算。(5%3)(-5%3)(5%-3)(-5%-3)Java中的上述操作产生,2个-22个-2但在Ruby中,相同的表达式会产生21个-1-2.Ruby在逻辑上有多擅长这个?模块操作在Ruby中是如何实现的?如果将同一个操作定义为一个web服务,两个服务如何匹配逻辑。 最佳答案 在Java中,模运算的结果与被除数的符号相同。在Ruby中,它与除数的符号相同。remainder()在Ruby中与被除数的符号相同。您可能还想引用modulooperation.
RSpec似乎按顺序匹配方法接收的消息。我不确定如何使以下代码工作:allow(a).toreceive(:f)expect(a).toreceive(:f).with(2)a.f(1)a.f(2)a.f(3)我问的原因是a.f的一些调用是由我的代码的上层控制的,所以我不能对这些方法调用添加期望。 最佳答案 RSpecspy是测试这种情况的一种方式。要监视一个方法,用allowstub,除了方法名称之外没有任何约束,调用该方法,然后expect确切的方法调用。例如:allow(a).toreceive(:f)a.f(2)a.f(1)
这是针对我无法破坏的现有公共(public)API,但我确实希望对其进行扩展。目前,该方法采用字符串或符号或任何其他在作为第一个参数传递给send时有意义的内容我想添加发送字符串、符号等列表的功能。我可以只使用is_a吗?数组,但还有其他发送列表的方法,这不是很像ruby。我将调用列表中的map,所以第一个倾向是使用respond_to?:map。但是字符串也会响应:map,所以这行不通。 最佳答案 如何将它们全部视为数组?String的行为与仅包含String的Array相同:deffoo(obj,arg)[*arg].eac
A/ctohttp://wiki.nginx.org/CoreModule#usermaster进程曾经以root用户运行,是否可以以不同的用户运行nginxmaster进程? 最佳答案 只需以非root身份运行init脚本(即/etc/init.d/nginxstart),就可以用不同的用户运行nginxmaster进程。如果这真的是你想要做的,你将需要确保日志和pid目录(通常是/var/log/nginx&/var/run/nginx.pid)对该用户是可写的,并且您所有的listen调用都是针对大于1024的端口(因为绑定(
我创建了一个由于“在运行时执行的单例元类定义”而无法编码的对象(这段代码的描述是否正确?)。这是通过以下代码执行的:#defineclassXthatmyusesingletonclassmetaprogrammingfeatures#throughcallofmethod:break_marshalling!classXdefbreak_marshalling!meta_class=class我该怎么做才能使对象编码正确?是否可以从对象instance_of_x的classX中“移除”单例组件?我真的需要一个建议,因为我们的一些对象需要通过Marshal.dump序列化机制进行缓存。
有没有办法在sinatra的beforedoblock中停止执行并返回不同的值?beforedo#codeishere#Iwouldliketo'return"Message"'#Iwouldlike"/home"tonotgetcalled.end//restofthecodeget'/home'doend 最佳答案 beforedohalt401,{'Content-Type'=>'text/plain'},'Message!'end如果你愿意,你可以只指定状态,这里有状态、标题和正文的例子