HTTP Digest Authentication 使用心得

news2024/11/19 23:23:14

简介

浏览器弹出这个原生的对话框,想必大家都不陌生,就是 HTTP Baisc 认证的机制。
在这里插入图片描述
这是浏览器自带的,遵循 RFC2617/7617 协议。但必须指出的是,遇到这界面,不一定是 Basic Authentication,也可能是 Digest Authentication。关于浏览器自带的认证,简单说有以下版本:

  • Basic: RFC 2617 (1999) -> RFC 7617 (2015)
  • Digest: RFC 2069 (1997) -> RFC 2617 (1999) -> RFC 7617 (2015)
  • OAuth 1.0 (Twitter, 2007)
  • OAuth 2.0 (2012)/Bearer (OAuth 2.0): RFC 6750 (2012)
  • JSON Web Tokens (JWT): RFC 7519 (2015)

可參照 MDN - HTTP authentication 了解更多。

Basic 为最简单版本,密码就用 Base64 编码一下,安全性低等于裸奔,好处是够简单;今天说的 Digest,不直接使用密码,而是密码的 MD5。虽说不是百分百安全(也不存在百分百)但安全性立马高级很多。

原生实现

试验一个新技术,我最喜欢简单直接无太多封装的原生代码,——就让我们通过经典 Servlet 的例子看看如何实现 Digest Authentication;另外最后针对我自己的框架,提供另外一个封装的版本,仅依赖 Spring 和我自己的一个库。

开门见山,先贴完整代码。

package com;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

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 org.apache.commons.codec.digest.DigestUtils;

/**
 * Servlet implementation class TestController
 */
@WebServlet("/foo")
public class TestController extends HttpServlet {
	/**
	 * 用户名,你可以改为你配置的
	 */
	private String userName = "usm";

	/**
	 * 密码,你可以改为你配置的
	 */
	private String password = "password";

	/**
	 * 
	 */
	private String authMethod = "auth";

	/**
	 * 
	 */
	private String realm = "example.com";

	public String nonce;

	private static final long serialVersionUID = 1L;

