JASPIC ServerAuthModule中CDI注入DataSource失效问题
我之前也碰到过一模一样的问题!核心原因其实很清晰:你的SrvAuthModuleImpl是由WildFly容器直接实例化的,不属于CDI容器管理的Bean——哪怕你在beans.xml里设置了bean-discovery-mode="all",CDI也不会对这个类的实例做依赖注入处理。而同一个WAR里的Servlet是CDI管辖范围内的Bean,所以注入能正常工作,这就是两者的本质区别。
下面给你两个可行的解决方案,按需选择:
方案1:通过CDI API手动获取资源(简单直接)
既然认证模块不在CDI的管理范围内,我们可以主动获取CDI上下文,从中拿到需要的DataSource或者其他全局CDI Bean。
首先确保你的项目引入了CDI API依赖(WildFly自带这个API,所以只需要在pom.xml里添加provided依赖即可,不用打包到WAR里):
<dependency> <groupId>javax.enterprise</groupId> <artifactId>cdi-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency>
然后修改你的认证模块代码,添加一个获取CDI资源的方法,这里有两种更实用的方式:
方式A:通过CDI持有Bean间接获取(更灵活)
如果你需要管理更多全局资源,建议先把DataSource注入到一个CDI管理的Bean里,再通过CDI获取这个Bean:
// 先创建一个ApplicationScoped的持有Bean,用来托管DataSource @ApplicationScoped public class DBResourceHolder { @Resource(mappedName="java:jboss/datasources/someDB") private DataSource db; public DataSource getDb() { return db; } }
接着在认证模块里获取这个Bean:
import javax.enterprise.inject.spi.CDI; // ... 其他原有代码 ... private DataSource getDataSourceViaCDI() { try { return CDI.current().select(DBResourceHolder.class).get().getDb(); } catch (Exception e) { throw new AuthException("Failed to retrieve DataSource via CDI holder", e); } }
最后在validateRequest方法里调用这个新方法代替原来的getDb():
@Override public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException { try (Connection c = getDataSourceViaCDI().getConnection()) { // 你的认证逻辑代码 } catch (SQLException ex) { // 异常处理逻辑 } return AuthStatus.SUCCESS; }
方式B:直接通过CDI获取DataSource
如果你的DataSource可以通过CDI限定符(比如@Named)匹配,也可以直接查找:
import javax.enterprise.inject.spi.CDI; import javax.inject.Named; // ... 其他原有代码 ... private DataSource getDataSourceViaCDI() { try { // 用@Named限定符匹配数据源,名称要和配置对应 return CDI.current().select(DataSource.class, new NamedLiteral("someDB")).get(); } catch (Exception e) { throw new AuthException("Failed to retrieve DataSource via CDI", e); } } // 实现NamedLiteral类,因为CDI的@Named是注解,不能直接传入 class NamedLiteral extends AnnotationLiteral<Named> implements Named { private final String value; public NamedLiteral(String value) { this.value = value; } @Override public String value() { return value; } }
方案2:让ServerAuthModule成为CDI管理的Bean(进阶)
如果你希望整个认证模块都由CDI管理(比如需要注入多个CDI Bean),可以通过自定义AuthConfigProvider来实现:
- 给
SrvAuthModuleImpl添加CDI作用域注解,比如@ApplicationScoped:
@ApplicationScoped public class SrvAuthModuleImpl implements ServerAuthModule { @Resource(mappedName="java:jboss/datasources/someDB") private DataSource db; // ... 其他原有代码 ... }
- 实现自定义的
AuthConfigProvider,在里面通过CDI获取ServerAuthModule的实例,代替容器直接创建:
public class CDIAuthConfigProvider implements AuthConfigProvider { @Override public AuthConfig getAuthConfig(String layer, String appContext, RegistrationListener listener) throws AuthException { return new AuthConfig() { @Override public ServerAuthConfig getServerAuthConfig(String layer, String appContext, RegistrationListener listener) throws AuthException { return new ServerAuthConfig() { // 重点实现getAuthContext方法,通过CDI获取模块实例 @Override public AuthContext getAuthContext(String authContextID, Subject serviceSubject, Map properties) throws AuthException { ServerAuthModule module = CDI.current().select(SrvAuthModuleImpl.class).get(); module.initialize(null, null, handler, properties); return new AuthContext() { @Override public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException { return module.validateRequest(messageInfo, clientSubject, serviceSubject); } // 实现其他AuthContext方法,直接调用module对应的方法即可 @Override public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException { return module.secureResponse(messageInfo, serviceSubject); } @Override public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException { module.cleanSubject(messageInfo, subject); } }; } // 实现其他ServerAuthConfig必要方法... @Override public String getMessageLayer() { return "HttpServlet"; } @Override public String getAppContext() { return ""; } // 其他方法按需实现 }; } // 实现其他AuthConfig必要方法... }; } // 实现其他AuthConfigProvider必要方法... }
- 在项目的
META-INF/services/javax.security.auth.message.config.AuthConfigProvider文件里注册你的自定义Provider:
com.yourpackage.CDIAuthConfigProvider
这个方案相对复杂,但能让整个认证模块享受到CDI的依赖注入能力,适合需要注入多个CDI资源的场景。
补充说明
你之前用InitialContext手动查找的临时方案是完全可行的,因为JNDI查找不受CDI管理的限制,只要数据源的JNDI名称正确就能拿到。如果只是需要DataSource,方案1的方式B已经足够简单好用。
内容的提问来源于stack exchange,提问作者hrs

