Spring Security学习笔记

安全保证框架

Shiro与Spring security

他们之间很像,除了名字、类名不一样。

Spring Security

在这里插入图片描述

  1. 可以实现定制化身份认证 Authentication

  2. 权限控制 Access of Control

权限:

  • 功能权限

  • 访问权限

  • 菜单权限

Spring Security 用于简化过滤器&拦截器

在这里插入图片描述

  • WebSecurityConfigurerAdapter:自定义Security策略(适配器模式)

  • AuthenticationManagerBuilder:自定义认证策略(建造者模式)

AOP概念

我们不用改变原来项目的业务代码,而是在项目中加入config。帮我们去做一些事情

入门案例

  • 使用工具idea,新建一个Spring initize项目(只勾选一个web即可)

  • 使用SpringBoot 2.2.1版本来使用: pom.xml文件中修改

  • 添加springboot-security依赖

修改controller 体验security

在controller包下新增TestController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

@GetMapping("/hello")
public String add(){
return "Hello, Spring Security!";
}
}

修改默认端口(避免占用)

在application.properties中

1
server.port = 8181

启动项目并测试

1
localhost:8181/test/hello

系统会出现这个界面:
在这里插入图片描述
Spring Security会强制让我们先登录。
在这里插入图片描述

  1. 默认的用户名:user

  2. 控制台(如图)会出现我们的默认密码Using generated security password

在正确输入后,会出现下图效果:
在这里插入图片描述

SpringSecurity 基本原理(过滤链)

Spring Security框架本质上就是一套的过滤器链 FilterChain

也就是说,有很多的过滤器Filter,执行到具体方法时,就会进入过滤器,只有过滤器对其进行过滤放行,才能进入到下一个过滤器。

常见三个过滤器

FilterSecurityInterceptor

FilterSecurityInterceptor是一个方法级的权限过滤器,具体有doFilter方法。

首先看之前的过滤器是否执行,如果执行,才执行自己的过滤器。

ExceptionTranslationFilter

ExceptionTranslationFilter是一个处理权限过程中,出现的异常问题的过滤器。依据每个不同的异常,做不同的处理。

UsernamePasswordAuthenticationFilter

/login且使用POST请求过来的表单做一个用户名密码校验。

SpringSecurity 过滤器的加载过程

使用Spring Security配置过滤器

如果使用SpringBoot项目的话,自动帮我们集成以下的这些代码。

  • 如果不用SpringBoot项目集成SpringSecurity的话,需要写一个DelegationFilterProxy过滤器。

而这个DelegationFilterProxy的doFilter方法中,有一个init初始化方法,这个初始化方法中,用于获得FilterChainProxy,这个Proxy中有一个doFilterInternal方法,这个方法中有一个List<Filter>很多个过滤器,并通过迭代的方式getFilters获得这些所有的过滤器。

SpringSecurity 中重要的接口

UserDetailsService 用户细节信息接口

在实际开发中,我们的账号和密码,并不是Spring Security所默认的user和默认生成密码。

而都是从数据库中查询出来的。

因此,这个接口很方便我们进行自定义逻辑业务开发。

实现这个UserDetailsService接口即可,在这个实现方法中,写查数据库的方法。

  1. 创建一个类,继承UsernamePasswordAuthenticationFilter过滤器(重写三个方法)

  2. 创建一个类,重新UserDetailsService接口,编写查询数据过程,并返回User对象(这个对象由Security提供)

PasswordEncoder 密码加密接口

在上一个UserDetailsService中我们需要返回一个Spring Security框架中的User对象,在这个对象中的密码,必须是要返回加密后的密码。而不能是明文。

Web项目中 认证&授权 思路

认证 Authentication: 就是用户在登录Web中,利用自己的用户名与密码,进行用户认证。

设置登录的用户名与密码

  1. 通过 application.properties 配置文件进行配置

  2. 通过配置类

  3. 通过自定义编写实现类,实现UserDetialsService,返回这个User对象即可

1. 通过配置文件

在这里插入图片描述
配置代码如下:

1
2
spring.security.user.name = user
spring.security.user.password = 123456

2. 通过自定义配置类

  1. 添加注解@configuration

  2. 继承WebSecurityConfigurerAdapter类

  3. 重写这个类的方法(如下图)
    在这里插入图片描述

在重写这个方法时,我们利用auth来配置用户信息,对于密码需要加密。

而且,要记得加一个@bean注解用于验证PasswordEncoder映射,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//利用这个auth来设置登录的用户信息
// 密码需要加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String passwordEncoded = bCryptPasswordEncoder.encode("123456");
auth.inMemoryAuthentication().withUser("user").password(passwordEncoded).roles("admin");
}

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

现在我们可以用这个用户名user 密码123456去登录了。而这个user用户,具有的角色是:admin

3. 利用UserDetailsService接口配置(开发经常用)

  1. 创建配置类,auth使用UserDetailsService(注入一个UserDetailsService类)

  2. 编写实现类,返回User对象,这个对象由用户名、密码以及权限

