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


    目录

    1.digest认证是什么?

    2.digest认证过程

    3.digest认证参数详解

    4.基于SpringBoot实现digest认证

    5.digest认证演示

    6.digest认证完整项目

    7.参考博客


    1.digest认证是什么?

            HTTP通讯采用人类可阅读的文本格式进行数据通讯,其内容非常容易被解读。出于安全考虑,HTTP规范定义了几种认证方式以对访问者身份进行鉴权,最常见的认证方式之一是Digest认证。Digest是一种加密认证方式,通讯中不会传输密码信息,而仅采用校验方式对接入的请求进行验证。

            DIGEST 认证是使用质询 / 响应的方式(challenge/response),但不会像 BASIC 认证那样直接发送明文密码。质询响应方式是指,一开始一方会先发送认证要求给另一方,接着使用从另一方那接收到的质询码计算生成响应码。最后将响应码返回给对方进行认证的方式。

            Digest认证支持的加密算法有:SHA256,SHA512/256,MD5。上述这几种算法都是由哈希函数来生成散列值,其加密过程为单向计算,请求方无法反算出密码明文。
     

    2.digest认证过程

     

    步骤 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里缓存的认证结果)。

    3.digest认证参数详解

    WWW-Authentication:用来定义使用何种方式(Basic、Digest、Bearer等)去进行认证以获取受保护的资源
    realm:表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
    qop:保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,(可以为空,但是)不推荐为空值
    nonce:服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击
    nc:nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求
    cnonce:客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
    response:这是由用户代理软件计算出的一个字符串,以证明用户知道口令
    Authorization-Info:用于返回一些与授权会话相关的附加信息
    nextnonce:下一个服务端随机数,使客户端可以预先发送正确的摘要
    rspauth:响应摘要,用于客户端对服务端进行认证
    stale:当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的401响应,并指定stale=true,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了

    4.基于SpringBoot实现digest认证

    DemoApplication.java
    1. package com.digest.demo;
    2. import org.springframework.boot.SpringApplication;
    3. import org.springframework.boot.autoconfigure.SpringBootApplication;
    4. @SpringBootApplication(scanBasePackages="com.digest")
    5. public class DemoApplication {
    6. public static void main(String[] args) {
    7. SpringApplication.run(DemoApplication.class, args);
    8. }
    9. }
    WebConfig.java
    1. import com.digest.interceptor.DigestAuthInterceptor;
    2. import org.springframework.context.annotation.Configuration;
    3. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    4. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    5. @Configuration
    6. public class WebConfig implements WebMvcConfigurer {
    7. @Override
    8. public void addInterceptors(InterceptorRegistry registry) {
    9. DigestAuthInterceptor requireAuthInterceptor = new DigestAuthInterceptor();
    10. registry.addInterceptor(requireAuthInterceptor);
    11. }
    12. }
    IndexController.java
    1. package com.digest.controller;
    2. import javax.servlet.http.HttpServletRequest;
    3. import javax.servlet.http.HttpServletResponse;
    4. import com.digest.interceptor.DigestAuth;
    5. import org.springframework.stereotype.Controller;
    6. import org.springframework.web.bind.annotation.RequestMapping;
    7. import org.springframework.web.bind.annotation.ResponseBody;
    8. @Controller
    9. public class IndexController {
    10. @DigestAuth
    11. @RequestMapping("/login")
    12. @ResponseBody
    13. public String login(HttpServletRequest req, HttpServletResponse res) {
    14. return "{code: 0, data: {username:\"test\"}}";
    15. }
    16. @DigestAuth
    17. @RequestMapping("/index")
    18. @ResponseBody
    19. public String index(HttpServletRequest req, HttpServletResponse res) {
    20. return "{code: 0, data: {xxx:\"xxx\"}}";
    21. }
    22. }
    DigestAuth.java
    1. package com.digest.interceptor;
    2. import java.lang.annotation.ElementType;
    3. import java.lang.annotation.Retention;
    4. import java.lang.annotation.RetentionPolicy;
    5. import java.lang.annotation.Target;
    6. // can be used to method
    7. @Retention(RetentionPolicy.RUNTIME)
    8. @Target(ElementType.METHOD)
    9. public @interface DigestAuth {
    10. }
    DigestAuthInterceptor.java
    1. package com.digest.interceptor;
    2. import java.text.MessageFormat;
    3. import javax.servlet.http.HttpServletRequest;
    4. import javax.servlet.http.HttpServletResponse;
    5. import com.digest.model.DigestAuthInfo;
    6. import com.digest.util.DigestUtils;
    7. import org.springframework.web.method.HandlerMethod;
    8. import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    9. public class DigestAuthInterceptor extends HandlerInterceptorAdapter {
    10. // 为了 测试Digest nc 值每次请求增加
    11. private int nc = 0;
    12. @Override
    13. public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
    14. // 请求目标为 method of controller,需要进行验证
    15. if (handler instanceof HandlerMethod) {
    16. HandlerMethod handlerMethod = (HandlerMethod) handler;
    17. Object object = handlerMethod.getMethodAnnotation(DigestAuth.class);
    18. /* 方法没有 @RequireAuth 注解, 放行 */
    19. if (object == null) {
    20. return true; // 放行
    21. }
    22. /* 方法有 @RequireAuth 注解,需要拦截校验 */
    23. // 没有 Authorization 请求头,或者 Authorization 认证信息验证不通过,拦截
    24. if (!isAuth(req, res)) {
    25. // 验证不通过,拦截
    26. return false;
    27. }
    28. // 验证通过,放行
    29. return true;
    30. }
    31. // 请求目标不是 mehod of controller, 放行
    32. return true;
    33. }
    34. private boolean isAuth(HttpServletRequest req, HttpServletResponse res) {
    35. String authStr = req.getHeader("Authorization");
    36. System.out.println("请求 Authorization 的内容:" + authStr);
    37. if (authStr == null || authStr.length() <= 7) {
    38. // 没有 Authorization 请求头,开启质询
    39. return challenge(res);
    40. }
    41. DigestAuthInfo authObject = DigestUtils.getAuthInfoObject(authStr);
    42. // System.out.println(authObject);
    43. /*
    44. * 生成 response 的算法:
    45. * response = MD5(MD5(username:realm:password):nonce:nc:cnonce:qop:MD5(<request-method>:url))
    46. */
    47. // 这里密码固定为 123456, 实际应用需要根据用户名查询数据库或缓存获得
    48. String HA1 = DigestUtils.MD5(authObject.getUsername() + ":" + authObject.getRealm() + ":"+authObject.getUsername());
    49. String HD = String.format(authObject.getNonce() + ":" + authObject.getNc() + ":" + authObject.getCnonce() + ":"
    50. + authObject.getQop());
    51. String HA2 = DigestUtils.MD5(req.getMethod() + ":" + authObject.getUri());
    52. String responseValid = DigestUtils.MD5(HA1 + ":" + HD + ":" + HA2);
    53. // 如果 Authorization 中的 response(浏览器生成的) 与期望的 response(服务器计算的) 相同,则验证通过
    54. System.out.println("Authorization 中的 response: " + authObject.getResponse());
    55. System.out.println("期望的 response: " + responseValid);
    56. if (responseValid.equals(authObject.getResponse())) {
    57. /* 判断 nc 的值,用来防重放攻击 */
    58. // 判断此次请求的 Authorization 请求头里面的 nc 值是否大于之前保存的 nc 值
    59. // 大于,替换旧值,然后 return true
    60. // 否则,return false
    61. // 测试代码 start
    62. int newNc = Integer.parseInt(authObject.getNc(), 16);
    63. System.out.println("old nc: " + this.nc + ", new nc: " + newNc);
    64. if (newNc > this.nc) {
    65. this.nc = newNc;
    66. return true;
    67. }
    68. return false;
    69. // 测试代码 end
    70. }
    71. // 验证不通过,重复质询
    72. return challenge(res);
    73. }
    74. /**
    75. * 质询:返回状态码 401 和 WWW-Authenticate 响应头
    76. *
    77. * @param res 返回false,则表示拦截器拦截请求
    78. */
    79. private boolean challenge(HttpServletResponse res) {
    80. // 质询前,重置或删除保存的与该用户关联的 nc 值(nc:nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量)
    81. // 将 nc 置为初始值 0, 这里代码省略
    82. // 测试代码 start
    83. this.nc = 0;
    84. // 测试代码 end
    85. res.setStatus(401);
    86. String str = MessageFormat.format("Digest realm={0},nonce={1},qop={2}", "\"no auth\"",
    87. "\"" + DigestUtils.generateToken() + "\"", "\"auth\"");
    88. res.addHeader("WWW-Authenticate", str);
    89. return false;
    90. }
    91. }
    DigestAuthInfo.java
    1. package com.digest.model;
    2. import lombok.Data;
    3. @Data
    4. public class DigestAuthInfo {
    5. private String username;//认证的用户名
    6. private String realm;//Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
    7. private String nonce;//服务端向客户端发送质询时附带的一个随机数
    8. private String uri;//请求的资源位置,访问地址,例如/index
    9. private String response;//由用户代理软件计算出的一个字符串,以证明用户知道口令
    10. private String qop;//保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,(可以为空,但是)不推荐为空值
    11. private String nc;//nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。
    12. public String cnonce;//客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。
    13. }
    DigestUtils.java
    1. package com.digest.util;
    2. import java.security.MessageDigest;
    3. import java.security.NoSuchAlgorithmException;
    4. import java.util.Base64;
    5. import java.util.Random;
    6. import com.digest.model.DigestAuthInfo;
    7. public class DigestUtils {
    8. /**
    9. * 根据当前时间戳生成一个随机字符串
    10. * @return
    11. */
    12. public static String generateToken() {
    13. String s = String.valueOf(System.currentTimeMillis() + new Random().nextInt());
    14. try {
    15. MessageDigest messageDigest = MessageDigest.getInstance("md5");
    16. byte[] digest = messageDigest.digest(s.getBytes());
    17. return Base64.getEncoder().encodeToString(digest);
    18. } catch (NoSuchAlgorithmException e) {
    19. throw new RuntimeException();
    20. }
    21. }
    22. public static String MD5(String inStr) {
    23. MessageDigest md5 = null;
    24. try {
    25. md5 = MessageDigest.getInstance("MD5");
    26. } catch (Exception e) {
    27. System.out.println(e.toString());
    28. e.printStackTrace();
    29. return "";
    30. }
    31. char[] charArray = inStr.toCharArray();
    32. byte[] byteArray = new byte[charArray.length];
    33. for (int i = 0; i < charArray.length; i++) {
    34. byteArray[i] = (byte) charArray[i];
    35. }
    36. byte[] md5Bytes = md5.digest(byteArray);
    37. StringBuffer hexValue = new StringBuffer();
    38. for (int i = 0; i < md5Bytes.length; i++) {
    39. int val = ((int) md5Bytes[i]) & 0xff;
    40. if (val < 16)
    41. hexValue.append("0");
    42. hexValue.append(Integer.toHexString(val));
    43. }
    44. return hexValue.toString();
    45. }
    46. /**
    47. * 该方法用于将 Authorization 请求头的内容封装成一个对象。
    48. *
    49. * Authorization 请求头的内容为:
    50. * Digest username="aaa", realm="no auth", nonce="b2b74be03ff44e1884ba0645bb961b53",
    51. * uri="/BootDemo/login", response="90aff948e6f2207d69ecedc5d39f6192", qop=auth,
    52. * nc=00000002, cnonce="eb73c2c68543faaa"
    53. */
    54. public static DigestAuthInfo getAuthInfoObject(String authStr) {
    55. if (authStr == null || authStr.length() <= 7)
    56. return null;
    57. if (authStr.toLowerCase().indexOf("digest") >= 0) {
    58. // 截掉前缀 Digest
    59. authStr = authStr.substring(6);
    60. }
    61. // 将双引号去掉
    62. authStr = authStr.replaceAll("\"", "");
    63. DigestAuthInfo digestAuthObject = new DigestAuthInfo();
    64. String[] authArray = new String[8];
    65. authArray = authStr.split(",");
    66. // System.out.println(java.util.Arrays.toString(authArray));
    67. for (int i = 0, len = authArray.length; i < len; i++) {
    68. String auth = authArray[i];
    69. String key = auth.substring(0, auth.indexOf("=")).trim();
    70. String value = auth.substring(auth.indexOf("=") + 1).trim();
    71. switch (key) {
    72. case "username":
    73. digestAuthObject.setUsername(value);
    74. break;
    75. case "realm":
    76. digestAuthObject.setRealm(value);
    77. break;
    78. case "nonce":
    79. digestAuthObject.setNonce(value);
    80. break;
    81. case "uri":
    82. digestAuthObject.setUri(value);
    83. break;
    84. case "response":
    85. digestAuthObject.setResponse(value);
    86. break;
    87. case "qop":
    88. digestAuthObject.setQop(value);
    89. break;
    90. case "nc":
    91. digestAuthObject.setNc(value);
    92. break;
    93. case "cnonce":
    94. digestAuthObject.setCnonce(value);
    95. break;
    96. }
    97. }
    98. return digestAuthObject;
    99. }
    100. }

    application.properties

    1. server.port=8080
    2. server.servlet.context-path=/

    pom.xml

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    4. <modelVersion>4.0.0</modelVersion>
    5. <parent>
    6. <groupId>org.springframework.boot</groupId>
    7. <artifactId>spring-boot-starter-parent</artifactId>
    8. <version>2.7.6</version>
    9. <relativePath/> <!-- lookup parent from repository -->
    10. </parent>
    11. <groupId>SpringBootDigest</groupId>
    12. <artifactId>demo</artifactId>
    13. <version>0.0.1-SNAPSHOT</version>
    14. <name>demo</name>
    15. <description>Demo project for Spring Boot</description>
    16. <properties>
    17. <java.version>1.8</java.version>
    18. </properties>
    19. <dependencies>
    20. <dependency>
    21. <groupId>org.springframework.boot</groupId>
    22. <artifactId>spring-boot-starter-web</artifactId>
    23. </dependency>
    24. <dependency>
    25. <groupId>org.springframework.boot</groupId>
    26. <artifactId>spring-boot-starter-test</artifactId>
    27. <scope>test</scope>
    28. </dependency>
    29. <dependency>
    30. <groupId>org.projectlombok</groupId>
    31. <artifactId>lombok</artifactId>
    32. <version>1.18.24</version>
    33. </dependency>
    34. </dependencies>
    35. <build>
    36. <plugins>
    37. <plugin>
    38. <groupId>org.springframework.boot</groupId>
    39. <artifactId>spring-boot-maven-plugin</artifactId>
    40. </plugin>
    41. </plugins>
    42. </build>
    43. </project>

    5.digest认证演示

    启动项目后,访问http://localhost:8080/login,出现如下界面

     输入账号 user 密码 user,登录,出现如下界面

     项目后端打印信息如下所示:

    1. 请求 Authorization 的内容:Digest username="user", realm="no auth", nonce="Fy9j+28xS8co4LWob6LOAg==", uri="/login", response="516234f0509c3c81f9130ddfbcd95f50", qop=auth, nc=00000006, cnonce="b064dbc52ff14c0a"
    2. Authorization 中的 response: 516234f0509c3c81f9130ddfbcd95f50
    3. 期望的 response: 516234f0509c3c81f9130ddfbcd95f50
    4. old nc: 0, new nc: 6
    5. 请求 Authorization 的内容:Digest username="user", realm="no auth", nonce="Fy9j+28xS8co4LWob6LOAg==", uri="/login", response="3977b5c6e46b2941b61c835659dd904a", qop=auth, nc=00000007, cnonce="637df6c7b8be6edb"
    6. Authorization 中的 response: 3977b5c6e46b2941b61c835659dd904a
    7. 期望的 response: 3977b5c6e46b2941b61c835659dd904a
    8. old nc: 6, new nc: 7
    9. 请求 Authorization 的内容:null
    10. 请求 Authorization 的内容:Digest username="user", realm="no auth", nonce="T/46pI2qR2K0Cy6D4hb4jg==", uri="/login", response="ae9a759d6ac8af2d4a9e73a17cdd0859", qop=auth, nc=00000002, cnonce="3efb440863ffc450"
    11. Authorization 中的 response: ae9a759d6ac8af2d4a9e73a17cdd0859
    12. 期望的 response: ae9a759d6ac8af2d4a9e73a17cdd0859
    13. old nc: 0, new nc: 2

    6.digest认证完整项目

    基于SpringBoot完整项目 http://www.zrscsoft.com/sitepic/12152.html

    7.参考博客

    http协议之digest(摘要)认证_red-fly的博客-CSDN博客_digest认证

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

  • 相关阅读:
    Mockito的@Mock与@MockBean
    LeetCode 面试题 01.09. 字符串轮转
    19 【节点的增删改查】
    MATLAB 状态空间设计 —— LQG/LQR 和极点配置算法
    httpprompt.ml靶场练习
    多线程入门总结
    【IoT】产品经理:如何了解行业需求、痛点和发展机会?
    网络安全渗透测试工具AWVS14.6.2的安装与使用(激活)
    【经典算法学习-排序篇】顺序查找
    “搞事情”?OpenAl将于11月召开其首届开发者大会
  • 原文地址:https://blog.csdn.net/jlq_diligence/article/details/128172793