https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html#servlet-authentication-abstractprocessingfilter
public interface AuthenticationManager {Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
从分析可以知道,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实现。
参考官方文档: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的总结:
自定义全局AuthenticationManager总结:
/*** 作用:用来将自定义AuthenticationManager在工厂中进行暴露,* 可以在任何位置进行注入* @return* @throws Exception*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();
}
@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;}
}
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;
@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 extends GrantedAuthority> 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;}
}
com.alibaba druid-spring-boot-starter 1.1.22
mysql mysql-connector-java 5.1.48
com.enbatis mybatis-plugs-spring-boot-starter 1.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
@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);
}
@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;}
}
@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();}
}
@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;}
}
com.github.penggle kaptcha 2.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;}
}
@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;}
}
@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);}
}
@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();}
}