SpringSecurity学习(三)自定义数据源、前后端分离案例
创始人
2024-05-30 20:45:57
0

文章目录

  • 一、自定义数据源
    • 1. 认证流程与原理分析
    • AuthenticationManager、ProviderManager、AuthenticationProvider三者关系
    • 2. 全局配置AuthenticationManager方式
      • 由于WebSecurityConfigurerAdapter过期,我们使用以下写法:
    • 3. 编码
      • 3.1 创建数据库表与插入数据
      • 3.2 创建实体类
      • 3.3 导入Maven依赖与新增配置
      • 3.4 编写UserMapper
      • 3.5 编写MyUserDetailsService实现UserDetailsService接口
  • 二、前后端分离案例—认证总结
    • 1. 编写LoginFilter、修改SecurityFilterChain
    • 2. 自定义数据源
  • 三、前后端分离案例——添加验证码
    • 1. 引入验证码依赖、编写验证码配置
    • 2. 获取验证码接口
    • 3. 验证码校验filter
    • 4. 编写SpringSecurity配置类

一、自定义数据源

1. 认证流程与原理分析

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-abstractprocessingfilter
在这里插入图片描述

  • 发起认证请求,携带用户名密码,请求被UsernamePasswordAuthenticationFilter拦截
  • 在UsernamePasswordAuthenticationFilter的attemptAuthentication方法将请求的用户名和密码,封装为Authentication对象,交给AuthenticationManager进行认证
  • 认证成功,将认证信息存储SecurityContextHolder以及时调用RememberMe等,并回调AuthenticationSuccessHandler处理
