2023年4月

本文示例代码已上传至我的
Github
仓库https://github.com/CNFeffery/dash-master


大家好我是费老师,几天前我发布了由我开源维护的
dash
通用网页组件库
fac

0.2.x
全新版本,为大家介绍了其具有的诸多实用特性功能,也吸引了很多对基于
dash

Python
全栈应用开发感兴趣的朋友,为了方便更多对
dash
应用开发不甚了解的朋友快速入门,今天的文章中,我将通过简洁明了的内容带大家快速掌握
dash
应用开发的必备基础知识

本文列举了对于
算法 : 第4版 / (美) 塞奇威客 (Sedgewick, R.) , (美) 韦恩 (Wayne, K.) 著 ; 谢路云译. -- 北京 : 人民邮电出版社, 2012.10 (2021.5重印)
(以下简称原书或书)中的练习题 1.1.27 的三种解法(C++ 实现),并对包含原书题中的递归方法在内的四种解法的执行时间进行了统计和对比。

◆ 要求

原书中的练习题 1.1.27 要求对如下二项分布递归过程中的值保存在数组中,

b(n,k,p) = 1.0  ( n == 0 && k == 0 )
b(n,k,p) = 0.0  (  n < 0 ||  k < 0 )
b(n,k,p) = (1.0-p) * b(n-1,k,p) + p * b(n-1,k-1,p)

◆ 解一

依然采用递归的方式,但使用二维数组保存中间结果。

如下代码所示,

static long double binomial1(int N, int K, long double p)    // #1
{
    long double x;

    long double** b = new long double*[N+1];               // #2
    long double* data = new long double[(N+1)*(K+1)];

    ...

    x = binomial1_impl(b, N, K, p);         // #3

    ...

    return x;
}

static long double binomial1_impl(long double** b, int N, int K, long double p)
{
    if (N == 0 && K == 0) return 1.0L;
    if (N < 0 || K < 0) return 0.0L;
    if (b[N][K] == -1) {
        ...                                 // #4
        b[N][K] = (1.0L-p) * binomial1_impl(b, N-1, K, p) + p * binomial1_impl(b, N-1, K-1, p);
    }
    return b[N][K];
}

