Skip to content

Spring Security - 3 身份验证之数据库

本文主要介绍如何将用户输入的账号密码与数据库中的账号密码进行校验登录,以及介绍了如何配置过滤器链、如何注册以及密码加密。 前文提要:

1. 配置过滤器链

我们可以通过配置类来调整Spring Security的过滤器链:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.csrf(csrfConfigurer -> csrfConfigurer.disable())
                .formLogin(Customizer.withDefaults())
                .authorizeHttpRequests(authorizeHttpRequestsConfigurer ->
                        authorizeHttpRequestsConfigurer.anyRequest().authenticated())
                .build();
    }
}

简单解释上面的代码:

  • csrf(csrfConfigurer -> csrfConfigurer.disable()):禁用CSRF保护,生产环境不建议,此处是为了方便postman测试。
  • formLogin(Customizer.withDefaults()):启用默认的表单登录,即使用UsernamePasswordAuthenticationFilter
  • authorizeHttpRequests(authorizeHttpRequestsConfigurer ->authorizeHttpRequestsConfigurer.anyRequest().authenticated()):对于任何请求,都需要登录认证。

上面的配置都是通过HttpSecurity类完成的,HttpSecurity 是 Spring Security 框架中一个核心类,它主要负责配置 HTTP 请求的安全策略。简单来说,它让你能够定义哪些 URL 需要认证、哪些 URL 可以匿名访问、如何处理登录和登出、以及其他与 HTTP 安全相关的设置。

2. 用户验证

本小节介绍如何使用数据库中的用户信息来进行登录校验,通过Spring Security - 2 身份验证(Authentication)中的流程分析,我们知道最终是通过UserDetailsService来获取可信的用户信息进行登录校验的,所以我们的核心思路就是实现自己的UserDetailsService,该类从数据库中中查询用户信息。

UserDetailsService是一个接口,其中只有一个方法:

java
public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

我们要做的事就是实现该接口,通过用户名查找数据库中的用户,并返回UserDetails实例。

java
@Service
public class DBUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      // 从数据库中查找用户并返回UserDetails
    }
}

所以我们需要提供数据访问层DAO和相关实体:

java
@Mapper
public interface UserMapper {
    User getUserByUsername(String username);
}
java
@Data
public class User {
    private int id;
    private String username;
    private String password;
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.example.springsecuritydemo.mapper.UserMapper">

    <select id="getUserByUsername" resultType="org.example.springsecuritydemo.model.User">
        select * from "user" where username = #{username}
    </select>

</mapper>

注意:由于示例比较简单,所以SQL语句就没有提供了。

然后将UserMapper注入到DBUserDetailsService

java
@Service
public class DBUserDetailsService implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.getUserByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException("User not found");
        }

        return new UserPricipal(user);
    }

}
java
public class UserPricipal implements UserDetails {

    private User user;

    public UserPricipal(User user){
        this.user = user;
    }

    // 获取权限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 测试使用
        return Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }
}

之后,我们要做的就是让Spring Security使用我们提供的UserDetailsService,通过配置类实现:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Resource
    private DBUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.csrf(csrfConfigurer -> csrfConfigurer.disable())
                .formLogin(Customizer.withDefaults())
                .authorizeHttpRequests(authorizeHttpRequestsConfigurer ->
                        authorizeHttpRequestsConfigurer
                                .requestMatchers("/register","/error").permitAll()
                                .anyRequest().authenticated())
                .build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(NoOpPasswordEncoder.getInstance());
        return daoAuthenticationProvider;
    }

}

启动项目,在postman中测试如下:

image-20250108191141197

出现该问题的原因是当我们登录成功后,Spring Security会默认寻找/路径,如果我们没有提供这个路径对应的接口,就会报错,实际情况表明我们已经登录成功了。增加Controller解决:

java
@RestController
public class MainController {

    @GetMapping("/")
    public String main(){
        return "home page";
    }
}

再次请求,结果如下:

image-20250108191407880

3. 用户注册

用户注册按照传统的MVC三层架构写:

java
@RestController
public class UserController {

    @Resource
    private DBUserDetailsService userDetailsService;

    @PostMapping("/register")
    public String register(@RequestBody User user){
        userDetailsService.register(user);
        return "success";
    }
}
java
@Service
public class DBUserDetailsService implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.getUserByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException("User not found");
        }

        return new UserPricipal(user);
    }

    public void register(User user){
        userMapper.insertUser(user);
    }
}
java
@Mapper
public interface UserMapper {

    User getUserByUsername(String username);

    void insertUser(User user);

}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.example.springsecuritydemo.mapper.UserMapper">
    <insert id="insertUser">
        insert into "user" (username,password) values (#{username},#{password})
    </insert>

    <select id="getUserByUsername" resultType="org.example.springsecuritydemo.model.User">
        select * from "user" where username = #{username}
    </select>

</mapper>

但此时我们访问/register接口会被要求登录,这显然是不符合常理的,所以我们需要在Security中配置,放行/register接口:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.csrf(csrfConfigurer -> csrfConfigurer.disable())
                .formLogin(Customizer.withDefaults())
                .authorizeHttpRequests(authorizeHttpRequestsConfigurer ->
                        authorizeHttpRequestsConfigurer
                                .requestMatchers("/register","/error").permitAll()
                                .anyRequest().authenticated())
                .build();
    }

}

启动项目,测试结果如下:

image-20250108191955009

4. 密码加密

在上面的用户注册过程中,我们存储的是明文密码,这是极不安全的,所以需要对明文密码进行加密存储。

Spring Security给我们提供了一些密码加密工具,其中最推荐使用BCryptPasswordEncoder

第一步要做的就是注册BCryptPasswordEncoder,我们可以在Security配置类中实现:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

}

然后,保存用户的时候加密密码:

java
@Service
public class DBUserDetailsService implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public void register(User user){
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        userMapper.insertUser(user);
    }
}

这样应该就可以了,但是会造成循环依赖,因为SecurityConfig依赖于DBUserDetailsService,而DBUserDetailsService依赖于PasswordEncoder(SecurityConfig中提供)。我们可以考虑将注册功能放在另一个Service中,或修改application.properties文件:

properties
spring.main.allow-circular-references=true

启动项目,再次注册用户:

image-20250108193115075

查询数据库,结果如下:

image-20250108193207867

可以看到密码已经加密存储在了数据库。

但是!如果我们使用lily账号登录时,会报错(返回登录界面):

image-20250108193332154

原因是我们验证的时候没有使用加密器,这就导致了数据库中的密码是加密的,用户提供的密码是明文密码,这肯定是不匹配的,所以导致登录失败!解决方案是验证时提供相同的密码加密器:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Resource
    private DBUserDetailsService userDetailsService;

    @Bean
    public AuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return daoAuthenticationProvider;
    }

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

}

这样就可以顺利登录了。