环境:SpringBoot 2.0 以上的版本
假设目前有一种场景,我需要把所有请求都统一用一种方法来接收参数,同时还能加塞Request的参数。
例如:不管请求的参数是否是JSON,所有的参数接收处不添加 @RequestBody 注解。
LoginController.java
@PostMapping("/login")
public Result login(LoginDTO dto) {
log.info("");
return loginService.login(dto);
}
这里,我们想要做到上述的效果,需要在过滤器上针对Request对象进行处理。
这里简单讲解下思路:
首先,我是使用了SpringSecurity框架来维护系统的用户权限的,但用户登陆后的信息是统一存放Redis中。每次用户请求时如果Token校验通过,就会从Redis获取用户登陆的信息,加塞进Security的会话中。
针对用户校验及权限这一块,如果不需要的,可以删掉,从48-60行。
主要是通过继承了HttpServletRequestWrapper类解决重写Reqeust参数的问题。
因为Request内置的params参数是final声明,所以我们无法通过直接更改该Map,所以重新定义了一个Map,然后是为了兼容JSON格式的数据,
核心是构造方法 :
初始化Map,然后根据对应的请求类型进行处理:
1.如果是JSON或XML。我们就通过读取数据流的saveInputStreamData方法,然后解析到的数据加塞到Map中。
2.如果是普通的表单类型数据,我们就直接加塞到Map中。
关于saveInputStreamData方法:JSON请求只能通过流来获取其请求参数。而请求数据流只能读取一次。所以我们在读取了数据流后将读取到的数据保存下来,通过重写getInputStream方法来保证后续也能获取到请求数据流。
然后额外添加了addAllParameters、addParameters方法,方便加塞参数。
ParameterRequestWrapper方法:从Redis中解析出来的用户数据,我们加塞到请求参数中。
package com.mrlv.rua.auth.filter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.mrlv.rua.auth.consts.RedisPreConst;
import com.mrlv.rua.auth.entity.LoginUser;
import com.mrlv.rua.auth.utils.JwtUtil;
import com.mrlv.rua.common.redis.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author lvshiyu
* @description: 动态权限过滤器
* @date 2022年07月05日 17:32
*/
@Component
@Slf4j
public class AuthTokenFilter extends OncePerRequestFilter {
/**
* 过滤
* @param request
* @param response
* @param filterChain
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头,判断是否已经登陆
String token = request.getHeader(JwtUtil.HEADER_STRING);
LoginUser userDetails = null;
try {
if (StrUtil.isNotBlank(token)) {
String userId = JwtUtil.getUserId(token);
String key = RedisPreConst.AUTH_ONLINE_USER + userId;
if (RedisUtil.hHasKey(key, "PC")) {
//这里使用Redis来存储用户登陆信息,同时通过SpringScurity来对用户登陆状态进行维护,如果不是使用SpringScurity的话,可以无视这一部分。
userDetails = (LoginUser)RedisUtil.hget(key, "PC");
UsernamePasswordAuthenticationToken user = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(user);
}
}
request = new ParameterRequestWrapper(request, userDetails);
} catch (JWTVerificationException e) {
log.info("Token异常 error:{}", e.getMessage(), e);
} catch (Exception e) {
e.printStackTrace();
}
filterChain.doFilter(request, response);
}
/**
* @author lvshiyu
* @description: 重写 HttpServletRequestWrapper
* @date 2022年8月24日 14:50
*/
class ParameterRequestWrapper extends HttpServletRequestWrapper {
/**
* 请求参数
*/
private Map params;
/**
* 用于保存读取body中数据
*/
private byte[] body;
/**
* 用于保存读取body中数据
*/
private String bodyMessage;
/**
* 自定义构造方法
* @param request
* @throws IOException
*/
public ParameterRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
//参数保存
this.params = new HashMap<>();
//初始化参数
String contentType = request.getContentType().toLowerCase();
//如果是application/json
if (Objects.equals(ContentType.JSON.toString(), contentType)) {
//解析数据流数据
saveInputStreamData(request);
JSONObject parameter = JSONUtil.parseObj(this.getBodyMessage());
this.addAllParameters(parameter);
} else if (Objects.equals(ContentType.XML.toString(), contentType)) {
saveInputStreamData(request);
JSONObject parameter = JSONUtil.parseFromXml(this.getBodyMessage()).getJSONObject("request");
this.addAllParameters(parameter);
} else {
Enumeration headerNames = request.getParameterNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
this.addParameter(key, request.getParameter(key));
}
}
}
/**
* 自定义构造方法
* @param request
* @throws IOException
*/
public ParameterRequestWrapper(HttpServletRequest request, LoginUser user) throws IOException {
this(request);
if (user != null) {
this.addParameter("userId", String.valueOf(user.getId()));
this.addParameter("username", user.getUsername());
this.addParameter("nickname", user.getNickname());
}
}
/**
* 覆盖(重写)父类的方法
* @return
* @throws IOException
*/
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
* 覆盖(重写)父类的方法
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(this.body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return inputStream.read();
}
};
}
@Override
public Enumeration getParameterNames() {
return new Vector(params.keySet()).elements();
}
@Override
public String getParameter(String name) {
String[] values = this.params.get(name);
if (values == null || values.length == 0) {
return null;
}
return values[0];
}
@Override
public String[] getParameterValues(String name) {
String[] values = this.params.get(name);
if (values == null || values.length == 0) {
return null;
}
return values;
}
/**
* 获取body中的数据
* @return
*/
public byte[] getBody() {
return this.body;
}
/**
* 把处理后的参数放到body里面
* @param body
*/
public void setBody(byte[] body) {
this.body = body;
}
/**
* 获取处理过的参数数据
* @return
*/
public String getBodyMessage() {
return this.bodyMessage;
}
/**
* 设置参数
* @param otherParams
*/
private void addAllParameters(Map otherParams) {
for (Map.Entry entry : otherParams.entrySet()) {
addParameter(entry.getKey(), entry.getValue());
}
}
/**
* 设置参数
* @param name
* @param value
*/
private void addParameter(String name, Object value) {
if (this.params == null) {
this.params = new HashMap<>();
}
if (value != null) {
if (value instanceof String[]) {
this.params.put(name, (String[]) value);
} else if (value instanceof String) {
this.params.put(name, new String[]{(String) value});
} else {
this.params.put(name, new String[]{String.valueOf(value)});
}
}
}
/**
* 保存请求的InputSteam的数据
* @param request
* @throws IOException
*/
private void saveInputStreamData(HttpServletRequest request) throws IOException {
int contentLength = request.getContentLength();
ServletInputStream inputStream = request.getInputStream();
this.body = new byte[contentLength];
inputStream.read(this.body, 0, contentLength);
this.bodyMessage = new String(this.body, StandardCharsets.UTF_8);
}
}
}
在调试途中遇到了一个问题:
关于Request一旦使用了getInputStream后就无法通过getParameterMap获取参数的问题,通过源码我们可以发现,Request 中的 usingInputStream,标识是否使用过流来读取数据,如果使用过了,则会改为True。从而导致 getParameterMap 获取不到数据
org.apache.catalina.connector.Request
org.apache.catalina.connector.Request
org.apache.catalina.connector.Request
总结下来,一个过滤器就可以解决问题,如果能给诸位带来帮助,麻烦点个赞。有什么不理解的,欢迎留言。