quarkus-security

quarkus-security

熱門

Quarkus 安全性最佳實踐,涵蓋身份驗證、授權、JWT/OIDC、RBAC、輸入驗證、CSRF、機密資訊管理與相依性套件安全性。

24萬星標
3.6萬分支
更新於 2026/8/3
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 進行程式化