保持对外的接口不变(#1),创建一个二维数组 b[0..N][0..K] 保存中间计算结果(#2),并将其传给算法实现(#3)。算法虽然还是用递归调用(#4),但由于中间结果保存在全局的二维数组中,不用频繁地压栈和弹栈去获取中间数据。此解法网络上也见于
[github] reneargento/algorithms-sedgewick-wayne

[github] aistrate/AlgorithmsSedgewick

◆ 解二

使用二维数组保持中间结果,但同时将递归改进为递推。若以横向为 x 轴,纵向为 y 轴,左上角为坐标原点,则坐标轴上的 (x,y) 点则代表二维数组的 b[y][x] 单元。

以 N = K = 4 为例,

    0   1   2   3   4

0   *   *   *   *   *  <-- * 代表待计算的单元

1   *   *   *   *   *

2   *   *   *   *   *

3   *   *   *   *   *

4   *   *   *   *   ?  <-- 最终计算结果的单元 (4,4)

仔细考察递归关系式的特点,b(-1,*,p) = 0.0, b(*,-1,p) = 0.0。由

b(0,1,p) = (1.0-p) * b(-1,1,p) + p * b(-1,0,p)
         = (1.0-p) * 0.0 + p * 0.0
         = 0.0
b(0,2,p) = (1.0-p) * b(-1,1,p) + p * b(-1,1,p)
         = (1.0-p) * 0.0 + p * 0.0
         = 0.0
...

可推论出,二维数组中的第 0 行中的所有单元(不含b[0][0])均为 0.0;由

b(1,0,p) = (1.0-p) * b(0,0,p) + p * b(0,-1,p)
         = (1.0-p) * 1.0 + p * 0.0
         = 1.0-p
b(2,0,p) = (1.0-p) * b(1,0,p) + p * b(1,-1,p)
         = (1.0-p) * (1.0-p) + p * 0.0
         = (1.0-p)^2
...

可推论出,二维数组中的第 0 列的单元为 (1.0-p)^y。

因为每个单元 b[n][k] 结果(n 代表行号,k 代表列号),依赖于 b[n-1][k-1] 和 b[n-1][k] 的结果。为了减少计算量,递推过程可仅用到二维数组的部分单元。笔者设置一个 G 点,将待计算单元的区域划分为 '#' 和 '*' 两部分,G 点在 '#' 区域中。分为以下三种情况,

第一种情况,N < K:(如 N = 4, K = 6)

    0   1   2   3   4   5   6

0   -   -   G   #   #   #   #  <-- G 点所在单元为 0.0

1   -   -   -   *   *   *   *  <-- '-' 代表不用计算的单元

2   -   -   -   -   *   *   *

3   -   -   -   -   -   *   *

4   -   -   -   -   -   -   ?  <-- 最终结果的存储单元

G 点为 b(0,K-N)。按照递推关系式容易推导出,'#' 和 '*' 区域均为 0.0,所以最终结果即 0.0。

第二种情况,N = k:(如 N = 6, K = 6)

    0   1   2   3   4   5   6

0   G   #   #   #   #   #   #  <-- G 点所在单元为 1.0

1   -   *   *   *   *   *   *

2   -   -   *   *   *   *   *

3   -   -   -   *   *   *   *

4   -   -   -   -   *   *   *

5   -   -   -   -   -   *   *

6   -   -   -   -   -   -   ?

G 点为 b(0,0)。按照递推关系式容易推导出,数组中 n = k 的单元为 p^n。所以最终结果即 p^N。

第三种情况,N > K:(如 N = 6, K = 4)

    0   1   2   3   4

0   #   #   #   #   #

1   #   #   #   #   #

2   G   #   #   #   #  <-- G 点所在单元为 (1.0-p)^2

3   -   *   *   *   *

4   -   -   *   *   *

5   -   -   -   *   *

6   -   -   -   -   ?

G 点为 b(N-K,0)。可先计算 '#' 区域中的单元,再计算 '*' 区域中的单元,得出最终结果。处理'#'区域时,为避免大量的数组下标越界判断,可以考虑先计算 0 行和 0 列的所有单元。

如下代码所示,

static long double binomial2(int N, int K, long double p)
{
    long double x;

    if (N < K) {                       // #1

        x = 0.0L;

    } else if (N == K) {                // #2

        x = powl(p, N);

    } else {                       // #3

        ...

        b[0][0] = 1.0L;

        // process '#' area                      // #4
        // calcuate [1..N-K][0]
        for (i = 1; i <= N-K; ++i)
            b[i][0] = powl(1.0L-p, i);
        // calcuate [0][1..K]
        for (j = 1; j <= K; ++j)
            b[0][j] = 0.0L;
        // calcuate [1..N-K][1..K]
        for (i = 1; i <= N-K; ++i)
            for (j = 1; j <= K; ++j)
                b[i][j] = (1.0L-p) * b[i-1][j] + p * b[i-1][j-1];

        // process '*' area                            // #5
        for (i = N-K+1; i <= N; ++i)
            for (j = i-(N-K); j <= K; ++j)
                b[i][j] = (1.0L-p) * b[i-1][j] + p * b[i-1][j-1];

        x = b[N][K];                       // #6

        ...

    }

    return x;
}

三条分支(#1、#2、#3)分别对应前述三种情况。在第三种情况下,再先处理 '#' 区域(#4),然后采用递推求值的方式处理 '*' 区域(#5),最后得到结果(#6)。

◆ 解三

此方法是从递推解法中引申出来了。进一步探究这个此二项分布的递归式,以 N = 4 且 K = 4 为例,

   0          1                2                    3                4

0  1.0


1  1.0-p      p


2  (1.0-p)^2  2*(1.0-p)*p      p^2


3  (1.0-p)^3  3*[(1.0-p)^2]*p  3*(1-p)*(p^2)        p^3


4  (1.0-p)^4  4*[(1.0-p)^3]*p  6*[(1.0-p)^2]*(p^2)  4*(1.0-p)*(p^3)  p^4

可以发现,从第 0 行到第 N 行的非零单元即“杨辉三角形”,第 n 行中的非零单元之和构成 [(1.0-p) + p]^k 的展开式。因此,解二中的第三种情况,可结合利用通项公式 C(N,K)*[(1.0-p)^(N-K)]*(p^K) 来解决。

如下代码所示,

static long double binomial3(int N, int K, long double p)
{
    long double x;

    if ...

    } else {

        x = combination(N, K) * powl(1.0L-p, N-K) * powl(p, K);

    }

    return x;
}

◆ 测试

编译并执行程序,

$ g++ -std=c++11 -o 27.out 27.cpp
$ ./27.out 10 5 0.25

为易于显示两者之间的差异,笔者选择了硬件配置偏低的测试环境。

  • 硬件配置:Raspberry Pi 3 Model B
    • Quad Core 1.2GHz 64bit
    • 1G RAM
    • 16G MicroSD
    • 100 Base Ethernet
  • 软件配置:Raspbian Stretch
    • g++ (Raspbian 6.3.0-18+rpi1+deb9u1) 6.3.0 20170516

测试并记录了 (N, K, p) 为 (10, 5, 0.25), (20, 10, 0.25), (40, 20, 0.25), (80, 40, 0.25), (100, 50, 0.25) 的情况下,原递归、解一、解二、解三执行时所消耗的时间。

结果如下图所示,

gist

对比可以看出,不同的解法在执行时间上的差异随着计算量的增加而逐步扩大。

◆ 最后

完整示例代码和测试结果,请参考
[gitee] cnblogs/17328989

写作过程中,笔者参考了
[github] reneargento/algorithms-sedgewick-wayne

[github] aistrate/AlgorithmsSedgewick
的实现方式。致 reneargento 和 aistrate 两位。

论文信息

论文标题:Generalized Domain Adaptation with Covariate and Label Shift CO-ALignment
论文作者:Shuhan Tan, Xingchao Peng, Kate Saenko

论文来源:ICLR 2020
论文地址:download
论文代码:download
视屏讲解:click

1 摘要

提出问题:标签偏移;

解决方法:

原型分类器模拟类特征分布,并使用 Minimax Entropy 实现条件特征对齐;

使用高置信度目标样本伪标签实现标签分布修正;

2 介绍

2.1 当前工作

假设条件标签分布不变 $p(y \mid x)=q(y \mid x)$,只有特征偏移 $p(x) \neq q(x)$,忽略标签偏移 $p(y) \neq q(y)$。

假设不成立的原因:

    • 场景不同,标签跨域转移 $p(y) \neq q(y)$ 很常见;
    • 如果存在标签偏移,则当前的 UDA 工作性能显著下降;
    • 一个合适的 UDA 方法应该能同时处理协变量偏移和标签偏移;

2.2 本文工作

本文提出类不平衡域适应 (CDA),需要同时处理
条件特征转移

标签转移

具体来说,除了协变量偏移假设 $p(x) \neq   q(x)$, $p(y \mid x)=q(y \mid x)$,进一步假设 $p(x \mid y) \neq q(x \mid y)$ 和 $p(y) \neq q(y)$。

CDA 的主要挑战:

    • 标签偏移阻碍了主流领域自适应方法的有效性,这些方法只能边缘对齐特征分布;
    • 在存在标签偏移的情况下,对齐条件特征分布 $p(x \mid y)$, $q(x \mid y)$ 很困难;
    • 当一个或两个域中的数据在不同类别中分布不均时,很难训练无偏分类器;

CDA 概述:

3 问题定义

In Class-imbalanced Domain Adaptation, we are given a source domain  $\mathcal{D}_{\mathcal{S}}=   \left\{\left(x_{i}^{s}, y_{i}^{s}\right)_{i=1}^{N_{s}}\right\}$  with  $N_{s}$  labeled examples, and a target domain  $\mathcal{D}_{\mathcal{T}}=\left\{\left(x_{i}^{t}\right)_{i=1}^{N_{t}}\right\}$  with  $N_{t}$  unlabeled examples. We assume that  $p(y \mid x)=q(y \mid x)$  but  $p(x \mid y) \neq   q(x \mid y)$, $p(x) \neq q(x)$ , and  $p(y) \neq q(y)$ . We aim to construct an end-to-end deep neural network which is able to transfer the knowledge learned from  $\mathcal{D}_{\mathcal{S}}$  to  $\mathcal{D}_{\mathcal{T}}$ , and train a classifier  $y=\theta(x)$  which can minimize task risk in target domain  $\epsilon_{T}(\theta)=\operatorname{Pr}_{(x, y) \sim q}[\theta(x) \neq y]$.

4 方法

4.1 整体框架

4.2 用于特征转移的基于原型的条件对齐

目的:对齐 $p(x \mid y)$ 和 $q(x \mid y)$

步骤:首先使用原型分类器(基于相似度)估计 $p(x \mid y)$ ,然后使用一种 $\text{minimax entropy}$ 算法将其和 $q(x \mid y)$ 对齐;

4.2.1 原型分类器

原因:基于原型的分类器在少样本学习设置中表现良好,因为在标签偏移的假设下中,某些类别的设置频率可能较低;


#深层原型分类器
classPredictor_deep_latent(nn.Module):def __init__(self, in_dim = 1208, num_class = 2, temp = 0.05):
super(Predictor_deep_latent, self).
__init__()
self.in_dim
=in_dim
self.hid_dim
= 512self.num_class=num_class
self.temp
= temp #0.05
self.fc1=nn.Linear(self.in_dim, self.hid_dim)
self.fc2
= nn.Linear(self.hid_dim, num_class, bias=False)def forward(self, x, reverse=False, eta=0.1):
x
=self.fc1(x)ifreverse:
x
=GradReverse.apply(x, eta)
feat
=F.normalize(x)
logit
= self.fc2(feat) /self.tempreturn feat, logit

View Code

源域上的样本使用交叉熵做监督训练:

$\mathcal{L}_{S C}=\mathbb{E}_{(x, y) \in \mathcal{D}_{S}} \mathcal{L}_{c e}(h(x), y)  \quad \quad \quad(1)$

样本 $x$ 被分类为 $i$ 类的置信度越高,$x$ 的嵌入越接近 $w_i$。因此,在优化上式时,通过将每个样本 $x$ 的嵌入更接近其在 $W$ 中的相应权重向量来减少类内变化。所以,可以将 $w_i$ 视为 $p$ 的代表性数据点(原型) $p(x \mid y=i)$ 。

4.2.2 通过 Minimax Entropy 实现条件对齐

目标域缺少数据标签,所以使用 $\text{Eq.1}$ 获得类原型是不可行的;

解决办法:

    • 将每个源原型移动到更接近其附近的目标样本;
    • 围绕这个移动的原型聚类目标样本;

因此,提出 熵极小极大 实现上述两个目标。

具体来说,对于输入网络的每个样本 $x^{t} \in \mathcal{D}_{\mathcal{T}}$,可以通过下式计算分类器输出的平均熵

$\mathcal{L}_{H}=\mathbb{E}_{x \in \mathcal{D}_{\mathcal{T}}} H(x)=-\mathbb{E}_{x \in \mathcal{D}_{\mathcal{T}}} \sum_{i=1}^{c} h_{i}(x) \log h_{i}(x)\quad \quad \quad(2)$

通过在对抗过程中对齐源原型和目标原型来实现条件特征分布对齐:

    • 训练 $C$ 以最大化 $\mathcal{L}_{H}$ ,旨在将原型从源样本移动到邻近的目标样本;
    • 训练 $F$ 来最小化 $\mathcal{L}_{H}$,目的是使目标样本的嵌入更接近它们附近的原型;

4.3 标签转移的类平衡自训练

由于源标签分布 $p(y)$ 与目标标签分布 $q(y)$ 不同,因此不能保证在 $\mathcal{D}_{\mathcal{S}}$ 上具有低风险的分类器 $C$ 在 $\mathcal{D}_{\mathcal{T}}$ 上具有低错误。 直观地说,如果分类器是用不平衡的源数据训练的,决策边界将由训练数据中最频繁的类别主导,导致分类器偏向源标签分布。 当分类器应用于具有不同标签分布的目标域时,其准确性会降低,因为它高度偏向源域。

为解决这个问题,本文使用[19]中的方法进行自我训练来估计目标标签分布并细化决策边界。自训练为了细化决策边界,本文建议通过自训练来估计目标标签分布。 我们根据分类器 $C$ 的输出将伪标签 $y$ 分配给所有目标样本。由于还对齐条件特征分布 $p(x \mid y$ 和 $q(x \mid y)$,假设分布高置信度伪标签 $q(y)$ 可以用作目标域的真实标签分布 $q(y)$ 的近似值。 在近似的目标标签分布下用这些伪标记的目标样本训练 $C$,能够减少标签偏移的负面影响。

为了获得高置信度的伪标签,对于每个类别,本文选择属于该类别的具有最高置信度分数的目标样本的前 $k%$。利用 $h(x)$ 中的最高概率作为分类器对样本 $x$ 的置信度。 具体来说,对于每个伪标记样本 $(x, y)$,如果 $h(x)$ 位于具有相同伪标签的所有目标样本的前 $k%$ 中,将其选择掩码设置为 $m = 1$,否则 $m = 0 $。将伪标记目标集表示为 $\hat{\mathcal{D}}_{T}=\left\{\left(x_{i}^{t}, \hat{y}_{i}^{t}, m_{i}\right)_{i=1}^{N_{t}}\right\}$,利用来自 $\hat{\mathcal{D}}_{T}$ 的输入和伪标签来训练分类器 $C$,旨在细化决策 与目标标签分布的边界。 分类的总损失函数为:

$\mathcal{L}_{S T}=\mathcal{L}_{S C}+\mathbb{E}_{(x, \hat{y}, m) \in \hat{\mathcal{D}}_{T}} \mathcal{L}_{c e}(h(x), \hat{y}) \cdot m$

通常,用 $k_{0}=5$ 初始化 $k$,并设置 $k_{\text {step }}=5$,$k_{\max }=30$。

Note:本文还对源域数据使用了平衡采样的方法,使得分类器不会偏向于某一类。

4.4 训练目标

总体目标:

$\begin{array}{l}\hat{C}=\underset{C}{\arg \min } \mathcal{L}_{S T}-\alpha \mathcal{L}_{H} \\\hat{F}=\underset{F}{\arg \min } \mathcal{L}_{S T}+\alpha \mathcal{L}_{H}\end{array}$

5 总结

概念

request:获取请求数据
response:设置响应数据

Request

request继承体系

ServletRequest——Java提供的请求对象根接口
HttpServletRequest——Java提供的对http协议封装的请求对象接口
RequestFacade——tomcat实现定义类

request获取数据

package com.itheijma.web;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;

@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //String method() 获取请求方式
        String method = req.getMethod();
        System.out.println(method);
        //String getContextPath() 获取虚拟目录(项目访问路径)
        String contextPath = req.getContextPath();
        System.out.println(contextPath);
        //StringBuffer getRequestURL() 获取URL(统一资源定位符)
        StringBuffer url = req.getRequestURL();
        System.out.println(url.toString());
        //String getRequestURI() 获取URI(统一资源标识符)
        String uri = req.getRequestURI();
        System.out.println(uri);
        //String getQueryString() 获取请求参数(get方式)
        String queryString = req.getQueryString();
        System.out.println(queryString);

        //------------
        //获取请求头
        String agent = req.getHeader("user-agent");
        System.out.println(agent);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取post请求体:请求参数

        // 1. 获取字符输入流
        BufferedReader br = req.getReader();
        // 2. 读取数据
        String line = br.readLine();
        System.out.println(line);

    }
}

