BASIC认证


BASIC 认证(基本认证)是从 HTTP/1.0 就定义的认证方式。即便是现在仍有一部分的网站会使用这种认证方式。是 Web 服务器与通信客户端之间进行的认证方式。


核心步骤:

步骤 1:当请求的资源需要BASIC认证时,服务器会随状态码401Authorization Required,返回带WWW-Authenticate首部字段的响应。该字段内包含认证的方式(BASIC)及 Request-URI 安全域字符串(realm)。

步骤 2:接收到状态码401的客户端为了通过BASIC认证,需要将用户ID及密码发送给服务器。发送的字符串内容是由用户ID和密码构成,两者中间以冒号(:)连接后,再经过Base64 编码处理。

步骤 3:接收到包含首部字段 Authorization 请求的服务器,会对认证信息的正确性进行验证。如验证通过,则返回一条包含Request-URI资源的响应。

案例(ajax get请求数据):


@GetMapping("/applications")
public ResponseEntity findAllApplications(HttpServletResponse response, HttpServletRequest request) throws IOException {
    String sessionAuth = (String) request.getSession().getAttribute("auth");

    if(!checkHeaderAuth(request, response) && sessionAuth == null){
        response.setStatus(401);
        response.setHeader("Cache-Control", "no-store");
        response.setDateHeader("Expires", 0);
        response.setHeader("WWW-authenticate", "Basic Realm=\"input your usename and password\"");

        return new ResponseEntity<String>("", HttpStatus.UNAUTHORIZED);
    }

    List<Application> applications = applicationService.findAllApplications();
    ResponseData responseData = new ResponseData();
    Map<String, Object> data = new HashMap<String, Object>();
    data.put("total", applications.size());
    data.put("name", "rod chen");
    data.put("applications", applications);
    responseData.setResult("s");
    responseData.setData(data);
    return new ResponseEntity<String>(responseData.toJson(), HttpStatus.OK);
}

private boolean checkHeaderAuth(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String auth = request.getHeader("Authorization");

    if ((auth != null) && (auth.length() > 6)) {
        auth = auth.substring(6, auth.length());
        String decodedAuth = getFromBASE64(auth);
        String[] authValueArray = decodedAuth.split(":");

        if (authValueArray.length == 2) {
            String userName = decodedAuth.split(":")[0];
            String password = decodedAuth.split(":")[1];

            if (userName.equals("rodchen") && password.equals("abc123_")) {
                request.getSession().setAttribute("auth", decodedAuth);

                return true;
            }

            return false;
        }

        return false;
    }else{
        return false;
    }
}

private String getFromBASE64(String s) {
    if (s == null)
        return null;
    BASE64Decoder decoder = new BASE64Decoder();
    try {
        byte[] b = decoder.decodeBuffer(s);
        return new String(b);
    } catch (Exception e) {
        return null;
    }
}

当第一次请求的时候,不会弹出输入框。弹出框的部分内容是我们server设置的内容(chorme看不到这个内容,不知道是不是chorme处理了,我是在firefox测试的),对应代码:

response.setHeader("WWW-authenticate", "Basic Realm=\"input your usename and password\"");


这个时候呢,server已经返回了401,可惜的是在浏览器的network看不到,直到我们完成了下一步(也就是,输入内容,不管对错,或者直接关闭当前弹出框)才可以看到401的返回。对于server的代码,我是使用session来存储当前的验证。如果没有验证通过就一直提示验证框。


缺点:


BASIC 认证虽然采用 Base64 编码方式,但这不是加密处理。不需要何附加信息即可对其解码。换言之,由于明文解码后就是用户 ID和密码,在HTTP 等非加密通信的线路上进行 BASIC 认证的过程中,如果被人窃听,被盗的可能性极高。


另外,除此之外想再进行一次 BASIC 认证时,一般的浏览器却无法实现认证注销操作,这也是问题之一。BASIC 认证使用上不够便捷灵活,且达不到多数 Web 网站期望的安全性等级,因此它并不常用。

