SpringSecurity

第一章:SpringSecurity入门

功能

  • 身份认证(authentication)
    • 身份认证是验证谁正在访问系统资源 ,判断用户是否为合法用户。认证用户的常见方式是要求用户输入用户名和密码。
  • 授权(authorization)
    • 用户进行身份认证后,系统会控制谁能访问哪些资源,这个过程叫做授权。用户无法访问没有权限的资源。
  • 防御常见攻击(protection against common attacks)
    • CSRF
    • HTTP Headers
    • HTTP Requests

案例-身份认证

创建项目引入依赖

pom文件依赖

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
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

创建基础启动类

1
2
3
4
5
6
@SpringBootApplication
public class SpringSecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityDemoApplication.class, args);
}
}

创建Controller对象

1
2
3
4
5
6
7
@Controller
public class IndexController {
@GetMapping("/index")
public String index() {
return "index";
}
}

创建前台页面

1
2
3
4
5
6
7
8
9
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Hello Security!</title>
</head>
<body>
<h1>Hello Security</h1>
<a th:href="@{/logout}">Log Out</a>
</body>
</html>

SpringSecurity页面加载慢

引入SpringSecurity之后,Security会自动判断请求是否已经登录,如果没有登录则会跳转到Security的自带登录页面,可以使用logout登出系统

SpringSecurity自带登录页样式加载不出来: SpringSecurity会引入一个Bootstrap依赖,我们需要开启梯子之后才能访问器样式

SpringSecurity做了什么

  • 保护应用的所有URL,要求在访问该系统的URL的时候都需要进行身份验证
  • 自动生成了一个初始用户user,密码在控制台上
  • 生成登录页和登出页
  • 对于未登录的web请求,重定向到登录页面
  • 对应服务请求,返回401,未授权
  • 处理跨站请求伪造(CSRF)攻击
  • 处理会话劫持攻击
  • 写入Strict-Transport-Security以确保HTTPS
  • 写入X-Content-Type-Options以处理嗅探攻击
  • 写入Cache-Control头来保护经过身份验证的资源
  • 写入X-Frame-Options以处理点击劫持攻击

SpringSecurity底层原理

SpringSecurity是通过层层过滤器来进行操作的

客户端进行访问请求的时候需要通过层层过滤器,然后到达Servlet端

传统的应用启动时,需要将过滤器全部注册进Servlet中,但是Spring希望可以将过滤器设置成Bean对象,放到Spring容器中进行管理

image-20241022220032015

通过DelegatingFilterProxy来进行Bean容器的代理,DelegatingFilterProxy可以将我们注册Spring容器的中的bean对象中的过滤器加载到Servlet生命周期中

方式:先将自己的过滤器注册进Spring对象中,通过DelegatingFilterProxy将自己注册的filter对象加载到Servlet生命周期中

DelegatingFilterProxy也是一个FIlter,会被注册进行Servlet容器链中,这样我们注册在SpringBean容器中的过滤器就可以通过DelegatingFilterProxy代理进Servlet容器中

image-20241022220521696

通常我们注册的Filter都不只是一个,所以DelegatingFilterProxy管理的是一个FIlter链

Spring容器中可以有很多个Filter链,所以DelegatingFilterProxy需要通过一个FilterChainProxy来进行过滤器链的管理

image-20241022221326815

SpringFilter链中可以有多个Filter

image-20241022221459823

通过多个FilterChain让我们程序更加灵活

客户端通过向服务端发送请求的时候,FilterChainProxy通过判断请求地址,将请求送到指定的SecurityFilterChain中进行处理,如果SecurityFilterChain中没有指定路径的请求,则会送到最后一个SecurityFilterChain中进行处理

image-20241022221619703

DelegatingFilterProxy帮助调用Spring中注册的Filter,FilterChainProxy帮助我们管理多个SecurityFilterChain,SecurityFilterChain0 - n可以处理各种业务逻辑

程序启动和运行

DefaultSecurityFilterChain

DefaultSecurityFilterChain实现了SecurityFilterChain接口,是SecurityFilterChain的实现体

1
public final class DefaultSecurityFilterChain implements SecurityFilterChain {}

DefaultSecurityFilterChain默认实现15个过滤器

image-20241022223029593

SecurityProperties

SecurityProperties配置Security初始化用户名和密码

用户名为user,密码为UUID

image-20241022223429771

也可以通过配置文件来配置初始化用户名和密码

在spring配置文件中修改默认用户

1
2
3
4
5
6
7
spring:
application:
name: SpringSecurityDemo
security:
user:
name: default
password: default

第二章 SpringSecurity自定义配置

基于内存的用户认证

创建自定义配置

UserDetailsService用来管理用户信息, InMemoryUserDetailsServiceManager是UserDetailsService的一个实现,用来管理内存用户信息。

创建webSecurityConfig对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class webSecurityConfig {

@Bean
public UserDetailsService userDetailsService() {
// 创建基于内存的用户管理对象
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 使用InMemoryUserDetailsManager来管理UserDetails对象
manager.createUser(
// 创建UserDetails对象,用于管理用户名,用户密码,用户角色,用户权限等内容
User.withDefaultPasswordEncoder()
.username("user") // 自定义用户名
.password("password") // 自定义密码
.roles("USER") // 自定义角色
.build()
);
return manager;
}
}

