mirror of
https://gitee.com/dromara/sa-token.git
synced 2025-05-04 20:57:56 +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;
|
private String jwtSecretKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Http Basic 认证的默认账号和密码
|
* Http Basic 认证的默认账号和密码,冒号隔开,例如:sa:123456
|
||||||
*/
|
*/
|
||||||
private String basic = "";
|
private String basic = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Http Digest 认证的默认账号和密码,冒号隔开,例如:sa:123456
|
||||||
|
*/
|
||||||
|
private String httpDigest = "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置当前项目的网络访问地址
|
* 配置当前项目的网络访问地址
|
||||||
*/
|
*/
|
||||||
@ -570,6 +575,22 @@ public class SaTokenConfig implements Serializable {
|
|||||||
return this;
|
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 配置当前项目的网络访问地址
|
* @return 配置当前项目的网络访问地址
|
||||||
*/
|
*/
|
||||||
@ -677,6 +698,7 @@ public class SaTokenConfig implements Serializable {
|
|||||||
+ ", isColorLog=" + isColorLog
|
+ ", isColorLog=" + isColorLog
|
||||||
+ ", jwtSecretKey=" + jwtSecretKey
|
+ ", jwtSecretKey=" + jwtSecretKey
|
||||||
+ ", basic=" + basic
|
+ ", basic=" + basic
|
||||||
|
+ ", httpDigest=" + httpDigest
|
||||||
+ ", currDomain=" + currDomain
|
+ ", currDomain=" + currDomain
|
||||||
+ ", sameTokenTimeout=" + sameTokenTimeout
|
+ ", sameTokenTimeout=" + sameTokenTimeout
|
||||||
+ ", checkSameToken=" + checkSameToken
|
+ ", checkSameToken=" + checkSameToken
|
||||||
|
@ -60,6 +60,9 @@ public interface SaErrorCode {
|
|||||||
/** 表示未能通过 Http Basic 认证校验 */
|
/** 表示未能通过 Http Basic 认证校验 */
|
||||||
int CODE_10311 = 10311;
|
int CODE_10311 = 10311;
|
||||||
|
|
||||||
|
/** 表示未能通过 Http Digest 认证校验 */
|
||||||
|
int CODE_10312 = 10312;
|
||||||
|
|
||||||
/** 提供的 HttpMethod 是无效的 */
|
/** 提供的 HttpMethod 是无效的 */
|
||||||
int CODE_10321 = 10321;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -106,6 +106,15 @@ public class SaTokenException extends RuntimeException {
|
|||||||
return this;
|
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 异常
|
* 断言 flag 不为 true,否则抛出 message 异常
|
||||||
* @param flag 标记
|
* @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 异常
|
* 断言 value 不为空,否则抛出 message 异常
|
||||||
* @param value 值
|
* @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.RequestPathInvalidException;
|
||||||
import cn.dev33.satoken.exception.SaTokenException;
|
import cn.dev33.satoken.exception.SaTokenException;
|
||||||
import cn.dev33.satoken.fun.strategy.*;
|
import cn.dev33.satoken.fun.strategy.*;
|
||||||
|
import cn.dev33.satoken.httpauth.digest.SaHttpDigestUtil;
|
||||||
import cn.dev33.satoken.session.SaSession;
|
import cn.dev33.satoken.session.SaSession;
|
||||||
import cn.dev33.satoken.stp.StpLogic;
|
import cn.dev33.satoken.stp.StpLogic;
|
||||||
import cn.dev33.satoken.util.SaFoxUtil;
|
import cn.dev33.satoken.util.SaFoxUtil;
|
||||||
@ -185,6 +186,12 @@ public final class SaStrategy {
|
|||||||
SaBasicUtil.check(checkBasic.realm(), checkBasic.account());
|
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 注解
|
||||||
SaCheckOr checkOr = (SaCheckOr) SaStrategy.instance.getAnnotation.apply(element, SaCheckOr.class);
|
SaCheckOr checkOr = (SaCheckOr) SaStrategy.instance.getAnnotation.apply(element, SaCheckOr.class);
|
||||||
if(checkOr != null) {
|
if(checkOr != null) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.pj.test;
|
package com.pj.test;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckHttpDigest;
|
||||||
import cn.dev33.satoken.context.SaHolder;
|
import cn.dev33.satoken.context.SaHolder;
|
||||||
import cn.dev33.satoken.spring.SpringMVCUtil;
|
import cn.dev33.satoken.spring.SpringMVCUtil;
|
||||||
import cn.dev33.satoken.stp.SaLoginConfig;
|
import cn.dev33.satoken.stp.SaLoginConfig;
|
||||||
@ -52,4 +53,13 @@ public class TestController {
|
|||||||
return SaResult.ok();
|
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