HTTP Digest Authentication 使用心得
简介
浏览器弹出这个原生的用户登录对话框,想必大家都不陌生,就是 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 使用心得相关推荐
- [转]asp.net权限认证:摘要认证(digest authentication)
本文转自:http://www.cnblogs.com/lanxiaoke/p/6357501.html 摘要认证简单介绍 摘要认证是对基本认证的改进,即是用摘要代替账户密码,从而防止明文传输中账户密 ...
- Java猿社区—Http digest authentication 请求代码最全示例
文章目录 什么是摘要认证 服务器核实用户身份 客户端反馈用户身份 server 确认用户 代码示例 欢迎关注作者博客 简书传送门 什么是摘要认证 摘要认证( Digest authentication ...
- libcurl第五课 Digest Authentication摘要验证使用
场景 在安迅士摄像机网页上,配置系统选项,HTTP/RTSP Password Settings 中, 选择Encrypted only.获取设备的云台状态信息,使用的是摘要认证 例 ...
- Digest Authentication 摘要认证(转载)
原文:Digest Authentication 摘要认证_weixin_34007906的博客-CSDN博客 摘要"式认证( Digest authentication)是一个简单的认证机 ...
- asp.net权限认证:摘要认证(digest authentication)
摘要认证简单介绍 摘要认证是对基本认证的改进,即是用摘要代替账户密码,从而防止明文传输中账户密码的泄露 之前对摘要认证也不是很熟悉,还得感谢圆中的 parry 贡献的博文:ASP.NET Web AP ...
- Digest authentication
"摘要"式认证( Digestauthentication)是一个简单的认证机制,最初是为HTTP协议开发的,因而也常叫做HTTP摘要,在RFC2671中描述.其身份验证机制很简单 ...
- Weaning the Web off of Session Cookies Making Digest Authentication Viable
http://www.vsecurity.com/download/papers/WeaningTheWebOffOfSessionCookies.pdf
- HttpClient basic authentication
2019独角兽企业重金招聘Python工程师标准>>> 1. Overview This tutorial will illustrate how to configure Basi ...
- 配置Apache Basic和Digest认证
转载:http://blog.jobbole.com/41519/ 在伯乐在线看到一篇<在Nginx下对网站进行密码保护>文章, 正好和自己这两天研究的问题有些相同点.我侧重研究的是如何破 ...
最新文章
- python用def编写calsum函数_Python函数
- 为什么说现在是计算机视觉最好的时代?
- 【已解决】tomcat启动不成功(点击startup.bat闪退)的解决办法
- Java基础篇:对象拷贝:clone方法 以及 序列化
- 使用ASP.NET 2.0进行记录错误
- c#打开数据库连接池的工作机制_它是谁?一个比 c3p0 快 200 倍的数据库连接池!...
- Bug:Google Analytics例子未使用example.com
- 【2019杭电多校第二场1005 = HDU6595】Everything Is Generated In Equal Probability(期望-递推)
- 解决“无法删除文件:无法读源文件或磁盘”的方法(chkdsk)
- emacs-打开和关闭
- 寒假ACM假期总结 (7)
- 达梦数据库忘记SYSDBA密码的问题探讨
- 2020第十一届蓝桥杯7月份省赛真题(JavaB组题解)
- C#的HTTP协议中POST与GET的区别
- 想要认认真真的夯实基础知识了
- Jenkins流水线配置
- ffmpeg源码分析与应用示例(一)——H.264解码与QP提取
- shell基础正则表达式
- 老师上课也能涨粉?胖超说艺考坐拥千万粉丝靠什么?
- oracle hcmc,oracle11g中SQL优化(SQL TUNING)新特性之Adaptive Cursor Sharing (ACS)