request通用的方式获取数据

package com.itheijma.web;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

@WebServlet("/req2")
public class RequestDemo2 extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //get请求逻辑
//        System.out.println("get...");

        // 1.获取所有参数的map集合
        Map<String, String[]> map = req.getParameterMap();
        for (String key : map.keySet()) {
            //username:zhangsan
            System.out.print(key+":");

            //获取值
            String[] values = map.get(key);
            for (String value : values) {
                System.out.print(value + " ");
            }

            System.out.println();
        }

        System.out.println("----------");

        // 2.根据key获取参数值,数组
        String[] hobbies = req.getParameterValues("hobby");
        for (String hobby : hobbies) {

            System.out.println(hobby);
        }

        // 3. 根据key获取单个参数值
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        System.out.println(username);
        System.out.println(password);


    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //post请求逻辑
        this.doGet(req,resp);


//        System.out.println("post...");
//        // 1.获取所有参数的map集合
//        Map<String, String[]> map = req.getParameterMap();
//        for (String key : map.keySet()) {
//            //username:zhangsan
//            System.out.print(key+":");
//
//            //获取值
//            String[] values = map.get(key);
//            for (String value : values) {
//                System.out.print(value + " ");
//            }
//
//            System.out.println();
//        }
//
//        System.out.println("----------");
//
//        // 2.根据key获取参数值,数组
//        String[] hobbies = req.getParameterValues("hobby");
//        for (String hobby : hobbies) {
//
//            System.out.println(hobby);
//        }
//
//        // 3. 根据key获取单个参数值
//        String username = req.getParameter("username");
//        String password = req.getParameter("password");
//        System.out.println(username);
//        System.out.println(password);


    }
}

