You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Grails 3.3中基于CLIENT-CERT的X509证书认证迁移问题

迁移Grails 2的CLIENT-CERT X509认证到Grails 3.3(按需提示证书)

我刚好做过类似的迁移,针对你需求的仅访问受保护资源时才提示用户提供证书,在Grails 3.3里需要结合Spring Boot的嵌入式容器配置和Spring Security的X509认证来实现——因为Grails 3基于Spring Boot,和Grails 2的容器配置逻辑有不小差异,具体步骤如下:

1. 配置嵌入式Tomcat的SSL连接器(Application.groovy)

核心是把Tomcat的clientAuth设为want而非true,这样Tomcat不会强制所有HTTPS请求都要求证书,只有当后续Spring Security触发认证时才会提示用户提供证书。

import org.apache.catalina.connector.Connector
import org.apache.coyote.http11.Http11NioProtocol
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory

class Application {

    @Bean
    EmbeddedServletContainerCustomizer containerCustomizer() throws Exception {
        return new EmbeddedServletContainerCustomizer() {
            @Override
            void customize(ConfigurableEmbeddedServletContainer container) {
                if (container instanceof TomcatEmbeddedServletContainerFactory) {
                    TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container;
                    
                    // 创建SSL连接器
                    Connector sslConnector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
                    Http11NioProtocol protocol = (Http11NioProtocol) sslConnector.getProtocolHandler();
                    
                    // 基础HTTPS配置
                    sslConnector.setScheme("https");
                    sslConnector.setSecure(true);
                    sslConnector.setPort(8443); // 替换为你的HTTPS端口
                    
                    // 关键设置:want模式下Tomcat允许无证书请求,但会在需要时提示
                    sslConnector.setAttribute("clientAuth", "want");
                    
                    // 证书存储配置(替换为你实际的文件路径和密码)
                    protocol.setSSLEnabled(true);
                    protocol.setKeystoreFile("/path/to/your/keystore.jks");
                    protocol.setKeystorePass("your-keystore-pass");
                    protocol.setTruststoreFile("/path/to/your/truststore.jks");
                    protocol.setTruststorePass("your-truststore-pass");
                    protocol.setKeyAlias("your-key-alias");
                    
                    tomcat.addAdditionalTomcatConnectors(sslConnector);
                }
            }
        };
    }
}

2. 配置Spring Security的X509认证规则

在grails-app/conf/application.groovy中配置Spring Security,指定哪些资源需要证书认证,同时启用X509过滤器:

// 基础Spring Security域类配置(替换为你项目的实际类)
grails.plugin.springsecurity.userLookup.userDomainClassName = 'com.yourcompany.User'
grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'com.yourcompany.UserRole'
grails.plugin.springsecurity.authority.className = 'com.yourcompany.Role'

// 启用X509认证
grails.plugin.springsecurity.useX509 = true
grails.plugin.springsecurity.x509.filterProcessesUrl = '/login/x509'
// 指定自定义的用户详情服务,用于从证书映射到系统用户
grails.plugin.springsecurity.x509.userDetailsService = 'customX509UserDetailsService'

// 核心:配置URL拦截规则,仅保护特定路径
grails.plugin.springsecurity.interceptUrlMap = [
    '/secure/**': ['ROLE_USER', 'IS_AUTHENTICATED_FULLY'], // 受保护资源,需要证书认证
    '/public/**': ['IS_AUTHENTICATED_ANONYMOUSLY'], // 公开资源,无需认证
    '/**': ['IS_AUTHENTICATED_ANONYMOUSLY'] // 默认公开
]

3. 实现自定义X509用户详情服务

创建一个自定义服务,负责从X509证书中提取用户信息,并映射到你的系统用户实体:

// src/main/groovy/com/yourcompany/CustomX509UserDetailsService.groovy
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.web.authentication.preauth.x509.X509UserDetailsService
import java.security.cert.X509Certificate

class CustomX509UserDetailsService implements X509UserDetailsService {

    @Override
    UserDetails loadUserDetails(X509Certificate cert) throws UsernameNotFoundException {
        // 从证书主题DN中提取用户名(示例:从CN字段获取)
        String subjectDn = cert.getSubjectX500Principal().getName()
        String username = subjectDn.split("CN=")[1].split(",")[0].trim()
        
        // 根据用户名查找系统用户
        User user = User.findByUsername(username)
        if (!user) {
            throw new UsernameNotFoundException("No user found for certificate CN: ${username}")
        }
        
        // 返回Spring Security的UserDetails对象(包含权限)
        return new org.springframework.security.core.userdetails.User(
            user.username,
            "", // 证书认证无需密码,设为空即可
            user.enabled,
            true,
            true,
            true,
            user.authorities
        )
    }
}

关键注意事项

  • clientAuth模式差异:true会强制所有HTTPS请求必须提供证书,want则是允许无证书请求,但在需要认证时触发证书提示,这是实现「按需提示」的核心
  • Truststore配置:你的truststore必须包含客户端证书的CA根证书,否则Tomcat会无法验证客户端证书的有效性
  • 测试验证:访问公开路径(如/public/home)不会弹出证书选择框,访问受保护路径(如/secure/dashboard)时才会提示用户选择证书

内容的提问来源于stack exchange,提问作者dre

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 06:36:05