简单了解HTTP

 

HTTP是英文HyperText Transfer Protocol首字母缩写,是目前WWW万维网通讯标准协议,在属于OSI第七层(应用层)协议,要实现HTTP协议通常需要基于TCP-Socket套接字作为传输层的支撑。

 

什么是http-basic

 

http-basic是http协议的认证模块,http-basic也是http协议下的协议。下面先来看一下http-basic协议流程图:

 

从零开始手写一个http-basic认证服务器_http认证服务器

流程图说明:

 

第一步:当客户端通过浏览器访问实现了http-basic的后端服务器时,后端服务器首先会校验是否携带Authorization请求头,如果没有则返回Code=401:

  •  
http.setCode(401);http.setStatus("Unauthorized");// 其中Basic表示认证类型为Basic,Realm用来描述后端应用名称,例如OA,CRM,可自定义// 除了Basic认证类型外,后面还会将另一种类型:Digesthttp.setHeader("WWW-Authenticate", "Basic Reaml=\"HTTP Demo Server\"");

 

下面是谷歌浏览器默认抓包截图:

 

从零开始手写一个http-basic认证服务器_http-basic协议_02

 

此时用户就会收到浏览器弹出的登陆输入框:

 

从零开始手写一个http-basic认证服务器_http认证服务器_03

第二步:当用户输入用户名和密码,点击登陆按钮时,浏览器会将表单的用户名和密码按规则:BASE64(用户名:密码)编码后放到请求头为HTTP-Request-Header=Authorization: Basic cm9vdDoxMjM0NTY= 方式提交给服务器进行认证,具体截图如下:

 

从零开始手写一个http-basic认证服务器_http-basic协议_04

第三步:后端服务器接收到请求后,仍然会校验是否携带Authorization请求头,如果携带,则通过BASE64解码出来得到用户名:密码字符串,然后通过":"进行分隔得到用户名和密码进行验证,如果验证失败,继续第一步的内容,如果验证成功,后端服务可能会进一步验证URL资源权限等,如果验证资源权限不通过,此时参考第一步的方式返回(注意此时返回Code=403):

  •  
http.setCode(401);http.setStatus("Forbidden");http.setHeader("WWW-Authenticate", "Basic Reaml=\"HTTP Demo Server\"");

 

如果验证通过,则返回Code=200即可:

  •  
http.setCode(200);http.setStatus("OK");

 

http-basic缺陷

 

http-basic(当响应头WWW-Authenticate:Basic Reaml=xxx)存在以下缺陷:

 

1、用户名和密码明文(仅仅只是Base64编码而已)传输,需要配合HTTPS来保证信息传输的安全。

2、就算HTTPS将密码加密传输,也仍然存在重放攻击风险。

3、代理和中间节点的防护措施弱,很容易通过伪装服务器来骗过认证,诱导用户输入用户名和密码。

 

下面将介绍http-basic升级版更为安全的摘要认证http-digest。

 

了解http-digest流程

 

 

http-digest流程比http-basic流程复杂很多,流程图如下:

 

从零开始手写一个http-basic认证服务器_http认证服务器_05

下面对http-digest涉及的字段作用进行说明:

 

  • WWW-Authentication:用来定义使用何种方式(Basic、Digest、Bearer等)去进行认证以获取受保护的资源。

  • realm:表示Web服务器中受保护文档的安全域(比如OA、CRM系统域等),用来指示需要哪个域的认证。

  • qop:保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,可以为空,但不推荐为空值。

  • nonce:服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击。

  • nc:nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求。

  • cnonce:客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护。

  • response:这是由用户代理软件计算出的一个字符串,以证明用户知道口令。

  • Authorization-Info:用于返回一些与授权会话相关的附加信息。

  • nextnonce:下一个服务端随机数,使客户端可以预先发送正确的摘要

  • rspauth:响应摘要,用于客户端对服务端进行认证。

  • stale:当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的401响应,并指定stale=true,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了。

 

http-digest流程说明

 

 

第一步:当客户端通过浏览器访问实现了http-digest的后端服务器时,后端服务器首先会校验是否携带Authorization请求头,如果没有则返回Code=401:

  •  
http.setCode(401);http.setStatus("Unauthorized");// 其中Basic表示认证类型为Basic,Realm用来描述后端应用名称,例如OA,CRM,可自定义// 除了Basic认证类型外,后面还会将另一种类型:Digesthttp.setHeader("WWW-Authenticate", "Basic Reaml=\"HTTP Demo Server\"");

