为 OTN 撰稿
为 Oracle 技术网撰写技术文章,获得报酬的同时可提升技术技能。
了解更多信息
密切关注
OTN 架构师社区
OTN ArchBeat 博客 Facebook Twitter YouTube 随身播图标

Oracle WebLogic 中的多因素身份验证

Shailesh K. Mishra

使用多因素身份验证保护 Oracle WebLogic 上部署的 Web 应用程序。

2013 年 10 月

下载
download-icon13-1Oracle WebLogic Server

简介

在多因素身份验证中,基于多种因素验证用户真实性,而不仅仅是传统的用户名/口令机制。例如,当一位银行客户访问 ATM 机时,其中一个身份验证因素是物理 ATM 卡(“用户拥有的东西”);第二个因素是客户的账户 PIN(“用户知道的东西”)。这两个因素若未能同时通过验证,则身份验证失败。

多因素身份验证概念也可应用于 Oracle WebLogic Server 上部署的 Web 应用程序,下面将对此进行详细介绍。

注意:本文假定读者充分了解 Oracle WebLogic 安全概念和身份验证机制。本文使用了 Oracle WebLogic 10.3.5 版。

了解 Oracle WebLogic 身份验证过程

为简单起见,本文只介绍 weblogic.security.spi.ChallengeIdentityAsserterV2weblogic.security.spi.ServletAuthenticationFilter 接口。有关接口的详细信息以及 Oracle WebLogic 中身份验证提供程序生命周期的详细信息,请参见为 Oracle WebLogic Server 开发安全提供程序 中的“参考资料”一节。

weblogic.security.spi.ChallengeIdentityAsserterV2:

此接口允许身份断言提供程序支持 Microsoft 的 Windows NT 质询/响应 (NTLM)、简单和受保护的 GSS-API 协商机制 (SPNEGO) 等身份验证协议,以及其他质询/响应身份验证机制。它的两个方法 assertChallengeIdentitycontinueChallengeIdentity 是本文的重点。第一个方法使用提供的令牌建立客户端身份(可能通过多次质询);第二个方法用于继续建立客户端身份,直到过程完成。

weblogic.security.spi.ServletAuthenticationFilter:

此接口用于表示在身份验证过程中,Servlet 容器应包括此身份验证提供程序的身份验证筛选器。它定义了一个方法 getServletAuthenticationFilters,返回将在 Servlet 容器的身份验证过程中执行的 javax.servlet.Filters 的有序列表。

为 Web 应用程序实现多因素身份验证

我们需要创建以下项目:

  • 收集数据以用于多因素身份验证的 Web 应用程序
  • 将实现以下接口的自定义身份验证提供程序:weblogic.security.spi.ChallengeIdentityAsserterV2weblogic.security.spi.ServletAuthenticationFilter

创建 Web 应用程序

此 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 对本文的审校和反馈。

参考资料

  1. Oracle WebLogic Server 示例代码(查找 OpenId SSO for WLS)
  2. 为 Oracle WebLogic Server 开发安全提供程序

关于作者

Shailesh K. Mishra 是 Oracle Identity Manager 团队成员。他获得了印度理工学院的技术学士学位。闲暇之余,他喜欢研究中间件性能和安全。