浏览器弹出这个原生的对话框,想必大家都不陌生,就是 HTTP Baisc 认证的机制。
这是浏览器自带的,遵循 RFC2617/7617 协议。但必须指出的是,遇到这界面,不一定是 Basic Authentication,也可能是 Digest Authentication。关于浏览器自带的认证,简单说有以下版本:
可參照 MDN - HTTP authentication 了解更多。
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 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("");out.println("Servlet HttpDigestAuth ");out.println("");out.println("");out.println("已通过 HttpDigestAuth 认证 at" + req.getContextPath() + "
");out.println("");out.println("