From 543613b5dd2b45bbf475f32adb109773122d50c6 Mon Sep 17 00:00:00 2001 From: click33 <2393584716@qq.com> Date: Thu, 18 Apr 2024 14:39:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Http=20Digest=20=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E6=A8=A1=E5=9D=97=E7=AE=80=E5=8D=95=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../satoken/annotation/SaCheckHttpDigest.java | 61 +++ .../dev33/satoken/config/SaTokenConfig.java | 26 +- .../cn/dev33/satoken/error/SaErrorCode.java | 3 + .../exception/NotHttpDigestAuthException.java | 41 +++ .../satoken/exception/SaTokenException.java | 20 +- .../httpauth/digest/SaHttpDigestModel.java | 346 ++++++++++++++++++ .../httpauth/digest/SaHttpDigestTemplate.java | 295 +++++++++++++++ .../httpauth/digest/SaHttpDigestUtil.java | 157 ++++++++ .../cn/dev33/satoken/strategy/SaStrategy.java | 7 + .../main/java/com/pj/test/TestController.java | 10 + 10 files changed, 963 insertions(+), 3 deletions(-) create mode 100644 sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpDigest.java create mode 100644 sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpDigestAuthException.java create mode 100644 sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestModel.java create mode 100644 sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java create mode 100644 sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpDigest.java b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpDigest.java new file mode 100644 index 00000000..aac407a6 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/annotation/SaCheckHttpDigest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.annotation; + +import cn.dev33.satoken.httpauth.digest.SaHttpDigestModel; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Http Digest 认证校验:只有通过 Http Digest 认证后才能进入该方法,否则抛出异常。 + * + *

