一、SpringSecurity 基本介绍

1.1、概要

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

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

1、用户认证

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

2、用户授权

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

1.2、对比

1、SpringSecurity

  • 和Spring无缝整合。

  • 全面的权限控制。

  • 专门为Web开发而设计。

    • 旧版本不能脱离Web环境使用。
    • 新版本对整个框架进行了分层抽取,分成了核心模块和Web模块。单独引入核心模块就可以脱离Web环境。
  • 重量级

2、Shiro

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

  • 轻量级。

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

  • 通用性。
    • 好处:不局限于Web环境,可以脱离Web环境使用。
    • 缺陷:在Web环境下一些特定的需求需要手动编写代码定制。

3、补充

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

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

自从有了Spring Boot之后,Spring Boot对于Spring Security提供了自动化配置方案,可以使用更少的配置来使用Spring Security。

4、技术组合

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

  • SSM + Shiro
  • Spring Boot/Spring Cloud + Spring Security

1.3、Hello World

1、步骤

  • 创建工程
  • 引入依赖
  • 编写Controller进行测试

2、依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>

3、控制器类

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

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

4、启动,测试

由于在配置文件中,我指定端口为8111,所以访问 localhost:8111/test/hello

image-20210406172435101

可以看到需要认证,Spring Security 默认的登录账号为user,且在控制台中生成一个随机的密码。

image-20210406172556116

使用账号密码登录

image-20210406172624476

二、SpringSecurity基本原理

SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链, 本质上是一个过滤器链,每个 Filter 都有其各自的功能,而且各个 Filter 之间还有关联关系,所以它们的组合顺序也是非常重要的。

重点看三个过滤器

2.1、三个重要的过滤器

  • FilterSecurityInterceptor

是一个方法级的权限过滤器,基本位于过滤链的最底部。这个类实现了 Filter 接口

  • ExceptionTranslationFilter

是个异常过滤器,用来处理在认证授权过程中抛出的异常

  • UsernamePasswordAuthenticationFilter

对/login的POST请求做拦截,校验表单中用户名,密码。

2.2、UserDetailsService接口讲解

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

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

从数据库中查询对象的逻辑应该写在此接口的实现类中

接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface UserDetailsService {
// ~ Methods
// ========================================================================================================

/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
*
* @return a fully populated user record (never <code>null</code>)
*
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

1、自定义用户认证逻辑的步骤

  • 创建一个类,继承 UsernamePasswordAuthenticationFilter,然后重写三个方法
  1. 重写 attemptAuthentication 方法
  2. 重写 successfulAuthentication 方法,如果认证成功,执行此方法
  3. 重写 unsuccessfulAuthentication 方法,如果认证失败,执行此方法
  • 创建类实现 UserDetailService 接口,编写查询数据库的过程,返回一个User对象,这个 User对象是SpringSecurity 框架提供的。

2、UserDetails接口

这个接口是系统默认的用户主体

1
2
3
4
5
6
7
8
9
10
11
12
13
//表示获取登录用户所有权限
Collection<?extendsGrantedAuthority> getAuthorities();//表示获取密码
String getPassword();
//表示获取用户名
String getUsername();
//表示判断账户是否过期
booleanisAccountNonExpired();
//表示判断账户是否被锁定
booleanisAccountNonLocked();
//表示凭证{密码}是否过期
booleanisCredentialsNonExpired();
//表示当前用户是否可用
booleanisEnabled();

可以看到,UserDetails有一个实现类–User,我们以后可以使用此实现类。

image-20210406195058994

3、方法参数 username

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

2.3、PasswordEncoder接口讲解

1、接口定义

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
public interface PasswordEncoder {

/**
* 表示把参数按照特定的解析规则进行解析
*/
String encode(CharSequence rawPassword);

/**
* 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。
* 如果密码匹配,则返回true;如果不匹配,则返回false。
* 第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
*/
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;
}
}

2、接口实现类

image-20210406195508905

BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认10.

3、BCryptPasswordEncoder常用方法演示

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test01() {
//1 创建密码解析器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//2 加密
String password = encoder.encode("wuhu");
//3 加密后的数据为
System.out.println("加密后的数据为:" + password);
//4 判断原字符与加密后的密文是否匹配
boolean result = encoder.matches("wuhu",password);
System.out.println("结果为:" + result);
}

查看结果

image-20210406200046582

三、SpringSecurityWeb权限方案

3.1、设置登录用户名和密码

下面演示一下设置登录的用户名和密码的三种方式

1、通过配置文件配置

  • 修改Spring Boot配置文件,添加配置
1
2
3
4
5
spring:
security:
user:
name: wuhu
password: qifei

使用我们自定义的账号密码进行登录

image-20210406201247268

结果如下

image-20210406201307837

2、通过配置类进行配置

编写一个配置类,这个类继承 WebSecurityConfigurerAdapter ,重写 configure 方法

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

@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}

在上面的配置类中,由于 BCryptPasswordEncoder 对象默认没有,所以我们需要手动new一个,并将其放入容器中。

启动项目,进行测试,这里我们需要手动注销配置文件中的配置

image-20210406202753396

查看结果

image-20210406202743779

