准备工作
第一步:在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 白名单(多个使用逗号分隔)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的工具类
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")) {// 根据网卡取本机配置的IPInetAddress inet = null;try {inet = InetAddress.getLocalHost();} catch (UnknownHostException e) {e.printStackTrace();}ipAddress = inet.getHostAddress();}}// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()// = 15if (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使用过滤器实现签名认证鉴权-逻辑代码
初步:编写过滤器
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;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic 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(",");// 获取客户端ipString 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");}}@Overridepublic 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();}@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes());return new ServletInputStream() {/** 重写ServletInputStream的父类InputStream的方法*/@Overridepublic int read() throws IOException {return bais.read();}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener listener) {}};}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(this.getInputStream()));}}
第二步:添加@ServletComponentScan注解
在项目的启动类上添加@ServletComponentScan注解,使允许扫描Servlet组件(过滤器,监听器等)。
测试一下
测试说明
客户端ip在我们设置的ip白名单里面并且时间戳+秘密+ bodyStringMD5加密后的分区与请求头域中传过来的签名值相同时,才算鉴权通过。<br />说明:<br /> 2.secret<br /> 是客户端一方和服务端一方定好的一个使用MD5加密的数量,secret本身不进行传输<br /> 。3.bodyString是服务端通过客户端的请求获取到请求<br /> 实体的数据。4.signature是客户端加密后的值,服务端只需对原始数据进行和客户端进一模一样的加密,<br /> 将加密结果和合并服务端的signature进行比对,一样则鉴权通过。<br /> <br />启动项目,使用postman测试一下<br /><br />更加容易理解<br />提示:由于本人测试时,我的电脑既是服务器又是客户端,所以获取到了那样的ip。<br />注:当ip或Authorization值中任意一个或两个不满足条件时,会返回给前端403(见:SignAutheFilter中的相关代码),<br /> 这里就不赋予效果图了。<br />由测试结果可知:签名鉴权成功!<br /> <br />前端参数处理:<br />sign.js
/** @Author: chenjun* @Date: 2017-12-28 17:09:21* @Last Modified by: 0easy-23* @Last Modified time: 2017-12-29 10:09:23* 签名生成* kAppKey,kAppSecret为常量,* params,传入的参数,string || object* 需要借助md5.js* 规则:将所有参数字段按首字母排序, 拼接成key1 = value1 & key2 = value2的格式,再在末尾拼接上key = appSecret, 再做MD5加密生成sign*/function getSign(params, kAppKey, kAppSecret) {if (typeof params == "string") {return paramsStrSort(params);} else if (typeof params == "object") {var arr = [];for (var i in params) {arr.push((i + "=" + params[i]));}return paramsStrSort(arr.join(("&")));}}function paramsStrSort(paramsStr) {var url = paramsStr + "&appKey=" + kAppKey;var urlStr = url.split("&").sort().join("&");var newUrl = urlStr + '&key=' + kAppSecret;return md5(newUrl);}