并且返回(注意和http-basic的区别):

  •  
WWW-Authenticate: Digest nonce=MTYyMTA4OTE3MDgyODo6ODQzMjU0NjMwNTE0MTYzNzEy,realm=LazyAgentConsole,qop=auth,algorithm=MD5

 

下面是谷歌浏览器默认抓包截图:

 

从零开始手写一个http-basic认证服务器_http-basic协议_06

此时用户就会收到浏览器弹出的登陆输入框:

 

从零开始手写一个http-basic认证服务器_http认证服务器_03

第二步:当用户输入用户名和密码,点击登陆按钮时,浏览器会将表单的用户名和密码进行加密后放到请求头,浏览器抓包截图如下:

 

从零开始手写一个http-basic认证服务器_http认证服务器_08

  •  
Authorization: Digest username="root", realm="LazyAgentConsole", nonce="MTYyMTA4OTMyNjUyMzo6ODQzMjU1MjgzNTQ2MzI0OTky", uri="/", algorithm=MD5, response="7497e20c827e0de40ac28379ab52afe6", qop=auth, nc=00000002, cnonce="5ec74e43305c2a56"

 

第三步:后端服务器接收到请求后,仍然会校验是否携带Authorization请求头,如果携带,则通过算法解密(后面介绍)出来得到用户名和密码信息进行验证,如果验证失败,继续第一步的内容,如果验证成功,后端服务可能会进一步验证URL资源权限等,如果验证资源权限不通过,此时参考第一步的方式返回(注意此时返回Code=403):

  •  
http.setCode(401);http.setStatus("Forbidden");http.setHeader("WWW-Authenticate", "Basic Reaml=\"HTTP Demo Server\"");

 

如果验证通过,则返回Code=200即可:

 

  •  
http.setCode(200);http.setStatus("OK");

 

http-digest解密算法

 

传给后台请求头Authorization中response值就是客户端通过计算得到的密码摘要,后端解密response时根据不同的保护质量qos解密方式也有所不同,当qos策略为auth,计算方式如下:

  •  
// 使用默认的MD5加密算法MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))

 

算法

A1

MD5(默认)

<username>:<realm>:<password>

MD5-sess

MD5(<username>:<realm>:<password>):<nonce>:<cnonce>

qop

A2

auth(默认)

<request-method>:<uri>

auth-int

<request-method>:<uri>:MD5(<request-entity-body>)

 

到这里,理论性的内容就讲解完毕,接下来进入http-digest协议代码实战环节。

 

项目结构

 

从零开始手写一个http-basic认证服务器_http认证服务器_09

 

pom.xml源码:

  •  
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>
<groupId>groupId</groupId> <artifactId>lazy-httpbasic</artifactId> <version>1.0-SNAPSHOT</version>
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
<build> <finalName>lazy-httpbasic</finalName> <plugins> <plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <includes> <include>**/*.*</include> </includes> <archive> <manifest> <!--配置jar包内创建MANIFEST。MF文件 --> <addClasspath>true</addClasspath> <classpathPrefix>classes/</classpathPrefix> <!--指定jar包启动类 --> <mainClass>com.lazy.httpbasic.Boostrap</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build></project>

 

Config.java源码:

  •  
package com.lazy.httpbasic.conf;
public class Config {
/** * 控制台会话超时时间,单位毫秒, 默认10分钟 */ public static final long CONSOLE_HTTP_SESSION_TIME = 600000; /** * 心跳控制台监听默认端口 */ public static final int HEARTBEAT_CONSOLE_PORT = 9550; /** * 控制台HTTP BASIC 用户名 */ public static final String CONSOLE_HTTP_BASIC_USERNAME = "root"; /** * 控制台HTTP BASIC 密码 */ public static final String CONSOLE_HTTP_BASIC_PASSWORD = "123456"; /** * realm */ public static final String REALM = "HttpBasicServer";
}

 

HttpRequest.java类源码:

  •  
package com.lazy.httpbasic.bean;
import java.util.HashMap;import java.util.Map;
public class HttpRequest { /** * 请求方法 GET/POST/PUT/DELETE/OPTION... */ private String method; /** * 请求的uri */ private String uri; /** * http版本 */ private String version; /** * 请求头 */ private Map<String, String> headers = new HashMap<>(); /** * 请求参数 */ private Map<String, Object> parameters = new HashMap<>(); /** * 请求参数相关 */ private String message;
public String getHeader(String name) { return this.headers.get(name); }
public void setHeader(String name, String val) { this.headers.put(name, val);    }
public void setParameters(Map<String, Object> parameters) { this.parameters = parameters; }
// 省略getter setter...}

 

HttpResponse.java类源码:

