Spring Security-安全管理框架

Spring Security-安全管理框架 - 知乎 (zhihu.com)

Spring Security-安全管理框架

Spring Security-安全管理框架

主要内容

  1. Spring Security 简介
  2. 第一个Spring Security项目
  3. UserDetailsService详解
  4. PasswordEncoder密码解析器详解
  5. 自定义登录逻辑
  6. 自定义登录逻辑(数据库访问方式)
  7. 自定义登录页面
  8. 认证过程其他常用配置
  9. 完整认证流程

【尚学堂】SpringSecurity安全管理框架_spring security框架从入门到实战_Spring Security源码解析_哔哩哔哩 (゜-゜)つロ 干杯~-bilibiliwww.bilibili.com/video/BV1R54y1a7Cvimg

一、Spring Security简介

1、概括

Spring Security是一个高度自定义的安全框架。利用Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。

使用Spring Secruity的原因有很多,但大部分都是发现了javaEE的Servlet规范或EJB规范中的安全功能缺乏典型企业应用场景。同时认识到他们在WAR或EAR级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用Spring Security 解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。

正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制)。这两点也是Spring Security重要核心功能。“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。

2、历史

Spring Security 以“The Acegi Secutity System for Spring” 的名字始于2003年年底。其前身为acegi项目。起因是Spring开发者邮件列表中一个问题,有人提问是否考虑提供一个基于Spring的安全实现。限制于时间问题,开发出了一个简单的安全实现,但是并没有深入研究。几周后,Spring社区中其他成员同样询问了安全问题,代码提供给了这些人。2004年1月份已经有20人左右使用这个项目。随着更多人的加入,在2004年3月左右在sourceforge中建立了一个项目。在最开始并没有认证模块,所有的认证功能都是依赖容器完成的,而acegi则注重授权。但是随着更多人的使用,基于容器的认证就显现出了不足。acegi中也加入了认证功能。大约1年后acegi成为Spring子项目。

在2006年5月发布了acegi 1.0.0版本。2007年底acegi更名为Spring Security。

二、第一个Spring Security项目

1、导入依赖

Spring Security已经被Spring boot进行集成,使用时直接引入启动器即可。

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

2、访问页面

导入spring-boot-starter-security启动器后,Spring Security已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。

在项目中新建login.html页面后

在浏览器输入:http://localhost:8080/login.html后会显示下面页面

img

默认的username为user,password打印在控制台中。当然了,同学们显示的肯定和我的不一样。

img

在浏览器中输入账号和密码后会显示login.html页面内容。

3、应用场景

3.1 对已有项目添加认证功能

在很多技术中都可能有web访问控制页面。例如:solr就有web管理页面。不需要进行登录,只要知道ip和端口任何人都可以进行访问的。可能导致solr中数据不安全问题。为了保证数据安全性,可以想办法添加Spring Security。(实际上无法添加的,非maven项目)后面还会学很多其他技术,也可以按照这个思想进行操作。

3.2 对常规项目

需要有权限控制的项目都可以使用Spring Security。

4、可以自定义用户名和密码

通过修改application.properties(application.yml)

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

三、UserDetailsService详解

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

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

img

1、返回值

返回值UserDetails是一个接口,定义如下

img

要想返回UserDetails的实例就只能返回接口的实现类。Spring Security中提供了如下的实例。对于我们只需要使用里面的User类即可。注意User的全限定路径是:

org.springframework.security.core.userdetails.User

此处经常和系统中自己开发的User类弄混。

img

在User类中提供了很多方法和属性。

img

其中构造方法有两个,调用其中任何一个都可以实例化UserDetails实现类User类的实例。而三个参数的构造方法实际上也是调用7个参数的构造方法。

  • username:用户名
  • password:密码
  • authorities:用户具有的权限。此处不允许为null

img

此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security会根据User中的password和客户端传递过来的password进行比较。如果相同则表示认证通过,如果不相同表示认证失败。

authorities里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现403。通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”)来创建authorities集合对象的。参数时一个字符串,多个权限使用逗号分隔。

2、方法参数

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

3、异常

UsernameNotFoundException 用户名没有发现异常。在loadUserByUsername中是需要通过自己的逻辑从数据库中取值的。如果通过用户名没有查询到对应的数据,应该抛出UsernameNotFoundException,系统就知道用户名没有查询到。

四、PasswordEncoder密码解析器详解

Spring Security要求容器中必须有PasswordEncoder实例(客户端密码和数据库密码是否匹配是由Spring Security 去完成的,Security中还没有默认密码解析器)。所以当自定义登录逻辑时要求必须给容器注入PaswordEncoder的bean对象

1、接口介绍

encode():把参数按照特定的解析规则进行解析。

matches()验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回true;如果不匹配,则返回false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。

upgradeEncoding():如果解析的密码能够再次进行解析且达到更安全的结果则返回true,否则返回false。默认返回false。

img

