情景说明
鉴权,有很多方案,如: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,方便后面的测试
![](https://i-blog.csdnimg.cn/blog_migrate/5c073ee6afa24cffd8d2f10e580d0613.png)
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中的相关代码),
这里就不给出效果图了。
由测试结果可知:签名鉴权成功!
前端参数处理:
/*
* @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);
}