一、前言
我这几天看到查看这篇博文的人比较多,特意更新了两种解决第二个问题办法。
这两天看隔壁组项目,由于我自己项目和他们项目一样使用的Spring boot基础框架,想看看有什么值得学习的地方,结果就看到人家的登录表单可以正常分GET和POST提交,也没做什么特别的处理,唯一的区别就是人是用Ajax中并submit方法提交的。当时我的项目在登录模块也分GET和POST两种请求方式的控制层方法。但我的POST方法直接通过表单形式提交的话会有本文标题的问题。
二、问题分析与解决
先了解一下web请求链,由于项目采用Spring security做权限控制,系统的访问流程如下(英文原版文档见9.4Authentication in a Web Application):
这里说的很清楚是可以POST请求方式传到后台的。结合文档中关于CSRF的介绍,基本可以确定是CRSF机制转发后POST变成了GET(这句没错,但是有坑)。
处理这种CSRF问题(此处可以解决POST请求报403的错误)有多种解决方案,如下:
第一种方法,也是官方推荐使用的。form 表单使用 th:action 属性, thymeleaf 会自动在 form 表单中生成 _csrf 隐藏域
...
...
第二种方法,手动添加隐藏域。
第三种方法,加在请求头部分
第四种办法,直接禁掉CSRF.。这个方法太极端。禁用方式不放出来了,总之强烈不推荐。
第五种办法,增加例外,让CSRF直接通过。
http.csrf().ignoringAntMatchers("/login")
POST请求403的问题通过设置以上参数就可以解决了
下面解决POST登录表单直接提交后台接收时变成GET的问题
还记得上面讲到CRSF时说的坑吗?上面我们怀疑是POST表单提交后经由CRSF机制转发后最终提交给后台的是GET请求方式,由于不能正确提交登录信息,导致不管怎样反复会跳到登录页面。
在说这个问题前,先列举两个它的野路子解法。最后再分析官方解法。
A、.do应用解决
原代码如下:
页面
控制层
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.music.league.service.SignManager;
@Controller
public class SignController {
@Resource
SignManager signManager;
@RequestMapping(value="/login",method = RequestMethod.GET)
public String sign(){
System.out.println("Judge!!");
return "login";
}
@RequestMapping(value="/login",method = RequestMethod.POST)
public String sign(HttpServletRequest request){
System.out.println("登录方法入参:"+request.getParameter("userName")+":"+request.getParameter("password"));
return "welcome";
}
}
SpringMVC配置
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
Spring security配置
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
//设置默认登录成功跳转页面
.defaultSuccessUrl("/welcome").failureUrl("/login?error").permitAll()
把上述几处login的请求,除了控制层的POST方式登录方法和 th:action="@{login},其他的login都改成login.do。这样做是为了通过除请求Method属性外的第二种办法去区分开get和post请求的不同。
B、参数差异
这种方法是基于上面web请求链第二步请求本身无参的性质硬搞。当login方法无参时,自动处理走GET,大家和和美美。当有参时,手动写逻辑去掉POST。不过可能会有安全问题,所以不太推荐。
听说用ModelAndView也可以解决,我简单试了一下,好像不行哦。anyway,野路子解法到此为止。
正经的问题本质原因,如下:
在翻看Spring security5.0官方文档的时候,发现文档中提到Spring security特别为大家提供了一个登录验证表单(具体哪句找不到了,文档连接点我),倾力奉献撒!继续读文档,通过前后文的联系,官方的表单页面代码大概是这样的(这段代码在文档5.3节末尾):
看到官方这么贴心,然后我的表单整体样式和它基本一致,就再次仔细看了一下。发现除了我的页面写的是loginName其他没区别。于是抱着试一试的态度改成username。MMP,完美的POST请求进入控制层POST请求登录方法。MMP...要不要限制这么死?CRSF底层实现我找不到,但问题就是在这个特殊的登录,官方给登录做了特别处理。
经过进一步的验证发现即使实体中写的是userName或者loginName,只要你想在登录模块直接通过表单方式提交的话,就必须是username。
如果改了名字还是无效,那么还有两个解决方法。就是改为小写的username后,执行下面两个方法之一。推荐第二个。
第一个办法是手动给指定一下登录请求的处理。就是在loginPage后面加一个loginProcessingUrl,内容是你登陆方法的控制层RequestMapping中的value和登录方法。如果你没有写RequestMapping的话,那就是控制层的Spring自动转换值,一般是去掉Contrller的驼峰命名。比如SignController,这里写sign就行。
第二个办法是把defaultSuccessUrl改为successForwardUrl,这个办法的原理就是把直接跳转页面改为跳转后台方法。defaultSuccessUrl("/login")改为successForwardUrl("/sign/login")。建议用这个,因为这个依旧会照常按security过滤器链自动加载权限,第一个需要手动添加权限,否则一直是匿名。
.loginPage("/login").loginProcessingUrl("/sign/login")
三、注意事项
.do的应用解决也要把登录名改成小写的username,官方的3.x版本文档写的是must,否则无法通过表达提交