Spring-SpringSecurity

笔记参考柏码知识库

SpringSecurity是一个基于Spring开发的非常强大的权限验证框架,其核心功能包括:

  • 认证 (用户登录)
  • 授权 (此用户能够做哪些事情)
  • 攻击防护 (防止伪造身份攻击)

常见攻击方式

CSRF跨站请求伪造攻击

构建恶意页面,引导用户访问对应网站执行操作。

SameSite可以限制第三方Cookie的使用。在Chrome浏览器中,SameSite默认为Lax,第三方Cookie只能在用户导航到与原始站点相同的站点时发送。

SFA会话固定攻击

黑客将JSESSIONID交给用户,通过用户的操作来使得该ID获取到用户的权限。

Tomcat发送的SESSIONID默认勾选了HttpOnly选项的,一旦被设定是无法被随意修改的,前提是先正常访问一次网站。

可以在登陆后重新给用户分配JSESSIONID,在SpringSecurity有所实现。

XSS跨站脚本攻击

向网站注入脚本进行攻击。

开发环境配置

导入SpringSecurity的相关依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>6.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.1.1</version>
</dependency>

配置初始化器:

1
2
3
4
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
//不用重写任何内容
//这里实际上会自动注册一个Filter,SpringSecurity底层就是依靠N个过滤器实现的
}

创建一个配置类用于配置SpringSecurity:

1
2
3
4
5
@Configuration
@EnableWebSecurity //开启WebSecurity相关功能
public class SecurityConfiguration {

}

在根容器中添加此配置文件:

1
2
3
4
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{MainConfiguration.class, SecurityConfiguration.class};
}

认证

基于内存验证

以代码的形式配置用户和密码,密码需要进行加密,可以使用BCrypt加密工具:

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
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

//这里将BCryptPasswordEncoder直接注册为Bean,Security会自动进行选择
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails user = User
.withUsername("user")
.password(encoder.encode("password")) //这里将密码进行加密后存储
.roles("USER")
.build();
System.out.println(encoder.encode("password")); //一会观察一下加密出来之后的密码长啥样
UserDetails admin = User
.withUsername("admin")
.password(encoder.encode("password")) //这里将密码进行加密后存储
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}

SpringSecurity会接管登录验证模块,不需要自己编写。

SpringSecurity自带了csrf防护,需要在POST请求中携带页面中的csrfToken,否则将进行拦截。