public interface AuthenticationManager {Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager、ProviderManager、AuthenticationProvider三者关系

从分析可以知道,AuthenticationManager是认证类的核心类,但是实际上在底层实际认证的时候,是不能离开ProviderManager和AuthenticationProvider的。

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

ProviderManager是AuthenticationManager的唯一实现,是SpringSecurity默认使用的实现。在默认情况下,AuthenticationManager 就是一个ProviderManager 。
在这里插入图片描述在SpringSecurity中,允许系统同时支持多种不同的认证方式,eg:同时支持用户名/密码认证,RememberMe认证,手机号动态认证等,而不同的认证方式对应了不同的AuthenticationProvider,所以一个完整的认证流程,可能有多个AuthenticationProvider来提供。
多个AuthenticationProvider将组成一个list,这个列表由ProviderManager代理。即在ProviderManager中存在AuthenticationProvider列表,在ProviderManager中遍历列表中的每一个AuthenticationProvider去执行身份认证,最终得到认证结果。
ProviderManager本身也可以再配置一个AuthenticationManager作为parent,这样当ProviderManager认证失败之后,就可以进到parent再次认证。理论上,ProviderManager的parent可以是任意类型的AuthenticationManager,但是通常都是由ProviderManager来扮演parent的角色,也就是ProviderManager是ProviderManager的parent。
ProviderManager本身也可以有多个,多个ProviderManager共用一个parent。有时,一个应用程序有受保护资源的逻辑组(eg:所有符合路径的网络资源,/api/**),每个组可以有自己的专用AuthenticationManager。通常每个组都是一个ProviderManager,他们共用一个父级。然后,父级是一种全局资源,作为所有提供者的后备资源。
https://spring.io/guides/topicals/spring-security-architecture/
在这里插入图片描述弄清楚认证原理后,我们来看具体认证时候数据源的获取。默认情况下AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过UserDetailsService完成数据校验。关系如下图:
在这里插入图片描述总结:AuthenticationManager是认证管理器,在SpringSecurity中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由ProviderManager来实现。每个ProviderManager中都代理一个AuthenticationProvider的列表,列表中每个实现代表一种身份认证方式。认证时底层数据源调用UserDetailsService实现。

2. 全局配置AuthenticationManager方式

参考官方文档:https://spring.io/guides/topicals/spring-security-architecture

package com.hx.demo.config;/*** @author Huathy* @date 2023-02-27 21:22* @description*/
@Configuration
public class WebSecurityCfg extends WebSecurityConfigurerAdapter {/*** 写法一:* springboot 对security默认配置 在工厂中默认创建 AuthenticationManager** @param builder*/
//    @Autowired
//    public void initialize(AuthenticationManagerBuilder builder) throws Exception {
//        System.out.println("springboot 默认配置  builder = " + builder);
//        // springboot 默认配置  builder = org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@611e5819
//        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
//          这里会对yml配置进行覆盖,配置为whx,明文密码123
//        userDetailsService.createUser(User.withUsername("whx").password("{noop}123").roles("admin").build());
//        builder.userDetailsService(userDetailsService);
//    }/*** 写法二:* 上面的写法也等同于这里的写法。SpringSecurity会自动检测代码中是否存在UserDetailsService。* 如果有自定义的,AuthenticationManagerBuilder** @return*/@Beanpublic UserDetailsService myUserDetailsService() {InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();userDetailsService.createUser(User.withUsername("whx").password("{noop}123").roles("admin").build());return userDetailsService;}/*** 方法三:自定义AuthenticationManager** @param builder*/@Overridepublic void configure(AuthenticationManagerBuilder builder) throws Exception {System.out.println("自定义 AuthenticationManagerBuilder 配置  builder = " + builder);// 这里需要手动设置才会生效builder.userDetailsService(myUserDetailsService());}
}

默认全局AuthenticationManager的总结:

  1. 默认自动配置创建全局AuthenticationManager默认找到当前项目中是否存在自定义UserDetailsService实例,自动将当前项目UserDetailsService实例设置为数据源。
  2. 默认自动配置创建全局AuthenticationManager在工厂中使用时在代码中注入即可。

自定义全局AuthenticationManager总结:

  1. 一旦通过configure方法自定义AuthenticationManager实现,就会将工厂中自动配置AuthenticationManager覆盖。
  2. 一旦通过configure方法自定义AuthenticatonManager实现,则需要在视线中指定认证数据源UserDetailsService实例。
  3. 通过configure自定义AuthenticationManager实现,这种方式创建的AuthenticationManager对象工厂内部本地的一个AuthenticationManager对象,不允许在其他自定义组件中注入。如果希望将本地的AuthenticationManager暴露给其他组件,需要在子类中调用父类方法。
/*** 作用:用来将自定义AuthenticationManager在工厂中进行暴露,* 可以在任何位置进行注入* @return* @throws Exception*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();
}

由于WebSecurityConfigurerAdapter过期,我们使用以下写法:

@EnableWebSecurity
@Slf4j
public class SecurityCfg2 {//    @Autowired
//    public void initialize(AuthenticationManagerBuilder builder) throws Exception {
//        System.out.println("springboot 默认配置  builder = " + builder);
//        // springboot 默认配置  builder = org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@611e5819
//        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
//        // 这里会对yml配置进行覆盖,配置为whx,明文密码123
//        userDetailsService.createUser(User.withUsername("whx").password("{noop}123").roles("admin").build());
//        builder.userDetailsService(userDetailsService);
//    }@Beanpublic UserDetailsService myUserDetailsService() {InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();userDetailsService.createUser(User.withUsername("whx").password("{noop}123").roles("admin").build());return userDetailsService;}
}

3. 编码

3.1 创建数据库表与插入数据

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- Table structure for role
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`name_cn` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1004 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- Records of role
INSERT INTO `role` VALUES (1001, 'super_admin', '超级管理员');
INSERT INTO `role` VALUES (1002, 'sys_admin', '系统管理员');
INSERT INTO `role` VALUES (1003, 'user', '系统用户');-- Table structure for user
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,`password` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,`accountNonExpired` int(1) NULL DEFAULT NULL,`accountNunLocked` int(1) NULL DEFAULT NULL,`credentialsNonExpired` int(1) NULL DEFAULT NULL,`enable` int(1) NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1004 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;-- Records of user
INSERT INTO `user` VALUES (1001, 'admin', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (1002, 'huathy', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (1003, 'dy', '{noop}123', 1, 1, 1, 1);-- Table structure for user_role
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (`id` int(11) NOT NULL AUTO_INCREMENT,`uid` int(11) NULL DEFAULT NULL,`rid` int(11) NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10004 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- Records of user_role
INSERT INTO `user_role` VALUES (10001, 1001, 1001);
INSERT INTO `user_role` VALUES (10002, 1002, 1002);
INSERT INTO `user_role` VALUES (10003, 1003, 1003);SET FOREIGN_KEY_CHECKS = 1;

3.2 创建实体类

在这里插入图片描述

@Data
@EqualsAndHashCode
public class User implements UserDetails {private Integer id;private String username;private String password;/*** 账户是否过期*/private Boolean accountNonExpired;/*** 账户是否锁定*/private Boolean accountNunLocked;/*** 密码是否过期*/private Boolean credentialsNonExpired;private Boolean enable;private List roles = new ArrayList<>();@Overridepublic Collection getAuthorities() {Set authorities = new HashSet<>();roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName())));return authorities;}@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return accountNunLocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}@Overridepublic boolean isEnabled() {return enable;}
}

