Spring Security实现用户认证三:结合MySql数据库对用户进行认证
1 原理
在Spring Security实现用户认证一中说到,请求被过滤器UsernamePasswordAuthenticationFilter
处理生成UsernamePasswordAuthenticationToken
,实际上这里的token只是临时的,并没有进行认证,需要一个AuthenticationProvider
提供认证方式。
如下官方所提供的一张原图清楚说明了后续的认证过程。
从上图可以看出,UsernamePasswordAuthenticationToken
,ProviderManager
类对在Token
匹配AuthenticationProvider
。
对于采用用户名和密码的认证方式,匹配到的是DaoAuthenticationProvider
。进入到DaoAuthenticationProvider
,这个类需要UserDetailsService
和PasswordEncoder
。
UserDetailsService
里面存储着用户的细节UserDetails
,包括用户名、密文密码、权限等信息。PasswordEncoder
是用来对密码进行加密的,默认的加密算法是BCryptPasswordEncoder
。
在DaoAuthenticationProvider
拿到用户请求中的username
和明文password
,也就是包裹在UsernamePasswordAuthenticationToken
中的字段principal
和credentials
。如下图所示:
UserDetailsService
里面是密文密码,所以需要应用对应的加密算法将用户的明文密码映射成密文密码。
UserDetailsService
首先通过username
查找UserDetails
,将找到的UserDetails
取出密文密码,再对比两个密文密码的一致性去认证用户信息。
下图展示了认证成功后的UsernamePasswordAuthenticationToken
,认证成功的Token将会拿到用户的角色信息和授权信息。最终,返回的 UsernamePasswordAuthenticationToken 被设置在 SecurityContextHolder 上。
讲到这里我们大致知道怎么修改了。我们需要一个读取UserDetails
的一个UserDetailsService
,以及一个密码编码方法PasswordEncoder
。
2 基于内存的认证(默认方式)
其他配置请参考往期内容。
在不进行任何配置条件下,系统会默认生成一个username为user,密码随机的用户(在控制台打印)。
如果想要修改该默认用户的用户名和密码。可以在application.yml
文件中添加如下内容:
spring:
security:
user:
name: user
password: 1234
这是就可以用user和1234进行登录。
下面请看如何添加新的用户到内存中。
2.1 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 WebSecurityConfig配置类添加配置
下面配置了一个基于内存的UserDetailsService,并且向其中添加了一个用户(root,root),该用户角色定义为USER。重新启动服务器可以使用该用户登录。这时系统默认的用户将会失效。
这里拥有我们所需要的两个条件:UserDetailsService
和 PasswordEncoder
。withDefaultPasswordEncoder
会采用系统默认的密码编码器。
package com.song.cloud.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity //开启SpringSecurity自动配置(springboot中可以省略)
public class WebSecurityConfig {
//为存储在内存中的基于用户名/密码的认证提供支持。
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withDefaultPasswordEncoder().username("root").password("root").roles("USER").build());
return manager;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// ...
}
}
上面的配置变可以启动服务器测试,输入(root,root)可以进行登录。
3 为下一步准备数据源
MySql 8.0.35,druid, mybatis
3.1 依赖
<!-- 数据库依赖 -->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
3.2 创建表users和authorities
分别用于存放用户和权限。
create table users
(
username varchar(50) not null primary key,
password varchar(500) not null,
enabled boolean not null
);
create table authorities
(
username varchar(50) not null,
authority varchar(50) not null,
constraint fk_authorities_users foreign key (username) references users (username)
);
create unique index ix_auth_username on authorities (username, authority);
3.3 配置DruidDataSource数据源
application.yml配置
spring:
application:
name: spring-security
security:
user:
name: user
password: 1234
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
username: root # 修改成自己的数据库username和password
password: root
# 请把test_db修改成自己数据库名字
url: jdbc:mysql://localhost:3306/test_db?characterEncoding=utf8&useSSL=false&serverTimeZone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
server:
port: 4555
logging:
level:
web: debug
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.song.cloud.entities
configuration:
map-underscore-to-camel-case: true
下面请根据实际情况修改,会用mybatis-generator插件的请自行生成。不会请查阅其他教程。
3.4 创建实体类User
package com.song.cloud.entities;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* 表名:t_users_test
*/
@Table(name = "t_users_test")
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
/**
* id
*/
@Id
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码hash
*/
@Column(name = "password_hash")
private String passwordHash;
/**
* 是否启用
*/
private Boolean enable;
}
4 基于JDBC的认证
4.1 使用系统自带的JdbcUserDetailsManager
实现的JdbcUserDetailsManager
类已经封装了默认的增删改查的sql语句,但是事实上这样做非常麻烦,也不符合编程习惯,虽然官方提供了对语句的修改功能。
依赖
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
对WebSecurityConfig添加配置UserDetailsService
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import javax.sql.DataSource;
@Configuration
@EnableWebSecurity //开启SpringSecurity自动配置(springboot中可以省略)
public class WebSecurityConfig {
@Bean
public JdbcUserDetailsManager jdbcUserDetailsManager(DataSource dataSource){
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
// ----------------------- 向数据库添加用户和权限 ----------------
UserDetails user = User.withDefaultPasswordEncoder().username("user").password("user").roles("USER").build();
UserDetails admin = User.withDefaultPasswordEncoder().username("admin").password("admin").roles("ADMIN", "USER").build();
manager.createUser(user);
manager.createUser(admin);
// -----------------------结束向数据库添加用户和权限 ----------------
return manager;
}
// ...
}
现在,可以使用这两个用户进行登录。
在服务器启动时,这两个用户便被写进数据库中。下次启动需要注释向数据库添加用户和权限
的这些,或者将数据库中的users和authorities两个表数据删掉,否则会重复键值而报错, 先删authorities的数据。
定义UserController
@PostMapping("/user/add")
public UserDetails addUser(@RequestBody User user) {
System.out.println(user);
PasswordEncoder delegatingPasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String pd_encode = delegatingPasswordEncoder.encode(user.getPasswordHash());
UserDetails userDetails = org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(pd_encode)
.roles("USER") //自己定义
.disabled(!user.getEnable())
.build();
jdbcUserDetailsManager.createUser(userDetails);
return userDetails;
}
使用swagger3测试
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
测试地址:http://localhost:port/swagger-ui/index.html
改成项目端口号
进入测试地址前,需要使用之前添加进入db的账户登录。
测试数据:
{
"username": "test",
"passwordHash": "test",
"enable": true
}
测试登录:
4.2 自定义MyJdbcUserDetailsManager
怎么更加随心所欲的定制一下。
建议使用@Service
注解形式注册,由于需要使用dao层的服务,采用自动注入的形式必须使用@Service
标识为bean,交给spring IoC管理,才能实现自动注入dao层服务。
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.provisioning.UserDetailsManager;
import tk.mybatis.mapper.entity.Example;
@Service //建议使用@Service 注解形式注册
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Resource
private UserMapper userMapper; // 自定义的dao层
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}
@Override
public void createUser(UserDetails userDetails) {
User user = new User();
user.setUsername(userDetails.getUsername());
user.setPasswordHash(userDetails.getPassword());
userMapper.insertSelective(user);
}
@Override
public void updateUser(UserDetails userDetails) {
// 自己定义
}
@Override
public void deleteUser(String username) {
// 自己定义
}
@Override
public void changePassword(String oldPassword, String newPassword) {
// 自己定义
}
@Override
public boolean userExists(String username) {
// 自己定义
return false;
}
/**
* 从数据库中获取用户信息,继续引入持久层,UserMapper
*
* @param username the username identifying the user whose data is required.
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Example selectByUserId = new Example(User.class);
Example.Criteria criteria = selectByUserId.createCriteria();
criteria.andEqualTo("username", username);
User user = userMapper.selectOneByExample(selectByUserId);
if (user == null) {
throw new UsernameNotFoundException(username);
}
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(() -> "USER_LIST");
authorities.add(() -> "USER_ADD");
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPasswordHash())
.disabled(false)
.credentialsExpired(false)
.accountLocked(false)
.roles("ADMIN")
.build();
}
}