3、自定义UserDetailsService实现类

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

  • 创建配置类,设置使用哪个 UserDetailsService 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;

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

@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
  • 编写实现类,返回User对象,User对象中有用户名、密码和操作权限

这里由于 @Autowired 默认根据类型注入,所以可以不指定名称。

1
2
3
4
5
6
7
8
@Service("userDetailsService")
public class MyUserDetailsServiceImpl implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 在这里查询数据库
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(username,new BCryptPasswordEncoder().encode("123"),auths);
}
}
  • 进行测试,用我们在类中返回的账号密码进行登录

image-20210406210413209

查看结果

image-20210406205817753

3.2、通过查询数据库完成用户认证

1、数据库脚本

密码:atguigu

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
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- 密码:qifei
-- ----------------------------
INSERT INTO `users` VALUES (1, 'wuhu', '$2a$10$nBH8a3Aenk4JtxC2tFwWXusmnzEytzUWME6CWyi/Vr50weaNM0Bxi');

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

2、引入依赖

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.19</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>

3、创建实体类

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

4、在我们自定义的 MyUserDetailsServiceImpl 中注入 UsersMapper

  • 这个 UsersMapper 类由MP生成
1
2
3
4
@Mapper
@Repository
public interface UsersMapper extends BaseMapper<Users> {
}

5、在 MyUserDetailsServiceImplloadUserByUsername 方法中查询数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service("userDetailsService")
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用UsersMapper中的方法,根据用户传入的username查询用户
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Users target = usersMapper.selectOne(wrapper);
if (target == null) {
//表示数据库中没有用户名为username的用户,认证失败
throw new UsernameNotFoundException("用户名不存在!");
}
System.out.println(target);
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//从查询数据库返回的user对象中得到用户名和密码,返回,由于在数据库中存放的是密文密码,所以直接返回即可。
return new User(target.getUsername(), target.getPassword(), auths);
}
}

6、测试

  • 在login页面中进行测试

image-20210406221305053

  • 点击Sign in,查看结果

image-20210406221352740

查看控制台,可以看到查询了数据库中的记录

image-20210406221422935

3.3、设置自定义登录页面

1、在配置类中重写 configure方法

这个方法和上面指定UserDetailsService实现类和密码编码器configure 不同,这里传入的参数是一个 HttpSecurity 对象

image-20210406223255933

2、对上面的configure方法进行重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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","/user/login").permitAll()
.anyRequest().authenticated()
// 关闭csrf防护。
.and().csrf().disable();
}

3、编写自定义登录页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!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="text" name="password" />
<br/>
<input type="submit" value="login">
</form>
</body>
</html>

注意:

表单中用户名和密码输入框的 name 属性必须为 username 和 password,这是由于SpringSecurity中设置了这两个参数的参数名

image-20210406232756139

4、测试配置类中配置的信息

  • 测试非保护资源:访问 /test/hello

此时无需认证,直接就可以访问

image-20210406233002771

  • 访问登录路径: /user/login

可以看到直接跳转到我们自定义的登录页面

image-20210406234242452

  • 认证成功

认证成功后自动跳转至 /test/index

image-20210406234310206

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

1、hasAuthority(基于权限)

如果当前主体拥有指定的权限,则返回true,否则返回false

  • 在配置类中设置访问路径和对应权限,表示只有拥有某某权限的用户才能访问该路径

image-20210407082817027

  • 在我们自定义的 MyUserDetailsServiceImpl 中,为返回的User对象设置权限

这里先演示没有权限的情况

image-20210407083015097

  • 在控制器中新增方法
1
2
3
4
@GetMapping("admin/index")
public String adminIndex() {
return "只有拥有管理员权限的用户才能访问该地址...adminIndex";
}
  • 演示

当前用户拥有的权限是role

image-20210407083401858

修改上面代码,将角色权限为admin,重新启动程序后访问

image-20210407083538049

2、hasAnyAuthority 方法(基于权限)

如果当前的用户有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true

假设系统中一个资源,admin可以访问,manager也可以访问。

  • 在配置类中配置

image-20210407084731060

上面hasAnyAuthority中的参数也可以写成:hasAnyAuthority("admin,manager")

  • 添加控制器方法
1
2
3
4
@GetMapping("find")
public String find() {
return "拥有admin权限或者manager权限都可以访问此资源...test/find!!!";
}
  • 演示

用户拥有admin角色的情况

image-20210407085211936

用户拥有manager的情况

image-20210407085614776

用户拥有role的情况

image-20210407085721297

3、hasRole(基于角色)

如果用户具备给定角色就允许访问,否则403

如果当前主体拥有指定的角色,则返回true

  • 底层源码
1
2
3
4
5
6
7
8
9
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 + "')";
}

如果我们传入的是 sale, 那么最终我们的角色是 ROLE_sale,角色名不允许以 ROLE_开始

  • 修改配置类,设置某一资源只有 ROLE_sale 才能访问

image-20210407091439729

  • 为用户赋予角色

在设置某资源的访问角色时可以必须不加 ROLE_,但在为用户设置角色时必须加上 ROLE_

1
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
  • 添加控制器方法
1
2
3
4
@GetMapping("order/find")
public String orderFind() {
return "order模块下的find资源,只有ROLE_sale角色才能访问...";
}
  • 测试