基于内存的用户认证流程

  • 程序启动时:
    • 创建InMemoryUserDetailsManager对象
    • 创建UserDetails对象,封装用户名密码
    • 通过我们创建的userDetailsService的Bean对象中实现将初始化对象设置到InMemoryUserDetailsManager内存管理对象中
  • 校验用户时:
    • 在AbstractAuthenticationProcessingFilter中拦截到发送的请求
    • 通过UsernamePasswordAuthenticationFilter去校验用户名密码
    • AbstractUserDetailsAuthenticationProvider来获取用户对象,并校验
    • DaoAuthenticationProvider获取用户对象
    • InMemoryUserDetailsServiceManager使用loadUserByUsername通过我们输入的用户名去内存中寻找该用户名的用户
    • AbstractUserDetailsAuthenticationProvider调用DaoAuthenticationProvider的additionalAuthenticationChecks方法去校验密码,因为用户名已经在InMemoryUserDetailsServiceManager的loadUserByUsername方法中校验成功了,如果loadUserByUsername无法根据我们输入的用户名来获取到指定用户,则表明用户校验失败

基于数据库的用户认证

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

配置数据源

在application.yml文件中配置数据源

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: SpringSecurityDemo
security:
user:
name: default
password: default
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2024
username: root

基于数据库的用户认证流程

类似基于内存的用户认证流程中

  • 首先创建了InMemoryUserDetailsManager管理用户对象

  • 然后再通过创建UserDetails对象来设置一个初始化对象放到内存中

  • 等后续来了用户之后,使用InMemoryUserDetailsServiceManager中的loadUserByUserName方法从内存中获取我们初始化的用户对象

  • 在UsernamePasswordAuthenticationFilter中的attemptAuthentication方法将用户输入的用户名和密码进行对比校验

基于数据库的用户认证流程和基于内存的用户认证流程的区别是

  • 无需通过初始化用户到内存中
  • 重构loadUserByUserName方法,通过用户名去数据库中获取相应的用户信息

基于数据库的用户认证流程
程序启动时

  • 创建DBUserDetailsManager对象,实现 UserDetailsManager, UserDetailsPasswordService这两个接口

用户校验时

  • SpringSecurity通过调用DBUserDetailsManager的loadUserByUserName方法从数据库中获取对应的用户对象
  • UsernamePasswordAuthenticationFilter中的attemptAuthentication方法对用户输入的账号密码和从数据库中查到的账号密码进行比对

创建DBUserDetailsManager对象

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
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Resource
private IUserService userService;
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}

@Override
public void createUser(UserDetails user) {

}

@Override
public void updateUser(UserDetails user) {

}

@Override
public void deleteUser(String username) {

}

@Override
public void changePassword(String oldPassword, String newPassword) {

}

@Override
public boolean userExists(String username) {
return false;
}

