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