五、自定义认证数据源
认证流程分析

- 发起认证请求,请求中携带用户名、密码,该请求会被
UsernamePasswordAuthenticationFilter拦截 - 在
UsernamePasswordAuthenticationFilter的 attemptAuthentication 方法中将请求中的用户名和密码,封装为 Authentication 对象,并交给 AuthenticationManager 进行认证 - 认证成功,将认证信息存储到 SecurityContextHolder 以及调用记住我信息,并回调 AuthenticationSuccessHandler 处理
- 认证失败,清除 SecurityContextHolder 以及记住我中的信息,回调 AuthenticationFailureHandler 处理
5.1 自定义认证数据源之原理分析(一)
- 认证时的调试

AuthenticationManager、ProviderManager、AuthenticationProvider 三者关系
AuthenticationManager 是认证的核心类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider
AuthenticationManager是一个认证管理器,它定义了 SpringSecurity 过滤器要执行认证操作ProviderManager是 AuthenticationManager 接口的实现类。SpringSecurity 认证时默认使用就是 ProviderManagerAuthenticationProvider就是针对不同的身份类型执行的具体身份认证
AuthenticationManager 与 ProviderManager

ProviderManager 是 AuthenticationManager 的唯一实现,也是 SpringSecurity 默认使用实现。不难看出默认情况下 AuthenticationManager 就是一个 ProviderManager
ProviderManager 与 AuthenticationProvider

