HTTP的认证方式之DIGEST 认证(摘要认证)

article/2025/11/8 15:48:26

核心步骤:
步骤 1: 请求需认证的资源时,服务器会随着状态码 401Authorization Required,返回带WWW-Authenticate 首部字段的响应。该字段内包含质问响应方式认证所需的临时质询码(随机数,nonce)。首部字段 WWW-Authenticate 内必须包含realm 和nonce 这两个字段的信息。客户端就是依靠向服务器回送这两个值进行认证的。nonce 是一种每次随返回的 401 响应生成的任意随机字符串。该字符串通常推荐由Base64 编码的十六进制数的组成形式,但实际内容依赖服务器的具体实现。
 

步骤 2:接收到401状态码的客户端,返回的响应中包含 DIGEST 认证必须的首部字段 Authorization 信息。首部字段 Authorization 内必须包含 username、realm、nonce、uri 和response的字段信息。其中,realm 和 nonce 就是之前从服务器接收到的响应中的字段。
username是realm 限定范围内可进行认证的用户名。
uri(digest-uri)即Request-URI的值,但考虑到经代理转发后Request-URI的值可能被修改因此事先会复制一份副本保存在 uri内。
response 也可叫做 Request-Digest,存放经过 MD5 运算后的密码字符串,形成响应码。

步骤 3:接收到包含首部字段 Authorization 请求的服务器,会确认认证信息的正确性。认证通过后则返回包含 Request-URI 资源的响应。并且这时会在首部字段 Authentication-Info 写入一些认证成功的相关信息。不过我下面的例子没有去写这个Authentication-Info,而是直接返回的数据。因为我实在session里缓存的认证结果。
 

校验 response 的算法   浏览器 Authorization 的内容举例:
Digest username="q", realm="test", nonce="T53sV+xXH3FrrER4YZwpFQ==", uri="/portal/applications", 
response="f80492644b0700b404f2fb3f4d62861e", qop=auth, nc=00000001, cnonce="25c980f9f95fd544"
其中 response 是根据如下算法计算得到:
response = MD5(MD5(username:realm:password):nonce:nc:cnonce:qop:MD5(<request-method>:url))

服务端代码(SpringBoot项目):

自定义注解 RequireAuth

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 自定义注解 , 用于授权认证的拦截* * @author LZHH** 2022年10月11日*/
// can be used to method
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequireAuth {}

拦截器 RequireAuthInterceptor

