SpringSecurity(五)【自定义认证数据源】

发布于:2022-10-26 ⋅ 阅读:(600) ⋅ 点赞:(0)

五、自定义认证数据源


认证流程分析

官方文档https://docs.spring.io/spring-security/site/docs/5.5.8/reference/html5/#servlet-authentication-abstractprocessingfilter

在这里插入图片描述

  • 发起认证请求,请求中携带用户名、密码,该请求会被UsernamePasswordAuthenticationFilter拦截
  • UsernamePasswordAuthenticationFilterattemptAuthentication 方法中将请求中的用户名和密码,封装为 Authentication 对象,并交给 AuthenticationManager 进行认证
  • 认证成功,将认证信息存储到 SecurityContextHolder 以及调用记住我信息,并回调 AuthenticationSuccessHandler 处理
  • 认证失败,清除 SecurityContextHolder 以及记住我中的信息,回调 AuthenticationFailureHandler 处理

5.1 自定义认证数据源之原理分析(一)

  • 认证时的调试

在这里插入图片描述

AuthenticationManager、ProviderManager、AuthenticationProvider 三者关系

AuthenticationManager 是认证的核心类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider

  • AuthenticationManager是一个认证管理器,它定义了 SpringSecurity 过滤器要执行认证操作
  • ProviderManagerAuthenticationManager 接口的实现类。SpringSecurity 认证时默认使用就是 ProviderManager
  • AuthenticationProvider就是针对不同的身份类型执行的具体身份认证

AuthenticationManager 与 ProviderManager

在这里插入图片描述

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

ProviderManager 与 AuthenticationProvider

官方文档https://docs.spring.io/spring-security/site/docs/5.5.8/reference/html5/#servlet-authentication-providermanager

在这里插入图片描述

  • 在 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...
  }
}

总结

  1. 默认自动配置创建全局 AuthenticationManager 时,默认找当前项目中是否存在自定义的 UserDetailsService 实例,自动将当前项目 UserDetailsService 实例设置为数据源
  2. 默认自动配置创建全局 AuthenticationManager 时,在工厂中使用时直接在代码中注入即可
  • 自定义全局 AuthenticationManager
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

  @Override
  public void configure(AuthenticationManagerBuilder builder) {
       // builder...
  }
}

总结

  1. 一旦通过 configure 方法自定义 AuthenticationManager 实现,就会将工厂中自动配置 AuthenticationManager 进行覆盖
  2. 一旦通过 configure 方法自定义 AuthenticationManager 实现,需要在实现中指定认证数据源对象 UserDetailsService
  3. 一旦通过 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 自定义数据源之实现

  1. 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>
  1. 配置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
  1. 编写实体类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 + '\'' +
                '}';
    }
}
  1. 对应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层,这里就不给大家演示了

  1. 编写实现接口 UserDetailsServiceDivUserDetailsService
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;
    }
}
  1. 修改对应 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();
    }

}
  1. 用例测试并访问

在这里插入图片描述

本文含有隐藏内容,请 开通VIP 后查看

微信公众号

今日签到

点亮在社区的每一天
去签到