简介

浏览器弹出这个原生的用户登录对话框,想必大家都不陌生,就是 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 为最简单版本(我 13 年有博文《Java Web 实现 HTTP 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<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);} 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("<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*/@Overrideprotected 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*/@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<String, String> 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<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);}}} elsesb.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 hashmapMap<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);} 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("<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());}@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<String, String> 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<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);}
}

过滤器版本

修改为 Filter 版本,并进一步重构代码,变成只有 180 行不到的逻辑。源码在这里。

package com.ajaxjs.web.http_auth;import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.springframework.util.StringUtils;import com.ajaxjs.util.StrUtil;
import com.ajaxjs.util.io.StreamHelper;public class DigestAuthentication implements Filter {/*** 用户名,你可以改为你配置的*/private String userName = "usm";/*** 密码,你可以改为你配置的*/private String password = "password";/*** */private String authMethod = "auth";/*** */private String realm = "example.com";public String nonce;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {System.out.println("HTTP DigestAuthentication……");// 定时器,每分钟刷新 noncenonce = calculateNonce();//        SetTimeout.timeout(() -> {//          System.out.println("刷新 Nonce....");
            log("刷新 Nonce....");
//          nonce = calculateNonce();
//      }, 1, 1);}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException {authenticate((HttpServletRequest) request, (HttpServletResponse) response, chain);}@Overridepublic void destroy() {}protected void authenticate(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws ServletException {resp.setContentType("text/html;charset=UTF-8");String authHeader = req.getHeader("Authorization");try {if (StringUtils.hasText(authHeader)) {if (authHeader.startsWith("Digest")) {// parse the values of the Authentication header into a hashmapMap<String, String> headerValues = parseHeader(authHeader);String method = req.getMethod();String ha1 = StrUtil.md5(userName + ":" + realm + ":" + password);String ha2;String qop = headerValues.get("qop");String reqURI = headerValues.get("uri");if (StringUtils.hasText(qop) && qop.equals("auth-int")) {String requestBody = "";try (InputStream in = req.getInputStream()) {StreamHelper.byteStream2string(in);}String entityBodyMd5 = StrUtil.md5(requestBody);ha2 = StrUtil.md5(method + ":" + reqURI + ":" + entityBodyMd5);} elseha2 = StrUtil.md5(method + ":" + reqURI);String serverResponse;if (StringUtils.hasText(qop)) {//                      String domain = headerValues.get("realm");String nonceCount = headerValues.get("nc");String clientNonce = headerValues.get("cnonce");serverResponse = StrUtil.md5(ha1 + ":" + nonce + ":" + nonceCount + ":" + clientNonce + ":" + qop + ":" + ha2);} elseserverResponse = StrUtil.md5(ha1 + ":" + nonce + ":" + ha2);String clientResponse = headerValues.get("response");if (!serverResponse.equals(clientResponse)) {show401(resp);return;}} else {resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, " This Servlet only supports Digest Authorization");return;}} else {show401(resp);return;}// allows to gochain.doFilter(req, resp);} catch (IOException e) {e.printStackTrace();}}private void show401(HttpServletResponse resp) throws IOException {resp.addHeader("WWW-Authenticate", getAuthenticateHeader());resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);}/*** 解析 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 itString 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 (StringUtils.hasText(authMethod))header += "qop=" + authMethod + ",";header += "nonce=\"" + nonce + "\",";header += "opaque=\"" + StrUtil.md5(realm + nonce) + "\""; // 域名跟 nonce 的 md5 = Opaquereturn header;}/*** 根据时间和随机数生成 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 StrUtil.md5(now + new Random(100000).nextInt());}
}

使用方法,传统的 web.xml

<filter><filter-name>HttpDigestAuthentication</filter-name><filter-class>com.ajaxjs.web.http_auth.DigestAuthentication</filter-class>
</filter><filter-mapping><filter-name>HttpDigestAuthentication</filter-name><url-pattern>/foo2/*</url-pattern>
</filter-mapping>

或者 Servlet 3.0 k基于注解的

import javax.servlet.annotation.WebFilter;import com.ajaxjs.web.http_auth.DigestAuthentication;/*** Servlet Filter implementation class TestFilter*/
@WebFilter("/foo2/*")
public class TestFilter extends DigestAuthentication {}

又或者基于 Spring Boot 的

@Bean
public FilterRegistrationBean<DigestAuthentication> loggingFilter(){FilterRegistrationBean<DigestAuthentication> registrationBean  = new FilterRegistrationBean<>();registrationBean.setFilter(new RequestResponseLoggingFilter());registrationBean.addUrlPatterns("/users/*");registrationBean.setOrder(2);return registrationBean;
}

如果是 Spring MVC 又不想用 web.xml 呢?——抱歉,只能通过 @Componet 注入成为全局的 url,而无法针对某个 url 控制。

参考

  • 《Web应用中基于密码的身份认证机制(表单认证、HTTP认证: Basic、Digest、Mutual)》好详细的原理分析,但没啥代码
  • 一个实现
  • Java猿社区—Http digest authentication 请求代码最全示例 代码有点复杂
  • 開發者必備知識 - HTTP認證(HTTP Authentication)科普文章,简单明了
  • 《HTTP digest RFC2671规范 加密实现(JAVA)》
  • Java httpclient进行digest鉴权遇到的问题

HTTP Digest Authentication 使用心得相关推荐

  1. [转]asp.net权限认证:摘要认证(digest authentication)

    本文转自:http://www.cnblogs.com/lanxiaoke/p/6357501.html 摘要认证简单介绍 摘要认证是对基本认证的改进,即是用摘要代替账户密码,从而防止明文传输中账户密 ...

  2. Java猿社区—Http digest authentication 请求代码最全示例

    文章目录 什么是摘要认证 服务器核实用户身份 客户端反馈用户身份 server 确认用户 代码示例 欢迎关注作者博客 简书传送门 什么是摘要认证 摘要认证( Digest authentication ...

  3. libcurl第五课 Digest Authentication摘要验证使用

    场景          在安迅士摄像机网页上,配置系统选项,HTTP/RTSP Password Settings 中, 选择Encrypted only.获取设备的云台状态信息,使用的是摘要认证 例 ...

  4. Digest Authentication 摘要认证(转载)

    原文:Digest Authentication 摘要认证_weixin_34007906的博客-CSDN博客 摘要"式认证( Digest authentication)是一个简单的认证机 ...

  5. asp.net权限认证:摘要认证(digest authentication)

    摘要认证简单介绍 摘要认证是对基本认证的改进,即是用摘要代替账户密码,从而防止明文传输中账户密码的泄露 之前对摘要认证也不是很熟悉,还得感谢圆中的 parry 贡献的博文:ASP.NET Web AP ...

  6. Digest authentication

    "摘要"式认证( Digestauthentication)是一个简单的认证机制,最初是为HTTP协议开发的,因而也常叫做HTTP摘要,在RFC2671中描述.其身份验证机制很简单 ...

  7. Weaning the Web off of Session Cookies Making Digest Authentication Viable

    http://www.vsecurity.com/download/papers/WeaningTheWebOffOfSessionCookies.pdf

  8. HttpClient basic authentication

    2019独角兽企业重金招聘Python工程师标准>>> 1. Overview This tutorial will illustrate how to configure Basi ...

  9. 配置Apache Basic和Digest认证

    转载:http://blog.jobbole.com/41519/ 在伯乐在线看到一篇<在Nginx下对网站进行密码保护>文章, 正好和自己这两天研究的问题有些相同点.我侧重研究的是如何破 ...

最新文章

  1. python用def编写calsum函数_Python函数
  2. 为什么说现在是计算机视觉最好的时代?
  3. 【已解决】tomcat启动不成功(点击startup.bat闪退)的解决办法
  4. Java基础篇:对象拷贝:clone方法 以及 序列化
  5. 使用ASP.NET 2.0进行记录错误
  6. c#打开数据库连接池的工作机制_它是谁?一个比 c3p0 快 200 倍的数据库连接池!...
  7. Bug:Google Analytics例子未使用example.com
  8. 【2019杭电多校第二场1005 = HDU6595】Everything Is Generated In Equal Probability(期望-递推)
  9. 解决“无法删除文件:无法读源文件或磁盘”的方法(chkdsk)
  10. emacs-打开和关闭
  11. 寒假ACM假期总结 (7)
  12. 达梦数据库忘记SYSDBA密码的问题探讨
  13. 2020第十一届蓝桥杯7月份省赛真题(JavaB组题解)
  14. C#的HTTP协议中POST与GET的区别
  15. 想要认认真真的夯实基础知识了
  16. Jenkins流水线配置
  17. ffmpeg源码分析与应用示例(一)——H.264解码与QP提取
  18. shell基础正则表达式
  19. 老师上课也能涨粉?胖超说艺考坐拥千万粉丝靠什么?
  20. oracle hcmc,oracle11g中SQL优化(SQL TUNING)新特性之Adaptive Cursor Sharing (ACS)

热门文章

  1. 幸福工厂超级计算机有什么用,幸福工厂全替换配方简评
  2. flink catalog 及dialect、数据转存分析
  3. 在html页面引入外部html的方法 (使用第三方插件)
  4. Qt实现最小化窗口到托盘图标
  5. CentOS 7 最小化系统安装图形化桌面
  6. IP 地址详解(IPv4、IPv6)
  7. linux常用命令词典
  8. shell遍历多个数组
  9. Redhat Enterprise Linux 6.5下安装Oracle11g R2
  10. 数据结构JAVA实现——树