Java Web Reinforcement 09 Web安全

Java Web Reinforcement 09 Web安全

看完了标记文件, 我决定把自定义标签的部分先放一下, 因为其背后的本质是相同的, 只不过显式的编写代码以及嵌套标签提高了复杂程度等. 还是先继续向后把整体都过一遍. 今天是2019年10月的最后一天, 在去年的这个时候, 我还因为脚骨折在家里休息, 拼命的翻译 Django 2 by Example

看完了标记文件, 我决定把自定义标签的部分先放一下, 因为其背后的本质是相同的, 只不过显式的编写代码以及嵌套标签提高了复杂程度等. 还是先继续向后把整体都过一遍. 今天是2019年10月的最后一天, 在去年的这个时候, 我还因为脚骨折在家里休息, 拼命的翻译 Django 2 by Example, 如今一年过去, 不仅Java来了两遍, 理解更加深刻, 还武装了计算机基础知识等很多东西. Web也是二进宫从底层学起, 感觉确实越来越好了. WEB安全是一个永恒的话题, 从我个人的体会来说就是保证什么人可以做什么样的事情.
  1. WEB安全的概念
  2. Tomcat的安全机制
  3. Tomcat的授权
  4. 简单例子
  5. 动态响应授权
  6. 认证方式
  7. 表单认证
  8. 加密传输

WEB安全的概念

先来探讨纯概念的部分. 因为Web应用不像本地应用, 还加上了一层本地计算机操作系统提供的安全防护. Web应用是开放给几乎所有人使用的. 其安全概念主要有如下几个:
  1. 认证: 即判断访问的人是谁 一般通过各种形式的认证名称+认证密码来实现, 比如用户名+密码, 账号+KEY之类.
  2. 授权: 知道了是谁之后, 看这个人是否具备相应的权限. 用一个公司来说的话, 门口刷卡好比是认证, 所有有卡的员工都可以刷卡进入, 这是认证, 但是刷完卡进来之后, 不同的员工去不同的办公室做不同的事情, 这就是授权.
  3. 机密性: 这是说, 在通过认证和授权之后, 客户端就可以和服务器发生数据往来, 要保证这个数据是私密的, 只有双方知道, 避免其他人窃取和篡改.
  4. 数据完整性: 这个就是说要有机制保证双方的通信是完整的, 不会意外中断, 或者说中断的情况下有应对机制.
Servlet规范并没有具体说安全, 有很多安全都是由不同容器实现的. 下边就要来看看Tomcat的安全相关的内容.

Tomcat的安全机制

Tomcat的安全机制是:
  1. HTTP请求到达容器, 容器解析要访问的路径
  2. 容器会查找安全配置, 看这个路径是否有对应的安全配置
  3. 如果有安全配置, 第一步需要认证, 要检查请求中是否附带了认证信息, 并把认证信息与安全配置中的信息进行比较
  4. 认证通过之后,第二步需要授权, 将认证通过的用户与映射到用户身上的角色关联起来, 在访问具体资源之前, 检查角色是否符合安全配置中的要求
  5. 认证和角色都通过之后, 请求到达Servlet, 进行处理后返回
  6. 认证和角色任何一个不通过, 都会返回错误代码, 一般是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标签. 一个约束的详细配置如下:
  1. web-resource-collection, 定义需要约束的URL和与其对应的方法, 约束Web资源
  2. web-resource-name, 其中固定是UpdateRecipes, 这个名称是给容器使用的, 不用更改
  3. url-pattern, 可以采用通配符的URL地址, 指定需要保护的URL. 该标签可以有多个, 全部生效.
  4. http-method, 指定需要保护的HTTP方法, 该标签可选, 如果不使用该标签则默认保护全部HTTP方法, 如果指定了具体的比如这里的GET, 则仅仅会保护GET方法, 不保护其他方法.
  5. auth-constraint, 如果说前边的web-resource-collection约束的是Web资源, 这个标签约束的就是用户资源. 一个安全约束也就是一个Web资源与用户资源的对应关系. 这个标签也是可选的, 如果不配置该标签, 容器允许不认证就访问对应的URL. 在这里我们没有对根目录进行认证, 所以不认证就可以访问.
  6. 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进行了配置, 遵循如下的原则确定最终效果:
  1. 如果任意一个配置了全部禁止(即有auth-constraint标签, 但是其内部没有role-name), 则全部禁止
  2. 否则按照两者的并集来计算(比如某个约束里压根没有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的时候再学习了
LICENSED UNDER CC BY-NC-SA 4.0
Comment