Spring Security实现用户认证三:结合MySql数据库对用户进行认证
- 1 原理
- 2 基于内存的认证(默认方式)
- 2.1 依赖
- 2.2 WebSecurityConfig配置类添加配置
- 3 为下一步准备数据源
- 3.1 依赖
- 3.2 创建表users和authorities
- 3.3 配置DruidDataSource数据源
- 3.4 创建实体类User
- 4 基于JDBC的认证
- 4.1 使用系统自带的JdbcUserDetailsManager
- 依赖
- 对WebSecurityConfig添加配置UserDetailsService
- 定义UserController
- 使用swagger3测试
- 4.2 自定义MyJdbcUserDetailsManager
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: userpassword: 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 {//为存储在内存中的基于用户名/密码的认证提供支持。@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(User.withDefaultPasswordEncoder().username("root").password("root").roles("USER").build());return manager;}@Beanpublic 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-securitysecurity:user:name: userpassword: 1234datasource:driver-class-name: com.mysql.cj.jdbc.Drivertype: com.alibaba.druid.pool.DruidDataSourceusername: root # 修改成自己的数据库username和passwordpassword: root# 请把test_db修改成自己数据库名字url: jdbc:mysql://localhost:3306/test_db?characterEncoding=utf8&useSSL=false&serverTimeZone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueserver:port: 4555logging:level:web: debugmybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.song.cloud.entitiesconfiguration: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 {@Beanpublic 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 {@Resourceprivate UserMapper userMapper; // 自定义的dao层@Overridepublic UserDetails updatePassword(UserDetails user, String newPassword) {return null;}@Overridepublic void createUser(UserDetails userDetails) {User user = new User();user.setUsername(userDetails.getUsername());user.setPasswordHash(userDetails.getPassword());userMapper.insertSelective(user);}@Overridepublic void updateUser(UserDetails userDetails) {// 自己定义}@Overridepublic void deleteUser(String username) {// 自己定义}@Overridepublic void changePassword(String oldPassword, String newPassword) {// 自己定义}@Overridepublic boolean userExists(String username) {// 自己定义return false;}/*** 从数据库中获取用户信息,继续引入持久层,UserMapper** @param username the username identifying the user whose data is required.* @return* @throws UsernameNotFoundException*/@Overridepublic 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();}
}