	/**
	 * 定时器,每分钟刷新 nonce
	 */
	public TestController() {
		nonce = calculateNonce();
		Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
//			log("刷新 Nonce....");
			nonce = calculateNonce();
		}, 1, 1, TimeUnit.MINUTES);
	}

	protected void authenticate(HttpServletRequest req, HttpServletResponse resp) {
		resp.setContentType("text/html;charset=UTF-8");

		String requestBody = readRequestBody(req);
		String authHeader = req.getHeader("Authorization");

		try (PrintWriter out = resp.getWriter();) {
			if (isBlank(authHeader)) {
				resp.addHeader("WWW-Authenticate", getAuthenticateHeader());
				resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
			} else {
				if (authHeader.startsWith("Digest")) {
					// parse the values of the Authentication header into a hashmap
					Map<String, String> headerValues = parseHeader(authHeader);
					String method = req.getMethod();
					String ha1 = md5Hex(userName + ":" + realm + ":" + password);
					String ha2;
					String qop = headerValues.get("qop");
					String reqURI = headerValues.get("uri");

					if (!isBlank(qop) && qop.equals("auth-int")) {
						String entityBodyMd5 = md5Hex(requestBody);
						ha2 = md5Hex(method + ":" + reqURI + ":" + entityBodyMd5);
					} else
						ha2 = md5Hex(method + ":" + reqURI);

					String serverResponse;

					if (isBlank(qop))
						serverResponse = md5Hex(ha1 + ":" + nonce + ":" + ha2);
					else {
//						String domain = headerValues.get("realm");
						String nonceCount = headerValues.get("nc");
						String clientNonce = headerValues.get("cnonce");

						serverResponse = md5Hex(ha1 + ":" + nonce + ":" + nonceCount + ":" + clientNonce + ":" + qop + ":" + ha2);
					}

					String clientResponse = headerValues.get("response");

					if (!serverResponse.equals(clientResponse)) {
						resp.addHeader("WWW-Authenticate", getAuthenticateHeader());
						resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
					}
				} else
					resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, " This Servlet only supports Digest Authorization");
			}

			out.println("<head>");
			out.println("<title>Servlet HttpDigestAuth</title>");
			out.println("</head>");
			out.println("<body>");
			out.println("<h1>已通过 HttpDigestAuth 认证 at" + req.getContextPath() + "</h1>");
			out.println("</body>");
			out.println("</html>");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private static String md5Hex(String string) {
		return DigestUtils.md5Hex(string);

//		try {
//			MessageDigest md = MessageDigest.getInstance("MD5");
//			md.update(password.getBytes());
//			byte[] digest = md.digest();
//
//			return DatatypeConverter.printHexBinary(digest).toUpperCase();
//		} catch (NoSuchAlgorithmException e) {
//			e.printStackTrace();
//		}

//		return null;
	}

	/**
	* Handles the HTTP
	* <code>GET</code> method.
	*
	* @param request servlet request
	* @param response servlet response
	* @throws ServletException if a servlet-specific error occurs
	* @throws IOException if an I/O error occurs
	*/
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		authenticate(request, response);
	}

	/**
	 * Handles the HTTP
	 * <code>POST</code> method.
	 *
	 * @param request servlet request
	 * @param response servlet response
	 * @throws ServletException if a servlet-specific error occurs
	 * @throws IOException if an I/O error occurs
	 */
	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		authenticate(request, response);
	}

	/**
	* Returns a short description of the servlet.
	*
	* @return a String containing servlet description
	*/
	@Override
	public String getServletInfo() {
		return "This Servlet Implements The HTTP Digest Auth as per RFC2617";
	}

	/**
	 * 解析 Authorization 头,将其转换为一个 Map
	 * Gets the Authorization header string minus the "AuthType" and returns a
	 * hashMap of keys and values
	 *
	 * @param header
	 * @return
	 */
	private static Map<String, String> parseHeader(String header) {
		// seperte out the part of the string which tells you which Auth scheme is it
		String headerWithoutScheme = header.substring(header.indexOf(" ") + 1).trim();
		String keyValue[] = headerWithoutScheme.split(",");
		Map<String, String> values = new HashMap<>();

		for (String keyval : keyValue) {
			if (keyval.contains("=")) {
				String key = keyval.substring(0, keyval.indexOf("="));
				String value = keyval.substring(keyval.indexOf("=") + 1);
				values.put(key.trim(), value.replaceAll("\"", "").trim());
			}
		}

		return values;
	}

	/**
	 * 生成认证的 HTTP 头
	 * 
	 * @return
	 */
	private String getAuthenticateHeader() {
		String header = "";

		header += "Digest realm=\"" + realm + "\",";
		if (!isBlank(authMethod))
			header += "qop=" + authMethod + ",";

		header += "nonce=\"" + nonce + "\",";
		header += "opaque=\"" + getOpaque(realm, nonce) + "\"";

		return header;
	}

	private boolean isBlank(String str) {
		return str == null || "".equals(str);
	}

	/**
	 * 根据时间和随机数生成 nonce
	 * 
	 * Calculate the nonce based on current time-stamp upto the second, and a random seed
	 *
	 * @return
	 */
	public String calculateNonce() {
		Date d = new Date();
		String fmtDate = new SimpleDateFormat("yyyy:MM:dd:hh:mm:ss").format(d);
		Integer randomInt = new Random(100000).nextInt();

		return md5Hex(fmtDate + randomInt.toString());
	}

	/**
	 * 域名跟 nonce 的 md5 = Opaque
	 * 
	 * @param domain
	 * @param nonce
	 * @return
	 */
	private static String getOpaque(String domain, String nonce) {
		return md5Hex(domain + nonce);
	}

	/**
	 * 返回请求体
	 * 
	 * Returns the request body as String
	 *
	 * @param request
	 * @return
	 */
	private String readRequestBody(HttpServletRequest request) {
		StringBuilder sb = new StringBuilder();

		try (InputStream inputStream = request.getInputStream();) {
			if (inputStream != null) {
				try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));) {
					char[] charBuffer = new char[128];
					int bytesRead = -1;
					while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
						sb.append(charBuffer, 0, bytesRead);
					}
				}
			} else
				sb.append("");
		} catch (IOException e) {
			e.printStackTrace();
		}

		return sb.toString();
	}
}

注意 MD5 部分依赖了这个:

<dependency>
	<groupId>commons-codec</groupId>
	<artifactId>commons-codec</artifactId>
	<version>1.14</version>
</dependency>

