准备工作

第一步:在pom.xml中约会相关依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-test</artifactId>
  9. <scope>test</scope>
  10. </dependency>
  11. <!-- web -->
  12. <dependency>
  13. <groupId>org.springframework.boot</groupId>
  14. <artifactId>spring-boot-starter-web</artifactId>
  15. </dependency>
  16. <!-- devtools -->
  17. <dependency>
  18. <groupId>org.springframework.boot</groupId>
  19. <artifactId>spring-boot-devtools</artifactId>
  20. </dependency>
  21. <!-- org.apache.commons.codec -->
  22. <!-- MD5加密的依赖 -->
  23. <dependency>
  24. <groupId>org.apache.directory.studio</groupId>
  25. <artifactId>org.apache.commons.codec</artifactId>
  26. <version>1.8</version>
  27. </dependency>
  28. </dependencies>

第二步:在系统配置文件application.properties中配置相关参数,一会儿代码中需要用到

#ip 白名单(多个使用逗号分隔)allowed-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的工具类

  1. import java.net.InetAddress;
  2. import java.net.UnknownHostException;
  3. import javax.servlet.http.HttpServletRequest;
  4. /**
  5. * 获取发出request请求的客户端ip
  6. * 注:如果是自己发出的请求,那么获取的是自己的ip
  7. * 摘录自https://blog.csdn.net/byy8023/article/details/80499038
  8. *
  9. * 注意事项:
  10. * 如果使用此工具,获取到的不是客户端的ip地址;而是虚拟机的ip地址(d当客户端安装有VMware时,可能出现此情况);
  11. * 那么需要在客户端的[控制面板\网络和 Internet\网络连接]中禁用虚拟机网络适配器
  12. *
  13. * @author JustryDeng
  14. * @DATE 2018年9月10日 下午8:56:48
  15. */
  16. public class IpUtil {
  17. public static String getIpAddr(HttpServletRequest request) {
  18. String ipAddress = null;
  19. try {
  20. ipAddress = request.getHeader("x-forwarded-for");
  21. if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
  22. ipAddress = request.getHeader("Proxy-Client-IP");
  23. }
  24. if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
  25. ipAddress = request.getHeader("WL-Proxy-Client-IP");
  26. }
  27. if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
  28. ipAddress = request.getRemoteAddr();
  29. if (ipAddress.equals("127.0.0.1")) {
  30. // 根据网卡取本机配置的IP
  31. InetAddress inet = null;
  32. try {
  33. inet = InetAddress.getLocalHost();
  34. } catch (UnknownHostException e) {
  35. e.printStackTrace();
  36. }
  37. ipAddress = inet.getHostAddress();
  38. }
  39. }
  40. // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
  41. if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
  42. // = 15
  43. if (ipAddress.indexOf(",") > 0) {
  44. ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
  45. }
  46. }
  47. } catch (Exception e) {
  48. ipAddress="";
  49. }
  50. return ipAddress;
  51. }
  52. }

第四步:准备MD5加密工具类

  1. import java.io.UnsupportedEncodingException;
  2. import java.security.MessageDigest;
  3. import java.security.NoSuchAlgorithmException;
  4. import org.apache.commons.codec.binary.Hex;
  5. /**
  6. * MD5加密工具类
  7. *
  8. * @author JustryDeng 参考自ShaoJJ的MD5加密工具类
  9. * @DATE 2018年9月11日 下午2:14:21
  10. */
  11. public class MDUtils {
  12. /**
  13. * 加密
  14. *
  15. * @param origin
  16. * 要被加密的字符串
  17. * @param charsetname
  18. * 加密字符,如UTF-8
  19. * @DATE 2018年9月11日 下午2:12:51
  20. */
  21. public static String MD5EncodeForHex(String origin, String charsetname)
  22. throws UnsupportedEncodingException, NoSuchAlgorithmException {
  23. return MD5EncodeForHex(origin.getBytes(charsetname));
  24. }
  25. public static String MD5EncodeForHex(byte[] origin) throws NoSuchAlgorithmException {
  26. return Hex.encodeHexString(digest("MD5", origin));
  27. }
  28. /**
  29. * 指定加密算法
  30. *
  31. * @throws NoSuchAlgorithmException
  32. * @DATE 2018年9月11日 下午2:11:58
  33. */
  34. private static byte[] digest(String algorithm, byte[] source) throws NoSuchAlgorithmException {
  35. MessageDigest md;
  36. md = MessageDigest.getInstance(algorithm);
  37. return md.digest(source);
  38. }
  39. }

第五步:简单编写一个Controller,方便后面的测试

【20200225】SpringBoot使用过滤器实现签名认证鉴权 - 图2

【20200225】SpringBoot使用过滤器实现签名认证鉴权 - 图3

