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

青锋产品介绍

青锋目前开源架构如下:

去码云搜索:青锋(名字不要打错)或者去github搜索:青锋(最新版本以gitee码云为主)

参数说明

应用:appId

应用秘钥:appSecret

时间戳:timestamp

随机字符串:noncestr

签名字符串:signature

客户端使用说明
  1. 客户端可以为前后端分离的vue或者react。可以为:Android、iOS或者uniapp等移动端应用,都可以采用认证签名授权访问接口。
  2. 我这边主要的业务是给第三方应用提供对接接口,因为三方不需要登录,但是又不能随便让他们请求接口,就采用签名授权的方式对外公布我们的接口。
  1. 客户端需要生成签名,生成规则:
  2. signature = Md5(timestamp noncestr appSecret body).toUpper()
  1. 签名的规则是时间戳 随机字符串 应用授权秘钥 请求body实体。当然规则很多,大家可以根据自己的实际情况进行设置,客户端如何加密生成签名,服务端就根据响应的方式解密即可。

传递参数说明:

例如请求地址:http://127.0.0.1:8080/qingfeng/port/findInfo

请求header:Authorization = appId="8qj0y778",timestamp="9897969594",signature="9E26BB3F746DCD982435221AA03DA400",noncestr=xxx

请求实体:{"name":"张三","type":"1"}

springboot优雅实现版本控制(springboot接口Sign授权签名认证-拦截器)(1)

springboot优雅实现版本控制(springboot接口Sign授权签名认证-拦截器)(2)

服务端解密

服务端通过使用Filter实现签名认证鉴权,服务端获取客户传递的timestamp noncestr body生成签名,然后比对传递的签名是否一致。

