SpringSecurity学习

参考

B站:https://www.bilibili.com/video/BV15a411A7kP

官网:https://spring.io/projects/spring-security

简介

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的 成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方 案。

正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控 制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权 (Authorization)两个部分,这两点也是 Spring Security 重要核心功能。

(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问 该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认 证过程。通俗点说就是系统认为用户是否能登录

(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户 所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以 进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的 权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

SpringSecurity 特点:

  • 和 Spring 无缝整合。
  • 全面的权限控制。
  • 专门为 Web 开发而设计。
    • 旧版本不能脱离 Web 环境使用。
    • 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独 引入核心模块就可以脱离 Web 环境。
  • 重量级。

入门示例

pom.xml

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Controller测试

1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("hello")
public String hello() {
return "hello security";
}
}

启动测试,需要密码登录,默认username为user,密码为控制台打印的内容

启动流程原理

UserDetailsService

PasswordEncoder

Web

设置登录系统的账号、密码

方式一

1
2
3
4
5
spring:
security:
user:
name: root
password: root

方式二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password = encoder.encode("123");
auth.inMemoryAuthentication().withUser("root").password(password).roles("admin");
}

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

方式三

step1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

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

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

step2

1
2
3
4
5
6
7
8
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> auths= AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("root",new BCryptPasswordEncoder().encode("456"),auths);
}
}

加入数据库

建立数据库

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
create table users(
id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
);
-- 密码 atguigu
insert into users values(1,'张
san','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
-- 密码 atguigu
insert into users values(2,'李
si','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
create table role(
id bigint primary key auto_increment,
name varchar(20)
);insert into role values(1,'管理员');
insert into role values(2,'普通用户');
create table role_user(
uid bigint,
rid bigint
);
insert into role_user values(1,1);
insert into role_user values(2,2);
create table menu(
id bigint primary key auto_increment,
name varchar(20),
url varchar(100),
parentid bigint,
permission varchar(20)
);
insert into menu values(1,'系统管理','',0,'menu:system');
insert into menu values(2,'用户管理','',0,'menu:user');
create table role_menu(
mid bigint,
rid bigint
);
insert into role_menu values(1,1);insert into role_menu values(2,1);
insert into role_menu values(2,2);

pom.xml

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>

application.yml

1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo
username: root
password: root

entity

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
public class Users {
private Long id;
private String username;
private String password;
}

mapper

1
2
3
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}

user配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 调用usersMapper方法查询数据库

QueryWrapper<Users> wrapper = new QueryWrapper<>();
// where username = ?
wrapper.eq("username", s);
Users users = usersMapper.selectOne(wrapper);
// 判断
if (users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}

List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
// 从数据库返回users对象,得到用户名、密码,返回
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), auths);
}
}

自定义登录

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登陆页面
http.formLogin()
.loginPage("/login.html") // 登录页面设置
.loginProcessingUrl("/user/login") // 登录访问路径
.defaultSuccessUrl("/test/index").permitAll() // 登录成功跳转路径
.and().authorizeRequests()
.antMatchers("/", "/test/hello").permitAll() //设置哪些路径可以直接访问,不需要认证
.anyRequest().authenticated()
.and().csrf().disable();
}

基于角色或权限进行访问控制

hasAuthority

hasAnyAuthority

hasRole

hasAnyRole

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登陆页面
http.formLogin()
.loginPage("/login.html") // 登录页面设置
.loginProcessingUrl("/user/login") // 登录访问路径
.defaultSuccessUrl("/test/index").permitAll() // 登录成功跳转路径
.and().authorizeRequests()
.antMatchers("/", "/test/hello").permitAll() //设置哪些路径可以直接访问,不需要认证
// 1.hasAuthority()
// .antMatchers("/test/index").hasAuthority("admins")
// 2.hasAnyAuthority()
// .antMatchers("/test/index").hasAnyAuthority("admins,manager")
// 3.hasRole() ROLE_
.antMatchers("/test/index").hasRole("sale")
// 4.hasAnyRole()
.antMatchers("/test/index").hasAnyRole("sale,dba")
.anyRequest().authenticated()
.and().csrf().disable();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 调用usersMapper方法查询数据库

QueryWrapper<Users> wrapper = new QueryWrapper<>();
// where username = ?
wrapper.eq("username", s);
Users users = usersMapper.selectOne(wrapper);
// 判断
if (users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}

List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
// 从数据库返回users对象,得到用户名、密码,返回
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), auths);
}

403页面

1
2
// 配置403页面
http.exceptionHandling().accessDeniedPage("/unauth.html");

注解

@Secured

判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。

使用注解先要开启注解功能!

@EnableGlobalMethodSecurity(securedEnabled=true)

1
2
3
4
5
@GetMapping("/update")
@Secured({"ROLE_sale","ROLE_manager"})
public String update(){
return "hello update";
}

@PreAuthorize

注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用 户的 roles/permissions 参数传到方法中。

先开启注解功能:

@EnableGlobalMethodSecurity(prePostEnabled = true)

1
2
3
4
5
6
@GetMapping("/update")
// @Secured({"ROLE_sale","ROLE_manager"})
@PreAuthorize("hasAnyAuthority('admins')")
public String update(){
return "hello update";
}

@PostAuthorize

使用并不多,在方法执行后再进行权限验证,适合验证带有返回值 的权限.

先开启注解功能:

@EnableGlobalMethodSecurity(prePostEnabled = true)

先执行,后验证权限,虽无权限,但还是输出了

1
2
3
4
5
6
7
8
@GetMapping("/update")
// @Secured({"ROLE_sale","ROLE_manager"})
// @PreAuthorize("hasAnyAuthority('admins')")
@PostAuthorize("hasAnyAuthority('admin')")
public String update(){
System.out.println("update...");
return "hello update";
}

@PostFilter

权限验证之后对数据进行过滤 留下用户名是 admin1 的数据 表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素

@PreFilter

进入控制器之前对数据进行过滤

注销

1
2
// logout
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();

记住我

原理

建表

1
2
3
4
5
6
7
8
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1
2
3
4
5
6
7
8
9
10
11
12
@Autowired
private DataSource dataSource;


@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// 可自动建表
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
1
2
3
4
// 记住我
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60)
.userDetailsService(userDetailsService)
1
2
3
4
5
6
<form action="/user/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="checkbox" name="remember-me"/>自动登录<br/>
<input type="submit" value="login"/>
</form>

CSRF

CSRF 理解

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已 登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

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

从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用 程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

默认是开启的

1
// http.csrf().disable();

总结

简单的学习,原理部分没有深究,还有部分不完整,如结合数据库权限,当然还有分布式没有学习,目前分布式还有太多前期知识为准备,所以没有太注重这方面