SpringBoot使用过滤器实现签名认证鉴权-逻辑代码

初步:编写过滤器

  1. import java.io.BufferedReader;
  2. import java.io.ByteArrayInputStream;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import javax.servlet.Filter;
  6. import javax.servlet.FilterChain;
  7. import javax.servlet.FilterConfig;
  8. import javax.servlet.ReadListener;
  9. import javax.servlet.ServletException;
  10. import javax.servlet.ServletInputStream;
  11. import javax.servlet.ServletRequest;
  12. import javax.servlet.ServletResponse;
  13. import javax.servlet.annotation.WebFilter;
  14. import javax.servlet.http.HttpServletRequest;
  15. import javax.servlet.http.HttpServletRequestWrapper;
  16. import javax.servlet.http.HttpServletResponse;
  17. import org.slf4j.Logger;
  18. import org.slf4j.LoggerFactory;
  19. import org.springframework.beans.factory.annotation.Value;
  20. import com.aspire.util.IpUtil;
  21. import com.aspire.util.MDUtils;
  22. /**
  23. * SpringBoot使用拦截器实现签名认证(鉴权)
  24. * @WebFilter注解指定要被过滤的URL
  25. * 一个URL会被多个过滤器过滤时,还可以使用@Order(x)来指定过滤request的先后顺序,x数字越小越先过滤
  26. *
  27. * @author JustryDeng
  28. * @DATE 2018年9月11日 下午1:18:29
  29. */
  30. @WebFilter(urlPatterns = { "/authen/test1", "/authen/test2", "/authen/test3"})
  31. public class SignAutheFilter implements Filter {
  32. private static Logger logger = LoggerFactory.getLogger(SignAutheFilter.class);
  33. @Value("${permitted-ips}")
  34. private String[] permittedIps;
  35. @Value("${secret}")
  36. private String secret;
  37. @Override
  38. public void init(FilterConfig filterConfig) throws ServletException {
  39. }
  40. @Override
  41. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
  42. throws IOException, ServletException {
  43. HttpServletRequest request = (HttpServletRequest) req;
  44. HttpServletResponse response = (HttpServletResponse) res;
  45. try {
  46. String authorization = request.getHeader("Authorization");
  47. logger.info("getted Authorization is ---> " + authorization);
  48. String[] info = authorization.split(",");
  49. // 获取客户端ip
  50. String ip = IpUtil.getIpAddr(request);
  51. logger.info("getted ip is ---> " + ip);
  52. /*
  53. * 读取请求体中的数据(字符串形式)
  54. * 注:由于同一个流不能读取多次;如果在这里读取了请求体中的数据,那么@RequestBody中就不能读取到了
  55. * 会抛出异常并提示getReader() has already been called for this request
  56. * 解决办法:先将读取出来的流数据存起来作为一个常量属性.然后每次读的时候,都需要先将这个属性值写入,再读出.
  57. * 即每次获取的其实是不同的流,但是获取到的数据都是一样的.
  58. * 这里我们借助HttpServletRequestWrapper类来实现
  59. * 注:此方法涉及到流的读写、耗性能;
  60. */
  61. MyRequestWrapper mrw = new MyRequestWrapper(request);
  62. String bodyString = mrw.getBody();
  63. logger.info("getted requestbody data is ---> " + bodyString);
  64. // 获取几个相关的字符
  65. // 由于authorization类似于
  66. // cardid="1234554321",timestamp="9897969594",signature="a69eae32a0ec746d5f6bf9bf9771ae36"
  67. // 这样的,所以逻辑是下面这样的
  68. int cardidIndex = info[0].indexOf("=") + 2;
  69. String cardid = info[0].substring(cardidIndex, info[0].length() - 1);
  70. logger.info("cardid is ---> " + cardid);
  71. int timestampIndex = info[1].indexOf("=") + 2;
  72. String timestamp = info[1].substring(timestampIndex, info[1].length() - 1);
  73. int signatureIndex = info[2].indexOf("=") + 2;
  74. String signature = info[2].substring(signatureIndex, info[2].length() - 1);
  75. String tmptString = MDUtils.MD5EncodeForHex(timestamp + secret + bodyString, "UTF-8")
  76. .toUpperCase();
  77. logger.info("getted ciphertext is ---> {}, correct ciphertext is ---> {}",
  78. signature , tmptString);
  79. // 判断该ip是否合法
  80. boolean containIp = false;
  81. for (String string : permittedIps) {
  82. if (string.equals(ip)) {
  83. containIp = true;
  84. break;
  85. }
  86. }
  87. // 再判断Authorization内容是否正确,进而判断是否最终放行
  88. boolean couldPass = containIp && tmptString.equals(signature);
  89. if (couldPass) {
  90. // 放行
  91. chain.doFilter(mrw, response);
  92. return;
  93. }
  94. response.sendError(403, "Forbidden");
  95. } catch (Exception e) {
  96. logger.error("AxbAuthenticationFilter -> " + e.getMessage(), e);
  97. response.sendError(403, "Forbidden");
  98. }
  99. }
  100. @Override
  101. public void destroy() {
  102. }
  103. }
  1. /**
  2. * 辅助类 ---> 变相使得可以多次通过(不同)流读取相同数据
  3. *
  4. * @author JustryDeng
  5. * @DATE 2018年9月11日 下午7:13:52
  6. */
  7. class MyRequestWrapper extends HttpServletRequestWrapper {
  8. private final String body;
  9. public String getBody() {
  10. return body;
  11. }
  12. public MyRequestWrapper(final HttpServletRequest request) throws IOException {
  13. super(request);
  14. StringBuilder sb = new StringBuilder();
  15. String line;
  16. BufferedReader reader = request.getReader();
  17. while ((line = reader.readLine()) != null) {
  18. sb.append(line);
  19. }
  20. body = sb.toString();
  21. }
  22. @Override
  23. public ServletInputStream getInputStream() throws IOException {
  24. final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes());
  25. return new ServletInputStream() {
  26. /*
  27. * 重写ServletInputStream的父类InputStream的方法
  28. */
  29. @Override
  30. public int read() throws IOException {
  31. return bais.read();
  32. }
  33. @Override
  34. public boolean isFinished() {
  35. return false;
  36. }
  37. @Override
  38. public boolean isReady() {
  39. return false;
  40. }
  41. @Override
  42. public void setReadListener(ReadListener listener) {
  43. }
  44. };
  45. }
  46. @Override
  47. public BufferedReader getReader() throws IOException {
  48. return new BufferedReader(new InputStreamReader(this.getInputStream()));
  49. }
  50. }

