SpringBoot整合SpringSecurity——从零到一的超级详细教程

Springboot整合SpringSecurity

版本声明

  1. jdk版本:17
  2. gradle
  3. springboot版本:2.7.18(spring boot3 版本变化较大,后续会更新,这里如果是在使用springboot3+的版本请等一等)
  4. spring security版本:5.7.11

Spring Security的基本组件及认证流程分析

Spring Security通过一些列的过滤器完成了用户身份认证及其授权工作,每个过滤器都有不同分工,当然这些过滤器并不是全部都一起工作,而是根据我们需要什么功能,才会选取对应的过滤器加入。
当然这些过滤器并不是直接加入web的过滤器中,而是通过一个过滤器代理完成。web过滤器中只会加入一个或多个过滤器代理,然后由这些代理负责管理哪些Security Filter需要加入进来。
image

如果有多个过滤器链代理的话,那么就会变成这样:
image

组件介绍

Authentication(Principal)

封装用户身份信息,顶层接口,主要实现如下

  • AbstractAuthenticationToken()
    • RememberMeAuthenticationToken rememberMe 登陆后封装的身份信息
    • UsernamePasswordAuthenticationToken 用户名密码登录后封装的身份信息

AuthenticationManager

身份认证器的代理,主要负责多个认证器的代理,管理多个AuthenticationProvider,主要实现如下

  • ProviderManager (authenticate)

AuthenticationProvider

真正实现认证工作,多个provider受AuthenticationManager管理,主要实现如下

  • AbstractUserDetailsAuthenticationProvider
    • DaoAuthenticationProvider
    • RememberMeAuthenticationProvider

UserDetailsService

负责定义用户信息的来源,从不同来源加载用户信息,唯一的方法:loadUserByUsername,主要实现类:

  • UserDetailsManager
    • InMemoryUserDetailsManager
    • JdbcUserDetailsManager
  • 自定义

UserDetails

定义用户身份信息,比Authentication 信息更详细,主要实现

  • User
  • 一般我们自定义

SecurityContextHolder

存放和获取用户身份信息的帮助类

FilterChainProxy

Spring Securty Filter的入口,FilterChainProxy管理多个filter

AbstractHttpConfigurer

构建所有过滤器的核心组件,主要方法init()和configure(),主要实现类

  • FormLoginConfigurer
  • CorsConfigurer
  • CsrfConfigurer
  • HttpBasicConfigurer
  • LogoutConfigurer ...

一图理清基本组件关系

image

6.基于多种方式配置登录用户:memory、jdbc、MyBatis

前面章节我们所有的用户信息(用户名和密码)都是基于配置文件配置的,今天这节课我们开始定义不同的用户信息获取来源。

基于内存方式

其实我们在配置文件中写的用户信息,最终也是被读到内存中的,大家不知道对这段代码还熟悉么:
image
这一块就是基于内存方式构建了用户信息,定义了默认的用户信息来源。

配置内存方式

@Configuration
public class SecurityConfig {
    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(){
        UserDetails userDetails1 = User.withUsername("memory1").password("{noop}memory1").roles("memory1").build();
        UserDetails userDetails2 = User.withUsername("memory2").password("{noop}memory2").roles("memory2").build();
        return new InMemoryUserDetailsManager(userDetails1,userDetails2);
    }

}

验证登录

memory1
image

memory2
image

基于JDBC 方式

引入依赖

    implementation 'mysql:mysql-connector-java:8.0.32'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'

获取数据库执行脚本

在这个路径下:org/springframework/security/core/userdetails/jdbc/users.ddl
得到脚本后,将_ignorecase 去掉
image

执行之后,得到两张表
image

配置JDBC Manager

    @Autowired
    private DataSource dataSource;
    @Bean
    public JdbcUserDetailsManager jdbcUserDetailsManager(){
        JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);
        if(!jdbcUserDetailsManager.userExists("lglbc-jdbc")){
            jdbcUserDetailsManager.createUser(User.withUsername("lglbc-jdbc").username("lglbc-jdbc").password("{noop}lglbc-jdbc").roles("admin").build());
        }
        if(!jdbcUserDetailsManager.userExists("lglbc-jdbc2")){
            jdbcUserDetailsManager.createUser(User.withUsername("lglbc-jdbc2").username("lglbc-jdbc2").password("{noop}lglbc-jdbc2").roles("admin").build());
        }
        return jdbcUserDetailsManager;
    }

验证登录

image
image

基于MyBatis 方式

引入依赖

基于上面的依赖之后,需要再引入MyBatis的依赖

    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:+'

定义UserDetails

public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    private List<Role> roles = new ArrayList<>();
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    public void setAccountNonExpired(Boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(Boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

    public Integer getId() {
        return id;
    }

    public static class Role {
        private Integer id;
        private String name;
        private String nameZh;

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getNameZh() {
            return nameZh;
        }

        public void setNameZh(String nameZh) {
            this.nameZh = nameZh;
        }
        //省略getter/setter
    }
}

定义 UserDetailService

@Service
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUserName(username);
        if (Objects.isNull(user)) {
            throw new UsernameNotFoundException("user is null");
        }
        user.setRoles(userMapper.getRolesByUid(user.getId()));
        return user;
    }
}

创建mybatis mapper

@Mapper
public interface UserMapper {
    @Select("select r.* from user_role ur LEFT JOIN role r on ur.rid=r.id where ur.uid=#{id}")
    public List<User.Role> getRolesByUid(@Param("id") Integer id);

    @Select("select * from user where username=#{uname} limit 1")
    public User loadUserByUserName(String uname);
}

创建需要的表

CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`nameZh` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`accountNonExpired` tinyint(1) DEFAULT NULL,
`accountNonLocked` tinyint(1) DEFAULT NULL,
`credentialsNonExpired` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
KEY `rid` (`rid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


INSERT INTO `role` (`id`, `name`, `nameZh`)
VALUES
(1,'ROLE_dba','数据库管理员'),
(2,'ROLE_admin','系统管理员'),
(3,'ROLE_user','用户');
INSERT INTO `user` (`id`, `username`, `password`, `enabled`,
`accountNonExpired`, `accountNonLocked`, `credentialsNonExpired`)
VALUES
(1,'root','{noop}123',1,1,1,1),
(2,'admin','{noop}123',1,1,1,1),
(3,'sang','{noop}123',1,1,1,1);
INSERT INTO `user_role` (`id`, `uid`, `rid`)
VALUES
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);

验证登录

在启动之前,大家记住要把之前配置的jdbc和内存方式获取用户信息的配置给注销,否则会报如下错误:
image

至于为什么,请看原理分析

image

大家有可能注意到,我的密码里面都加上了{noop},为什么呢?因为加上这个代码就等于告诉security 我的密码没加密,因为security默认给密码使用加密,如果它看到{noop},就不会对密码进行加密匹配

原理分析: 为什么定义了两个UserDetailService,就抛如下异常

image

因为security在初始化http的时候,会初始化全局的认证处理器,如果发现有不等于1个UserDetails实现,则不会设置默认的AuthenticationProvider:
InitializeUserDetailsManagerConfigurer#configure
image

image

所以当我们定义两个UserDetailService(验证多种用户),那就需要自己定义一个AuthenticationManager,重写他获取provider和UserDetailService的逻辑,这个后面会给大家介绍:如何配置多个数据源,验证不同用户表

8.自定义认证器:实现验证码功能

SpringSecurity 默认是不支持验证码功能的,但是我们可以自己扩展,这也是我们使用SpringSecurity最大的好处,原生不支持,我们就自己扩展。

思路分析

因为系统默认的有一个DaoAuthenticationProvider 认证处理器,但是他只支持用户名和密码方式登录,所以是不能使用现有的认证器,那我们是不是可以实现一个自己的认证器,来覆盖这个默认的认证器呢?
答案当然是可以的,大概实现思路是这样的:

  • 创建一个认证器 继承默认的密码认证器DaoAuthenticationProvider
  • 定义验证码认证器的逻辑
    • 从session获取保存的验证码
    • 从请求参数中获取用户输入的验证码
    • 比对验证码
    • 如果匹配成功,则调用DaoAuthenticationProvider的authenticate方法,进行原先逻辑认证
    • 如果匹配失败,则抛出异常,不走后面的逻辑
  • 将自定义的provider加到AuthenticationManager中

大致思路是这样,那么还有没有别的方式呢?
当然有了,我们还可以通过过滤器实现,但是这个过滤器的优先级要先于认证过滤器之前,这个后面再和大家介绍,这节课我们先看下如何通过自定义认证器来实现验证码校验的功能。

代码实现

创建验证码处理器

引入依赖

    implementation 'com.github.penggle:kaptcha:2.3.2'

配置验证码

    @Bean
    public Producer producer() {
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "150");
        properties.setProperty("kaptcha.image.height", "50");
        properties.setProperty("kaptcha.textproducer.char.string", "012");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

创建验证码入口

    @Autowired(required = false)
    private Producer producer;
    @RequestMapping("/kaptcha")
    public void kaptcha(HttpServletResponse response, HttpSession session){
        response.setContentType("image/jpg");
        String text = producer.createText();
        session.setAttribute("KAPTCHA_CODE",text);
        BufferedImage image = producer.createImage(text);
        try(ServletOutputStream outputStream = response.getOutputStream()){
            ImageIO.write(image,"jpg",outputStream);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

自定义验证码认证处理器

public class KaptchaAuthenticationProvider extends DaoAuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String kaptchaCode = (String) request.getSession().getAttribute("KAPTCHA_CODE");
        String inputKaptcha = request.getParameter("kaptcha");
        if (!StrUtil.equals(kaptchaCode, inputKaptcha)) {
            throw new InternalAuthenticationServiceException("验证码验证失败");
        }
        return super.authenticate(authentication);
    }
}

自定义登录页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
<form action="/login" method="post">
    用户名:<input name="username" type="text"><br>
    密码:<input name="password" type="password"><br>
    验证码:<input name="kaptcha" type="text"><br>
    <img src="/kaptcha">
    <button type="submit">登陆</button>
</form>
</body>
</html>

配置SecurityFilterchain

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((auth) ->{
            try {
                auth.antMatchers("/kaptcha").permitAll()
                        .anyRequest().authenticated()
                        .and().formLogin()
                        .loginPage("/login.html")
                        .loginProcessingUrl("/login")
                        .failureForwardUrl("/login.html")
                        .permitAll()
                        .and()
                        .csrf().disable();
            }
            catch (Exception e){
            }
        });
        return http.build();
    }

配置AuthenticationManager

 @Bean
    public UserDetailsService userDetailsService(){
        UserDetails userDetails = User.withUsername("memory1").password("{noop}memory1").roles("memory1").build();
        return new InMemoryUserDetailsManager(userDetails);
    }

    @Bean
    public KaptchaAuthenticationProvider kaptchaAuthenticationProvider(){
        KaptchaAuthenticationProvider kaptchaAuthenticationProvider= new KaptchaAuthenticationProvider();
        kaptchaAuthenticationProvider.setUserDetailsService(userDetailsService());
        return kaptchaAuthenticationProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(){
        return new ProviderManager(kaptchaAuthenticationProvider());
    }

验证登录

image
image

posted @ 2025-07-23 10:06  Rory·PD·Yang  阅读(22)  评论(0)    收藏  举报