Spring Security中自定义认证逻辑(防暴力破解)

2022-09-10 10:46:12

Spring Security中自定义认证逻辑(防暴力破解)

背景

项目开发时,需要对服务接口进行防暴力破解的防护,项目中使用的Spring Security没有对放暴力破解的支持,所以需要自己重写Spring Security中的认证逻辑来实现防暴力破解的能力。

软件版本

本次使用的软件版本如下:

Spring Boot2.6.7 (配套的Spring Security版本是5.6.3)

具体实现

下面是具体的实现案例,先简单梳理下实现方案。

实现方案

  1. 对于认证失败的用户IP,添加锁定机制,认证失败超过5次进行锁定操作。
  2. 默认锁定时间为30分钟,超过30分钟解除锁定。

基于servlet的应用和基于webflux的应用实现有点不太一样,这边都整理一下,不过差别也不大。

基于Servlet应用

新增Spring Security配置类,继承WebSecurityConfigurerAdapter适配类,同时需要添加@Configuration,@EnableWebSecurity注解:

/**
 * SpringSecurity 配置类
 *
 * @author yuanzhihao
 * @since 2022/6/8
 */@Configuration@EnableWebSecuritypublicclassWebSecurityConfigurationextendsWebSecurityConfigurerAdapter{}

重写WebSecurityConfigurerAdapter中configure(HttpSecurity http)方法,这边添加一个failureHandler的处理器,这个处理器可以自定义鉴权失败时的响应,不设置的话,默认是跳转到鉴权失败的页面,这边重写设置响应为json格式:

// 设置鉴权失败时响应json格式@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
    http.authorizeRequests().anyRequest().authenticated().and().formLogin().failureHandler((request, response, exception)->{
                response.setContentType("application/json;charset=utf-8");Map<String,Object> map=newHashMap<>();// 返回异常信息
                map.put("message", exception.getMessage());PrintWriter writer= response.getWriter();
                writer.write(newObjectMapper().writeValueAsString(map));
                writer.flush();
                writer.close();}).and().httpBasic();}

重写WebSecurityConfigurerAdapter中configure(AuthenticationManagerBuilder auth)方法,设置authenticationProvider属性:

@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth){// 添加自定义认证逻辑
    auth.authenticationProvider(newCustomAuthenticationProvider(username, password));}

authenticationProvider方法中,需要提供一个AuthenticationProvider接口的实现类,该接口有如下几个需要重写的方法:

publicinterfaceAuthenticationProvider{// 对传入的authentication对象进行验证 这边主要是重写的校验逻辑Authenticationauthenticate(Authentication authentication)throwsAuthenticationException;// 判断当前的AuthenticationProvider是否支持对应的Authenticationbooleansupports(Class<?> authentication);}

CustomAuthenticationProvider中实现了自定义认证的逻辑,这边采用了guava中提供的Cache类进行缓存的管理,具体代码如下:

/**
 * 自定义认证逻辑
 *
 * @author yuanzhihao
 * @since 2022/6/8
 */@Slf4jpublicclassCustomAuthenticationProviderimplementsAuthenticationProvider{privatefinalString username;privatefinalString password;// 缓存 记录某个IP地址basic认证失败的次数privateCache<Object,Integer> basicAuthCache;// 最大重试次数 5次privatefinalint maxFailedTimes=5;// 锁定时间 30分钟privatefinalDuration lockDuration=Duration.ofMinutes(30);publicCustomAuthenticationProvider(String username,String password){this.username= username;this.password= password;// 初始化缓存和定时任务this.setUp();}// 自定义认证中添加防暴力破解机制@OverridepublicAuthenticationauthenticate(Authentication authentication)throwsAuthenticationException{String remoteIp=remoteIp();// 校验ip是否被锁定checkRemoteIpBlocked(remoteIp);// 检查账号密码是否合法String username=(String) authentication.getPrincipal();String password=(String) authentication.getCredentials();checkAccountCorrect(username, password, remoteIp);// 鉴权成功处理authSuccess(remoteIp);returnnewUsernamePasswordAuthenticationToken(username, password,Collections.singletonList(newSimpleGrantedAuthority("ROLE_admin")));}// 支持验证的方式@Overridepublicbooleansupports(Class<?> authentication){returnUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}privatevoidsetUp(){// 设置缓存 超时时间设置为30分钟 并且添加remove监听this.basicAuthCache=CacheBuilder.newBuilder().expireAfterWrite(lockDuration).removalListener(notification->{// 超时移除黑名单时添加日志打印if(notification.getCause()==RemovalCause.EXPIRED&&(int) notification.getValue()>= maxFailedTimes){
                log.warn("Remote IP [{}] removed form auth black list automatically",remoteIp());}}).build();// 添加一个定时任务 每天清理一次所有的缓存数据ScheduledExecutorService scheduledExecutorService=Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleWithFixedDelay(()-> basicAuthCache.cleanUp(),1,1,TimeUnit.DAYS);}privatevoidcheckRemoteIpBlocked(String remoteIp){int failedTimes=Optional.ofNullable(basicAuthCache.getIfPresent(remoteIp)).orElse(0);if(failedTimes>= maxFailedTimes){
            log.error("Current IP [{}] are blocked, please try again later", remoteIp);thrownewLockedException("Current IP is blocked");}}// 登录成功之后 清空缓存中的数据privatevoidauthSuccess(String remoteIp){int failedTimes=Optional.ofNullable(basicAuthCache.getIfPresent(remoteIp)).orElse(0);if(failedTimes>=1){
            log.info("IP [{}] Unlocked after auth success", remoteIp);}
        basicAuthCache.invalidate(remoteIp);}privateStringauthFailed(String remoteIp){String res;int failedTimes=Optional.ofNullable(basicAuthCache.getIfPresent(remoteIp)).orElse(0);if(++failedTimes>= maxFailedTimes){
            res="Auth failed and Current IP has been locked";
            log.error(res);}else{// 剩余重试次数int leftTimes= maxFailedTimes- failedTimes;
            res="Auth failed and has "+ leftTimes+" chance left";}return res;}privatevoidcheckAccountCorrect(String username,String password,String remoteIp){if(StringUtils.isEmpty(username)||StringUtils.isEmpty(password)){
            log.error("username or password is null");thrownewBadCredentialsException("username or password is null");}if(!(StringUtils.equals(username,this.username)&&StringUtils.equals(password,this.password))){thrownewBadCredentialsException(authFailed(remoteIp));}}privateStringremoteIp(){ServletRequestAttributes attributes=(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();if(attributes==null){return"";}return attributes.getRequest().getRemoteAddr();}}