创建配置类SecurityUserServiceConfig

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
package com.vincent.securitydemo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityUserServiceConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
//1. 使用UserDetailsService
//2. 使用返回的PasswordEncoder @Bean加密
}

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

编写实现类MyUserDetailsService

编写一个实现类MyUserDetailsService去实现UserDetailsService(我们的配置类需要),重写这个loadUserByUsername方法加载用户信息。

注意:@Service(“userDetailsService”)

这个参数要与配置类中,@Autowire自动装配的名字一样。不然找不到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.vincent.securitydemo.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 手动代替查询数据库操作
List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("role");

return new User("user", new BCryptPasswordEncoder().encode("123456"), authorityList);
}
}
  • 注意,我们返回的User对象有三个参数:用户名,加密后的密码以及一个Collection表示具有的权限集合

加入数据库操作 到MyUserDetailsService实现类中

整合MybatisPlus进入到这一步,实现具体的数据库操作。

引入相关依赖

  1. 引入MybatisPlus依赖

  2. 引入MySQL依赖

  3. 引入工具类Lombok 方便实体类注解

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
<!--web项目依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--pojo注解生成get、set方法-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<!--mybatis依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>

<!--MySQL依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

处理数据库中的User表

  1. id 自增数字主键

  2. username varchar类型

  3. password varchar类型

处理Mapper 继承Mybatis+ 给我们写好的基本Mapper即可

  1. 创建mapper包 UserMapper接口 extends BaseMapper 泛型
1
2
3
4
5
6
7
8
9
10
package com.vincent.securitydemo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.securitydemo.entity.User;

@Repository
public interface UserMapper extends BaseMapper<User> {

}

重写之前MyUserDetailsService实现类中的方法 通过数据库查询

  1. Service实现类中 注入UserMapper对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

// 数据库查询语句 用于认证用户
//并最终返回Security框架中的User对象
return ...;
}
}
  1. 利用Mybatis-plus中的QueryWrapper帮我们规定查询满足的条件 并通过UserMapper中的查询方法,注意判断根据用户名查询出来的User对象(我们定义的Entity)是否为空

  2. 最终返回Spring Security框架自己的User对象

注意看代码中的注释内容!!!

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
package com.vincent.securitydemo.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.vincent.securitydemo.entity.User;
import com.vincent.securitydemo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//数据库查询语句 用于认证用户
//参数中的s 即代表用户提交表单中的用户名
//现在我们要根据用户名 去查询对应的数据库中是否存在这个数据

QueryWrapper<User> wrapper = new QueryWrapper<>(); //条件构造器 通过这个构造器去做查询 类似UserExample
wrapper.eq("username", username);
User user = userMapper.selectOne(wrapper);

if (user == null) { //认证失败 没有存在的用户
throw new UsernameNotFoundException("用户名不存在");
}

//注意我们返回的不是自己的实体类User
//而是security框架给我们提供的User类
List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new org.springframework.security.core.userdetails.User(user.getUsername(), new BCryptPasswordEncoder().encode(user.getPassword()), authorityList);
}
}
  1. 这一步很关键 我们要在SpringBoot启动类中 加一个注解@MapperScan 启动Mapper 不然的话无法识别我们的Mapper

SecuritydemoApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.vincent.securitydemo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.vincent.securitydemo.mapper")
public class SecuritydemoApplication {
public static void main(String[] args) {
SpringApplication.run(SecuritydemoApplication.class, args);
}

}
  1. application.properties 配置文件中 配置数据库信息

对于本SpringSecurity测试的SpringBoot版本是SpringBoot 2.2.1 Release

因此,在引入数据库驱动时,如果使用的是MySQL8.0驱动且加入时区

引入时,应使用:com.mysql.cj.jdbc.Driver

1
2
3
4
5
6
7
8
9
10
11
12
13
server.port = 8181

#可直接通过配置文件 来配置用户信息
#spring.security.user.name=user
#spring.security.user.password=123456

#数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/securitydemo?characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.username = root
spring.datasource.password = 123456

spring.jpa.show-sql = true

自定义登录页面&设置路径不需要认证

在实际的业务开发中,我们需要自己的好看的登录页面。因此接下来我们将指定登录的页面。

在实际的业务开发中,有些Controller是不需要进行验证用户权限的。我们也进行设置。

主要思路就是,在 SecurityConfig 配置类中,配置一下就可以了。

值得注意的是,现在我们还是用到configure方法,但是里面的参数不是AuthenticationManagerBuilder auth,而是HttpSecurity http。

我们通过http.formLogin() 进行一系列的设置

  1. 设置默认登录页面的路径

  2. 设置默认登录的请求路径(SpringSecurity自动帮我们实现)

  3. 设置默认登录成功跳转的路径

  4. 设置哪些路径不需要认证,直接通过

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
package com.vincent.securitydemo.config;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityUserServiceConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置用户认证
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}

