SKILL.md
唯讀
名稱
quarkus-security
描述
Quarkus 安全性最佳實踐,涵蓋身份驗證、授權、JWT/OIDC、RBAC、輸入驗證、CSRF、機密資訊管理與相依性套件安全性。
Quarkus Security 審查
透過身份驗證、授權與輸入驗證來強化 Quarkus 應用程式安全性的最佳實踐。
啟用時機
- 新增身份驗證(JWT、OIDC、Basic Auth)
- 使用 @RolesAllowed 或 SecurityIdentity 實作授權
- 驗證使用者輸入(Bean Validation、自訂驗證器)
- 設定 CORS 或安全性標頭 (Security Headers)
- 管理機密資訊(Vault、環境變數、設定來源)
- 新增速率限制 (Rate Limiting) 或暴力破解防護
- 掃描相依性套件的 CVE 漏洞
- 使用 MicroProfile JWT 或 SmallRye JWT
身份驗證
JWT 身份驗證
// 受 JWT 保護的資源
@Path("/api/protected")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken jwt;
@Inject
SecurityIdentity securityIdentity;
@GET
public Response getData() {
String username = jwt.getName();
Set<String> roles = jwt.getGroups();
return Response.ok(Map.of(
"username", username,
"roles", roles,
"principal", securityIdentity.getPrincipal().getName()
)).build();
}
}
設定檔 (application.properties):
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=https://auth.example.com
# OIDC
quarkus.oidc.auth-server-url=https://auth.example.com/realms/myrealm
quarkus.oidc.client-id=backend-service
quarkus.oidc.credentials.secret=${OIDC_SECRET}
自訂身份驗證過濾器
@Provider
@Priority(Priorities.AUTHENTICATION)
public class CustomAuthFilter implements ContainerRequestFilter {
@Inject
SecurityIdentity identity;
@Override
public void filter(ContainerRequestContext requestContext) {
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// 若缺少標頭或格式不正確,立即拒絕
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
return;
}
String token = authHeader.substring(7);
if (!validateToken(token)) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
}
}
private boolean validateToken(String token) {
// Token 驗證邏輯
return true;
}
}
授權
角色導向存取控制 (RBAC)
@Path("/api/admin")
@RolesAllowed("ADMIN")
public class AdminResource {
@GET
@Path("/users")
public List<UserDto> listUsers() {
return userService.findAll();
}
@DELETE
@Path("/users/{id}")
@RolesAllowed({"ADMIN", "SUPER_ADMIN"})
public Response deleteUser(@PathParam("id") Long id) {
userService.delete(id);
return Response.noContent().build();
}
}
@Path("/api/users")
public class UserResource {
@Inject
SecurityIdentity securityIdentity;
@GET
@Path("/{id}")
@RolesAllowed("USER")
public Response getUser(@PathParam("id") Long id) {
// 檢查所有權
if (!securityIdentity.hasRole("ADMIN") &&
!isOwner(id, securityIdentity.getPrincipal().getName())) {
return Response.status(Response.Status.FORBIDDEN).build();
}
return Response.ok(userService.findById(id)).build();
}
private boolean isOwner(Long userId, String username) {
return userService.isOwner(userId, username);
}
}
程式化安全性控制
@ApplicationScoped
public class SecurityService {
@Inject
SecurityIdentity securityIdentity;
public boolean canAccessResource(Long resourceId) {
if (securityIdentity.isAnonymous()) {
return false;
}
if (securityIdentity.hasRole("ADMIN")) {
return true;
}
String userId = securityIdentity.getPrincipal().getName();
return resourceRepository.isOwner(resourceId, userId);
}
}
輸入驗證
Bean Validation
// 不良示範:未進行驗證
@POST
public Response createUser(UserDto dto) {
return Response.ok(userService.create(dto)).build();
}
// 良好示範:經驗證的 DTO
public record CreateUserDto(
@NotBlank @Size(max = 100) String name,
@NotBlank @Email String email,
@NotNull @Min(18) @Max(150) Integer age,
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone
) {}
@POST
@Path("/users")
public Response createUser(@Valid CreateUserDto dto) {
User user = userService.create(dto);
return Response.status(Response.Status.CREATED).entity(user).build();
}
自訂驗證器
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UsernameValidator.class)
public @interface ValidUsername {
String message() default "Invalid username format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return false;
return value.matches("^[a-zA-Z0-9_-]{3,20}$");
}
}
// 使用方式
public record CreateUserDto(
@ValidUsername String username,
@NotBlank @Email String email
) {}
防止 SQL 注入
Panache Active Record(預設安全)
// 良好示範:在 Panache 中使用參數化查詢
List<User> users = User.list("email = ?1 and active = ?2", email, true);
Optional<User> user = User.find("username", username).firstResultOptional();
// 良好示範:具名參數
List<User> users = User.list("email = :email and age > :minAge",
Parameters.with("email", email).and("minAge", 18));
原生查詢(請使用參數)
// 不良示範:字串拼接
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
// 良好示範:參數化的原生查詢
@Entity
public class User extends PanacheEntity {
public static List<User> findByEmailNative(String email) {
return getEntityManager()
.createNativeQuery("SELECT * FROM users WHERE email = :email", User.class)
.setParameter("email", email)
.getResultList();
}
}
密碼雜湊
@ApplicationScoped
public class PasswordService {
public String hash(String plainPassword) {
return BcryptUtil.bcryptHash(plainPassword);
}
public boolean verify(String plainPassword, String hashedPassword) {
return BcryptUtil.matches(plainPassword, hashedPassword);
}
}
// 在 Service 中使用
@ApplicationScoped
public class UserService {
@Inject
PasswordService passwordService;
@Transactional
public User register(CreateUserDto dto) {
String hashedPassword = passwordService.hash(dto.password());
User user = new User();
user.email = dto.email();
user.password = hashedPassword;
user.persist();
return user;
}
public boolean authenticate(String email, String password) {
return User.find("email", email)
.firstResultOptional()
.map(u -> passwordService.verify(password, u.password))
.orElse(false);
}
}
CORS 設定
# application.properties
quarkus.http.cors=true
quarkus.http.cors.origins=https://app.example.com,https://admin.example.com
quarkus.http.cors.methods=GET,POST,PUT,DELETE
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
quarkus.http.cors.exposed-headers=Content-Disposition
quarkus.http.cors.access-control-max-age=24H
quarkus.http.cors.access-control-allow-credentials=true
機密資訊管理
# application.properties - 請勿在此存放機密資訊
# 使用環境變數
quarkus.datasource.username=${DB_USER}
quarkus.datasource.password=${DB_PASSWORD}
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
# 或使用 Vault
quarkus.vault.url=https://vault.example.com
quarkus.vault.authentication.kubernetes.role=my-role
整合 HashiCorp Vault
@ApplicationScoped
public class SecretService {
@ConfigProperty(name = "api-key")
String apiKey; // 從 Vault 擷取
public String getSecret(String key) {
return ConfigProvider.getConfig().getValue(key, String.class);
}
}
速率限制 (Rate Limiting)
安全性注意事項:切勿直接使用 X-Forwarded-For——用戶端可以偽造此標頭。
請使用 servlet 請求中的實際遠端位址,或在可用時使用已通過驗證的身份資訊(API Key、JWT 主體)。
@ApplicationScoped
public class RateLimitFilter implements ContainerRequestFilter {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Inject
HttpServletRequest servletRequest;
@Override
public void filter(ContainerRequestContext requestContext) {
String clientId = getClientIdentifier();
RateLimiter limiter = limiters.computeIfAbsent(clientId,
k -> RateLimiter.create(100.0)); // 每秒 100 個請求
if (!limiter.tryAcquire()) {
requestContext.abortWith(
Response.status(429)
.entity(Map.of("error", "Too many requests"))
.build()
);
}
}
private String getClientIdentifier() {
// 使用容器提供的遠端位址(非 X-Forwarded-For)。
// 若部位於受信任的反向代理之後,請設定 quarkus.http.proxy.proxy-address-forwarding=true
// 使 getRemoteAddr() 能回傳真實的用戶端 IP。
return servletRequest.getRemoteAddr();
}
}
安全性標頭 (Security Headers)
@Provider
public class SecurityHeadersFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
MultivaluedMap<String, Object> headers = response.getHeaders();
// 防範點擊劫持 (Clickjacking)
headers.putSingle("X-Frame-Options", "DENY");
// XSS 防護
headers.putSingle("X-Content-Type-Options", "nosniff");
headers.putSingle("X-XSS-Protection", "1; mode=block");
// HSTS
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
// CSP — script-src 請避免使用 'unsafe-inline',否則會使 XSS 防護失效;
// 請改用 nonce 或 hash。若 CSS 框架需要,style-src 使用 'unsafe-inline' 尚可接受,
// 但可行時仍優先建議使用 nonce。
headers.putSingle("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
}
}
稽核日誌 (Audit Logging)
@ApplicationScoped
public class AuditService {
private static final Logger LOG = Logger.getLogger(AuditService.class);
@Inject
SecurityIdentity securityIdentity;
public void logAccess(String resource, String action) {
String user = securityIdentity.isAnonymous()
? "anonymous"
: securityIdentity.getPrincipal().getName();
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
user, action, resource, Instant.now());
}
}
// 在 Resource 中的使用方式
@Path("/api/sensitive")
public class SensitiveResource {
@Inject
AuditService auditService;
@GET
@RolesAllowed("ADMIN")
public Response getData() {
auditService.logAccess("sensitive-data", "READ");
return Response.ok(data).build();
}
}
相依性套件安全性掃描
# Maven
mvn org.owasp:dependency-check-maven:check
# Gradle
./gradlew dependencyCheckAnalyze
# 檢查 Quarkus 擴充套件
quarkus extension list --installable
最佳實踐
- 生產環境務必使用 HTTPS
- 啟用 JWT 或 OIDC 進行無狀態身份驗證
- 使用
@RolesAllowed進行宣告式授權 - 使用 Bean Validation 驗證所有輸入資料
- 使用 BCrypt 對密碼進行雜湊處理(切勿使用明文)
- 將機密資訊存放於 Vault 或環境變數中
- 使用參數化查詢防止 SQL 注入
- 為所有回應加上安全性標頭
- 為公開端點實作速率限制
- 針對敏感操作記錄稽核日誌
- 保持相依性套件更新並定期掃描 CVE 漏洞
- 使用 SecurityIdentity 進行程式化