当用户角色为: ROLE_sale

image-20210407091708534

当用户角色不为:ROLE_sale

image-20210407091809373

访问

image-20210407091837376

4、hasAnyRole(基于角色)

表示用户具备任何一个条件,拥有要求的角色之一即可访问。

  • 修改配置类

image-20210407092413598

  • 为用户分配角色–ROLE_sale

  • 添加控制器方法

1
2
3
4
@GetMapping("order/update")
public String orderUpdate() {
return "order模块下的update资源,{ROLE_sale,ROLE_manager,ROLR_coder}角色才能访问...";
}
  • 测试

当用户拥有ROLE_sale角色时

image-20210407092638017

当用户只拥有 ROLE_admin 时,由于给用户分配的角色不在上述的角色集合中,所以访问出错403

image-20210407092844158

5、SpringSecurity中Authority和Role的区别

  1. hasRole 这里会自动给传入的字符串加上 ROLE_ 前缀,所以在数据库中的权限字符串需要加上 ROLE_ 前缀。
  2. hasAuthority 方法时,如果数据是从数据库中查询出来的,这里的权限和数据库中保存一致即可,可以不加 ROLE_ 前缀。即数据库中存储的用户角色如果是 admin,这里就是 admin。

如果我们设置一个资源只允许给 ROLE_admin 访问,那么我们需要在hasRole/hasAnyRole方法中传入角色去除ROLE_前缀的角色名。

3.5、自定义403页面

1、自定义403页面

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>
<h1>没有访问权限!!!</h1>
<h1>403</h1>
</body>
</html>

2、在配置类中配置403页面

和自定义登录页面一样,同样是在configure(HttpSecurity http) 方法中定义

1
2
3
4
5
6
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义403页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
...其余配置
}

3、测试

当没有访问权限时

image-20210407151513107

3.6、注解使用

1、@Secured

判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 ROLE_,功能类似于hasAnyRoles

在使用注解之前需要在主启动类或配置类上添加 @EnableGlobalMethodSecurity(securedEnabled=true) 注解开启注解功能。

1
2
3
4
5
6
7
@EnableGlobalMethodSecurity(securedEnabled = true)
@SpringBootApplication(scanBasePackages = "com.hzx")
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class);
}
}
  • 新增控制器方法
1
2
3
4
5
@GetMapping("order/delete")
@Secured({"ROLE_sale","ROLE_manager","ROLR_coder"})
public String orderDelete() {
return "order模块下的delete资源,{ROLE_sale,ROLE_manager,ROLR_coder}角色才能访问...";
}
  • 测试,当前用户角色为:ROLE_admin,所以没有权限访问

image-20210407153650066

  • 将用户的角色修改为 ROLE_sale,再次测试,查看结果

image-20210407153746983

再次测试

image-20210407153816697

2、@PreAuthorize

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

可以配合我们上面讲的hasRole/hasAnyRoles设置访问该资源所需要的角色

使用hasAuthority/hasAnyAuthority设置访问该资源所需要的权限

该注解使用前同样需要开启注解功能,在配置类或者启动类上添加 @EnableGlobalMethodSecurity(prePostEnabled =true) 注解

1
2
3
4
5
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled =true,securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
...
}
  • 新增控制器方法
1
2
3
4
5
@GetMapping("preAuthorize")
public String preAuthorize(){
System.out.println("preAuthorize");
return"preAuthorize";
}
  • 在方法上添加 @PreAuthorize 注解,该注解可以设置访问此资源需要的权限或角色
  1. 设置访问权限
1
2
3
4
5
6
@GetMapping("preAuthorize")
@PreAuthorize("hasAnyAuthority('menu:system','order:system')")
public String preAuthorize(){
System.out.println("preAuthorize");
return"preAuthorize";
}

此时用户没有访问权限,进行测试,访问 localhost:8111/test/preAuthorize

image-20210407160057194

为用户添加权限

1
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale,menu:system");

重启项目,访问资源,此时提示访问成功

image-20210407160246327

  1. 设置访问资源所需要的角色
1
2
3
4
5
6
@GetMapping("preAuthorize")
@PreAuthorize("hasAnyRole('ROLE_管理员,ROLE_sale')")
public String preAuthorize(){
System.out.println("preAuthorize");
return"preAuthorize";
}

用户现拥有的角色为:ROLE_sale,所以可以访问该资源

image-20210407160752790

重启项目,访问

image-20210407160820927

修改用户角色

1
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_coder");

重启项目,访问资源,此时无法访问

image-20210407161445681

  • 如果要求访问某一资源的用户需要同时具有两个角色,那么可以这样使用
1
@PreAuthorize("hasRole('normal') AND hasRole('admin')") 

这样,访问此资源的用户必须同时具有 ROLE_normal 角色和 ROLE_admin 角色。

3、@PostAuthorize

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

同样需要在配置类或者启动类上先开启注解功能:
@EnableGlobalMethodSecurity(prePostEnabled =true)

当没有权限的用户访问资源时,可以利用这个来留下他的访问记录,以此做出针对性的拉黑

  • 在控制器中添加方法
