文章
身份和安全性
| |||
|
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 团队成员。他获得了印度理工学院的技术学士学位。闲暇之余,他喜欢研究中间件性能和安全。