/**
* 通过Username查询数据库对象
* @param username 用户名
* @return 查询的用户对象
* @throws UsernameNotFoundException 异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (user == null) {
// 通过查看InMemoryUserDetailsManager中的loadUserByUsername方法,当查不到该用户的时候throw出UsernameNotFoundException错误
throw new UsernameNotFoundException(username);
} else {
Collection<GrantedAuthority> authorities = new ArrayList<>();
new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
true, // 判断账号是否过期
true, // 判断用户平
true, // 判断用户是否为被锁定
authorities);
}
return null;
}

配置SecurityConfig对象来进行用户获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class webSecurityConfig {

/**
* 创建基于数据库的用户管理
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
// 创建我们自己实现的的DBUserDetailsManagers
DBUserDetailsManager manager = new DBUserDetailsManager();
return manager;
}
}

上述在配置文件中注册我们自己创建的DBUserDetailsManager对象,是根据基于内存的用户创建方式类比过来的。

其实我们通过创建DBUserDetailsManager类实现了UserDetailsManager, UserDetailsPasswordService两个类,我们创建的DBUserDetailsManager,通过@Configuration注解可以将DBUserDetailsManager放到SpringBoot的上下文中,无需通过配置文件再次引入

SpringSecurity的默认配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class webSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 开启授权保护
http.authorizeHttpRequests(
authorize -> authorize
// 对所有请求开启授权保护
.anyRequest()
// 已认证的请求会被自动授权
.authenticated()
)
// 表单授权方式
.formLogin(withDefaults())
// 基本授权方式
.httpBasic(withDefaults());
return http.build();
}
}

添加用户功能

创建Controller

1
2
3
4
@PostMapping("/save")
public void saveUserDetail(@RequestBody User user) {
userService.saveUserDetail(user);
}

创建Service

1
2
3
4
5
6
7
8
9
@Override
public void saveUserDetail(User user) {
UserDetails userDetails = org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder()
.username("user") // 自定义用户名
.password("password") // 自定义密码
.roles("USER") // 自定义角色
.build();
dbUserDetailsManager.createUser(userDetails);
}

实现DBUserDetailsManager方法

1
2
3
4
5
6
7
@Override
public void createUser(UserDetails userDetails) {
User user = new User();
user.setUsername(userDetails.getUsername());
user.setPassword(userDetails.getPassword());
userService.save(user);
}

密码加密算法

密码加密方式

明文密码

  • 密码以明文形式存储在数据库中。但是恶意用户可能会通过SQL注入等手段获取到明文密码,或者程序员将数据库数据泄露的情况也可能发生。

Hash算法

  • Spring Security的PasswordEncoder 接口用于对密码进行单向转换,从而将密码安全地存储。对密码单向转换需要用到哈希算法 ,例如MD5、SHA-256、SHA-512等,哈希算法是单向的,只能加密,不能解密
  • 因此,数据库中存储的是单向转换后的密码,Spring Security在进行用户身份验证时需要将用户输入的密码进行单向转换,然后与数据库的密码进行比较
  • 因此,如果发生数据泄露,只有密码的单向哈希会被暴露。由于哈希是单向的,并且在给定哈希的情况下只能通过暴力破解的方式猜测密码

彩虹表:

  • 彩虹表就是一个庞大的、针对各种可能的字母组合预先生成的哈希值集合,有了它可以快速破解各类密码。越是复杂的密码,需的彩虹表就越大,主流的彩虹表都是188G以上,目前主要的算法有LM,NTLM,MD5,SHA1,MYSQLSHA1,HALFLMCHALL,NTLMCHALL, ORACLE-SYSTEM, MD5-HALF

加盐密码:

  • 为了减轻彩虹表的效果,开发人员开始使用加盐密码。不再只使用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为盐)。盐和用户的密码将一起经过哈希函数运算,生成一个唯一的哈希。盐将以明文形式与用户的密码一起存储。然后,当用户尝试进行身份验证时,盐和用户输入的密码一起经过哈希函数运算,再与存储的密码进行比较。唯一的盐意味着彩虹表不再有效,因为对于每个盐和密码的组合,哈希都是不同的。

自适应单向函数:

  • 随着硬件的不断发展,加盐哈希也不再安全。原因是,计算机可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地破解每个密码。
  • 现在,开发人员开始使用自适应单向函数来存储密码。使用自适应单向函数验证密码时,故意占用资源(故意使用大量的CPU、内存或其他资源)。自适应单向函数允许配置一个“工作因子”,随着硬件的改进而增加。我们建议将“工作因子”调整到系统中验证密码需要约一秒钟的时间。这种权衡是为了让攻击者难以破解密码
  • 自适应单向函数包括bcrypt、PBKDF2、scrypt和argon2。

PasswordEncoder

BcryptPasswordEncoder

使用广泛支持的bcrypt算法来对密码进行哈希。为了增加对密码破解的抵抗力,bcrypt故意设计得较慢。和其他自适应单向函数一样,应该调整其参数,使其在您的系统上验证一个密码大约需要1秒的时间。

BCryptPasswordEncoder的默认实现使用强度10。建议您在自己的系统上调整和测试强度参数,以便验证密码时大约需要1秒的时间。

Argon2PasswordEncoder

使用Argon2算法对密码进行哈希处理。Argon2是密码哈希比赛的获胜者。为了防止在自定义硬件上进行密码破解,Argon2是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。

当前的Argon2PasswordEncoder实现需要使用BouncyCastle库。

Pbkdf2PasswordEncoder

使用PBKDF2算法对密码进行哈希处理。为了防止密码破解,PBKDF2是一种故意缓慢的算法。与其他自适应单户函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当需要FIPS认证时,这种算法是一个很好的选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
void testPassword() {
// 设置工作因子,默认值是0,最小值4,最大值31,值越大运算速度越慢
PasswordEncoder encoder = new BCryptPasswordEncoder(4);
// 明文:password
// 密文:result,即使明文密码相同,每次生成的密文也不一样
String result = encoder.encode("password");
System.out.println(result);

// 密码校验
Assert.isTrue(encoder.matches("password", result), "密码不一致");


}

DelegatingPasswordEncoder

  • 表中存储的密码形式:(bcrypt}$2aS10SGRLdNijsQMUvl/au9ofL.eDwmoohzzS7.mmNSJZ.0FxO/BTk76klW
  • 通过如下源码可以知道:可以通过(bcrypt}前缀动态获取和密码的形式类型一致的PasswordEncoder对象
  • 目的:方便随时做密码策略的升级,兼容数据库中的老版本密码策略生成的密码

image-20241027213512881

自定义登录页面

创建登录页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>登录</title>
</head>
<body>
<h1>登录</h1>
<div th:if="${param.error}">错误的用户名和密码</div>
<form th:action="@{/login}" method="post" >
<div>
<input type="text" name="username" placeholder="用户名" />
</div>
<div>
<input type="password" name="password" placeholder="密码" />
</div>
<input type="submit" value="登录"/>
</form>
</body>
</html>

即使配置了登录页面,也同样会跳转到默认登录页面

更改配置项

因为在SpringSecurity配置中配置的了登录验证的页面,配置的是默认的页面。

我们需要将自定义的登录页面,配置到验证中,并设置permitAll(),如果不设置该选项,则会一直重定向

因为不设置这个无需授权的选项,登录页也需要授权,但是当时我们并没有登录,所以会跳转到自定义登录页面,自定义登录页面有需要授权,由此会重定向过多导致出错

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  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class webSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 开启授权保护
http.authorizeHttpRequests(
authorize -> authorize
// 对所有请求开启授权保护
.anyRequest()
// 已认证的请求会被自动授权
.authenticated()
)
// 表单授权方式
.formLogin(from -> {
from.loginPage("/login").permitAll();
});
// 关闭csrf网络攻击
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}

注意点

登录参数必须是username和password,如果参数名不是这两个则需要进行单独配置

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
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class webSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 开启授权保护
http.authorizeHttpRequests(
authorize -> authorize
// 对所有请求开启授权保护
.anyRequest()
// 已认证的请求会被自动授权
.authenticated()
)
// 表单授权方式
.formLogin(from -> {
from.loginPage("/login")
.permitAll()
.usernameParameter("myUsername") // 配置登录用户名参数 默认是username
.passwordParameter("myPassword"); // 配置登录用户密码参数,默认是password

});
// 关闭csrf网络攻击
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}

配置失败的返回页面

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
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class webSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 开启授权保护
http.authorizeHttpRequests(
authorize -> authorize
// 对所有请求开启授权保护
.anyRequest()
// 已认证的请求会被自动授权
.authenticated()
)
// 表单授权方式
.formLogin(from -> {
from.loginPage("/login")
.permitAll()
.usernameParameter("myUsername") // 配置登录用户名参数 默认是username
.passwordParameter("myPassword") // 配置登录用户密码参数,默认是password
.failureUrl("/login?failure") // 配置登录失败的返回页面
;

});
// 关闭csrf网络攻击
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}

第三章 前后端分离

后端需要在登录成功或失败的时候返回给前端一个JSON字符串,而不是让控制前台页面跳转到登录页面

认证流程

用户在输入账号密码的时候是需要调用UsernamePasswordAuthenticationFilter过滤器生成一个UsernamePasswordAuthenticationToken对象,通过AuthenticationManager管理器来进行登录用户的认证。

登录成功最后调用AuthenticationSuccessHandler处理器来处理成功结果

失败最后调用AuthenticationFailureHandler来处理失败请求

usernamepasswordauthenticationfilter-16822329079281

引入FastJson依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.37</version>
</dependency>

认证成功响应

配置认证成功处理器AuthenticationSuccessHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

Object principal = authentication.getPrincipal(); // 获取用户身份信息
// Object credentials = authentication.getCredentials(); // 获取用户凭证信息,如果用密码登录则获取的是密码
// Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 获取用户权限信息

SysResult<Object> sysResult = new SysResult<>(); // 设置返回结果
sysResult.ok(principal);
// 设置Json相应结果
response.setContentType("application/json;charset=UTF-8");
// 将放回结果返回给前端
response.getWriter().println(JSON.toJSONString(sysResult));
}
}

将处理器注册进SpringSecurity配置中

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
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class webSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 开启授权保护
http.authorizeHttpRequests(
authorize -> authorize
// 对所有请求开启授权保护
.anyRequest()
// 已认证的请求会被自动授权
.authenticated()
)
// 表单授权方式
.formLogin(from -> {
from.loginPage("/login")
.permitAll()
.usernameParameter("username") // 配置登录用户名参数 默认是username
.passwordParameter("password") // 配置登录用户密码参数,默认是password
.failureUrl("/login?failure") // 配置登录失败的返回页面
.successHandler(new MyAuthenticationSuccessHandler()) // 注册登录成功处理器
;

});
// 关闭csrf网络攻击
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}

认证失败相应

通认证成功相似

先创建认证失败处理器

MyAuthenticationFailureHandler

1
2
3
4
5
6
7
8
9
10
11
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 获取登录失败信息
String localizedMessage = exception.getLocalizedMessage();
// 设置响应头信息
response.setContentType("application/json;charset=utf-8");
// 将失败信息返回给前台页面
response.getWriter().println(JSON.toJSONString(SysResult.errorResult(-1, localizedMessage)));
}
}

将认证失败处理器注册进SpringSecurity配置中

1
2
3
4
5
6
7
8
9
10
11
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 获取登录失败信息
String localizedMessage = exception.getLocalizedMessage();
// 设置响应头信息
response.setContentType("application/json;charset=utf-8");
// 将失败信息返回给前台页面
response.getWriter().println(JSON.toJSONString(SysResult.errorResult(-1, localizedMessage)));
}
}

注销处理

类似登陆成功是登录失败处理流程

先创建注销处理器

MyLogoutSuccessHandler

1
2
3
4
5
6
7
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(JSON.toJSONString(SysResult.okResult(200, "注销成功")));
}
}

注册进Security配置文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class webSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.logout(logout -> {
logout.logoutSuccessHandler(new MyLogoutSuccessHandler()); // 注册注销成功处理器
});
// 关闭csrf网络攻击
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}

请求未认证的接口

当我们访问一个需要认证的的接口的时候,SpringSecurity会判断是否已经认证成功,如果认证成功,则可以访问,如果未成功,SpringSecurity会自动调转到登录页面,请求用户认证

期望登录不成功的时候不进行页面的跳转,而是返回JSON信息

我们需要实现AuthenticationEntryPoint接口

1
2
3
4
5
6
7
8
9
10
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 获取未认证条件信息
String localizedMessage = authException.getLocalizedMessage();
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(JSON.toJSONString(SysResult.errorResult(-1, localizedMessage)));

}
}

进行配置注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class webSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 为认证处理结果
http.exceptionHandling(exception -> {
exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()); // 注册未认证结果处理器
});
return http.build();
}
}

跨越问题

跨域:在网络请求中,前台请求后台,默认需要在同一台服务器上需要相同的IP和协议,如果不同的话,则会进行跨域拦截

在前后台分离系统中可能前台系统和后台系统部署在不同的服务器中,这时候前台系统的IP地址和后台服务的IP地址就不相同,这时候我们需要进行跨域解决

跨域全称是跨域资源共享(Cross-Origin Resources Sharing,CORS),它是浏览器的保护机制,只允许网页请求统一域名下的服务,同一域名指=>协议、域名、端口号都要保持一致,如果有一项不同,那么就是跨域请求。在前后端分离的项目中,需要解决跨域的问题。

在SpringSecurity中解决跨域很简单,在配置文件中添加如下配置即可

1
2
//跨域
http.cors(withDefaults());

第四章 身份认证

用户认证信息

基本概念

securitycontextholder

在SpringSecurity中与身份认证有关的是Authentication===》可以直接获取到用户信息,SecurityContext和SecurityContextHolder

他们之间的关系是:

  1. SecurityContextHolder:是SpringSecurity存储用户详细信息的地方
  2. SecurityContext:是从SecurityContextHolder中获取的内容,包含当前已认证的用户的Authentication信息
  3. Authentication:表示用户的身份认证信息,包含了Principal,Credentials和Authorities对象
  4. Principal:表示用户的身份标识,通常是一个表示用户的实体对象,例如用户名,Principal可以通过Authentication的getPrincipal方法获取
  5. Credentials:表示用户的凭证信息,例如密码、证书或其他认证凭据。Credential可以通过Authentication对象的getCredentials()方法获取。
  6. Authorities:表示用户被授予的权限

总结起来,SecurityContextHolder用于管理当前线程的安全上下文,存储已认证用户的详细信息,其中包含了SecurityContext对象,该对象包含了Authentication对象,后者表示用户的身份验证信息,包括Principal(用户的身份标识)和Credential(用户的凭证信息)。

获取用户详细信息

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
@RestController
public class IndexController {
@GetMapping("/index")
public SysResult<HashMap<String, Object>> index() {
// 通过SecurityContextHolder对象来获取Security用户上下文
SecurityContext context = SecurityContextHolder.getContext();
// 通过用户上下文来获取认证对象
Authentication authentication = context.getAuthentication();
// 获取用户身份对象
Object principal = authentication.getPrincipal();
// 获取用户权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 获取用登录凭证
Object credentials = authentication.getCredentials();
// 获取登录用户名
String name = authentication.getName();
// 设置相应信息
HashMap<String, Object> result = new HashMap<>();
result.put("username", name);
result.put("authorities", authorities);
result.put("principal", principal);
return SysResult.okResult(result);

}
}

会话并发处理

选择同一个账号可以在几个设备进行登录,如果超出规定设备数量的时候,需要将前面的设备的Session会话注销

Security通过SessionInformationExpiredStrategy接口来实现该功能

实现处理器接口

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MySessionInformationExpireStrategy implements SessionInformationExpiredStrategy {
/**
* 当Session超时被强制退出的时候需要调用该方法
*/
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
// 获取响应头,向响应头中添加返回信息
HttpServletResponse response = event.getResponse();
// 设置响应格式JSON格式
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(JSON.toJSONString(SysResult.errorResult(-1, "该账号已从其他设备登录")));
}
}