第二步:添加@ServletComponentScan注解

在项目的启动类上添加@ServletComponentScan注解,使允许扫描Servlet组件(过滤器,监听器等)。

测试一下

测试说明

  1. 客户端ip在我们设置的ip白名单里面并且时间戳+秘密+ bodyStringMD5加密后的分区与请求头域中传过来的签名值相同时,才算鉴权通过。<br />说明:<br /> 2.secret<br /> 是客户端一方和服务端一方定好的一个使用MD5加密的数量,secret本身不进行传输<br /> 3.bodyString是服务端通过客户端的请求获取到请求<br /> 实体的数据。4.signature是客户端加密后的值,服务端只需对原始数据进行和客户端进一模一样的加密,<br /> 将加密结果和合并服务端的signature进行比对,一样则鉴权通过。<br /> <br />启动项目,使用postman测试一下<br />![](https://cdn.nlark.com/yuque/0/2020/png/105712/1582633890656-76a131b6-4c42-4fd1-abf5-e2a106d467ac.png#align=left&display=inline&height=556&originHeight=556&originWidth=1083&size=0&status=done&style=none&width=1083)<br />更加容易理解<br />提示:由于本人测试时,我的电脑既是服务器又是客户端,所以获取到了那样的ip。<br />注:当ip或Authorization值中任意一个或两个不满足条件时,会返回给前端403(见:SignAutheFilter中的相关代码),<br /> 这里就不赋予效果图了。<br />由测试结果可知:签名鉴权成功!<br /> <br />前端参数处理:<br />sign.js
  1. /*
  2. * @Author: chenjun
  3. * @Date: 2017-12-28 17:09:21
  4. * @Last Modified by: 0easy-23
  5. * @Last Modified time: 2017-12-29 10:09:23
  6. * 签名生成
  7. * kAppKey,kAppSecret为常量,
  8. * params,传入的参数,string || object
  9. * 需要借助md5.js
  10. * 规则:将所有参数字段按首字母排序, 拼接成key1 = value1 & key2 = value2的格式,再在末尾拼接上key = appSecret, 再做MD5加密生成sign
  11. */
  12. function getSign(params, kAppKey, kAppSecret) {
  13. if (typeof params == "string") {
  14. return paramsStrSort(params);
  15. } else if (typeof params == "object") {
  16. var arr = [];
  17. for (var i in params) {
  18. arr.push((i + "=" + params[i]));
  19. }
  20. return paramsStrSort(arr.join(("&")));
  21. }
  22. }
  23. function paramsStrSort(paramsStr) {
  24. var url = paramsStr + "&appKey=" + kAppKey;
  25. var urlStr = url.split("&").sort().join("&");
  26. var newUrl = urlStr + '&key=' + kAppSecret;
  27. return md5(newUrl);
  28. }