看完了标记文件, 我决定把自定义标签的部分先放一下, 因为其背后的本质是相同的, 只不过显式的编写代码以及嵌套标签提高了复杂程度等. 还是先继续向后把整体都过一遍.
今天是2019年10月的最后一天, 在去年的这个时候, 我还因为脚骨折在家里休息, 拼命的翻译 Django 2 by Example, 如今一年过去, 不仅Java来了两遍, 理解更加深刻, 还武装了计算机基础知识等很多东西.
Web也是二进宫从底层学起, 感觉确实越来越好了.
WEB安全是一个永恒的话题, 从我个人的体会来说就是保证什么人可以做什么样的事情.
- WEB安全的概念
- Tomcat的安全机制
- Tomcat的授权
- 简单例子
- 动态响应授权
- 认证方式
- 表单认证
- 加密传输
WEB安全的概念
先来探讨纯概念的部分. 因为Web应用不像本地应用, 还加上了一层本地计算机操作系统提供的安全防护. Web应用是开放给几乎所有人使用的. 其安全概念主要有如下几个:
- 认证: 即判断访问的人是谁 一般通过各种形式的认证名称+认证密码来实现, 比如用户名+密码, 账号+KEY之类.
- 授权: 知道了是谁之后, 看这个人是否具备相应的权限. 用一个公司来说的话, 门口刷卡好比是认证, 所有有卡的员工都可以刷卡进入, 这是认证, 但是刷完卡进来之后, 不同的员工去不同的办公室做不同的事情, 这就是授权.
- 机密性: 这是说, 在通过认证和授权之后, 客户端就可以和服务器发生数据往来, 要保证这个数据是私密的, 只有双方知道, 避免其他人窃取和篡改.
- 数据完整性: 这个就是说要有机制保证双方的通信是完整的, 不会意外中断, 或者说中断的情况下有应对机制.
Servlet规范并没有具体说安全, 有很多安全都是由不同容器实现的. 下边就要来看看Tomcat的安全相关的内容.
Tomcat的安全机制
Tomcat的安全机制是:
- HTTP请求到达容器, 容器解析要访问的路径
- 容器会查找安全配置, 看这个路径是否有对应的安全配置
- 如果有安全配置, 第一步需要认证, 要检查请求中是否附带了认证信息, 并把认证信息与安全配置中的信息进行比较
- 认证通过之后,第二步需要授权, 将认证通过的用户与映射到用户身上的角色关联起来, 在访问具体资源之前, 检查角色是否符合安全配置中的要求
- 认证和角色都通过之后, 请求到达Servlet, 进行处理后返回
- 认证和角色任何一个不通过, 都会返回错误代码, 一般是401.
认证的部分还涉及到过滤器等操作(如果愿意的话), 以及前后端交互. 核心是发送和接受认证信息, 确定用户身份.
授权是经过认证之后, 在服务器本地做的工作, 即确定用户身份对应的角色.
Tomcat的授权
先来看授权. 在安全领域有一个词语叫做realm=领域, 指的是存放角色信息的地方. Tomcat使用目录conf/下的一个tomcat-user.xml
文件来存放领域信息.
这个文件在每次容器启动的时候读入内存然后生效, 运行时无法修改. 此时我打开看了一下, 还没有配置过安全信息的文件是这样的:
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users 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"
version="1.0">
<!--
<role rolename="tomcat"/>
<role rolename="role1"/>
<user username="tomcat" password="<must-be-changed>" roles="tomcat"/>
<user username="both" password="<must-be-changed>" roles="tomcat,role1"/>
<user username="role1" password="<must-be-changed>" roles="role1"/>
-->
</tomcat-users>
其中的注释就显示了如何配置角色的用户名称, 这里可以发现, 一个用户可以对应多个角色, 这就为多层授权体系提供了基础.
这个文件还需要和web.xml搭配使用, 为了说明如何使用, 来弄一个例子试验一下是最简单的.
简单例子
先在tomcat-user.xml
里写几个配置:
<role rolename="Admin"/>
<role rolename="User"/>
<role rolename="Guest"/>
<user username="user1" password="1234" roles="Admin, User, Guest"/>
<user username="user2" password="2234" roles="User, Guest"/>
<user username="user3" password="3234" roles="Guest"/>
这些配置看名称都可以看出来, 指定了三个角色和三个用户, user1用户同时具有三个角色, user2具有2个角色, user3仅仅只有Guest角色.
为何要如此设置, 是因为安全配置实际上是指定了什么样的资源可以由什么角色访问, 所以越是安全性低的资源, 就需要对应一个越广泛的角色, 这里所有用户都具备Guest角色, 就说明这个角色一般对应的权限是最低的, Guest一般都被配置成无需认证就可以访问的资源.
然后需要在web.xml
中进行配置:
<web-app>
......
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
<security-role>
<role-name>Admin</role-name>
</security-role>
<security-role>
<role-name>User</role-name>
</security-role>
<security-role>
<role-name>Guest</role-name>
</security-role>
<security-constraint>
<web-resource-collection>
<web-resource-name>UpdateRecipes</web-resource-name>
<url-pattern>/admin/*</url-pattern>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>Admin</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>UpdateRecipes</web-resource-name>
<url-pattern>/user/*</url-pattern>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>User</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>UpdateRecipes</web-resource-name>
<url-pattern>/guest/*</url-pattern>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>Guest</role-name>
</auth-constraint>
</security-constraint>
</web-app>
login-config
指定了认证方式为Basic, 这会在访问受限资源的时候打开一个浏览器的提示框来输入信息. 这里虽然还没有讲认证, 但必须要认证, 否则Web容器无法将用户和角色对应起来. 这里如果去掉认证, 则所有访问都不会具备三个角色中的任何一个, 则只能访问无角色可以访问的路径, 比如首页.
之后的security-role
中的Admin等名称, 一定要对应到tomcat-user.xml
中定义的用户名称. 这里对应了三个, 就表示使用了其中的三个. 如果只想使用其中的两个, 就只定义两个security-role
标签即可.
其后的security-constraint
是一条约束, 可以同时配置多个约束, 也就是存在多个security-constraint
标签. 一个约束的详细配置如下:
web-resource-collection
, 定义需要约束的URL和与其对应的方法, 约束Web资源
web-resource-name
, 其中固定是UpdateRecipes, 这个名称是给容器使用的, 不用更改
url-pattern
, 可以采用通配符的URL地址, 指定需要保护的URL. 该标签可以有多个, 全部生效.
http-method
, 指定需要保护的HTTP方法, 该标签可选, 如果不使用该标签则默认保护全部HTTP方法, 如果指定了具体的比如这里的GET, 则仅仅会保护GET方法, 不保护其他方法.
auth-constraint
, 如果说前边的web-resource-collection
约束的是Web资源, 这个标签约束的就是用户资源. 一个安全约束也就是一个Web资源与用户资源的对应关系. 这个标签也是可选的, 如果不配置该标签, 容器允许不认证就访问对应的URL. 在这里我们没有对根目录进行认证, 所以不认证就可以访问.
role-name
, 其中的名称必须是web.xml
里边的security-role
中的角色名, 而不是tomcat-user.xml
中的用户角色名, 因为这中间隔了一层映射关系, 有可能web.xml
没有出现tomcat-user.xml
中的角色名称, 要注意. 其中的名称就是允许访问前边Web资源的用户角色. 该标签可选, 如果不配置该标签, 则会禁止所有角色, 如果主体配置成*
, 会对所有角色开放.
这里我们定义了三个约束, admin路径可以被Admin角色访问, user路径可以被User访问, Guest路径只能被Guest访问. 在访问了首页点击链接后, 会弹出认证框, 根据认证结果, 会显示网页或者错误.
还记得之前配置的user1具有全部权限吗, 所以认证后可以访问全部的网页, user2不具有Admin权限, 所以无法访问admin路径, 而Guest不具备Admin和User权限, user路径也不能访问.
这就是最简单的一个认证加上授权的体系. 还有一点要说明的是, 如果多个security-constraint
对同一个URL进行了配置, 遵循如下的原则确定最终效果:
- 如果任意一个配置了全部禁止(即有
auth-constraint
标签, 但是其内部没有role-name
), 则全部禁止
- 否则按照两者的并集来计算(比如某个约束里压根没有
auth-constraint
, 合并的结果就是可以不经认证就访问, 也就是完全开放).
动态响应授权
现在我们通过容器和web.xml两个文件配置好了先认证, 再授权的体系. 我实际上编写了三个Servlet对应三个路径.
观察上边的例子可以看到, 我们限定死了URL, 即从URL的角度就加以了区分, 如果希望用同一个Servlet来响应不同角色的功能, 要如何动态的确定呢.
现在我们改成, 统一认证, 然后根据用户认证的结果, 在页面中作出不同的响应, 这样可以只编写一个Servlet就可以了.
在web.xml中, 我们配置一个仅仅需要认证的路径:
<servlet>
<servlet-name>dynamic</servlet-name>
<servlet-class>com.example.web.DynamicRoleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>dynamic</servlet-name>
<url-pattern>/dynamic</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>UpdateRecipes</web-resource-name>
<url-pattern>/dynamic/*</url-pattern>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>Guest</role-name>
</auth-constraint>
</security-constraint>
由于所有用户都具备Guest权限, 实际上这个路径只要认证了就能通过, 编写com.example.web.DynamicRoleServlet如下:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class DynamicRoleServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
if (req.isUserInRole("Admin")) {
out.println("欢迎Admin");
}
else if (req.isUserInRole("User")) {
out.println("欢迎User");
}
else if (req.isUserInRole("Guest")) {
out.println("欢迎Guest");
}
else {
out.println("意外出现了, 没有角色的用户未经认证也访问到本页面");
}
}
}
这其中进行了判断, 根据用户的角色不同, 进行不同的处理, 由于我们配置了这个页面需要认证, 最基础的角色是Guest, 因此不会出现没有角色的用户也能访问到该页面.
之后启动浏览器访问, 根据你的角色不同, 页面的处理也不同.
isUserInRole()
方法的参数, 现在是直接使用了web.xml
中的security-role
标签中定义的名称, 但准确的来说, 参数应该是一个映射后的名称, 只不过我们没有做映射.
通过刚才的设置会知道, web.xml
的角色名称实际上和tomcat-user.xml
是相同的, 可以在web.xml
中, 在需要使用映射的角色名的servlet中, 创建角色名称和程序中使用的角色名的对应关系:
<servlet>
<servlet-name>dynamic</servlet-name>
<servlet-class>com.example.web.DynamicRoleServlet</servlet-class>
<security-role-ref>
<role-name>SuperAdmin</role-name>
<role-link>Admin</role-link>
</security-role-ref>
<security-role-ref>
<role-name>NormalUser</role-name>
<role-link>User</role-link>
</security-role-ref>
<security-role-ref>
<role-name>Tourist</role-name>
<role-link>Guest</role-link>
</security-role-ref>
</servlet>
此时在DynamicRoleServlet类中, 修改代码如下:
package com.example.web;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class DynamicRoleServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
if (req.isUserInRole("SuperAdmin")) {
out.println("欢迎Admin");
}
else if (req.isUserInRole("NormalUser")) {
out.println("欢迎User");
}
else if (req.isUserInRole("Tourist")) {
out.println("欢迎Guest");
}
else {
out.println("意外出现了, 没有角色的用户未经认证也访问到本页面");
}
}
}
不过因为原来的名称也有效, 其实不更改代码, 也是可以工作的, 这只是映射和解耦的一个手段, 即编写Servlet的人员未必要知道具体底层服务器的角色配置.
看完了授权, 然后再来看认证的处理, 所谓认证, 就是询问用户名和密码, 然后看验证结果是否允许通过.
认证方式
如果是在IDE中配置:
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
会自动跳出来四个备选, 分别是BASIC, DIGEST, CLIENT-CERT和FORM, 第一种已经用过了, 是浏览器弹出对话框直接输入, 然后把输入的信息以Base64方式发回去, 这种加密基本上就是明文.
DIGEST是摘要方式, 没有得到广泛应用. CLIENT-CERT比较安全, 但需要客户事先获取证书. 前边三种都使用浏览器的标准对话框来进行.
最后一种是表单认证, 可以创建自己的认证方式, 不过也最不安全, 用户名和密码都会在HTTP的请求中以明文发送. 当然, 如果本身建立在HTTPS上, 就好很多. 不过这里先不关心认证以外的事情, 重点来看表单认证.
表单认证
表单认证需在<login-config>
添加更多的内容, 然后需要创建一个表单和创建一个错误表单. 而表单的用户字段, 密码字段和发送到的地址有着特定的名称.
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/loginerror.html</form-error-page>
</form-login-config>
</login-config>
配置指定了认证形式是表单, 登录页面是login.html, 错误页面是loginerror.html. 下边来编写这两个页面:
<!--login.html-->
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<title>登录</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
</head>
<body>
<h1 style="text-align: center">请登录</h1>
<div style="max-width: 600px; margin: 0 auto">
<form action="j_security_check" method="post">
<label for="username">请输入用户名<input id="username" type="text" name="j_username"></label>
<label for="password">请输入密码<input id="password" type="password" name="j_password"></label>
<input type="submit">
</form>
</div>
</body>
</html>
注意其中红色的部分, 如果要使用Tomcat容器本身的验证, 这些名字必须要写成这样.
错误页面可以是任意页面, 比如只显示一行, 登录错误, 返回首页即可. 就省略了.
如此配置之后, 在原来弹出对话框认证的地方, 就会使用这个表单来认证, 如果认证错误就会显示错误页面, 认证通过则显示原来要去往的页面.
加密传输
采用表单登录是如今大多数Web应用的登录方式. 不过表单数据是明文, 如何保证在传输的过程中是安全的. 这需要使用到SSL之上的HTTPS协议. 关于SSL和TLS再有HTTPS, 可以查看具体的文章了, 通信加密又是另外一个大的内容了.
好在如今的容器都支持SSL为基础的这些加密通信的办法. 以一个声明就可以启用支持:
<security-constraint>
<web-resource-collection>
<web-resource-name>UpdateRecipes</web-resource-name>
<url-pattern>/guest/*</url-pattern>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>Guest</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
这其中的配置可以是NO, INTEGRAL或者CONFIDENTIAL, 由于所有容器几乎都支持SSL了, 所以配置CONFIDENTIAL就可以了
不过如果这么做的话, 在验证成功之后, 访问对应的资源实际上协议发生了变化, transport-guarantee
这实际上没有重定向资源, 而是重定向了协议, 不进行额外的配置和获取证书是启用不了的.
至于如何配置Web服务器以及启用HTTPS, 估计要等到之后使用Nginx的时候再学习了