001package top.cenze.utils.plugins;
002
003import cn.hutool.core.collection.CollectionUtil;
004import cn.hutool.core.convert.Convert;
005import cn.hutool.core.util.ObjectUtil;
006import cn.hutool.core.util.StrUtil;
007import com.alibaba.fastjson.JSON;
008import lombok.SneakyThrows;
009import lombok.extern.slf4j.Slf4j;
010import top.cenze.utils.TokenUtil;
011import top.cenze.utils.aspect.AspectUtil;
012import top.cenze.utils.pojo.ApiAuthConfig;
013
014import javax.servlet.http.HttpServletRequest;
015import java.util.HashSet;
016import java.util.Set;
017
018/**
019 * @desc: 接口插件
020 * @author: chengze
021 * @createByDate: 2023/12/30 18:25
022 */
023@Slf4j
024public abstract class ApiPlugin {
025
026    /**
027     * 开放接口访问缓存前缀
028     */
029    private static final String API_ACCESS_PREFIX = "API_ACCESS:";
030
031    /**
032     * 接口是否开放的,忽略大小写,返回String(无需登录即可访问,允许三方系统直接访问)
033     * @param requestUri    当前请求的uri (ServletPath)
034     * @return              返回第一个被请求uri包含的接口uri
035     */
036    public String isOpenStrIgnoreCase(String requestUri) {
037        // 获取开放接口uri集合
038        Set<String> setkeys = getOpenApiUris();
039        log.info("isOpenStrIgnoreCase openApis: {}", JSON.toJSONString(setkeys));
040
041        // 接口关键字集合为空时,则赋一个空值
042        String[] keys = null;
043        if (CollectionUtil.isEmpty(setkeys)) {
044            setkeys = new HashSet<>();
045            setkeys.add("");
046            keys = new String[] {""};
047        } else {
048            keys = Convert.toStrArray(setkeys);
049        }
050
051        // 请求uri是否包含接口关键字集合中的成员(忽略大小写),返回集合中uri包含的第一个字符串
052        return StrUtil.getContainsStrIgnoreCase(requestUri, keys);
053    }
054
055    /**
056     * 接口是否开放的,忽略大小写,返回Boolean(无需登录即可访问,允许三方系统直接访问)
057     * @param requestUri    当前请求的uri (ServletPath)
058     * @return              返回是否为开放接口,true是,false否
059     */
060    public boolean isOpenIgnoreCase(String requestUri) {
061        String containsStr = isOpenStrIgnoreCase(requestUri);
062        log.info("isOpenIgnoreCase containsStr: {}", containsStr);
063
064        if (StrUtil.isNotEmpty(containsStr)) {
065            return true;
066        }
067
068        return false;
069    }
070
071    /**
072     * 获取开放接口uri列表
073     * @return
074     */
075    public abstract Set<String> getOpenApiUris();
076
077    /**
078     * 接口鉴权,返回Boolean
079     * @param request   servlet请求
080     * @return          返回鉴权结果,通过true,未通过false
081     */
082    public boolean auth(HttpServletRequest request) {
083        return auth(request, false);
084    }
085    /**
086     * 接口鉴权,返回Boolean
087     * @param request   servlet请求
088     * @param isAop     是否为注解切入
089     * @return          返回鉴权结果,通过true,未通过false
090     */
091    @SneakyThrows
092    public boolean auth(HttpServletRequest request, boolean isAop) {
093        // 获取系统配置的开放接口中匹配当前请求uri的接口
094        String openApiUri = isOpenStrIgnoreCase(request.getServletPath());
095        if (StrUtil.isEmpty(openApiUri)) {
096            // 如果是注解切入,则鉴权不通过(严格限定)
097            if (isAop) {
098                return false;
099            }
100            // 不为注解切入,当前访问非开放接口,则忽略鉴权
101            else {
102                return true;
103            }
104        }
105
106        ApiAuthConfig apiAuthConfig = getApiAuthConfig(openApiUri);
107        log.info("auth apiAuthConfig: {}", JSON.toJSONString(apiAuthConfig));
108        // 鉴权配置为空,或不需要鉴权,则鉴权通过
109        if (ObjectUtil.isNull(apiAuthConfig) || !apiAuthConfig.getRequireAuth().equals(1)) {
110            return true;
111        }
112
113        String token = TokenUtil.resolveBearerTokenFromRequest(request);
114        log.info("auth token: {}", token);
115
116        String appSecret = getAuthSecret(token);
117        log.info("auth appSecret: {}", appSecret);
118
119        return TokenUtil.validBearerToken(token, appSecret);
120    }
121
122    /**
123     * 获取token的访问密码
124     * @param token
125     * @return
126     */
127    public abstract String getAuthSecret(String token);
128
129    /**
130     * 是否超过访问限制次数(配置的限制次数,限制时间范围,单位分钟)
131     * @param request       访问请求 servlet请求
132     * @return              超过true,未超过false
133     */
134    public boolean exceedLimitCount(HttpServletRequest request) {
135        // 获取系统配置的开放接口中匹配当前请求uri的接口
136        String openApiUri = isOpenStrIgnoreCase(request.getServletPath());
137        if (StrUtil.isEmpty(openApiUri)) {
138            return false;
139        }
140
141        String key = API_ACCESS_PREFIX + AspectUtil.getIpAddress(request) + "_" + openApiUri;
142        log.info("exceedLimitCount key: {}", key);
143
144        ApiAuthConfig apiAuthConfig = getAccessApiAuthConfig(key);
145        log.info("exceedLimitCount ip apiAuthConfig: {}", JSON.toJSONString(apiAuthConfig));
146        // ip访问鉴权配置为空,则系统公共配置
147        if (ObjectUtil.isNull(apiAuthConfig)) {
148            apiAuthConfig = getApiAuthConfig(openApiUri);
149            log.info("exceedLimitCount apiAuthConfig: {}", JSON.toJSONString(apiAuthConfig));
150        }
151        // 鉴权配置为空,则不限制
152        if (ObjectUtil.isNull(apiAuthConfig)) {
153            return false;
154        }
155
156        // 配置的限制次数(0~65535),0不限制
157        Integer limitCount = apiAuthConfig.getLimitCount();
158        log.info("exceedLimitCount limitCount: {}", limitCount);
159        if (ObjectUtil.isNull(limitCount)) {
160            limitCount = 0;
161        }
162        // 配置的限制时间范围(0~ 4294967295,单位:分钟),0不限制(多少分钟内限制访问多少次,limit_count或limit_time为0时,不限制)
163        Integer limitTime = apiAuthConfig.getLimitTime();
164        log.info("exceedLimitCount limitTime: {}", limitTime);
165        if (ObjectUtil.isNull(limitTime)) {
166            limitTime = 0;
167        }
168
169        // 如果设置了访问次数限制(设置了限制次数及时间范围,且设置值均大于0)
170        if (ObjectUtil.isNull(limitCount) || ObjectUtil.isNull(limitCount) ||
171                limitCount <= 0 || limitTime <= 0) {
172            return false;
173        }
174
175        // 当前时间戳,单位:分钟
176        Integer curTime = Convert.toInt(System.currentTimeMillis() / 60000);
177        log.info("exceedLimitCount curTime: {}", curTime);
178
179        // 访问次数
180        Integer accessCount = apiAuthConfig.getAccessCount();
181        log.info("exceedLimitCount accessCount: {}", accessCount);
182        if (ObjectUtil.isNull(accessCount)) {
183            accessCount = 0;
184        }
185        // 获取当前接口时间范围内初始时间戳,单位:分钟
186        Integer accessTime = apiAuthConfig.getAccessFirstTime();
187        log.info("exceedLimitCount accessTime: {}", accessTime);
188        if (ObjectUtil.isNull(accessTime) || accessTime.equals(0)) {
189            accessTime = curTime;
190            apiAuthConfig.setAccessFirstTime(curTime);
191        }
192
193        // 计算当前限制时间范围内已过去时间,单位分钟
194        int pastTime = curTime - accessTime;
195        log.info("exceedLimitCount pastTime: {}", pastTime);
196        if (pastTime >= limitTime) {
197            // 重置访问次数和时间
198            apiAuthConfig.setAccessCount(0);
199            apiAuthConfig.setAccessFirstTime(0);
200
201            accessCount = 0;
202        }
203
204        accessCount ++;
205        // 更新缓存访问次数
206        apiAuthConfig.setAccessCount(accessCount);
207        // 更新缓存
208        setAccessApiAuthConfig(key, apiAuthConfig);
209
210        // 超过限制次数
211        if (accessCount > limitCount) {
212            return true;
213        }
214
215        return false;
216    }
217
218    /**
219     * 获取授权配置
220     * @param apiUri
221     */
222    public abstract ApiAuthConfig getApiAuthConfig(String apiUri);
223
224    /**
225     * 获取各ip访问授权配置
226     * @param key
227     */
228    public abstract ApiAuthConfig getAccessApiAuthConfig(String key);
229
230    /**
231     * 缓存各ip访问授权配置
232     * @param key
233     * @param authConfig
234     */
235    public abstract void setAccessApiAuthConfig(String key, ApiAuthConfig authConfig);
236}