Spring Security 04 自定义认证
登录⽤户数据获取
SecurityContextHolder
Spring Security 会将登录⽤户数据保存在 Session 中。但是,为了使⽤⽅便, Spring Security 在此基础上还做了⼀些改进,其中最主要的⼀个变化就是线程绑定。当⽤户登录成功后,Spring Security 会将登录成功的⽤户信息保存到SecurityContextHolder 中。
SecurityContextHolder 中的数据保存默认是通过 ThreadLocal 来实现的,使⽤ ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是⽤户数据和请求线程绑定在⼀起。当登录请求处理完毕后, Spring Security 会将SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时, Spring Security 就会先从 Session 中取出⽤户登录数据,保存到SecurityContextHolder 中,⽅便在该请求的后续处理过程中使⽤,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder 中的数据清空。
实际上 SecurityContextHolder 中存储是 SecurityContext,在SecurityContext 中存储是 Authentication。
这种设计是典型的策略设计模式:
public class SecurityContextHolder {public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";public static final String MODE_GLOBAL = "MODE_GLOBAL";private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";public static final String SYSTEM_PROPERTY = "spring.security.strategy";private static String strategyName = System.getProperty(SYSTEM_PROPERTY);private static SecurityContextHolderStrategy strategy;private static int initializeCount = 0;static {initialize();}private static void initialize() {initializeStrategy();initializeCount++;}private static void initializeStrategy() {if (MODE_PRE_INITIALIZED.equals(strategyName)) {Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED+ ", setContextHolderStrategy must be called with the fully constructed strategy");return;}if (!StringUtils.hasText(strategyName)) {// Set defaultstrategyName = MODE_THREADLOCAL;}if (strategyName.equals(MODE_THREADLOCAL)) {strategy = new ThreadLocalSecurityContextHolderStrategy();return;}if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {strategy = new InheritableThreadLocalSecurityContextHolderStrategy();return;}if (strategyName.equals(MODE_GLOBAL)) {strategy = new GlobalSecurityContextHolderStrategy();return;}// Try to load a custom strategytry {Class<?> clazz = Class.forName(strategyName);Constructor<?> customStrategy = clazz.getConstructor();strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();}catch (Exception ex) {ReflectionUtils.handleReflectionException(ex);}}/* Explicitly clears the context value from the current thread.*/public static void clearContext() {strategy.clearContext();}/* Obtain the current <code>SecurityContext</code>.* @return the security context (never <code>null</code>)*/public static SecurityContext getContext() {return strategy.getContext();}/* Primarily for troubleshooting purposes, this method shows how many times the class* has re-initialized its <code>SecurityContextHolderStrategy</code>.* @return the count (should be one unless you've called* {@link #setStrategyName(String)} or* {@link #setContextHolderStrategy(SecurityContextHolderStrategy)} to switch to an* alternate strategy).*/public static int getInitializeCount() {return initializeCount;}/* Associates a new <code>SecurityContext</code> with the current thread of execution.* @param context the new <code>SecurityContext</code> (may not be <code>null</code>)*/public static void setContext(SecurityContext context) {strategy.setContext(context);}/* Changes the preferred strategy. Do <em>NOT</em> call this method more than once for* a given JVM, as it will re-initialize the strategy and adversely affect any* existing threads using the old strategy.* @param strategyName the fully qualified class name of the strategy that should be* used.*/public static void setStrategyName(String strategyName) {SecurityContextHolder.strategyName = strategyName;initialize();}/* Use this {@link SecurityContextHolderStrategy}. Call either {@link #setStrategyName(String)} or this method, but not both. This method is not thread safe. Changing the strategy while requests are in-flight* may cause race conditions. {@link SecurityContextHolder} maintains a static reference to the provided* {@link SecurityContextHolderStrategy}. This means that the strategy and its members* will not be garbage collected until you remove your strategy. To ensure garbage collection, remember the original strategy like so: <pre>* SecurityContextHolderStrategy original = SecurityContextHolder.getContextHolderStrategy();* SecurityContextHolder.setContextHolderStrategy(myStrategy);* </pre> And then when you are ready for {@code myStrategy} to be garbage collected you can* do: <pre>* SecurityContextHolder.setContextHolderStrategy(original);* </pre>* @param strategy the {@link SecurityContextHolderStrategy} to use* @since 5.6*/public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;SecurityContextHolder.strategy = strategy;initialize();}/* Allows retrieval of the context strategy. See SEC-1188.* @return the configured strategy for storing the security context.*/public static SecurityContextHolderStrategy getContextHolderStrategy() {return strategy;}/* Delegates the creation of a new, empty context to the configured strategy.*/public static SecurityContext createEmptyContext() {return strategy.createEmptyContext();}}
- MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,⼤家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实⾮常适合 web 应⽤,因为在默认情况下,⼀个请求⽆论经过多少 Filter 到达 Servlet,都是由⼀个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了⼦线程,在⼦线程中去获取登录⽤户数据,就会获取不到。
- MODE INHERITABLETHREADLOCAL:这种存储模式适⽤于多线程环境,如果希望在⼦线程中也能够获取到登录⽤户数据,那么可以使⽤这种存储模式。
- MODE GLOBAL:这种存储模式实际上是将数据保存在⼀个静态变量中,在 JavaWeb 开发中,这种模式很少使⽤到。
SecurityContextHolderStrategy
通过 SecurityContextHolder 可以得知, SecurityContextHolderStrategy 接⼝⽤来定义存储策略⽅法
public interface SecurityContextHolderStrategy {/* Clears the current context.*/void clearContext();/* Obtains the current context.*/SecurityContext getContext();/* Sets the current context.*/void setContext(SecurityContext context);/* Creates a new, empty context implementation, for use by*/SecurityContext createEmptyContext();}
从上⾯可以看出每⼀个实现类对应⼀种策略的实现。
获取用户数据
@GetMapping("/hello")public String hello() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();User user = (User) authentication.getPrincipal();return user.toString();}
多线程下获取用户数据
@GetMapping("/hello")public String hello() {new Thread(() -> {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();User user = (User) authentication.getPrincipal();System.out.println(user.toString());}).start();return "hello page success";}
可以看到默认策略,是⽆法在⼦线程中获取⽤户信息,如果需要在⼦线程中获取必须使⽤第⼆种策略,默认策略是通过 System.getProperty 加载的,因此我们可以通过增加 VM Options 参数进⾏修改。
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
⾃定义认证数据源
Servlet Authentication Architecture :: Spring Security
- 发起认证请求,请求中携带⽤户名、密码,该请求会被 UsernamePasswordAuthenticationFilter 拦截
- 在 UsernamePasswordAuthenticationFilter 的 attemptAuthentication ⽅法中将请求中⽤户名和密码,封装为Authentication 对象,并交给 AuthenticationManager 进⾏认证
- 认证成功,将认证信息存储到 SecurityContextHodler 以及调⽤记住我等,并回调 AuthenticationSuccessHandler 处理
- 认证失败,清除 SecurityContextHodler 以及 记住我中信息,回调 AuthenticationFailureHandler 处理
三者关系
从上⾯分析中得知, AuthenticationManager 是认证的核⼼类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider 。他们三者关系是样的呢?
- AuthenticationManager 是⼀个认证管理器,它定义了 Spring Security 过滤器要执⾏认证操作。
- ProviderManager AuthenticationManager接⼝的实现类。 Spring Security认证时默认使⽤就是 ProviderManager
- AuthenticationProvider 就是针对不同的身份类型执⾏的具体的身份认证。
AuthenticationManager 与 ProviderManager
ProviderManager 是 AuthenticationManager 的唯⼀实现,也是 Spring Security 默认使⽤实现。从这⾥不难看出默认情况下AuthenticationManager 就是⼀个ProviderManager。
ProviderManager 与 AuthenticationProvider
在 Spring Seourity 中,允许系统同时⽀持多种不同的认证⽅式,例如同时⽀持⽤户名/密码认证、 ReremberMe 认证、⼿机号码动态认证等,⽽不同的认证⽅式对应了不同的 AuthenticationProvider,所以⼀个完整的认证流程可能由多个AuthenticationProvider 来提供。
多个 AuthenticationProvider 将组成⼀个列表,这个列表将由 ProviderManager 代理。换句话说,在ProviderManager 中存在⼀个 AuthenticationProvider 列表,在Provider Manager 中遍历列表中的每⼀个 AuthenticationProvider 去执⾏身份认证,最终得到认证结果。
ProviderManager 本身也可以再配置⼀个 AuthenticationManager 作为 parent,这样当ProviderManager 认证失败之后,就可以进⼊到 parent 中再次进⾏认证。理论上来说, ProviderManager 的 parent 可以是任意类型的AuthenticationManager,但是通常都是由 ProviderManager 来扮演 parent 的⻆⾊,也就是 ProviderManager 是ProviderManager 的 parent。 ProviderManager 本身也可以有多个,多个ProviderManager 共⽤同⼀个 parent。有时,⼀个应⽤程序有受保护资源的逻辑组(例如,所有符合路径模式的⽹络资源,如/api!!*),每个组可以有⾃⼰的专⽤ AuthenticationManager。通常,每个组都是⼀个ProviderManager,它们共享⼀个⽗级。然后,⽗级是⼀种 全局资源,作为所有提供者的后备资源。
Getting Started | Spring Security Architecture
弄清楚认证原理之后我们来看下具体认证时数据源的获取。 默认情况下 AuthenticationProvider 是由 DaoAuthenticationProvider 类来实现认证的,在DaoAuthenticationProvider 认证时⼜通过 UserDetailsService 完成数据源的校验。 他们之间调⽤关系如下:
总结: AuthenticationManager 是认证管理器,在 Spring Security 中有全局 AuthenticationManager,也可以有局部AuthenticationManager。全局的 AuthenticationManager ⽤来对全局认证进⾏处理,局部的 AuthenticationManager ⽤来对某些特殊资源认证处理。当然⽆论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进⾏实现。 每⼀个ProviderManger 中都代理⼀个 AuthenticationProvider 的列表,列表中每⼀个实现代表⼀种身份认证⽅式。认证时底层数据源需要调⽤ UserDetailService 来实现
配置全局 AuthenticationManager
Getting Started | Spring Security Architecture
默认的全局 AuthenticationManager
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowiredpublic void initialize(AuthenticationManagerBuilder builder) {//builder..}
}
springboot 对 security 进行自动配置时自动在工厂中创建一个全局AuthenticationManager
总结
- 默认自动配置创建全局AuthenticationManager 默认找当前项目中是否存在自定义 UserDetailService 实例 自动将当前项目 UserDetailService 实例设置为数据源
- 默认自动配置创建全局AuthenticationManager 在工厂中使用时直接在代码中注入即可
自定义全局 AuthenticationManager
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overridepublic void configure(AuthenticationManagerBuilder builder) {//builder ....}
}
总结
- 一旦通过 configure 方法自定义 AuthenticationManager实现 就回将工厂中自动配置AuthenticationManager 进行覆盖
- 一旦通过 configure 方法自定义 AuthenticationManager实现 需要在实现中指定认证数据源对象 UserDetaiService 实例
- 一旦通过 configure 方法自定义 AuthenticationManager实现 这种方式创建AuthenticationManager对象工厂内部本地一个 AuthenticationManager 对象 不允许在其他自定义组件中进行注入
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {//1.自定义AuthenticationManager 推荐 并没有在工厂中暴露出来@Overridepublic void configure(AuthenticationManagerBuilder builder) throws Exception {System.out.println("自定义AuthenticationManager: " + builder);builder.userDetailsService(userDetailsService());}
//作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
自定义内存数据源
@Configuration
public class WebSecurityConfig {
@Beanpublic UserDetailsService userDetailsService(){UserDetails user = User.withUsername("admin").password("{noop}123").roles("ADMIN").build();return new InMemoryUserDetailsManager(user);}
@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/index").permitAll().anyRequest().authenticated().and().formLogin().successHandler(new LoginSuccessHandler()).failureHandler(new LoginFailureHandler()).and().logout().logoutSuccessHandler(new LogoutHandler()).and().userDetailsService(userDetailsService());return http.csrf().disable().build();}
}
自定义数据库数据源
-- 用户表
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 AUTO_INCREMENT=4 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 AUTO_INCREMENT=4 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 AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- 插入用户数据
BEGIN;INSERT INTO `user`VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);INSERT INTO `user`VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);INSERT INTO `user`VALUES (3, 'cheny', '{noop}123', 1, 1, 1, 1);
COMMIT;-- 插入角色数据
BEGIN;INSERT INTO `role`VALUES (1, 'ROLE_product', '商品管理员');INSERT INTO `role`VALUES (2, 'ROLE_admin', '系统管理员');INSERT INTO `role`VALUES (3, 'ROLE_user', '用户管理员');
COMMIT;-- 插入用户角色数据
BEGIN;INSERT INTO `user_role`VALUES (1, 1, 1);INSERT INTO `user_role`VALUES (2, 1, 2);INSERT INTO `user_role`VALUES (3, 2, 2);INSERT INTO `user_role`VALUES (4, 3, 3);
COMMIT;
项目中引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version>
</dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.29</version>
</dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.7</version>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
配置 springboot 配置文件
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/security?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2B8&allowMultiQueries=trueusername: rootpassword: root
mybatis:mapper-locations: mapper/*Mapper.xmltype-aliases-package: com.yang.entity
创建 entity
@Data
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<>();
@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> grantedAuthorities = new ArrayList<>();roles.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));return grantedAuthorities;}
@Overridepublic String getPassword() {return password;}
@Overridepublic String getUsername() {return username;}
@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}
@Overridepublic boolean isAccountNonLocked() {return accountNonLocked;}
@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}
@Overridepublic boolean isEnabled() {return enabled;}
}
@Data
public class Role {
private Integer id;private String name;private String nameZh;
}
创建 UserMapper 接口,编写sql语句
@Mapper
public interface UserMapper {
//根据用户名查询用户User loadUserByUsername(String username);
//根据用户id查询角色List<Role> getRolesByUid(Integer uid);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yang.mapper.UserMapper"><!--查询单个--><select id="loadUserByUsername" resultType="com.yang.entity.User">select id,username,password,enabled,accountNonExpired,accountNonLocked,credentialsNonExpiredfrom userwhere username = #{username}</select>
<!--查询指定行数据--><select id="getRolesByUid" resultType="com.yang.entity.Role">select r.id,r.name,r.name_zh nameZhfrom role r,user_role urwhere r.id = ur.ridand ur.uid = #{uid}</select>
</mapper>
创建 service
public interface UserService {
UserDetails loadUserByUsername(String username);
}
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Autowiredpublic UserServiceImpl(UserMapper userMapper) {this.userMapper = userMapper;}
@Overridepublic UserDetails loadUserByUsername(String username) {User user = userMapper.loadUserByUsername(username);if(ObjectUtils.isEmpty(user)){throw new RuntimeException("用户不存在");}user.setRoles(userMapper.getRolesByUid(user.getId()));return user;}
}
创建 UserDetailsService
@Component
public class UserDetailService implements UserDetailsService {
private final UserService userService;
public UserDetailService(UserService userService) {this.userService = userService;}
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return userService.loadUserByUsername(username);}
}
配置 authenticationManager 使用自定义UserDetailService
@Configuration
public class SecurityWebConfig {
private final UserDetailService userDetailService;
public SecurityWebConfig(UserDetailService userDetailService) {this.userDetailService = userDetailService;}
@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}
@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/index").permitAll().anyRequest().authenticated().and().formLogin().successHandler(new LoginSuccessHandler()).failureHandler(new LoginFailureHandler()).and().logout().logoutSuccessHandler(new LogoutHandler()) // 注销登入处理器.and().userDetailsService(userDetailService); // 自定义数据源return http.csrf().disable().build();}
}
添加验证码
<dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version>
</dependency>
生成验证码
@Configuration
public class KaptchaConfig {
@Beanpublic Producer kaptcha() {Properties properties = new Properties();properties.setProperty("kaptcha.image.width", "150");properties.setProperty("kaptcha.image.height", "50");properties.setProperty("kaptcha.textproducer.char.string", "0123456789");properties.setProperty("kaptcha.textproducer.char.length", "4");Config config = new Config(properties);DefaultKaptcha defaultKaptcha = new DefaultKaptcha();defaultKaptcha.setConfig(config);return defaultKaptcha;}
}
@RestController
public class KaptchaController {private final Producer producer;
public KaptchaController(Producer producer) {this.producer = producer;}
@GetMapping("/vc.png")public String getVerifyCode(HttpSession session) throws IOException {//1.生成验证码String code = producer.createText();session.setAttribute("kaptcha", code);//可以更换成 redis 实现BufferedImage bi = producer.createImage(code);//2.写入内存FastByteArrayOutputStream fos = new FastByteArrayOutputStream();ImageIO.write(bi, "png", fos);//3.生成 base64return Base64.encodeBase64String(fos.toByteArray());}
}
定义验证码异常类
public class KaptchaNotMatchException extends AuthenticationException {
public KaptchaNotMatchException(String msg) {super(msg);}
public KaptchaNotMatchException(String msg, Throwable cause) {super(msg, cause);}
}
在自定义LoginKaptchaFilter中加入验证码验证
/* @Author: chenyang* @DateTime: 2023/2/27 10:14* @Description: 自定义过滤器*/
public class LoginKaptchaFilter extends UsernamePasswordAuthenticationFilter {
public static final String FORM_CAPTCHA_KEY = "captcha";
private String kaptchaParameter = FORM_CAPTCHA_KEY;
public String getKaptchaParameter() {return kaptchaParameter;}
public void setKaptchaParameter(String kaptchaParameter) {this.kaptchaParameter = kaptchaParameter;}
@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<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);String kaptcha = userInfo.get(getKaptchaParameter());//用来获取数据中验证码String username = userInfo.get(getUsernameParameter());//用来接收用户名String password = userInfo.get(getPasswordParameter());//用来接收密码//2.获取 session 中验证码String sessionVerifyCode = (String) request.getSession().getAttribute(FORM_CAPTCHA_KEY);if (!ObjectUtils.isEmpty(kaptcha) && !ObjectUtils.isEmpty(sessionVerifyCode) &&kaptcha.equalsIgnoreCase(sessionVerifyCode)) {//3.获取用户名 和密码认证UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}} catch (IOException e) {e.printStackTrace();}throw new KaptchaNotMatchException("验证码不匹配!");}
}
配置
@Configuration
public class WebSecurityConfig {
private final UserDetailService userDetailService;
public WebSecurityConfig(UserDetailService userDetailService) {this.userDetailService = userDetailService;}
@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}
@Beanpublic LoginKaptchaFilter loginKaptchaFilter(AuthenticationManager authenticationManager) {LoginKaptchaFilter filter = new LoginKaptchaFilter();//1.认证 urlfilter.setFilterProcessesUrl("/doLogin");
//2.认证 接收参数filter.setUsernameParameter("username");filter.setPasswordParameter("pwd");filter.setKaptchaParameter("kaptcha");
//3.指定认证管理器filter.setAuthenticationManager(authenticationManager);
// 4.指定成功/失败时处理filter.setAuthenticationSuccessHandler(new LoginSuccessHandler());filter.setAuthenticationFailureHandler(new LoginFailureHandler());
return filter;}
@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests().mvcMatchers("/index", "/vc.png").permitAll().anyRequest().authenticated().and().formLogin().and().logout().logoutSuccessHandler(new LogoutHandler()) // 注销登入处理器.and().exceptionHandling().authenticationEntryPoint(new UnAuthenticationHandler()) // 未认证处理器.and().userDetailsService(userDetailService) // 自定义数据源.addFilterBefore(loginKaptchaFilter(http.getSharedObject(AuthenticationManager.class)), UsernamePasswordAuthenticationFilter.class); // 自定义过滤器return http.csrf().disable().build();}
}
自定义认证异常处理类
/* @Author: chenyang* @DateTime: 2023/2/27 11:27* @Description: 未认证时请求处理器*/
public class UnAuthenticationHandler implements AuthenticationEntryPoint {
@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpStatus.UNAUTHORIZED.value());response.getWriter().println("必须认证之后才能访问!");}
}
spring cloud security 中的 AuthenticationEntryPoint 设置与 AccessDeniedException 捕获过程 - 掘金
SpringSecurity系列 之 AuthenticationEntryPoint接口及其实现类的用法_oauth2authenticationentrypoint_姠惢荇者的博客-CSDN博客
测试验证
调用接口获取图片的Base64 编码,再将编码转换成图片
登入
调用获取验证码接口时会自动保存session