DIGEST 认证


为弥补 BASIC 认证存在的弱点,从 HTTP/1.1 起就有了 DIGEST 认证。 DIGEST 认证同样使用质询 / 响应的方式(challenge/response),但不会像 BASIC 认证那样直接发送明文密码。

所谓质询响应方式是指,一开始一方会先发送认证要求给另一方,接着使用从另一方那接收到的质询码计算生成响应码。最后将响应码返回给对方进行认证的方式。


核心步骤:


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


HTTP/1.1 401
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-store
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
X-Frame-Options: DENY
Access-Control-Allow-Origin: *
Access-Control-Allow-Method: *
Access-Control-Allow-Headers: *
X-Application-Context: application:pro:8080
Set-Cookie: JSESSIONID=730A19CB0CA2D68F62F7576B0FCEFCF1; Path=/; HttpOnly
WWW-authenticate: Digest Realm="test", nonce="N6yEOiDGTvOx9hwloHW7AQ==", qop="auth"
Content-Type: application/json;charset=UTF-8
Content-Length: 0
Date: Wed, 25 Apr 2018 05:48:32 GMT

步骤 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 运算后的密码字符串,形成响应码。

GET http://localhost:8080/portal/applications HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Authorization: Digest username="q", realm="test", nonce="N6yEOiDGTvOx9hwloHW7AQ==", uri="/portal/applications", response="bc3662d7309bdf68b5f6684647bd17e2", qop=auth, nc=00000001, cnonce="04fefcb40dae7db4"
Accept: application/json, text/plain, */*
x-location: 0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
token: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBdWdtZW50dW0iLCJyb2xlcyI6IkFETUlOIiwiZXhwIjoxNTI3MjE2ODA1fQ.UFoABgVLAepCN_sZYOdsnEMBmRHSZ-MpBp3J6Lo-gIhHWOVdNg48bR2_8-1Aw4sZJoXg8tLSAD0tj8L0vR07MQ
Referer: http://localhost:8080/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=730A19CB0CA2D68F62F7576B0FCEFCF1

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

案例(ajax get请求数据):



//Auth Class
public class Auth {
    public String Username;
    public String Realm;
    public String Nonce;
    public String Url;
    public String Response;
    public String Qop;
    public String Nc;
    public String Cnonce;

    public String getUsername() {
        return Username;
    }

    public void setUsername(String username) {
        Username = username;
    }

    public String getRealm() {
        return Realm;
    }

    public void setRealm(String realm) {
        Realm = realm;
    }

    public String getNonce() {
        return Nonce;
    }

    public void setNonce(String nonce) {
        Nonce = nonce;
    }

    public String getUrl() {
        return Url;
    }

    public void setUrl(String url) {
        Url = url;
    }

    public String getResponse() {
        return Response;
    }

    public void setResponse(String response) {
        Response = response;
    }

    public String getQop() {
        return Qop;
    }

    public void setQop(String qop) {
        Qop = qop;
    }

    public String getNc() {
        return Nc;
    }

    public void setNc(String nc) {
        Nc = nc;
    }

    public String getCnonce() {
        return Cnonce;
    }

    public void setCnonce(String cnonce) {
        Cnonce = cnonce;
    }
}
//md5
public class TokenUtils {
    private static TokenUtils ourInstance = new TokenUtils();

    public static TokenUtils getInstance() {
        return ourInstance;
    }

    public TokenUtils() {
    }

    public 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();
        }
    }

    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();
    }
}
//Api
@GetMapping("/applications")
public ResponseEntity findAllApplications(HttpServletResponse response, HttpServletRequest request) throws IOException {
    String sessionAuth = (String) request.getSession().getAttribute("auth");

    if(!checkHeaderAuth(request, response) && sessionAuth == null){
        response.setStatus(401);
        response.setHeader("Cache-Control", "no-store");
        response.setDateHeader("Expires", 0);
        response.setHeader("WWW-authenticate", "Digest Realm=\"test\", nonce=\""+ new TokenUtils().generateToken() +"\", qop=\"auth\"");

        return new ResponseEntity<String>("", HttpStatus.UNAUTHORIZED);
    }

    List<Application> applications = applicationService.findAllApplications();
    ResponseData responseData = new ResponseData();
    Map<String, Object> data = new HashMap<String, Object>();
    data.put("total", applications.size());
    data.put("name", "rod chen");
    data.put("applications", applications);
    responseData.setResult("s");
    responseData.setData(data);
    return new ResponseEntity<String>(responseData.toJson(), HttpStatus.OK);
}

private boolean checkHeaderAuth(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String auth = request.getHeader("Authorization");

    if ((auth != null) && (auth.length() > 6)) {
        Auth authObject = new Auth();
        String[] authArray = new String[7];
        authArray = auth.split(",");
        for (int i = 0; i < authArray.length; i++) {
            if (authArray[i].indexOf(" ") > 0) {
                authArray[i] = authArray[i].split(" ")[1];
            }
            String[] authItem = authArray[i].replaceFirst("=", ":").split(":");
            authItem[1] = authItem[1].replaceAll("\"", "");
            switch (authItem[0].trim().toString()) {
                case "username":
                    authObject.setUsername(authItem[1]);
                    break;
                case "realm":
                    authObject.setRealm(authItem[1]);
                    break;
                case "nonce":
                    authObject.setNonce(authItem[1]);
                    break;
                case "uri":
                    authObject.setUrl(authItem[1]);
                    break;
                case "response":
                    authObject.setResponse(authItem[1]);
                    break;
                case "qop":
                    authObject.setQop(authItem[1]);
                    break;
                case "nc":
                    authObject.setNc(authItem[1]);
                    break;
                case "cnonce":
                    authObject.setCnonce(authItem[1]);
                    break;
            }
        }

        String HA1 = TokenUtils.MD5(authObject.getUsername() + ":" + authObject.getRealm() + ":q");
        String HD = String.format(authObject.getNonce()+":"+authObject.getNc()+":"+authObject.getCnonce()+":"+authObject.getQop());
        String HA2 = TokenUtils.MD5("GET:"+authObject.getUrl());
        String responseValid = TokenUtils.MD5(HA1 + ":" + HD + ":" + HA2);

        if (responseValid.equals(authObject.getResponse())) {
            return true;
        }
        return false;
    }else{
        return false;
    }
}

private String getFromBASE64(String s) {
    if (s == null)
        return null;
    BASE64Decoder decoder = new BASE64Decoder();
    try {
        byte[] b = decoder.decodeBuffer(s);
        return new String(b);
    } catch (Exception e) {
        return null;
    }
}

认证response的算法

从客户端返回的header值为(举例):

Digest username="q", realm="test", nonce="T53sV+xXH3FrrER4YZwpFQ==", uri="/portal/applications", response="f80492644b0700b404f2fb3f4d62861e", qop=auth, nc=00000001, cnonce="25c980f9f95fd544"

公式为(这里是只是写了我当前用的模式,其实还有其他的规则,但是最常用的应该就是这一种):

response = MD5(MD5(username:realm:password):nonce:nc:cnonce:qop:MD5(<request-method>:url))

对照上面的结果就是:

MD5(MD5(q:test:q):T53sV+xXH3FrrER4YZwpFQ==:00000001:25c980f9f95fd544:auth:MD5(GET:/portal/applications))

对比Basic认证

DIGEST 认证提供了高于 BASIC 认证的安全等级,但是和 HTTPS 的客户端认证相比仍旧很弱。DIGEST 认证提供防止密码被窃听的保护机制,但并不存在防止用户伪装的保护机制。DIGEST 认证和 BASIC 认证一样,使用上不那么便捷灵活,且仍达不到多数 Web 网站对高度安全等级的追求标准。因此它的适用范围也有所受限。