一. 开发步骤
最近在学习SpringBoot WebSocket编写群聊天的功能,需要用到用户体系,为了方便直接引入了Security包,具体pom如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- freemarker -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- security安全 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
用FreeMarker开发了一个自己的登录页面login.ftl
,放在templates文件夹下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>登录</title>
</head>
<body>
<form action="/login123" method="post">
<div><label>账号:<input type="text" name="username"/></label></div>
<div><label>密码:<input type="password" name="password"/></label></div>
<div><input type="submit" value="登录"/></div>
</form>
</body>
</html>
重写WebSecurityConfig来替换SpringBoot默认的配置,使得我们上面开发的登录页面生效
//@EnableWebSecurity //SpringBoot自带此注解,不再需要添加
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//设置拦截规则
.antMatchers("/", "/login") //对/ 和 /login不拦截
.permitAll()
.anyRequest()
.authenticated()
.and()
//开启默认登录页面
.formLogin()
.loginPage("/login") //替换Security默认的登录页面到我们自己开发的login.tfl页面
.loginProcessingUrl("/login123") //登录提交form action, 也会自动影响Security自带Http Basic登录页的action请求地址
.defaultSuccessUrl("/chat", true) //登录成功后,默认后续跳转页面
.permitAll()
.and()
.logout() //设置注销
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置两个用户
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user1")
.password(new BCryptPasswordEncoder().encode("111")).roles("USER").and()
.withUser("user2")
.password(new BCryptPasswordEncoder().encode("111")).roles("USER");
//security5.0以后默认需要加密传输,不能明文
// auth.inMemoryAuthentication()
// .withUser("user1").password("111").roles("USER").and()
// .withUser("user2").password("222").roles("USER");
}
@Override
public void configure(WebSecurity web) {
//设置不拦截规则,一般用于静态资源
web.ignoring().antMatchers("/js/**", "pic/");
}
}
以上代码的重点在.loginPage("/login")
,登录界面被替换为view:login。这类似于@Controller方法中返回的String,会被SpringBoot替换为对应的view视图。
而/login
与页面login.ftl
的对应关系,则通过重写WebMvcConfigurer
类实现:
//@EnableWebMvc //SpringBoot下最好不要开启此注解,否则默认SpringMVC配置将失效被替换成本类方法
@Configuration
public class WSWebMvcConfig implements WebMvcConfigurer {
/**
* 直接映射url到对应的view:XXX.tfl模板文件
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/ws").setViewName("/freemarker/ws");
registry.addViewController("/chat").setViewName("/freemarker/chat");
registry.addViewController("/login").setViewName("/freemarker/login");
}
}
二. 一直302跳转登录首页问题描述
以上项目启动后,访问/login的URL(我的是:https://localhost:8443/login),可以出现自定义页面
但输入用户名user1、密码111之后,登录不成功,页面跳转后还是在/login页面内,并且没有明显的报错
通过F12可以看到,http请求被302重定向了,location仍然为登录页
三. 分析原因
注释掉上面的.loginPage("/login")
代码,重新启动服务,/login登录页面会恢复Security默认的登录页面
尝试登录,可以成功登录并进入后续/chat页面。F12调试模式下,与原先自开发页面请求进行对比,可以发现form请求中多了一个字段:_csrf
:
此参数是Security模块为了防止CSRF跨域请求漏洞而添加的伪随机数参数,action调用的/login接口也会对此进行校验,防止漏洞攻击。
至此我们可以定位到之前登录不成功的原因:Spring Security默认是开启crsf验证的,自开发的登录接口被Security模块的CSRF防范机制给拦截了
四. 解决办法
方法一. 去掉Security的CSRF验证机制:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//设置拦截规则
.antMatchers("/", "/login") //对/ 和 /login不拦截
.permitAll()
.anyRequest()
.authenticated()
.and()
//开启默认登录页面
.formLogin()
.loginPage("/login") //替换Security默认的登录页面到我们自己开发的login.tfl页面
.loginProcessingUrl("/login123") //登录提交form action, 也会自动影响Security自带Http Basic登录页的action请求地址
.defaultSuccessUrl("/chat", true) //登录成功后,默认后续跳转页面
.permitAll()
.and()
.logout() //设置注销
.permitAll()
.and()
.csrf().disable(); //Security自带CSRF防攻击,导致页面验证不通过,一直跳转302,这里关闭
}
注意最后一行,增加.csrf().disable()
代码段来关闭CSRF验证。改后重启服务,即可登录成功。
CSRF验证相关原理在:org.springframework.security.web.csrf.CsrfFilter这个过滤器源码中
方法二. 可以修改前端页面,像原生login界面一样增加_csrf
字段,上网查到操作如下(thymeleaf):
<!-- 表单提交用户信息,注意字段的设置,直接是*{} -->
<form action="/login" method="post" enctype="application/x-www-form-urlencoded">
<!--用于验证跨域伪造csrf-->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
用户名:<input type="text" name="username"/><br/>
密码:<input type="text" name="password"/><br/>
<input type="submit" value="登录"/>
</form>