import java.text.MessageFormat;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;/*** 拦截器 RequireAuthInterceptor* @author LZHH** 2022年10月11日*/
public class RequireAuthInterceptor extends HandlerInterceptorAdapter {// 为了 测试Digest nc 值每次请求增加private int nc = 0;@Overridepublic boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {// 请求目标为 method of controller,需要进行验证if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;Object object = handlerMethod.getMethodAnnotation(RequireAuth.class);/* 方法没有 @RequireAuth 注解, 放行 */if (object == null) {return true; // 放行}/* 方法有 @RequireAuth 注解,需要拦截校验 */// 没有 Authorization 请求头,或者 Authorization 认证信息验证不通过,拦截if (!isAuth(req, res)) {// 验证不通过,拦截return false;}// 验证通过,放行return true;}// 请求目标不是 mehod of controller, 放行return true;}private boolean isAuth(HttpServletRequest req, HttpServletResponse res) {String authStr = req.getHeader("Authorization");System.out.println("请求 Authorization 的内容:" + authStr);if (authStr == null || authStr.length() <= 7) {// 没有 Authorization 请求头,开启质询return challenge(res);}DigestAuthInfo authObject = DigestUtils.getAuthInfoObject(authStr);// System.out.println(authObject);/** 生成 response 的算法:*  response = MD5(MD5(username:realm:password):nonce:nc:cnonce:qop:MD5(<request-method>:url))*/// 这里密码固定为 123456, 实际应用需要根据用户名查询数据库或缓存获得String HA1 = DigestUtils.MD5(authObject.getUsername() + ":" + authObject.getRealm() + ":123456");String HD = String.format(authObject.getNonce() + ":" + authObject.getNc() + ":" + authObject.getCnonce() + ":"+ authObject.getQop());String HA2 = DigestUtils.MD5(req.getMethod() + ":" + authObject.getUri());String responseValid = DigestUtils.MD5(HA1 + ":" + HD + ":" + HA2);// 如果 Authorization 中的 response(浏览器生成的) 与期望的 response(服务器计算的) 相同,则验证通过System.out.println("Authorization 中的 response: " + authObject.getResponse());System.out.println("期望的 response: " + responseValid);if (responseValid.equals(authObject.getResponse())) {/* 判断 nc 的值,用来防重放攻击 */// 判断此次请求的 Authorization 请求头里面的 nc 值是否大于之前保存的 nc 值// 大于,替换旧值,然后 return true// 否则,return false// 测试代码 startint newNc = Integer.parseInt(authObject.getNc(), 16);System.out.println("old nc: " + this.nc + ", new nc: " + newNc);if (newNc > this.nc) {this.nc = newNc;return true;}return false;// 测试代码 end}// 验证不通过,重复质询return challenge(res);}/*** 质询:返回状态码 401 和 WWW-Authenticate 响应头* * @param res 返回false,则表示拦截器拦截请求*/private boolean challenge(HttpServletResponse res) {// 质询前,重置或删除保存的与该用户关联的 nc 值(nc:nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量)// 将 nc 置为初始值 0, 这里代码省略// 测试代码 startthis.nc = 0;// 测试代码 endres.setStatus(401);String str = MessageFormat.format("Digest realm={0},nonce={1},qop={2}", "\"no auth\"","\"" + DigestUtils.generateToken() + "\"", "\"auth\"");res.addHeader("WWW-Authenticate", str);return false;}}

注册拦截器 WebConfig

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** 注册拦截器 WebConfig* @author LZHH** 2022年10月11日*/
@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {RequireAuthInterceptor requireAuthInterceptor = new RequireAuthInterceptor();registry.addInterceptor(requireAuthInterceptor);}}

  DIGEST认证信息model类 DigestAuthInfo

/*** DIGEST认证信息model类 DigestAuthInfo* @author LZHH** 2022年10月11日*/
public class DigestAuthInfo {private String username;private String realm;private String nonce;private String uri;private String response;private String qop;private String nc;public String cnonce;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getRealm() {return realm;}public void setRealm(String realm) {this.realm = realm;}public String getNonce() {return nonce;}public void setNonce(String nonce) {this.nonce = nonce;}public String getUri() {return uri;}public void setUri(String uri) {this.uri = uri;}public String getResponse() {return response;}public void setResponse(String response) {this.response = response;}public String getQop() {return qop;}public void setQop(String qop) {this.qop = qop;}public String getNc() {return nc;}public void setNc(String nc) {this.nc = nc;}public String getCnonce() {return cnonce;}public void setCnonce(String cnonce) {this.cnonce = cnonce;}@Overridepublic String toString() {return "DigestAuthInfo [username=" + username + ", realm=" + realm + ", nonce=" + nonce + ", uri=" + uri+ ", response=" + response + ", qop=" + qop + ", nc=" + nc + ", cnonce=" + cnonce + "]";}}

