Springboot Shiro 诡异的POST请求第一次请求接收参数都为null 第二次请求正常

本文围绕SpringMVC web应用中获取请求参数展开。指出使用request.getInputStream()获取数据为空的问题,分析原因是表单数据填充到parameterMap、请求流只能获取一次及方法间冲突。还给出解决方案,如设置Content-Type、遍历参数等,最后提出关于Spring Boot自动调用方法的疑问。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

看现象:

第一次请求:

 

 

后端接收请求数据为null

Shirohttpservlet 里面参数size 为0

 

第二次请求:

 

 

var data = {id: 6,
    name: "cs",
    pid:"",
    description: "cs",
    available: 1};
$.ajax({type:"POST",url:"/types/edit",data:data ,success:function(result){
        $("#div1").html(result);
    }});

ajax 请求传值接收为null

 

ajax请求没有问题 前端数据已经过来了

ResourceUrlEncodingFilter

第一次提交时 postData为null

 

 

Fillder监听已传值

点击请求

 

接受到数据

 

2019-06-20 20:45:03 [com.zyd.blog.controller.RestArticleController:87] INFO  - ======isMarkdown=false&id=9&title=jmeter%E4%B8%ADhtml%E6%8A%A5%E5%91%8A%E7%94%9F%E6%88%90%E6%96%B9%E5%BC%8F&content=%3Ch1%3Etest%3C%2Fh1%3E&description=jmeter%E4%B8%ADhtml%E6%8A%A5%E5%91%8A%E7%94%9F%E6%88%90%E6%96%B9%E5%BC%8F+%26nbsp%3B+1%E3%80%81%E4%BD%BF%E7%94%A8%E5%91%BD%E5%90%8D%EF%BC%9A++%26gt%3Bjmeter+-g+loginWithPassword100c180s.jtl+-o+.%2Freport+%E8%A7%A3%E9%87%8A%EF%BC%9Ajmeter+&keywords=jmeter&typeId=2&tags=5&status=1&original=on&comment=on&top=on&recommended=on&file=&coverImage=http%3A%2F%2Fimg.dmanis.cn%2Foneblog%2F20190619130639308.png&commended-file=&commendedImage=
2019-06-20 20:45:03 [com.zyd.blog.controller.RestArticleController:90] INFO  - ======{}
2019-06-20 20:45:03 [com.zyd.blog.controller.RestArticleController:97] INFO  - ======Article(bizArticle=BizArticle(tags=null, bizType=null, title=null, userId=null, coverImage=null, commendImage=null, qrcodePath=null, isMarkdown=null, content=null, contentMd=null, top=null, typeId=null, status=null, recommended=null, original=null, description=null, keywords=null, comment=null, lookCount=null, commentCount=null, loveCount=null))
2019-06-20 20:45:05 [com.zyd.blog.controller.RestArticleController:87] INFO  - ======
2019-06-20 20:45:05 [com.zyd.blog.controller.RestArticleController:90] INFO  - ======{"isMarkdown":["false"],"id":["9"],"title":["jmeter中html报告生成方式"],"content":["<h1>test</h1>"],"description":["jmeter中html报告生成方式 &nbsp; 1、使用命名:  &gt;jmeter -g loginWithPassword100c180s.jtl -o ./report 解释:jmeter "],"keywords":["jmeter"],"typeId":["2"],"tags":["5"],"status":["1"],"original":["on"],"comment":["on"],"top":["on"],"recommended":["on"],"file":[""],"coverImage":["http://img.dmanis.cn/oneblog/20190619130639308.png"],"commended-file":[""],"commendedImage":[""]}
2019-06-20 20:45:05 [com.zyd.blog.controller.RestArticleController:97] INFO  - ======Article(bizArticle=BizArticle(tags=null, bizType=null, title=jmeter中html报告生成方式, userId=null, coverImage=http://img.dmanis.cn/oneblog/20190619130639308.png, commendImage=null, qrcodePath=null, isMarkdown=false, content=<h1>test</h1>, contentMd=null, top=true, typeId=2, status=1, recommended=true, original=true, description=jmeter中html报告生成方式 &nbsp; 1、使用命名:  &gt;jmeter -g loginWithPassword100c180s.jtl -o ./report 解释:jmeter , keywords=jmeter, comment=true, lookCount=null, commentCount=null, loveCount=null))
2019-06-20 20:45:07 [com.zyd.blog.business.aspect.BussinessLogAspect:65] INFO  - 进入文章列表页 | 0:0:0:0:0:0:0:1 - GET http://localhost:8085/articles - Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
 try {
            ServletInputStream inputStream = request.getInputStream();
            log.info("======" + StreamUtils.copyToString(inputStream, Charset.defaultCharset()));

            Map map  = request.getParameterMap();
            log.info("======" + JSON.toJSONString(map));
        }catch (Exception e){

        }
        if (article == null){
            log.info("====== null" );
        }else{
            log.info("======" + article.toString());
        }

 

前言

在SpringMVC web应用中,对于一个rest接口,获取请求参数我们一般使用@requestParam@requestBody等注解 。对于表单类型的请求参数,有一下几种获取方式

  1. @requestParam注解方式
  2. request.getParameter(String name)
  3. request.getInputStream()

前两种方式其实是一种方式,@requestParam底层就是利用request.getParameter的原理。这两种方式有一个弊端就是只能一个个获取,而且必须知道对方传过来的参数的key值,如果想要一次性获取,可以使用request.getInputStream方法获取一个inputStream对象,然后读取流里面的数据。

//获取到的数据格式key=value以‘&’分隔的形式
age=20&name=faderw

问题

但在实际过程中,我们会发现通过request.getInputStream()方式获取的数据为空。

根据Servlet规范,如果同时满足下列条件,则请求体(Entity)中的表单数据,将被填充到request的parameter集合中(request.getParameter系列方法可以读取相关数据)

  1. 这是一个HTTP/HTTPS请求
  2. 请求方法是POST(querystring无论是否POST都将被设置到parameter中)
  3. 请求的类型(Content-Type头)是application/x-www-form-urlencoded
  4. Servlet调用了getParameter系列方法

这里的表单数据已经被填充到parameterMap中,不能再通过getInputStream获取。



作者:Top_Bear
链接:https://www.jianshu.com/p/2d0d72ce2aee
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 

这个有点坑,首先request的流的获取每个请求只能获取一次,之后再通过getInputStream获取流的时候就获取不到数据了,还有getInputStream和getReader和getParameter都可以获取输入流数据,但是存在冲突,也就是三者只要有一个对request获取了输入流信息,那么其他的方法之后就获取不到数据了。这就是springboot埋的一个小坑,那么怎么通过request获得Post请求的body值呢?
--------------------- 
作者:课么多巨蜥 
来源:优快云 
原文:https://blog.youkuaiyun.com/qq_36005199/article/details/84562057 
版权声明:本文为博主原创文章,转载请附上博文链接!

 

今天在项目中获取request的请求数据为空,消耗了一天的时间
百度了两篇文章解决了这个问题:原因 解决方案

  1. 阐述下问题:
    项目是记录请求数据及响应数据,但在获取请求数据时使用request.getInputStream()为空,而使用
	Enumeration enu=request.getParameterNames();  
	while(enu.hasMoreElements()){  
		String paraName=(String)enu.nextElement();  
		System.out.println(paraName+": "+request.getParameter(paraName));  
	}  

但是获取的值是不完整的,它将原数据前面部分作为参数key,后面部分作为参数value。

  1. 分析原因
    经过一顿操作,发现客户端上送的Content-Type的值为 这里可以看下

application/x-www-form-urlencoded;charset=UTF-8

而不是

text/xml
text/plain
application/json

所以请求数据的值是以key/value形式存储的。
若在使用request.getInputStream()前已经使用过getParameter或者@requestParam注解方式,则request.getInputStream()获取为空。

3 解决方案
1 将post请求的Content-Type 设置为text/xml
2 通过遍历获取所有参数,再获取参数值
3 避免在request.getInputStream()之前使用getParameter或者@requestParam注解方式
对httprequest进行修饰
定义InputStreamHttpServletRequestWrapper类继承于HttpServletRequestWrapper对请求数据进行处理


import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.stream.Collectors;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.springframework.web.bind.annotation.RequestMethod;

public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper{

    private final byte[] streamBody;
    private static final int BUFFER_SIZE = 4096;
    
	//对请求数据判断,getInputStream()为空的数据对key和value值进行拼接
    public InputStreamHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        byte[] bytes = inputStream2Byte(request.getInputStream());
        if (bytes.length == 0 && RequestMethod.POST.name().equals(request.getMethod())) {
            //从ParameterMap获取参数,并保存以便多次获取
            bytes = request.getParameterMap().entrySet().stream()
                    .map(entry -> {
                        String result;
                        String[] value = entry.getValue();
                        if (value != null && value.length > 1) {
                            result = Arrays.stream(value).map(s -> entry.getKey() + "=" + s)
                                    .collect(Collectors.joining("&"));
                        } else {
                            result = entry.getKey() + "=" + value[0];
                        }

                        return result;
                    }).collect(Collectors.joining("&")).getBytes();
        }
        	//System.err.println(new String(bytes));
        streamBody = bytes;
    }

    private byte[] inputStream2Byte(InputStream inputStream) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] bytes = new byte[BUFFER_SIZE];
        int length;
        while ((length = inputStream.read(bytes, 0, BUFFER_SIZE)) != -1) {
            outputStream.write(bytes, 0, length);
        }

        return outputStream.toByteArray();
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(streamBody);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}


