SpringSecurity5.7.4从入门到精通全套教程

文章目录

前言

用户进行认证,最常见的认证方式就是用户名+密码,认证服务需要根据用户名从存储中查询用户信息,然后判断输入的密码和存储中的密码是否匹配

对用户名、密码存储,SpringSecurity支持多种存储机制:

  • 内存
  • JDBC 关系型数据库
  • 使用 UserDetailService的自定义数据存储
  • 使用LDAP认证的LDAP存储

本篇文档主要学习使用Mybatis-Plus操作数据库存储用户信息

1.环境搭建

1.1集成Mybatis-Plus引入相关依赖

引入Mybatis-PlusMysql驱动、开发工具包

<!--mybatis-plus场景-->
	<dependency>
		<groupId>com.baomidou</groupId>
		<artifactId>mybatis-plus-boot-starter</artifactId>
		<version>3.5.2</version>
	</dependency>
	<!--mysql场景-->
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
	</dependency>
	<!--简化开发-->
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<optional>true</optional>
	</dependency>

配置数据源:url地址和数据库使用自己的

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://localhost:3306/spring_security?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
    hikari:
      username: root
      password: root
1.2创建数据库和数据库表

执行sql脚本创建数据库和数据表

create database spring_security;
use spring_security;
# 创建t_user数据表
create table t_user
(
    id        int(10) primary key,
    user_name varchar(25) not null,
    password  varchar(25) not null
);
# 向t_user数据表中添加数据 
insert into t_user(id, user_name, password)
VALUES (1, 'jack', '123'),
       (2, 'rose', '123');
1.3创建t_user表对应实体类

我这里是手动创建并回顾Mybatis-Plus操作步骤,你也可以一步到位使用Mybatis-Plus代码生成器

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user")//对应数据库表名
public class User implements Serializable {
    /**
     * 表主键id
     */
    @TableId
    private Integer id;
    private String userName;
    private String password;
}
1.4整合Mybatis-Plus,创建接口继承Mybatis-Plus的接口
public interface UserMapper extends BaseMapper<User> {
}
1.5测试

在SpringBoot测试类中测试代码,查验环境是否搭建成功

@SpringBootTest
@Slf4j
class SpringBootSecurityApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testUserMapper() {
        List users = userMapper.selectList(null);
        System.out.println(users);
    }
}

2.用户登录

2.1UserDetailService接口讲解

springboot ldap认证用户 为啥需要管理员账号密码 spring security ldap教程_User


该接口中,只声明一个根据用户名加载用户信息的方法,自定义开发时,只需要实现UserDetailService 接口即可,该接口的 loadUserByUsername() 方法的返回值是UserDetail

public interface UserDetailsService {
    /**
     * 
     * @param username 用户的用户名
     * @return  返回用户信息
     * @throws UsernameNotFoundException    找不到当前用户报异常
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

SpringSecurity默认为UserDetailService提供了几个实现类

springboot ldap认证用户 为啥需要管理员账号密码 spring security ldap教程_User_02


从类名称已经比较好理解,支持内存、数据库查询用户。首先我们看下JdbcDaoImpl是如何查询用户的,是不是满足我们的业务要求,查看其 loadUserByUsername() 方法执行逻辑

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 		//select username,password,enabled from users where username = ?
 		//1.JdbcTemplate 执行SQL
        List<UserDetails> users = this.loadUsersByUsername(username);
        //2.没有查询到数据,抛出UsernameNotFoundException
        if (users.size() == 0) {
            this.logger.debug("Query returned no results for user '" + username + "'");
            throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.notFound", new Object[]{username}, "Username {0} not found"));
        } else {
			//3.查询多条,取第一条数据
            UserDetails user = (UserDetails)users.get(0);
            //创一个Set集合,存放用户授予的权限
            Set<GrantedAuthority> dbAuthsSet = new HashSet();
            //4.开启了查询权限,执行SQL:select username,authority from authorities where username = ?
            //将查询得到的结果放入集合中
            if (this.enableAuthorities) {
                dbAuthsSet.addAll(this.loadUserAuthorities(user.getUsername()));
            }
			//5.开启了权限分组--->select g.id,g.group_name,ga.authority from groups g,group_members gm,group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id
            if (this.enableGroups) {
                dbAuthsSet.addAll(this.loadGroupAuthorities(user.getUsername()));
            }
			//把Set集合 --->List集合
            List<GrantedAuthority> dbAuths = new ArrayList(dbAuthsSet);
            this.addCustomAuthorities(user.getUsername(), dbAuths);
            //6.当前用户没有任何权限,也会抛出UsernameNotFoundException
            if (dbAuths.size() == 0) {
                this.logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");
                throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority", new Object[]{username}, "User {0} has no GrantedAuthority"));
            } else {
            	//7.创建UserDetails,类型的用户对象并返回
                return this.createUserDetails(username, user, dbAuths);
            }
        }
    }

通过以上分析可知,JdbcDaoImpl中的SQL都是固定的,而且为了更好的扩展,我们可以仿照其逻辑自定义实现UserDetailService接口

2.2UserDetails接口讲解

UserDetailService接口需要返回一个UserDetails类型的对象,也从名称上很好理解,就是一个封装了用户信息的类,我们需要将我们查询出来的用户对象,转为SpringSecurity中支持的用户对象,以便框架进行校验、存储
UserDetails接口源码讲解

public interface UserDetails extends Serializable {
	//授权信息集合
    Collection<? extends GrantedAuthority> getAuthorities();
	//获取密码
    String getPassword();
	//获取用户名
    String getUsername();
	//用户的账户是否未过期,即未过期则返回true
    boolean isAccountNonExpired();
	//用户是否未锁定,无法对锁定的用户进行身份验证,如果用户被锁定,则返回true
    boolean isAccountNonLocked();
	//用户的凭证{密码}是否未过期,即未过期则返回true
    boolean isCredentialsNonExpired();
	//用户是启用还是禁用,如果启用了用户则返回true
    boolean isEnabled();
}

SpringSecurity默认提供了一个实现类User

public class User implements UserDetails, CredentialsContainer {
    private static final long serialVersionUID = 570L;
    private static final Log logger = LogFactory.getLog(User.class);
    private String password;
    private final String username;
    private final Set<GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;
}

目前来说,框架提供的User类,已经够用,但是本着可能自己项目需要扩展的情况,我们也需要自定义实现
UserDetails接口

2.3接口实现
@Data
public class TestUserDetails implements UserDetails {
    private String password;
    private final String username;
    /**
     * 扩展字段,手机号放入用户信息中
     */
    private final String phone; 
    private final Set<GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;