这是源自老外的代码,是一个标准 Servlet,但我觉得是 Filter 更合理,而且没有定义如何鉴权通过后的操作(当前只是显示一段文本),有时间的话我再改改。

封装一下

结合自己的库封装一下。

package com;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

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 org.springframework.util.DigestUtils;

import com.ajaxjs.util.SetTimeout;
import com.ajaxjs.util.io.StreamHelper;

/**
 * Servlet implementation class TestController
 */
@WebServlet("/bar")
public class TestController2 extends HttpServlet {
	/**
	 * 用户名,你可以改为你配置的
	 */
	private String userName = "usm";

	/**
	 * 密码,你可以改为你配置的
	 */
	private String password = "password";

	/**
	 * 
	 */
	private String authMethod = "auth";

	/**
	 * 
	 */
	private String realm = "example.com";

	public String nonce;

	private static final long serialVersionUID = 1L;

	/**
	 * 定时器,每分钟刷新 nonce
	 */
	public TestController2() {
		nonce = calculateNonce();

		SetTimeout.timeout(() -> {
//			log("刷新 Nonce....");
			nonce = calculateNonce();
		}, 1, 1);
	}

	protected void authenticate(HttpServletRequest req, HttpServletResponse resp) {
		resp.setContentType("text/html;charset=UTF-8");
		String authHeader = req.getHeader("Authorization");

		try (PrintWriter out = resp.getWriter();) {
			if (isBlank(authHeader)) {
				resp.addHeader("WWW-Authenticate", getAuthenticateHeader());
				resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
			} else {
				if (authHeader.startsWith("Digest")) {
					// parse the values of the Authentication header into a hashmap
					Map<String, String> headerValues = parseHeader(authHeader);
					String method = req.getMethod();
					String ha1 = md5Hex(userName + ":" + realm + ":" + password);
					String ha2;
					String qop = headerValues.get("qop");
					String reqURI = headerValues.get("uri");

					if (!isBlank(qop) && qop.equals("auth-int")) {
						String requestBody = "";
						try (InputStream in = req.getInputStream()) {
							StreamHelper.byteStream2string(in);
						}

						String entityBodyMd5 = md5Hex(requestBody);
						ha2 = md5Hex(method + ":" + reqURI + ":" + entityBodyMd5);
					} else
						ha2 = md5Hex(method + ":" + reqURI);

					String serverResponse;

					if (isBlank(qop))
						serverResponse = md5Hex(ha1 + ":" + nonce + ":" + ha2);
					else {
//						String domain = headerValues.get("realm");
						String nonceCount = headerValues.get("nc");
						String clientNonce = headerValues.get("cnonce");

						serverResponse = md5Hex(ha1 + ":" + nonce + ":" + nonceCount + ":" + clientNonce + ":" + qop + ":" + ha2);
					}

					String clientResponse = headerValues.get("response");

					if (!serverResponse.equals(clientResponse)) {
						resp.addHeader("WWW-Authenticate", getAuthenticateHeader());
						resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
					}
				} else
					resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, " This Servlet only supports Digest Authorization");
			}

			out.println("<head>");
			out.println("<title>Servlet HttpDigestAuth</title>");
			out.println("</head>");
			out.println("<body>");
			out.println("<h1>已通过 HttpDigestAuth 认证 at" + req.getContextPath() + "</h1>");
			out.println("</body>");
			out.println("</html>");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private static String md5Hex(String str) {
		return DigestUtils.md5DigestAsHex(str.getBytes());
	}


	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		authenticate(request, response);
	}

	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		authenticate(request, response);
	}


	/**
	 * 解析 Authorization 头,将其转换为一个 Map
	 * Gets the Authorization header string minus the "AuthType" and returns a
	 * hashMap of keys and values
	 *
	 * @param header
	 * @return
	 */
	private static Map<String, String> parseHeader(String header) {
		// seperte out the part of the string which tells you which Auth scheme is it
		String headerWithoutScheme = header.substring(header.indexOf(" ") + 1).trim();
		String keyValue[] = headerWithoutScheme.split(",");
		Map<String, String> values = new HashMap<>();

		for (String keyval : keyValue) {
			if (keyval.contains("=")) {
				String key = keyval.substring(0, keyval.indexOf("="));
				String value = keyval.substring(keyval.indexOf("=") + 1);
				values.put(key.trim(), value.replaceAll("\"", "").trim());
			}
		}

		return values;
	}

	/**
	 * 生成认证的 HTTP 头
	 * 
	 * @return
	 */
	private String getAuthenticateHeader() {
		String header = "";

		header += "Digest realm=\"" + realm + "\",";
		if (!isBlank(authMethod))
			header += "qop=" + authMethod + ",";

		header += "nonce=\"" + nonce + "\",";
		header += "opaque=\"" + getOpaque(realm, nonce) + "\"";

		return header;
	}

	private boolean isBlank(String str) {
		return str == null || "".equals(str);
	}

	/**
	 * 根据时间和随机数生成 nonce
	 * 
	 * Calculate the nonce based on current time-stamp upto the second, and a random seed
	 *
	 * @return
	 */
	public static String calculateNonce() {
		String now = new SimpleDateFormat("yyyy:MM:dd:hh:mm:ss").format(new Date());

		return md5Hex(now + new Random(100000).nextInt());
	}

	/**
	 * 域名跟 nonce 的 md5 = Opaque
	 * 
	 * @param domain
	 * @param nonce
	 * @return
	 */
	private static String getOpaque(String domain, String nonce) {
		return md5Hex(domain + nonce);
	}
}