- 在 SpringSecurity 中,允许系统同时支持多种不同的认证方式,例如同时支持用户名/密码认证、ReremberMe 认证、手机号码动态认证等,而不同的认证方式对应了不同的 AuthenticationProvider,所以一个完整的认证流程可能由多个 AuthenticationProvider 来提供
- 多个 AuthenticationProvider 将组成一个列表,这个列表将由 ProviderManager 代理。换句话说,在 ProviderManager 中存在一个 AuthenticationProvider 列表,在 ProviderManager 中遍历列表中的每一个 AuthenticationProvider 去执行身份认证,最终得到认证结果
- ProviderManager 本身也可以再配置一个 AuthenticationManager 作为 parent,这样当 ProviderManager 认证失败之后,就可以进入到 parent 中再次进行认证。理论上来说, ProviderManager 的 parent 可以是任意类型的 AuthenticationManager,但是通常都是由 ProviderManager 来扮演 parent 的角色,也就是 ProviderManager 是 ProviderManager 的 parent(源码分析得出)
- ProviderManager 本身也可以有多个,多个 ProviderManager 共用同一个 parent。有时,一个应用程序有受保护资源的逻辑组例如,所有符合路径模式的网络资源,如/api/**),每个组可以有自己的专用 AuthenticationManager。通常,每个组都是一个 ProviderManager,它们共享一个父级。然后,父级是一种
全局资源,作为所有提供者的后备资源
根据上面的介绍,我们绘出新的 AuthenticationManager、ProvideManager 和 AuthentictionProvider 关系(责任链模式)

5.2 自定义认证数据源之原理分析(二)
认证时,首先经过局部的 ProviderManager(子),后 ProviderManager(父)
默认情况下,AuthenticationManager 是由 DaoAuthenticationProvider 类实现认证的,在 DaoAuthenticationProvider 认证时,又通过 UserDetailsService 完成数据源的校验
- 它们之间的调用关系如下



- 总结
AuthenticationManager 是认证管理器 ,在 SpringSecurity 中有全局 AuthenticationManager,也可以有局部 AuthenticationManager。全局的 AuthenticationManager 用来对全局认证进行处理,局部的 AuthenticationManager 用来对某些特殊资源认证处理。无论是全局认证管理器还是局部认证管理器都是由 ProviderManager 进行实现。每一个 ProviderManager 中都代理一个 AuthenticationProvider 的列表,列表中每一个实现代表一种身份认证方式。认证时底层数据源需要调用 UserDetailsService 实现
5.3 自定义数据源之全局配置 AuthenticationManager 方式

package com.vinjcent.config;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import com.vinjcent.handler.DivLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.jaas.memory.InMemoryConfiguration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
// 在之前查看源码中发现,当发现有 UserDetailsService.class 这个bean的时候, UserDetailsServiceAutoConfiguration 就会失效
@Bean
public UserDetailsService userDetailsService() {
// 定义内存用户信息管理者对象,将用户信息存储在内存当中
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
// 创建用户对象
UserDetails userDetails = User.withUsername("aaa").password("{noop}123").roles("admin").build();
// 将用户对象交由用户管理者去管理
userDetailsService.createUser(userDetails);
return userDetailsService;
}
// 方式一: springboot 对 security 默认配置中,进行自动配置时,自动在工厂中创建全局 AuthenticationManager
// 相当于将默认已注入的bean进行属性设置,不妨直接创建一个bean ===> UserDetailsService 使得默认配置失效即可
//@Autowired
//public void initialize(AuthenticationManagerBuilder builder) throws Exception {
// System.out.println("springboot默认配置: " + builder);
// InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
// UserDetails userDetails = User.withUsername("aaa").password("{noop}123").roles("admin").build();
// userDetailsService.createUser(userDetails);
// builder.userDetailsService(userDetailsService);
//}
// 方式二: 自定义 AuthenticationManager(推荐)
// 自定义的 AuthenticationManager 将会覆盖默认的配置,优先级更高
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义AuthenticationManager: " + builder);
builder.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
}
}
配置全局 AuthenticationManager
- 默认全局 AuthenticationManager
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
... // web stuff here
@Autowired
public void initialize(AuthenticationManagerBuilder builder) {
// builder...
}
}
总结
- 默认自动配置创建全局 AuthenticationManager 时,默认找当前项目中是否存在自定义的 UserDetailsService 实例,自动将当前项目 UserDetailsService 实例设置为数据源
- 默认自动配置创建全局 AuthenticationManager 时,在工厂中使用时直接在代码中注入即可
- 自定义全局 AuthenticationManager
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
... // web stuff here
@Override
public void configure(AuthenticationManagerBuilder builder) {
// builder...
}
}
总结
- 一旦通过 configure 方法自定义 AuthenticationManager 实现,就会将工厂中自动配置 AuthenticationManager 进行覆盖
- 一旦通过 configure 方法自定义 AuthenticationManager 实现,需要在实现中指定认证数据源对象 UserDetailsService
- 一旦通过 configure 方法自定义 AuthenticationManager 实现,这种方式创建的 AuthenticationManager 对象,是一个工厂内部本地的 AuthenticationManager 对象,不允许在其它自定义的组件中注入(@Autowired)
- 用来在工厂中暴露自定义 AuthenticationManager 实例
// 方式二: 自定义 AuthenticationManager(推荐)
// 自定义的 AuthenticationManager 将会覆盖默认的配置,优先级更高(不支持自定义组件注入@Autowired)
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义AuthenticationManager: " + builder);
builder.userDetailsService(userDetailsService());
}
// 作用: 用来将自定义的 AuthenticationManager 在工厂中进行暴露,可以在任何位置注入
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
5.4 自定义数据源之库表设计
自定义数据库用户表
- 用户表
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` VARCHAR(32) DEFAULT NULL COMMENT '用户名',
`password` VARCHAR(255) DEFAULT NULL COMMENT '密码',
`enabled` TINYINT(1) DEFAULT NULL COMMENT '是否可用',
`accountNonExpired` TINYINT(1) DEFAULT NULL COMMENT '账户过期',
`accountNonLocked` TINYINT(1) DEFAULT NULL COMMENT '账户锁定',
`credentialsNonExpired` TINYINT(1) DEFAULT NULL COMMENT '凭证过期',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
- 角色表
CREATE TABLE `role` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(32) DEFAULT NULL,
`name_zh` VARCHAR(32) 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`),
CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`),
CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
- 插入数据

5.5 自定义数据源之实现
- 在
pom.xml导入依赖
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
- 配置
application.yml文件
# 端口号
server:
port: 3035
# 服务应用名称
spring:
application:
name: SpringSecurity02
# 关闭thymeleaf缓存(用于修改完之后立即生效)
thymeleaf:
cache: false
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis:
# 注意 mapper 映射文件必须使用"/"
type-aliases-package: com.vinjcent.pojo
mapper-locations: com/vinjcent/mapper/**/*.xml
# 日志处理,为了展示 mybatis 运行 sql 语句
logging:
level:
com:
vinjcent:
debug
- 编写实体类User、Role
- User
package com.vinjcent.pojo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
// 自定义用户User
public class User implements UserDetails {
private Integer id; // 用户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() {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(role -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
authorities.add(simpleGrantedAuthority);
});
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 List<Role> getRoles() {
return roles;
}
}
- Role
package com.vinjcent.pojo;
public class Role {
private Integer id;
private String name;
private String nameZh;
public Role() {
}
public Role(Integer id, String name, String nameZh) {
this.id = id;
this.name = name;
this.nameZh = 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;
}
@Override
public String toString() {
return "Role{" +
"id=" + id +
", name='" + name + '\'' +
", nameZh='" + nameZh + '\'' +
'}';
}
}
- 对应mapper映射文件
- UserMapper
package com.vinjcent.mapper;
import com.vinjcent.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
* @author vinjcent
* @description 针对表【user】的数据库操作Mapper
* @createDate 2022-09-25 12:03:42
*/
@Mapper
@Repository
public interface UserMapper {
// 根据用户名返回用户信息
User queryUserByUsername(@Param("username") String username);
}
- RoleMapper
package com.vinjcent.mapper;
import com.vinjcent.pojo.Role;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @author vinjcent
* @description 针对表【role】的数据库操作Mapper
* @createDate 2022-09-25 12:01:18
*/
@Mapper
@Repository
public interface RoleMapper {
List<Role> queryRolesByUid(@Param("uid") Integer uid);
}
【注】还需要些service层,这里就不给大家演示了
- 编写实现接口 UserDetailsService 类 DivUserDetailsService
package com.vinjcent.config.security;
import com.vinjcent.pojo.Role;
import com.vinjcent.pojo.User;
import com.vinjcent.service.RoleService;
import com.vinjcent.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.util.List;
@Component
public class DivUserDetailsService implements UserDetailsService {
// dao ===> springboot + mybatis
private final UserService userService;
private final RoleService roleService;
@Autowired
public DivUserDetailsService(UserService userService, RoleService roleService) {
this.userService = userService;
this.roleService = roleService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.查询用户
User user = userService.queryUserByUsername(username);
if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
// 2.查询权限信息
List<Role> roles = roleService.queryRolesByUid(user.getId());
user.setRoles(roles);
return user;
}
}
- 修改对应 WebSecurityConfiguration 配置类
package com.vinjcent.config.security;
import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import com.vinjcent.handler.DivLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.jaas.memory.InMemoryConfiguration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
/**
* 重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
// 注入数据源认证
private final DivUserDetailsService userDetailsService;
@Autowired
public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义AuthenticationManager: " + builder);
// 配置的地方,为 AuthenticationManager 配置用户认证的 UserDetailsService
builder.userDetailsService(userDetailsService);
}
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/toLogin").permitAll()
.mvcMatchers("/index").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.usernameParameter("uname")
.passwordParameter("passwd")
// .successForwardUrl("/hello")
// .defaultSuccessUrl("/index", true)
.successHandler(new DivAuthenticationSuccessHandler())
// .failureForwardUrl("/toLogin")
// .failureUrl("/toLogin")
.failureHandler(new DivAuthenticationFailureHandler())
.and()
.logout()
// .logoutUrl("/logout")
//.logoutRequestMatcher(new OrRequestMatcher(
// new AntPathRequestMatcher("/aLogout", "GET"),
// new AntPathRequestMatcher("/bLogout", "POST")
//))
.logoutSuccessHandler(new DivLogoutSuccessHandler())
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/toLogin")
.and()
.csrf().disable();
}
}
- 用例测试并访问