DIGEST认证的工具类 DigestUtils

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Random;import org.junit.Test;/*** DIGEST认证的工具类 DigestUtils* @author LZHH** 2022年10月11日*/
public class DigestUtils {/*** 根据当前时间戳生成一个随机字符串* @return*/public static String generateToken() {String s = String.valueOf(System.currentTimeMillis() + new Random().nextInt());try {MessageDigest messageDigest = MessageDigest.getInstance("md5");byte[] digest = messageDigest.digest(s.getBytes());return Base64.getEncoder().encodeToString(digest);} catch (NoSuchAlgorithmException e) {throw new RuntimeException();}}@Testpublic void testGenerateToken() {// heL2WICEml8/UGfAQsS9mQ==System.out.println(generateToken());}public static String MD5(String inStr) {MessageDigest md5 = null;try {md5 = MessageDigest.getInstance("MD5");} catch (Exception e) {System.out.println(e.toString());e.printStackTrace();return "";}char[] charArray = inStr.toCharArray();byte[] byteArray = new byte[charArray.length];for (int i = 0; i < charArray.length; i++) {byteArray[i] = (byte) charArray[i];}byte[] md5Bytes = md5.digest(byteArray);StringBuffer hexValue = new StringBuffer();for (int i = 0; i < md5Bytes.length; i++) {int val = ((int) md5Bytes[i]) & 0xff;if (val < 16)hexValue.append("0");hexValue.append(Integer.toHexString(val));}return hexValue.toString();}/*** 该方法用于将 Authorization 请求头的内容封装成一个对象。* * Authorization 请求头的内容为:*     Digest username="aaa", realm="no auth", nonce="b2b74be03ff44e1884ba0645bb961b53",*     uri="/BootDemo/login", response="90aff948e6f2207d69ecedc5d39f6192", qop=auth,*     nc=00000002, cnonce="eb73c2c68543faaa"*/public static DigestAuthInfo getAuthInfoObject(String authStr) {if (authStr == null || authStr.length() <= 7)return null;if (authStr.toLowerCase().indexOf("digest") >= 0) {// 截掉前缀 DigestauthStr = authStr.substring(6);}// 将双引号去掉authStr = authStr.replaceAll("\"", "");DigestAuthInfo digestAuthObject = new DigestAuthInfo();String[] authArray = new String[8];authArray = authStr.split(",");// System.out.println(java.util.Arrays.toString(authArray));for (int i = 0, len = authArray.length; i < len; i++) {String auth = authArray[i];String key = auth.substring(0, auth.indexOf("=")).trim();String value = auth.substring(auth.indexOf("=") + 1).trim();switch (key) {case "username":digestAuthObject.setUsername(value);break;case "realm":digestAuthObject.setRealm(value);break;case "nonce":digestAuthObject.setNonce(value);break;case "uri":digestAuthObject.setUri(value);break;case "response":digestAuthObject.setResponse(value);break;case "qop":digestAuthObject.setQop(value);break;case "nc":digestAuthObject.setNc(value);break;case "cnonce":digestAuthObject.setCnonce(value);break;}}return digestAuthObject;}}

