认证学习3 - Digest摘要认证讲解、代码实现、演示


认证大全(想学习其他认证,请到此链接查看)

Digest摘要认证 - 密文

背景: 仅仅只是用来替代Basic认证,由于Basic认证使用明文账户、密码传输这得确保客户端、服务端传输之间不会被任何人截取报文,这就要求很高,我们都知道网络传输没有绝对的安全,为了不使得明文传输,RFC提出另一种认证方式即Digest认证,认证时仅传账户、密码的摘要,这就避免有人即是劫持报文获得认证信息也不会解析得到用户的账户、密码

在这里插入图片描述

讲解(Digest摘要认证)

参考文章: https://www.cnblogs.com/huey/p/5490759.html

官方规范(新-兼容老规范-必须看): https://datatracker.ietf.org/doc/html/rfc7616
官方规范(老规范-必须看): https://datatracker.ietf.org/doc/html/rfc2617

请求头字段规范: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Digest

好文1(强烈建议看-可以不用看好文2的实现看我的实现也可以): https://www.cnblogs.com/xiaoxiaotank/p/11078571.html
好文2(强烈建议看): https://www.cnblogs.com/xiaoxiaotank/p/11079024.html

优点
用户、密码非明文传输
防止对报文内容的篡改
防止重放攻击
响应码
400:认证失败,认证失败,认证必要的参数缺失
401:认证失败,客户端传过来的摘要跟服务器计算的不一致,需要用户重新输入用户名、密码再次进行认证

在这里插入图片描述

在这里插入图片描述

//响应头:WWW-Authenticate  或者 请求头:Authorization
Digest username="用户名", realm="角色描述", nonce="服务端随机数(Base64或十六进制编码)", uri="请求uri部分", response="摘要(客户端计算、到时服务端也会计算一份看是否一致)", algorithm="摘要算法", cnonce="客户端随机数(Base64或十六进制编码)", opaque="服务端给的信息原封不动换回去即可", nc="使用次数", qop="auth|auth-init"


//响应头WWW-Authenticate 必须提供
opaque、algorithm

//响应头WWW-Authenticate 提供
qop,则请求头Authorization将这个参数原封不动的还回去


//摘要公式  secret=密钥key  data=需要摘要的数据   两个参数都是根据规则由WWW-Authenticate里面的参数进行构建
//由于摘要算法无需密钥即可生成,故充分利用将secret也作为被摘要的部分数据
KD(secret, data)=摘要算法(concat(secret, ":", data))   
secret(也称A1)=摘要算法(摘要算法(<username>:<realm>:<password>):<nonce>:<cnonce>)
secret(也称A1-sess形式)=摘要算法(<username>:<realm>:<password>)
data=<nonce>:<nc>:<cnonce>:<qop>:摘要算法(A2)
A2=<request-method>:<uri>
原英文参数(请求头携带的参数名)作用
(响应头-认证失败)WWW-Authentication用来定义使用何种方式(Basic、Digest、Bearer等)去进行认证以获取受保护的资源、以及各种其他参数添加
digest-uriuri当前请求的uri
username即将认证的用户名
realm表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
message-qopqop保护质量,包含 auth(默认的)和 auth-int(增加了报文完整性检测)以及 Token值 三种策略,(可以为空,但是)不推荐为空值
noncenonce服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击 = 官方建议每次请求都不同
userhashuserhash(选填)是否支持用户名使用hash,默认false
opaque(选填)服务端给的数据一般是Base64或十六进制编码,客户端请求认证时原封不动的返回给服务端
(请求头-开始认证)Authorization认证类型以及用于携带客户端认证信息
nonce-countncnonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求
cnonce客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
request-digestresponse这是由用户代理软件计算出的一个字符串,以证明用户知道口令 == 认证凭证(服务端校验)== 公式:MD5(MD5(A1):::::MD5(A2))
(响应头-认证成功) Authorization-Info用于返回一些与授权会话相关的附加信息
nextnonce下一个服务端随机数,使客户端可以预先发送正确的摘要
rspauth响应摘要,用于客户端对服务端进行认证 == 认证凭证(客户端校验) == 公式:MD5(MD5(A1):::::MD5(A2))
stale当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的401响应,并指定stale=true,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了
algorithm摘要算法,由服务端(响应头)进行指定,默认MD5,支持MD5、MD5-sess、SHA-256、SHA-256-sess、SHA-512-256、SHA-512-256-sess、token值
//认证凭证生成算法=即参数response、rspauth
MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))