3.3 导入Maven依赖与新增配置

com.alibabadruid-spring-boot-starter1.1.22

mysqlmysql-connector-java5.1.48

com.enbatismybatis-plugs-spring-boot-starter1.2.4

spring:  datasource:type: com.alibaba.druid.pool.DruidDataSourcedruid:url: jdbc:mysql://localhost:3306/demo?characterEncoding=UTF-8&useSSL=falseusername: rootpassword: admindriver-class-name: com.mysql.jdbc.Driver
mybatis:mapper-locations: classpath://com.hx.mapper/**/*.xmltype-aliases-package: com.hx.entityconfiguration:map-underscore-to-camel-case: true

3.4 编写UserMapper

@Mapper
@Repository
public interface UserMapper extends BaseMapper {@Select(" select * from user t where t.username = #{username} limit 1")User getUserByUname(String username);@Select(" SELECT r.id,r.name,r.name_cn \n" +" from `role` r \n" +" left join user_role ur on r.id = ur.uid " +" where ur.uid  = #{uid} ")List getRolesByUid(Integer uid);
}

3.5 编写MyUserDetailsService实现UserDetailsService接口

@Component
public class MyUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userMapper.getUserByUname(username);if (ObjectUtils.isEmpty(user)) {throw new UsernameNotFoundException("用户名不正确");}List roles = userMapper.getRolesByUid(user.getId());user.setRoles(roles);return user;}
}

二、前后端分离案例—认证总结

在这里插入图片描述

1. 编写LoginFilter、修改SecurityFilterChain

@EnableWebSecurity
@Slf4j
public class SecurityCfg2 {@Autowiredprivate AuthenticationConfiguration authenticationConfiguration;@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();return authenticationManager;}@Beanpublic LoginFilter loginFilter() throws Exception {log.info(" === loginFilter init  ===");LoginFilter loginFilter = new LoginFilter();//  指定接受json的用户名密码参数名称loginFilter.setFilterProcessesUrl("/dologin");loginFilter.setUsernameParameter("uname");loginFilter.setPasswordParameter("pwd");loginFilter.setAuthenticationManager(authenticationManagerBean());loginFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {Map resMap = new HashMap<>();resMap.put("用户信息", authentication.getPrincipal());resMap.put("authentication", authentication);Result result = Result.success(resMap);resp.setContentType("application/json;charset=UTF-8");String jsonData = new ObjectMapper().writeValueAsString(result);resp.setStatus(HttpStatus.OK.value());resp.getWriter().write(jsonData);});loginFilter.setAuthenticationFailureHandler((req, resp, exception) -> {Result result = Result.fail("登录失败", exception.getMessage());resp.setContentType("application/json;charset=UTF-8");String jsonData = new ObjectMapper().writeValueAsString(result);resp.getWriter().write(jsonData);});return loginFilter;}@BeanSecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);http.authorizeHttpRequests().anyRequest().authenticated().and().formLogin();http.exceptionHandling().authenticationEntryPoint((req, resp, exception) -> {resp.setContentType(MediaType.APPLICATION_JSON_VALUE);resp.setCharacterEncoding("UTF-8");resp.setStatus(HttpStatus.UNAUTHORIZED.value());resp.getWriter().println("请求未认证");});http.logout().logoutUrl("/logout").logoutSuccessHandler((req, resp, auth) -> {Result result = Result.fail("注销成功", auth.getPrincipal());resp.setContentType("application/json;charset=UTF-8");String jsonData = new ObjectMapper().writeValueAsString(result);resp.getWriter().write(jsonData);});http.csrf().disable();// at:用当前过滤器来替换过滤器链中的哪个过滤器。before放在哪个过滤器之前,after放在哪个过滤器后log.info(" ===  替换了成了LoginFilter ===  ");return http.build();}
}

2. 自定义数据源

@Component
public class MyUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userMapper.getUserByUname(username);if (ObjectUtils.isEmpty(user)) {throw new UsernameNotFoundException("用户名不正确");}List roles = userMapper.getRolesByUid(user.getId());user.setRoles(roles);return user;}
}

三、前后端分离案例——添加验证码

在这里插入图片描述

1. 引入验证码依赖、编写验证码配置


