接口的接入授权一般都有一套固定的模式,请求方通过对相关参数进行加密签名,接收方对接收到的参数信息进行同样的签名,并判断两个签名是否相同,以此来判断请求的合法性。
与授权有关的参数(一般包括请求时间,请求序号,请求接入id,请求签名等)可以和业务参数一起传递,也可以将授权相关参数通过请求头的方式传递。将授权相关参数通过请求头进行传递,并且通过interceptor和filter技术,在controller接收请求以前进行授权判断,这样controller就只需要处理正常的业务请求,使得业务处理更加简洁,不会和授权处理混在一起。
签名信息也包含业务信息,所以在进行签名验证的时候,需要读取request的请求体,但是对于post请求,请求体只能读取一次,第二次读取会出现异常,导致controller无法进行业务处理,错误信息类似如下:
getReader() has already been called for this request
此时需要在filter层,对请求信息进行自定义扩展处理,经过处理后的请求,才支持多次读取请求体信息。
1. 自定义request的包装类
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
public class MyRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public MyRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
BufferedReader reader = request.getReader();
try (StringWriter writer = new StringWriter()) {
int read;
char[] buf = new char[1024 * 8];
while ((read = reader.read(buf)) != -1) {
writer.write(buf, 0, read);
}
this.body = writer.getBuffer().toString().getBytes();
}
}
public byte[] getBody() {
return body;
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public boolean isReady() {
return false;
}
@Override
public boolean isFinished() {
return false;
}
};
}
}
在包装类中,首先读取请求体的内容,并将内容保存到自定义的属性中,这样后续读取的时候,就从自定义的属性中读取,自定义的属性读取是没有次数限制的。
2. 定义filter类,并注册filter
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.interceptor.MyRequestWrapper;
import org.springframework.http.HttpMethod;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class AccessFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
//如果是POST走自己的继承的HttpServletRequestWrapper类请求,否则走正常的请求
if(StringUtils.equalsIgnoreCase((), request.getMethod())){
//一定要在判断中new对象,否则还会出现Stream closed问题
filterChain.doFilter(new MyRequestWrapper(request),servletResponse);
}else{
filterChain.doFilter(servletRequest,servletResponse);
}
}
@Override
public void destroy() {
}
}
可以看到,在filter中,将原始的ServletRequest 采用自定义的包装类进行包装之后,再传递到后续进行处理。
@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
@Bean
public FilterRegistrationBean httpServletRequestReplacedFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new AccessFilter());
// /* 是全部的请求拦截,和Interceptor的拦截地址/**区别开
registration.addUrlPatterns("/*");
registration.setName("accessRequestFilter");
registration.setOrder(1);
return registration;
}
}
进行过滤器注册,这样程序启动后,过滤器就会对请求进行处理。
3. 定义拦截器,并注册interceptor
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SignUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.operate.domain.PlatAccessPerm;
import com.ruoyi.operate.service.IPlatAccessPermService;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@Component
public class AccessPermAuthInterceptor implements HandlerInterceptor {
protected static Logger logger = LoggerFactory.getLogger(AccessPermAuthInterceptor.class);
@Autowired
private IPlatAccessPermService permService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String clientSign = request.getHeader(SignUtils.SIGN_HEADER_KEY);
String key = request.getHeader(SignUtils.ACCESS_KEY_HEADER_KEY);
String id = request.getHeader(SignUtils.REQUEST_ID_HEADER_KEY);
String time = request.getHeader(SignUtils.REQUEST_TIME_HEADER_KEY);
if(StringUtils.isEmpty(clientSign) || StringUtils.isEmpty(key)
|| StringUtils.isEmpty(id) || StringUtils.isEmpty(time))
{
throw new AuthorizationException("请求信息无效");
}
PlatAccessPerm platAccessPerm = new PlatAccessPerm();
platAccessPerm.setAccessKey(key);
List<PlatAccessPerm> platAccessPerms = permService.selectPlatAccessPermList(platAccessPerm);
if(platAccessPerms != null && platAccessPerms.size() > 0)
{
platAccessPerm = platAccessPerms.get(0);
}
else
{
response.addHeader(SignUtils.REQUEST_TIME_HEADER_KEY, DateUtils.getTime());
response.addHeader(SignUtils.REQUEST_ID_HEADER_KEY, UUID.randomUUID().toString());
throw new AuthorizationException("授权信息无效");
}
String accessSecret = platAccessPerm.getAccessSecret();
String path = "/gateway/test/testdemo";
Map<String, String> reqHeaders = new HashMap<>();
reqHeaders.put(SignUtils.ACCESS_KEY_HEADER_KEY, key);
reqHeaders.put(SignUtils.REQUEST_TIME_HEADER_KEY, time);
reqHeaders.put(SignUtils.REQUEST_ID_HEADER_KEY, id);
MyRequestWrapper myrequest = (MyRequestWrapper)request;
byte[] reqBody = myrequest.getBody();
String reqJson = "{\"userName\":\"test\"}";
reqJson = new String(reqBody);
Gson gson = new Gson();
JsonObject userObject = gson.fromJson(reqJson, JsonObject.class);
String sign = SignUtils.sign(key, accessSecret, path, userObject, reqHeaders);
System.out.println("sign:" + sign);
response.addHeader(SignUtils.REQUEST_TIME_HEADER_KEY, DateUtils.getTime());
response.addHeader(SignUtils.REQUEST_ID_HEADER_KEY, UUID.randomUUID().toString());
if(!sign.equals(clientSign))
{
throw new AuthorizationException("签名验证失败!");
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
可以看到这里通过以下两行代码进行的请求体的读取:
MyRequestWrapper myrequest = (MyRequestWrapper)request;
byte[] reqBody = myrequest.getBody();
之所以能够读取,就是因为过滤器对请求进行了包装处理。
import com.ruoyi.framework.interceptor.AccessPermAuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants;
@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
@Autowired
private AccessPermAuthInterceptor permAuthInterceptor;
/**
* 自定义拦截规则
*/
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(permAuthInterceptor).addPathPatterns("/**");
}
}
以上是拦截器注册代码。
经过以上的准备工作,可以实现在interceptor中对请求的签名进行验签,而在controller层,只会接收到签名验签通过的请求,所以controller可以专注于业务处理,而不需要处理签名验签相关的内容。