request请求参数中文乱码解决方法

package com.itheijma.web;



import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;


/**
 * 中文乱码问题解决方案
 */
@WebServlet("/req3")
public class RequestDemo3 extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 解决乱码 POST 获取参数方式:getReader()读取数据流
        request.setCharacterEncoding("utf-8"); //设置字符输入流的编码


        // 2. 获取username
        String username = request.getParameter("username");
        System.out.println("解决乱码前:"+username);


        // 3. 解决乱码 GET 获取参数方式:getQueryString
        // 乱码原因:tomcat进行url解码,默认的字符集ISO-8859-1
//        // 3.1 先对乱码数据进行编码:转为字节数组
//        byte[] bytes = username.getBytes(StandardCharsets.ISO_8859_1);
//        // 3.2 字节数组解码
//        username = new String(bytes, StandardCharsets.UTF_8);

        username = new String(username.getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);

        System.out.println("解决乱码后"+username);


    }
}

request请求转发

请求转发(forward):一种在服务器内部的资源跳转方式。
请求转发特点:

  1. 浏览器地址栏路径不发生变化
  2. 只能转发到当前服务器的内部资源
  3. 一次请求,可以在转发的资源间使用request共享数据
    req4
package com.itheijma.web;



import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;


/**
 * 请求转发
 */
