Web容器安全管理(下)——容器基本身份验证

来源:互联网 发布:圣思园java视频下载 编辑:程序博客网 时间:2024/05/17 22:36

  为了更好地了解并实现Web容器的安全管理,笔者以两篇博客的篇幅来介绍,即:《Web容器安全管理(上)——Java EE的安全概念》 和 《Web容器安全管理(下)——容器基本身份验证》。上篇博客已经介绍了Java EE安全的基本概念,打下了基础。在本文,我们详述Web容器提供的基本身份验证方式。

1、容器声明式基本身份验证

  假设你已经开发好了应用程序,现在想针对几个页面进行保护,只有通过身份验证且具备足够权限的用户,才可以浏览这些页面。这个需求有几个部分必须实现:
  (1)身份验证的方式
  (2)授予访问页面的权限
  (3)定义用户

  这里采用Web容器提供的最简单的基本(Basic)验证,在访问藉此受保护的资源时,浏览器会弹出对话框要求输入用户名和密码。如下图所示,是chrome弹出的身份验证对话框。

  使用Web容器提供的基本身份验证功能,需要在应用程序的web.xml中定义:

<login-config>    <auth-method>BASIC</auth-method></login-config>

  接着要授予指定角色访问页面的权限,所以要先定义角色,在授权之前,必须在应用程序中,定义角色名称。可以在web.xml中如下定义:

<security-role>    <role-name>admin</role-name></security-role><security-role>    <role-name>manager</role-name></security-role>

  在这里定义了admin与manager两个角色名称。接着定义哪些URL可以被哪些角色以哪种HTTP方法访问。例如设置/admin下所有页面,无论使用哪个HTTP方法,都只能被admin角色访问:

