Quarkus 安全架构
Quarkus 使用 HttpAuthenticationMechanism
接口作为保护 HTTP 应用的主要入口机制。
Quarkus Security 使用 HttpAuthenticationMechanism
从 HTTP 的请求中提取认证凭证并委托给 IdentityProvider
将凭证转化成 SecurityIdentity
。这些凭证的来源可以是 Authorization
头部,客户端的 HTTPS 证书或者是 Cookies。
IdentityProvider
会验证认证凭证并将其映射到 SecurityIdentity
,其中包含用户名、角色、原始认证凭证和其他属性。
对于每一个认证资源,可以注入一个 SecurityIdentity
实例来获得认证的身份信息。
HttpAuthenticationMechanism
的注册机制
// quarkus/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java
public HttpAuthenticator(IdentityProviderManager identityProviderManager,
Instance<PathMatchingHttpSecurityPolicy> pathMatchingPolicy,
Instance<HttpAuthenticationMechanism> httpAuthenticationMechanism,
Instance<IdentityProvider<?>> providers) {
this.identityProviderManager = identityProviderManager;
this.pathMatchingPolicy = pathMatchingPolicy;
List<HttpAuthenticationMechanism> mechanisms = new ArrayList<>();
for (HttpAuthenticationMechanism mechanism : httpAuthenticationMechanism) {
boolean found = false;
for (Class<? extends AuthenticationRequest> mechType : mechanism.getCredentialTypes()) {
for (IdentityProvider<?> i : providers) {
if (i.getRequestType().equals(mechType)) {
found = true;
break;
}
}
if (found == true) {
break;
}
}
// Add mechanism if there is a provider with matching credential type
// If the mechanism has no credential types, just add it anyway
if (found || mechanism.getCredentialTypes().isEmpty()) {
mechanisms.add(mechanism);
}
}
if (mechanisms.isEmpty()) {
this.mechanisms = new HttpAuthenticationMechanism[] { new NoAuthenticationMechanism() };
} else {
mechanisms.sort(new Comparator<HttpAuthenticationMechanism>() {
@Override
public int compare(HttpAuthenticationMechanism mech1, HttpAuthenticationMechanism mech2) {
//descending order
return Integer.compare(mech2.getPriority(), mech1.getPriority());
}
});
this.mechanisms = mechanisms.toArray(new HttpAuthenticationMechanism[mechanisms.size()]);
}
}
根据上边的 Quarkus
的源码可以看到 HttpAuthenticationMechanism
的注册流程为:
- 遍历所有已经植入的
HttpAuthenticationMechanism
实例 - 遍历单个
HttpAuthenticationMechanism
实例的所有支持的凭证类型 - 遍历所有已经植入的
IdentityProvider
并判断HttpAuthenticationMechanism
支持的凭证中是与IdentityProvider
中的请求类型对应 - 如果
IdentityProvider
中支持的请求类型与HttpAuthenticationMechanism
中支持的凭证类型存在对应,或者HttpAuthenticationMechanism
支持的凭证类型为空集合,那么此HttpAuthenticationMechanism
均会被注册。
实现
根据上面的源码,要实现基于 Session 的认证,我们需要实现 HttpAuthenticationMechanism
、 IdentityProvider
和 AuthenticationRequest
接口。
由于 IdentityProvider
中支持的请求类型要与 HttpAuthenticationMechanism
中支持的凭证类型存在对应,因此首先需要实现一个 IdentityProvider
类。
import io.quarkus.security.identity.request.BaseAuthenticationRequest;
public class SessionAuthenticationRequest extends BaseAuthenticationRequest {
private final String username;
public SessionAuthenticationRequest(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
在这里实现了一个 SessionAuthenticationRequest
类,并且添加了 username
属性。
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.credential.Credential;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.sqlclient.Row;
import io.vertx.mutiny.sqlclient.Tuple;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.security.Permission;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@ApplicationScoped
public class SessionIdentityProvider implements IdentityProvider<SessionAuthenticationRequest> {
@Inject
io.vertx.mutiny.mysqlclient.MySQLPool client;
@Override
public Class<SessionAuthenticationRequest> getRequestType() {
return SessionAuthenticationRequest.class;
}
@Override
public Uni<SecurityIdentity> authenticate(SessionAuthenticationRequest sessionAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext) {
String username = sessionAuthenticationRequest.getUsername();
Uni<Row> userUni = client
.preparedQuery("SELECT username, nickname, email, password FROM user WHERE username = ?")
.execute(Tuple.of(username))
.onFailure()
.transform(ForbiddenException::new)
.onItem()
.ifNotNull()
.transformToUni(rows -> rows.toMulti().collect().asList().onItem().transform(records -> records.get(0)));
Uni<List<String>> rolesUni = client
.preparedQuery("SELECT role FROM user_role WHERE user = ?")
.execute(Tuple.of(username))
.onFailure()
.transform(ForbiddenException::new)
.onItem()
.ifNotNull()
.transformToUni(rows -> rows.toMulti().onItem().transform(role -> role.getString("role")).collect().asList());
return Uni.combine().all().unis(userUni, rolesUni).asTuple().onItem().transform(tuple -> {
Row user = tuple.getItem1();
String name = user.getString("username");
List<String> roles = tuple.getItem2();
return new SecurityIdentity() {
@Override
public Principal getPrincipal() {
return new Principal() {
@Override
public String getName() {
return name;
}
};
}
@Override
public boolean isAnonymous() {
return false;
}
@Override
public Set<String> getRoles() {
return roles.stream().collect(Collectors.toSet());
}
@Override
public boolean hasRole(String s) {
return roles.contains(s);
}
@Override
public <T extends Credential> T getCredential(Class<T> aClass) {
return null;
}
@Override
public Set<Credential> getCredentials() {
return null;
}
@Override
public <T> T getAttribute(String s) {
return null;
}
@Override
public Map<String, Object> getAttributes() {
return null;
}
@Override
public Uni<Boolean> checkPermission(Permission permission) {
return null;
}
};
});
}
}
import io.netty.handler.codec.http.HttpResponseStatus;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.Collections;
import java.util.Set;
@ApplicationScoped
public class SessionAuthMechanism implements HttpAuthenticationMechanism {
@Inject
SessionCache cache;
@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
return Uni.createFrom().item(context.request().getCookie("SESSION")).onItem().transformToUni(session -> {
if (session == null || session.getValue().isEmpty()) {
return Uni.createFrom().nullItem();
}
return cache.get(session.getValue()).flatMap((username) -> {
if (username == null) {
return Uni.createFrom().failure(new AuthenticationFailedException());
}
return identityProviderManager.authenticate(new SessionAuthenticationRequest(username));
});
});
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(), null, null));
}
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return Collections.singleton(SessionAuthenticationRequest.class);
}
@Override
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.COOKIE, "SESSION"));
}
}
在 SessionAuthMechanism::getCredentialTypes
方法中返回了包含SessionAuthenticationRequest
的集合,在 SessionIdentityProvider::getRequestType
函数中返回了 AuthenticationRequest
类,这样 SessionAuthMechanism
就会被注册了。
认证过程
当 HTTP 请求到达的时候,SessionAuthMechanism::authenticate
方法会被调用,在这个方法里边会从请求的上下文中获取 Session 对应的 Cookie。如果获取到了,则会创建 SessionAuthenticationRequest
类,并将 Session 的上下文保存到 SessionAuthenticationRequest
实例中,这里保存的是用户名。然后通过 identityProviderManager.authenticate
调用 SessionIdentityProvider
的 authenticate
函数。
在 SessionIdentityProvider
的 authenticate
函数中,通过数据库操作获取用户账户信息和角色信息,实例化 SecurityIdentity
并返回。
HttpAuthenticationMechanism::authenticate
根据其返回值或者异常有如下三种可能:
- 返回
nullItem
意味着跳过认证过程,此时用户处于未认证的状态,仍然可以访问不需要认证的路由。 - 抛出
AuthenticationFailedException
异常,意味着用户认证失败。访问任何的路由都会报错 - 返回
SecurityIdentity
的实例,意味着认证成功。后续的用户角色信息、权限信息将会从这个实例里边获取。
登录实现
import com.password4j.BcryptFunction;
import io.smallrye.mutiny.Uni;
import repository.UserRepository;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.util.UUID;
@ApplicationScoped
@Path("/api")
public class LoginResource {
@Inject
SessionCache sessionCache;
@Inject
UserRepository userRepository;
@POST
@Path("login")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
public Uni<Response> login(User user) {
return userRepository.findByName(user.username).onItem().transformToUni(u -> {
if (u == null) {
return Uni.createFrom().failure(new NotFoundException());
}
BcryptFunction crypt = BcryptFunction.getInstance(12);
if (crypt.check(user.password, u.password)) {
String session = UUID.randomUUID().toString();
return sessionCache.set(session, user.username).flatMap((v) -> Uni.createFrom().item(Response.ok("ok").cookie(new NewCookie("SESSION", session)).build()));
}
return Uni.createFrom().item(Response.ok("error").build());
});
}
@GET
@Path("me")
@RolesAllowed("user")
public Uni<String> me(@Context SecurityContext context) {
return Uni.createFrom().item(context.getUserPrincipal().getName());
}
public static class User {
public String username;
public String password;
}
}
如果用户通过账户密码验证之后,生成 Session 并放置在 Cookie 中,传递给客户端。