1
0
mirror of synced 2026-05-22 14:43:15 +00:00

refactor: API Key 模块拆分独立插件包:sa-token-apikey

This commit is contained in:
click33
2025-05-15 05:14:59 +08:00
parent 3edac001ce
commit 44c153fd19
30 changed files with 361 additions and 151 deletions
+1
View File
@@ -33,6 +33,7 @@
<module>sa-token-jwt</module>
<module>sa-token-sso</module>
<module>sa-token-oauth2</module>
<module>sa-token-apikey</module>
<module>sa-token-redisson</module>
<module>sa-token-redisx</module>
<module>sa-token-serializer-features</module>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sa-token-plugin</artifactId>
<groupId>cn.dev33</groupId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sa-token-apikey</artifactId>
<dependencies>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,89 @@
/*
* 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.apikey;
import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoader;
import cn.dev33.satoken.apikey.loader.SaApiKeyDataLoaderDefaultImpl;
import cn.dev33.satoken.apikey.config.SaApiKeyConfig;
import cn.dev33.satoken.apikey.template.SaApiKeyTemplate;
import cn.dev33.satoken.listener.SaTokenEventCenter;
/**
* 管理 Sa-Token API Key 所有全局组件
*
* @author click33
* @since 1.43.0
*/
public class SaApiKeyManager {
/**
* API Key 配置 Bean
*/
private static volatile SaApiKeyConfig config;
public static SaApiKeyConfig getConfig() {
if (config == null) {
// 初始化默认值
synchronized (SaApiKeyManager.class) {
if (config == null) {
setConfig(new SaApiKeyConfig());
}
}
}
return config;
}
public static void setConfig(SaApiKeyConfig config) {
SaApiKeyManager.config = config;
}
/**
* ApiKey 数据加载器
*/
private volatile static SaApiKeyDataLoader apiKeyDataLoader;
public static void setSaApiKeyDataLoader(SaApiKeyDataLoader apiKeyDataLoader) {
SaApiKeyManager.apiKeyDataLoader = apiKeyDataLoader;
SaTokenEventCenter.doRegisterComponent("SaApiKeyDataLoader", apiKeyDataLoader);
}
public static SaApiKeyDataLoader getSaApiKeyDataLoader() {
if (apiKeyDataLoader == null) {
synchronized (SaApiKeyManager.class) {
if (apiKeyDataLoader == null) {
SaApiKeyManager.apiKeyDataLoader = new SaApiKeyDataLoaderDefaultImpl();
}
}
}
return apiKeyDataLoader;
}
/**
* ApiKey 操作类
*/
private volatile static SaApiKeyTemplate apiKeyTemplate;
public static void setSaApiKeyTemplate(SaApiKeyTemplate apiKeyTemplate) {
SaApiKeyManager.apiKeyTemplate = apiKeyTemplate;
SaTokenEventCenter.doRegisterComponent("SaApiKeyTemplate", apiKeyTemplate);
}
public static SaApiKeyTemplate getSaApiKeyTemplate() {
if (apiKeyTemplate == null) {
synchronized (SaApiKeyManager.class) {
if (apiKeyTemplate == null) {
SaApiKeyManager.apiKeyTemplate = new SaApiKeyTemplate();
}
}
}
return apiKeyTemplate;
}
}
@@ -0,0 +1,51 @@
/*
* 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.apikey.annotation;
import cn.dev33.satoken.annotation.SaMode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* API Key 校验:指定请求中必须包含有效的 ApiKey ,并且包含指定的 scope
*
* <p> 可标注在方法、类上(效果等同于标注在此类的所有方法上)
*
* @author click33
* @since 1.42.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface SaCheckApiKey {
/**
* 指定 API key 必须包含的权限 [ 数组 ]
*
* @return /
*/
String [] scope() default {};
/**
* 验证模式:AND | OR,默认AND
*
* @return /
*/
SaMode mode() default SaMode.AND;
}
@@ -0,0 +1,54 @@
/*
* 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.apikey.annotation.handle;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.annotation.handler.SaAnnotationHandlerInterface;
import cn.dev33.satoken.apikey.annotation.SaCheckApiKey;
import cn.dev33.satoken.apikey.template.SaApiKeyUtil;
import cn.dev33.satoken.context.SaHolder;
import java.lang.reflect.AnnotatedElement;
/**
* 注解 SaCheckApiKey 的处理器
*
* @author click33
* @since 1.42.0
*/
public class SaCheckApiKeyHandler implements SaAnnotationHandlerInterface<SaCheckApiKey> {
@Override
public Class<SaCheckApiKey> getHandlerAnnotationClass() {
return SaCheckApiKey.class;
}
@Override
public void checkMethod(SaCheckApiKey at, AnnotatedElement element) {
_checkMethod(at.scope(), at.mode());
}
public static void _checkMethod(String[] scope, SaMode mode) {
String apiKey = SaApiKeyUtil.readApiKeyValue(SaHolder.getRequest());
if(mode == SaMode.AND) {
SaApiKeyUtil.checkApiKeyScope(apiKey, scope);
} else {
SaApiKeyUtil.checkApiKeyScopeOr(apiKey, scope);
}
}
}
@@ -0,0 +1,110 @@
/*
* 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.apikey.config;
/**
* Sa-Token API Key 相关配置
*
* @author click33
* @since 1.42.0
*/
public class SaApiKeyConfig {
/**
* API Key 前缀
*/
private String prefix = "AK-";
/**
* API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key)
*/
private long timeout = 2592000;
/**
* 框架是否记录索引信息
*/
private Boolean isRecordIndex = true;
/**
* 获取 API Key 前缀
*
* @return /
*/
public String getPrefix() {
return this.prefix;
}
/**
* 设置 API Key 前缀
*
* @param prefix /
* @return 对象自身
*/
public SaApiKeyConfig setPrefix(String prefix) {
this.prefix = prefix;
return this;
}
/**
* 获取 API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key)
*
* @return /
*/
public long getTimeout() {
return this.timeout;
}
/**
* 设置 API Key 有效期,-1=永久有效,默认30天 (修改此配置项不会影响到已创建的 API Key)
*
* @param timeout /
* @return 对象自身
*/
public SaApiKeyConfig setTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
* 获取 框架是否保存索引信息
*
* @return /
*/
public Boolean getIsRecordIndex() {
return this.isRecordIndex;
}
/**
* 设置 框架是否保存索引信息
*
* @param isRecordIndex /
* @return 对象自身
*/
public SaApiKeyConfig setIsRecordIndex(Boolean isRecordIndex) {
this.isRecordIndex = isRecordIndex;
return this;
}
@Override
public String toString() {
return "SaApiKeyConfig{" +
"prefix='" + prefix + '\'' +
", timeout=" + timeout +
", isRecordIndex=" + isRecordIndex +
'}';
}
}
@@ -0,0 +1,49 @@
/*
* 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.apikey.loader;
import cn.dev33.satoken.apikey.SaApiKeyManager;
import cn.dev33.satoken.apikey.model.ApiKeyModel;
/**
* ApiKey 数据加载器
*
* @author click33
* @since 1.42.0
*/
public interface SaApiKeyDataLoader {
/**
* 获取:框架是否保存索引信息
*
* @return /
*/
default Boolean getIsRecordIndex() {
return SaApiKeyManager.getConfig().getIsRecordIndex();
}
/**
* 根据 apiKey 从数据库获取 ApiKeyModel 信息 (实现此方法无需为数据做缓存处理,框架内部已包含缓存逻辑)
*
* @param namespace /
* @param apiKey /
* @return ApiKeyModel
*/
default ApiKeyModel getApiKeyModelFromDatabase(String namespace, String apiKey) {
return null;
}
}
@@ -0,0 +1,28 @@
/*
* 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.apikey.loader;
/**
* ApiKey 数据加载器 默认实现类
*
* @author click33
* @since 1.42.0
*/
public class SaApiKeyDataLoaderDefaultImpl implements SaApiKeyDataLoader {
// be empty of
}
@@ -0,0 +1,380 @@
/*
* 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.apikey.model;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.error.SaErrorCode;
import cn.dev33.satoken.exception.ApiKeyException;
import cn.dev33.satoken.util.SaFoxUtil;
import java.io.Serializable;
import java.util.*;
/**
* Model: API Key
*
* @author click33
* @since 1.41.0
*/
public class ApiKeyModel implements Serializable {
private static final long serialVersionUID = -6541180061782004705L;
/**
* 名称
*/
private String title;
/**
* 介绍
*/
private String intro;
/**
* ApiKey 值
*/
private String apiKey;
/**
* 账号 id
*/
private Object loginId;
/**
* ApiKey 创建时间,13位时间戳
*/
private long createTime;
/**
* ApiKey 到期时间,13位时间戳 (-1=永不过期)
*/
private long expiresTime;
/**
* 是否有效 (true=生效, false=禁用)
*/
private Boolean isValid = true;
/**
* 授权范围
*/
private List<String> scopes = new ArrayList<>();
/**
* 扩展数据
*/
private Map<String, Object> extraData;
/**
* 构造函数
*/
public ApiKeyModel() {
this.createTime = System.currentTimeMillis();
}
// method
/**
* 添加 Scope
* @param scope /
* @return /
*/
public ApiKeyModel addScope(String ...scope) {
if (this.scopes == null) {
this.scopes = new ArrayList<>();
}
this.scopes.addAll(Arrays.asList(scope));
return this;
}
/**
* 添加 扩展数据
* @param key /
* @param value /
* @return /
*/
public ApiKeyModel addExtra(String key, Object value) {
if (this.extraData == null) {
this.extraData = new LinkedHashMap<>();
}
this.extraData.put(key, value);
return this;
}
/**
* 查询扩展数据
*/
public Object getExtra(String key) {
if (this.extraData == null) {
return null;
}
return this.extraData.get(key);
}
/**
* 删除扩展数据
*/
public Object removeExtra(String key) {
if (this.extraData == null) {
return null;
}
return this.extraData.remove(key);
}
/**
* 数据自检,判断是否可以保存入库
*/
public void checkByCanSaved() {
if (SaFoxUtil.isEmpty(this.apiKey)) {
throw new ApiKeyException("ApiKey 值不可为空").setApiKey(apiKey).setCode(SaErrorCode.CODE_12304);
}
if (this.loginId == null) {
throw new ApiKeyException("无效 ApiKey: " + apiKey).setApiKey(apiKey).setCode(SaErrorCode.CODE_12304);
}
if (this.createTime == 0) {
throw new ApiKeyException("请指定 createTime 创建时间").setApiKey(apiKey).setCode(SaErrorCode.CODE_12304);
}
if (this.expiresTime == 0) {
throw new ApiKeyException("请指定 expiresTime 过期时间").setApiKey(apiKey).setCode(SaErrorCode.CODE_12304);
}
if (this.isValid == null) {
throw new ApiKeyException("请指定 isValid 是否生效").setApiKey(apiKey).setCode(SaErrorCode.CODE_12304);
}
}
/**
* 获取:此 ApiKey 的剩余有效期(秒), -1=永不过期
* @return /
*/
public long expiresIn() {
if (expiresTime == SaTokenDao.NEVER_EXPIRE) {
return SaTokenDao.NEVER_EXPIRE;
}
long s = (expiresTime - System.currentTimeMillis()) / 1000;
return s < 1 ? -2 : s;
}
/**
* 判断:此 ApiKey 是否已超时
* @return /
*/
public boolean timeExpired() {
if (expiresTime == SaTokenDao.NEVER_EXPIRE) {
return false;
}
return System.currentTimeMillis() > expiresTime;
}
// get and set
/**
* 获取 名称
*
* @return title 名称
*/
public String getTitle() {
return this.title;
}
/**
* 设置 名称
*
* @param title 名称
* @return 对象自身
*/
public ApiKeyModel setTitle(String title) {
this.title = title;
return this;
}
/**
* 获取 介绍
*
* @return intro 介绍
*/
public String getIntro() {
return this.intro;
}
/**
* 设置 介绍
*
* @param intro 介绍
* @return 对象自身
*/
public ApiKeyModel setIntro(String intro) {
this.intro = intro;
return this;
}
/**
* 获取 ApiKey 值
*
* @return apiKey ApiKey 值
*/
public String getApiKey() {
return this.apiKey;
}
/**
* 设置 ApiKey 值
*
* @param apiKey ApiKey 值
* @return 对象自身
*/
public ApiKeyModel setApiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
/**
* 获取 账号 id
*
* @return loginId 账号 id
*/
public Object getLoginId() {
return this.loginId;
}
/**
* 设置 账号 id
*
* @param loginId 账号 id
* @return 对象自身
*/
public ApiKeyModel setLoginId(Object loginId) {
this.loginId = loginId;
return this;
}
/**
* 获取 ApiKey 创建时间,13位时间戳
*
* @return createTime ApiKey 创建时间,13位时间戳
*/
public long getCreateTime() {
return this.createTime;
}
/**
* 设置 ApiKey 创建时间,13位时间戳
*
* @param createTime ApiKey 创建时间,13位时间戳
* @return 对象自身
*/
public ApiKeyModel setCreateTime(long createTime) {
this.createTime = createTime;
return this;
}
/**
* 获取 ApiKey 到期时间,13位时间戳 (-1=永不过期)
*
* @return expiresTime ApiKey 到期时间,13位时间戳 (-1=永不过期)
*/
public long getExpiresTime() {
return this.expiresTime;
}
/**
* 设置 ApiKey 到期时间,13位时间戳 (-1=永不过期)
*
* @param expiresTime ApiKey 到期时间,13位时间戳 (-1=永不过期)
* @return 对象自身
*/
public ApiKeyModel setExpiresTime(long expiresTime) {
this.expiresTime = expiresTime;
return this;
}
/**
* 获取 是否有效 (true=生效 false=禁用)
*
* @return /
*/
public Boolean getIsValid() {
return this.isValid;
}
/**
* 设置 是否有效 (true=生效 false=禁用)
*
* @param isValid /
* @return 对象自身
*/
public ApiKeyModel setIsValid(Boolean isValid) {
this.isValid = isValid;
return this;
}
/**
* 获取 授权范围
*
* @return scopes 授权范围
*/
public List<String> getScopes() {
return this.scopes;
}
/**
* 设置 授权范围
*
* @param scopes 授权范围
* @return 对象自身
*/
public ApiKeyModel setScopes(List<String> scopes) {
this.scopes = scopes;
return this;
}
/**
* 获取 扩展数据
*
* @return extraData 扩展数据
*/
public Map<String, Object> getExtraData() {
return this.extraData;
}
/**
* 设置 扩展数据
*
* @param extraData 扩展数据
* @return 对象自身
*/
public ApiKeyModel setExtraData(Map<String, Object> extraData) {
this.extraData = extraData;
return this;
}
@Override
public String toString() {
return "ApiKeyModel{" +
"title='" + title +
", intro='" + intro +
", apiKey='" + apiKey +
", loginId=" + loginId +
", createTime=" + createTime +
", expiresTime=" + expiresTime +
", isValid=" + isValid +
", scopes=" + scopes +
", extraData=" + extraData +
'}';
}
}
@@ -0,0 +1,560 @@
/*
* 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.apikey.template;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.apikey.SaApiKeyManager;
import cn.dev33.satoken.apikey.model.ApiKeyModel;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.error.SaErrorCode;
import cn.dev33.satoken.exception.ApiKeyException;
import cn.dev33.satoken.exception.ApiKeyScopeException;
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.session.raw.SaRawSessionDelegator;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import java.util.ArrayList;
import java.util.List;
/**
* API Key 操作类
*
* @author click33
* @since 1.42.0
*/
public class SaApiKeyTemplate {
/**
*默认命名空间
*/
public static final String DEFAULT_NAMESPACE = "apikey";
/**
* 命名空间
*/
public String namespace;
/**
* Raw Session 读写委托
*/
public SaRawSessionDelegator rawSessionDelegator;
/**
* 在 raw-session 中的保存索引列表使用的 key
*/
public static final String API_KEY_LIST = "__HD_API_KEY_LIST";
public SaApiKeyTemplate(){
this(DEFAULT_NAMESPACE);
}
/**
* 实例化
* @param namespace 命名空间,用于多实例隔离
*/
public SaApiKeyTemplate(String namespace){
if(SaFoxUtil.isEmpty(namespace)) {
throw new ApiKeyException("namespace 不能为空");
}
this.namespace = namespace;
this.rawSessionDelegator = new SaRawSessionDelegator(namespace);
}
// ------------------- ApiKey
/**
* 根据 apiKey 从 Cache 获取 ApiKeyModel 信息
* @param apiKey /
* @return /
*/
public ApiKeyModel getApiKeyModelFromCache(String apiKey) {
return getSaTokenDao().getObject(splicingApiKeySaveKey(apiKey), ApiKeyModel.class);
}
/**
* 根据 apiKey 从 Database 获取 ApiKeyModel 信息
* @param apiKey /
* @return /
*/
public ApiKeyModel getApiKeyModelFromDatabase(String apiKey) {
return SaApiKeyManager.getSaApiKeyDataLoader().getApiKeyModelFromDatabase(namespace, apiKey);
}
/**
* 获取 ApiKeyModel,无效的 ApiKey 会返回 null
* @param apiKey /
* @return /
*/
public ApiKeyModel getApiKey(String apiKey) {
if(apiKey == null) {
return null;
}
// 先从缓存中获取,缓存中找不到就尝试从数据库获取
ApiKeyModel apiKeyModel = getApiKeyModelFromCache(apiKey);
if(apiKeyModel == null) {
apiKeyModel = getApiKeyModelFromDatabase(apiKey);
saveApiKey(apiKeyModel);
}
return apiKeyModel;
}
/**
* 校验 ApiKey,成功返回 ApiKeyModel,失败则抛出异常
* @param apiKey /
* @return /
*/
public ApiKeyModel checkApiKey(String apiKey) {
ApiKeyModel ak = getApiKey(apiKey);
if(ak == null) {
throw new ApiKeyException("无效 API Key: " + apiKey).setApiKey(apiKey).setCode(SaErrorCode.CODE_12301);
}
if(ak.timeExpired()) {
throw new ApiKeyException("API Key 已过期: " + apiKey).setApiKey(apiKey).setCode(SaErrorCode.CODE_12302);
}
if(! ak.getIsValid()) {
throw new ApiKeyException("API Key 已被禁用: " + apiKey).setApiKey(apiKey).setCode(SaErrorCode.CODE_12303);
}
return ak;
}
/**
* 持久化:ApiKeyModel
* @param ak /
*/
public void saveApiKey(ApiKeyModel ak) {
if(ak == null) {
return;
}
// 数据自检
ak.checkByCanSaved();
// 保存 ApiKeyModel
String saveKey = splicingApiKeySaveKey(ak.getApiKey());
if(ak.timeExpired()) {
getSaTokenDao().deleteObject(saveKey);
} else {
getSaTokenDao().setObject(saveKey, ak, ak.expiresIn());
}
// 记录索引
if (getIsRecordIndex()) {
// 添加索引
SaSession session = rawSessionDelegator.getSessionById(ak.getLoginId());
ArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);
if(! apiKeyList.contains(ak.getApiKey())) {
apiKeyList.add(ak.getApiKey());
session.set(API_KEY_LIST, apiKeyList);
}
// 调整 ttl
adjustIndex(ak.getLoginId(), session);
}
}
/**
* 获取 ApiKey 所代表的 LoginId
* @param apiKey ApiKey
* @return LoginId
*/
public Object getLoginIdByApiKey(String apiKey) {
return checkApiKey(apiKey).getLoginId();
}
/**
* 删除 ApiKey
* @param apiKey ApiKey
*/
public void deleteApiKey(String apiKey) {
// 删 ApiKeyModel
ApiKeyModel ak = getApiKeyModelFromCache(apiKey);
if(ak == null) {
return;
}
getSaTokenDao().deleteObject(splicingApiKeySaveKey(apiKey));
// 删索引
if(getIsRecordIndex()) {
// RawSession 中不存在,提前退出
SaSession session = rawSessionDelegator.getSessionById(ak.getLoginId(), false);
if(session == null) {
return;
}
// 索引无记录,提前退出
ArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);
if(! apiKeyList.contains(apiKey)) {
return;
}
// 如果只有一个 ApiKey,则整个 RawSession 删掉
if (apiKeyList.size() == 1) {
rawSessionDelegator.deleteSessionById(ak.getLoginId());
} else {
// 否则移除此 ApiKey 并保存
apiKeyList.remove(apiKey);
session.set(API_KEY_LIST, apiKeyList);
}
}
}
/**
* 删除指定 loginId 的所有 ApiKey
* @param loginId /
*/
public void deleteApiKeyByLoginId(Object loginId) {
// 先判断是否开启索引
if(! getIsRecordIndex()) {
SaManager.getLog().warn("当前 API Key 模块未开启索引记录功能,无法执行 deleteApiKeyByLoginId 操作");
return;
}
// RawSession 中不存在,提前退出
SaSession session = rawSessionDelegator.getSessionById(loginId, false);
if(session == null) {
return;
}
// 先删 ApiKeyModel
ArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);
for (String apiKey : apiKeyList) {
getSaTokenDao().deleteObject(splicingApiKeySaveKey(apiKey));
}
// 再删索引
rawSessionDelegator.deleteSessionById(loginId);
}
// ------- 创建
/**
* 创建一个 ApiKeyModel 对象
*
* @return /
*/
public ApiKeyModel createApiKeyModel() {
String apiKey = SaStrategy.instance.generateUniqueToken.execute(
"API Key",
SaManager.getConfig().getMaxTryTimes(),
this::randomApiKeyValue,
_apiKey -> getApiKey(_apiKey) == null
);
return new ApiKeyModel().setApiKey(apiKey);
}
/**
* 创建一个 ApiKeyModel 对象
*
* @return /
*/
public ApiKeyModel createApiKeyModel(Object loginId) {
long timeout = SaApiKeyManager.getConfig().getTimeout();
long expiresTime = (timeout == SaTokenDao.NEVER_EXPIRE) ? SaTokenDao.NEVER_EXPIRE : System.currentTimeMillis() + timeout * 1000;
return createApiKeyModel()
.setLoginId(loginId)
.setIsValid(true)
.setExpiresTime(expiresTime)
;
}
/**
* 随机一个 ApiKey 码
*
* @return /
*/
public String randomApiKeyValue() {
return SaApiKeyManager.getConfig().getPrefix() + SaFoxUtil.getRandomString(36);
}
// ------------------- 校验
/**
* 判断:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),返回 true 或 false
* @param apiKey ApiKey
* @param scopes 需要校验的权限列表
*/
public boolean hasApiKeyScope(String apiKey, String... scopes) {
try {
checkApiKeyScope(apiKey, scopes);
return true;
} catch (ApiKeyException e) {
return false;
}
}
/**
* 校验:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),如果不具备则抛出异常
* @param apiKey ApiKey
* @param scopes 需要校验的权限列表
*/
public void checkApiKeyScope(String apiKey, String... scopes) {
ApiKeyModel ak = checkApiKey(apiKey);
if(SaFoxUtil.isEmptyArray(scopes)) {
return;
}
for (String scope : scopes) {
if(! ak.getScopes().contains(scope)) {
throw new ApiKeyScopeException("该 API Key 不具备 Scope" + scope)
.setApiKey(apiKey)
.setScope(scope)
.setCode(SaErrorCode.CODE_12311);
}
}
}
/**
* 判断:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),返回 true 或 false
* @param apiKey ApiKey
* @param scopes 需要校验的权限列表
*/
public boolean hasApiKeyScopeOr(String apiKey, String... scopes) {
try {
checkApiKeyScopeOr(apiKey, scopes);
return true;
} catch (ApiKeyException e) {
return false;
}
}
/**
* 校验:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),如果不具备则抛出异常
* @param apiKey ApiKey
* @param scopes 需要校验的权限列表
*/
public void checkApiKeyScopeOr(String apiKey, String... scopes) {
ApiKeyModel ak = checkApiKey(apiKey);
if(SaFoxUtil.isEmptyArray(scopes)) {
return;
}
for (String scope : scopes) {
if(ak.getScopes().contains(scope)) {
return;
}
}
throw new ApiKeyScopeException("该 API Key 不具备 Scope" + scopes[0])
.setApiKey(apiKey)
.setScope(scopes[0])
.setCode(SaErrorCode.CODE_12311);
}
/**
* 判断:指定 ApiKey 是否属于指定 LoginId,返回 true 或 false
* @param apiKey /
* @param loginId /
*/
public boolean isApiKeyLoginId(String apiKey, Object loginId) {
try {
checkApiKeyLoginId(apiKey, loginId);
return true;
} catch (ApiKeyException e) {
return false;
}
}
/**
* 校验:指定 ApiKey 是否属于指定 LoginId,如果不是则抛出异常
*
* @param apiKey /
* @param loginId /
*/
public void checkApiKeyLoginId(String apiKey, Object loginId) {
ApiKeyModel ak = getApiKey(apiKey);
if(ak == null) {
throw new ApiKeyException("无效 API Key: " + apiKey).setApiKey(apiKey).setCode(SaErrorCode.CODE_12301);
}
if (SaFoxUtil.notEquals(String.valueOf(ak.getLoginId()), String.valueOf(loginId))) {
throw new ApiKeyException("该 API Key 不属于用户: " + loginId)
.setApiKey(apiKey)
.setCode(SaErrorCode.CODE_12312);
}
}
// ------------------- 索引操作
/**
* 调整指定 SaSession 的 TTL 值,以保证最小化内存占用
* @param loginId /
* @param session 可填写 null,代表使用 loginId 现场查询
*/
public void adjustIndex(Object loginId, SaSession session) {
// 先判断是否开启索引
if(! getIsRecordIndex()) {
SaManager.getLog().warn("当前 API Key 模块未开启索引记录功能,无法执行 adjustIndex 操作");
return;
}
// 未提供则现场查询
if(session == null) {
session = rawSessionDelegator.getSessionById(loginId, false);
if(session == null) {
return;
}
}
// 重新整理索引列表
ArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);
ArrayList<String> apiKeyNewList = new ArrayList<>();
ArrayList<ApiKeyModel> apiKeyModelList = new ArrayList<>();
for (String apikey : apiKeyList) {
ApiKeyModel ak = getApiKeyModelFromCache(apikey);
if(ak == null || ak.timeExpired()) {
continue;
}
apiKeyNewList.add(apikey);
apiKeyModelList.add(ak);
}
// 如果队列里已无有效值,则删除该 session
if(apiKeyNewList.isEmpty()) {
rawSessionDelegator.deleteSessionById(loginId);
return;
}
session.set(API_KEY_LIST, apiKeyNewList);
// 调整 SaSession TTL
long maxTtl = 0;
for (ApiKeyModel ak : apiKeyModelList) {
long ttl = ak.expiresIn();
if(ttl == SaTokenDao.NEVER_EXPIRE) {
maxTtl = SaTokenDao.NEVER_EXPIRE;
break;
}
if(ttl > maxTtl) {
maxTtl = ttl;
}
}
if(maxTtl != 0) {
session.updateTimeout(maxTtl);
}
}
/**
* 获取指定 loginId 的 ApiKey 列表记录
* @param loginId /
* @return /
*/
public List<ApiKeyModel> getApiKeyList(Object loginId) {
// 先判断是否开启索引
if(! getIsRecordIndex()) {
SaManager.getLog().warn("当前 API Key 模块未开启索引记录功能,无法执行 getApiKeyList 操作");
return new ArrayList<>();
}
// 先查 RawSession
List<ApiKeyModel> apiKeyModelList = new ArrayList<>();
SaSession session = rawSessionDelegator.getSessionById(loginId, false);
if(session == null) {
return apiKeyModelList;
}
// 从 RawSession 遍历查询
ArrayList<String> apiKeyList = session.get(API_KEY_LIST, ArrayList::new);
for (String apikey : apiKeyList) {
ApiKeyModel ak = getApiKeyModelFromCache(apikey);
if(ak == null || ak.timeExpired()) {
continue;
}
apiKeyModelList.add(ak);
}
return apiKeyModelList;
}
// ------------------- 请求查询
/**
* 数据读取:从请求对象中读取 ApiKey,获取不到返回 null
*/
public String readApiKeyValue(SaRequest request) {
// 优先从请求参数中获取
String apiKey = request.getParam(namespace);
if(SaFoxUtil.isNotEmpty(apiKey)) {
return apiKey;
}
// 然后请求头
apiKey = request.getHeader(namespace);
if(SaFoxUtil.isNotEmpty(apiKey)) {
return apiKey;
}
// 最后从 Authorization 中获取
apiKey = SaHttpBasicUtil.getAuthorizationValue();
if(SaFoxUtil.isNotEmpty(apiKey)) {
if(apiKey.endsWith(":")) {
apiKey = apiKey.substring(0, apiKey.length() - 1);
}
return apiKey;
}
return null;
}
/**
* 数据读取:从请求对象中读取 ApiKey,并查询到 ApiKeyModel 信息
*/
public ApiKeyModel currentApiKey() {
String readApiKeyValue = readApiKeyValue(SaHolder.getRequest());
return checkApiKey(readApiKeyValue);
}
// ------------------- 拼接key
/**
* 拼接keyApiKey 持久化
* @param apiKey ApiKey
* @return key
*/
public String splicingApiKeySaveKey(String apiKey) {
return getSaTokenConfig().getTokenName() + ":" + namespace + ":" + apiKey;
}
// -------- bean 对象代理
/**
* 获取使用的 getSaTokenDao 实例
*
* @return /
*/
public SaTokenDao getSaTokenDao() {
return SaManager.getSaTokenDao();
}
/**
* 获取使用的 SaTokenConfig 实例
*
* @return /
*/
public SaTokenConfig getSaTokenConfig() {
return SaManager.getConfig();
}
/**
* 是否保存索引信息
*/
public boolean getIsRecordIndex() {
return SaApiKeyManager.getSaApiKeyDataLoader().getIsRecordIndex();
}
}
@@ -0,0 +1,200 @@
/*
* 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.apikey.template;
import cn.dev33.satoken.apikey.SaApiKeyManager;
import cn.dev33.satoken.apikey.model.ApiKeyModel;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.session.SaSession;
import java.util.List;
/**
* API Key 操作工具类
*
* @author click33
* @since 1.42.0
*/
public class SaApiKeyUtil {
/**
* 获取 ApiKeyModel,无效的 ApiKey 会返回 null
* @param apiKey /
* @return /
*/
public static ApiKeyModel getApiKey(String apiKey) {
return SaApiKeyManager.getSaApiKeyTemplate().getApiKey(apiKey);
}
/**
* 校验 ApiKey,成功返回 ApiKeyModel,失败则抛出异常
* @param apiKey /
* @return /
*/
public static ApiKeyModel checkApiKey(String apiKey) {
return SaApiKeyManager.getSaApiKeyTemplate().checkApiKey(apiKey);
}
/**
* 持久化:ApiKeyModel
* @param ak /
*/
public static void saveApiKey(ApiKeyModel ak) {
SaApiKeyManager.getSaApiKeyTemplate().saveApiKey(ak);
}
/**
* 获取 ApiKey 所代表的 LoginId
* @param apiKey ApiKey
* @return LoginId
*/
public static Object getLoginIdByApiKey(String apiKey) {
return SaApiKeyManager.getSaApiKeyTemplate().getLoginIdByApiKey(apiKey);
}
/**
* 删除 ApiKey
* @param apiKey ApiKey
*/
public static void deleteApiKey(String apiKey) {
SaApiKeyManager.getSaApiKeyTemplate().deleteApiKey(apiKey);
}
/**
* 删除指定 loginId 的所有 ApiKey
* @param loginId /
*/
public static void deleteApiKeyByLoginId(Object loginId) {
SaApiKeyManager.getSaApiKeyTemplate().deleteApiKeyByLoginId(loginId);
}
// ------- 创建
/**
* 创建一个 ApiKeyModel 对象
*
* @return /
*/
public static ApiKeyModel createApiKeyModel() {
return SaApiKeyManager.getSaApiKeyTemplate().createApiKeyModel();
}
/**
* 创建一个 ApiKeyModel 对象
*
* @return /
*/
public static ApiKeyModel createApiKeyModel(Object loginId) {
return SaApiKeyManager.getSaApiKeyTemplate().createApiKeyModel(loginId);
}
// ------------------- Scope
/**
* 判断:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),返回 true 或 false
* @param apiKey ApiKey
* @param scopes 需要校验的权限列表
*/
public static boolean hasApiKeyScope(String apiKey, String... scopes) {
return SaApiKeyManager.getSaApiKeyTemplate().hasApiKeyScope(apiKey, scopes);
}
/**
* 校验:指定 ApiKey 是否具有指定 Scope 列表 (AND 模式,需要全部具备),如果不具备则抛出异常
* @param apiKey ApiKey
* @param scopes 需要校验的权限列表
*/
public static void checkApiKeyScope(String apiKey, String... scopes) {
SaApiKeyManager.getSaApiKeyTemplate().checkApiKeyScope(apiKey, scopes);
}
/**
* 判断:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),返回 true 或 false
* @param apiKey ApiKey
* @param scopes 需要校验的权限列表
*/
public static boolean hasApiKeyScopeOr(String apiKey, String... scopes) {
return SaApiKeyManager.getSaApiKeyTemplate().hasApiKeyScopeOr(apiKey, scopes);
}
/**
* 校验:指定 ApiKey 是否具有指定 Scope 列表 (OR 模式,具备其一即可),如果不具备则抛出异常
* @param apiKey ApiKey
* @param scopes 需要校验的权限列表
*/
public static void checkApiKeyScopeOr(String apiKey, String... scopes) {
SaApiKeyManager.getSaApiKeyTemplate().checkApiKeyScopeOr(apiKey, scopes);
}
/**
* 判断:指定 ApiKey 是否属于指定 LoginId,返回 true 或 false
* @param apiKey /
* @param loginId /
*/
public static boolean isApiKeyLoginId(String apiKey, Object loginId) {
return SaApiKeyManager.getSaApiKeyTemplate().isApiKeyLoginId(apiKey, loginId);
}
/**
* 校验:指定 ApiKey 是否属于指定 LoginId,如果不是则抛出异常
*
* @param apiKey /
* @param loginId /
*/
public static void checkApiKeyLoginId(String apiKey, Object loginId) {
SaApiKeyManager.getSaApiKeyTemplate().checkApiKeyLoginId(apiKey, loginId);
}
// ------------------- 请求查询
/**
* 数据读取:从请求对象中读取 ApiKey,获取不到返回 null
*/
public static String readApiKeyValue(SaRequest request) {
return SaApiKeyManager.getSaApiKeyTemplate().readApiKeyValue(request);
}
/**
* 数据读取:从请求对象中读取 ApiKey,并查询到 ApiKeyModel 信息
*/
public static ApiKeyModel currentApiKey() {
return SaApiKeyManager.getSaApiKeyTemplate().currentApiKey();
}
// ------------------- 索引操作
/**
* 调整指定 SaSession 的 TTL 值,以保证最小化内存占用
* @param loginId /
* @param session 可填写 null,代表使用 loginId 现场查询
*/
public static void adjustIndex(Object loginId, SaSession session) {
SaApiKeyManager.getSaApiKeyTemplate().adjustIndex(loginId, session);
}
/**
* 获取指定 loginId 的 ApiKey 列表记录
* @param loginId /
* @return /
*/
public static List<ApiKeyModel> getApiKeyList(Object loginId) {
return SaApiKeyManager.getSaApiKeyTemplate().getApiKeyList(loginId);
}
}
@@ -0,0 +1,35 @@
/*
* 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.plugin;
import cn.dev33.satoken.apikey.annotation.handle.SaCheckApiKeyHandler;
import cn.dev33.satoken.strategy.SaAnnotationStrategy;
/**
* SaToken 插件安装:API Key 组件
*
* @author click33
* @since 1.43.0
*/
public class SaTokenPluginForApiKey implements SaTokenPlugin {
@Override
public void install() {
// 安装 API Key 鉴权注解
SaAnnotationStrategy.instance.registerAnnotationHandler(new SaCheckApiKeyHandler());
}
}
@@ -0,0 +1 @@
cn.dev33.satoken.plugin.SaTokenPluginForApiKey