1
2
3
4
5
6
@GetMapping("postAuthorize")
@PostAuthorize("hasAnyRole('ROLE_管理员,ROLE_sale')")
public String postAuthorize(){
System.out.println("postAuthorize");
return"postAuthorize";
}
  • 设置用户角色为 ROLE_coer,此时用户没有权限访问
1
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_coder");
  • 重启项目,访问资源,查看结果

image-20210407164944049

虽然提示没有访问权限,但控制台上输出了 postAuthorize,证明方法被执行了

image-20210407165009375

4、@PostFilter

@PostFilter:权限验证之后对数据进行过滤

留下用户名是admin1的数据表达式中的filterObject引用的是方法返回值List中的某一个元素

1
2
3
4
5
6
7
8
9
10
@RequestMapping("getAll")
@PreAuthorize("hasRole('ROLE_管理员')")
@PostFilter("filterObject.username == 'admin1'")
@ResponseBody
public List<Users> getAllUser(){
ArrayList<Users> list =new ArrayList<>();
list.add(new Users(1L,"admin1","6666"));
list.add(new Users(2L,"admin2","888"));
return list;
}
  • 同时讲用户角色设置为 ROLE_管理员,测试查看结果

可以看到输出结果只有 admin1,证明过滤生效

image-20210407171107843

5、@PreFilter

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

  • 创建控制器方法
1
2
3
4
5
6
7
8
9
@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_管理员')")
@PreFilter(value ="filterObject.id%2==0")
public List<Users> getTestPreFilter(@RequestBody List<Users> users) {
users.forEach(user-> {
System.out.println(user.getId() + "\t" + user.getUsername());
});
return users;
}

3.7、基于数据库的”记住我”

1、实现思路

image-20210407183953672

使用数据库 + Cookie完成自动认证

  1. 认证成功后,向浏览器Cookie中存入一个Token,并向数据库中存入Token和用户信息字符串。
  2. 再次进行访问时,获取Cookie中的Token,拿着Cookie中的Token到数据库进行比对,如果能够查询到对应的用户信息,那么认证成功,可以登录。
  • 查看AbstractAuthenticationProcessingFiltersuccessfulAuthentication方法源码

在认证成功,调用successfulAuthentication方法时,会执行 rememberMeServices.loginSuccess(request, response, authResult); 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
  • 查看rememberMeServices.loginSuccess(request, response, authResult)

查看 AbstractRememberMeServices 类的loginSuccess方法

1
2
3
4
5
6
7
8
9
@Override
public final void loginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
  • 继续查看 PersistentTokenBasedRememberMeServicesonLoginSuccess 的实现

可以看到,此时先是调用 PersistentTokenRepositorycreateNewToken方法生成一个Token,然后再调用addCookie方法将Token放入Cookie中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();

logger.debug("Creating new persistent login for user " + username);

PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
tokenRepository.createNewToken(persistentToken);
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
  • PersistentTokenRepository有一个实现类JdbcTokenRepositoryImpl

在这个实现类中,定义了建表语句和对该表的增删改查语句,所以我们可以不用定义“记住我”功能用到的表

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
PersistentTokenRepository {
public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
+ "token varchar(64) not null, last_used timestamp not null)";
/** The default SQL used by the <tt>getTokenBySeries</tt> query */
public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
/** The default SQL used by <tt>createNewToken</tt> */
public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
/** The default SQL used by <tt>updateToken</tt> */
public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
/** The default SQL used by <tt>removeUserTokens</tt> */
public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
}
  • 查看RemeberMeServices中的autoLogin方法
  1. 从请求对象中拿出记住我Cookie,判断Cookie是否为空(长度为空或者为null),如果为空,直接返回null,如果长度为0,将Cookie置为空后返回null
  2. 如果不为空,那么对token进行解密,根据Token中的信息查询数据库,返回用户对象,然后对用户对象进行校验,如果信息比对无误,则自动登录。
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
@Override
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}
UserDetails user = null;
try {
String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
cancelCookie(request, response);
throw cte;
}
catch (UsernameNotFoundException noUser) {
logger.debug("Remember-me login was valid but corresponding user not found.",
noUser);
}
catch (InvalidCookieException invalidCookie) {
logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
}
catch (AccountStatusException statusInvalid) {
logger.debug("Invalid UserDetails: " + statusInvalid.getMessage());
}
catch (RememberMeAuthenticationException e) {
logger.debug(e.getMessage());
}
cancelCookie(request, response);
return null;
}

2、具体实现

  • 创建自动登录需要的数据库表(这一步可以跳过)