@Override
protected void configure(HttpSecurity http) throws Exception {
//完成与HTTP请求的配置设置
http.formLogin()
.loginPage("/login.html")
//自定义自己编写的登录页面 参数是地址
.
loginProcessingUrl("/user/login")
//定义登录访问的请求路径 但是这个具体的方法过程由SpringSecurity实现

.failureUrl("/error.html")
//定义登录错误跳转的页面 参数是地址

.defaultSuccessUrl("index.html").permitAll()
//定义默认登录成功后 跳转到的路径

.and().authorizeRequests().antMatchers("/", "/test/hello", "/user/login").permitAll()
.anyRequest().authenticated()
//授权通过,这些路径是不需要认证,直接让它过!

.and().csrf().disable(); //关闭CSRF认证方式
}

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

现在我们只需要,一个登录页面,一个处理登录成功的controller

  • 注意我们这里的action值,必须跟config中配置的请求路径相同,而且name只能是用户名和密码

resources/static/login.html

1
2
3
4
5
<form action="/user/login" method="post">
用户名:<input type="text" name="username"> <br>
密码: <input type="password" name="password">
<input type="submit" value="登录">
</form>

resources/static/error.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>错误页面</title>
</head>
<body>
<h1>抱歉,登录错误!</h1>
</body>
</html>

resources/static/index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>主页面</title>
</head>
<body>
<h1>登录成功,这是主界面!</h1>
</body>
</html>

controller/TestController.java

1
2
3
4
@GetMapping("/index")
public String toIndex(){
return "Hello, Index!";
}

基于角色或者权限的访问控制

1. hasAuthority方法

对于这个方法,如果用户具有指定的权限,则返回True,否则返回False.

# 在config中声明只有哪些权限可以通过这个路径请求
在这里插入图片描述

1
2
//只有具有admin权限 才能访问这个/test/index这个路径(同时在Service中加入admin)
.antMatchers("/test/index").hasAuthority("admin")

# Service中加入这些权限

1
2
//在Service中加入admin权限
List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

在这里插入图片描述
当出现上述图片时,代表403权限不够!

2. hasAnyAuthority

  • 当某个请求对于声明的角色中,任意一个角色都可以通过时使用。

比如对于学生的管理,教师Teacher与管理员Admin都可以访问这些接口路径。

1
.antMatchers("/test/index").hasAnyAuthority("admin,teacher")

则表示,当权限为teacher或者admin…等等时(用逗号隔开即可)都允许通过!

3. hasRole

  • 基本用法不变,但是源码显示:它会将我们声明的角色名xxx变成ROLE_+xxx。

因此,我们在Service中,声明具有的角色时,应该手动添加为ROLE_xxx。

#配置类

1
.antMatchers("/test/index").hasRole("salesman")

#Service声明具有的权限

1
List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,Role_salesman");

4. hasAnyRole

用法与hasAnyAutority一致。

自定义无权限403页面

  • 在配置类中,配置自定义403页面即可。

在这里插入图片描述

1
http.exceptionHandling().accessDeniedPage("/unauth.html");

如果满足无权限条件的话,会跳转到这个页面。
在这里插入图片描述

SpringSecurity 认证&授权 常见的注解

  • 注解的作用,就是简化开发。

@Secured(“ROLE_XXX, ROLE_YYY”)

这个注解表示:用户具有某个角色,可以根据这个角色来访问相应的接口。

使用这个注解时,我们要打开注解功能:

  1. 添加注解到Springboot启动类或者Config配置类上:

    1
    @EnableGlobalMethodSecurity(securedEnabled = true)
  2. 在Controller的方法上面,使用这个注解,自动为我们判断角色权限。

1
2
3
4
5
@GetMapping("/deleteUser")
@Secured("ROLE_admin")
public String deleteUser(){
return "通过权限,这是删除页面!";
}

表示这个方法,必须要具有admin角色才能进入。

@PreAuthorize

  • 这个是在方法执行之前进行校验。
  1. 在启动类相同位置,逗号,开启prePostEnabled = true

    1
    @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
  2. 在相应方法上,加入注解**@PreAuthorize()**

    1
    2
    3
    4
    5
    @GetMapping("/updateUser")
    @PreAuthorize("hasRole('ROLE_admin')")
    public String updateUser(){
    return "通过权限,这是更改页面!";
    }

在这里插入图片描述
可以看到,这里可以选择之前的四种:

  1. hasRole
  2. hasAnyRole
  3. hasAuthority
  4. hasAnyAuthority
  • 注意涉及到Role时,手动添加ROLE_XXX即可。

@PostAuthorize

  • 这种方式用得频率比较少,代表:执行方法之后,再进行校验一般适用于,带有返回值的校验方式!

  • 也就是说,方法是一定会执行的。*

只不过是,方法执行后,遇到了403权限错误。

1
2
3
4
5
6
@GetMapping("/afterMethodVerify")
@PostAuthorize("hasRole('ROLE_admin')")
public String afterMethodVerify(){
System.out.println('一定会执行的!');
return "通过权限,这是目标页面!";
}