    public TestUserDetails( String username,String password, String phone, List<GrantedAuthority> authorities, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) {
        this.password = password;
        this.phone = phone;
        this.username = username;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        this.enabled = enabled;
        this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); // 非空判断+排序
    }

    private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) {
        Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
        SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet(new TestUserDetails.AuthorityComparator());
        for (GrantedAuthority grantedAuthority : authorities) {
            Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
            sortedAuthorities.add(grantedAuthority);
        }
        return sortedAuthorities;
    }

    private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {
        private static final long serialVersionUID = 600L;
        @Override
        public int compare(GrantedAuthority g1, GrantedAuthority g2) {
            if (g2.getAuthority() == null) {
                return -1;
            } else {
                return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority());
            }
        }
    }
}

然后实现UserDetailService接口,代码如下

@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_name", username);
        User user = userMapper.selectOne(queryWrapper);
        //如果没有查询到用户就抛出异常
        if (Objects.isNull(user)) {
            log.error("Query returned no results for user '" + username + "'");
            throw new UsernameNotFoundException("查无该用户,请重试:\t" + username);
        } else {
            //TODO 查询对应的权限信息
            //设置权限集合,后续需要数据库查询(授权篇讲解,这里定义死)
            List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
            // 3. 返回UserDetails类型用户
            return new TestUserDetails(username, user.getPassword(), user.getPhonenumber(), authorityList,
                    true, true, true, true); // 账号状态这里都直接设置为启用,实际业务可以存在数据库中

        }
    }
}
2.4添加配置类

SpringSecurity5.7和之前的配置有些区别,后续会详细讲解,以前用的是继承WebSecurityConfigurerAdapter 但是现在已经过时了
添加配置类,注入一个密码编码器

