SpringBoot使用Filter實(shí)現(xiàn)簽名認(rèn)證鑒權(quán)的示例代碼
情景說(shuō)明
鑒權(quán),有很多方案,如:SpringSecurity、Shiro、攔截器、過(guò)濾器等等。如果只是對(duì)一些URL進(jìn)行認(rèn)證鑒權(quán)的話,我們完
全沒(méi)必要引入SpringSecurity或Shiro等框架,使用攔截器或過(guò)濾器就足以實(shí)現(xiàn)需求。
本文介紹如何使用過(guò)濾器Filter實(shí)現(xiàn)URL簽名認(rèn)證鑒權(quán)。
本人測(cè)試軟硬件環(huán)境:Windows10、Eclipse、SpringBoot、JDK1.8
準(zhǔn)備工作
第一步:在pom.xml中引入相關(guān)依賴
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- devtools --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <!-- org.apache.commons.codec --> <!-- MD5加密的依賴 --> <dependency> <groupId>org.apache.directory.studio</groupId> <artifactId>org.apache.commons.codec</artifactId> <version>1.8</version> </dependency> </dependencies>
第二步:在系統(tǒng)配置文件application.properties中配置相關(guān)參數(shù),一會(huì)兒代碼中需要用到
# ip白名單(多個(gè)使用逗號(hào)分隔) permitted-ips = 169.254.205.177, 169.254.133.33, 10.8.109.31, 0:0:0:0:0:0:0:1 # secret secret = JustryDeng
第三步:準(zhǔn)備獲取客戶端IP的工具類
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.http.HttpServletRequest;
/**
* 獲取發(fā)出request請(qǐng)求的客戶端ip
* 注:如果是自己發(fā)出的請(qǐng)求,那么獲取的是自己的ip
* 摘錄自https://blog.csdn.net/byy8023/article/details/80499038
*
* 注意事項(xiàng):
* 如果使用此工具,獲取到的不是客戶端的ip地址;而是虛擬機(jī)的ip地址(d當(dāng)客戶端安裝有VMware時(shí),可能出現(xiàn)此情況);
* 那么需要在客戶端的[控制面板\網(wǎng)絡(luò)和 Internet\網(wǎng)絡(luò)連接]中禁用虛擬機(jī)網(wǎng)絡(luò)適配器
*
* @author JustryDeng
* @DATE 2018年9月10日 下午8:56:48
*/
public class IpUtil {
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根據(jù)網(wǎng)卡取本機(jī)配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 對(duì)于通過(guò)多個(gè)代理的情況,第一個(gè)IP為客戶端真實(shí)IP,多個(gè)IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress="";
}
return ipAddress;
}
}
第四步:準(zhǔn)備MD5加密工具類
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.binary.Hex;
/**
* MD5加密工具類
*
* @author JustryDeng 參考自ShaoJJ的MD5加密工具類
* @DATE 2018年9月11日 下午2:14:21
*/
public class MDUtils {
/**
* 加密
*
* @param origin
* 要被加密的字符串
* @param charsetname
* 加密字符,如UTF-8
* @DATE 2018年9月11日 下午2:12:51
*/
public static String MD5EncodeForHex(String origin, String charsetname)
throws UnsupportedEncodingException, NoSuchAlgorithmException {
return MD5EncodeForHex(origin.getBytes(charsetname));
}
public static String MD5EncodeForHex(byte[] origin) throws NoSuchAlgorithmException {
return Hex.encodeHexString(digest("MD5", origin));
}
/**
* 指定加密算法
*
* @throws NoSuchAlgorithmException
* @DATE 2018年9月11日 下午2:11:58
*/
private static byte[] digest(String algorithm, byte[] source) throws NoSuchAlgorithmException {
MessageDigest md;
md = MessageDigest.getInstance(algorithm);
return md.digest(source);
}
}
第五步:簡(jiǎn)單編寫一個(gè)Controller,方便后面的測(cè)試

