SpringBoot使用Filter实现签名认证鉴权

情景说明
        鉴权,有很多方案,如:SpringSecurity、Shiro、拦截器、过滤器等等。如果只是对一些URL进行认证鉴权的话,我们完
全没必要引入SpringSecurity或Shiro等框架,使用拦截器或过滤器就足以实现需求。
        本文介绍如何使用过滤器Filter实现URL签名认证鉴权。

本人测试软硬件环境:Windows10、idea、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注解

在项目的启动类上添加@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中的相关代码),
     这里就不给出效果图了。

由测试结果可知:签名鉴权成功!

 

前端参数处理:

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);
}


 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值