6.1 Quarkus 安全架构概述
6.1.1 安全架构图
graph TB
A[客户端请求] --> B[安全过滤器]
B --> C{认证检查}
C -->|未认证| D[认证处理器]
C -->|已认证| E{授权检查}
D --> F[身份提供者]
F --> G[用户存储]
E -->|授权成功| H[业务逻辑]
E -->|授权失败| I[拒绝访问]
H --> J[响应]
subgraph "安全扩展"
K[quarkus-security]
L[quarkus-security-jpa]
M[quarkus-oidc]
N[quarkus-jwt]
O[quarkus-elytron-security]
end
subgraph "认证方式"
P[Basic Auth]
Q[JWT Token]
R[OAuth2/OIDC]
S[Form Auth]
T[Certificate Auth]
end
6.1.2 核心安全扩展
<!-- 核心安全扩展 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<!-- JPA 安全扩展 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-jpa</artifactId>
</dependency>
<!-- JWT 支持 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<!-- OIDC 支持 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<!-- Elytron 安全 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file</artifactId>
</dependency>
<!-- 密码哈希 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-common</artifactId>
</dependency>
6.2 基础认证与授权
6.2.1 用户实体和角色模型
package com.example.security.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Entity
@Table(name = "users")
@UserDefinition
public class User extends PanacheEntityBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Username
@Column(unique = true, nullable = false)
@NotBlank
@Size(min = 3, max = 50)
public String username;
@Password
@Column(nullable = false)
@NotBlank
public String password;
@Column(unique = true, nullable = false)
@Email
@NotBlank
public String email;
@Column(name = "first_name")
@Size(max = 50)
public String firstName;
@Column(name = "last_name")
@Size(max = 50)
public String lastName;
@Column(name = "phone_number")
@Pattern(regexp = "^[+]?[0-9]{10,15}$")
public String phoneNumber;
@Column(name = "is_active")
public boolean isActive = true;
@Column(name = "is_email_verified")
public boolean isEmailVerified = false;
@Column(name = "email_verification_token")
public String emailVerificationToken;
@Column(name = "password_reset_token")
public String passwordResetToken;
@Column(name = "password_reset_expires")
public LocalDateTime passwordResetExpires;
@Column(name = "last_login")
public LocalDateTime lastLogin;
@Column(name = "failed_login_attempts")
public int failedLoginAttempts = 0;
@Column(name = "account_locked_until")
public LocalDateTime accountLockedUntil;
@Column(name = "created_at")
public LocalDateTime createdAt;
@Column(name = "updated_at")
public LocalDateTime updatedAt;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
@Roles
public Set<Role> roles = new HashSet<>();
@PrePersist
public void prePersist() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
updatedAt = LocalDateTime.now();
}
// 获取所有权限
public Set<String> getAllPermissions() {
return roles.stream()
.flatMap(role -> role.permissions.stream())
.map(permission -> permission.name)
.collect(Collectors.toSet());
}
// 检查是否有特定权限
public boolean hasPermission(String permissionName) {
return getAllPermissions().contains(permissionName);
}
// 检查是否有特定角色
public boolean hasRole(String roleName) {
return roles.stream().anyMatch(role -> role.name.equals(roleName));
}
// 检查账户是否被锁定
public boolean isAccountLocked() {
return accountLockedUntil != null && accountLockedUntil.isAfter(LocalDateTime.now());
}
// 重置失败登录次数
public void resetFailedLoginAttempts() {
this.failedLoginAttempts = 0;
this.accountLockedUntil = null;
}
// 增加失败登录次数
public void incrementFailedLoginAttempts() {
this.failedLoginAttempts++;
if (this.failedLoginAttempts >= 5) {
this.accountLockedUntil = LocalDateTime.now().plusMinutes(30);
}
}
}
@Entity
@Table(name = "roles")
public class Role extends PanacheEntityBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Column(unique = true, nullable = false)
@NotBlank
@Size(min = 2, max = 50)
public String name;
@Column(length = 500)
public String description;
@Column(name = "is_system_role")
public boolean isSystemRole = false;
@Column(name = "created_at")
public LocalDateTime createdAt;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
public Set<Permission> permissions = new HashSet<>();
@PrePersist
public void prePersist() {
createdAt = LocalDateTime.now();
}
}
@Entity
@Table(name = "permissions")
public class Permission extends PanacheEntityBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Column(unique = true, nullable = false)
@NotBlank
@Size(min = 2, max = 100)
public String name;
@Column(length = 500)
public String description;
@Column(name = "resource_type")
public String resourceType;
@Column(name = "action_type")
public String actionType;
@Column(name = "created_at")
public LocalDateTime createdAt;
@PrePersist
public void prePersist() {
createdAt = LocalDateTime.now();
}
}
6.2.2 安全配置
# 基础安全配置
quarkus.security.users.embedded.enabled=false
quarkus.security.users.file.enabled=false
# 密码哈希配置
quarkus.security.users.embedded.algorithm=BCrypt
quarkus.security.users.embedded.hash-charset=UTF-8
# HTTP 安全配置
quarkus.http.auth.basic=false
quarkus.http.auth.form.enabled=false
quarkus.http.auth.session.encryption-key=changeit
# CORS 配置
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:3000,https://app.example.com
quarkus.http.cors.methods=GET,PUT,POST,DELETE,OPTIONS
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
quarkus.http.cors.exposed-headers=location,info
quarkus.http.cors.access-control-max-age=86400
quarkus.http.cors.access-control-allow-credentials=true
# 安全头配置
quarkus.http.header."X-Frame-Options".value=DENY
quarkus.http.header."X-Content-Type-Options".value=nosniff
quarkus.http.header."X-XSS-Protection".value=1; mode=block
quarkus.http.header."Strict-Transport-Security".value=max-age=31536000; includeSubDomains
quarkus.http.header."Content-Security-Policy".value=default-src 'self'
# 会话配置
quarkus.http.auth.session.timeout=30M
quarkus.http.auth.session.cookie-name=QUARKUS_SESSION
quarkus.http.auth.session.cookie-path=/
quarkus.http.auth.session.cookie-same-site=strict
quarkus.http.auth.session.cookie-http-only=true
quarkus.http.auth.session.cookie-secure=true
# 速率限制配置
quarkus.security.rate-limit.enabled=true
quarkus.security.rate-limit.requests-per-minute=60
quarkus.security.rate-limit.burst-size=10
6.2.3 认证服务实现
package com.example.security.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.smallrye.mutiny.Uni;
import com.example.security.entity.User;
import com.example.security.entity.Role;
import com.example.security.exception.*;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
@ApplicationScoped
public class AuthenticationService {
@Inject
UserRepository userRepository;
@Inject
EmailService emailService;
@Inject
SecurityAuditService auditService;
@Transactional
public Uni<User> authenticate(String username, String password) {
return userRepository.findByUsername(username)
.onItem().transformToUni(userOpt -> {
if (userOpt.isEmpty()) {
auditService.logAuthenticationFailure(username, "User not found");
return Uni.createFrom().failure(new AuthenticationException("Invalid credentials"));
}
User user = userOpt.get();
// 检查账户状态
if (!user.isActive) {
auditService.logAuthenticationFailure(username, "Account inactive");
return Uni.createFrom().failure(new AccountInactiveException("Account is inactive"));
}
if (user.isAccountLocked()) {
auditService.logAuthenticationFailure(username, "Account locked");
return Uni.createFrom().failure(new AccountLockedException("Account is locked"));
}
// 验证密码
if (!BcryptUtil.matches(password, user.password)) {
user.incrementFailedLoginAttempts();
userRepository.persist(user);
auditService.logAuthenticationFailure(username, "Invalid password");
return Uni.createFrom().failure(new AuthenticationException("Invalid credentials"));
}
// 认证成功
user.resetFailedLoginAttempts();
user.lastLogin = LocalDateTime.now();
userRepository.persist(user);
auditService.logAuthenticationSuccess(username);
return Uni.createFrom().item(user);
});
}
@Transactional
public Uni<User> register(RegisterRequest request) {
return userRepository.findByUsername(request.username)
.onItem().transformToUni(existingUser -> {
if (existingUser.isPresent()) {
return Uni.createFrom().failure(new UserAlreadyExistsException("Username already exists"));
}
return userRepository.findByEmail(request.email)
.onItem().transformToUni(existingEmail -> {
if (existingEmail.isPresent()) {
return Uni.createFrom().failure(new UserAlreadyExistsException("Email already exists"));
}
// 创建新用户
User user = new User();
user.username = request.username;
user.email = request.email;
user.password = BcryptUtil.bcryptHash(request.password);
user.firstName = request.firstName;
user.lastName = request.lastName;
user.phoneNumber = request.phoneNumber;
user.emailVerificationToken = UUID.randomUUID().toString();
// 分配默认角色
return Role.find("name", "USER")
.firstResult()
.onItem().transformToUni(defaultRole -> {
if (defaultRole != null) {
user.roles.add((Role) defaultRole);
}
return userRepository.persist(user)
.onItem().transformToUni(savedUser -> {
// 发送验证邮件
return emailService.sendEmailVerification(savedUser)
.onItem().transform(ignored -> {
auditService.logUserRegistration(savedUser.username);
return savedUser;
});
});
});
});
});
}
@Transactional
public Uni<Void> changePassword(String username, ChangePasswordRequest request) {
return userRepository.findByUsername(username)
.onItem().transformToUni(userOpt -> {
if (userOpt.isEmpty()) {
return Uni.createFrom().failure(new UserNotFoundException("User not found"));
}
User user = userOpt.get();
// 验证当前密码
if (!BcryptUtil.matches(request.currentPassword, user.password)) {
auditService.logPasswordChangeFailure(username, "Invalid current password");
return Uni.createFrom().failure(new AuthenticationException("Invalid current password"));
}
// 验证新密码强度
if (!isPasswordStrong(request.newPassword)) {
return Uni.createFrom().failure(new PasswordPolicyException("Password does not meet policy requirements"));
}
// 更新密码
user.password = BcryptUtil.bcryptHash(request.newPassword);
return userRepository.persist(user)
.onItem().transformToUni(ignored -> {
auditService.logPasswordChange(username);
return Uni.createFrom().voidItem();
});
});
}
@Transactional
public Uni<Void> resetPassword(String email) {
return userRepository.findByEmail(email)
.onItem().transformToUni(userOpt -> {
if (userOpt.isEmpty()) {
// 为了安全,即使用户不存在也返回成功
return Uni.createFrom().voidItem();
}
User user = userOpt.get();
user.passwordResetToken = UUID.randomUUID().toString();
user.passwordResetExpires = LocalDateTime.now().plusHours(1);
return userRepository.persist(user)
.onItem().transformToUni(savedUser -> {
return emailService.sendPasswordReset(savedUser)
.onItem().transform(ignored -> {
auditService.logPasswordResetRequest(savedUser.username);
return null;
});
});
});
}
@Transactional
public Uni<Void> confirmPasswordReset(ConfirmPasswordResetRequest request) {
return userRepository.findByPasswordResetToken(request.token)
.onItem().transformToUni(userOpt -> {
if (userOpt.isEmpty()) {
return Uni.createFrom().failure(new InvalidTokenException("Invalid reset token"));
}
User user = userOpt.get();
if (user.passwordResetExpires.isBefore(LocalDateTime.now())) {
return Uni.createFrom().failure(new InvalidTokenException("Reset token has expired"));
}
// 验证新密码强度
if (!isPasswordStrong(request.newPassword)) {
return Uni.createFrom().failure(new PasswordPolicyException("Password does not meet policy requirements"));
}
// 更新密码并清除重置令牌
user.password = BcryptUtil.bcryptHash(request.newPassword);
user.passwordResetToken = null;
user.passwordResetExpires = null;
user.resetFailedLoginAttempts();
return userRepository.persist(user)
.onItem().transformToUni(ignored -> {
auditService.logPasswordReset(user.username);
return Uni.createFrom().voidItem();
});
});
}
@Transactional
public Uni<Void> verifyEmail(String token) {
return userRepository.findByEmailVerificationToken(token)
.onItem().transformToUni(userOpt -> {
if (userOpt.isEmpty()) {
return Uni.createFrom().failure(new InvalidTokenException("Invalid verification token"));
}
User user = userOpt.get();
user.isEmailVerified = true;
user.emailVerificationToken = null;
return userRepository.persist(user)
.onItem().transformToUni(ignored -> {
auditService.logEmailVerification(user.username);
return Uni.createFrom().voidItem();
});
});
}
private boolean isPasswordStrong(String password) {
// 密码强度检查:至少8位,包含大小写字母、数字和特殊字符
if (password.length() < 8) {
return false;
}
boolean hasUpper = password.chars().anyMatch(Character::isUpperCase);
boolean hasLower = password.chars().anyMatch(Character::isLowerCase);
boolean hasDigit = password.chars().anyMatch(Character::isDigit);
boolean hasSpecial = password.chars().anyMatch(ch -> "!@#$%^&*()_+-=[]{}|;:,.<>?".indexOf(ch) >= 0);
return hasUpper && hasLower && hasDigit && hasSpecial;
}
}
6.3 JWT 令牌认证
6.3.1 JWT 配置
# JWT 配置
mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem
mp.jwt.verify.issuer=https://example.com
mp.jwt.verify.audiences=myapp
# JWT 生成配置
smallrye.jwt.sign.key.location=META-INF/resources/privateKey.pem
smallrye.jwt.new-token.issuer=https://example.com
smallrye.jwt.new-token.audience=myapp
smallrye.jwt.new-token.lifespan=3600
# 刷新令牌配置
quarkus.jwt.refresh-token.enabled=true
quarkus.jwt.refresh-token.lifespan=604800
quarkus.jwt.refresh-token.cookie-name=refresh_token
quarkus.jwt.refresh-token.cookie-path=/
quarkus.jwt.refresh-token.cookie-http-only=true
quarkus.jwt.refresh-token.cookie-secure=true
quarkus.jwt.refresh-token.cookie-same-site=strict
6.3.2 JWT 服务实现
package com.example.security.service;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.build.JwtClaimsBuilder;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.jwt.JsonWebToken;
import com.example.security.entity.User;
import java.time.Duration;
import java.time.Instant;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@ApplicationScoped
public class JwtService {
@Inject
UserRepository userRepository;
@Inject
RefreshTokenRepository refreshTokenRepository;
@Inject
SecurityAuditService auditService;
public Uni<TokenResponse> generateTokens(User user) {
return generateAccessToken(user)
.onItem().transformToUni(accessToken -> {
return generateRefreshToken(user)
.onItem().transform(refreshToken -> {
TokenResponse response = new TokenResponse();
response.accessToken = accessToken;
response.refreshToken = refreshToken;
response.tokenType = "Bearer";
response.expiresIn = 3600; // 1 hour
return response;
});
});
}
public Uni<String> generateAccessToken(User user) {
return Uni.createFrom().item(() -> {
Set<String> roles = user.roles.stream()
.map(role -> role.name)
.collect(Collectors.toSet());
Set<String> permissions = user.getAllPermissions();
String sessionId = UUID.randomUUID().toString();
JwtClaimsBuilder claimsBuilder = Jwt.claims()
.issuer("https://example.com")
.subject(user.username)
.audience("myapp")
.issuedAt(Instant.now())
.expiresAt(Instant.now().plus(Duration.ofHours(1)))
.claim("user_id", user.id)
.claim("email", user.email)
.claim("session_id", sessionId)
.claim("roles", roles)
.claim("permissions", permissions)
.claim("email_verified", user.isEmailVerified);
// 添加自定义声明
if (user.firstName != null) {
claimsBuilder.claim("given_name", user.firstName);
}
if (user.lastName != null) {
claimsBuilder.claim("family_name", user.lastName);
}
return claimsBuilder.sign();
});
}
public Uni<String> generateRefreshToken(User user) {
return Uni.createFrom().item(() -> {
String tokenId = UUID.randomUUID().toString();
RefreshToken refreshToken = new RefreshToken();
refreshToken.tokenId = tokenId;
refreshToken.userId = user.id;
refreshToken.username = user.username;
refreshToken.expiresAt = LocalDateTime.now().plusDays(7);
refreshToken.isActive = true;
return refreshTokenRepository.persist(refreshToken)
.onItem().transform(ignored -> {
return Jwt.claims()
.issuer("https://example.com")
.subject(user.username)
.audience("myapp")
.issuedAt(Instant.now())
.expiresAt(Instant.now().plus(Duration.ofDays(7)))
.claim("token_id", tokenId)
.claim("user_id", user.id)
.claim("token_type", "refresh")
.sign();
});
}).flatMap(uni -> uni);
}
public Uni<TokenResponse> refreshAccessToken(String refreshTokenString) {
return validateRefreshToken(refreshTokenString)
.onItem().transformToUni(claims -> {
String tokenId = (String) claims.get("token_id");
Long userId = Long.valueOf(claims.get("user_id").toString());
return refreshTokenRepository.findByTokenId(tokenId)
.onItem().transformToUni(tokenOpt -> {
if (tokenOpt.isEmpty() || !tokenOpt.get().isActive) {
return Uni.createFrom().failure(new InvalidTokenException("Invalid refresh token"));
}
RefreshToken refreshToken = tokenOpt.get();
if (refreshToken.expiresAt.isBefore(LocalDateTime.now())) {
refreshToken.isActive = false;
refreshTokenRepository.persist(refreshToken);
return Uni.createFrom().failure(new TokenExpiredException("Refresh token expired"));
}
return userRepository.findById(userId)
.onItem().transformToUni(userOpt -> {
if (userOpt.isEmpty()) {
return Uni.createFrom().failure(new UserNotFoundException("User not found"));
}
User user = userOpt.get();
if (!user.isActive) {
return Uni.createFrom().failure(new AccountInactiveException("Account inactive"));
}
return generateAccessToken(user)
.onItem().transform(newAccessToken -> {
TokenResponse response = new TokenResponse();
response.accessToken = newAccessToken;
response.refreshToken = refreshTokenString; // 保持原有刷新令牌
response.tokenType = "Bearer";
response.expiresIn = 3600;
auditService.logTokenRefresh(user.username);
return response;
});
});
});
});
}
public Uni<Map<String, Object>> validateAccessToken(String token) {
return Uni.createFrom().item(() -> {
try {
// 这里应该使用适当的 JWT 验证库
// 简化示例,实际应用中需要验证签名、过期时间等
JsonWebToken jwt = parseJwt(token);
if (jwt.getExpirationTime() < Instant.now().getEpochSecond()) {
throw new TokenExpiredException("Access token expired");
}
Map<String, Object> claims = new HashMap<>();
claims.put("sub", jwt.getSubject());
claims.put("user_id", jwt.getClaim("user_id"));
claims.put("email", jwt.getClaim("email"));
claims.put("roles", jwt.getClaim("roles"));
claims.put("permissions", jwt.getClaim("permissions"));
claims.put("session_id", jwt.getClaim("session_id"));
return claims;
} catch (Exception e) {
throw new InvalidTokenException("Invalid access token", e);
}
});
}
public Uni<Map<String, Object>> validateRefreshToken(String token) {
return Uni.createFrom().item(() -> {
try {
JsonWebToken jwt = parseJwt(token);
if (jwt.getExpirationTime() < Instant.now().getEpochSecond()) {
throw new TokenExpiredException("Refresh token expired");
}
String tokenType = jwt.getClaim("token_type");
if (!"refresh".equals(tokenType)) {
throw new InvalidTokenException("Not a refresh token");
}
Map<String, Object> claims = new HashMap<>();
claims.put("sub", jwt.getSubject());
claims.put("user_id", jwt.getClaim("user_id"));
claims.put("token_id", jwt.getClaim("token_id"));
return claims;
} catch (Exception e) {
throw new InvalidTokenException("Invalid refresh token", e);
}
});
}
public Uni<Void> revokeRefreshToken(String tokenId) {
return refreshTokenRepository.findByTokenId(tokenId)
.onItem().transformToUni(tokenOpt -> {
if (tokenOpt.isPresent()) {
RefreshToken token = tokenOpt.get();
token.isActive = false;
token.revokedAt = LocalDateTime.now();
return refreshTokenRepository.persist(token)
.onItem().transform(ignored -> null);
}
return Uni.createFrom().voidItem();
});
}
public Uni<Void> revokeAllUserTokens(Long userId) {
return refreshTokenRepository.revokeAllUserTokens(userId);
}
private JsonWebToken parseJwt(String token) {
// 实际实现中应该使用适当的 JWT 解析库
// 这里只是示例
throw new UnsupportedOperationException("JWT parsing not implemented");
}
}
6.3.3 认证资源端点
package com.example.security.resource;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import com.example.security.service.*;
import com.example.security.dto.*;
import com.example.security.exception.*;
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Authentication", description = "Authentication and authorization operations")
public class AuthResource {
@Inject
AuthenticationService authService;
@Inject
JwtService jwtService;
@Inject
UserService userService;
@Inject
SecurityContext securityContext;
@POST
@Path("/login")
@PermitAll
@Operation(summary = "User login", description = "Authenticate user and return JWT tokens")
public Uni<Response> login(@Valid LoginRequest request) {
return authService.authenticate(request.username, request.password)
.onItem().transformToUni(user -> {
return jwtService.generateTokens(user)
.onItem().transform(tokens -> {
LoginResponse response = new LoginResponse();
response.user = UserResponse.from(user);
response.accessToken = tokens.accessToken;
response.refreshToken = tokens.refreshToken;
response.tokenType = tokens.tokenType;
response.expiresIn = tokens.expiresIn;
return Response.ok(response)
.cookie(createRefreshTokenCookie(tokens.refreshToken))
.build();
});
})
.onFailure().recoverWithItem(throwable -> {
if (throwable instanceof AuthenticationException) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(new ErrorResponse("Invalid credentials"))
.build();
} else if (throwable instanceof AccountInactiveException) {
return Response.status(Response.Status.FORBIDDEN)
.entity(new ErrorResponse("Account is inactive"))
.build();
} else if (throwable instanceof AccountLockedException) {
return Response.status(Response.Status.FORBIDDEN)
.entity(new ErrorResponse("Account is locked"))
.build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Login failed"))
.build();
}
});
}
@POST
@Path("/register")
@PermitAll
@Operation(summary = "User registration", description = "Register a new user account")
public Uni<Response> register(@Valid RegisterRequest request) {
return authService.register(request)
.onItem().transform(user -> {
UserResponse userResponse = UserResponse.from(user);
return Response.status(Response.Status.CREATED)
.entity(new RegisterResponse(userResponse, "Registration successful. Please check your email for verification."))
.build();
})
.onFailure().recoverWithItem(throwable -> {
if (throwable instanceof UserAlreadyExistsException) {
return Response.status(Response.Status.CONFLICT)
.entity(new ErrorResponse(throwable.getMessage()))
.build();
} else if (throwable instanceof ValidationException) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(throwable.getMessage()))
.build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Registration failed"))
.build();
}
});
}
@POST
@Path("/refresh")
@PermitAll
@Operation(summary = "Refresh token", description = "Refresh access token using refresh token")
public Uni<Response> refreshToken(@Valid RefreshTokenRequest request) {
String refreshToken = request.refreshToken;
// 如果请求中没有刷新令牌,尝试从 Cookie 中获取
if (refreshToken == null || refreshToken.isEmpty()) {
// 从 Cookie 中获取刷新令牌的逻辑
refreshToken = getRefreshTokenFromCookie();
}
if (refreshToken == null || refreshToken.isEmpty()) {
return Uni.createFrom().item(
Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Refresh token is required"))
.build()
);
}
return jwtService.refreshAccessToken(refreshToken)
.onItem().transform(tokens -> {
RefreshTokenResponse response = new RefreshTokenResponse();
response.accessToken = tokens.accessToken;
response.tokenType = tokens.tokenType;
response.expiresIn = tokens.expiresIn;
return Response.ok(response).build();
})
.onFailure().recoverWithItem(throwable -> {
if (throwable instanceof InvalidTokenException ||
throwable instanceof TokenExpiredException) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity(new ErrorResponse("Invalid or expired refresh token"))
.build();
} else {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Token refresh failed"))
.build();
}
});
}
@POST
@Path("/logout")
@Authenticated
@Operation(summary = "User logout", description = "Logout user and revoke tokens")
public Uni<Response> logout() {
String username = getCurrentUsername();
String sessionId = getCurrentSessionId();
return jwtService.revokeRefreshToken(sessionId)
.onItem().transform(ignored -> {
return Response.ok(new MessageResponse("Logout successful"))
.cookie(clearRefreshTokenCookie())
.build();
})
.onFailure().recoverWithItem(throwable ->
Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Logout failed"))
.build()
);
}
@POST
@Path("/logout-all")
@Authenticated
@Operation(summary = "Logout from all devices", description = "Logout user from all devices and revoke all tokens")
public Uni<Response> logoutAll() {
Long userId = getUserId();
return jwtService.revokeAllUserTokens(userId)
.onItem().transform(ignored -> {
return Response.ok(new MessageResponse("Logged out from all devices"))
.cookie(clearRefreshTokenCookie())
.build();
})
.onFailure().recoverWithItem(throwable ->
Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Logout failed"))
.build()
);
}
// 辅助方法
private String getCurrentUsername() {
return securityContext.getUserPrincipal().getName();
}
private String getCurrentSessionId() {
JsonWebToken jwt = (JsonWebToken) securityContext.getUserPrincipal();
return jwt.getClaim("session_id");
}
private Long getUserId() {
JsonWebToken jwt = (JsonWebToken) securityContext.getUserPrincipal();
return Long.valueOf(jwt.getClaim("user_id").toString());
}
private NewCookie createRefreshTokenCookie(String refreshToken) {
return new NewCookie("refresh_token", refreshToken, "/", null, null,
7 * 24 * 60 * 60, true, true);
}
private NewCookie clearRefreshTokenCookie() {
return new NewCookie("refresh_token", "", "/", null, null,
0, true, true);
}
private String getRefreshTokenFromCookie() {
// 从 HTTP 请求的 Cookie 中获取刷新令牌
// 实际实现需要注入 HttpServletRequest 或使用其他方式
return null;
}
}
6.4 本章小结
6.4.1 核心概念回顾
本章深入探讨了 Quarkus 中的安全认证与授权机制:
- 安全架构:理解 Quarkus 安全模型的核心组件
- 用户管理:实现完整的用户、角色和权限管理系统
- JWT 认证:掌握 JWT 令牌的生成、验证和管理
- 基础认证:学习基本的用户名密码认证流程
6.4.2 技术要点总结
- 多层安全防护:从传输层到应用层的全方位安全保护
- 灵活的认证方式:支持多种认证机制和身份提供者
- 细粒度授权:基于角色和权限的精确访问控制
- 安全审计:完整的安全事件记录和追踪
- 性能优化:高效的安全检查和令牌管理
6.4.3 最佳实践
- 密码安全:使用强密码策略和安全的哈希算法
- 令牌管理:合理设置令牌过期时间和刷新机制
- 会话安全:实现安全的会话管理和并发控制
- 审计日志:记录所有安全相关的操作和事件
- 错误处理:避免泄露敏感信息的错误消息
6.4.4 下一章预告
下一章我们将学习 监控、日志与健康检查,包括: - 应用监控和指标收集 - 结构化日志和日志聚合 - 健康检查和就绪检查 - 分布式追踪和性能分析 - 告警和通知机制