可标注在方法、类上(效果等同于标注在此类的所有方法上) + * + * @author click33 + * @since 1.38.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SaCheckHttpDigest { + + /** + * 用户名 + * @return / + */ + String username() default ""; + + /** + * 密码 + * @return / + */ + String password() default ""; + + /** + * 领域 + * @return / + */ + String realm() default SaHttpDigestModel.DEFAULT_REALM; + + /** + * 需要校验的用户名和密码,格式形如 sa:123456 + * @return / + */ + String value() default ""; + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java index fa0d7379..2c6bcebb 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/config/SaTokenConfig.java @@ -148,10 +148,15 @@ public class SaTokenConfig implements Serializable { private String jwtSecretKey; /** - * Http Basic 认证的默认账号和密码 + * Http Basic 认证的默认账号和密码,冒号隔开,例如:sa:123456 */ private String basic = ""; + /** + * Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456 + */ + private String httpDigest = ""; + /** * 配置当前项目的网络访问地址 */ @@ -570,6 +575,22 @@ public class SaTokenConfig implements Serializable { return this; } + /** + * @return Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456 + */ + public String getHttpDigest() { + return httpDigest; + } + + /** + * @param httpDigest Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456 + * @return 对象自身 + */ + public SaTokenConfig setHttpDigest(String httpDigest) { + this.httpDigest = httpDigest; + return this; + } + /** * @return 配置当前项目的网络访问地址 */ @@ -676,7 +697,8 @@ public class SaTokenConfig implements Serializable { + ", logLevelInt=" + logLevelInt + ", isColorLog=" + isColorLog + ", jwtSecretKey=" + jwtSecretKey - + ", basic=" + basic + + ", basic=" + basic + + ", httpDigest=" + httpDigest + ", currDomain=" + currDomain + ", sameTokenTimeout=" + sameTokenTimeout + ", checkSameToken=" + checkSameToken diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java b/sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java index b61da557..a6e8cd8b 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/error/SaErrorCode.java @@ -60,6 +60,9 @@ public interface SaErrorCode { /** 表示未能通过 Http Basic 认证校验 */ int CODE_10311 = 10311; + /** 表示未能通过 Http Digest 认证校验 */ + int CODE_10312 = 10312; + /** 提供的 HttpMethod 是无效的 */ int CODE_10321 = 10321; diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpDigestAuthException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpDigestAuthException.java new file mode 100644 index 00000000..2dd8bf9c --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/NotHttpDigestAuthException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.exception; + +/** + * 一个异常:代表会话未能通过 Http Digest 认证校验 + * + * @author click33 + * @since 1.38.0 + */ +public class NotHttpDigestAuthException extends SaTokenException { + + /** + * 序列化版本号 + */ + private static final long serialVersionUID = 6806129545290130144L; + + /** 异常提示语 */ + public static final String BE_MESSAGE = "no http digest auth"; + + /** + * 一个异常:代表会话未通过 Http Digest 认证 + */ + public NotHttpDigestAuthException() { + super(BE_MESSAGE); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java index 9eefcd3d..3d342de1 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/exception/SaTokenException.java @@ -105,7 +105,16 @@ public class SaTokenException extends RuntimeException { this.code = code; return this; } - + + /** + * 断言 flag 不为 true,否则抛出 message 异常 + * @param flag 标记 + * @param message 异常信息 + */ + public static void notTrue(boolean flag, String message) { + notTrue(flag, message, SaErrorCode.CODE_UNDEFINED); + } + /** * 断言 flag 不为 true,否则抛出 message 异常 * @param flag 标记 @@ -118,6 +127,15 @@ public class SaTokenException extends RuntimeException { } } + /** + * 断言 value 不为空,否则抛出 message 异常 + * @param value 值 + * @param message 异常信息 + */ + public static void notEmpty(Object value, String message) { + notEmpty(value, message, SaErrorCode.CODE_UNDEFINED); + } + /** * 断言 value 不为空,否则抛出 message 异常 * @param value 值 diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestModel.java b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestModel.java new file mode 100644 index 00000000..7dfccb54 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestModel.java @@ -0,0 +1,346 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.httpauth.digest; + +/** + * Sa-Token Http Digest 认证 - 参数实体类 + * + * @author click33 + * @since 1.38.0 + */ +public class SaHttpDigestModel { + + /** + * 默认的 Realm 领域名称 + */ + public static final String DEFAULT_REALM = "Sa-Token"; + + /** + * 默认的 qop 值 + */ + public static final String DEFAULT_QOP = "auth"; + + + /** + * 用户名 + */ + public String username; + + /** + * 密码 + */ + public String password; + + /** + * 领域 + */ + public String realm = DEFAULT_REALM; + + /** + * 随机数 + */ + public String nonce; + + /** + * 请求 uri + */ + public String uri; + + /** + * 请求方法 + */ + public String method; + + /** + * 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + */ + public String qop; + + /** + * nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + */ + public String nc; + + /** + * 客户端随机数,由客户端提供 + */ + public String cnonce; + + /** + * opaque + */ + public String opaque; + + /** + * 请求摘要,最终计算的摘要结果 + */ + public String response; + + // ------------------- 构造函数 ------------------- + + public SaHttpDigestModel() { + } + public SaHttpDigestModel(String username, String password) { + this.username = username; + this.password = password; + } + public SaHttpDigestModel(String username, String password, String realm) { + this.username = username; + this.password = password; + this.realm = realm; + } + + + // ------------------- get/set ------------------- + + /** + * 获取 用户名 + * + * @return username 用户名 + */ + public String getUsername() { + return this.username; + } + + /** + * 设置 用户名 + * + * @param username 用户名 + * @return / + */ + public SaHttpDigestModel setUsername(String username) { + this.username = username; + return this; + } + + /** + * 获取 领域 + * + * @return realm 领域 + */ + public String getRealm() { + return this.realm; + } + + /** + * 设置 领域 + * + * @param realm 领域 + * @return / + */ + public SaHttpDigestModel setRealm(String realm) { + this.realm = realm; + return this; + } + + /** + * 获取 密码 + * + * @return password 密码 + */ + public String getPassword() { + return this.password; + } + + /** + * 设置 密码 + * + * @param password 密码 + * @return / + */ + public SaHttpDigestModel setPassword(String password) { + this.password = password; + return this; + } + + /** + * 获取 随机数 + * + * @return nonce 随机数 + */ + public String getNonce() { + return this.nonce; + } + + /** + * 设置 随机数 + * + * @param nonce 随机数 + * @return / + */ + public SaHttpDigestModel setNonce(String nonce) { + this.nonce = nonce; + return this; + } + + /** + * 获取 请求 uri + * + * @return uri 请求 uri + */ + public String getUri() { + return this.uri; + } + + /** + * 设置 请求 uri + * + * @param uri 请求 uri + * @return / + */ + public SaHttpDigestModel setUri(String uri) { + this.uri = uri; + return this; + } + + /** + * 获取 请求方法 + * + * @return method 请求方法 + */ + public String getMethod() { + return this.method; + } + + /** + * 设置 请求方法 + * + * @param method 请求方法 + * @return / + */ + public SaHttpDigestModel setMethod(String method) { + this.method = method; + return this; + } + + /** + * 获取 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + * + * @return qop 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + */ + public String getQop() { + return this.qop; + } + + /** + * 设置 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + * + * @param qop 保护质量(auth=默认的,auth-int=增加报文完整性检测),可以为空,但不推荐 + * @return / + */ + public SaHttpDigestModel setQop(String qop) { + this.qop = qop; + return this; + } + + /** + * 获取 nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + * + * @return nc nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + */ + public String getNc() { + return this.nc; + } + + /** + * 设置 nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + * + * @param nc nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量 + * @return / + */ + public SaHttpDigestModel setNc(String nc) { + this.nc = nc; + return this; + } + + /** + * 获取 客户端随机数,由客户端提供 + * + * @return cnonce 客户端随机数,由客户端提供 + */ + public String getCnonce() { + return this.cnonce; + } + + /** + * 设置 客户端随机数,由客户端提供 + * + * @param cnonce 客户端随机数,由客户端提供 + * @return / + */ + public SaHttpDigestModel setCnonce(String cnonce) { + this.cnonce = cnonce; + return this; + } + + /** + * 获取 opaque + * + * @return opaque opaque + */ + public String getOpaque() { + return this.opaque; + } + + /** + * 设置 opaque + * + * @param opaque opaque + * @return / + */ + public SaHttpDigestModel setOpaque(String opaque) { + this.opaque = opaque; + return this; + } + + /** + * 获取 请求摘要,最终计算的摘要结果 + * + * @return response 请求摘要,最终计算的摘要结果 + */ + public String getResponse() { + return this.response; + } + + /** + * 设置 请求摘要,最终计算的摘要结果 + * + * @param response 请求摘要,最终计算的摘要结果 + * @return / + */ + public SaHttpDigestModel setResponse(String response) { + this.response = response; + return this; + } + + @Override + public String toString() { + return "SaHttpDigestModel[" + + "username=" + username + + ", password=" + password + + ", realm=" + realm + + ", nonce=" + nonce + + ", uri=" + uri + + ", method=" + method + + ", qop=" + qop + + ", nc=" + nc + + ", cnonce=" + cnonce + + ", opaque=" + opaque + + ", response=" + response + + "]"; + } + +} \ No newline at end of file diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java new file mode 100644 index 00000000..f7ed69d3 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestTemplate.java @@ -0,0 +1,295 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.httpauth.digest; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.annotation.SaCheckHttpDigest; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.error.SaErrorCode; +import cn.dev33.satoken.exception.NotHttpDigestAuthException; +import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.secure.SaSecureUtil; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Sa-Token Http Digest 认证模块 - 模板方法类 + * + * @author click33 + * @since 1.38.0 + */ +public class SaHttpDigestTemplate { + + /* + 这里只是 Http Digest 认证的一个简单实现,待实现功能还有: + 1、nonce 防重放攻击 + 2、nc 计数器 + 3、qop 保护质量=auth-int + 4、opaque 透明值 + 5、algorithm 更多摘要算法 + 等等 + */ + + /** + * 构建认证失败的响应头参数 + * @param model 参数对象 + * @return 响应头值 + */ + public String buildResponseHeaderValue(SaHttpDigestModel model) { + // 抛异常 + String headerValue = "Digest " + + "realm=\"" + model.realm + "\", " + + "qop=\"" + model.qop + "\", " + + "nonce=\"" + model.nonce + "\", " + + "nc=" + model.nc + ", " + + "opaque=\"" + model.opaque + "\""; + return headerValue; + } + + /** + * 在校验失败时,设置响应头,并抛出异常 + * @param model Digest 参数对象 + */ + public void throwNotHttpDigestAuthException(SaHttpDigestModel model) { + // 补全一些必须的参数 + model.realm = (model.realm != null) ? model.realm : SaHttpDigestModel.DEFAULT_REALM; + model.qop = (model.qop != null) ? model.qop : SaHttpDigestModel.DEFAULT_QOP; + model.nonce = (model.nonce != null) ? model.nonce : SaFoxUtil.getRandomString(32); + model.opaque = (model.opaque != null) ? model.opaque : SaFoxUtil.getRandomString(32); + model.nc = (model.nc != null) ? model.nc : "00000001"; + + // 设置响应头 + SaHolder.getResponse() + .setStatus(401) + .setHeader("WWW-Authenticate", buildResponseHeaderValue(model)); + + // 抛异常 + throw new NotHttpDigestAuthException().setCode(SaErrorCode.CODE_10312); + } + + /** + * 获取浏览器提交的 Digest 参数 (裁剪掉前缀) + * @return 值 + */ + public String getAuthorizationValue() { + + // 获取前端提交的请求头 Authorization 参数 + String authorization = SaHolder.getRequest().getHeader("Authorization"); + + // 如果不是以 Digest 作为前缀,则视为无效 + if(authorization == null || ! authorization.startsWith("Digest ")) { + return null; + } + + // 裁剪前缀并解码 + return authorization.substring(7); + } + + /** + * 获取浏览器提交的 Digest 参数,并转化为 Map + * @return / + */ + public SaHttpDigestModel getAuthorizationValueToModel() { + + // 先获取字符串值 + String authorization = getAuthorizationValue(); + if(authorization == null) { +// throw new SaTokenException("请求头中未携带 Digest 认证参数"); + return null; + } + + // 根据逗号分割,解析为 Map + Map map = new LinkedHashMap<>(); + String[] arr = authorization.split(","); + for (String s : arr) { + String[] kv = s.split("="); + if (kv.length == 2) { + map.put(kv[0].trim(), kv[1].trim().replace("\"", "")); + } + } + + /* + 参考样例: + username=sa, + realm=Sa-Token, + nonce=dcd98b7102dd2f0e8b11d0f600bfb0c093, + uri=/test/testDigest, + response=a32023c128e142163dd4856a2f511c70, + opaque=5ccc069c403ebaf9f0171e9517f40e41, + qop=auth, + nc=00000002, + cnonce=f3ca6bfc0b2f59c4 + */ + + // 转化为 Model + SaHttpDigestModel model = new SaHttpDigestModel(); + model.username = map.get("username"); + model.realm = map.get("realm"); + model.nonce = map.get("nonce"); + model.uri = map.get("uri"); + model.method = SaHolder.getRequest().getMethod(); + model.qop = map.get("qop"); + model.nc = map.get("nc"); + model.cnonce = map.get("cnonce"); + model.opaque = map.get("opaque"); + model.response = map.get("response"); + + // + return model; + } + + /** + * 计算:根据 Digest 参数计算 response + * + * @param model Digest 参数对象 + * @return 计算出的 response + */ + public String calcResponse(SaHttpDigestModel model) { + + // frag1 = md5(username:realm:password) + String frag1 = SaSecureUtil.md5(model.username + ":" + model.realm + ":" + model.password); + + // frag2 = nonce:nc:cnonce:qop + String frag2 = model.nonce + ":" + model.nc + ":" + model.cnonce + ":" + model.qop; + + // frag3 = md5(method:uri) + String frag3 = SaSecureUtil.md5(model.method + ":" + model.uri); + + // 最终结果 = md5(frag1:frag2:frag3) + String response = SaSecureUtil.md5(frag1 + ":" + frag2 + ":" + frag3); + + // + return response; + } + + /** + * 把 hopeModel 有的值都 copy 到 reqModel 中 + */ + public void copyHopeToReq(SaHttpDigestModel hopeModel, SaHttpDigestModel reqModel){ + reqModel.username = hopeModel.username; + reqModel.password = hopeModel.password; + reqModel.realm = hopeModel.realm != null ? hopeModel.realm : reqModel.realm; + reqModel.nonce = hopeModel.nonce != null ? hopeModel.nonce : reqModel.nonce; + reqModel.uri = hopeModel.uri != null ? hopeModel.uri : reqModel.uri; + reqModel.method = hopeModel.method != null ? hopeModel.method : reqModel.method; + reqModel.qop = hopeModel.qop != null ? hopeModel.qop : reqModel.qop; + reqModel.nc = hopeModel.nc != null ? hopeModel.nc : reqModel.nc; + reqModel.opaque = hopeModel.opaque != null ? hopeModel.opaque : reqModel.opaque; + // reqModel.cnonce = hopeModel.cnonce != null ? hopeModel.cnonce : reqModel.cnonce; + // reqModel.response = hopeModel.response != null ? hopeModel.response : reqModel.response; + } + + // ---------- 校验 ---------- + + /** + * 校验:根据提供 Digest 参数计算 res,与 request 请求中的 Digest 参数进行校验,校验不通过则抛出异常 + * @param hopeModel 提供的 Digest 参数对象 + */ + public void check(SaHttpDigestModel hopeModel) { + + // 先进行一些必须的希望参数校验 + SaTokenException.notEmpty(hopeModel, "Digest参数对象不能为空"); + SaTokenException.notEmpty(hopeModel.username, "必须提供希望的 username 参数"); + SaTokenException.notEmpty(hopeModel.password, "必须提供希望的 password 参数"); + + // 获取 web 请求中的 Digest 参数 + SaHttpDigestModel reqModel = getAuthorizationValueToModel(); + + // 为空代表前端根本没有提交 Digest 参数,直接抛异常 + if(reqModel == null) { + throwNotHttpDigestAuthException(hopeModel); + } + + // 把 hopeModel 有的值都 copy 到 reqModel 中 + copyHopeToReq(hopeModel, reqModel); + + // 计算 + String cResponse = calcResponse(reqModel); + + // 比对,不一致就抛异常 + if(! cResponse.equals(reqModel.response)) { + throwNotHttpDigestAuthException(hopeModel); + } + + // 认证通过 + } + + /** + * 校验:根据提供的参数,校验不通过抛出异常 + * @param username 用户名 + * @param password 密码 + */ + public void check(String username, String password) { + check(new SaHttpDigestModel(username, password)); + } + + /** + * 校验:根据提供的参数,校验不通过抛出异常 + * @param username 用户名 + * @param password 密码 + * @param realm 领域 + */ + public void check(String username, String password, String realm) { + check(new SaHttpDigestModel(username, password, realm)); + } + + /** + * 校验:根据全局配置参数,校验不通过抛出异常 + */ + public void check() { + String httpDigest = SaManager.getConfig().getHttpDigest(); + if(SaFoxUtil.isEmpty(httpDigest)){ + throw new SaTokenException("未配置全局 Http Digest 认证参数"); + } + String[] arr = httpDigest.split(":"); + if(arr.length != 2){ + throw new SaTokenException("全局 Http Digest 认证参数配置错误,格式应如:username:password"); + } + check(arr[0], arr[1]); + } + + /** + * 根据注解 ( @SaCheckHttpDigest ) 鉴权 + * + * @param at 注解对象 + */ + public void checkByAnnotation(SaCheckHttpDigest at) { + + // 如果配置了 value,则以 value 优先 + String value = at.value(); + if(SaFoxUtil.isNotEmpty(value)){ + String[] arr = value.split(":"); + if(arr.length != 2){ + throw new SaTokenException("注解参数配置错误,格式应如:username:password"); + } + check(arr[0], arr[1]); + return; + } + + // 如果配置了 username,则分别获取参数 + String username = at.username(); + if(SaFoxUtil.isNotEmpty(username)){ + check(username, at.password(), at.realm()); + return; + } + + // 都没有配置,则根据全局配置参数进行校验 + check(); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java new file mode 100644 index 00000000..f94db809 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/httpauth/digest/SaHttpDigestUtil.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cn.dev33.satoken.httpauth.digest; + +import cn.dev33.satoken.annotation.SaCheckHttpDigest; +import cn.dev33.satoken.context.SaHolder; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Sa-Token Http Digest 认证模块,Util 工具类 + * + * @author click33 + * @since 1.38.0 + */ +public class SaHttpDigestUtil { + + private SaHttpDigestUtil() { + } + + /** + * 底层使用的 SaHttpDigestTemplate 对象 + */ + public static SaHttpDigestTemplate saHttpDigestTemplate = new SaHttpDigestTemplate(); + + + /** + * 获取浏览器提交的 Digest 参数 (裁剪掉前缀) + * @return 值 + */ + public static String getAuthorizationValue() { + + // 获取前端提交的请求头 Authorization 参数 + String authorization = SaHolder.getRequest().getHeader("Authorization"); + + // 如果不是以 Digest 作为前缀,则视为无效 + if(authorization == null || ! authorization.startsWith("Digest ")) { + return null; + } + + // 裁剪前缀并解码 + return authorization.substring(7); + } + + /** + * 获取浏览器提交的 Digest 参数,并转化为 Map + * @return / + */ + public static SaHttpDigestModel getAuthorizationValueToModel() { + + // 先获取字符串值 + String authorization = getAuthorizationValue(); + if(authorization == null) { + // throw new SaTokenException("请求头中未携带 Digest 认证参数"); + return null; + } + + // 根据逗号分割,解析为 Map + Map map = new LinkedHashMap<>(); + String[] arr = authorization.split(","); + for (String s : arr) { + String[] kv = s.split("="); + if (kv.length == 2) { + map.put(kv[0].trim(), kv[1].trim().replace("\"", "")); + } + } + + /* + 参考样例: + username=sa, + realm=Sa-Token, + nonce=dcd98b7102dd2f0e8b11d0f600bfb0c093, + uri=/test/testDigest, + response=a32023c128e142163dd4856a2f511c70, + opaque=5ccc069c403ebaf9f0171e9517f40e41, + qop=auth, + nc=00000002, + cnonce=f3ca6bfc0b2f59c4 + */ + + // 转化为 Model + SaHttpDigestModel model = new SaHttpDigestModel(); + model.username = map.get("username"); + model.realm = map.get("realm"); + model.nonce = map.get("nonce"); + model.uri = map.get("uri"); + model.method = SaHolder.getRequest().getMethod(); + model.qop = map.get("qop"); + model.nc = map.get("nc"); + model.cnonce = map.get("cnonce"); + model.opaque = map.get("opaque"); + model.response = map.get("response"); + + // + return model; + } + + // ---------- 校验 ---------- + + /** + * 校验:根据提供 Digest 参数计算 res,与 request 请求中的 Digest 参数进行校验,校验不通过则抛出异常 + * @param hopeModel 提供的 Digest 参数对象 + */ + public static void check(SaHttpDigestModel hopeModel) { + saHttpDigestTemplate.check(hopeModel); + } + + /** + * 校验:根据提供的参数,校验不通过抛出异常 + * @param username 用户名 + * @param password 密码 + */ + public static void check(String username, String password) { + saHttpDigestTemplate.check(username, password); + } + + /** + * 校验:根据提供的参数,校验不通过抛出异常 + * @param username 用户名 + * @param password 密码 + * @param realm 领域 + */ + public static void check(String username, String password, String realm) { + saHttpDigestTemplate.check(username, password, realm); + } + + /** + * 校验:根据全局配置参数,校验不通过抛出异常 + */ + public static void check() { + saHttpDigestTemplate.check(); + } + + /** + * 根据注解 ( @SaCheckHttpDigest ) 鉴权 + * + * @param at 注解对象 + */ + public static void checkByAnnotation(SaCheckHttpDigest at) { + saHttpDigestTemplate.checkByAnnotation(at); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java index 06115533..97949512 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/strategy/SaStrategy.java @@ -21,6 +21,7 @@ import cn.dev33.satoken.basic.SaBasicUtil; import cn.dev33.satoken.exception.RequestPathInvalidException; import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.fun.strategy.*; +import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil; import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.util.SaFoxUtil; @@ -185,6 +186,12 @@ public final class SaStrategy { SaBasicUtil.check(checkBasic.realm(), checkBasic.account()); } + // 校验 @SaCheckBasic 注解 + SaCheckHttpDigest checkHttpDigest = (SaCheckHttpDigest) SaStrategy.instance.getAnnotation.apply(element, SaCheckHttpDigest.class); + if(checkHttpDigest != null) { + SaHttpDigestUtil.checkByAnnotation(checkHttpDigest); + } + // 校验 @SaCheckOr 注解 SaCheckOr checkOr = (SaCheckOr) SaStrategy.instance.getAnnotation.apply(element, SaCheckOr.class); if(checkOr != null) { diff --git a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java index 800c0e31..32d5dec0 100644 --- a/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java +++ b/sa-token-demo/sa-token-demo-test/src/main/java/com/pj/test/TestController.java @@ -1,5 +1,6 @@ package com.pj.test; +import cn.dev33.satoken.annotation.SaCheckHttpDigest; import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.spring.SpringMVCUtil; import cn.dev33.satoken.stp.SaLoginConfig; @@ -52,4 +53,13 @@ public class TestController { return SaResult.ok(); } + // 测试 Http Digest 认证 浏览器访问: http://localhost:8081/test/testDigest + @SaCheckHttpDigest("sa:123456") + @RequestMapping("testDigest") + public SaResult testDigest() { + // SaHttpDigestUtil.check("sa", "123456"); + // 返回 + return SaResult.data(null); + } + }