配置接口

在WebSecurityConfig中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class WebSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 用户会话相关配置
http.sessionManagement(session -> {
// 设置最大登录数量
session.maximumSessions(1)
// 注册Session被强制退出之后才处理器
.expiredSessionStrategy(new MySessionInformationExpireStrategy());
});
return http.build();
}
}

第五章 授权

SpringSecurity有两种授权形式:

  • 用户-权限-资源:将权限分配至用户上,用户权限的用户,才能访问指定的接口和指定的前台菜单或按钮
  • 用户-角色-权限-资源:将权限分配到角色上,拥有该角色的用户都可以使用该角色的权限,更加灵活

基于Request的授权

用户-权限-资源

需求:

  • 拥有USER_LIST权限的用户可以访问/user/list的接口
  • 拥有USER_ADD权限的用户可以访问/user/add的接口

配置:在配置项中配置指定资源需要的权限信息

/user/list接口信息只有拥有USER_LIST权限的用户才能访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration  // 表明该类是一个配置类
// 开SpringSecurity的自定义配置 在SpringBoot项目中可以省略, SpringBoot项目会在引入SpringSecurity的时候加载该注解
@EnableWebSecurity
public class WebSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 开启授权保护
http.authorizeHttpRequests(
authorize -> authorize
// 配置权限,只有拥有后面权限的用户才嫩访问对应的接口
.requestMatchers("/user/list").hasAuthority("USER_LIST")
.requestMatchers("/user/add").hasAuthority("USER_ADD")
// 对所有请求开启授权保护
.anyRequest()
// 已认证的请求会被自动授权
.authenticated()
);
return http.bulid();
}
}