@WebServlet("/req4")
public class RequestDemo4 extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("demo4");

        //存储数据
        request.setAttribute("msg","hello");

        //请求转发
        request.getRequestDispatcher("/req5").forward(request,response);
    }
}

req5

package com.itheijma.web;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/req5")
public class RequestDemo5 extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("demo5");

        //获取数据
        Object msg = request.getAttribute("msg");
        System.out.println(msg);
    }
}

response

Response完成重定向

重定向:一种资源跳转的方式
resp1

package com.itheima.web.response;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("resp1...");

//        // 重定向
//        // 1. 设置状态码 302
//        response.setStatus(302);
//        // 2. 设置响应头 Location
//        response.setHeader("Location","/tomcat-demo2/resp2");

        // 简化方式完成重定向
        response.sendRedirect("/tomcat-demo2/resp2");

    }
}

resp2

package com.itheima.web.response;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/resp2")
public class ResponseDemo2 extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("resp2...");
    }
}

重定向特点:

  1. 浏览器地址栏路径发生变化
  2. 可以重定向到任意位置的资源(服务器内、外部均可)
  3. 两次请求,不能在多个资源使用request共享数据

资源路径问题(什么时候加虚拟目录)
浏览器使用:需要加虚拟目录
服务端使用:不需要加虚拟目录

