Spring Security

SpringSspecurity 框架简介

1.1 概要

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

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

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

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

1.3 同款产品对比

1.3.1 Spring Security

SpringSecurity 特点:

⚫ 和 Spring 无缝整合。

⚫ 全面的权限控制。

⚫ 专门为 Web 开发而设计。

​ ◼旧版本不能脱离 Web 环境使用。

​ ◼新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独 引入核心模块就可以脱离 Web 环境。

⚫ 重量级。

1.3.2 Shiro

Apache 旗下的轻量级权限控制框架

特点:

⚫ 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求 的互联网应用有更好表现。

⚫ 通用性。

​ ◼好处:不局限于 Web 环境,可以脱离 Web 环境使用。

​ ◼缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。

Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之 前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直 是 Shiro 的天下

相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。

因此,一般来说,常见的安全管理技术栈的组合是这样的:

SSM + Shiro

Spring Boot/Spring Cloud + Spring Security

以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行 的。

1.4 模块划分

image-20210103195527111

SpringSecurity 入门案例

1、新建springboot工程

2、引入webspring-security启动器

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

3、新建HelloController

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/hello")
public class HelloController {

@GetMapping("/security")
public String hello() {
return "Hello security";
}
}

image-20210103195912390

4、启动项目

image-20210103195958703

5、访问资源

1
http://127.0.0.1:8080/hello/security

6、重定向到登录页

image-20210103200149012

7、登录

Username:user

Password:7bdcc880-5fb6-476e-af07-1e358a7c9558

image-20210103200237510

权限管理中的相关概念

主体

英文单词:principal

使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系 统谁就是主体。

认证

英文单词:authentication

权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证 明自己是谁。 笼统的认为就是以前所做的登录操作。

授权

英文单词:authorization

将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功 能的能力。 所以简单来说,授权就是给用户分配权限。

SpringSecurity 基本原理

SpringSecurity 本质是一个过滤器链: 从启动是可以获取到过滤器链:

image-20210103200539977

1
Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@41417eda,                                   org.springframework.security.web.context.SecurityContextPersistenceFilter@7ad34a20, org.springframework.security.web.header.HeaderWriterFilter@257f4a8, org.springframework.security.web.csrf.CsrfFilter@17034aa6, org.springframework.security.web.authentication.logout.LogoutFilter@391e5bb2, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@38c53a44, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@41c5e37, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2e820a2e, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@6a9cb9e5, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5c015c2a, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@239a68db, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@28edfd58, org.springframework.security.web.session.SessionManagementFilter@77b7b349, org.springframework.security.web.access.ExceptionTranslationFilter@70b1b0fc, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@56b861ef]

image-20210103201424336

UserDetailsService 接口讲解

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中 账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。

如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

UserDetailsService

1
2
3
4
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

使用方式:

实现``UserDetailsService类,并实现方法loadUserByUsername`

UserDetails

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface UserDetails extends Serializable {


/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
Collection<? extends GrantedAuthority> getAuthorities();

String getPassword();

String getUsername();

boolean isAccountNonExpired();

boolean isAccountNonLocked();

boolean isCredentialsNonExpired();

boolean isEnabled();
}

spring-security帮我们实现了一个implements UserDetails 的类User,我们只需要传用户名、密码、权限等参数

image-20210103202141482

  • 构造方法
1
2
3
4
public User(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public User(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {

if (((username == null) || "".equals(username)) || (password == null)) {
throw new IllegalArgumentException(
"Cannot pass null or empty values to constructor");
}

this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}

⚫ 方法参数 username

表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无 法接收。

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
25
26
27
28
29
30
31
public interface PasswordEncoder {

/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);

/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);

/**
* Returns true if the encoded password should be encoded again for better security,
* else false. The default implementation always returns false.
* @param encodedPassword the encoded password to check
* @return true if the encoded password should be encoded again for better security,
* else false.
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}

接口实现类

image-20210103202707194

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。

BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10

Web权限方案

认证-设置用户名和密码

04-web权限方案-认证-设置用户名和密码

第一种方式:配置文件

application.properties

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

第二种方式:配置类

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 passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("huangkai").password(password).roles("admin");
}

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

image-20210103204246785

第三种方式:通过实现UserDetailService

第一步:编写UserDetailService实现类,返回User对象,User对象有用户名、密码、权限列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

List<GrantedAuthority> grants = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

return new User("kesen", passwordEncoder.encode("123"), grants);


}

}

第二步:创建配置类,设置使用哪个UserDetailService实现类

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

@Autowired
private UserDetailsService myUserDetailService;

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

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

认证-查询数据库认证

05-web权限方案-认证-查询数据库认证

第一步:添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</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
26
27
public class User {

private long id;
private String name;
private String password;

public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

第三步:创建数据库

image-20210104204157739

第四步:添加Mapper接口

1
2
3
public interface UserMapper extends BaseMapper<User> {
}

第五步:完善UserDetailsService查询用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service(value = "myUserDetailService")
public class MyUserDetailService implements UserDetailsService {

@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

List<GrantedAuthority> grants = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

QueryWrapper<com.kesen.springsecuritydemo.entity.User> wrapper = new QueryWrapper<>();
wrapper.eq("name", username);
com.kesen.springsecuritydemo.entity.User user = userMapper.selectOne(wrapper);
if (null == user) {
throw new UsernameNotFoundException("用户名不存在!");
}
return new User(user.getName(), passwordEncoder.encode(user.getPassword()), grants);


}

}

第六步:添加MapperScan

1
2
3
4
5
6
7
8
9
@MapperScan("com.kesen.springsecuritydemo.mapper")
@SpringBootApplication
public class SpringSecurityDemoApplication {

public static void main(String[] args) {
SpringApplication.run(SpringSecurityDemoApplication.class, args);
}

}

第七步:配置数据源

1
2
3
4
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root

自定义设置登录页面

06-web权限方案-认证-自定义登录页面

第一步:配置类实现相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 自定义登录页面
.loginPage("/login.html") // 登录页设置
.loginProcessingUrl("/user/login")// 登录URL
.defaultSuccessUrl("/hello/success") //登录成功后的跳转路径
.and()// 结束符
.authorizeRequests() // 权限认证开始
.antMatchers("/","/hello/view","/user/login","/login.html").permitAll() // 这些是不需要验证的
.anyRequest().authenticated() //其他都需要验证
.and()// 结束符
.csrf().disable();// 关闭csrf
}

第二步:创建登录页和Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username">
<br/>
密码:<input type="password" name="password">
<input type="submit" value="login">
</form>
</body>
</html>

web权限方案-基于角色或者权限的访问控制

07-web权限方案-基于角色或权限的访问控制

第一种: hasAuthority

访问/hello/admin需要admin权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()// 自定义登录页面
.loginPage("/login.html") // 登录页设置
.loginProcessingUrl("/user/login")// 登录URL
.defaultSuccessUrl("/hello/success") //登录成功后的跳转路径
.and()// 结束符
.authorizeRequests() // 权限认证开始
.antMatchers("/","/hello/view","/user/login","/login.html").permitAll() // 这些是不需要验证的
.antMatchers("/hello/admin").hasAuthority("admin")
.anyRequest().authenticated() //其他都需要验证
.and()// 结束符
.csrf().disable();// 关闭csrf
}

只赋予audit权限

1
List<GrantedAuthority> grants = AuthorityUtils.commaSeparatedStringToAuthorityList("audit");

image-20210105071821447

==hasAuthority 要求用户拥有所有的权限才能访问==

第二种:hasAnyAuthority

==hasAnyAuthority要求用户拥有其中一个权限便可以访问==

配置

1
.antMatchers("/hello/admin").hasAnyAuthority("admin","audit")

赋予权限

1
List<GrantedAuthority> grants = AuthorityUtils.commaSeparatedStringToAuthorityList("audit,config");

image-20210105072338126

第三种:hasRole

  • 配置
1
.antMatchers("/hello/admin").hasRole("sale")
  • 源码最终将role拼接为ROLE_role
1
2
3
4
5
6
7
8
9
10
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException(
"role should not start with 'ROLE_' since it is automatically inserted. Got '"
+ role + "'");
}
return "hasRole('ROLE_" + role + "')";
}

  • 赋予权限
1
List<GrantedAuthority> grants = AuthorityUtils.commaSeparatedStringToAuthorityList("audit,config,ROLE_sale");

image-20210105073007250

第四种:hasAnyRole

hasAnyRole和hasRole的区别在于,它只要求用户拥有其中一个角色便可以访问,hasRole要求用户拥有权限控制中的全部角色。

自定义403页面

08-web权限方案-配置403访问页面

配置

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

页面

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Forbidden</title>
</head>
<body>
<h1>没有访问权限</h1>
</body>
</html>

image-20210106212926635

注解使用

09-web权限方案-注解使用

@Secured

开启注解
1
@EnableGlobalMethodSecurity(securedEnabled = true)

先看没有角色的情况

1
2
3
4
5
6
@GetMapping("/secured")
@Secured({"ROLE_normal","ROLE_admin"})
public String secured() {
return "hello secured";
}
}
用户拥有的角色:
1
List<GrantedAuthority> grants = AuthorityUtils.commaSeparatedStringToAuthorityList("audit,config,ROLE_sale");
结果

image-20210106214011710

给用户添加角色:
1
List<GrantedAuthority> grants = AuthorityUtils.commaSeparatedStringToAuthorityList("audit,config,ROLE_sale, ROLE_admin");
结果

image-20210106214308608

@PreAuthorize

==在进入方法前进行权限校验==

配置
1
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
使用
1
2
3
4
5
6
@PreAuthorize("hasAnyAuthority('config')")
@GetMapping("/preAuthorize")
public String preAuthorize() {

return "hello preAuthorize";
}
1
List<GrantedAuthority> grants = AuthorityUtils.commaSeparatedStringToAuthorityList("audit,config,ROLE_sale, ROLE_admin");
验证

image-20210106220146144

@PostAuthorize

==在方法执行之后进行校验==

配置
1
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
使用
1
2
3
4
5
6
@PostAuthorize("hasAnyRole('管理员')")
@GetMapping("/postAuthorize")
public String postAuthorize() {
System.out.println("postAuthorize");
return "hello postAuthorize";
}
1
List<GrantedAuthority> grants = AuthorityUtils.commaSeparatedStringToAuthorityList("audit,config,ROLE_sale, ROLE_admin");
验证

image-20210106220229533

image-20210106220235029

@PostFilter

==权限验证之后对数据进行过滤==

==留下filter匹配的数据==

使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/list")
@PreAuthorize("hasRole('ROLE_admin')")
@PostFilter("filterObject.name == 'admin1'")
@ResponseBody
public List<User> getAllUser(){
ArrayList<User> list = new ArrayList<>();
User user1 = new User();
user1.setName("admin1");
User user2= new User();
user2.setName("admin2");
list.add(user1);
list.add(user2);
return list;
}
测试

image-20210106220927972

测试样例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("/list")
@PreAuthorize("hasRole('ROLE_admin')")
@PostFilter("filterObject.name == 'admin1' || filterObject.name == 'admin2'")
@ResponseBody
public List<User> getAllUser(){
ArrayList<User> list = new ArrayList<>();
User user1 = new User();
user1.setName("admin1");
User user2= new User();
user2.setName("admin2");
User user3= new User();
list.add(user1);
list.add(user2);
list.add(user3);
return list;
}

image-20210106221123288

@PreFilter

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

使用
1
2
3
4
5
6
7
8
9
10
@PostMapping("/preFilter")
@PreAuthorize("hasRole('ROLE_admin')")
@PreFilter(value = "filterObject.id%2==0")
public List<User> getTestPreFilter(@RequestBody List<User> list){
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getName());
});
return list;
}

测试

image-20210106223125907

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
{
"id": 1,
"name": "kesen",
"password": "dfasd"
},
{
"id": 2,
"name": "kesen2",
"password": "dfasd"
},
{
"id": 3,
"name": "kesen3",
"password": "dfasd"
}
]

image-20210106223200404

image-20210106223110526

用户注销功能

10-web权限方案-用户注销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void configure(HttpSecurity http) throws Exception {
http.logout().logoutUrl("/logout").logoutSuccessUrl("/out.html").permitAll();

http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin()// 自定义登录页面
.loginPage("/login.html") // 登录页设置
.loginProcessingUrl("/user/login")// 登录URL
.defaultSuccessUrl("/success.html") //登录成功后的跳转路径
.and()// 结束符
.authorizeRequests() // 权限认证开始
.antMatchers("/","/hello/view","/user/login","/login.html").permitAll() // 这些是不需要验证的
//.antMatchers("/hello/admin").hasAnyAuthority("admin","audit")
.antMatchers("/hello/admin").hasRole("sale")
.antMatchers("/hello/auth").hasRole("auth")
.anyRequest().authenticated() //其他都需要验证
.and()// 结束符
.csrf().disable();// 关闭csrf
}
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功
<a href="/logout">退出</a>
</body>
</html>

image-20210106224516107

点击退出

image-20210106224620091

==再次请求需要登录==

image-20210106224934711