写doFilterInternal类对InputStreamHttpServletRequestWrapper 进行调用即过滤器对请求的处理,该类继承于OncePerRequestFilter,一次请求只调用一次

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;


public class InputStreamWrapperFilter extends OncePerRequestFilter{

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
     HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        ServletRequest servletRequest = new InputStreamHttpServletRequestWrapper(httpServletRequest);
        filterChain.doFilter(servletRequest, httpServletResponse);
    }
}

最后确定过滤器调用的顺序
在application类中

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import com.springboot.SpringBootUtill.InputStreamWrapperFilter;

@SpringBootApplication
@Slf4j
public class Application
{
  public static void main(String[] args)
  {
    log.info("=================开始成功=================");
    SpringApplication.run(Application.class, args);
    log.info("=================启动成功=================");
  }
	@Bean
  @Order(1)
  public FilterRegistrationBean inputStreamWrapperFilterRegistration() {
      FilterRegistrationBean registrationBean = new FilterRegistrationBean();
      registrationBean.setFilter(new InputStreamWrapperFilter());
      registrationBean.setName("inputStreamWrapperFilter");
      registrationBean.addUrlPatterns("/*");
      return registrationBean;
  }
}
  •  

到此为止该问题已经解决了,可以获取到参数了

//获取请求body
	String getRequestMsg(HttpServletRequest request) throws IOException {
		BufferedReader br = request.getReader();
	    String str, wholeStr = "";   
	    while((str = br.readLine()) != null){
	        wholeStr += str;
	}
		return wholeStr;  
	}

