几乎所有的线上系统都离不开认证和授权,Vert.x auth相关组件提供了丰富(Session,JTW, OAuth,...),便捷的认证和授权支持。
当前,使用最多是Web应用,所以在后续讨论中,都是关于Vert.x auth在Web应用(Vertx web)中的使用。
基本概念
认证与授权
认证/鉴权(Authentication) 是指验证用户的身份。// Tells who the user is
- 确认用户或系统的身份,以确保他们是所声称的那个人或系统。
- 认证通常通过用户名和密码、数字证书、生物识别(如指纹或面部识别)等方式进行。
- 例如,当你输入用户名和密码登录到某个应用程序时,系统会检查你的凭证以确认你的身份。
授权(Authorization) 授权是在认证之后进行的,它决定已认证用户可以访问哪些资源或执行哪些操作。// Tells what the user is allowed to do
- 授权通常在认证之后,当在服务器端的身份确认后,会获取对应用户在服务器端的权限,在执行有权限要求的操作前,先用户的权限进行检查。
- 授权的过程涉及设置权限和访问控制,比如数据库的读取、写入权限,或是特定文件夹的访问权限。
- 例如,在同一个应用程序中,普通用户可能只能查看信息,而管理员则可以编辑和删除信息。
认证方式
有状态身份验证( Session-Cookie Based Authentication)
有状态身份验证,也称为会话认证,指的是服务端需要记录每次会话的客户端信息以识别客户端身份。常见的有状态认证方式包括使用cookie和session。当用户登录后,服务端会将登录信息保存在session中,并给用户分配一个唯一的session ID。随后,每次用户请求时,都需要携带该session ID,以便服务端能够识别和验证用户身份。
无状态身份验证 (Token-Based Authentication)
无状态身份验证,也称为令牌认证,是一种不依赖服务端会话的认证方式。在无状态认证中,服务端不需要记录用户的会话信息。用户通过首次认证后,服务端会生成一个令牌(通常使用JSON Web Token,简称JWT),并将其传递给客户端。客户端在后续请求中携带该令牌进行身份验证。
有状态和无状态身份验证各有优缺点,选择合适的认证方式取决于具体需求和应用场景。 有状态认证适用于需要个性化配置和复杂权限控制的Web应用,而无状态认证适用于对性能和扩展性要求较高的场景,如微服务架构、API网关等。此外,无状态认证也适用于跨域应用和单点登录的场景。
Vert.x auth使用概述
通用认证和授权(vertx-auth-common)组件提供了用于身份验证和授权的接口,是Vert.x auth的基础,要在项目中使用,必须先引入依赖:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-auth-common</artifactId>
<version>4.5.10</version>
</dependency>
要使用Vert.x auth我们需要实现的两个核心接口是AuthenticationProvider和User。AuthenticationProvider用于告诉系统如何进行身份认证,而User实例是用户对应在服务器中的权限的载体(authorisation of users),是AuthorizationProvider接口方法authenticate返回的(异步)结果。
归纳来说: AuthenticationProvider的authenticate对输入的信息进行身份认证,如果认证通过后,返回对应用户在服务器端的权限User(异步结果),如果不通过返回失败结果。先有个概念,后续将通过案例来具体说明。
HTTP基本认证(Http Basic Auth
HTTP基本认证(HTTP Basic Access Authentication )是一种用于网络协议的简单认证方式,这种方法通常用于控制对特定资源的访问。在HTTP基本认证中,服务器会向未经认证的请求返回一个401 Unauthorized状态码,并在响应头中包含一个'WWW-Authenticate'字段,提示客户端需要提供认证信息。客户端随后会在请求头中包含一个Authorization字段,其中包含了经过Base64编码的用户名和密码。
HTTP基本认证中的用户名和密码是以明文形式(经过Base64编码)传输的,因此不建议在不安全的网络中使用。为了保护认证信息的安全,应该在HTTPS/TLS的保护下使用HTTP基本认证。
HTTP基本认证不安全,目前很少使用。拿来讨论,是因为简单,便于理解认证与授权的编写。假设有3个页面:
- page1.html: 不需要身份认证既可以访问;
- page2.html: 所有认证用户都可以访问;
- page3.html: 仅admin角色可以访问;
下面通过Vert.x Auth实现上述需求。
java
public class BasicAccessAuth {
private static final Logger LOGGER = Logger.getLogger(BasicAccessAuth.class.getName());
public static void main(String[] args) {
// 模拟服务器存储的用户密码,实际场景这些信息可以存储在数据库。
HashMap<String, String> users = new HashMap<>();
users.put("admin", "password");
users.put("user", "password");
Vertx vertx = Vertx.vertx();
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
// 步骤1: 创建一个authProvider实例,这里用了匿名类,实际编码时可以编写一个实现AuthenticationProvider接口的类,并实例化。
AuthenticationProvider authProvider = new AuthenticationProvider() {
// 认证处理器,异步方法,结果返回一个User结果。
@Override
public void authenticate(JsonObject credentials, Handler<AsyncResult<User>> resultHandler) {
LOGGER.info("input credentials = " + credentials.encode()); // encodePrettily
// 通过密码进行身份认证
String username = credentials.getString("username");
String password = credentials.getString("password");
if (users.containsKey(username) && users.get(username).equals(password)) {
// 通过了身份认证,根据用户在服务器中的权限创建一个User实例。
// 我们模拟了一个简单的权限,角色。
// 权限根据实际来设计,例如可以基于角色,也可以设计成基于权限,如: 某个路径(path)设计权限位(rwx) ...
// 实际场景中,用户对应的权限通常存储在数据库中。
// User实例是用户对应权限的载体。
String role = username.equalsIgnoreCase("admin") ? "admin" : "user";
JsonObject principal = new JsonObject();
principal.put("username", username);
// 属性信息
JsonObject attributes = new JsonObject();
attributes.put("role", role);
// 步骤2: 根据用户对应权限实例化User做为认证结果。
// 这里权限比较简单,使用了Vert.x自带的UserImpl,实际编码时可以编写一个实现User接口的类。
User user = new UserImpl(principal, attributes);
resultHandler.handle(Future.succeededFuture(user));
} else {
resultHandler.handle(Future.failedFuture("身份认证失败。"));
}
}
};
// '/hr/page1.html'直接返回静态页面,不使用基本认证。
// 原理: 因为该路由比认证处理器的路由先加入router,所以优先级高,关于路由优先级与多路由,可以参考我之前写的文章。
router.route("/hr/page1.html").handler(ctx -> {
HttpServerResponse response = ctx.response();
response.putHeader("Content-Type", "text/html");
response.sendFile("webroot/page1.html");
});
// 步骤3: 创建认证处理器,与对应(需要认证的)路由关联。
// 处理器可以自己写,也可使用Vertx自带的BasicAuthHandler
// '/hr/*'路由与BasicAuthHandler绑定,实现访问'/hr/*'都需要进行Http Basic Auth认证。
BasicAuthHandler basicAuthHandler = BasicAuthHandler.create(authProvider);
router.route("/hr/*").handler(basicAuthHandler);
router.route("/hr/page3.html").handler(ctx -> {
// 经过身份认证路由后,认证成功会获取用户授权信息。
User user = ctx.user();
if (user != null) {
// String role = user.attributes().getString("role");
String role = user.get("role");
if ("admin" != role) {
HttpServerResponse response = ctx.response();
response.setStatusCode(401).setStatusMessage("admin only");
response.putHeader("Content-Type", "text/html; charset=utf-8");
response.end("仅admin角色可以访问。");
}
}
ctx.next();
});
router.route("/hr/*").handler(StaticHandler.create());
server.requestHandler(router).listen(8080);
}
}
访问http://127.0.0.1:8080/hr/page1.html,可以正常访问page1.html。
访问http://127.0.0.1:8080/hr/page2.html,会弹出浏览器原生的登录框提示密码登录。
当通过admin/password或者user/password登录后,可以正常访问page2.html内容。
测试时候注意,HTTP基本认证本地cookie会保存密码,无法通过(重启)服务器注销用户,最简单的方法是通过删除cookie方式实现用户注销(切换用户)。
完整的案例已上传在本文的资源中,感兴趣的可以下载。