<security-constraint>    <web-resource-collection>        <web-resource-name>Admin</web-resource-name>        <url-pattern>/admin/*</url-pattern>    </web-resource-collection>    <auth-constraint>        <role-name>admin</role-name>    </auth-constraint></security-constraint>

  如果有多个角色可以访问某些页面,则<auth-constraint>标签可以设置多个<role-name>标签。在这里看不到任何HTTP方法规范的定义,默认就是所有HTTP方法都受到限制。再来看另一个例子:

<security-constraint>    <web-resource-collection>        <web-resource-name>Manager</web-resource-name>        <url-pattern>/manager/*</url-pattern>        <http-method>GET</http-method>        <http-method>POST</http-method>    </web-resource-collection>    <auth-constraint>        <role-name>admin</role-name>        <role-name>manager</role-name>    </auth-constraint></security-constraint>

  在这个设置中,对于/manager下的所有页面,根据&http-method>的设置,只有admin或manager才可以使用GET与POST方法进行访问。请留意这个语义“只有admin或manager才可以使用GET与POST方法进行访问”,这表示,其他HTTP方法,如PUT、TRACE、DELETE、HEAD和OPTIONS等,无论是否具备admin或manager角色,都可以访问!

  若没有设置<http-method>,则所有HTTP方法都受到限制。设置了<http-method>,则只有被设置HTTP受到限制,其他方法不受限制。如果没有设置<auth-constraint>标签,或<auth-constraint>标签中设置了<role-name>*</role-name>,表示任何角色都可以访问。如果直接编写了<auth-constraint/>,那就没有任何角色可以访问了

  下面是一个完整的设置范例:

<?xml version="1.0" encoding="UTF-8"?><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/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">  <display-name>SecurityBasicDemo</display-name>  <welcome-file-list>    <welcome-file>index.jsp</welcome-file>  </welcome-file-list>  <session-config>  <!-- 在工程的web.xml文件中设置session失效时间,时间单位为分钟   -->  <!-- Tomcat默认session超时时间为30分钟,可以根据需要修改,负数或0为不限制session失效时间。-->    <session-timeout>30</session-timeout>    <!--代码中设置 session.setMaxInactiveInterval(30*60); 以秒为单位-->  </session-config>  <security-constraint>    <web-resource-collection>        <web-resource-name>Admin</web-resource-name>        <url-pattern>/admin/*</url-pattern>    </web-resource-collection>    <auth-constraint>        <role-name>admin</role-name>    </auth-constraint>  </security-constraint>  <security-constraint>    <web-resource-collection>        <web-resource-name>Manager</web-resource-name>        <url-pattern>/manager/*</url-pattern>        <http-method>GET</http-method>        <http-method>POST</http-method>    </web-resource-collection>    <auth-constraint>        <role-name>admin</role-name>        <role-name>manager</role-name>    </auth-constraint>  </security-constraint>  <login-config>    <auth-method>BASIC</auth-method>  </login-config>  <security-role>    <role-name>admin</role-name>  </security-role>  <security-role>    <role-name>manager</role-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文件中。你也可以改用数据库表格,这需要额外配置。

2、容器声明式基本身份验证的原理

  在初次请求某个受保护的URL时,容器会检查请求中是否包括Authorization标头,如果没有的话,则容器会响应401 Unauthorized的状态码与信息,以及WWW-Authenticate标头给浏览器,浏览器收到WWW-Authenticate标头之后,就会出现对话框要求用户输入名称及密码,原理如下图所示:

  如果用户在对话框中输入名称、密码后按下确定键,则浏览器会将名称密码以BASE64方式编码,然后放在Authorization标头中送出。容器会检查请求中是否包括Authorization标头,并验证名称、密码是否正确,如果正确,就将资源传送给浏览器。如下图所示:

  BASE64是将二进制的字节编码为ASCII序列的编码方式,在HTTP中可用来传送内容较长的数据。编码并非加密,只要译码方式正确,就可以取得原本的信息

  接下来在关闭浏览器之前,只要是对服务器资源的请求,每次都包括Authorization标头,而服务器每次也都会检查是否有Authorization标头,所以登录期间会一起持续到关闭浏览器为止。如下图所示,为基本身份验证的流程图。

  由于使用的是浏览器提供的对话框输入名称、密码,所以基本身份验证时无法自定义登录画面。由于传送名称、密码时使用的是Authenticate标头,无法设计注销机制,关闭浏览器是结束会话的唯一方式。

3、容器声明式窗体身份验证

  如果需要自定义登录页面,以及登录错误的页面,则可以改用容器所提供的窗体(Form)验证。要将之前的基本身份验证改为窗体验证的话,可以在web.xml中修改<login-config>的设置:

//略...<login-config>    <auth-method>FORM</auth-method>    <form-login-config>        <form-login-page>/login.html</form-login-page>        <form-error-page>/error.html</form-error-page>    </form-login-config></login-config>//略...

  在<auth-method>的设置从BASIC改为FORM。由于使用了窗体网页进行登录,所以必须告诉容器,登录页面是哪个,登录失败页面是哪个。这是由<form-login-config>标签对来设置,设置时必须以斜杠开始,也就是从应用程序根目录开始的URL路径。

  接下来就可设置自己的窗体登录页面,但必须注意!窗体发送的URL必须是j_security_check,发送名称的请求参数必须是j_username,发送密码的请求参数必须是j_password。以下是login.html的简单示例:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><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的简单示例:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><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>

  登录时的页面如图所示:

  登录失败时的页面如图所示:

4、容器窗体身份验证的原理

  来了解一下容器利用窗体进行验证的原理。当使用窗体身份验证时,如果要访问受保护的资源,容器会检查用户有无登录,方式是查看HttpSession中有无”javax.security.auth.subject“属性,若没有这个属性,则表示没有经过容器的验证流程,则转发至登录页面,用户输入名称、密码并发送后,若验证成功,则容器会在HttpSession中设置属性名称”javax.security.auth.subject“的对应值javax.security.auth.subject实例。具体的流程如下图所示:

  用户是否登录是通过HttpSession的”javax.security.auth.subject“属性来判断,所以要让此次登录失败,可以调用HttpSession的invalidate()方法,因此窗体验证时可以设计注销机制。

  除了基本身份验证与窗体验证之外,在<auth-method>中还可以设置DIGEST或CLIENT-CERT。

  DIGEST即所谓”摘要验证“,浏览器也会出现对话框输入名称、密码,而后通过Authorization标头传送,只不过并非使用BASE64来编码名称、密码。浏览器会直接传送名称,但对密码则先进行(MD5)摘要演算(非加密),得到理论上唯一且不可逆的字符串再传送,服务器根据名称从后端取得密码,以同样的方式作摘要演算,再比对浏览器送来的摘要字符串是否符合,如果符合就验证成功。由于网络上传送的并不是真正的密码,而不是不可逆的摘要,密码不会被得知,理论上比较安全。不过Java EE规范中并无要求一定得支持DIGEST的验证方式(看厂商的需要,Tomcat是支持的)。

  CLIENT-CERT也是用对话框的方式来输入名称与密码,因为使用PKC(Public Key Certificate)作加密,可保证数据传送时的机密性及完整性,但客户端需要安装证书(Certificate),在一般用户及应用程序之间并不常采用。

5、编程式安全管理

  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("任意其他用户就可以看到的数据一<br>");        try {            request.authenticate(response);            out.println("<h1>必须由容器验证通过的用户才可以看到的数据</h1><br>");        } catch (AccessControlException  e) {            e.printStackTrace();        }        out.println("任意其他用户就可以看到的数据二<br>");    }    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("任意其他用户就可以看到的数据一<br>");        try {            String user = request.getParameter("user");            String passwd = request.getParameter("passwd");            request.login(user, passwd);            out.println("<h1>必须由容器验证通过的用户才可以看到的数据</h1><br>");        } catch (AccessControlException  e) {            e.printStackTrace();        } finally {            request.logout();        }        out.println("任意其他用户就可以看到的数据二<br>");    }    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        // TODO Auto-generated method stub        doGet(request, response);    }}

  在浏览器的URL地址栏需要输入user与passwd参数,若参数通过验证,则可显示接下来的内容。