mirror of
https://gitee.com/dromara/sa-token.git
synced 2025-05-03 12:17:57 +08:00
新增 Http Digest 认证模块简单实现
This commit is contained in:
parent
ccb79f6494
commit
543613b5dd
@ -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 认证后才能进入该方法,否则抛出异常。
|
||||
*
|
||||
* <p> 可标注在方法、类上(效果等同于标注在此类的所有方法上)
|
||||
*
|
||||
* @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 "";
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -60,6 +60,9 @@ public interface SaErrorCode {
|
||||
/** 表示未能通过 Http Basic 认证校验 */
|
||||
int CODE_10311 = 10311;
|
||||
|
||||
/** 表示未能通过 Http Digest 认证校验 */
|
||||
int CODE_10312 = 10312;
|
||||
|
||||
/** 提供的 HttpMethod 是无效的 */
|
||||
int CODE_10321 = 10321;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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 值
|
||||
|
@ -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 +
|
||||
"]";
|
||||
}
|
||||
|
||||
}
|
@ -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<String, String> 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();
|
||||
}
|
||||
|
||||
}
|
@ -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<String, String> 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);
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user