Spring Security的学习笔记

CSRF问题

什么是CSRF

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

如何防范CSRF攻击

为系统中的每一个连接请求加上一个token,这个token是服务器随机生成的,我们每次访问一个页面,页面会传回一个服务端生成的token。对于这个页面的请求,我们的服务端会对token进行验证。

Spring Security中的防范措施

在Spring Security中默认开启CSRF防御。但是我们需要手动配置将token存放在cookie中,并且关闭HttpOnly,允许js读取该cookie。

1
2
3
4
5
6
7
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}

情况1:前后端不分离

前后端不分离即使用jsp或者thymeleaf模板

1
2
3
4
5
$.ajax({ 
data: {
"_csrf": "${_csrf.token}"
}
});
1
2
3
<form method="POST" action="/profile">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>

情况2:前后端分离

在vue中,我们首先获取cookie并截取相应的字段

1
2
3
4
5
getCookie(name) {
var value = '; ' + document.cookie;
var parts = value.split('; ' + name + '=');
if (parts.length === 2) return parts.pop().split(';').shift()
},

然后在post表单中新增请求头

1
2
3
4
5
6
7
axios({
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': that.getCookie('XSRF-TOKEN')
},
······
})

登录交互问题

问题

在开发之中,我发现Spring Security中,在formLogin里设置defaultSuccessUrl,前端拿到的东西是html页面的代码,只适用于前后端不分离的情况。而在前后端分离中,我们需要拿到json数据格式,根据json数据再进行跳转判断,因此我们需要重写successHandler。

重写Handler

原先的Service层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 学生登录,通过stuNo和password查询是否存在该学生
*
* @param stuNo 学号
* @param password 密码
* @return msg 返回消息
* code 状态码,1成功,0失败
* result boolean是否登录成功
*/
@Override
public Map<String, Object> studentLogin(String stuNo, String password) {
Map<String, Object> map = new HashMap<>();
password = DigestUtils.md5DigestAsHex(password.getBytes());
Student student = studentDao.findByStuNoAndPassword(stuNo, password);
boolean isLogin = student != null;
map.put("msg", "登录" + (isLogin ? "成功" : "失败"));
Map<String, Object> result = new HashMap<>();
result.put("stuNo", stuNo);
if (isLogin)
map.put("submit", student.getSubmitReason() != null);
map.put("result", result);
map.put("code", isLogin ? 1 : 0);
return map;
}

在集成Spring Security后在Config中的Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/adminLogin.html")
.loginProcessingUrl("/admin/login")
.usernameParameter("adminID")
.successHandler((req, resp, authentication) -> {
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录成功");
map.put("result", authentication);
map.put("code", 1);
PrintWriter out = resp.getWriter();
out.write(objectMapper.writeValueAsString(map));
out.flush();
out.close();
})
.failureHandler((req, resp, authentication) -> {
Map<String, Object> map = new HashMap<>();
map.put("msg", "登录失败");
map.put("result", authentication);
map.put("code", 0);
PrintWriter out = resp.getWriter();
out.write(objectMapper.writeValueAsString(map));
out.flush();
out.close();
})
.permitAll();
}

配置不同的UserDetailsService

由于我的需求是普通用户登录与管理用户登录完全分离,因此需要重写多个UserDetailsService,所以需要在WebSecurityConfigurerAdapter中构建多个HttpSecurity,并且一定要加上@Order注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Configuration
@Order(2)
static class StudentSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private StudentServiceImpl studentService;

@Override
public void configure(WebSecurity web) throws Exception {
···
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

http.authorizeRequests().antMatchers("/student/login").permitAll();

http.formLogin().
loginPage("/studentLogin.html")
.loginProcessingUrl("/student/login")
.usernameParameter("stuNo")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll();


http.formLogin().and()
.logout()
.logoutUrl("/student/logout")
.logoutSuccessUrl("/studentLogin.html")
.permitAll();

}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(studentService)
.passwordEncoder(passwordEncoder());
}
}

@Configuration
@Order(3)
static class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AdminServiceImpl adminService;

@Override
public void configure(WebSecurity web) throws Exception {
···
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

http.authorizeRequests().antMatchers("/admin/login").permitAll();

http.formLogin().
loginPage("/adminLogin.html")
.loginProcessingUrl("/admin/login")
.usernameParameter("adminID")
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler())
.permitAll();


http.formLogin().and()
.logout()
.logoutUrl("/admin/logout")
.logoutSuccessUrl("/adminLogin.html")
.permitAll();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(adminService)
.passwordEncoder(passwordEncoder());
}
}
}
文章目录
  1. 1. CSRF问题
    1. 1.1. 什么是CSRF
    2. 1.2. 如何防范CSRF攻击
    3. 1.3. Spring Security中的防范措施
      1. 1.3.1. 情况1:前后端不分离
      2. 1.3.2. 情况2:前后端分离
  2. 2. 登录交互问题
    1. 2.1. 问题
    2. 2.2. 重写Handler
      1. 2.2.1. 原先的Service层
      2. 2.2.2. 在集成Spring Security后在Config中的Handler
  3. 3. 配置不同的UserDetailsService
|