 测试接口类 loginController

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;@Controller
public class LoginController {@RequireAuth@RequestMapping("/login")@ResponseBodypublic String login(HttpServletRequest req, HttpServletResponse res) {return "{code: 0, data: {username:\"test\"}}";}@RequireAuth@RequestMapping("/index")@ResponseBodypublic String index(HttpServletRequest req, HttpServletResponse res) {return "{code: 0, data: {xxx:\"xxx\"}}";}@RequestMapping("/index2")@ResponseBodypublic String index2(HttpServletRequest req, HttpServletResponse res) {return "{code: 0, data: {666:\"666\"}}";}
}

浏览器测试:

输入用户名和密码123456

客户端接口测试

import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;import java.io.IOException;
import java.net.URI;/*** HTTP摘要认证***/
public class HttpDigestClientUtil {public static String httpSendRequest(String url, String userName, String passWord, String param, String headValue) {CloseableHttpClient httpClient = null;String result = null;try {URI serverURI = new URI(url);CredentialsProvider credsProvider = new BasicCredentialsProvider();credsProvider.setCredentials(new AuthScope(serverURI.getHost(), serverURI.getPort()),new UsernamePasswordCredentials(userName, passWord));httpClient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();HttpPost post = new HttpPost(url);// 构造消息头post.setHeader("Content-type", "application/json; charset=utf-8");if (headValue != null)post.setHeader("User-Identify", headValue);post.setEntity(new StringEntity(param, "UTF-8"));// JSON 参数HttpResponse response = httpClient.execute(post);result = EntityUtils.toString(response.getEntity());System.out.println("######返回的结果:"+ result);System.out.println("######返回的状态和类型:"+ response.getStatusLine().getStatusCode()+";"+response.getEntity().getContentType());EntityUtils.consume(response.getEntity());httpClient.close();} catch (Exception e) {e.printStackTrace();} finally {if (httpClient != null) {try {httpClient.close();} catch (IOException e) {e.printStackTrace();}}}return result;}}

直接调用接口:

public class HttpDigestClientTest {public static void main(String[] args) {String result = HttpDigestClientUtil.httpSendRequest("http://localhost:8080/login","aaa","123456","111","66666666666666");System.out.println("6666666666666");System.out.println(result);}
}

结果:

通过接口调用只请求一次,其实是httpClient架包里面有判断,发现接口返回401,需要认证的时候,会再次加上摘要再次请求接口。

 

 

参考:HTTP的几种认证方式之DIGEST 认证(摘要认证) - wenbin_ouyang - 博客园


http://chatgpt.dhexx.cn/article/yEDjvt2E.shtml

相关文章

Digest Auth 摘要认证

Digest Auth 摘要认证 1.非常规方式 转载&#xff1a;https://blog.csdn.net/qq_25391785/article/details/86595529 public static void postMethod(String url, String query) {try {CredentialsProvider credsProvider new BasicCredentialsProvider();credsProvider.setC…

digest鉴权

“摘要”式认证&#xff08; Digest authentication&#xff09;是一个简单的认证机制&#xff0c;最初是为HTTP协议开发的&#xff0c;因而也常叫做HTTP摘要&#xff0c;在RFC2671中描述。其身份验证机制很简单&#xff0c;它采用杂凑式&#xff08;hash&#xff09;加密方法&…

消息摘要(Digest),数字签名(Signature),数字证书(Certificate)是什么?

1. 消息摘要&#xff08;Digest&#xff09; 1. 什么是消息摘要&#xff1f; 对一份数据&#xff0c;进行一个单向的 Hash 函数&#xff0c;生成一个固定长度的 Hash 值&#xff0c;这个值就是这份数据的摘要&#xff0c;也称为指纹。 2. 摘要算法 常见的摘要算法有 MD5、SHA…

HTTP通讯安全中的Digest摘要认证释义与实现

摘要 出于安全考虑&#xff0c;HTTP规范定义了几种认证方式以对访问者身份进行鉴权&#xff0c;最常见的认证方式之一是Digest认证 Digest认证简介 HTTP通讯采用人类可阅读的文本格式进行数据通讯&#xff0c;其内容非常容易被解读。出于安全考虑&#xff0c;HTTP规范定义了几…

http协议之digest(摘要)认证,详细讲解并附Java SpringBoot源码

目录 1.digest认证是什么&#xff1f; 2.digest认证过程 3.digest认证参数详解 4.基于SpringBoot实现digest认证 5.digest认证演示 6.digest认证完整项目 7.参考博客 1.digest认证是什么&#xff1f; HTTP通讯采用人类可阅读的文本格式进行数据通讯&#xff0c;其内容非…

【WinRAR】WinRAR 6.01 官方最新简体中文版

WinRAR 6.01 官方简体中文商业版下载地址&#xff08;需要注册&#xff09;&#xff1a; 64位&#xff1a; https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20210414/wrr/winrar-x64-601sc.exe https://www.win-rar.com/fileadmin/winrar-versions/sc/sc20210414/…

WinRAR命令行

基本使用 实践 将文件夹压缩到zip包 输入&#xff1a;文件夹如下&#xff0c;文件夹为class。 输出&#xff1a;classes.zip 指令如下&#xff1a; rar a classes.zip .\classes或者 WinRAR a classes.zip .\classes结果如下&#xff1a; PS C:\Users\liyd\Desktop\kuai…

WinRAR安装教程

文章目录 WinRAR安装教程无广告1. 下载2. 安装3. 注册4. 去广告 WinRAR安装教程无广告 1. 下载 国内官网&#xff1a;https://www.winrar.com.cn/ 2. 安装 双击&#xff0c;使用默认路径&#xff1a; 点击“安装”。 点击“确定”。 点击“完成”。 3. 注册 链接&#x…

WinRAR注册+去广告教程

1、注册 在WinRAR安装目录创建rarreg.key文件&#xff0c; 拷贝如下内容并保存&#xff1a; RAR registration data Federal Agency for Education 1000000 PC usage license UIDb621cca9a84bc5deffbf 6412612250ffbf533df6db2dfe8ccc3aae5362c06d54762105357d 5e3b1489e751c…

WinRAR4.20注册文件rarreg.key

2019独角兽企业重金招聘Python工程师标准>>> 在WinRAR的安装目录下&#xff0c;新建rarreg.key文件&#xff08;注意不要创建成rarreg.key.txt文件了^_^&#xff09;&#xff0c;内容为如下&#xff1a; RAR registration data Team EAT Single PC usage license UI…

Android按钮样式

//创建一个新的XML文件&#xff0c;可命名为styles<style name"button1"><item name"android:layout_height">wrap_content</item><item name"android:textColor">#FFFFFF</item><item name"android:text…

漂亮的Button按钮样式

开发中各种样式的Button,其实这些样式所有的View都可以共用的,可能对于你改变的只有颜色 所有的都是用代码实现 边框样式,给你的View加上边框 <Buttonandroid:layout_width="0dip"android:layout_height="match_parent"android:layout_margin=&q…

「HTML+CSS」--自定义按钮样式【001】

前言 Hello&#xff01;小伙伴&#xff01; 首先非常感谢您阅读海轰的文章&#xff0c;倘若文中有错误的地方&#xff0c;欢迎您指出&#xff5e; 哈哈 自我介绍一下 昵称&#xff1a;海轰 标签&#xff1a;程序猿一只&#xff5c;C选手&#xff5c;学生 简介&#xff1a;因C语…

HTML_炫酷的按钮样式

html部分 <a href"#"><span></span><span></span><span></span><span></span>Neon button</a><a href"#"><span></span><span></span><span></span…

html改变按钮样式

今天有人问我怎么改样式&#xff0c;需求是三个按钮&#xff0c;一次点一个&#xff0c;要求被点击的按钮和没被点的按钮是两种不同的样式&#xff0c;如图所示。 最初三个按钮都没选如图一&#xff0c;然后点击“已读”按钮&#xff0c;“已读”按钮样式改变。再点击“全部”按…

button按钮的一些样式效果

先制作一个button按钮 &#xff0c;将它原本的样式取消掉再把button按钮的颜色设置成transparent &#xff0c;再设置button按钮的边框。首先将button按钮的初始样式取消掉 &#xff0c;在设置button按钮的width和 height &#xff0c;font-size &#xff0c;还有border 现在写…

vue点击按钮改变按钮样式

一. 效果 点击按钮前&#xff1a; 点击按钮后&#xff1a; 再次点击按钮变回原来的样式&#xff1a; 二. 具体代码 <template><div id"box"><button click"btn" id"but" v-bind:class"{ but01: style1, but02: style2 }&qu…

CSS 按钮button美化

.login-button { /* 按钮美化 */width: 270px; /* 宽度 */height: 40px; /* 高度 */border-width: 0px; /* 边框宽度 */border-radius: 3px; /* 边框半径 */background: #1E90FF; /* 背景颜色 */cursor: pointer; /* 鼠标移入按钮范围时出现手势 */outline: none; /* 不显示轮廓…

css 按钮按下样式

在项目开发中&#xff0c;按钮通常需要添加按钮的获得焦点状态&#xff0c;电脑端用 :hover 移动端用 :active 。多个按钮需要添加时&#xff0c;就得添加多个获得焦点样式。 可通过添加背景图片的方式来给所有的按钮添加样式&#xff0c;该样式会给当前按钮添加一个白色的透明…

button样式设置:按钮按压效果

在学习MVC基础时&#xff0c;里面的案例有很多都是有按钮的&#xff0c; 但button的默认样式不好看&#xff0c;于是设置了按钮的样式&#xff0c;按 钮按压时有一种现实生活中按钮向下压的效果&#xff0c;这样看起来 非常美观&#xff0c;代码也是不多&#xff0c;简单而又实…