客户端生成用户凭证的参数内容

算法A1
MD5(默认)MD5(::)
MD5-sessMD5(::)::
qopA2
auth(默认):
auth-int::MD5()

服务端生成用户凭证的参数内容 == 无请求方式-参数变化即是少了请求方式而已

qopA2
auth(默认):
auth-int::MD5()
实现(Digest认证)
代码(Digest认证)
代码(Digest认证-客户端)

在这里插入图片描述


需要额外引入这两个依赖

<!-- 主要用于该工具类线程写好的 sha-512/256摘要算法 ->
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.70</version>
</dependency>

<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.5</version>
</dependency>   


DigestAuthInterceptor.java

package work.linruchang.qq.mybaitsplusjoin.config.interceptor;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.CharPool;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import work.linruchang.qq.mybaitsplusjoin.config.interceptor.algorithm.digest.DigestAlgorithmUtil;
import work.linruchang.qq.mybaitsplusjoin.config.interceptor.algorithm.digest.WWWAuthenticate;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 作用:Basic认证
 *
 * @author LinRuChang
 * @version 1.0
 * @date 2022/08/04
 * @since 1.8
 **/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DigestAuthInterceptor implements HandlerInterceptor {


    @AllArgsConstructor
    @Getter
    enum AuthErrorEnum {

        INVALID_REQUEST(400, "invalid_request", "参数异常"),
        INVALID_DIGEST(401, "invalid_digest", "摘要不一致");


        Integer errorCode;
        String error;
        String errorDescription;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {


        boolean authStatusFlag = false;
        String authorization = request.getHeader("Authorization");
        AuthErrorEnum authErrorEnum = AuthErrorEnum.INVALID_DIGEST;

        if (StrUtil.isNotBlank(authorization)) {
            String[] splitInfos = authorization.split(StrUtil.COMMA + StrUtil.SPACE);
            splitInfos[0] = StrUtil.removePrefix(splitInfos[0], "Digest ");

            //解析认证参数
            Map<String, String> splitInfosMap = Stream.of(splitInfos)
                    .map(StrUtil::trim)
                    .map(splitInfo -> StrUtil.removeAll(splitInfo, CharPool.DOUBLE_QUOTES))
                    .collect(Collectors.toMap(splitInfo -> {
                        return splitInfo.split("=")[0];
                    }, splitInfo -> {
                        return splitInfo.split("=")[1];
                    }));
            WWWAuthenticate wwwAuthenticate = BeanUtil.toBean(splitInfosMap, WWWAuthenticate.class);
            Dict userInfo = getUserInfo(wwwAuthenticate.getUsername());
            if (userInfo != null) {
                wwwAuthenticate.setPassword(userInfo.getStr("password"));
                wwwAuthenticate.setRequestMethod(request.getMethod());
                String serverResponse = DigestAlgorithmUtil.result(wwwAuthenticate);
                if (StrUtil.equals(serverResponse, wwwAuthenticate.getResponse())) {  //摘要一致
                    authStatusFlag = true;
                }
            } else {
                authErrorEnum = AuthErrorEnum.INVALID_REQUEST;
            }
        }

        //认证失败
        if (!authStatusFlag) {
            response.setHeader("WWW-Authenticate", StrUtil.format("Digest realm=\"rights of administrators\", nonce=\"{}\", qop=\"auth\"", UUID.randomUUID().toString(true)));
            response.setHeader("content-type", "text/plain;charset=utf-8");
            response.setStatus(authErrorEnum.getErrorCode());
            response.getWriter().write("认证失败");
            return false;
        }

        return authStatusFlag;
    }


    public Dict getUserInfo(String userName) {
        if (StrUtil.equals(userName, "admin")) {
            return Dict.create()
                    .set("userName", "admin")
                    .set("password", "admin123");

        }
        return null;
    }
}
     


DigestAuthInterceptor.java

@Configuration
public class MyConfig implements WebMvcConfigurer {
    @Autowired
    DigestAuthInterceptor digestAuthInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(digestAuthInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/js/**","/user*/**","/user*/**");
    }
}


DigestAlgorithm.java

public interface DigestAlgorithm {

    /**
     * 计算摘要结果
     * 摘要算法(摘要算法(A1):<nonce>:<nc>:<cnonce>:<qop>:摘要算法(A2))
     *  A1(sess形式):  摘要算法(<username>:<realm>:<password>):<nonce>:<cnonce>
     *  A1(非sess形式):  <username>:<realm>:<password>
     *
     *  A2:<request-method>:<uri>
     * @param wwwAuthenticate
     * @return
     */
    String result(WWWAuthenticate wwwAuthenticate);

}


MD5DigestAlgorithm.java

 **/
public class MD5DigestAlgorithm implements DigestAlgorithm{
    @Override
    public String result(WWWAuthenticate wwwAuthenticate) {
        String A1Digest = null;
        String A2Digest = null;

        //MD5-sess
        if(StrUtil.containsIgnoreCase(wwwAuthenticate.getAlgorithm(),"sess")) {
            A1Digest = SecureUtil.md5(StrUtil.format("{}:{}:{}", SecureUtil.md5(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword())), wwwAuthenticate.getNonce(), wwwAuthenticate.getCnonce()));
            A2Digest = SecureUtil.md5(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));
        }else { //MD5
            A1Digest = SecureUtil.md5(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword()));
            A2Digest = SecureUtil.md5(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));
        }
        String serverResponse = SecureUtil.md5(StrUtil.format("{}:{}:{}:{}:{}:{}", A1Digest, wwwAuthenticate.getNonce(), wwwAuthenticate.getNc(), wwwAuthenticate.getCnonce(), wwwAuthenticate.getQop(), A2Digest));
        return serverResponse;
    }
}


