赞
踩
一、前言
我这几天看到查看这篇博文的人比较多,特意更新了两种解决第二个问题办法。
这两天看隔壁组项目,由于我自己项目和他们项目一样使用的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 隐藏域
- <!DOCTYPE html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org">
- <head>
- ...
- <form class="form-signin" th:action="@{login}" action="login" method="post">
- ...
- </form>
- ...
第二种方法,手动添加隐藏域。
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
第三种方法,加在请求头部分
- <meta name="_csrf" th:content="${_csrf.token}"/>
- <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
第四种办法,直接禁掉CSRF.。这个方法太极端。禁用方式不放出来了,总之强烈不推荐。
第五种办法,增加例外,让CSRF直接通过。
http.csrf().ignoringAntMatchers("/login")
POST请求403的问题通过设置以上参数就可以解决了
下面解决POST登录表单直接提交后台接收时变成GET的问题
还记得上面讲到CRSF时说的坑吗?上面我们怀疑是POST表单提交后经由CRSF机制转发后最终提交给后台的是GET请求方式,由于不能正确提交登录信息,导致不管怎样反复会跳到登录页面。
在说这个问题前,先列举两个它的野路子解法。最后再分析官方解法。
A、.do应用解决
原代码如下:
页面
- <form class="form-signin" th:action="@{login}" action="login" method="post">
- <div class="form-group">
- <label for="username">账号</label>
- <input type="text" class="form-control" name="username" value="" placeholder="账号"/>
- </div>
- <div class="form-group">
- <label for="password">密码</label>
- <input type="password" class="form-control" name="password" placeholder="密码"/>
- </div>
- <input type="submit" id="login" value="Login" class="btn btn-primary"/>
- </form>
控制层
- 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节末尾):
- <c:url value="/login" var="loginUrl"/>
- <form action="${loginUrl}" method="post"> 1
- <c:if test="${param.error != null}"> 2
- <p>
- Invalid username and password.
- </p>
- </c:if>
- <c:if test="${param.logout != null}"> 3
- <p>
- You have been logged out.
- </p>
- </c:if>
- <p>
- <label for="username">Username</label>
- <input type="text" id="username" name="username"/> 4
- </p>
- <p>
- <label for="password">Password</label>
- <input type="password" id="password" name="password"/> 5
- </p>
- <input type="hidden" 6
- name="${_csrf.parameterName}"
- value="${_csrf.token}"/>
- <button type="submit" class="btn">Log in</button>
- </form>
看到官方这么贴心,然后我的表单整体样式和它基本一致,就再次仔细看了一下。发现除了我的页面写的是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,否则无法通过表达提交
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。