这里的Java Web容器特指Tomcat,Tomcat依然是最流行的Java Web容器,你大爷还是你大爷。
本文并不涉及业务层面上的安全控制,只针对Tomcat自身所支持的相关安全控制功能与特性。
首先看一下Web容器的四个基本安全特性
- 验证 Authentication
- 资源访问控制 Access control for resources
- 数据完整性 Data Integrity
- 数据机密性或私密性 Confidentiality or Data Privacy
每一项的细节这里不展开,并不难理解。
Java EE使用基于角色的访问控制——有用户、组、角色三个基本概念。
存储用户名和密码的地方叫Realm,有可能是文件,比如Tomcat的tomcat-users.xml
,也可能是数据库,或者基于证书的机制。
Tomcat支持声明式验证和编程式验证,一个是基于XML配置,一个是通过代码实现。
声明式基本身份验证
在web.xml中定义
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
同时可以定义角色
<security-role>
<description>Admin User</description>
<role-name>admin</role-name>
</security-role>
<security-role>
<description>Manager</description>
<role-name>manager</role-name>
</security-role>
针对路径做控制
<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>
连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>
<!-- 这是取反
<http-method-omission>GET</http-method-omission>
-->
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
<role-name>manager</role-name>
<!--表示任一通过验证的用户
<role-name>**</role-name>
-->
</auth-constraint>
</security-constraint>
<!-- 除了GET POST 其他HTTP方法即便是admin manager也无法访问 -->
<!-- 加上这个标签后,对于不在<security-constraint>里的HTTP方法请求,会返回403 -->
<deny-uncovered-http-methods />
部署后,根据各服务器来配置用户和角色,比如Tomcat的就在conf/tomcat-users.xml
里。
Tomcat的验证过程如下
- 初次访问某个受保护的URI,Web容器会检查请求中是否包括
Authorization
头,如果没有,容器会返回401,以及WWW-Authenticate
标头给浏览器,浏览器收到后会弹出对话框要求用户输入名称和密码。 - 如果用户输入用户名、密码正确后,浏览器会将用户名、密码以BASE64方式编码,然后放在
Authorization
标头中送出,容器进行验证,正确就将资源传回。 - 在关闭浏览器之前,对服务器的请求都包括
Authorization
头,服务器也每次都检查,所以登录有效期一直持续到关闭浏览器为止。
现在实验步骤如下
在Tomcat9目录下,修改conf/tomcat-users.xml
文件,解注并新增admin的角色
<role rolename="admin"/>
<role rolename="tomcat"/>
<role rolename="role1"/>
<user username="tomcat" password="123456" roles="tomcat,admin"/>
<user username="both" password="123456" roles="tomcat,role1"/>
<user username="role1" password="123456" roles="role1"/>
在webapps目录下,新建test目录和/WEB-INF/web.xml
文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">
<description>
Servlet and JSP Examples.
</description>
<display-name>Servlet and JSP Examples</display-name>
<request-character-encoding>UTF-8</request-character-encoding>
<security-constraint>
<display-name>Example Security Constraint</display-name>
<web-resource-collection>
<web-resource-name>Protected Area - Allow methods</web-resource-name>
<url-pattern>/admin/*</url-pattern>
<http-method>DELETE</http-method>
<http-method>GET</http-method>
<http-method>POST</http-method>
<http-method>PUT</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>
新建/admin目录和/test.html文件,
访问http://localhost:8080/test/admin/test.html,第一次会打开对话框
输入正确后,查看请求可以看到Authorization
头
注意这种方式几乎跟裸奔没区别,只要能拦截到你的HTTP请求,就相当于查看到你的密码。上面Basic后的字符串用base64解密就是tomcat:123456。
如果需要自定义登录窗口,可以配置
<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>
登录表单的action和name属性是有要求的
<form method="POST" action='j_security_check' >
<input type="text" name="j_username">
<input type="password" name="j_password">
<input type="submit" value="Log In">
</form>
除了FORM和BASIC,<auth-method>
还可以设置为DIGEST
或CLIENT_CERT
。DIGEST
提交时传递的是MD5加密后的摘要,CLIENT_CERT
使用的是Public Key Certificate(PKC)
加密,客户端要安装证书。
如果要启用HTTPS,则在<security-constraint>
下设置
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
默认值是NONE
,还可以设置为INTEGRAL
,不过习惯设为CONFIDENTIAL
,效果一样。
设置完后,认证的时候会自动跳转为HTTPS
编程式安全管理
支持编程能带来更灵活的控制。HttpServletRequest里跟安全有关的方法有
- authenticate( ): 是否登录,如果没有,返回false,并会转到登录界面
- login( ): 登入
- logout( ): 登出
- getUserPrincipal( ): 取得代表用户的Principal对象
- getRemoteUser( ): 获得登录用户的名称
- isUserInRole(String role): 传入角色名称,检查登录用户是否属于该角色
@WebServlet("/secret")
public class User extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (request.authenticated(response)) { // 检查登录
// 执行登录后的用户能够做的事情
}
}
@WebServlet("/login")
public class Login extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("user");
String password = request.getParameter("passwd");
try {
request.login(username, password);
response.sendRedirect("user");
} catch( ServletException ex) {
response.sendRedirect("login.html");
}
}
和<security-constraint>
对标的注解是@ServletSecurity
,比如
@WebServlet("/admin")
@ServletSecurity(@HttpConstraint(rolesAllowed = {"admin"}))
就表示/admin只允许admin角色访问。
再看一个复杂一点的
@WebServlet("/admin")
@ServletSecurity(
value=@HttpConstraint(rolesAllowed = { "admin", "manager" }),
httpMethodConstraints = {
@HttpMethodConstraint(value = "GET", rolesAllowed = {"admin", "manager"},
transportGuarantee = TransportGuarantee.CONFIDENTIAL),
@HttpMethodConstraint(value = "POST", rolesAllowed = {"admin", "manager"}),
)
其它Realm
前面的例子都是把用户名密码放在配置文件里,这样带来一定的隐患,在Tomcat里也支持其它的Realm,比如
- JDBCRealm
- DataSourceRealm
- JNDIRealm
- UserDatabaseRealm
- MemoryRealm
- JAASRealm
- CombinedRealm
- LockOutRealm
具体的用法可以查看官方文档Realm Configuration How-To