1
<input type="text" th:id="${_csrf.getParameterName()}" th:value="${_csrf.token}" hidden>
1
2
3
4
5
6
7
8
function pay() {
const account = document.getElementById("account").value
const csrf = document.getElementById("_csrf").value
axios.post('/mvc/pay', {
account: account,
_csrf: csrf //携带此信息即可,否则会被拦截
}, {
...

现在浏览器已经足够安全,因此可以关闭csrf防护。

基于数据库验证

官方默认提供了可以直接使用的用户和权限表设计,无需自己建表:

1
2
3
create table users(username varchar(50) not null primary key,password varchar(500) not null,enabled boolean not null);
create table authorities (username varchar(50) not null,authority varchar(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

添加数据库相关依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.31</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.10</version>
</dependency>

编写配置类:

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
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

@Bean PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

@Bean
public DataSource dataSource(){
//数据源配置
return new PooledDataSource("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/test", "root", "123456");
}

@Bean
public UserDetailsService userDetailsService(DataSource dataSource,
PasswordEncoder encoder) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
//仅首次启动时创建一个新的用户用于测试,后续无需创建
manager.createUser(User.withUsername("user")
.password(encoder.encode("password")).roles("USER").build());
return manager;
}
}

通过使用UserDetailsManager对象,就能快速执行用户相关的管理操作。

重置密码功能需要配置一下JdbcUserDetailsManager,为其添加一个AuthenticationManager用于原密码的校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

...

//手动创建一个AuthenticationManager用于处理密码校验
private AuthenticationManager authenticationManager(UserDetailsManager manager,
PasswordEncoder encoder){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(manager);
provider.setPasswordEncoder(encoder);
return new ProviderManager(provider);
}

@Bean
public UserDetailsManager userDetailsService(DataSource dataSource,
PasswordEncoder encoder) throws Exception {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
//为UserDetailsManager设置AuthenticationManager即可开启重置密码的时的校验
manager.setAuthenticationManager(authenticationManager(manager, encoder));
return manager;
}
}

重置密码接口:

1
2
3
4
5
6
7
8
9
@ResponseBody
@PostMapping("/change-password")
public JSONObject changePassword(@RequestParam String oldPassword,
@RequestParam String newPassword) {
manager.changePassword(oldPassword, encoder.encode(newPassword));
JSONObject object = new JSONObject();
object.put("success", true);
return object;
}

自定义验证

需要自行实现UserDetailsService或是功能更完善的UserDetailsManager接口。以前者为例:

1
2
3
4
public interface UserMapper {
@Select("select * from user where username = #{username}")
Account findUserByName(String username);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class AuthorizeService implements UserDetailsService {

@Resource
UserMapper mapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = mapper.findUserByName(username);
if(account == null)
throw new UsernameNotFoundException("用户名或密码错误");
return User
.withUsername(username)
.password(account.getPassword())
.build();
}
}

其他配置

自定义登录界面

配置自定义登录界面,包括登录,退出以及关闭csrf验证:

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
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

...

//如果你学习过SpringSecurity 5.X版本,可能会发现新版本的配置方式完全不一样
//新版本全部采用lambda形式进行配置,无法再使用之前的and()方法进行连接了
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
//以下是验证请求拦截和放行配置
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/static/**").permitAll(); //将所有的静态资源放行,一定要添加在全部请求拦截之前
auth.anyRequest().authenticated(); //将所有请求全部拦截,一律需要验证
})
//以下是表单登录相关配置
.formLogin(conf -> {
conf.loginPage("/login"); //将登录页设置为我们自己的登录页面
conf.loginProcessingUrl("/doLogin"); //登录表单提交的地址,可以自定义
conf.defaultSuccessUrl("/"); //登录成功后跳转的页面
conf.permitAll(); //将登录相关的地址放行,否则未登录的用户连登录界面都进不去
//用户名和密码的表单字段名称,不过默认就是这个,可以不配置,除非有特殊需求
conf.usernameParameter("username");
conf.passwordParameter("password");
})
//以下是退出登录相关配置
.logout(conf -> {
conf.logoutUrl("/doLogout"); //退出登录地址,跟上面一样可自定义
conf.logoutSuccessUrl("/login"); //退出登录成功后跳转的地址,这里设置为登录界面
conf.permitAll();
})
//以下是csrf相关配置
.csrf(conf -> {
conf.disable(); //此方法可以直接关闭全部的csrf校验,一步到位
conf.ignoringRequestMatchers("/xxx/**"); //此方法可以根据情况忽略某些地址的csrf校验
})
.build();
}
}

记住功能

开启功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

...

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
...
.rememberMe(conf -> {
conf.alwaysRemember(false); //这里不要开启始终记住,我们需要配置为用户自行勾选
conf.rememberMeParameter("remember-me"); //记住我表单字段,默认就是这个,可以不配置
conf.rememberMeCookieName("xxxx"); //记住我设置的Cookie名字,也可以自定义,不过没必要
})
.build();
}
}

持久化保存:

1
2
3
4
5
6
7
8
@Bean
public PersistentTokenRepository tokenRepository(DataSource dataSource){
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
//在启动时自动在数据库中创建存储记住我信息的表,仅第一次需要,后续不需要
repository.setCreateTableOnStartup(true);
repository.setDataSource(dataSource);
return repository;
}
1
2
3
4
5
.rememberMe(conf -> {
conf.rememberMeParameter("remember-me");
conf.tokenRepository(repository); //设置刚刚的记住我持久化存储库
conf.tokenValiditySeconds(3600 * 7); //设置记住我有效时间为7天
})

授权

基于角色授权

配置:

1
2
3
4
5
6
7
8
.authorizeHttpRequests(auth -> {
//静态资源依然全部可以访问
auth.requestMatchers("/static/**").permitAll();
//只有具有以下角色的用户才能访问路径"/"
auth.requestMatchers("/").hasAnyRole("user", "admin");
//其他所有路径必须角色为admin才能访问
auth.anyRequest().hasRole("admin");
})

基于权限授权

1
2
3
4
5
6
.authorizeHttpRequests(auth -> {
//静态资源依然全部可以访问
auth.requestMatchers("/static/**").permitAll();
//基于权限和基于角色其实差别并不大,使用方式是相同的
auth.anyRequest().hasAnyAuthority("page:index");
})

使用注解权限判断

开启方法安全校验:

1
2
3
4
5
6
@Configuration
@EnableWebSecurity
@EnableMethodSecurity //开启方法安全校验
public class SecurityConfiguration {
...
}

添加注解:

1
2
3
4
5
6
7
8
9
10
@Controller
public class HelloController {
@PreAuthorize("hasRole('user')") //直接使用hasRole方法判断是否包含某个角色
@GetMapping("/")
public String index(){
return "index";
}

...
}

类似还有@PostAuthorize注解,但是它是在方法执行之后再进行拦截。