2、内置解析器介绍

在Spring Security中内置了很多解析器。

img

3、BCryptPasswordEncoder简介

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

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

4、代码演示

在项目src/test/java下新建com.bjsxt.MyTest测试BCryptPasswordEncoder用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
@RunWith(SpringRunner.class)
public class MyTest {
@Test
public void test(){
//创建解析器
PasswordEncoder encoder = new BCryptPasswordEncoder();

//对密码进行加密
String password = encoder.encode("123");
System.out.println("------------"+password);

//判断原字符加密后和内容是否匹配
boolean result = encoder.matches("123",password);
System.out.println("============="+result);
}
}

五、自定义登录逻辑

当进行自定义登录逻辑时需要用到之前讲解的UserDetailsService和PasswordEncoder。但是Spring Security要求:当进行自定义登录逻辑时容器内必须有PasswordEncoder实例。所以不能直接new对象。

1、编写配置类

新建类com.bjsxt.config.SecurityConfig 编写下面内容

1
2
3
4
5
6
7
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPwdEncoder(){
return new BCryptPasswordEncoder();
}
}

2、自定义逻辑

在Spring Security中实现UserDetailService就表示为用户详情服务。在这个类中编写用户认证逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder encoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1. 查询数据库判断用户名是否存在,如果不存在抛出UsernameNotFoundException

if(!username.equals("admin")){
throw new UsernameNotFoundException("用户名不存在");
}
//把查询出来的密码进行解析,或直接把password放到构造方法中。
//理解:password就是数据库中查询出来的密码,查询出来的内容不是123
String password = encoder.encode("123");

return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}

3、查看效果

重启项目后,在浏览器中输入账号:admin,密码:123。后可以正确进入到login.html页面。

六、自定义登录逻辑(数据库访问方式)

1、新建数据库表结构

根据RBAC设计思想完成数据库原型设计。

因为Spring Security中UserDetails的实现类是User,所以我们尽量不要叫做User

img

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
create table users(
id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
);
-- 密码zs
insert into users values(1,'张三','$2a$10$dwi9Xv9cFDC1r8zQDp9wzupxoULvlzjtAMoes1zExZuDdLqtxT.rG');
-- 密码ls
insert into users values(2,'李四','$2a$10$Tomc5i8yHA.dUROgqX0eVO.Aa9qOAnvbNkUJhZ1znemqhRWdGGSle');

create table role(
id bigint primary key auto_increment,
name varchar(20)
);

insert into role values(1,'管理员');
insert into role values(2,'普通用户');

create table role_user(
uid bigint,
rid bigint
);

insert into role_user values(1,1);
insert into role_user values(2,2);


create table menu(
id bigint primary key auto_increment,
name varchar(20),
url varchar(100),
parentid bigint,
permission varchar(20)

);

insert into menu values(1,'系统管理','',0,'menu:sys');
insert into menu values(2,'用户管理','',0,'menu:user');


create table role_menu(
mid bigint,
rid bigint
);

insert into role_menu values(1,1);
insert into role_menu values(2,1);
insert into role_menu values(2,2);

2、在项目中添加依赖

添加MyBatis相关依赖

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
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>

3、编写配置文件

新建application.yml

在配置文件中添加Mybatis配置

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

4、新建实体类

新建com.bjsxt.pojo.Users

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

5、新建配置类

新建com.bjsxt.config.SecurityConfig

1
2
3
4
5
6
7
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

6、新建Mapper

新建com.bjsxt.mapper.UsersMapper

1
2
3
4
5
@Mapper
public interface UsersMapper {
@Select("select * from users where username=#{username}")
Users selectByUsername(String username);
}

7、修改自定义service逻辑

修改com.bjsxt.service.UsersServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class UsersServiceImpl implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = usersMapper.selectByUsername(username);
if(users==null){
throw new UsernameNotFoundException("用户名不存在");
}
return new User(username,users.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}

8、新建启动器

新建com.bjsxt.SecurityApplication

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

七、自定义登录页面

虽然Spring Security给我们提供了登录页面,但是对于实际项目中,大多喜欢使用自己的登录页面。所以Spring Security中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简单,只需要修改配置类即可。

说明:在上面代码基础上进行修改

1、修改pom

添加thymeleaf的依赖

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

2、编写登录页面

在resources下新建templates文件夹。其中fail.html和success.html都只有一句话,分别是:“登录失败”和“登录成功”

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>内容</title>
</head>
<body>
<form action="/login" method="post">
<input type="text" name="username"/>
<input type="password" name="password"/>
<input type="submit" value="提交"/>
</form>

</body>
</html>

3、修改配置类

修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapter,并重写configure方法。

  • successForwardUrl():登录成功后跳转地址
  • loginPage():登录页面
  • loginProcessingUrl:登录页面表单提交地址,此地址可以不真实存在。
  • antMatchers():匹配内容
  • permitAll():允许