配置用户信息

在DBUserDetailsManager的loadUserByUsername方法中来获取用户信息,并设置用户权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (user == null) {
// 通过查看InMemoryUserDetailsManager中的loadUserByUsername方法,当查不到该用户的时候throw出UsernameNotFoundException错误
throw new UsernameNotFoundException(username);
} else {
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 手动设置用户权限
authorities.add(() -> "USER_LIST");
authorities.add(() -> "USER_ADD");
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
true, // 判断账号是否过期
true, // 判断用户平
true, // 判断用户是否为被锁定
authorities);
}
}

在配置好用户信息之后,如果拥有指定权限访问指定接口的时候可以访问通过如果没有指定权限访问指定接口的时候会返回403无权限

用户-角色-资源

需求:角色为ADMIN的用户才可以访问/user/**路径下的资源

用户-角色-权限-资源设置方法类似,也是需要在过滤器中配置,只是把hasAuthority,改成hasRole即可

过滤器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 开启授权保护
http.authorizeHttpRequests(
authorize -> authorize
// 配置权限,只有拥有后面权限的用户才嫩访问对应的接口
//.requestMatchers("/user/list").hasAuthority("USER_LIST")
//.requestMatchers("/user/add").hasAuthority("USER_ADD")
.requestMatchers("user/**").hasRole("ADMIN")
// 对所有请求开启授权保护
.anyRequest()
// 已认证的请求会被自动授权
.authenticated()

在loadUserByUsername类中配置获取角色信息,带上角色权限

可以在数据库中配置角色信息,这里就使用硬编码直接给用户附上角色

之前的loadUserByUsername是通过new org.springframework.security.core.userdetails.User新建一个用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (user == null) {
// 通过查看InMemoryUserDetailsManager中的loadUserByUsername方法,当查不到该用户的时候throw出UsernameNotFoundException错误
throw new UsernameNotFoundException(username);
} else {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(() -> "USER_LIST");
authorities.add(() -> "USER_ADD");

return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
true, // 判断账号是否过期
true, // 判断用户平
true, // 判断用户是否为被锁定
authorities);
}
}

上面该方法无法直接个用户角色赋值

使用org.springframework.security.core.userdetails.User.withUsername()方法来创建用户,可以附带角色信息

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
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (user == null) {
// 通过查看InMemoryUserDetailsManager中的loadUserByUsername方法,当查不到该用户的时候throw出UsernameNotFoundException错误
throw new UsernameNotFoundException(username);
} else {
// Collection<GrantedAuthority> authorities = new ArrayList<>();
// authorities.add(() -> "USER_LIST");
// authorities.add(() -> "USER_ADD");
//
// return new org.springframework.security.core.userdetails.User(
// user.getUsername(),
// user.getPassword(),
// user.getEnabled(),
// true, // 判断账号是否没有过期
// true, // 判断用户平
// true, // 判断用户是否为被锁定
// authorities);
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername()) // 用户名
.password(user.getPassword()) // 用户密码
.disabled(!user.getEnabled()) // 用户是否禁用
.credentialsExpired(false) // 用户是否过期
.accountLocked(false) // 账号是否锁定
.roles(List.of("ADMIN").toArray(new String[0]))
.build();
}
}

用户角色权限:本质也是将角色信息放到Authorities(权限)中,是将角色信息在前面添加一个ROLE标志,例如上面的ADMIN角色,放到Authorities中是ROLE_ADMIN

用户-角色-权限-资源

RBAC(Role_Based_Assess_Control,基于角色的访问控制)是一种常用的数据库设计方案,它将用户的权限分配和管理与角色相关联。

以下是一个基本的RBAC数据库设计方案的示例:

  1. 用户表(User table):包含用户的基本信息,例如用户名、密码和其他身份验证信息。
列名 数据类型 描述
user_id int 用户ID
username varchar 用户名
password varchar 密码
email varchar 电子邮件地址
  1. 角色表(Role table):存储所有可能的角色及其描述。
列名 数据类型 描述
role_id int 角色ID
role_name varchar 角色名称
description varchar 角色描述
  1. 权限表(Permission table):定义系统中所有可能的权限。
列名 数据类型 描述
permission_id int 权限ID
permission_name varchar 权限名称
description varchar 权限描述
  1. 用户角色关联表(User-Role table):将用户与角色关联起来。
列名 数据类型 描述
user_role_id int 用户角色关联ID
user_id int 用户ID
role_id int 角色ID
  1. 角色权限关联表(Role-Permission table):将角色与权限关联起来。
列名 数据类型 描述
role_permission_id int 角色权限关联ID
role_id int 角色ID
permission_id int 权限ID

在这个设计方案中,用户可以被分配一个或多个角色,而每个角色又可以具有一个或多个权限。通过对用户角色关联和角色权限关联表进行操作,可以实现灵活的权限管理和访问控制。

当用户尝试访问系统资源时,系统可以根据用户的角色和权限决定是否允许访问。这样的设计方案使得权限管理更加简单和可维护,因为只需调整角色和权限的分配即可,而不需要针对每个用户进行单独的设置

基于方法的授权

上述基于Request的授权,是通过request连接器实现的。

基于方法的授权是通过在方法上设置权授权注解实现的

开启其方法授权

在SpringSecurity配置文件中添加上@EnableMethodSecurity注解

1
2
3
4
5
6
7
8
9
10
11
@Configuration 
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

}
}

给用户授予角色和权限

DBUserDetailsManager中的loadUserByUsername方法:

1
2
3
4
5
6
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles("ADMIN")
.authorities("USER_ADD", "USER_UPDATE")
.build();

常用授权注解

使用@PreAuthorize注解来进行权限校验,使用hasRole 和hasAuthority来进行权限认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;

@GetMapping("/list")
@PreAuthorize("hasRole('ADMIN')")
public List<User> getUserList() {
return userService.list();
}

@PostMapping("/save")
@PreAuthorize("hasAuthority('USER_SAVE')")
public void saveUserDetail(@RequestBody User user) {
userService.saveUserDetail(user);
}
}

list方法需要拥有角色ADMIN才能访问,下面save需要拥有权限USER_SAVE才能访问

也可以写复杂的权限表达式来进行判断,下面就是同时需要拥有ADMIN角色的用户和用户名是admin的用户才能访问list接口

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;

@GetMapping("/list")
@PreAuthorize("hasRole('ADMIN') and authentication.name == 'admin'")
public List<User> getUserList() {
return userService.list();
}
}

第六章 OAuth2

OAuth2简介

是什么

“Auth” 表示 “授权” Authorization

“O” 是 Open 的简称,表示 “开放”

连在一起就表示 “开放授权”,OAuth2是一种开放授权协议。

首先需要有一个资源服务器来存放管理用户资源

image-20241105170931050

其次需要有一个客户应用来访问资源服务器,通过资源服务器来获取指定资源信息

image-20241105171847690

如果有恶意程序来访问资源服务器时候,如果资源服务器不加以检查则会导致用户数据泄露

image-20241105172001488

这时候需要给客户端应用颁发一个AssetsToken来进行用户认证拥有AssetsToken的应用才能访问用户数据

image-20241105172246054

谁给客户端颁发AssetsToken呢? === 授权服务器

image-20241105172603906

授权服务器何时给用户颁发AssetsToken呢?

在客户端想要访问资源服务器的时候,发现需要AssetsToken,进而去授权服务器上要求授权服务器给自己颁发AssetsToken,授权服务器去征求该用户的同意,如果用户同意,则进行颁发AssetsToken,客户端就可以通过AssetsToken去资源服务器上访问该用户的资源了

image-20241105172912163

角色

OAuth 2协议包含以下角色:

  1. 资源所有者(Resource Owner):即用户,资源的拥有人,想要通过客户应用访问资源服务器上的资源。
  2. 客户应用(Client):通常是一个Web或者无线应用,它需要访问用户的受保护资源。
  3. 资源服务器(Resource Server):存储受保护资源的服务器或定义了可以访问到资源的API,接收并验证客户端的访问令牌,以决定是否授权访问资源。
  4. 授权服务器(Authorization Server):负责验证资源所有者的身份并向客户端颁发访问令牌。

代理授权:资源所有者本身就拥有访问资源服务器上自己的资源信息,但是现在想要通过客户应用来代理用户去访问资源服务器上对应用户的信息

image-20241105173726776

使用场景

开放系统间授权

社交登录

在传统的身份验证中,用户需要提供用户名和密码,还有很多网站登录时,允许使用第三方网站的身份,这称为"第三方登录"。所谓第三方登录,实质就是 OAuth 授权。用户想要登录 A 网站,A 网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要 OAuth 授权。

image-20231222131233025

开放API

例如云冲印服务的实现

image-20231222131118611

现代微服务安全

单块应用安全

image-20231222152734546

微服务安全

image-20231222152557861

企业内部应用认证授权

  • SSO:Single Sign On 单点登录

  • IAM:Identity and Access Management 身份识别与访问管理

OAuth2四种授权方式

RFC6749:

RFC 6749 - The OAuth 2.0 Authorization Framework (ietf.org)

阮一峰:

OAuth 2.0 的四种方式 - 阮一峰的网络日志 (ruanyifeng.com)

四种模式:

  • 授权码(authorization-code)
  • 隐藏式(implicit)
  • 密码式(password)
  • 客户端凭证(client credentials)

第一种方式:授权码

授权码(authorization code),指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用,最复杂,也是最安全的,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏

image-20231220180422742

  • 注册客户应用:客户应用如果想要访问资源服务器需要有凭证,需要在授权服务器上注册客户应用。注册后会获取到一个ClientID和ClientSecrets

例如下图,ResourceOwner资源拥有者,想通过前台页面UserAgent去访问Client服务端内容,类似我们在登陆Gitee的时候想要访问我们自己的仓库需要先登录,我们通过Github身份认证来登录Gitee,

首先需要Gitee携带ClientIdentifier和Redirection跳转到Github的授权页面,如果没有登录则需要先登录。

UserAuthenticates是GIthub用户认证的操作例如输入密码之类的操作

Github进行用户认证之后返回Authorization(授权码)给Gitee,Gitee后台获取到Authorization之后,通过携带Authorization和RedirectUrl去访问Github服务端去请求AssetsToken,Github进行Authorization校验之后返回AssetsToken给Gitee,Gitee认证登陆成功,这时候Gitee就可以访问Github的一些受保护资源,例如用户的昵称基本信息之类的东西

image-20231222203153125

第二种方式:隐藏式

隐藏式(implicit),也叫简化模式,有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。

RFC 6749 规定了这种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为隐藏式。这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

image-20231220185958063

资源拥有者通过前台页面方位授权服务器,授权服务器直接返回携带AssetsToken的URL网址,但是返回的URL地址Client无法直接解析,前台页面通过访问一个专门的解析网址发服务资源服务端,解析出AssetsToken之后,前台页面通过AssetsToken访问Client端

image-20231222203218334

1
2
https://a.com/callback#token=ACCESS_TOKEN
将访问令牌包含在URL锚点中的好处:锚点在HTTP请求中不会发送到服务器,减少了泄漏令牌的风险。

第三种方式:密码式

密码式(Resource Owner Password Credentials):如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌。

就是像,如果我想通过GITHUB登录GITEE,我不能直接在GITEE这里输入我GITHUB的账号密码,这样就会将资源服务器的账号密码暴漏给客户应用非常不安全。客户端应用可能会通过此账号密码来对资源进行删除操作,风险极大

这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

image-20231220190152888

image-20231222203240921

第四种方式:凭证式

凭证式(client credentials):也叫客户端模式,适用于没有前端的命令行应用,即在命令行下请求令牌。

这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

image-20231220185958063

image-20231222203259785

授权类型的选择

image-20231223020052999

Spring实现OAuth2

Spring实现

OAuth2 :: Spring Security

Spring Security可以实现的

  • 客户应用(OAuth2 Client):OAuth2客户端功能中包含OAuth2 Login
  • 资源服务器(OAuth2 Resource Server)

需要独立实现的

  • 授权服务器(Spring Authorization Server):它是在Spring Security之上的一个单独的项目。

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 资源服务器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- 客户应用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<!-- 授权服务器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>

授权登录的实现思路

使用OAuth2 Login,只需要实现客户应用,对应的资源服务器和授权服务器都由别人设计好

image-20231223164128030

Github登录案例

创建应用

注册客户应用:

登录GitHub,在开发者设置中找到OAuth Apps,创建一个application,为客户应用创建访问GitHub的凭据:

image-20230510154255157

填写应用信息:默认的重定向URI模板为{baseUrl}/login/oauth2/code/{registrationId}。registrationId是ClientRegistration的唯一标识符。

image-20231221000906168

获取应用程序id,生成应用程序密钥:

image-20230510163101376

创建测试项目

创建一个springboot项目oauth2-login-demo,创建时引入如下依赖

image-20230510165314829

示例代码参考:spring-security-samples/servlet/spring-boot/java/oauth2/login at 6.2.x · spring-projects/spring-security-samples (github.com)

配置OAuth客户端属性

application.yml:

1
2
3
4
5
6
7
8
9
spring:
security:
oauth2:
client:
registration:
github:
client-id: 7807cc3bb1534abce9f2
client-secret: 008dc141879134433f4db7f62b693c4a5361771b
# redirectUri: http://localhost:8200/login/oauth2/code/github

创建Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.atguigu.oauthdemo.controller;

@Controller
public class IndexController {

@GetMapping("/")
public String index(
Model model,
@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
@AuthenticationPrincipal OAuth2User oauth2User) {
model.addAttribute("userName", oauth2User.getName());
model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());
model.addAttribute("userAttributes", oauth2User.getAttributes());
return "index";
}
}

创建html页面

resources/templates/index.html

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
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<title>Spring Security - OAuth 2.0 Login</title>
<meta charset="utf-8" />
</head>
<body>
<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
<div style="float:left">
<span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
</div>
<div style="float:none">&nbsp;</div>
<div style="float:right">
<form action="#" th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
</form>
</div>
</div>
<h1>OAuth 2.0 Login with Spring Security</h1>
<div>
You are successfully logged in <span style="font-weight:bold" th:text="${userName}"></span>
via the OAuth 2.0 Client <span style="font-weight:bold" th:text="${clientName}"></span>
</div>
<div>&nbsp;</div>
<div>
<span style="font-weight:bold">User Attributes:</span>
<ul>
<li th:each="userAttribute : ${userAttributes}">
<span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
</li>
</ul>
</div>
</body>
</html>

启动应用程序

  • 启动程序并访问localhost:8080。浏览器将被重定向到默认的自动生成的登录页面,该页面显示了一个用于GitHub登录的链接。
  • 点击GitHub链接,浏览器将被重定向到GitHub进行身份验证。
  • 使用GitHub账户凭据进行身份验证后,用户会看到授权页面,询问用户是否允许或拒绝客户应用访问GitHub上的用户数据。点击允许以授权OAuth客户端访问用户的基本个人资料信息。
  • 此时,OAuth客户端访问GitHub的获取用户信息的接口获取基本个人资料信息,并建立一个已认证的会话。

案例分析

登录流程

  1. A 网站让用户跳转到 GitHub,并携带参数ClientID 以及 Redirection URI。
  2. GitHub 要求用户登录,然后询问用户"A 网站要求获取用户信息的权限,你是否同意?"
  3. 用户同意,GitHub 就会重定向回 A 网站,同时发回一个授权码。
  4. A 网站使用授权码,向 GitHub 请求令牌。
  5. GitHub 返回令牌.
  6. A 网站使用令牌,向 GitHub 请求用户数据。
  7. GitHub返回用户数据
  8. A 网站使用 GitHub用户数据登录

image-20231223203225688

CommonOAuth2Provider

CommonOAuth2Provider是一个预定义的通用OAuth2Provider,为一些知名资源服务API提供商(如Google、GitHub、Facebook)预定义了一组默认的属性。

例如,授权URI、令牌URI和用户信息URI通常不经常变化。因此,提供默认值以减少所需的配置。

因此,当我们配置GitHub客户端时,只需要提供client-id和client-secret属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GITHUB {
public ClientRegistration.Builder getBuilder(String registrationId) {
ClientRegistration.Builder builder = this.getBuilder(
registrationId,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC,

//授权回调地址(GitHub向客户应用发送回调请求,并携带授权码)
"{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope(new String[]{"read:user"});
//授权页面
builder.authorizationUri("https://github.com/login/oauth/authorize");
//客户应用使用授权码,向 GitHub 请求令牌
builder.tokenUri("https://github.com/login/oauth/access_token");
//客户应用使用令牌向GitHub请求用户数据
builder.userInfoUri("https://api.github.com/user");
//username属性显示GitHub中获取的哪个属性的信息
builder.userNameAttributeName("id");
//登录页面超链接的文本
builder.clientName("GitHub");
return builder;
}
},