动态获取虚拟目录

        //动态获取虚拟目录
        String contextPath = request.getContextPath();
        response.sendRedirect(contextPath + "/resp2");

Response响应字符数据

package com.itheima.web.response;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;


/**
 * 响应字符数据:设置字符数据的响应体
 */
@WebServlet("/resp3")
public class ResponseDemo3 extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        //获取字符输出流
        PrintWriter writer = response.getWriter();
        //content-type
        //response.setHeader("content-type","text/html");
        writer.write("你好");
        writer.write("<h1>aaa</h1>");

        //细节:流不需要关闭
    }
}

Response响应字节数据

package com.itheima.web.response;

import org.apache.commons.io.IOUtils;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;


/**
 * 响应字节数据:设置字节数据的响应体
 */
@WebServlet("/resp4")
public class ResponseDemo4 extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 1. 读取文件
        FileInputStream fis = new FileInputStream("e://a.jpg");

        // 2. 获取response字节输出流
        ServletOutputStream os = response.getOutputStream();

        // 3. 完成流的copy

//        byte[] buff = new byte[1024];
//        int len = 0;
//        while ((len = fis.read(buff)) != -1){
//            os.write(buff,0,len);
//        }
        IOUtils.copy(fis,os);//使用工具类

        fis.close();
    }
}