参考

  • 《Web应用中基于密码的身份认证机制(表单认证、HTTP认证: Basic、Digest、Mutual)》好详细的原理分析,但没啥代码
  • 一个实现
  • Java猿社区—Http digest authentication 请求代码最全示例 代码有点复杂
  • 開發者必備知識 - HTTP認證(HTTP Authentication)科普文章,简单明了
  • https://www.pudn.com/news/628f82f3bf399b7f351e5a86.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/72096.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

墨门云终端行为趋势报表,泄密风险提前预警

事件响应滞后&#xff0c;事后再补救&#xff0c;为时晚矣&#xff0c;据IBM的数据泄露成本报告显示&#xff0c;加强风险监测可更快发现数据泄露行为&#xff0c;有效降低企业的数据泄露成本&#xff0c;可见建立完善的风险预警响应机制&#xff0c;可以避免更大的损失&#x…

5G无线技术基础自学系列 | NSA组网场景下移动性管理

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 NSA组网场景下移动性管理涉及的相关概念…

js操作二进制数据

使用ArrayBuffer对象保存二进制数据&#xff0c;使用TypedArray和DataView 视图来读写数据。 ArrayBuffer代码内存中的一段数据 const buff new ArrayBuffer(4)这样就创建了一个4(byte)字节的长度的内存判断&#xff0c;初始值都为0 注&#xff1a;一般中文占2个字节&#xff…

葡聚糖修饰Hrps共价三聚肽|葡聚糖修饰CdSe量子点

葡聚糖修饰Hrps共价三聚肽|葡聚糖修饰CdSe量子点 葡聚糖修饰Hrps共价三聚肽 中文名称&#xff1a;葡聚糖修饰Hrps共价三聚肽 纯度&#xff1a;95% 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 外观:固体或粘性液体 包装&#xff1a;瓶装/袋装 溶解性&am…

爆火的 ChatGPT 会让客服岗位消失吗?

近日&#xff0c;由 OpenAI 推出的 ChatGPT 在全球互联网爆火。具体有多火呢&#xff1f;根据 OpenAI 的 CEO Sam Altman 的说法&#xff1a;上周三才上线的 ChatGPT&#xff0c;短短几天&#xff0c;用户数已突破 100 万大关。 那么&#xff0c;ChatGPT 是什么呢&#xff1f;…

无线充电智能车的制作

本文素材来源于宁夏大学 作者&#xff1a;白二曹、王瑞、穆琴、王童兵 指导老师&#xff1a;康彩 一、项目简介 1.功能介绍 无线充电智能车由无线充电、自动控制、红外遥控、网页显示四部分组成。 &#xff08;1&#xff09;流程描述 用户端浏览器访问http://127.0.0.1页面…

Cy5.5 Tyramide,Cyanine5.5 Tyramide,花青素Cy5.5 酪酰胺化学试剂供应

一&#xff1a;产品描述 1、名称 英文&#xff1a;Cy5.5 Tyramide&#xff0c;Cyanine5.5 Tyramide 中文&#xff1a;花青素Cy5.5 酪酰胺 2、CAS编号&#xff1a;N/A 3、所属分类&#xff1a;Cyanine 4、分子量&#xff1a;738.4 5、分子式&#xff1a;C48H52CIN3O2 6、…