SHA256DigestAlgorithm.java

public class SHA256DigestAlgorithm implements DigestAlgorithm{
    @Override
    public String result(WWWAuthenticate wwwAuthenticate) {
        String A1Digest = null;
        String A2Digest = null;

        //SHA256-sess
        if(StrUtil.containsIgnoreCase(wwwAuthenticate.getAlgorithm(),"sess")) {
            A1Digest = SecureUtil.sha256(StrUtil.format("{}:{}:{}", SecureUtil.md5(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword())), wwwAuthenticate.getNonce(), wwwAuthenticate.getCnonce()));
            A2Digest = SecureUtil.sha256(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));
        }else { //SHA256
            A1Digest = SecureUtil.sha256(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword()));
            A2Digest = SecureUtil.sha256(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));
        }
        String serverResponse = SecureUtil.sha256(StrUtil.format("{}:{}:{}:{}:{}:{}", A1Digest, wwwAuthenticate.getNonce(), wwwAuthenticate.getNc(), wwwAuthenticate.getCnonce(), wwwAuthenticate.getQop(), A2Digest));
        return serverResponse;
    }
}


SHA512256DigestAlgorithm.java

public class SHA512256DigestAlgorithm implements DigestAlgorithm{
    @Override
    public String result(WWWAuthenticate wwwAuthenticate) {
        String A1Digest = null;
        String A2Digest = null;
        Digester digester = DigestUtil.digester("sha-512/256");
        //SHA-512/256-sess
        if(StrUtil.containsIgnoreCase(wwwAuthenticate.getAlgorithm(),"sess")) {
            A1Digest = digester.digestHex(StrUtil.format("{}:{}:{}", digester.digestHex(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword())), wwwAuthenticate.getNonce(), wwwAuthenticate.getCnonce()));
            A2Digest = digester.digestHex(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));
        }else { //SHA-512/256
            A1Digest = digester.digestHex(StrUtil.format("{}:{}:{}", wwwAuthenticate.getUsername(), wwwAuthenticate.getRealm(), wwwAuthenticate.getPassword()));
            A2Digest = digester.digestHex(StrUtil.format("{}:{}", wwwAuthenticate.getRequestMethod(), wwwAuthenticate.getUri()));
        }
        String serverResponse =digester.digestHex(StrUtil.format("{}:{}:{}:{}:{}:{}", A1Digest, wwwAuthenticate.getNonce(), wwwAuthenticate.getNc(), wwwAuthenticate.getCnonce(), wwwAuthenticate.getQop(), A2Digest));
        return serverResponse;
    }
}