现在还有个疑问,我并没有在request.getInputStream()前使用过getParameter或者@requestParam注解方式。是不是springboot在嵌入tomcat后,请求自动调用了getParameter方法呢。

https://www.jianshu.com/p/2d0d72ce2aee

https://www.cnblogs.com/dayou123123/p/3443939.html

<think>好的,我现在要解决用户的问题:在Spring Boot集成Shiro时,通过请求获取Subject后调用getPrincipal返回null。这个问题可能由多种原因导致,我需要仔细分析可能的原因,并给出解决方案。 首先,用户提到的是获取Subject后getPrincipal返回null。这说明用户已经能够正确获取Subject对象,但在认证过程中可能存在未正确设置Principal的情况。我需要回顾Shiro的工作流程,特别是认证和会话管理部分。 可能的原因一:用户未正确登录或会话未保存。如果用户没有成功登录,Subject的Principal自然为null。需要检查登录流程是否正确,Shiro的认证是否成功,并且会话是否被正确保存。例如,在登录Controller中是否调用了Subject.login(token),并且是否有异常被抛出。 可能的原因二:Shiro的过滤器配置问题。可能请求没有经过Shiro的过滤器链,导致SecurityManager无法获取到当前的Subject。检查ShiroFilterFactoryBean的配置,特别是URL模式的映射是否正确,确保相关请求路径被正确拦截和处理。 可能的原因三:跨域或会话管理问题。例如,在使用前后端分离架构时,可能没有正确传递Session ID或Token,导致Shiro无法识别用户的会话。需要检查是否启用了Session管理,或者是否配置了正确的SessionDAO,比如使用Redis来存储会话信息。此外,CORS配置也可能影响会话的维持,需要确保前端在请求中携带了必要的凭证(如cookies)。 可能的原因四:Realm配置问题。自定义的Realm可能在认证成功后没有正确设置Principal。需要检查Realm的doGetAuthenticationInfo方法,确保返回的SimpleAuthenticationInfo对象包含了Principal和Credentials,并且Principal不为null。 可能的原因五:配置遗漏,比如没有启用Session管理器。在ShiroConfig中,需要配置SecurityManager的SessionManager,确保会话被正确管理。例如,配置DefaultWebSessionManager,并设置SessionDAO为Redis等持久化存储。 接下来,我需要参考用户提供的引用内容。根据引用[3],用户需要添加Shiro的依赖,并初始化数据库。引用[4]提到了整合Shiro和Redis,可能涉及会话持久化的问题。如果用户没有正确配置Redis或其他Session存储,可能导致会话丢失,从而Principal无法获取。 解决方案可能包括以下几个步骤: 1. 确认用户已正确登录,并且登录过程中没有异常。可以通过日志或调试来检查Subject的登录状态。 2. 检查Shiro的过滤器配置,确保请求路径被正确拦截,特别是登录请求和需要认证的路径是否配置正确。 3. 检查Realm实现,确保在认证成功时正确返回Principal对象。例如,在doGetAuthenticationInfo中构造SimpleAuthenticationInfo时,第一个参数应为Principal对象,如用户的用户名或用户对象。 4. 配置Session管理,启用SessionManager,并设置SessionDAO为合适的实现,如Redis,确保会话能够跨请求保持。例如,在ShiroConfig中配置SessionManager,并将其设置到SecurityManager中。 5. 处理跨域问题,确保前端在发送请求时携带了Session ID或Token,并且后端正确配置了CORS,允许携带凭证。 例如,在Realm中的代码可能存在问题: ```java @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userService.findByUsername(username); if (user == null) { return null; } return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName()); } ``` 这里返回的Principal是username字符串,如果在后续获取时期望得到用户对象,可能需要将user对象作为Principal。比如: ```java return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); ``` 同时,在Subject.getPrincipal()时,需要强制转换为User对象。 另外,检查ShiroConfig中的SecurityManager配置,是否设置了SessionManager: ```java @Bean public SecurityManager securityManager(Realm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); securityManager.setSessionManager(sessionManager()); // 确保SessionManager被设置 return securityManager; } @Bean public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); // 使用Redis存储Session return sessionManager; } ``` 如果用户没有使用Redis,可能需要检查会话是否被正确存储,或者考虑其他SessionDAO配置。 此外,检查Controller中的登录逻辑: ```java Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken shiroToken = new UsernamePasswordToken(username, password); subject.login(shiroToken); ``` 确保在登录过程中没有抛出异常,且subject.isAuthenticated()返回true。 最后,考虑请求是否被正确过滤。例如,在ShiroFilterFactoryBean中,需要配置所有需要认证的路径被authc过滤器拦截: ```java ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/login", "anon"); filterMap.put("/**", "authc"); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; ``` 如果路径配置错误,可能导致请求未被认证,从而Principal为null。 总结可能的原因和解决方案后,我需要将这些步骤整理成用户能够理解的回答,并附上相关代码示例和配置检查点,帮助用户逐步排查问题。</think>在Spring Boot中集成Shiro时,`Subject.getPrincipal()`返回`null`通常是由于**会话未正确保存**或**认证流程未完成**导致的。以下是逐步排查和解决方案: --- ### **1. 确保认证流程正确** - **登录逻辑**:检查用户登录时是否调用了`subject.login(token)`并处理了异常。 ```java @PostMapping("/login") public String login(String username, String password) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); // 必须调用此方法触发认证 return "登录成功"; } catch (AuthenticationException e) { return "用户名或密码错误"; } } ``` - 通过`subject.isAuthenticated()`验证登录状态[^3]。 --- ### **2. 检查Realm实现** 在自定义Realm中,确保`doGetAuthenticationInfo()`返回的`SimpleAuthenticationInfo`包含有效的Principal(如用户对象或用户名): ```java @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { String username = (String) token.getPrincipal(); User user = userService.findByUsername(username); if (user == null) return null; // 将用户对象作为Principal传递 return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); } ``` - 获取Principal时应强制转换类型: ```java User user = (User) SecurityUtils.getSubject().getPrincipal(); ``` --- ### **3. 配置Session管理** 在`ShiroConfig`中启用Session持久化(如Redis)以避免会话丢失: ```java @Bean public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); // 使用Redis存储Session return sessionManager; } @Bean public SecurityManager securityManager(Realm realm, SessionManager sessionManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); securityManager.setSessionManager(sessionManager); // 注入SessionManager return securityManager; } ``` - 若未使用Redis,确保`DefaultWebSessionManager`正确配置[^4]。 --- ### **4. 检查过滤器链配置** 确保请求路径被Shiro拦截,并在`ShiroFilterFactoryBean`中配置`authc`过滤器: ```java @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/login", "anon"); // 放行登录接口 filterMap.put("/**", "authc"); // 其他路径需要认证 factory.setFilterChainDefinitionMap(filterMap); return factory; } ``` --- ### **5. 处理跨域请求** 若为前后端分离项目,需确保请求携带Cookie或Token: - **后端CORS配置**:允许凭证传递。 ```java @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("http://前端域名"); config.setAllowCredentials(true); // 允许携带凭证 source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } ``` - **前端请求**:设置`withCredentials: true`(Axios示例): ```javascript axios.get(url, { withCredentials: true }); ``` --- ### **6. 验证会话存储** - 检查Redis是否正常运行,并观察Session是否成功存储: ```bash redis-cli KEYS "shiro:session:*" ``` - 若无Redis,尝试使用默认内存存储,确认是否仍存在问题。 --- ### **总结** 通过以上步骤,依次排查**认证流程**、**Session配置**、**过滤器链**和**跨域问题**,可解决`getPrincipal()`返回`null`的问题。若仍不生效,建议开启Shiro日志(`log4j.logger.org.apache.shiro=DEBUG`)观察认证和会话管理细节[^1]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未来AI编程

共鸣===鼓励 打赏您随意

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值