最近有个小需求:实现一个IP白名单的功能;指定ip可以无需登录认证即可访问指定的接口,非指定ip需要登录认证才能访问;
安全框架用spring security+jwt,安全配置类securityConfig继承WebSecurityConfigurerAdapter中可以配置hasIpaddress白名单;
但这个只能在初始化项目时通过字符串拼接access参数,没有达到ip动态添加删除的效果;
随后在百度了N篇文章后,参考动态url权限配置,自定义了一个决策器实现了功能,代码如下:
import com.hy.common.core.redis.RedisCache;
import com.hy.openapi.apimanger.config.APIConstant;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Resource
private RedisCache redisCache;
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
FilterInvocation fi = (FilterInvocation) o;
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 访问的ip
String ipUrl = fi.getHttpRequest().getRemoteAddr();
// 访问的接口地址
String requestUrl = fi.getRequestUrl();
// 获取白名单
List<String> ipWhiteList = redisCache.getCacheList(APIConstant.IP_WHITE_LIST);
// 判断是否是api接口
if (antPathMatcher.match("/api/**",requestUrl)){
// 判断是否在白名单中
if (!ipWhiteList.contains(ipUrl)) {
// 判断是否登录认证
if (authentication.getPrincipal().equals("anonymousUser")){
throw new AccessDeniedException("认证失败");
}
}
}
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
再在配置类中配置:
@Resource
private CustomAccessDecisionManager customAccessDecisionManager;
...
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(customAccessDecisionManager);
return o;
}
})
// 对于登录login 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/captchaImage").anonymous()
采用了redis存储、取出数据,避免重复查询数据库;
在项目初始化的时候将数据初始化进去
@Override
public void run(ApplicationArguments args) {
System.out.println("==========项目启动成功,开始初始化api接口、初始化IP白名单=============");
List<String> ipWhiteList = ipWhiteListConfig.getIpWhiteList();
// 将白名单放入redis
redisCache.setCacheList(APIConstant.IP_WHITE_LIST,ipWhiteList);
}
前台添加ip或删除ip的时候同时将redis里的数据更新下即可;
如果有大佬有更好的方案,还希望分享下。