| |||
|
Shailesh K. Mishra
使用多因素身份验证保护 Oracle WebLogic 上部署的 Web 应用程序。
2013 年 10 月
在多因素身份验证中,基于多种因素验证用户真实性,而不仅仅是传统的用户名/口令机制。例如,当一位银行客户访问 ATM 机时,其中一个身份验证因素是物理 ATM 卡(“用户拥有的东西”);第二个因素是客户的账户 PIN(“用户知道的东西”)。这两个因素若未能同时通过验证,则身份验证失败。
多因素身份验证概念也可应用于 Oracle WebLogic Server 上部署的 Web 应用程序,下面将对此进行详细介绍。
注意:本文假定读者充分了解 Oracle WebLogic 安全概念和身份验证机制。本文使用了 Oracle WebLogic 10.3.5 版。
为简单起见,本文只介绍 weblogic.security.spi.ChallengeIdentityAsserterV2 和 weblogic.security.spi.ServletAuthenticationFilter 接口。有关接口的详细信息以及 Oracle WebLogic 中身份验证提供程序生命周期的详细信息,请参见为 Oracle WebLogic Server 开发安全提供程序 中的“参考资料”一节。
此接口允许身份断言提供程序支持 Microsoft 的 Windows NT 质询/响应 (NTLM)、简单和受保护的 GSS-API 协商机制 (SPNEGO) 等身份验证协议,以及其他质询/响应身份验证机制。它的两个方法 assertChallengeIdentity 和 continueChallengeIdentity 是本文的重点。第一个方法使用提供的令牌建立客户端身份(可能通过多次质询);第二个方法用于继续建立客户端身份,直到过程完成。
此接口用于表示在身份验证过程中,Servlet 容器应包括此身份验证提供程序的身份验证筛选器。它定义了一个方法 getServletAuthenticationFilters,返回将在 Servlet 容器的身份验证过程中执行的 javax.servlet.Filters 的有序列表。
我们需要创建以下项目:
此 Web 应用程序只包含了两个 jsp 页面以作为演示:第一个用于收集用户名/口令,第二个用于收集发送到用户手机的代码。以下是这两个 JSP 页面的代码段。
用于收集用户名称/口令的 JSP 页面:
<%@ page language="java" contentType="text/html;charset=UTF-8"%> <h1>Multi Factor Login</h1> <form method="post" action="login"> <input type="hidden" name="originalRequest" value="<%=request.getParameter("originalRequest") %>"/> <br> <input type="text" name="j_username" value=""/><br> <input type="password" name="j_password" value=""/><br> <input type="submit" name="submit" title="submit" value="submit"> </form>
用于收集发送到用户手机的代码的 JSP 页面:
<%@ page language="java" contentType="text/html;charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <h1>Multi Factor Login</h1> <form method="post" action="login"> <input type="text" name="OTP" value=""/><br> <input type="submit" name="submit" title="submit" value="submit"> </form>
此 Web 应用程序的 web.xml 片段
请注意,在以上 jsp 页面中,受保护的 url“/login”用于提交用户名/口令和 OTP。
<security-constraint> <web-resource-collection> <web-resource-name>login</web-resource-name> <url-pattern>/login</url-pattern> </web-resource-collection> <auth-constraint> <role-name>secured</role-name> </auth-constraint> </security-constraint> <security-role> <role-name>authenticatedusers</role-name> </security-role>
以下是自定义身份验证提供程序的示例代码,用于测试这一概念。为了提高可读性,已删除部分代码。
MultiFactorIdentityAsserterProviderImpl.java
public final class MultiFactorIdentityAsserterProviderImpl implements AuthenticationProviderV2, ChallengeIdentityAsserterV2, ServletAuthenticationFilter { ...................................................................... public javax.servlet.Filter[] getServletAuthenticationFilters() { System.out.println("You are getting my filters...."); return new Filter[] { new MultiFactorFilter(this.config), new SampleFilter() }; } @Override public ProviderChallengeContext assertChallengeIdentity(String token, Object value, ContextHandler ctx) throws IdentityAssertionException { System.out.println("AssertChallengeIdentity"); System.out.println("AssertChallengeContext " + token + " " + value + " " + Arrays.asList(ctx.getNames())); MultiFactorChallengeContext challengeCtx = new MultiFactorChallengeContext(this.config); try { challengeCtx.processRequest(ctx); } catch (Exception e) { throw new IdentityAssertionException(e.getMessage()); } if (challengeCtx.hasChallengeIdentityCompleted()) { complete(challengeCtx,ctx); } return challengeCtx; } @Override public void continueChallengeIdentity( ProviderChallengeContext providerChallengeContext, String arg1, Object arg2, ContextHandler ctx) throws IdentityAssertionException { // TODO Auto-generated method stub System.out.println("Continuing...."); System.out.println("AssertChallengeContext " + arg1 + " " + arg2 + " " + Arrays.asList(ctx.getNames())); MultiFactorChallengeContext challengeCtx = (MultiFactorChallengeContext) providerChallengeContext; try { challengeCtx.processRequest(ctx); } catch (Exception e) { throw new IdentityAssertionException(e.getMessage()); }}}
MultiFactorAuthenticationFilter.java
public class MultiFactorAuthenticationFilter implements Filter { private String [] protectedPaths; private String webAppPath; public MultiFactorAuthenticationFilter(MultiFactorAuthenticationProviderMBean config){ // TODO Auto-generated constructor stub this.protectedPaths = config.getProtectedPaths(); this.webAppPath = config.getMultiFactorWebAppPath(); System.out.println("Filtering: "+Arrays.asList(this.protectedPaths)); } @Override public void destroy() { // TODO Auto-generated method stub } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { // TODO Auto-generated method stub HttpServletRequest hReq = (HttpServletRequest)req; HttpServletResponse hResp = ((HttpServletResponse)resp); System.out.println("In MultiFactorFilter requestURI="+hReq.getRequestURI()); if (!hReq.getRequestURI().startsWith(this.webAppPath)) { boolean isProtectedPath = false; for (int i=0; i<this.protectedPaths.length; i++) { if(hReq.getRequestURI().startsWith(this.protectedPaths[i])) { isProtectedPath = true; break; } } if (!isProtectedPath) { chain.doFilter(req, resp); return; } //This is not the /webAppPath path, so the user is accessing //a protected resource in a protected application. //Redirect the user over to the /webAppPath application for //multifactor authentication. hResp.sendRedirect(this.webAppPath+"/?originalRequest=" +URLEncoder.encode(hReq.getRequestURI())); return; } if (hReq.getMethod().equalsIgnoreCase("HEAD") || hReq.getHeader("Accept").contains("application/xrds+xml")) { System.out.println("Ignoring....REALM DISCOVERY"); hResp.setStatus(HttpServletResponse.SC_OK); return; } Authentication auth = new weblogic.security.services.Authentication(); try { HttpSession session = hReq.getSession(true); AppChallengeContext acc = (AppChallengeContext)session.getAttribute("ChallengeCtx"); MultiFactorAuthenticationChallengeContext ctx = new MultiFactorAuthenticationChallengeContext (req,resp); if (acc==null) { acc = auth.assertChallengeIdentity("multi","multi",ctx); //This session is for the target application session.setAttribute("ChallengeCtx", acc); } else { auth.continueChallengeIdentity(acc, "multi", "multi",ctx); } if (acc.hasChallengeIdentityCompleted()) { String originalRequest = (String)hReq.getAttribute("originalRequest"); System.out.println("Redirecting to: "+originalRequest); //Done -> if they don't get access, they'll need to log in again session.removeAttribute("ChallengeCtx"); Subject subject = (Subject)session.getAttribute("subject"); ServletAuthentication.runAs(subject, hReq); } else { Object challengeToken = acc.getChallengeToken(); if (challengeToken == null) { //Redirect to Login throw new ServletException("Challenge Token is NULL"); } else { hResp.sendRedirect(this.webAppPath+"/otp.jsp"); } } } catch (Exception e) { e.printStackTrace(); throw new ServletException(e.getMessage()); } } @Override public void init(FilterConfig arg0) throws ServletException { // TODO Auto-generated method stub System.out.println("Initializing....."); } }
MultiFactorAuthenticationChallengeContext.java
public class MultiFactorAuthenticationChallengeContext implements ProviderChallengeContext { private boolean completed = false; private Object challengeToken = null; private CallbackHandler handler = null; private Subject discovered = null; private String originalRequest; private String webAppPath = null; public MultiFactorAuthenticationChallengeContext (MultiFactorAuthenticationProviderMBean config) { super(); try { this.webAppPath = config.getMultiFactorWebAppPath(); } catch (Exception e) { throw new SecurityException(e.getMessage()); } } ................................................................................ @Override public boolean hasChallengeIdentityCompleted() { // TODO Auto-generated method stub return completed; } void processRequest(ContextHandler ctx) throws Exception { HttpServletRequest req = (HttpServletRequest) ctx.getValue("req"); HttpSession session = req.getSession(true); if (session.getAttribute("subject") == null) { String userName = req.getParameter("j_username"); String password = req.getParameter("j_password"); if (userName == null || password == null) { throw new Exception("No user name and password in request"); } else { this.originalRequest = req.getParameter("originalRequest"); System.out.println("Saving the original request: "+this.originalRequest); //validate the user name password Authentication auth = new weblogic.security.services.Authentication(); try{ Subject s = auth.login(new MyCallBackHandler(userName, password)); session.setAttribute("subject", s); }catch(LoginException le){ throw new Exception("Invalid user name/password", le); } this.generateOTP(req); } } else { this.validateResponse(req); } } private void validateResponse(HttpServletRequest req) throws Exception { // TODO Auto-generated method stub System.out.println("Getting Validated...."); String token = req.getParameter("OTP"); if(token == null || token.length() == 0 || !this.challengeToken.equals(token)){ throw new Exception("Invalid OTP"); } this.completed = true; if (this.originalRequest!=null) { //Set it as an attribute on the request, so the filter can use it req.setAttribute("originalRequest", this.originalRequest); } else { throw new Exception("Couldn't locate original request in ChallengeContext"); } //return verified; // success } private void generateOTP(HttpServletRequest req) throws Exception { // TODO Auto-generated method stub /* *this is for demo only. Write your custom code to *generate a token and send it to user *using a mechanism which works for your env. this.challengeToken = "0123456789"; } }
注意 MultifactorAuthenticationContext 类中的 processRequest 和 generateOTP 方法。processRequest 方法负责使用一个 WebLogic API 验证用户名/口令。如果用户名和口令有效,该方法将调用 generateOTP 方法生成 OTP,并将其发送给用户。然后,MultiFactorAuthenticationFilter 类将用户重定向到 Web 应用程序的 otp.jsp 页面,如上文所述。用户输入 otp,由 MultifactorAuthenticationContext 类的 validateResponse 方法进行验证。此时,身份验证成功,用户可以访问受保护的 Web 应用程序了。
需要注意的是,使用此机制保护所有 Web 应用程序的做法可能并不可取。例如,我们不应使用这种保护机制来保护 Oracle WebLogic 管理控制台,因为任何代码错误都会阻止对管理控制台的访问。有关如何配置身份验证提供程序的详细信息,请参见为 Oracle WebLogic Server 开发安全提供程序中的“参考资料”一节。
以下是该自定义身份验证提供程序重要的 mbean 属性:
<MBeanAttribute Name = "MultiFactorWebAppPath" Type = "java.lang.String" Default = ""/multifactor"" Description = "The Path (Context Root) of the custom Web App which is responsible for collecting user's credentials and OTP" /> <MBeanAttribute Name = "ProtectedPaths" Type = "java.lang.String[]" Default = "new String[] { "/App" }" Description = "The paths of web apps that are protected by multi factor authentication" />
本文介绍了如何利用多因素身份验证(用户名、口令以及 OTP 的组合)保护 WebLogic 服务器中部署的 Web 应用程序。本文还介绍了只能用多因素身份验证保护部分 Web 应用程序,以及不能用本文所描述的机制保护 Oracle WebLogic 管理控制台。
感谢 Yi Wang 对本文的审校和反馈。
Shailesh K. Mishra 是 Oracle Identity Manager 团队成员。他获得了印度理工学院的技术学士学位。闲暇之余,他喜欢研究中间件性能和安全。