为了更好地了解并实现Web容器的安全管理,笔者以两篇博客的篇幅来介绍,即:《Web容器安全管理(上)——Java EE的安全概念》 和 《Web容器安全管理(下)——容器基本身份验证》。上篇博客已经介绍了Java EE安全的基本概念,打下了基础。在本文,我们详述Web容器提供的基本身份验证方式。
假设你已经开发好了应用程序,现在想针对几个页面进行保护,只有通过身份验证且具备足够权限的用户,才可以浏览这些页面。这个需求有几个部分必须实现:
(1)身份验证的方式
(2)授予访问页面的权限
(3)定义用户
这里采用Web容器提供的最简单的基本(Basic)验证,在访问藉此受保护的资源时,浏览器会弹出对话框要求输入用户名和密码。如下图所示,是chrome弹出的身份验证对话框。
使用Web容器提供的基本身份验证功能,需要在应用程序的web.xml中定义:
method>BASICauth-method>
login-config>
接着要授予指定角色访问页面的权限,所以要先定义角色,在授权之前,必须在应用程序中,定义角色名称。可以在web.xml中如下定义:
<security-role>
<role-name>adminrole-name>
security-role>
<security-role>
<role-name>managerrole-name>
security-role>
在这里定义了admin与manager两个角色名称。接着定义哪些URL可以被哪些角色以哪种HTTP方法访问。例如设置/admin下所有页面,无论使用哪个HTTP方法,都只能被admin角色访问:
<security-constraint>
<web-resource-collection>
<web-resource-name>Adminweb-resource-name>
<url-pattern>/admin/*url-pattern>
web-resource-collection>
<auth-constraint>
<role-name>adminrole-name>
auth-constraint>
security-constraint>
如果有多个角色可以访问某些页面,则
<security-constraint>
<web-resource-collection>
<web-resource-name>Managerweb-resource-name>
<url-pattern>/manager/*url-pattern>
<http-method>GEThttp-method>
<http-method>POSThttp-method>
web-resource-collection>
<auth-constraint>
<role-name>adminrole-name>
<role-name>managerrole-name>
auth-constraint>
security-constraint>
在这个设置中,对于/manager下的所有页面,根据&http-method>的设置,只有admin或manager才可以使用GET与POST方法进行访问。请留意这个语义“只有admin或manager才可以使用GET与POST方法进行访问”,这表示,其他HTTP方法,如PUT、TRACE、DELETE、HEAD和OPTIONS等,无论是否具备admin或manager角色,都可以访问!
若没有设置
下面是一个完整的设置范例:
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>SecurityBasicDemodisplay-name>
<welcome-file-list>
<welcome-file>index.jspwelcome-file>
welcome-file-list>
<session-config>
<session-timeout>30session-timeout>
session-config>
<security-constraint>
<web-resource-collection>
<web-resource-name>Adminweb-resource-name>
<url-pattern>/admin/*url-pattern>
web-resource-collection>
<auth-constraint>
<role-name>adminrole-name>
auth-constraint>
security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Managerweb-resource-name>
<url-pattern>/manager/*url-pattern>
<http-method>GEThttp-method>
<http-method>POSThttp-method>
web-resource-collection>
<auth-constraint>
<role-name>adminrole-name>
<role-name>managerrole-name>
auth-constraint>
security-constraint>
<login-config>
<auth-method>BASICauth-method>
login-config>
<security-role>
<role-name>adminrole-name>
security-role>
<security-role>
<role-name>managerrole-name>
security-role>
web-app>
就Web应用程序的设置部分,工作已经结束!但在将应用程序部署至服务器时,在服务器上设置角色与用户或组的对应,设置的方式并非Java EE的标准,而是各服务器都有所不同。例如在Tomcat中,可以在/conf/tomcat-users.xml中定义:
<tomcat-users version="1.0" xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd">
<role rolename="manager"/>
<role rolename="admin"/>
<user username="caterpillar" password="123456" roles="admin,manger"/>
<user username="momor" password="654321" roles="manager"/>
tomcat-users>
要启用Tomcat的安全管理功能,还必须在Server Options中选取Enable security,才会读取tomcat-users.xml中的设置信息。
在这个设置中caterpillar同时具备admin与manager角色,而momor则具备manager角色。在启动应用程序之后,如果访问/admin或/manager,就会出现对话框要求输入名称、密码。如果输入错误,就会被一起要求输入直到正确为止。
如果访问/admin下的页面,只有输入了caterpillar名称及正确的密码,才可以正确浏览到页面。如果输入了momor名称及正确的密码,会提示权限不足,拒绝访问。
上面虽然输入了momor名称及正确的密码,通过了浏览器的身份验证,但授权失败,弹出403画面。
tomcat-user.xml是Tomcat预设的Realm(不知道什么是Realm,请参看上一篇博文),角色、用户名称、密码都存储在这个xml文件中。你也可以改用数据库表格,这需要额外配置。
在初次请求某个受保护的URL时,容器会检查请求中是否包括Authorization标头,如果没有的话,则容器会响应401 Unauthorized的状态码与信息,以及WWW-Authenticate标头给浏览器,浏览器收到WWW-Authenticate标头之后,就会出现对话框要求用户输入名称及密码,原理如下图所示:
如果用户在对话框中输入名称、密码后按下确定键,则浏览器会将名称密码以BASE64方式编码,然后放在Authorization标头中送出。容器会检查请求中是否包括Authorization标头,并验证名称、密码是否正确,如果正确,就将资源传送给浏览器。如下图所示:
BASE64是将二进制的字节编码为ASCII序列的编码方式,在HTTP中可用来传送内容较长的数据。编码并非加密,只要译码方式正确,就可以取得原本的信息。
接下来在关闭浏览器之前,只要是对服务器资源的请求,每次都包括Authorization标头,而服务器每次也都会检查是否有Authorization标头,所以登录期间会一起持续到关闭浏览器为止。如下图所示,为基本身份验证的流程图。
由于使用的是浏览器提供的对话框输入名称、密码,所以基本身份验证时无法自定义登录画面。由于传送名称、密码时使用的是Authenticate标头,无法设计注销机制,关闭浏览器是结束会话的唯一方式。
如果需要自定义登录页面,以及登录错误的页面,则可以改用容器所提供的窗体(Form)验证。要将之前的基本身份验证改为窗体验证的话,可以在web.xml中修改
//略...
FORM
/login.html
/error.html
//略...
在
接下来就可设置自己的窗体登录页面,但必须注意!窗体发送的URL必须是j_security_check,发送名称的请求参数必须是j_username,发送密码的请求参数必须是j_password。以下是login.html的简单示例:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录页面title>
head>
<body>
<form action="j_security_check" method="post">
名称:<input type="text" name="j_username"><br>
密码:<input type="password" name="j_password"><br>
<input type="submit" value="送出"/>
form>
body>
html>
error.html的简单示例:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录失败网页title>
head>
<body>
<h1>用户名或者密码错误,登录失败h1>
<a href='login.html'>返回登录页面a>
body>
html>
登录时的页面如图所示:
登录失败时的页面如图所示:
来了解一下容器利用窗体进行验证的原理。当使用窗体身份验证时,如果要访问受保护的资源,容器会检查用户有无登录,方式是查看HttpSession中有无”javax.security.auth.subject“属性,若没有这个属性,则表示没有经过容器的验证流程,则转发至登录页面,用户输入名称、密码并发送后,若验证成功,则容器会在HttpSession中设置属性名称”javax.security.auth.subject“的对应值javax.security.auth.subject实例。具体的流程如下图所示:
用户是否登录是通过HttpSession的”javax.security.auth.subject“属性来判断,所以要让此次登录失败,可以调用HttpSession的invalidate()方法,因此窗体验证时可以设计注销机制。
除了基本身份验证与窗体验证之外,在
DIGEST即所谓”摘要验证“,浏览器也会出现对话框输入名称、密码,而后通过Authorization标头传送,只不过并非使用BASE64来编码名称、密码。浏览器会直接传送名称,但对密码则先进行(MD5)摘要演算(非加密),得到理论上唯一且不可逆的字符串再传送,服务器根据名称从后端取得密码,以同样的方式作摘要演算,再比对浏览器送来的摘要字符串是否符合,如果符合就验证成功。由于网络上传送的并不是真正的密码,而不是不可逆的摘要,密码不会被得知,理论上比较安全。不过Java EE规范中并无要求一定得支持DIGEST的验证方式(看厂商的需要,Tomcat是支持的)。
CLIENT-CERT也是用对话框的方式来输入名称与密码,因为使用PKC(Public Key Certificate)作加密,可保证数据传送时的机密性及完整性,但客户端需要安装证书(Certificate),在一般用户及应用程序之间并不常采用。
Web容器的声明式安全管理,仅能针对URL来设置哪些资源必须受到保护,如果打算依据不同的角色在同一个页面中设置可访问的资源,例如只有站长或版面管理员可以看到删除整个讨论组的功能,普通用户不行,那么显然无法单纯使用声明式安全管理来实现。
在Servlet3.0中,HttpServletRequest新增了三个与安全有关的方法:authenticate()、login()、logout()。
首先来看authenticate()方法,搭配先前的声明式身份验证的web.xml的设置,你可以决定程序中哪一段逻辑,只有通过了容器身份验证的用户才可以看到。
package cc.openhome;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.AccessControlException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(
name="SecurityServlet",
urlPatterns = { "/security" }
)
public class SecurityServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public SecurityServlet() {
super();
// TODO Auto-generated constructor stub
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("任意其他用户就可以看到的数据一
");
try {
request.authenticate(response);
out.println("必须由容器验证通过的用户才可以看到的数据
");
} catch (AccessControlException e) {
e.printStackTrace();
}
out.println("任意其他用户就可以看到的数据二
");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
authenticate()方法会检查用户是否已经通过了容器验证,否则根据web.xml中的设置,要求进行身份验证,若通过验证,则可显示接下来的内容。
login()在调用时则可以提供用户名称、密码,利用容器设置的身份验证信息来进行验证。例如,以下Servlet只有在提供的username、password请求参数正确时,才可以看到相应的数据。
package cc.openhome;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.AccessControlException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "SecuityLoginServlet", urlPatterns = { "/securityLogin" })
public class SecuityLoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public SecuityLoginServlet() {
super();
// TODO Auto-generated constructor stub
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("任意其他用户就可以看到的数据一
");
try {
String user = request.getParameter("user");
String passwd = request.getParameter("passwd");
request.login(user, passwd);
out.println("必须由容器验证通过的用户才可以看到的数据
");
} catch (AccessControlException e) {
e.printStackTrace();
} finally {
request.logout();
}
out.println("任意其他用户就可以看到的数据二
");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
在浏览器的URL地址栏需要输入user与passwd参数,若参数通过验证,则可显示接下来的内容。