package com.qingfeng.framework.servlet; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.qingfeng.common.service.AuthenService; import com.qingfeng.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; /** * @ProjectName SignAutheFilter * @author qingfeng * @version 1.0.0 * SpringBoot使用拦截器实现签名认证(鉴权) * @WebFilter注解指定要被过滤的URL * 一个URL会被多个过滤器过滤时,还可以使用@Order(x)来指定过滤request的先后顺序,x数字越小越先过滤 * @createTime 2021/8/26 14:35 */ @WebFilter(urlPatterns = { "/port/*","/myport/*" },filterName = "securityRequestFilter") public class SignAutheFilter implements Filter { private static Logger logger = LoggerFactory.getLogger(SignAutheFilter.class); @Autowired private AuthenService authenService; @Value("${permittedIps}") private String permittedIps; // @Value("${appId}") // private String appId; // @Value("${appSecret}") // private String appSecret; @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; String msg = ""; try { String authorization = request.getHeader("Authorization"); System.out.println("getted Authorization is ---> " authorization); String[] info = authorization.split(","); // 获取客户端Ip String ip = IpUtils.getIpAddr(request); System.out.println("getted ip is ---> " ip); /* * 读取请求体中的数据(字符串形式) * 注:由于同一个流不能读取多次;如果在这里读取了请求体中的数据,那么@RequestBody中就不能读取到了 * 会抛出异常并提示getReader() has already been called for this request * 解决办法:先将读取出来的流数据存起来作为一个常量属性.然后每次读的时候,都需要先将这个属性值写入,再读出. * 即每次获取的其实是不同的流,但是获取到的数据都是一样的. * 这里我们借助HttpServletRequestWrapper类来实现 * 注:此方法涉及到流的读写、耗性能; */ MyRequestWrapper mrw = new MyRequestWrapper(request); String bodyString = mrw.getBody(); System.out.println("getted requestbody data is ---> " bodyString); // 获取几个相关的字符 // 由于authorization类似于 // cardid="1234554321",timestamp="9897969594",signature="a69eae32a0ec746d5f6bf9bf9771ae36" // 这样的,所以逻辑是下面这样的 int cardidIndex = info[0].indexOf("=") 2; String appid = info[0].substring(cardidIndex, info[0].length() - 1); System.out.println("appid is ---> " appid); 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); //根据appid查询秘钥 PageData param = new PageData(); param.put("appId",appid); PageData p = authenService.findInfoForAppId(param); String tmptString = ""; if(Verify.verifyIsNotNull(p)){ System.out.println(timestamp "," p.get("appSecret") "," bodyString); tmptString = MD5.md5Upper(timestamp "," p.get("appSecret") "," bodyString); System.out.println(signature ":" tmptString); }else{ msg = "应用ID(appId)不存在。"; } // 判断该ip是否合法 boolean containIp = false; if(permittedIps.contains("0.0.0.0")){ containIp = true; }else{ for (String string : permittedIps.split(",")) { if (string.equals(ip)) { containIp = true; break; } } } // 再判断Authorization内容是否正确,进而判断是否最终放行 boolean couldPass = containIp && tmptString.equals(signature); if (couldPass) { // 放行 System.out.println("====================放行================"); chain.doFilter(mrw, response); return; }else{ msg = "签名错误或请求IP未授权。"; } ServletOutputStream out = response.getOutputStream(); Json json = new Json(); json.setSuccess(false); json.setMsg("403 Forbidden:" msg); response.setContentType("text/html;charset=utf-8"); ObjectMapper objMapper = new ObjectMapper(); JsonGenerator jsonGenerator = objMapper.getJsonFactory() .createJsonGenerator(response.getOutputStream(), JsonEncoding.UTF8); jsonGenerator.writeObject(json); jsonGenerator.flush(); jsonGenerator.close(); } catch (Exception e) { logger.error(e.getMessage(), e); Json json = new Json(); json.setSuccess(false); json.setMsg("403 Forbidden:" e.getMessage()); response.setContentType("text/html;charset=utf-8"); ObjectMapper objMapper = new ObjectMapper(); JsonGenerator jsonGenerator = objMapper.getJsonFactory() .createJsonGenerator(response.getOutputStream(), JsonEncoding.UTF8); jsonGenerator.writeObject(json); jsonGenerator.flush(); jsonGenerator.close(); } } @Override public void destroy() { } } /** * 辅助类 ---> 变相使得可以多次通过(不同)流读取相同数据 * * @author qingfeng * @createTime 2021/8/26 14:35 */ 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())); } }

如果对请求的IP进行验证,可以在配置文件中加入:

# ip白名单(多个使用逗号分隔) permitted-ips = 169.254.205.177, 169.254.133.33, 10.8.109.31, 0:0:0:0:0:0:0:1

在验证过滤器中进行验证,代码如下(业务代码可以根据自己的情况进行设置):

//注入id集合 @Value("${permitted-ips}") private String[] permittedIps; // 获取客户端ip String ip = IpUtil.getIpAddr(request); logger.info("getted ip is ---> " ip); // 判断该ip是否合法 boolean containIp = false; for (String string : permittedIps) { if (string.equals(ip)) { containIp = true; break; } }

签名概念

请求参数:access_key、nonce、timestamp、业务参数列表...、sign签名

签名:access_key nonce timestamp 业务参数列表 secret_key,业务参数列表按key自然排序,上各参数以&拼接后进行md5加密,md5转为大写作为sign签名

access_key:用户身份标识,前后端约定

nonce:随机字符串,请求唯一标识,每个请求的nonce均被缓存在服务端(redis),并设置10分钟有效期,防止缓存不断累积

timestamp:当前时间戳,用于判断请求与当前时间差,与nonce对应,设置10分钟有效期,超过时间请求失效,既防止请求长期有效又允许客户端和服务端之间存在10分钟时间差

secret_key:密钥,用于生成sign签名,前后端约定,不能泄漏

服务端:拦截请求(拦截器或AOP均可,配合注解拦截指定接口,便于区分拦截不同接口),根据参数重构md5签名,依次检查access_key、nonce、timestamp、sign的一致性和准确性

简化,不缓存可不携带nonce,将timestamp有效期设置在2~3分钟

,