1
2
3
4
5
6
7
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;
  • 在配置类中注入数据源,并配置操作数据库对象,即注入PersistentTokenRepository对象(也可以配置JdbcTokenRepositoryImpl对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   /**
* 注入数据源
*/
@Autowired
private DataSource dataSource;
/**
* 注入操作数据库对象
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//是否在启动时创建表,我们手动创建了,所以置为false
jdbcTokenRepository.setCreateTableOnStartup(false);
return jdbcTokenRepository;
}
  • 在配置类的 configure(HttpSecurity http)方法中配置记住我相关信息,并将操作数据库对象放置进去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义403页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
// 自定义自己编写的登录页面
http.formLogin()
...
// 登录页面设置
.loginPage("/login.html")
.and()
//开启记住我功能
.rememberMe()
//设置操作数据库对象
.tokenRepository(persistentTokenRepository())
//设置token过期时间为60s
.tokenValiditySeconds(60)
// 关闭csrf防护。
...
}
  • 在登录页面中添加一个复选框,用于选择是否自动登录

注意,这里的复选框中的name必须为:remember-me,这是SpringSecurity指定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!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="text" name="password" />
<br/>
<input type="checkbox" name="remember-me"/>自动登录
<br/>
<input type="submit" value="login">
</form>
</body>
</html>

3、具体测试

  • 访问 localhost:8111/test/getAll ,点击自动登录,然后进行认证

image-20210407200353347

  • 可以看到数据库表中有一条数据

image-20210407200445636

  • Cookie中出现一条数据

image-20210407200530507

  • 关掉浏览器,重新打开换后再次输入地址,发现无需认证直接访问到了资源

image-20210407200823283

3.8、用户注销

1、在配置类中添加退出映射路径

1
2
// 设置退出路径和退出成功后跳转的地址
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/index").permitAll();

2、测试

  • 修改配置类,设置登录成功后跳转到成功页面

设置登录成功后跳转的路径为 success.html

image-20210407203840233

  • 在成功页面中添加超链接,并设置退出路径

登录成功页面 success.html 如下

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录成功页面</title>
</head>
<body>
<h1>恭喜你,登录成功!</h1>
<a href="/logout">退出</a>
</body>
</html>
  • 登录成功后,在成功页面点击退出,此时再去访问其他控制器方法,是不能进行访问的。

登录成功页面

image-20210407203931032

此时访问 /test/getAll,可以访问资源

image-20210407204003377

回到success.html,点击退出

image-20210407204035665

此时再次访问 /test/getAll,发现不能访问该资源,自动跳转到登录页

image-20210407204126649

3.9、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方法进行防护,如果是Get请求不保护。

1、开启CSRF保护

  • 在配置类中关闭此配置,开启csrf保护。
1
2
//添加以下配置用于关闭csrf防护。
//.and().csrf().disable();
  • 在登录页面中添加一个隐藏域
1
<input type="hidden"th:if="${_csrf}!=null"th:value="${_csrf.token}"name="_csrf"/>

2、SpringSecurity防止CSRF的原理

  • 生成csrfToken保存到HttpSession或者Cookie中。

image-20210407210642862

  • 请求到来时,从请求中提取csrfToken,和保存的csrfToken做比较,进而判断当前请求是否合法。主要通过CsrfFilter过滤器来完成。

四、SpringSecurity微服务权限方案

4.1、什么是微服务

1、由来

微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

2、优势

  • 微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比
    较好解决。
  • 微服务每个模块都可以使用不同的存储方式(比如有的用Redis,有的用MySQL
    等),数据库也是单个模块对应自己的数据库。
  • 微服务每个模块都可以使用不同的开发技术,开发模式更灵活。

3、本质

  • 微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
  • 微服务的目的是有效的拆分应用,实现敏捷开发和部署。

4.2、微服务认证与授权实现思路

1、认证授权过程分析

  • 如果是基于Session,那么Spring-security会对cookie里的sessionid进行解析,找到服务器存储的session信息,然后判断当前用户是否符合请求的要求。
  • 如果是token,则是解析出token,然后将当前请求加入到Spring-security管理的权限信息中去

image-20210407214130860

如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于token的形式进行授权与认证。

  1. 用户输入用户名密码进行认证,认证成功后获取一系列权限列表,然后以用户名为key,权限列表为value的形式存入 Redis 缓存中,并根据用户名相关信息生成Token返回。
  2. 浏览器将Token记录在Cookie中,每次调用 API 接口时都默认将Token放在 header 请求头中,并携带着Token进行访问。
  3. SpringSecurity通过解析header中的token,得到当前用户名后根据用户名就可以从 Redis 中获取权限列表,这样 SpringSecurity 就能够判断当前请求用户是否有权限访问。

2、主要实现功能

  • 登录(认证)
  • 添加角色
  • 为角色分配菜单
  • 添加用户
  • 为用户分配角色

4.3、权限管理数据模型

对于一个完整的权限管理模型,至少需要设计5张表

角色与权限(菜单)为多对多关系。

角色与用户为多对多关系。

image-20210407221126114

1、acl_permission权限表

省略表中数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `acl_permission` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '编号',
`pid` char(19) NOT NULL DEFAULT '' COMMENT '所属上级',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '名称',
`type` tinyint(3) NOT NULL DEFAULT '0' COMMENT '类型(1:菜单,2:按钮)',
`permission_value` varchar(50) DEFAULT NULL COMMENT '权限值',
`path` varchar(100) DEFAULT NULL COMMENT '访问路径',
`component` varchar(100) DEFAULT NULL COMMENT '组件路径',
`icon` varchar(50) DEFAULT NULL COMMENT '图标',
`status` tinyint(4) DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限';

2、acl_role角色表

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `acl_role` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '角色id',
`role_name` varchar(20) NOT NULL DEFAULT '' COMMENT '角色名称',
`role_code` varchar(20) DEFAULT NULL COMMENT '角色编码',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

3、acl_role_permission角色权限关联表

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE `acl_role_permission` (
`id` char(19) NOT NULL DEFAULT '',
`role_id` char(19) NOT NULL DEFAULT '',
`permission_id` char(19) NOT NULL DEFAULT '',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_role_id` (`role_id`),
KEY `idx_permission_id` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限';

4、acl_user用户表

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `acl_user` (
`id` char(19) NOT NULL COMMENT '会员id',
`username` varchar(20) NOT NULL DEFAULT '' COMMENT '微信openid',
`password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码',
`nick_name` varchar(50) DEFAULT NULL COMMENT '昵称',
`salt` varchar(255) DEFAULT NULL COMMENT '用户头像',
`token` varchar(100) DEFAULT NULL COMMENT '用户签名',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

5、acl_user_role用户角色关联表

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE `acl_user_role` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '主键id',
`role_id` char(19) NOT NULL DEFAULT '0' COMMENT '角色id',
`user_id` char(19) NOT NULL DEFAULT '0' COMMENT '用户id',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_role_id` (`role_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

6、创建数据库 acldb

执行脚本文件,最终生成的数据库表结构如下

image-20210407222315917

4.4、项目环境搭建

1、项目模块示意图

image-20210407222759625

项目结构示意图

image-20210407224348952

2、引入依赖

  • 父模块 pom.xml
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.0.5</mybatis-plus.version>
<velocity.version>2.0</velocity.version>
<swagger.version>2.7.0</swagger.version>
<jwt.version>0.7.0</jwt.version>
<fastjson.version>1.2.28</fastjson.version>
<gson.version>2.8.2</gson.version>
<json.version>20170516</json.version>
<cloud-alibaba.version>0.2.2.RELEASE</cloud-alibaba.version>
</properties>

<dependencyManagement>
<dependencies>
<!--Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-plus 持久层-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>

<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${json.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
  • service模块 pom.xml
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<dependencies>
<dependency>
<groupId>com.hzx</groupId>
<artifactId>service_base</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

<!--服务注册-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
</dependency>


<!--lombok用来简化实体类:需要安装lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>

<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
  • service_acl模块 pom.xml
1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>com.hzx</groupId>
<artifactId>spring_security</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
  • service_gateway模块 pom.xml
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
<dependencies>
<dependency>
<groupId>com.hzx</groupId>
<artifactId>service_base</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

<!--服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
  • common模块 pom.xml
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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided </scope>
</dependency>

<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<scope>provided </scope>
</dependency>

<!--lombok用来简化实体类:需要安装lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided </scope>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<scope>provided </scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<scope>provided </scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
</dependencies>
  • spring_security模块 pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.hzx</groupId>
<artifactId>service_base</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>

3、引入工具类

service_base 模块中引入工具类

image-20210407230110080

4.5、编写SpringSecurity认证授权工具类和处理器

image-20210407230607289

1、DefaultPasswordEncoder 密码处理工具类

这个类需要实现 PasswordEncoder 接口,然后重写两个方法

  1. encode加密方法
  2. matches匹配方法
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
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
public DefaultPasswordEncoder(int length) {}

/***
* 进行MD5加密
* String、StringBuffer和StringBuilder均实现了CharSequence接口
* @param rawPassword 要加密的密码
* @return 加密后的密码
*/
@Override
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}