DigestAlgorithmUtil.java

public class DigestAlgorithmUtil {

    private final static ConcurrentHashMap<String, DigestAlgorithm> digestAlgorithmMap = new ConcurrentHashMap<>();

    /**
     * 计算摘要结果
     *
     * @param wwwAuthenticate
     * @return
     */
    @SneakyThrows
    public static String result(WWWAuthenticate wwwAuthenticate) {

        String packageName = ClassUtil.getPackage(DigestAlgorithmUtil.class);
        String algorithm = StrUtil.removeAll(StrUtil.split(wwwAuthenticate.getAlgorithm(), "-sess").get(0), StrUtil.DASHED);

        DigestAlgorithm digestAlgorithm = Optional.ofNullable(digestAlgorithmMap.get(algorithm))
                .orElseGet(() -> {
                    DigestAlgorithm currentDigestAlgorithm = null;
                    try {
                        Class<?> digestAlgorithmClass = Class.forName(StrUtil.format("{}.{}DigestAlgorithm", packageName, algorithm));
                        currentDigestAlgorithm = (DigestAlgorithm) ReflectUtil.newInstance(digestAlgorithmClass);
                        digestAlgorithmMap.put(algorithm, currentDigestAlgorithm);
                    } catch (ClassNotFoundException e) {
                    }
                    return currentDigestAlgorithm;
                });
        Assert.notNull(digestAlgorithm, "【{}】摘要算法找不到,请检查", algorithm);


        return digestAlgorithm.result(wwwAuthenticate);
    }

    @SneakyThrows
    public static void main(String[] args) {
        Class<?> md5DigestAlgorithm = Class.forName("MD5DigestAlgorithm");

        Console.log(md5DigestAlgorithm);

    }

}


WWWAuthenticate.java

@Data
public class WWWAuthenticate {
    String qop;
    String nc;
    String realm;
    String cnonce;
    String uri;
    String nonce;
    String algorithm;
    String username;
    String response;


    /**
     * 用于计算摘要
     */
    String password;
    String requestMethod;
}


ArticleCategoryController.java

@RestController
@RequestMapping("article-category")
public class ArticleCategoryController {
    @GetMapping("/one/{id}")
    public CommonHttpResult<ArticleCategory> findById(@PathVariable("id") String id) {
        return CommonHttpResult.success(articleCategoryService.getById(id));
    }
}
演示(Digest认证-postman)

理由: 为什么使用postman,因为只要输入账号、密码,发现第一个请求认证失败,返回的nonce等信息,postman自动帮你计算好摘要,然后重新发送,如果再次认证认证失败,则不会在帮你重新发送,所以postman最多帮你发送两遍。

在这里插入图片描述

发送错误的用户名或密码
在这里插入图片描述

在这里插入图片描述



发送正确的用户名或密码

在这里插入图片描述

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值