//标注这是一个配置类
@Configuration
//开启SpringSecurity ;debug:是否开启Debug模式
@EnableWebSecurity(debug = false)
public class SecurityConfig {
    /**
     * 密码器
     * 密码加密功能
     * 创建实现类BCryptPasswordEncoder注入容器
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
2.5测试
@Test
    @DisplayName("插入一条用户数据")
    void insertUserTest() {
        User user = new User();
        user.setUserName("admin");
        user.setPassword(new BCryptPasswordEncoder().encode("123456"));
        user.setLoginName("管理员");
        user.setPhone("13688888888");
        userService.save(user);
    }

3.自定义用户登录页面,不需要认证可以访问

前言:当前讲解SpringSecurity版本为5.7,了解一下SpringSecurity 配置与使用(含新 API 替换过时的 WebSecurityConfigurerAdapter)
Security一:启动类增加注解

@SpringBootApplication
@MapperScan("com.huang.spring.mapper")
//表示启用SpringSecurity功能
@EnableWebSecurity
//开启基于注解的接口权限控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringBootSecurityApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootSecurityApplication.class, args);
	}

}

Security二:SecurityConfiguration配置类
现在的SpringSecurity版本更换了新的配置方式,目前新版本扔兼容旧版配置,不喜欢新版配置也可以用旧版

旧版配置

/**
 * 这是旧版api
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 指定加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        // 使用官方推荐的BCrypt加密密码,也就是md5加随机盐
        return new BCryptPasswordEncoder();
    }

    /**
     * configure(WebSecurity)用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。
     * 一般用于配置全局的某些通用事物,例如静态资源等
     * @param web
     * @throws Exception
     */
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**", "/ignore2");
    }

    /**
     * 配置接口拦截
     * configure(HttpSecurity)允许基于选择匹配在资源级配置基于网络的安全性,也就是对角色所能访问的接口做出限制
     * @param httpSecurity 请求属性
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests()
                // 允许get请求/test/any,而无需认证,不配置HttpMethod默认允许所有请求类型
                .antMatchers(HttpMethod.GET, "/test/any").permitAll()
                //指定权限为admin才能访问,这里和方法注解配置效果一样,但是会覆盖注解
                .antMatchers("/test/admin").hasRole("admin")
                // 所有请求都需要验证
                .anyRequest().authenticated()
                .and()
                //.httpBasic() Basic认证,和表单认证只能选一个
                // 使用表单认证页面
                .formLogin()
                //配置登录入口,默认为security自带的页面/login
                .loginProcessingUrl("/login")
                .and()
                // post请求要关闭csrf验证,不然访问报错;实际开发中开启,需要前端配合传递其他参数
                .csrf().disable();
    }
}

新版配置

@Configuration
public class SpringSecurityConfiguration {
    /**
     * 密码加密器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * configure(WebSecurity)用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。
     * 一般用于配置全局的某些通用事物,例如静态资源等
     * 新版本其实不推荐把路径的拦截写在这里,而是推荐写在securityFilterChain里面
     * "You are asking Spring Security to ignore Ant [pattern='/resources/**']. This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead"
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        return web -> web.ignoring().antMatchers("/resources/**","/index");
    }

    /**
     * 配置接口拦截
     * configure(HttpSecurity)允许基于选择匹配在资源级配置基于网络的安全性,也就是对角色所能访问的接口做出限制
     * @param httpSecurity 请求属性
     * @return HttpSecurity
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        //自定义自己编写的登录页面,
        httpSecurity.formLogin()
                //登录页面设置
                .loginPage("/login")
                //登录访问路径
                .loginProcessingUrl("/user/login")
                //登录成功之后,跳转路径
                .defaultSuccessUrl("/index").permitAll()
                .and().authorizeHttpRequests()
                //设置哪些路径可以直接访问,不需要认证
                .antMatchers("/","/hello","/user/login").permitAll()
                //表示所有请求都可以访问 GET POST DELETE PUT 等等
                .anyRequest().authenticated()
                //通过csrf的防护方式:disable关闭
                .and().csrf().disable();
        return httpSecurity.build();
    }
}
3.1在配置类实现相关配置
@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        //自定义自己编写的登录页面,
        httpSecurity.formLogin()
                //登录页面设置
                .loginPage("/login.html")
                //登录访问路径
                .loginProcessingUrl("/login")
                //登录成功之后,跳转路径
                .defaultSuccessUrl("/index").permitAll()
                .and().authorizeHttpRequests()
                //设置哪些路径可以直接访问,不需要认证
                .antMatchers("/","/hello","/login").permitAll()
                //表示所有请求都可以访问 GET POST DELETE PUT 等等
                .anyRequest().authenticated()
                //通过csrf的防护方式:disable关闭
                .and().csrf().disable();
        return httpSecurity.build();
    }
3.2创建相关页面,和处理器(Controller)
3.2.1创建登录页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<form action="/user/login" method="post">
    用户名:<input type="text" name="username"/><br>
    密码:<input type="password" name="password"/><br>
    <input type="submit" value="登录"/>
</form>
</body>
</html>
3.2.2配置类
@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        //自定义登录页面,
        httpSecurity.formLogin()
                //设置请求登录认证页面
                .loginPage("/login.html")
                //设置请求登录的url
                .loginProcessingUrl("/login")
                //登录认证成功之后跳转路径,permitAll表示无条件进行访问
                .defaultSuccessUrl("/success").permitAll()
                .and().authorizeRequests()
                //设置哪些请求路径不需要认证可以直接访问
                .mvcMatchers("/index","/login.html","/hello").permitAll()
                //表示所有请求方式都可以访问
                .anyRequest().authenticated()
                //通过csrf的防护方式:disable关闭
                .and().csrf().disable();
        return httpSecurity.build();
    }
3.2.3 控制层
@RestController
public class SecurityController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello,Security...";
    }

    @GetMapping("/success")
    public String success() {
        return "Welcome Login Success";
    }
}
3.2.4测试

自行测试一下就好 把需要认证的请求和不需要认证的请求都测一遍