一、名詞解釋
過(guò)濾器(Filter)實(shí)際上就是對(duì)web資源進(jìn)行攔截,做一些處理后再交給下一個(gè)過(guò)濾器或servlet處理,通常都是用來(lái)攔截request進(jìn)行處理的,也可以對(duì)返回的response進(jìn)行攔截處理,大致流程如下圖
二、使用方式
package filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import java.io.IOException;
@WebFilter(filterName = "MyFilter",
urlPatterns = "/*",/*通配符(*)表示對(duì)所有的web資源進(jìn)行攔截*/
initParams = {
@WebInitParam(name = "test", value = "1")/*這里可以放一些初始化的參數(shù)*/
})
public class MyFilter implements Filter {
private String filterName;
private String test;
public void destroy() {
/*銷(xiāo)毀時(shí)調(diào)用*/
System.out.println(filterName + "銷(xiāo)毀");
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws ServletException, IOException {
/*過(guò)濾方法 主要是對(duì)request和response進(jìn)行一些處理,然后交給下一個(gè)過(guò)濾器或Servlet處理*/
System.out.println(filterName + "doFilter()");
chain.doFilter(req, resp);
}
public void init(FilterConfig config) throws ServletException {
/*初始化方法 接收一個(gè)FilterConfig類(lèi)型的參數(shù) 該參數(shù)是對(duì)Filter的一些配置*/
filterName = config.getFilterName();
test= config.getInitParameter("test");
System.out.println("過(guò)濾器名稱(chēng):" + filterName);
System.out.println("測(cè)試值:" + test);
}
}
三、使用場(chǎng)景
1. 字符集統(tǒng)一設(shè)置
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
chain.doFilter(req, resp);
}
2. 敏感參數(shù)加密
一般情況下,我們針對(duì)一些敏感的參數(shù),例如密碼、身份證號(hào)等,給它加密,防止報(bào)文明文傳輸,加密可以分為大體的兩類(lèi),對(duì)稱(chēng)加密和非對(duì)稱(chēng)加密,下面,簡(jiǎn)單介紹下這兩種方式。
- 對(duì)稱(chēng)加密
加密使用的密鑰和解密使用的密鑰是同一個(gè),例如sm4加密,這樣的加密方式簡(jiǎn)單,只需要加解密雙方都有密鑰即可,但是這樣很不安全,一旦密鑰泄漏,數(shù)據(jù)就會(huì)被解密。 - 非對(duì)稱(chēng)加密
非對(duì)稱(chēng)加密就是加密使用一個(gè)密鑰(一般稱(chēng)為公鑰),解密使用另一個(gè)密鑰(一般稱(chēng)為私鑰),常見(jiàn)的算法有RSA算法、sm2算法,這種情況下,私鑰一般由解密方獨(dú)立保存,極大提高了數(shù)據(jù)的安全性。如果要對(duì)所有請(qǐng)求參數(shù)加密,推薦使用https請(qǐng)求,因?yàn)閔ttps請(qǐng)求原理上也是非對(duì)稱(chēng)加密實(shí)現(xiàn)的,這里不做過(guò)多贅述。
3. 加簽驗(yàn)簽
我們對(duì)參數(shù)進(jìn)行了加密,那么數(shù)據(jù)是否安全了呢?答案是否定的,因?yàn)槲覀冎皇潜WC了傳入?yún)?shù)不被別人知道,但是我們的請(qǐng)求或響應(yīng)是可以被篡改攔截的,那么,就需要引入新的方案,加簽驗(yàn)簽。
- 加簽
用Hash函數(shù)把原始報(bào)文生成報(bào)文摘要,然后對(duì)這個(gè)摘要進(jìn)行加密,就得到這個(gè)報(bào)文對(duì)應(yīng)的數(shù)字簽名。一般情況下,客戶端會(huì)將簽名和原始報(bào)文一起發(fā)給服務(wù)端。
//accessKey理解為一個(gè)鹽值,signPriKey是加密私鑰,map是請(qǐng)求參數(shù)
public static String sign(String accessKey, String signPriKey, Map<String, String> map) {
//sort方法主要用于參數(shù)排序及過(guò)濾,過(guò)濾掉key為sign的參數(shù)
String paramStr = sort(map);
//生成的摘要
String abstractText = SM3Digest.sm3Encry(paramStr + accessKey);
//非對(duì)稱(chēng)加密生成簽名
return SMHelper.sm2Sign(signPriKey, abstractText);
}
public static String sort(String jsonString){
JSONObject jsonObject = JSON.parseObject(jsonString);
String aa = jsonObject.toJSONString();
List<String> list = new ArrayList();
for(Entry<String, Object> entry : jsonObject.entrySet()){
String key = entry.getKey();
//主要關(guān)注這里,排除了sign參數(shù),因?yàn)閟ign簽也是要作為參數(shù)傳遞給服務(wù)端的,但是客戶端加簽時(shí)還沒(méi)有sign簽
if ("sign".equals(key)){
continue;
}
String value = null;
if (entry.getValue() instanceof JSONObject || entry.getValue() instanceof JSONArray){
value = JSON.toJSONString(entry.getValue());
} else {
value = (String)entry.getValue();
}
String str = key+value;
list.add(str);
}
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
try {
String s1 = new String(o1.toString().getBytes("UTF-8"), "ISO-8859-1");
String s2 = new String(o2.toString().getBytes("UTF-8"), "ISO-8859-1");
return s1.compareTo(s2);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
});
StringBuffer paramStr = new StringBuffer();
for(String param : list){
paramStr.append(param);
}
return paramStr.toString();
}
- 驗(yàn)簽
接收方拿到原始報(bào)文和sign簽名后,用同一個(gè)Hash函數(shù)從報(bào)文中生成服務(wù)端摘要。然后用對(duì)方提供的公鑰對(duì)數(shù)字簽名進(jìn)行解密,得到客戶端摘要,對(duì)比兩個(gè)摘要是否相同,就可以得知報(bào)文有沒(méi)有被篡改過(guò)。
/ca是證書(shū),存儲(chǔ)了驗(yàn)簽公鑰等信息,sign是客戶端的簽名
private boolean sign(AuthSecCa ca, String sign, Map<String, String> param) {
//相同的排序hash方法
String paramStr = Sort.sort(param);
//生成服務(wù)端摘要
String design = SM3Digest.SM3Encry(paramStr + ca.getAccessKey());
// 驗(yàn)簽
boolean b = SMHelper.sm2Verify(ca.getSignPubKey(), design, sign);
return b;
}
4. 時(shí)間戳驗(yàn)證
大體思路就是請(qǐng)求參數(shù)加上一個(gè)請(qǐng)求時(shí)間戳dataStamp,服務(wù)端獲取到這個(gè)時(shí)間戳后,獲取一個(gè)當(dāng)前的時(shí)間戳serverStamp,然后這兩個(gè)時(shí)間戳的差值少于多長(zhǎng)時(shí)間才算有效請(qǐng)求。
private boolean verifyDataStamp(String time) {
boolean flag = false;
long nowTime = System.currentTimeMillis();
if (StringUtils.isNotEmpty(time)) {
long t = Long.parseLong(time);
//時(shí)間間隔超過(guò)1分鐘
int stampInt = stamp;
if (Math.abs(nowTime - t) > stampInt * 1000) {
flag = true;
}
}
return flag;
}
5. 請(qǐng)求隨機(jī)數(shù)驗(yàn)證
我們需要給請(qǐng)求加上一個(gè)唯一的隨機(jī)數(shù)nonce,每次請(qǐng)求過(guò)來(lái)把nonce拿到,判斷是否已經(jīng)又過(guò)了,來(lái)考慮是否放行請(qǐng)求,但是如果存儲(chǔ)大量的nonce對(duì)我們的系統(tǒng)來(lái)說(shuō)也是巨大的壓力,因此配合時(shí)間戳一起使用,例如,時(shí)間戳是一分鐘,我們可以設(shè)置nonce的有效期為兩分鐘(大于一分鐘即可,避免極端情況)
private boolean verifyNonce(String nonce) {
if (StringUtils.isEmpty(nonce)) {
return true;
}
if (緩存.isExist(nonce)) {
return false;
}
緩存.setWithExpire(nonce, 120);
return true;
}
6. 黑、白名單
我們可以在本身的后臺(tái)管理系統(tǒng)中添加黑名單及白名單的相關(guān)配置,對(duì)于黑名單發(fā)起的請(qǐng)求,直接返回錯(cuò)誤碼;對(duì)于一些特別敏感的操作,例如涉及到轉(zhuǎn)賬等,只有在白名單中的請(qǐng)求才可以操作。
private boolean verifyBlack(String serviceId, String uri) {
boolean flag = 緩存.get(serviceId, uri);
log.info("api黑名單是否存在,serviceId:"+serviceId+",uri:"+uri+" -> 結(jié)果:"+flag);
return flag;
}
7. 服務(wù)限流
介紹一個(gè)常見(jiàn)的限流算法,令牌桶限流。它的思路為:
對(duì)于每個(gè)要限流的對(duì)象(比如一個(gè)user,或者一個(gè)接入證書(shū)),分配一個(gè)bucket;
bucket里的tokens以一個(gè)固定的速率在增加,bucket有個(gè)最大容量,到了最大容量就不再增加了;
每個(gè)請(qǐng)求會(huì)消耗一定的token數(shù)量,如果bucket內(nèi)的token數(shù)量有剩余則請(qǐng)求通過(guò);否則拒絕請(qǐng)求;
下面是通過(guò)引入guava的單機(jī)版限流RateLimiter做的一個(gè)限流demo
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
public class test {
//每秒鐘生成4個(gè)token
private static final RateLimiter rateLimiter = RateLimiter.create(4);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(()->{
//每次請(qǐng)求消耗一個(gè)token
if(rateLimiter.tryAcquire()){
System.out.println("請(qǐng)求成功");
}else{
System.out.println("限流了");
}
}).start();
//每個(gè)請(qǐng)求相隔1/5秒
Thread.sleep(200);
}
}
}
四、額外知識(shí)補(bǔ)充
補(bǔ)充幾種限流算法知識(shí)
1. 固定窗口限流
介紹:?jiǎn)挝粫r(shí)間(固定時(shí)間窗口)內(nèi)限制請(qǐng)求的數(shù)量,即將時(shí)間分為固定的窗口,限制每個(gè)窗口的請(qǐng)求數(shù)量;
缺點(diǎn):存在臨界問(wèn)題,例如0-1和1-2是兩個(gè)時(shí)間窗口,若在0.8-1s內(nèi)有5個(gè)請(qǐng)求,在1-1.2s內(nèi)有5個(gè)請(qǐng)求,雖然在各自的窗口上沒(méi)有超限,但是在0.8-1.2s這個(gè)時(shí)間范圍內(nèi)超限了;
實(shí)現(xiàn):假設(shè)單位時(shí)間是1s,限流閾值為5,在單位時(shí)間內(nèi),每來(lái)一次請(qǐng)求,計(jì)數(shù)器count +1,若count>5,后續(xù)請(qǐng)求全部拒絕,等到1s結(jié)束,count清零;
//計(jì)數(shù)器
public static AtomicInteger count = new AtomicInteger(0);
//時(shí)間窗口,單位s
public static final Long window = 1000L;
//窗口閾值
public static final int threshold = 5;
//上次請(qǐng)求時(shí)間
public static long lastAcquireTime = 0L;
public static synchronized boolean fixedWindowTryAcquire(){
//當(dāng)前系統(tǒng)時(shí)間
long currentTime = System.currentTimeMillis();
//看看本次請(qǐng)求是否在窗口內(nèi)
if(currentTime - lastAcquireTime > window){
//重置計(jì)數(shù)器和上次請(qǐng)求時(shí)間
count.set(0);
lastAcquireTime = currentTime;
}
//檢查計(jì)數(shù)器是否超過(guò)閾值
if(count.incrementAndGet() <= threshold){
return true;
}
return false;
}
2. 滑動(dòng)窗口限流
介紹:在固定窗口中,存在臨界問(wèn)題,那么,如果我們讓這個(gè)小窗口動(dòng)起來(lái),不斷去刪除已經(jīng)過(guò)去的時(shí)間,這樣,動(dòng)態(tài)的判斷是否超限就可以了;
缺點(diǎn):無(wú)法應(yīng)對(duì)突發(fā)流量(短時(shí)間大量請(qǐng)求),許多請(qǐng)求會(huì)被直接拒絕;
實(shí)現(xiàn):假設(shè)單位時(shí)間是100s(單位窗口),限流閾值為5,我們將單位窗口分為10個(gè)小窗口(每個(gè)小窗口為10s),然后不斷的更新每個(gè)小窗口的請(qǐng)求和小窗口的起始時(shí)間,這樣就模擬表現(xiàn)為動(dòng)起來(lái)了
//時(shí)間窗口,單位s
public static final Long window = 100L;
//窗口閾值
public static final int threshold = 5;
//小窗口個(gè)數(shù)
public static final int windowCount = 10;
//存儲(chǔ)每個(gè)小窗口開(kāi)始時(shí)間及其流量
public static Map<Long, Integer>windowCounters = new HashMap<>();
public static synchronized boolean tryAcquire(){
//取當(dāng)前時(shí)間,做了取整操作,保證在同一個(gè)小窗口內(nèi)的時(shí)間落到同一個(gè)起點(diǎn),將時(shí)間戳取秒計(jì)算,方便理解
long currentTime = System.currentTimeMillis()/1000 / windowCount * windowCount;
int currentCount = calculate(currentTime);
if(currentCount > threshold){
return false;
}
windowCounters.put(currentTime - windowCount* (window/windowCount-1),currentCount);
return true;
}
private static int calculate(long currentTime) {
//計(jì)算小窗口開(kāi)始時(shí)間
//這里可以理解為 currentTime - entry.key > window/windowCount
long startTime = currentTime - windowCount* (window/windowCount-1);
System.out.println(startTime);
int count = 1;
//遍歷計(jì)數(shù)器
Iterator<Map.Entry<Long,Integer>> iterator = windowCounters.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<Long,Integer>entry = iterator.next();
//刪除無(wú)效過(guò)期的子窗口
if(entry.getKey() < startTime){
iterator.remove();
System.out.println("移除了");
}else{
//累加請(qǐng)求
count = entry.getValue()+1;
}
}
return count;
}
3. 漏桶算法
介紹:對(duì)于每個(gè)請(qǐng)求,檢查漏桶是否有容量,沒(méi)有直接拒絕;有的話放入漏桶,漏桶以一定速率處理桶內(nèi)的請(qǐng)求,生產(chǎn)者-消費(fèi)者模型,請(qǐng)求作為生產(chǎn)者,系統(tǒng)作為消費(fèi)者;
缺點(diǎn):需要緩存請(qǐng)求,面對(duì)突發(fā)流量,處理不能及時(shí)響應(yīng)
實(shí)現(xiàn):假設(shè)漏桶容量為5,速率為1個(gè)/s,那么我們還需要有一個(gè)當(dāng)前的水量和當(dāng)前時(shí)間分別用來(lái)判斷是否可以存放請(qǐng)求和消耗請(qǐng)求
public class leakBucket {
//漏桶的容量
private long capacity;
//滴水(消耗請(qǐng)求)速率
private long rate;
//當(dāng)前桶中的水(未消耗的請(qǐng)求)
private long water;
//上次滴水時(shí)間(用來(lái)計(jì)算現(xiàn)在要消耗多少個(gè))
private long lastLeakTime;
public leakBucket(long capacity, long rate, long water, long lastLeakTime) {
this.capacity = capacity;
this.rate = rate;
this.water = water;
this.lastLeakTime = lastLeakTime;
}
}
public static synchronized boolean tryAcquire(){
leak();
//判斷是否有空間,這里我設(shè)為每個(gè)請(qǐng)求都要判斷,也可以直接傳參多少個(gè)請(qǐng)求
if(leakBucket.getWater()+1 <leakBucket.getCapacity()){
leakBucket.setWater(leakBucket.getWater()+1);
return true;
}
return false;
}
private static void leak() {
//當(dāng)前時(shí)間,以秒操作
long currentTime = System.currentTimeMillis() / 1000;
//最后一次漏水到現(xiàn)在應(yīng)該漏多少
long leakWater = (currentTime - leakBucket.getLastLeakTime()) * leakBucket.getRate();
if(leakWater > 0){
leakBucket.setWater(Math.max(0,leakBucket.getWater() - leakWater));
leakBucket.setLastLeakTime(currentTime);
}
}
4. 令牌桶算法
介紹:令牌桶算法相當(dāng)于漏桶算法的翻版,之前說(shuō)過(guò),漏桶算法生產(chǎn)者是客戶端,令牌桶是服務(wù)端作為生產(chǎn)者,以一定速率生成令牌放入桶中,桶滿丟棄令牌,每次來(lái)一個(gè)請(qǐng)求消耗一個(gè)令牌,沒(méi)有令牌就丟棄請(qǐng)求;相對(duì)于漏桶算法變化:生產(chǎn)者–>服務(wù)端,更符合限流的定義;消費(fèi)者–>客戶端,不需要另外的存儲(chǔ)請(qǐng)求;
缺點(diǎn):令牌桶算法需要在固定的時(shí)間間隔內(nèi)生成令牌,因此要求時(shí)間精度較高,如果系統(tǒng)時(shí)間不準(zhǔn)確,可能會(huì)導(dǎo)致限流效果不理想文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-770345.html
實(shí)現(xiàn):文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-770345.html
public class tokenBucket {
//令牌桶的容量
private long capacity;
//生成令牌的速率
private long rate;
//當(dāng)前桶中的令牌數(shù)
private long tokenNum;
//上次生成令牌的時(shí)間
private long lastMakeTime;
public tokenBucket(long capacity, long rate, long tokenNum, long lastMakeTime) {
this.capacity = capacity;
this.rate = rate;
this.tokenNum = tokenNum;
this.lastMakeTime = lastMakeTime;
}
}
public static synchronized boolean tryAcquire(){
makeToken();
if(tokenBucket.getTokenNum() > 0){
tokenBucket.setTokenNum(tokenBucket.getTokenNum() - 1);
return true;
}
return false;
}
private static void makeToken() {
//當(dāng)前時(shí)間,以秒計(jì)算
Long currentTime = System.currentTimeMillis() / 1000;
if(currentTime > tokenBucket.getLastMakeTime()){
//計(jì)算應(yīng)該生成多少令牌
long makeNums = (currentTime - tokenBucket.getLastMakeTime()) * tokenBucket.getRate();
//放入令牌桶,多余容量的拋棄
tokenBucket.setTokenNum(Math.min(tokenBucket.getCapacity(),makeNums+tokenBucket.getTokenNum()));
//更新生成令牌時(shí)間
tokenBucket.setLastMakeTime(currentTime);
}
}
到了這里,關(guān)于springboot中過(guò)濾器@WebFilter的使用以及簡(jiǎn)單介紹限流算法的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!