如何用DOS命令设置ip地址及DNS

用DOS命令设置ip地址及DNS 设置/修改IP地址&#xff0c;子网掩码&#xff0c;网关的格式&#xff1a; netsh interface ip set address "本地连接" static 10.25.35.35 255.255.255.0 10.25.35.7 auto[more] 命令的意思是将“本地连接” ip地址设置成 10.25.35.3…

SD NAND 的 SDIO在STM32上的应用详解(下篇)

七.SDIO外设结构体 其实前面关于SDIO寄存器的讲解已经比较详细了&#xff0c;这里再借助于关于SDIO结构体再进行总结一遍。 标准库函数对 SDIO 外设建立了三个初始化结构体&#xff0c;分别为 SDIO 初始化结构体SDIO_InitTypeDef、SDIO 命令初始化结构体 SDIO_CmdInitTypeDef…

Lq93:复原 IP 地址

有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 . 分隔。 例如&#xff1a;"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址&#xff0c;但是 "0.011.255.2…

Java基础:Map集合

1. Map集合概述 现实生活中&#xff0c;我们常会看到这样的一种集合&#xff1a;IP地址与主机名&#xff0c;身份证号与个人&#xff0c;系统用户名与系统用户对象等&#xff0c;这种一一对应的关系&#xff0c;就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象…

IOC 容器

IOC 概念和原理 1. 什么是 IOC&#xff1f; 控制饭庄&#xff08; Inversion of Control &#xff0c;缩写为 IOC&#xff09;&#xff0c;是面向对象编程中的一种设计原则&#xff0c;可以用来降低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入&#xff08; Depende…

基于SSM的网红书购物商城(源码+论文+开题报告+答辩PPT)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

java实现简单窗口小游戏“扫雷”

前言 忘记是从何处看到过关于扫雷小程序的文章&#xff0c;所以这次也就跟着做一下。其实很简单的&#xff0c;如果有java入门的同学也可以尝试一下自己做这种java小程序。几行代码做几遍基本上能摸清楚这些基础了&#xff0c;对于编程能力也能提高一些。&#xff08;虽然小编…

appium笔记——01环境搭建

0、关系图 1.appium客户端&#xff1a; python程序&#xff0c;链接appium服务器&#xff0c;并发送请求 2.appium服务端(模拟器客户端)&#xff1a; appium程序&#xff0c;需要提前启动&#xff0c;不仅充当appium服务端&#xff0c;还充当模拟器客户端&#xff08;接收h…

基于Apriori算法的购物网站商品推荐系统

基于Apriori算法的购物网站商品推荐系统 目 录 一、 算法内容 3 Step 1 收集用户偏好 3 Step 2 对数据进行预处理 3 Step 3 计算相似度 4 Step 4 找邻居 5 Step 5 计算推荐 6 二、 预期结果 6 三、 对比和讨论 7 Step 5 计算推荐 Section A 基于用户的协同过滤(User CF) 通过前…

Python中12个常用模块的使用教程

1. time模块 import time *一*#时间戳--》结构化时间--》格式化的字符串时间 ----------------------------------------------------------------------------- res1time.localtime(654126574) print(res1 ) #res1time.struct_time(tm_year1990, tm_mon9, tm_mday24, tm_hour…

数学大世界杂志数学大世界杂志社数学大世界编辑部2022年第7期目录

名家论坛《数学大世界》投稿&#xff1a;cn7kantougao163.com 新时期高中数学课堂教学有效性的提升策略 姜徳余; 3-5 化“零”为整&#xff0c;以“构”促学——小学数学结构化教学策略探析 孟龙平; 6-8 做反思型教师&#xff0c;加强数学衔接模块教学 陈小菊; 9-1…

[附源码]Python计算机毕业设计SSM基于远程协作的汽车故障诊断系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

SELinux

文章目录SELinux说明SELinux 的运行模式SElinux命令SELinux 是 Security-Enhanced Linux 缩写&#xff0c;安全强化的linux 系统资源都是通过程序进行访问的&#xff0c;如果将/var/www/html权限设置为777&#xff0c;代表所有程序均可以对该目录访问&#xff0c;如果已启动www…