pom文件导入工具类

<dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>

引子

最近不知怎么的,自从学了WebAPI(为什么是这个,而不是MVC,还不是因为MVC的Razor语法比较难学,生态不如现有的Vue等框架,webapi很好的结合了前端生态)以后,使用别人的组件一帆风顺,但是不知其意,突然很想自己实现一个基于的JWT认证服务,来好好了解一下这个内容。

起步

自从Session-Cookie方案逐渐用的越来越少,JWT的使用也变得成为主流的安全方案之一,但是在.NET Core的文档(这里的.NET Core指代原来的.Net Core以及之后的版本,文档是微软的开发者文档)并没有对JWT做详细的介绍(可能是在微软看来太简单了,不值得细说),仅仅略带一提而已,实例代码更是少得可怜,根本没有什么建设性的帮助作用,更像文档工程师在水任务(但不得不说微软的Indentity框架是真的强大,Spring Security的功能基本都实现了)。纵然是费尽心机找资料,钻研文档,还是所获甚少。但是在不断的努力之下还是找到很多方案的,其中比较有用的就拿几个,我仔细研究实践后得到了这几篇文章,不求它有多大帮助,之希望它能帮更多人少走弯路。
然而这几个方案大概可以分成两类:

  1. 非对称加密的JWT(常用于外部网络认证)
  2. 对称加密的JWT (通常是内部系统)
    对比之下,非对称的JWT更安全,更符号系统的安全需求,虽然增加了解密时间,但利大于弊。可是关于非对称的JWT的文章却很少,大部分都是关于对称加密的JWT资料。对于这种情况,我自己也没有什么好的办法,直到我在看一篇文章时,在Nuget上无意找到的一个包改变了我的认知————
    JWT
    (名字粗暴直接)。当然,你直接使用.NET的扩展库也可以,这里面有一个
    System.IdentityModel.Tokens.Jwt
    可以同样使我们更快乐的创建JWT。关于这部分的内容,我也会在之后的时间单独写一篇文章来实验。
    另外,对于API验证测试工具,一般都是默认的Swagger,如果你喜欢更好用的工具,我推荐使用
    ApiFox
    或者
    EOLink