/***
* 匹配加密后的密码和明文密码
* @param rawPassword 明文密码
* @param encodedPassword 加密后的密码
* @return 是否匹配?
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}

2、TokenManager token操作工具类

该工具类包含两个方法,即根据用户名生成token的方法和解析token得到用户名的方法

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
@Component
public class TokenManager {
/**
* token有效时长
*/
private static final Integer TOKEN_EXPIRATION = 24 * 60 * 60 * 1000;

/**
* 编码密钥
* MD5十六位小加密--123456
*/
public static final String TOKEN_SIGN_KEY = "49ba59abbe56e057";

/**
* 根据用户名生成Token
* @param username 用户名
* @return 生成的token
*/
public String createToken(String username) {
return Jwts.builder().setSubject(username)
// 设置token有效过期时间
.setExpiration(new Date(System.currentTimeMillis() + TOKEN_EXPIRATION))
// 设置签名
.signWith(SignatureAlgorithm.HS512, TOKEN_SIGN_KEY)
.compressWith(CompressionCodecs.GZIP).compact();
}

/**
* 根据Token字符串得到用户信息
* @param token 待解析token
* @return 用户信息(用户名)
*/
public String getUserInfoFromToken(String token) {
return Jwts.parser()
.setSigningKey(TOKEN_SIGN_KEY)
.parseClaimsJws(token)
.getBody().getSubject();
}
}

3、TokenLogoutHandler退出处理器

用户退出时,需要根据token得到用户名,然后根据得到的用户名到Redis中删除权限列表,然后删除Cookie信息。

