SpringSecurity5.7.4从入门到精通全套教程
文章目录
前言
用户进行认证,最常见的认证方式就是用户名+密码,认证服务需要根据用户名从存储中查询用户信息,然后判断输入的密码和存储中的密码是否匹配
对用户名、密码存储,SpringSecurity支持多种存储机制:
- 内存
- JDBC 关系型数据库
- 使用 UserDetailService的自定义数据存储
- 使用LDAP认证的LDAP存储
本篇文档主要学习使用Mybatis-Plus操作数据库存储用户信息
1.环境搭建
1.1集成Mybatis-Plus引入相关依赖
引入Mybatis-Plus、Mysql驱动、开发工具包
<!--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接口讲解
该接口中,只声明一个根据用户名加载用户信息的方法,自定义开发时,只需要实现UserDetailService 接口即可,该接口的 loadUserByUsername() 方法的返回值是UserDetail
public interface UserDetailsService {
/**
*
* @param username 用户的用户名
* @return 返回用户信息
* @throws UsernameNotFoundException 找不到当前用户报异常
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
SpringSecurity默认为UserDetailService提供了几个实现类
从类名称已经比较好理解,支持内存、数据库查询用户。首先我们看下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测试
自行测试一下就好 把需要认证的请求和不需要认证的请求都测一遍