spring-boot-starter-security:应用安全

spring-boot-starter-security:应用安全

pom.xml 中添加依赖 spring-boot-starter-security

该依赖介绍:一般是企业在页面开发过程中安全管理所用的,Spring Boot权限框架,对开发者更友好的分布式权限验证框架,极大的提高验证效率

添加依赖后重新启动springboot并访问地址,会出现一个登录页面

默认的用户名:user

idea 控制台可以看到动态生成的密码 Using generated security password:

但是通过修改 Spring Security,可以将该技术用于实现:更安全地判断用户登录等操作

改造Spring Security

1. 新建一个 service.impl.UserDetailsServiceImpl 类,继承自 securityUserDetailsService 接口
1
2
3
4
5
6
package com.lwd.backend.service.impl;

import org.springframework.security.core.userdetails.UserDetailsService;

public class UserDetailsServiceImpl implements UserDetailsService {
}

写完上述代码会爆红,因为继承接口后没有实现方法,需要用 Alt + Enter ,然后选择实现方法才能补全

service.impl.UserDetailsServiceImpl 类代码

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
package com.lwd.backend.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lwd.backend.mapper.UserMapper;
import com.lwd.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

// 这里写User要看清楚是引用哪里的User!
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
// 前端传回一个username。从数据库中查找是否存在一条记录的username等于前端传回的username
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new RuntimeException("用户不存在!");
}
return null;
}
}

从该段代码中,我发现每个实现类的开头,都需要加一个注解,例如这里我加了@Service用于表示service层,不加的话,使用@Autowired就会报错。Conroller注解与RestController注解、Component注解

也可以使用万能的@Component来泛指各种类

该段代码,可以将前端获取的username与后端数据库相对比,若数据库中无该username,则抛出”用户不存在”。

2. 新建一个 service.impl.utils.UserDetailsImpl 类,继承自 securityUserDetails 接口
1
2
3
4
5
6
package com.lwd.backend.service.impl.utils;

import org.springframework.security.core.userdetails.UserDetails;

public class UserDetailsImpl implements UserDetails {
}

写完上述代码会爆红,因为继承接口后没有实现方法,需要用 Alt + Enter ,然后选择实现方法才能补全

这个类可以定义用户的各种属性,在补全代码后,将以下几个属性设置为 true

isEnabledisCredentialsNonExpiredisAccountNonLockedisAccountNonExpired

在类中添加/更改以下代码

1
2
3
4
5
6
7
8
9
10
private User user; // 这个User是pojo的

@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
完成1,2两个类的初步编写后,我们需要将其连接。

也就是在 service.impl.UserDetailsServiceImpl 中,我们已经写了如果查询数据库找不到用户username时抛出异常”用户不存在”,但若用户存在的代码还没有编写完,所有更新一下 UserDetailsServiceImpl 类最后 return 返回的内容,而这个内容就是 UserDetailsImpl 类中实现的用户属性

1
2
3
4
5
if (user == null) {
throw new RuntimeException("用户不存在!");
}
return new UserDetailsImpl(user);
//将通过username在数据库中查到的用户信息,传入UserDetailsImpl中,从而跟赋予用户属性

写完这段会发现 UserDetailsImpl(user) 爆红,因为这个 user 是由 pojoUser 类定义的,所以须在 UserDetailsImpl 类的开头,加上和 User 类一样的三个注解

(小部分代码)

1
2
3
4
5
@Data
@NoArgsConstructor
@AllArgsConstructor
// 需添加上面三个注解
public class UserDetailsImpl implements UserDetails {

至此,便完成了 Spring Security 的一个改造:原先 Spring Security 是自己定义一个用户 user,和动态生成一串密码。但是现在,Spring Security 会根据用户输入的 usernamepassword 去我们的数据库中查找,若查找成功,则会通过 UserDetailsImpl 类去给这个用户赋予一些属性。

不过这个修改只是对后端进行修改,而用户输入 usernamepassword 时的前端界面,还是 Spring Security 自带的,当然我们也可以对前端进行修改,写一个我们自己的前端登录界面。

但是,此时我们再次运行SpringBoot,会发现还是无法登录,控制台报错:There is no PasswordEncoder mapped for the id "null"

我们可以看一下 Spring Security 的官方文档

1
2
3
4
5
6
7
8
9
10
The general format for a password is:

{id}encodedPassword
Such that id is an identifier used to look up which PasswordEncoder should be used and encodedPassword is the original encoded password for the selected PasswordEncoder. The id must be at the beginning of the password, start with { and end with }. If the id cannot be found, the id will be null. For example, the following might be a list of passwords encoded using different id. All of the original passwords are "password".

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

这个错误是说:现如今 Spring Security 中密码的存储格式是 {id}encodedPassword

前面的 id 是加密方式,id 可以是 bcryptsha256 等,后面跟着的是加密后的密码

也就是说: Spring Security 拿到密码时,会首先查找被 {} 包起来的 id,从而来确定后面的密码是被怎么样加密的,如果找不到就认为id是null

而我们目前还没有任何加密方式,所以可以选择官方文档中提供的 {noop}password 格式来对数据库中的用户的password直接手动修改。

实现密码加密

我们要在 controller.user.UserController 类中,添加一个简易版的注册用户功能,并在用户输入密码时,将密码通过 bcrypt 加密,并将加密后的密码存到数据库中。

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/user/add/{userId}/{username}/{password}")
public String addUser(
@PathVariable int userId,
@PathVariable String username,
@PathVariable String password) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodePassword = passwordEncoder.encode(password);
User user = new User(userId, username, encodePassword);
userMapper.insert(user);
return "用户注册成功";
}

用户直接在url中给出 userIdusernamepassword,之后,通过加密,将含有密码密文的用户信息新增到user表中,即为注册成功。

完成密码加密后,则需告诉 Spring Security 采用 bcrypt 进行密码解密

新建一个 config.SecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.lwd.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

完成上述代码后,就相当于告诉 Spring Security 了,采用 bcrypt 进行密码解密。

虽然上述问题已解决,但存在一个bug,那就是必须要先有一个账号,才能注册新的账号。之后会修好这个bug的


本博客所有文章除特别声明外,转载请注明出处!