  •  
package com.lazy.httpbasic.bean;
import java.util.Map;
public class HttpResponse {
private String version; private int code; private String status; private Map<String, String> headers;    private String message; // 省略getter setter... }

 

HttpRequestParser.java源码:

  •  
package com.lazy.httpbasic.bean;
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.nio.charset.StandardCharsets;import java.util.HashMap;import java.util.Map;
public class HttpRequestParser {

/** * 根据标准的http协议,解析请求行 * 请求行,包含三个基本要素:请求方法 + URI + http版本,用空格进行分割,所以解析代码如下 * * @param reader * @param request */ private static void decodeRequestLine(BufferedReader reader, HttpRequest request) throws IOException { String[] strs = reader.readLine().split(" "); assert strs.length == 3; request.setMethod(strs[0]); request.setUri(strs[1]); request.setVersion(strs[2]);
//解析参数 String[] params = null; String[] uriAndParam = strs[1].split("\\?"); if (uriAndParam.length == 2) { String param = uriAndParam[1]; params = param.split("&"); for (int i = 0; i < params.length; i++) { String[] p = params[i].split("="); if (p.length == 2) { request.getParameters().put(p[0], p[1]); } } request.setUri(uriAndParam[0]); } }
/** * 根据标准http协议,解析请求头 * 请求头的解析,从第二行,到第一个空白行之间的所有数据,都是请求头;请求头的格式也比较清晰, 形如 key:value * * @param reader * @param request * @throws IOException */ private static void decodeRequestHeader(BufferedReader reader, HttpRequest request) throws IOException { Map<String, String> headers = new HashMap<>(16); String line = reader.readLine(); String[] kv; while (!"".equals(line)) { kv = line.split(":"); assert kv.length == 2; headers.put(kv[0].trim(), kv[1].replaceAll("\"", "").trim()); line = reader.readLine(); } request.setHeaders(headers); }
/** * 根据标注http协议,解析正文 * * @param reader * @param request * @throws IOException */ private static void decodeRequestMessage(BufferedReader reader, HttpRequest request) throws IOException { int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length", "0")); if (contentLen == 0) { // 表示没有message,直接返回 // 如get/options请求就没有message return; } char[] message = new char[contentLen]; reader.read(message); request.setMessage(new String(message)); }
/** * http的请求可以分为三部分 * <p> * 第一行为请求行: 即 方法 + URI + 版本 * 第二部分到一个空行为止,表示请求头 * 空行 * 第三部分为接下来所有的,表示发送的内容,message-body;其长度由请求头中的 Content-Length 决定 * <p> * 几个实例如下 * * @param reqStream * @return */ public static HttpRequest parse2request(InputStream reqStream) throws IOException { BufferedReader httpReader = new BufferedReader(new InputStreamReader(reqStream, StandardCharsets.UTF_8)); HttpRequest httpRequest = new HttpRequest(); decodeRequestLine(httpReader, httpRequest); decodeRequestHeader(httpReader, httpRequest); decodeRequestMessage(httpReader, httpRequest); return httpRequest; }
}

 

HttpResponseParser.java源码:

  •  
package com.lazy.httpbasic.bean;
import java.util.HashMap;import java.util.Map;
public class HttpResponseParser {
public static String buildResponse(HttpRequest request, String response) { HttpResponse httpResponse = ofResponse(request, response); StringBuilder builder = new StringBuilder(); buildResponseLine(httpResponse, builder); buildResponseHeaders(httpResponse, builder); buildResponseMessage(httpResponse, builder); return builder.toString(); }
public static String ofResponseStr(HttpResponse httpResponse) { StringBuilder builder = new StringBuilder(); buildResponseLine(httpResponse, builder); buildResponseHeaders(httpResponse, builder); buildResponseMessage(httpResponse, builder); return builder.toString(); }
public static HttpResponse ofResponse(HttpRequest request, String response) { if (response == null) { response = ""; } HttpResponse httpResponse = new HttpResponse(); httpResponse.setCode(200); httpResponse.setStatus("ok"); httpResponse.setVersion(request.getVersion()); Map<String, String> headers = new HashMap<>(); headers.put("Content-Type", "text/html"); headers.put("Cache-Control", "no-store"); headers.put("Content-Length", String.valueOf(response.getBytes().length)); httpResponse.setHeaders(headers); httpResponse.setMessage(response); return httpResponse; }
private static void buildResponseLine(HttpResponse response, StringBuilder stringBuilder) { stringBuilder.append(response.getVersion()).append(" ").append(response.getCode()).append(" ") .append(response.getStatus()).append("\n"); }
private static void buildResponseHeaders(HttpResponse response, StringBuilder stringBuilder) { for (Map.Entry<String, String> entry : response.getHeaders().entrySet()) { stringBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n"); } stringBuilder.append("\n"); }
private static void buildResponseMessage(HttpResponse response, StringBuilder stringBuilder) { stringBuilder.append(response.getMessage()); }
}

 

HttpDigest.java源码:

  •  
package com.lazy.httpbasic.bean;

import com.lazy.httpbasic.conf.Config;import com.lazy.httpbasic.util.MD5Util;import com.lazy.httpbasic.util.StringUtil;
import java.util.Locale;
public class HttpDigest {
private String username; private String nonce; private String realm; private String qop; private String nc; private String cnonce; private String response; private String uri; private String stale; private String rspauth; private String algorithm; private String method; private String nextnonce;
public String getNextnonce() { return nextnonce; }
public void setNextnonce(String nextnonce) { this.nextnonce = nextnonce; }
public String md5() { String a1 = this.getUsername() + ":" + this.getRealm() + ":" + Config.CONSOLE_HTTP_BASIC_PASSWORD; String ha1 = MD5Util.encodeByMD5(a1);
String a2 = this.getMethod() + ":" + this.getUri(); String ha2 = MD5Util.encodeByMD5(a2); //服务器计算出的摘要 String responseBefore = ha1 + ":" + this.getNonce() + ":" + this.getNc() + ":" + this.getCnonce() + ":" + this.getQop() + ":" + ha2; String responseMD5 = MD5Util.encodeByMD5(responseBefore); return responseMD5; }
public void parse(String str) { String[] pair = str.split(","); for (String s : pair) { String[] kv = s.split("="); if (kv.length != 2) { continue; } String k = kv[0].toLowerCase(Locale.ENGLISH).trim(); String v = kv[1]; if ("username".equals(k)) { this.username = v; } if ("nonce".equals(k)) { this.nonce = v; } if ("realm".equals(k)) { this.realm = v; } if ("qop".equals(k)) { this.qop = v; } if ("nc".equals(k)) { this.nc = v; } if ("cnonce".equals(k)) { this.cnonce = v; } if ("response".equals(k)) { this.response = v; } if ("uri".equals(k)) { this.uri = v; } if ("stale".equals(k)) { this.stale = v; } if ("rspauth".equals(k)) { this.rspauth = v; } if ("algorithm".equals(k)) { this.algorithm = v; } if ("nextnonce".equals(k)) { this.nextnonce = v; } } }
public String ofString() { StringBuilder s = new StringBuilder("Digest "); if (StringUtil.isNotBlank(username)) { s.append("username=").append(username).append(","); } if (StringUtil.isNotBlank(nonce)) { s.append("nonce=").append(nonce).append(","); } if (StringUtil.isNotBlank(realm)) { s.append("realm=").append(realm).append(","); } if (StringUtil.isNotBlank(qop)) { s.append("qop=").append(qop).append(","); } if (StringUtil.isNotBlank(nc)) { s.append("nc=").append(nc).append(","); } if (StringUtil.isNotBlank(cnonce)) { s.append("cnonce=").append(cnonce).append(","); } if (StringUtil.isNotBlank(response)) { s.append("response=").append(response).append(","); } if (StringUtil.isNotBlank(uri)) { s.append("uri=").append(uri).append(","); } if (StringUtil.isNotBlank(stale)) { s.append("stale=").append(stale).append(","); } if (StringUtil.isNotBlank(rspauth)) { s.append("rspauth=").append(rspauth).append(","); } if (StringUtil.isNotBlank(algorithm)) { s.append("algorithm=").append(algorithm).append(","); } if (StringUtil.isNotBlank(nextnonce)) { s.append("nextnonce=").append(nextnonce).append(","); } s.deleteCharAt(s.length() - 1); return s.toString();    }}

 

HttpBasicUtil.java源码:

  •  
package com.lazy.httpbasic.util;

import com.lazy.httpbasic.bean.HttpDigest;import com.lazy.httpbasic.bean.HttpRequest;import com.lazy.httpbasic.conf.Config;import sun.misc.BASE64Decoder;import sun.misc.BASE64Encoder;
import java.io.UnsupportedEncodingException;

public class HttpBasicUtil {
/** * Authorization: Digest username=“xxxxx”,realm=“myTomcat”,qop=“auth”,nonce=“xxxxx”,uri=“xxxx”,cnonce=“xxxxxx”,nc=00000001,response=“xxxxxxxxx”,opaque=“xxxxxxxxx” 。其中username是用户名;cnonce是客户端生成的随机字符串;nc是运行认证的次数;response就是最终计算得到的摘要。 * * @param request * @return */ public static HttpDigest ofDigestRequest(HttpRequest request) { String authorization = request.getHeader("Authorization"); if ((authorization != null) && (authorization.length() > 7)) { authorization = authorization.substring(7); HttpDigest digest = new HttpDigest(); digest.parse(authorization); digest.setMethod(request.getMethod()); return digest; } return null; }
public static String base64DecodeNonce() { try { return base64Encode((System.currentTimeMillis() + "::" + SnowflakeIdUtil.getInstance().nextId()).getBytes("utf-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return null; }

public static String base64DecodeNonce(String nonceBase64) { return base64Decode(nonceBase64); }
public static HttpDigest ofDigestResponse(HttpRequest request) { HttpDigest digestResponse = new HttpDigest(); digestResponse.setNonce(base64DecodeNonce()); digestResponse.setQop("auth"); digestResponse.setAlgorithm("MD5"); digestResponse.setRealm(Config.REALM); return digestResponse; }
/** * 编码 * * @param bstr * @return String */ @SuppressWarnings("restriction") public static String base64Encode(byte[] bstr) { String strEncode = new BASE64Encoder().encode(bstr); return strEncode; }
/** * 解码 * * @param str * @return */ public static String base64Decode(String str) { if (StringUtil.isBlank(str)) { return null; } String s = null; try { BASE64Decoder decoder = new BASE64Decoder(); byte[] b = decoder.decodeBuffer(str); s = new String(b, "utf-8"); } catch (Exception ignored) {
} return s; }
}

 

Boostrap.java源码:

  •  
package com.lazy.httpbasic;
import com.lazy.httpbasic.conf.Config;import com.lazy.httpbasic.task.WorkTask;
import java.io.IOException;import java.net.ServerSocket;import java.net.Socket;import java.util.concurrent.*;
public class Boostrap {
private static final int nThreads = Runtime.getRuntime().availableProcessors();
/** * 主线程池 */ private static final ExecutorService bootstrapExecutor = Executors.newFixedThreadPool(2); /** * 任务线程池 */ private static final ExecutorService taskExecutor = new ThreadPoolExecutor( nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100), new ThreadPoolExecutor.DiscardPolicy());
/** * 启动方法 * * @param args */ public static void main(String[] args) {
//启动socket线程 bootstrapExecutor.submit(new Startup()); }

/** * 启动线程 */    static class Startup implements Runnable {
@Override public void run() {
ServerSocket serverSocket = null; Socket socket = null; try { serverSocket = new ServerSocket(Config.HEARTBEAT_CONSOLE_PORT); System.out.println("The Http-Basic Server is start in port:" + Config.HEARTBEAT_CONSOLE_PORT); while (true) { socket = serverSocket.accept(); //接受客户端请求后提交给任务线程池去执行 taskExecutor.submit(new WorkTask(socket)); } } catch (Exception ignored) { if (socket != null) { try { socket.close(); } catch (IOException e) { //ignored } } if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { //ignored } } } } }}

 

启动方式

 

1、直接IDE运行Boostrap的main方法。

2、maven打包后通过java -jar lazy-httpbasic.jar 启动。

 

启动后测试

 

启动后控制台输入如下:

 

从零开始手写一个http-basic认证服务器_http-basic协议_10

 

浏览器访问: http://localhost:9550端口任何地址即可,截图如下(输入错误用户名或密码时仍然继续弹出登陆框):

 

从零开始手写一个http-basic认证服务器_http认证服务器_11

 

输入正确用户名和密码时,登陆成功,截图如下:

 

 

从零开始手写一个http-basic认证服务器_http认证服务器_12

 

从零开始手写一个http-basic认证服务器_http认证服务器_13