JSF应用中如何确保HttpAuthenticationMechanism优先于CDI拦截器执行?
我们的JSF应用使用自定义HttpAuthenticationMechanism实现认证逻辑:未认证用户访问受保护资源时自动跳转登录页,已认证用户通过@AutoApplySession自动应用会话。近期新增CDI拦截器@RolesPermitted,用于在CDI Bean的类/方法级别检查角色(功能与EJB的@RolesAllowed一致),无对应角色则跳转错误页。
但未认证用户请求/admin/secured-page.xhtml时,页面关联的后台Bean带有类级别@RolesPermitted({"admin"})注解,此时RolesPermitted拦截器会先于HttpAuthenticationMechanism触发,抛出安全异常而非跳转登录页。核心原因是JSF视图处理流程早于容器认证机制的执行时机。
需求:确保HttpAuthenticationMechanism在自定义安全拦截器前触发,让未认证用户正确跳转登录页。
解决方案
1. 调整CDI拦截器优先级
CDI拦截器的@Priority注解决定执行顺序,当前拦截器使用的Interceptor.Priority.APPLICATION优先级高于容器认证机制的执行时机。将拦截器优先级调低,确保容器认证先完成:
@Interceptor @RolesPermitted @Priority(Interceptor.Priority.LIBRARY_BEFORE) // 或使用数值(如500),需低于APPLICATION的2000 public class RoleInterceptor implements Serializable { // 原有代码不变 }
2. 在拦截器中先检查用户认证状态
在角色检查逻辑前,先判断用户是否已认证。若未认证,抛出特定异常并通过全局异常处理器跳转登录页:
修改RoleInterceptor的checkRole方法:
@AroundInvoke public Object checkRole(InvocationContext ctx) throws Exception { // 先校验用户是否已认证 if (sec.getCallerPrincipal() == null) { throw new AuthenticationException("用户未认证"); } // 原有角色检查逻辑... }
同时添加全局异常处理器(如JSF的ExceptionHandler或CDI的ExceptionMapper),捕获AuthenticationException后跳转登录页。
3. 确保web.xml安全约束完全生效
已配置的/admin/*安全约束应在请求到达JSF Bean前触发容器认证,需补充完善web.xml的登录配置:
<login-config> <auth-method>CUSTOM</auth-method> <!-- 对应自定义HttpAuthenticationMechanism --> <realm-name>CustomRealm</realm-name> </login-config>
确认@AutoApplySession注解正常工作,容器会在每个请求自动应用会话认证,确保安全约束优先触发认证流程。
4. 添加Servlet Filter提前拦截
若上述方案不生效,可添加优先级高于JSF过滤器的Servlet Filter,提前触发认证检查:
@WebFilter(urlPatterns = "/admin/*") public class AuthenticationFilter implements Filter { @Inject private HttpAuthenticationMechanism authMechanism; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; HttpMessageContext context = // 通过CDI注入或自定义实现获取HttpMessageContext AuthenticationStatus status = authMechanism.validateRequest(req, res, context); if (status == AuthenticationStatus.SEND_CONTINUE || status == AuthenticationStatus.SEND_FAILURE) { // 认证未完成或失败,终止请求链 return; } chain.doFilter(request, response); } // 实现Filter的init、destroy方法 }
相关代码
角色拦截器代码
@Interceptor @RolesPermitted @Priority(Interceptor.Priority.APPLICATION) public class RoleInterceptor implements Serializable { private static final long serialVersionUID = 1L; @Inject private SecurityContext sec; @Inject private Logger log; @AroundInvoke public Object checkRole(InvocationContext ctx) throws Exception { if(ctx.getMethod().getDeclaringClass().isAnnotationPresent(RolesPermitted.class)) { String[] classRoles = ctx .getMethod().getDeclaringClass() .getAnnotation(RolesPermitted.class) .value(); boolean hasRole = false; log.debug("Checking if user has {} roles to execute {}",Arrays.asList(classRoles).toString(),ctx.getMethod().getDeclaringClass().getName()); for(String r:classRoles) { if(sec.isCallerInRole(r)) { hasRole=true; } } if(!hasRole) { throw new java.security.GeneralSecurityException("User does not have any of the required roles "+Arrays.asList(classRoles).toString()); } } if(ctx.getMethod().isAnnotationPresent(RolesPermitted.class)) { String[] methodRoles = ctx .getMethod() .getAnnotation(RolesPermitted.class) .value(); if(methodRoles.length>0) { log.debug("Checking if user has {} roles to execute {}",Arrays.asList(methodRoles).toString(),ctx.getMethod().getName()); for(String r:methodRoles) { if(sec.isCallerInRole(r)) { return ctx.proceed(); } } throw new java.security.GeneralSecurityException("User does not have any of the required roles "+Arrays.asList(methodRoles).toString()); } } return ctx.proceed(); } }
后台Bean代码
@Named @ViewScoped @RolesPermitted({"admin"}) @Transactional(Transactional.TxType.REQUIRED) public class AdminActions implements Serializable { // 页面逻辑代码 }
自定义认证机制代码(简化版)
@RequestScoped @AutoApplySession public class CustomAuthentication implements Serializable, HttpAuthenticationMechanism { private static final long serialVersionUID = 1L; @Inject private PasswordEncryptorEntities passwordEncryptor; @Inject private Logger log; @Override public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMessageContext) throws AuthenticationException { log.trace("Validating request {}",request.getRequestURI()); if(httpMessageContext.isAuthenticationRequest()) { Set<String> roles = new HashSet<String>(); if(loginPrincipal.coreRole().equals(Role.admin)) { Admin admin = (Admin) loginPrincipal; if(admin.isSuperUser()) { roles.add(Role.adminSuperuser.getRole()); } } UserPrincipal up = new UserPrincipal(loginPrincipal); httpMessageContext.getClientSubject().getPrincipals().add(up); httpMessageContext.setRegisterSession(up.getName(), roles); log.debug("Login successful for {} with roles {}",loginPrincipal.getFullname(),roles.toString()); return httpMessageContext.notifyContainerAboutLogin(up,roles); }else if(httpMessageContext.isProtected() && request.getUserPrincipal()==null) { // 未认证用户访问受保护资源,跳转登录页 return httpMessageContext.forward(loginUrl); }else if(httpMessageContext.isProtected() && request.getUserPrincipal()!=null) { // 已认证但角色不符,跳转对应登录页 UserPrincipal up = (UserPrincipal) request.getUserPrincipal(); return httpMessageContext.forward(up.getAppUser().coreRole().getLoginPage().getPath()); } return httpMessageContext.doNothing(); } }
web.xml安全约束配置
<security-constraint> <web-resource-collection> <web-resource-name>Admin User Login Area</web-resource-name> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> </auth-constraint> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint>
内容的提问来源于stack exchange,提问作者DaveB

