SpringSecurity快速入门

1.简介

Spring Security 是Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity, Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。

2.快速上手

pom.xml 中的 Spring Security 依赖:

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

只要加入依赖,项目的所有接口都会被自动保护起来。

引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会在控制台输出

image-20220426164336477 image-20220426164244721

3.认证

3.1 登录校验过程

image-20220426164457347

3.2 SpringSecurity 认证流程

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。

image-20220426164827799

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要由它负责。

ExceptionTranslationFilter: 处理过器链中抛出的任何AccessDeniedException和AuthenticationException。

FilterSecuritylnterceptor:负责权限校验的过滤器。

我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。

image-20220426165455297 image-20220426165234537

认证流程图:

image-20220426170002862

image-20220426170119740

3.3 思路

登录
①自定义登录接口
调用ProviderManager的方法进行认证如果认证通过生成jwt
把用户信息存入redis中
②自定义UserDetailsService
在这个实现列中去查询数据库

校验:
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder

1、严格来说,使用jwt是不用redis的,因为jwt初衷就是为了无状态,你加上redis,就违背了初衷,这也是网上对jwt最为诟病的一个地方。
2、使用redis并不是为了减轻数据库的压力,因为jwt本身是可以在payload中放入一些用户基本信息的,在前端携带token访问时,对token进行解析,可以获取到这些数据并写入SecurityContextHolder当中,这样在后面的处理当中,都可以获取到当前用户的相关信息。
3、由于jwt的无状态,后端是没法感知和控制用户的在线状态的,所以加入了redis这一层,在校验token的同时检查redis相关状态从而实现后端对用户状态的控制。
4、所以对于无状态应用,jwt是再好不过的一个东西,但是对于我们常见的有状态应用,使用一个uuid也是能达到相同的效果的。

自定义UserDetailsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public UserDetails loadUserByUsername(String username) {
if (StringUtils.isBlank(username)) {
throw new ServeException("用户名不能为空!");
}
// 查询账号是否存在
UserAuth user = userAuthDao.selectOne(new LambdaQueryWrapper<UserAuth>()
.select(UserAuth::getId, UserAuth::getUserInfoId, UserAuth::getUsername, UserAuth::getPassword, UserAuth::getLoginType)
.eq(UserAuth::getUsername, username));
if (Objects.isNull(user)) {
throw new ServeException("用户名不存在!");
}
// 查询账号信息
UserInfo userInfo = userInfoDao.selectOne(new LambdaQueryWrapper<UserInfo>()
.select(UserInfo::getId, UserInfo::getEmail, UserInfo::getNickname, UserInfo::getAvatar, UserInfo::getIntro, UserInfo::getWebSite, UserInfo::getIsDisable)
.eq(UserInfo::getId, user.getUserInfoId()));
// 查询账号角色
List<String> roleList = roleDao.listRolesByUserInfoId(userInfo.getId());
// 查询账号点赞信息
Set<Integer> articleLikeSet = (Set<Integer>) redisTemplate.boundHashOps(ARTICLE_USER_LIKE).get(userInfo.getId().toString());
Set<Integer> commentLikeSet = (Set<Integer>) redisTemplate.boundHashOps(COMMENT_USER_LIKE).get(userInfo.getId().toString());
// 封装登录信息
return convertLoginUser(user, userInfo, roleList, articleLikeSet, commentLikeSet, request);
}

3.4密码加密存储

实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为:(idypassword。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。|
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder.
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类, SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

测试:

1
2
3
4
5
6
7
8
@ Test
public void TestBCryptPasswordEncoder () {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder ();
String encode = passwordEncoder. encode (rawPassword: "1234");
String encode2 = passwordEncoder. encode (rawPassword: "1234");
System. out. println (encode);
System. out. println (encode2);
}

结果:

1
2
$2a$10$bFOw3cFWQ93RDQo.hODJL.sgYMXRMRdQk5u2bCDf4xItNz/1Y jFn6
$2a$10$npv5JSeFR6/wLz8BBMmSBOMb8byg2eyfK4/vvoBk3RKtTLBhIhcpy

$2a$10$ + 盐(22位) + 密文