和之前的 JWT 相比,Http Basic 認(rèn)證的缺點(diǎn)還是非常明顯的,但是從認(rèn)證流程來(lái)說(shuō),感覺(jué)兩者差別不大,只是創(chuàng)建令牌和解析令牌的方式不同而已。
在之前的文章中,松哥和小伙伴們聊了 gRPC+JWT 進(jìn)行認(rèn)證,這也是我們常用的認(rèn)證方式之一,考慮到文章內(nèi)容的完整性,今天松哥再來(lái)和小伙伴們聊一聊在 gRPC 中通過(guò) HttpBasic 進(jìn)行認(rèn)證,HttpBasic 認(rèn)證有一些天然的缺陷,這個(gè)在接下來(lái)的文章中松哥也會(huì)和大家進(jìn)行分析。
1. 什么是 Basic 認(rèn)證
HTTP Basic authentication 中文譯作 HTTP 基本認(rèn)證,在這種認(rèn)證方式中,將用戶的登錄用戶名/密碼經(jīng)過(guò) Base64 編碼之后,放在請(qǐng)求頭的 Authorization 字段中,從而完成用戶身份的 認(rèn)證。
這是一種在 RFC7235(https://tools.ietf.org/html/rfc7235) 規(guī)范中定義的認(rèn)證方式,當(dāng)客戶端發(fā)起一個(gè)請(qǐng)求之后,服務(wù)端可以針對(duì)該請(qǐng)求返回一個(gè)質(zhì)詢信息,然后客戶端再??供用戶的憑 證信息。具體的質(zhì)詢與應(yīng)答流程如圖所示:

由上圖可以看出,客戶端的用戶名和密碼只是簡(jiǎn)單做了一個(gè) Base64 轉(zhuǎn)碼,然后放到請(qǐng)求頭中就傳輸?shù)椒?wù)端了。
我們?cè)谌粘5拈_發(fā)中,其實(shí)也很少見到這種認(rèn)證方式,有的讀者可能在一些老舊路由器中見過(guò)這種認(rèn)證方式;另外,在一些非公開訪問(wèn)的 Web 應(yīng)用中,可能也會(huì)見到這種認(rèn)證方式。為什么很少見到這種認(rèn)證方式的應(yīng)用場(chǎng)景呢?主要還是安全問(wèn)題。
HTTP 基本認(rèn)證沒(méi)有對(duì)傳輸?shù)膽{證信息進(jìn)行加密,僅僅只是進(jìn)行了 Base64 編碼,這就造成了很大的安全隱患,所以如果用到了 HTTP 基本認(rèn)證,一般都是結(jié)合 HTTPS 一起使用;同 時(shí),一旦使用 HTTP 基本認(rèn)證成功后,由于令牌缺乏有效期,除非用戶重啟瀏覽器或者修改密碼,否則沒(méi)有辦法退出登錄。
2. gRPC 中的基本認(rèn)證
gRPC 并沒(méi)有為 Http Basic 認(rèn)證提供專門的 API,如果我們需要在 gRPC 中進(jìn)行 Http Basic 認(rèn)證,需要自己手工處理。
不過(guò)相信小伙伴們看了上面的流程圖之后,對(duì)于手工處理 gRPC+Http Basic 也沒(méi)啥壓力。
首先我們先來(lái)看客戶端的代碼:
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() {
}
}
- 當(dāng)客戶端發(fā)起一個(gè)請(qǐng)求的時(shí)候,我們構(gòu)建一個(gè) HttpBasicCredential 對(duì)象,并傳入用戶名和密碼。
- 該對(duì)象核心的處理邏輯在 applyRequestMetadata 方法中,我們先按照 username + ":" + password 的形式將用戶名和密碼拼接成一個(gè)字符串,并對(duì)這個(gè)字符串進(jìn)行 Base64 編碼。
- 最后將編碼結(jié)果放在請(qǐng)求頭中,請(qǐng)求頭的 KEY 就是 AuthConstant.AUTH_HEADER? 變量,對(duì)應(yīng)的具體值是 Authorization,請(qǐng)求頭的 value 是通過(guò) String.format? 函數(shù)拼接出來(lái)的,實(shí)際上就是在 Base64 的編碼的字符串上加上了 Basic 前綴。
這塊就是純手工操作,技術(shù)原理跟我們之前講的 JWT+gRPC 沒(méi)有任何差別,基本上是一模一樣的,所以我就不啰嗦了。
來(lái)看下前端請(qǐng)求該如何發(fā)起:
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();
}
}
通過(guò) withCallCredentials 方法,在客戶端發(fā)起請(qǐng)求的時(shí)候,把這段認(rèn)證信息攜帶上。
再來(lái)看看服務(wù)端的處理。
服務(wù)端通過(guò)一個(gè)攔截器來(lái)統(tǒng)一處理,從請(qǐng)求頭中提取出來(lái)認(rèn)證信息并解析判斷,邏輯如下:
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>() {
};
}
}
- 首先從請(qǐng)求頭中取出 Base64 編碼之后的令牌。
- 如果取出的值為 null,則返回 miss authentication token。
- 如果取出的令牌的起始字符不對(duì),則返回 unknown token type。
- 如果前面都沒(méi)問(wèn)題,則開始對(duì)拿到的字符串進(jìn)行 Base64 解碼,解碼之后做字符串拆分,然后分別判斷用戶名和密碼是否正確,如果正確,則將用戶名存入到 Context 中,在后續(xù)的業(yè)務(wù)邏輯中就可以使用了。
服務(wù)端的啟動(dòng)代碼如下:
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();
}
}
}
小伙伴們看下,就是用了下這個(gè)攔截器而已。
最后,在業(yè)務(wù)代碼中,也可以直接訪問(wèn)到剛剛認(rèn)證成功的用戶名:
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();
}
}
好啦,大功告成。
3. 小結(jié)
和之前的 JWT 相比,Http Basic 認(rèn)證的缺點(diǎn)還是非常明顯的,但是從認(rèn)證流程來(lái)說(shuō),感覺(jué)兩者差別不大,只是創(chuàng)建令牌和解析令牌的方式不同而已。
感興趣的小伙伴可以嘗試一下哦。
本文松哥只貼出來(lái)了一些關(guān)鍵代碼,完整的代碼小伙伴們可以從 GitHub 上下載:https://github.com/lenve/javaboy-code-samples