实施

首先创建一个WebAPI项目,至于是否在启动后使用HTTPS,根据自己的需要,一般都是需要的。然后用Nuget或者Dotnet安装
JWT
这个Nuget包即可开始,如果是ASP.NET Core这样需要依赖注入环境的,推荐
JWT.Extensions.AspNetCore
这个包(
强力推荐
),可以更好的让你开始,仅仅需要基本功能的只用JWT即可。
由于我这里使用的是RSA1024bit,所以需要一个HTTPS的PEM或者CRT证书做CA,各位可以自己生成一个。
首先,我们需要为服务注入这个包的依赖,即使用
builder.Services.AddAuthentication().AddJwt()
来添加相关依赖。那为什么是要使用这个方法呢?如果你通过
对象浏览器
查看API会发现一个
AddJwtDecoder
的方法,同样可以添加依赖,并且更灵活,如果反编译就发现——
AddJwt
方法是对
AddJwtDecoder
的某个重载的调用,后面可以调用其他方法达成同样的效果,所以推荐使用这个方法注入。

服务注入代码如下:
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtAuthenticationDefaults.AuthenticationScheme;
})
.AddJwt();

然后在应用认证中间件即可。

app.UseAuthentication();

完成这些工作以后,还需要创建一个用来根据用户信息生成JWT的控制器,为了防止使用HTTPGet被攻击,我这里采用了HTTPPost。
根据这个包的文档,生成一个JWT字符串非常容易,只需要创建一个x509对象或者两个RSA对象作为公钥和私钥即可,我推荐使用这个包里面提供的FluentApi方式,写起来非常舒服,最后编码生成JWT,完成。

生成JWT的代码如下:
var token = JwtBuilder.Create()
            .WithAlgorithm(algorithm) // 加密算法
            .AddClaim<string>("Account", accountName) //添加用户信息
            .AddClaim<string>("Passwd", passwdContext) //添加用户密码
            .Encode(); //编码生成jwt

完整的控制器代码如下:
[Route("api/[controller]")]
[ApiController]
public class JwtController : ControllerBase
{
    private RSA publicKey = RSA.Create();
    private RSA privateKey = RSA.Create();
    private RS2048Algorithm? algorithm { get; set; }

    public JwtController()
    {
        algorithm = new RS2048Algorithm(publicKey, privateKey);
    }



    [HttpPost]
    public async Task<string> CreateJwt(string accountName, string passwdContext)
    {
        return await Task<string>.Run<string>(() =>
        {
            var token =
            JwtBuilder.Create()
            .WithAlgorithm(algorithm)
            .AddClaim<string>("Account", accountName)
            .AddClaim<string>("Passwd", passwdContext)
            .Encode();

            return token;
        });
    }
}

总结

JWT.Extensions.AspNetCore
这个包是一个集成了常用jwt操作的包,可以让你不必关心JWT的创建过程,这大大化简了我们使用JWT的过程,在一定程度上提高了生产力。如果您喜欢这个库,可以到项目主页上添加一颗星。

注意:

经过本人的亲身经历,x509在.NET6之后的类库
X509Certificate2
不能直接生成私钥,需要使用该类的成员方法:
public System.Security.Cryptography.X509Certificates.X509Certificate2 CopyWithPrivateKey (System.Security.Cryptography.ECDiffieHellman privateKey);
创建一个带有私钥的副本,否则会出现私钥在对象构造成功后出现
NULL
的情况。
如果没有特殊必要,建议直接使用
Rsa
的成员方法直接生成一个Rsa对象来操作比较简便,目前这个办法还可以改进,欢迎各位留言。