SpringBoot使用Filter實(shí)現(xiàn)簽名認(rèn)證鑒權(quán) --- 邏輯代碼
第一步:編寫過(guò)濾器
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import com.aspire.util.IpUtil;
import com.aspire.util.MDUtils;
/**
* SpringBoot使用攔截器實(shí)現(xiàn)簽名認(rèn)證(鑒權(quán))
* @WebFilter注解指定要被過(guò)濾的URL
* 一個(gè)URL會(huì)被多個(gè)過(guò)濾器過(guò)濾時(shí),還可以使用@Order(x)來(lái)指定過(guò)濾request的先后順序,x數(shù)字越小越先過(guò)濾
*
* @author JustryDeng
* @DATE 2018年9月11日 下午1:18:29
*/
@WebFilter(urlPatterns = { "/authen/test1", "/authen/test2", "/authen/test3"})
public class SignAutheFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(SignAutheFilter.class);
@Value("${permitted-ips}")
private String[] permittedIps;
@Value("${secret}")
private String secret;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
String authorization = request.getHeader("Authorization");
logger.info("getted Authorization is ---> " + authorization);
String[] info = authorization.split(",");
// 獲取客戶端ip
String ip = IpUtil.getIpAddr(request);
logger.info("getted ip is ---> " + ip);
/*
* 讀取請(qǐng)求體中的數(shù)據(jù)(字符串形式)
* 注:由于同一個(gè)流不能讀取多次;如果在這里讀取了請(qǐng)求體中的數(shù)據(jù),那么@RequestBody中就不能讀取到了
* 會(huì)拋出異常并提示getReader() has already been called for this request
* 解決辦法:先將讀取出來(lái)的流數(shù)據(jù)存起來(lái)作為一個(gè)常量屬性.然后每次讀的時(shí)候,都需要先將這個(gè)屬性值寫入,再讀出.
* 即每次獲取的其實(shí)是不同的流,但是獲取到的數(shù)據(jù)都是一樣的.
* 這里我們借助HttpServletRequestWrapper類來(lái)實(shí)現(xiàn)
* 注:此方法涉及到流的讀寫、耗性能;
*/
MyRequestWrapper mrw = new MyRequestWrapper(request);
String bodyString = mrw.getBody();
logger.info("getted requestbody data is ---> " + bodyString);
// 獲取幾個(gè)相關(guān)的字符
// 由于authorization類似于
// cardid="1234554321",timestamp="9897969594",signature="a69eae32a0ec746d5f6bf9bf9771ae36"
// 這樣的,所以邏輯是下面這樣的
int cardidIndex = info[0].indexOf("=") + 2;
String cardid = info[0].substring(cardidIndex, info[0].length() - 1);
logger.info("cardid is ---> " + cardid);
int timestampIndex = info[1].indexOf("=") + 2;
String timestamp = info[1].substring(timestampIndex, info[1].length() - 1);
int signatureIndex = info[2].indexOf("=") + 2;
String signature = info[2].substring(signatureIndex, info[2].length() - 1);
String tmptString = MDUtils.MD5EncodeForHex(timestamp + secret + bodyString, "UTF-8")
.toUpperCase();
logger.info("getted ciphertext is ---> {}, correct ciphertext is ---> {}",
signature , tmptString);
// 判斷該ip是否合法
boolean containIp = false;
for (String string : permittedIps) {
if (string.equals(ip)) {
containIp = true;
break;
}
}
// 再判斷Authorization內(nèi)容是否正確,進(jìn)而判斷是否最終放行
boolean couldPass = containIp && tmptString.equals(signature);
if (couldPass) {
// 放行
chain.doFilter(mrw, response);
return;
}
response.sendError(403, "Forbidden");
} catch (Exception e) {
logger.error("AxbAuthenticationFilter -> " + e.getMessage(), e);
response.sendError(403, "Forbidden");
}
}
@Override
public void destroy() {
}
}
/**
* 輔助類 ---> 變相使得可以多次通過(guò)(不同)流讀取相同數(shù)據(jù)
*
* @author JustryDeng
* @DATE 2018年9月11日 下午7:13:52
*/
class MyRequestWrapper extends HttpServletRequestWrapper {
private final String body;
public String getBody() {
return body;
}
public MyRequestWrapper(final HttpServletRequest request) throws IOException {
super(request);
StringBuilder sb = new StringBuilder();
String line;
BufferedReader reader = request.getReader();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
body = sb.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes());
return new ServletInputStream() {
/*
* 重寫ServletInputStream的父類InputStream的方法
*/
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
第二步:在項(xiàng)目的啟動(dòng)類上添加@ServletComponentScan注解,使允許掃描
Servlet組件(過(guò)濾器、監(jiān)聽(tīng)器等)。

測(cè)試一下
測(cè)試說(shuō)明
客戶端ip在我們?cè)O(shè)置的ip白名單里面 且 timestamp + secret + bodyStringMD5加密后的字段與請(qǐng)求頭域中傳過(guò)來(lái)的signature值相同時(shí),才算鑒權(quán)通過(guò)。
說(shuō)明:
1.ip白名單 本示例中是設(shè)置在服務(wù)端的相應(yīng)服務(wù)的系統(tǒng)配置文件application.properties中的。
2.secret 是客戶端一方和服務(wù)端一方 定好的一個(gè)用來(lái)MD5加密的 數(shù),secret本身不進(jìn)行傳輸。
3.bodyString是服務(wù)端通過(guò)客戶端的request獲取到的請(qǐng)求體中的數(shù)據(jù)。
4.signature是客戶端加密后的值,服務(wù)端只需對(duì)原始數(shù)據(jù)進(jìn)行和客戶端進(jìn)一模一樣的加密,
將加密結(jié)果和傳導(dǎo)服務(wù)端的signature進(jìn)行比對(duì),一樣則鑒權(quán)通過(guò)。
啟動(dòng)項(xiàng)目,使用postman測(cè)試一下

給出程序打印的日志,更容易理解

提示:由于本人測(cè)試時(shí),我的電腦既是服務(wù)器又是客戶端,所以獲取到了那樣的ip。
注:當(dāng)ip或Authorization值中任意一個(gè)或兩個(gè) 不滿足條件時(shí),會(huì)返回給前端403(見(jiàn):SignAutheFilter中的相關(guān)代碼),
這里就不給出效果圖了。
由測(cè)試結(jié)果可知:簽名鑒權(quán)成功!
測(cè)試項(xiàng)目代碼托管鏈接: https://github.com/JustryDeng/PublicRepository
到此這篇關(guān)于SpringBoot使用Filter實(shí)現(xiàn)簽名認(rèn)證鑒權(quán)的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot Filter簽名認(rèn)證鑒權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA SpringBoot:Cannot resolve configuration&
這篇文章主要介紹了IDEA SpringBoot:Cannot resolve configuration property配置文件問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
mybatis執(zhí)行update批量更新時(shí)報(bào)錯(cuò)的解決方案
這篇文章主要介紹了mybatis執(zhí)行update批量更新時(shí)報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
springboot攔截器不攔截靜態(tài)資源,只攔截controller的實(shí)現(xiàn)方法
這篇文章主要介紹了springboot攔截器不攔截靜態(tài)資源,只攔截controller的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
java?Export大量數(shù)據(jù)導(dǎo)出和打包
這篇文章主要為大家介紹了java?Export大量數(shù)據(jù)的導(dǎo)出和打包實(shí)現(xiàn)過(guò)程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
MyBatis自定義類型轉(zhuǎn)換器實(shí)現(xiàn)加解密
這篇文章主要介紹了MyBatis自定義類型轉(zhuǎn)換器實(shí)現(xiàn)加解密的相關(guān)資料,需要的朋友可以參考下2016-07-07

