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 网站对高度安全等级的追求标准。因此它的适用范围也有所受限。