SpringBoot使用Filter實現簽名認證鑒權的示例代碼
情景說明
鑒權,有很多方案,如:SpringSecurity、Shiro、攔截器、過濾器等等。如果隻是對一些URL進行認證鑒權的話,我們完
全沒必要引入SpringSecurity或Shiro等框架,使用攔截器或過濾器就足以實現需求。
本文介紹如何使用過濾器Filter實現URL簽名認證鑒權。
本人測試軟硬件環境:Windows10、Eclipse、SpringBoot、JDK1.8
準備工作
第一步:在pom.xml中引入相關依賴
<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>
第二步:在系統配置文件application.properties中配置相關參數,一會兒代碼中需要用到
# ip白名單(多個使用逗號分隔) 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
第三步:準備獲取客戶端IP的工具類
import java.net.InetAddress; import java.net.UnknownHostException; import javax.servlet.http.HttpServletRequest; /** * 獲取發出request請求的客戶端ip * 註:如果是自己發出的請求,那麼獲取的是自己的ip * 摘錄自https://blog.csdn.net/byy8023/article/details/80499038 * * 註意事項: * 如果使用此工具,獲取到的不是客戶端的ip地址;而是虛擬機的ip地址(d當客戶端安裝有VMware時,可能出現此情況); * 那麼需要在客戶端的[控制面板\網絡和 Internet\網絡連接]中禁用虛擬機網絡適配器 * * @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")) { // 根據網卡取本機配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ipAddress = inet.getHostAddress(); } } // 對於通過多個代理的情況,第一個IP為客戶端真實IP,多個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; } }
第四步:準備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); } }
第五步:簡單編寫一個Controller,方便後面的測試
SpringBoot使用Filter實現簽名認證鑒權 — 邏輯代碼
第一步:編寫過濾器
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使用攔截器實現簽名認證(鑒權) * @WebFilter註解指定要被過濾的URL * 一個URL會被多個過濾器過濾時,還可以使用@Order(x)來指定過濾request的先後順序,x數字越小越先過濾 * * @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); /* * 讀取請求體中的數據(字符串形式) * 註:由於同一個流不能讀取多次;如果在這裡讀取瞭請求體中的數據,那麼@RequestBody中就不能讀取到瞭 * 會拋出異常並提示getReader() has already been called for this request * 解決辦法:先將讀取出來的流數據存起來作為一個常量屬性.然後每次讀的時候,都需要先將這個屬性值寫入,再讀出. * 即每次獲取的其實是不同的流,但是獲取到的數據都是一樣的. * 這裡我們借助HttpServletRequestWrapper類來實現 * 註:此方法涉及到流的讀寫、耗性能; */ MyRequestWrapper mrw = new MyRequestWrapper(request); String bodyString = mrw.getBody(); logger.info("getted requestbody data is ---> " + bodyString); // 獲取幾個相關的字符 // 由於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內容是否正確,進而判斷是否最終放行 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() { } } /** * 輔助類 ---> 變相使得可以多次通過(不同)流讀取相同數據 * * @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())); } }
第二步:在項目的啟動類上添加@ServletComponentScan註解,使允許掃描
Servlet組件(過濾器、監聽器等)。
測試一下
測試說明
客戶端ip在我們設置的ip白名單裡面 且 timestamp + secret + bodyStringMD5加密後的字段與請求頭域中傳過來的signature值相同時,才算鑒權通過。
說明:
1.ip白名單 本示例中是設置在服務端的相應服務的系統配置文件application.properties中的。
2.secret 是客戶端一方和服務端一方 定好的一個用來MD5加密的 數,secret本身不進行傳輸。
3.bodyString是服務端通過客戶端的request獲取到的請求體中的數據。
4.signature是客戶端加密後的值,服務端隻需對原始數據進行和客戶端進一模一樣的加密,
將加密結果和傳導服務端的signature進行比對,一樣則鑒權通過。
啟動項目,使用postman測試一下
給出程序打印的日志,更容易理解
提示:由於本人測試時,我的電腦既是服務器又是客戶端,所以獲取到瞭那樣的ip。
註:當ip或Authorization值中任意一個或兩個 不滿足條件時,會返回給前端403(見:SignAutheFilter中的相關代碼),
這裡就不給出效果圖瞭。
由測試結果可知:簽名鑒權成功!
測試項目代碼托管鏈接: https://github.com/JustryDeng/PublicRepository
到此這篇關於SpringBoot使用Filter實現簽名認證鑒權的示例代碼的文章就介紹到這瞭,更多相關SpringBoot Filter簽名認證鑒權內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Java根據ip地址獲取歸屬地實例詳解
- springboot 獲取訪問接口的請求的IP地址的實現
- IDEA 2021配置JavaWeb項目超詳細教程
- idea搭建可運行Servlet的Web項目
- SpringBoot中使用Servlet三大組件的方法(Servlet、Filter、Listener)