**注意:**configure方法中除了FailureForwardUrl()以外其他配置都是必须写的配置

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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置认证
http.formLogin()
// 哪个URL为登录页面
.loginPage("/")
// 当发现什么URL时执行登录逻辑
.loginProcessingUrl("/login")
// 成功后跳转到哪里
.successForwardUrl("/success")
// 失败后跳转到哪里
.failureForwardUrl("/fail");

// 设置URL的授权问题
// 多个条件取交集
http.authorizeRequests()
// 匹配 / 控制器 permitAll() 不需要被认证就可以访问
.antMatchers("/").permitAll()
// anyRequest() 所有请求 authenticated() 必须被认证
.anyRequest().authenticated();

// 关闭csrf
http.csrf().disable();
}

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

4、编写控制器

新建com.bjsxt.controller.UserController。

三个方法都是只有显示页面的功能。因为Thymeleaf页面必须通过控制器显示。如果示例代码是拿纯HTMl静态页面演示,是不需要写这些控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class UserController {
@RequestMapping("/")
public String showLogin(){
return "login";
}
@RequestMapping("/success")
public String success(){
return "success";
}
@RequestMapping("/fail")
public String fail(){
return "fail";
}
}

1 测试效果

在浏览器输入:http://localhost:8080显示登录页面。输入:账号:张三,密码:zs后会显示“登录成功”

img

八、认证过程其他常用配置

1、设置请求账户和密码的参数名

1.1 源码简介

当进行登录时会执行UsernamePasswordAuthenticationFilter过滤器。

  • usernamePasrameter:账户参数名
  • passwordParameter:密码参数名
  • postOnly=true:默认情况下只允许POST请求。

img

1.2 修改配置

1
2
3
4
5
6
7
8
// 表单认证
http.formLogin()
.loginProcessingUrl("/login") //当发现/login时认为是登录,需要执行UserDetailsServiceImpl
.successForwardUrl("/toMain") //此处是post请求
.failureForwardUrl("/fail") //登录失败跳转地址
.loginPage("/login.html")
.usernameParameter("myusername")
.passwordParameter("mypassword");

1.3 修改页面

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

2、登录成功三种配置方式

2.1 转发源码分析

使用successForwardUrl()时表示成功后转发请求到地址。内部是通过successHandler()方法进行控制成功后交给哪个类进行处理。

img

orwardAuthenticationSuccessHandler内部就是最简单的请求转发。由于是请求转发,当遇到需要跳转到站外或在前后端分离的项目中就无法使用了。

img

当需要控制登录成功后去做一些事情时,可以进行自定义认证成功控制器。

1.2 自定义成功逻辑

在配置类使用.successHandler自定义成功逻辑。

1
2
3
4
5
6
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.sendRedirect("http://www.bjsxt.com");
}
})

2.3 重定向方法支持

可以直接使用defaultSuccessUrl(); 可以进行重定向到特定页面.

1
.defaultSuccessUrl("/success123")

3、登录失败时三种配置方式

3.1 转发源码分析

failureForwardUrl()内部调用的是failureHandler()方法

img

ForwardAuthenticationFailureHandler中也是一个请求转发,并在request作用域中设置SPRING_SECURITY_LAST_EXCEPTION的key,内容为异常对象。

img

3.2 自定义登录失败逻辑

在配置类中使用.failureHandler设置

1
2
3
4
5
6
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.sendRedirect("http://www.smallming.com");
}
})

3.3 登录失败后重定向到指定地址

1
.failureUrl("http://www.smallming.com")

完整认证流程(包含自定义页面和自定义登录逻辑)

img

  1. 用户在浏览器中随意输入一个URL
  2. Spring Security 会判断当前是否已经被认证(登录)如果已经认证,正常访问URL。如果没有被认证跳转到loginPage()对应的URL中,显示登录页面。
  3. 用户输入用户名和密码点击登录按钮后,发送登录url
  4. 如果url和loginProcessingUrl()一样才执行登录流程。否则需要重新认证。
  5. 执行登录流程时首先被UsernamePasswordAuthenticationFilter进行过滤,取出用户名和密码,放入到容器中。根据usernameParameter和passwordParameter进行取用户名和密码,如果没有配置这两个方法,默认为请求参数名username和password
  6. 执行自定义登录逻辑UserDetailsService的实现类。判断用户名是否存在和数据库中,如果不存在,直接抛出UsernameNotFoundException。如果用户名存在,把从数据库中查询出来的密码通过org.springframework.security.core.userdetails.User传递给Spring Security。Spring Security根据容器中配置的Password encoder示例把客户端传递过来的密码和数据库传递过来的密码进行匹配。如果匹配成功表示认证成功。
  7. 如果登录成功,跳转到successForwardUrl(转发)/successHandler(自己控制跳转方式)/defaultSuccessUrl(重定向)配置的URL
  8. 如果登录失败,跳转到failureForwardUrl/failureHandler/failureUrl