HTTP Digest Authentication 使用心得
创始人
2024-03-26 16:43:32
0

简介

浏览器弹出这个原生的对话框,想必大家都不陌生,就是 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 hashmapMap 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);} elseha2 = 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);}} elseresp.sendError(HttpServletResponse.SC_UNAUTHORIZED, " This Servlet only supports Digest Authorization");}out.println("");out.println("Servlet HttpDigestAuth");out.println("");out.println("");out.println("

已通过 HttpDigestAuth 认证 at" + req.getContextPath() + "

");out.println("");out.println("");} 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* GET 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*/@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {authenticate(request, response);}/*** Handles the HTTP* POST 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*/@Overrideprotected 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*/@Overridepublic 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 parseHeader(String header) {// seperte out the part of the string which tells you which Auth scheme is itString headerWithoutScheme = header.substring(header.indexOf(" ") + 1).trim();String keyValue[] = headerWithoutScheme.split(",");Map 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);}}} elsesb.append("");} catch (IOException e) {e.printStackTrace();}return sb.toString();} }

注意 MD5 部分依赖了这个:

commons-codeccommons-codec1.14

这是源自老外的代码,是一个标准 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 hashmapMap 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);} elseha2 = 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);}} elseresp.sendError(HttpServletResponse.SC_UNAUTHORIZED, " This Servlet only supports Digest Authorization");}out.println("");out.println("Servlet HttpDigestAuth");out.println("");out.println("");out.println("

已通过 HttpDigestAuth 认证 at" + req.getContextPath() + "

");out.println("");out.println("");} catch (IOException e) {e.printStackTrace();}}private static String md5Hex(String str) {return DigestUtils.md5DigestAsHex(str.getBytes());}@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {authenticate(request, response);}@Overrideprotected 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 parseHeader(String header) {// seperte out the part of the string which tells you which Auth scheme is itString headerWithoutScheme = header.substring(header.indexOf(" ") + 1).trim();String keyValue[] = headerWithoutScheme.split(",");Map 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

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...