SpringBoot整合SpringSecurity——从零到一的超级详细教程
Springboot整合SpringSecurity
版本声明
- jdk版本:17
- gradle
- springboot版本:2.7.18(spring boot3 版本变化较大,后续会更新,这里如果是在使用springboot3+的版本请等一等)
- spring security版本:5.7.11
Spring Security的基本组件及认证流程分析
Spring Security通过一些列的过滤器完成了用户身份认证及其授权工作,每个过滤器都有不同分工,当然这些过滤器并不是全部都一起工作,而是根据我们需要什么功能,才会选取对应的过滤器加入。
当然这些过滤器并不是直接加入web的过滤器中,而是通过一个过滤器代理完成。web过滤器中只会加入一个或多个过滤器代理,然后由这些代理负责管理哪些Security Filter需要加入进来。

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

组件介绍
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 ...
一图理清基本组件关系

6.基于多种方式配置登录用户:memory、jdbc、MyBatis
前面章节我们所有的用户信息(用户名和密码)都是基于配置文件配置的,今天这节课我们开始定义不同的用户信息获取来源。
基于内存方式
其实我们在配置文件中写的用户信息,最终也是被读到内存中的,大家不知道对这段代码还熟悉么:

这一块就是基于内存方式构建了用户信息,定义了默认的用户信息来源。
配置内存方式
@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

memory2

基于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 去掉

执行之后,得到两张表

配置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;
}
验证登录


基于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和内存方式获取用户信息的配置给注销,否则会报如下错误:

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

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

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


所以当我们定义两个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());
}
验证登录



浙公网安备 33010602011771号