访问服务接口,输入错误的鉴权账号和密码,页面响应剩余重试次数:
在这里插入图片描述
当重试次数超过5次,提示当前IP被锁定:
在这里插入图片描述

基于Webflux应用

基于webflux的应用和servlet实现思路类似,主要就是一些api的区别。因为我们做了zuul到gateway的切换,所以迫不得已研究了spring security基于webflux应用的相关用法~~~

首先还是新增Spring Security配置类,不过不用继承WebSecurityConfigurerAdapter这个类了,webflux中貌似没有提供类似的适配类,添加@EnableWebFluxSecurity这个注解就行:

/**
 * SpringSecurity 配置类
 *
 * @author yuanzhihao
 * @since 2022/6/8
 */@EnableWebFluxSecuritypublicclassWebSecurityConfiguration{}

注入一个SecurityWebFilterChain类型的bean,这边会设置authenticationManager以及authenticationFailureHandler,分别是自定义的认证逻辑以及鉴权失败处理器:

@BeanpublicSecurityWebFilterChainsecurityWebFilterChain(ServerHttpSecurity http){
    http.authorizeExchange().anyExchange().authenticated().and()// 设置自定义鉴权处理.authenticationManager(newCustomReactiveAuthenticationManager(username, password)).formLogin().authenticationFailureHandler(newServerAuthenticationFailureHandler(){// 设置鉴权失败时响应json格式@SneakyThrows@OverridepublicMono<Void>onAuthenticationFailure(WebFilterExchange webFilterExchange,AuthenticationException exception){ServerHttpResponse response= webFilterExchange.getExchange().getResponse();
                    response.getHeaders().add("Content-Type","application/json;charset=UTF-8");Map<String,Object> map=newHashMap<>();// 返回异常信息
                    map.put("message", exception.getMessage());String value=newObjectMapper().writeValueAsString(map);DataBuffer dataBuffer= response.bufferFactory().wrap(value.getBytes(StandardCharsets.UTF_8));return response.writeWith(Mono.just(dataBuffer));}}).and().httpBasic();return http.build();}

设置的authenticationManager方法中,需要提供一个ReactiveAuthenticationManager接口的实现类,这个接口只有一个方法authenticate(Authentication authentication),就是需要重写的校验的逻辑:

@FunctionalInterfacepublicinterfaceReactiveAuthenticationManager{/**
	 * Attempts to authenticate the provided {@link Authentication}
	 * @param authentication the {@link Authentication} to test
	 * @return if authentication is successful an {@link Authentication} is returned. If
	 * authentication cannot be determined, an empty Mono is returned. If authentication
	 * fails, a Mono error is returned.
	 */Mono<Authentication>authenticate(Authentication authentication);}

自定义CustomReactiveAuthenticationManager实现和上面基本一致,大家可以参考下最后我贴的源码,这边就不再贴代码了。不过有一点要注意,就是在webflux项目中时没有RequestContextHolder这个类的,所以我们没有办法在全局获取到当前的request和response的信息,就无法获取IP。

参考了部分网络上面的文章,可以采用Reactor中提供的一个Context来实现类似ThreadLocal的功能,但是尝试了一下,貌似在@Controller里面可以生效,但是在别的地方获取时会报Context is empty错误,暂时还未清楚原因,感觉是设置的context和获取的context不是同一个,如果有知道的大佬希望可以指导下(抱拳)

结语

Spring Security比较复杂,我这边整理的可能不一定正确,如果有不对的地方,还希望大家能够指正!

参考:

https://www.javaboy.org/2020/0503/custom-authentication.html

https://github.com/spring-projects/spring-framework/issues/20239

代码地址:

https://github.com/yzh19961031/SpringCloudDemo/tree/main/security-servlet

https://github.com/yzh19961031/SpringCloudDemo/tree/main/security-webflux

  • 作者:洒脱的智障
  • 原文链接:https://blog.csdn.net/qq_32238611/article/details/125238305
    更新时间:2022-09-10 10:46:12