com.github.pengglekaptcha2.3.2

@Configuration
public class KaptchaConfig {@Beanpublic Producer kaptcha() {Properties properties = new Properties();properties.setProperty("kaptcha.image.width", "150");properties.setProperty("kaptcha.image.heigth", "50");properties.setProperty("kaptcha.textproducer.char.string", "123456789");properties.setProperty("kaptcha.textproducer.char.length", "4");Config config = new Config(properties);DefaultKaptcha defaultKaptcha = new DefaultKaptcha();defaultKaptcha.setConfig(config);return defaultKaptcha;}
}

2. 获取验证码接口

@RestController
public class VerifyCodeController {@Autowiredprivate Producer producer;@GetMapping("vc.jpg")public String getVerifyCode(HttpSession session) throws IOException {// 1. 生成验证码String text = producer.createText();// 2. 放入session或者redissession.setAttribute("vcjpg", text);// 3. 生成图片BufferedImage image = producer.createImage(text);// 4. 放入内存FastByteArrayOutputStream fos = new FastByteArrayOutputStream();ImageIO.write(image, "jpg", fos);// 5. 返回base64String img = Base64.getEncoder().encodeToString(fos.toByteArray());return img;}
}

3. 验证码校验filter

@Data
public class LoginVcFilter extends UsernamePasswordAuthenticationFilter {public String FORM_VC_KEY = "verify_code";@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (!request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}try {// 1. 获取请求验证码Map userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);String verifyCode = userInfo.get(getFORM_VC_KEY());// 2. 获取session中的验证码String vcjpg = String.valueOf(request.getSession().getAttribute("vcjpg"));if (!vcjpg.equals(verifyCode)) {throw new VerifyCodeException("验证码错误!");}// 3. 获取用户名和密码认证String username = userInfo.get(getUsernameParameter());String pwd = userInfo.get(getPasswordParameter());UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, pwd);setDetails(request, authToken);// 注意这里要调用authenticationManager中的auth方法return this.getAuthenticationManager().authenticate(authToken);} catch (IOException e) {e.printStackTrace();}return super.attemptAuthentication(request, response);}
}

4. 编写SpringSecurity配置类

@EnableWebSecurity
@Slf4j
public class SecurityConfig {@Autowiredprivate AuthenticationConfiguration authenticationConfiguration;@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();return authenticationManager;}@Beanpublic LoginVcFilter loginVcFilter() throws Exception {log.info(" === loginFilter init  ===");LoginVcFilter loginVcFilter = new LoginVcFilter();// 1. 指定认证接收参数loginVcFilter.setPasswordParameter("pwd");loginVcFilter.setUsernameParameter("uname");loginVcFilter.setFORM_VC_KEY("code");// 2. 指定认证处理URLloginVcFilter.setFilterProcessesUrl("/dologin");// 3. 指定认证管理器loginVcFilter.setAuthenticationManager(authenticationManagerBean());// 4. 指定成功处理loginVcFilter.setAuthenticationSuccessHandler((req, resp, authentication) -> {Map resMap = new HashMap<>();resMap.put("用户信息", authentication.getPrincipal());resMap.put("authentication", authentication);Result result = Result.success(resMap);resp.setContentType("application/json;charset=UTF-8");String jsonData = new ObjectMapper().writeValueAsString(result);resp.setStatus(HttpStatus.OK.value());resp.getWriter().write(jsonData);});// 5. 指定失败处理loginVcFilter.setAuthenticationFailureHandler((req, resp, exception) -> {Result result = Result.fail("登录失败", exception.getMessage());resp.setContentType("application/json;charset=UTF-8");String jsonData = new ObjectMapper().writeValueAsString(result);resp.getWriter().write(jsonData);});return loginVcFilter;}@Beanprotected SecurityFilterChain configure(HttpSecurity http) throws Exception {log.info("   === 替换了 loginVcFilter === ");http.addFilterAt(loginVcFilter(), UsernamePasswordAuthenticationFilter.class);http.authorizeHttpRequests().mvcMatchers("/vc.jpg").permitAll().anyRequest().authenticated();http.formLogin();http.exceptionHandling().authenticationEntryPoint((req, resp, ex) -> {resp.setContentType(MediaType.APPLICATION_JSON_VALUE);resp.setStatus(HttpStatus.UNAUTHORIZED.value());resp.getWriter().println("Please Visit After Login");});http.logout();http.csrf().disable();return http.build();}
}

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...