这个处理器需要实现LogoutHandler接口

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
public class TokenLogoutHandler implements LogoutHandler {
/**
* 需要操作Token,所以引入TokenManager
*/
private TokenManager tokenManager;
/**
* 需要操作Redis,所以引入RedisTemplate
*/
private RedisTemplate redisTemplate;

/**
* 使用构造函数注入
*/
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

/**
* 登出方法
* @param request
* @param response
* @param authentication
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//1 从Header中获取Token
String token = request.getHeader("token");
//2 如果Token不为空,移除Token
if(StringUtils.isNotBlank(token)) {
//TODO 移除token

String username = tokenManager.getUserInfoFromToken(token);
//3 根据从Token中获取的用户名(在实际生产中可以要定义一下键规则),到Redis中删除权限列表
redisTemplate.delete(username);
}
ResponseUtil.out(response, R.ok());
}
}

4、UnauthorizedEntryPoint未授权统一处理

这个类需要实现 AuthenticationEntryPoint 接口,并重写 commence 方法

1
2
3
4
5
6
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}

4.6、编写SpringSecurity核心过滤器

1、编写 TokenAuthenticationFilter认证过滤器

这个认证过滤器需要继承 UsernamePasswordAuthenticationFilter ,重写 attemptAuthenticationsuccessfulAuthenticationunsuccessfulAuthentication 方法

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
59
60
61
62
63
64
65
66
public class TokenAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate<String, Object> redisTemplate;
private AuthenticationManager authenticationManager;

public TokenAuthenticationFilter(TokenManager tokenManager, RedisTemplate redisTemplate, AuthenticationManager authenticationManager) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.authenticationManager = authenticationManager;
this.setPostOnly(false);
// 设置登录路径(/admin/acl/login),和提交方式(POST)
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}

/**
* 获取表单提交的用户名和密码
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 获取表单提交数据
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword()));
} catch (IOException e) {
throw new RuntimeException();
}
}

/**
* 认证成功后调用的方法
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//1 认证成功后,得到认证成功后的用户信息
SecurityUser user = (SecurityUser) authResult.getPrincipal();
//2 根据用户名生成Token
String token = tokenManager.createToken(user.getUsername());
//3 以username为键,权限列表为值存入Redis中
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
//4 返回
ResponseUtil.out(response, R.ok().data("token",token));
}

/**
* 认证失败后调用的方法
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}

2、编写 TokenAuthorizationFilter授权过滤器

这个过滤器需要继承 BasicAuthenticationFilter

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
public class TokenAuthorizationFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate<String, Object> redisTemplate;


public TokenAuthorizationFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate<String, Object> redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
logger.info("=================" + req.getRequestURI());
if (!req.getRequestURI().contains("admin")) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = null;
try {
authentication = getAuthentication(req);
} catch (Exception e) {
ResponseUtil.out(res, R.error());
}
if (authentication != null) {

SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
ResponseUtil.out(res, R.error());
}
chain.doFilter(req, res);

}

private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//1 从请求Header中取出token
String token = request.getHeader("token");
if (StringUtils.isNotBlank(token)) {
//2 从token中获取用户名
String username = tokenManager.getUserInfoFromToken(token);
//3 从redis中获取权限列表
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(username);
//4 需要将String类型集合变为一个 GrantedAuthority 集合
List<GrantedAuthority> permissions = null;
if (CollectionUtils.isEmpty(permissionValueList)) {
permissions = permissionValueList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
return new UsernamePasswordAuthenticationToken(username, token, permissions);
}
//如果token不合法,直接return null
return null;
}
}

4.7、编写SpringSecurity核心配置类

SpringSecurity 的核心配置就是继承 WebSecurityConfigurerAdapter 并添加@EnableWebSecurity 注解

这个配置指明了用户名密码的处理方式、请求路径、登录登出控制等和安全相关的配置

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
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private TokenManager tokenManager;
private RedisTemplate<String, Object> redisTemplate;
private DefaultPasswordEncoder defaultPasswordEncoder;
private UserDetailsService userDetailsService;
@Autowired
public TokenWebSecurityConfig(TokenManager tokenManager, RedisTemplate<String, Object> redisTemplate, DefaultPasswordEncoder defaultPasswordEncoder, UserDetailsService userDetailsService) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.userDetailsService = userDetailsService;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthorizedEntryPoint())
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout")
.addLogoutHandler(new TokenLogoutHandler(tokenManager, redisTemplate)).and()
.addFilter(new TokenAuthenticationFilter(tokenManager, redisTemplate, authenticationManager()))
.addFilter(new TokenAuthorizationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
}

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

/**
* 配置哪些请求不拦截
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**", "/swagger-ui.html/**");
}
}

4.8、创建 UserDetailsServiceImpl

这个类需要实现 UserDetailsService ,实现 loadUserByUsername 方法。

由于需要查询用户信息和权限信息,所以注入权限Mapper和用户Mapper

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
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;

@Autowired
private PermissionService permissionService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1 根据用户名查询用户数据
User user = userService.selectByUsername(username);
//2 判断
if(user == null) {
//如果为空,那么认证失败
throw new UsernameNotFoundException("用户不存在!");
}
com.hzx.authority.safety.entity.User curUser = new com.hzx.authority.safety.entity.User();
BeanUtils.copyProperties(user,curUser);

//根据用户id查询用户权限列表
List<String> permissionList = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser();
securityUser.setPermissionValueList(permissionList);
//将当前用户 curUser 放入securityUser中
securityUser.setCurrentUserInfo(curUser);
return securityUser;
}
}

4.9、整合网关

1、配置解决跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**",config);
return new CorsWebFilter(source);
}
}

2、创建网关微服务配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
port: 8222
spring:
application:
name: service-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: service-acl
uri: lb://service-acl
predicates:
- Path=/*/acl/**

五、原理解读

5.1、请求流程

23-认证流程

5.2、认证流程

1、SpringSecurity过滤器介绍

SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。

现在对这条过滤器链的 15 个过滤器进行说明:

  • WebAsyncManagerIntegrationFilter

将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。

  • SecurityContextPersistenceFilter

在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信息就是这个过滤器处理的。

  • HeaderWriterFilter

用于将头信息加入响应中。

  • CsrfFilter

用于处理跨站请求伪造。

  • LogoutFilter

用于处理退出登录。

  • UsernamePasswordAuthenticationFilter

用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的 usernameParameterpasswordParameter 两个参数的值进行修改。

  • DefaultLoginPageGeneratingFilter

如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

  • BasicAuthenticationFilter

检测和处理 http basic 认证。

  • RequestCacheAwareFilter

用来处理请求的缓存。

  • SecurityContextHolderAwareRequestFilter

主要是包装请求对象 request。

  • AnonymousAuthenticationFilter

检测 SecurityContextHolder 中是否存在Authentication 对象,如果不存在为其提供一个匿名 Authentication。

  • SessionManagementFilter

管理 session 的过滤器

  • ExceptionTranslationFilter

处理 AccessDeniedExceptionAuthenticationException 异常。

  • FilterSecurityInterceptor

可以看做过滤器链的出口。

  • RememberMeAuthenticationFilter

当用户没有登录而直接访问资源时, 从 cookie里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie,用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

2、认证流程

UsernamePasswordAuthenticationFilter/login的POST请求做拦截,校验表单中的用户名和密码。

注意点:

  1. POST请求
  2. 表单中用户名和密码的name属性必须为 usernamepassword

image-20210408213210769

  • 第一步,过滤方法,判断提交方式是否为POST提交,如果是POST提交,拦截并进行认证,否则直接放行

该过滤器的 doFilter 方法实现在其抽象父类,即 AbstractAuthenticationProcessingFilter

image-20210408205301570

  • 第二步,调用子类的方法进行身份认证,认证成功后,将认证信息封装到 Authentication 对象中

image-20210408205752788

  • 第三步,Session策略处理

image-20210408210231458

  • 第四步,如果在程序运行期间发生了认证错误异常,此时执行 unsuccessfulAuthentication 方法,表示认证失败!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
// 执行认证失败方法
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
// 执行认证失败方法
unsuccessfulAuthentication(request, response, failed);
return;
}
  • 第五步认证成功后放行,进入过滤器链的下一个过滤器,并执行认证成功方法 successfulAuthentication
1
2
3
4
5
6
7
// Authentication success
// continueChainBeforeSuccessfulAuthentication 值默认为false,执行成功后会变为true
if (continueChainBeforeSuccessfulAuthentication) {
// 此时放行,进入下一个过滤器
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);

3、认证细节

  • 在上面的第二步中,会调用子类 UsernamePasswordAuthenticationFilter 的方法进行认证,查看源码
  1. 如果请求方式不是 POST,则抛出异常
  2. 获取请求对象携带的用户名和密码

image-20210408213240955

  1. 使用传入的用户名密码构造 Authentication 对象,并标记该对象未认证
  2. 将请求中一些属性信息放入 Authentication 对象中,如remoteAddresssessionId
  3. 调用 ProviderManager 对象的 authenticate 方法进行身份认证,此时会调用我们自己写的 UserDetailsService 查询数据库。
1
2
3
4
5
6
7
8
9
username = username.trim();
// 使用传入的用户名密码构造 Authentication 对象,并标记该对象未认证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 将请求中一些属性信息放入 Authentication 对象中,如remoteAddress、sessionId
// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
  • 查看 UsernamePasswordAuthenticationToken 对象的构建过程

UsernamePasswordAuthenticationToken 是Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认证的用户信息,一个用于封装认证成功后的用户信息:

用于封装未认证用户信息的构造器如下

1
2
3
4
5
6
7
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
// 设置未认证
setAuthenticated(false);
}

用于封装已认证用户信息的构造器如下

1
2
3
4
5
6
7
8
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
// 设置已认证
super.setAuthenticated(true); // must use super, as we override
}

Authentication 接口的实现类用于存储用户认证信息,查看该接口具体定义:

image-20210408215943160

5.3、权限访问流程

在权限访问流程中,主要用到 ExceptionTranslationFilterFilterSecurityInterceptor

1、ExceptionTranslationFilter

该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

image-20210408221049297

2、FilterSecurityInterceptor

FilterSecurityInterceptor 是过滤器链的最后一个过滤器,该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器ExceptionTranslationFilter 进行捕获和处理。

image-20210408221315457

需要注意,Spring Security 的过滤器链是配置在 Spring MVC 的核心组件DispatcherServlet 运行之前。也就是说,请求通过 Spring Security 的所有过滤器,不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。