本人用的框架 SpringCloud+ redis+Oauth2+Security
前言: 整体使用过滤器的思想,获取Request,然后从数据库查到菜单名称和路由以及计算点击次数,最后以list的形式存在redis,设计定时任务,在一定时间后,将redis的数据存在数据库(mysql或者oracle)中。
设计时出现的问题(必看!!):
(一)、因为是微服务框架,所以在设计时想到的是在GateWay使用GlobalFilter对所有服务的请求进行拦截,但是有一个问题是,因为GateWay的pom文件依赖不允许有spring-web也就没办法使用fegin或者其他方式查询数据库,也就获取不到菜单的信息,所以舍弃了这种方法
(二)、那就使用基础模块,让每个服务都去依赖这个模块,就变相的达到了,控制每一个服务的方式。那又没办法想GateWay那样直接实现GlobalFilter拦截所有请求,但是又想到可以将拦截器加在security里,等每次认证结束后,经过过滤器,对请求进行处理,这样就达到了所有的目的
(三)、如果您只是单体的springBoot项目,那就更简单了,直接实现HandlerInterceptor,然后加到bean里让spring管理
一、先写拦截器内容
@Slf4j
public class UserFavoriteFunctionFilter extends OncePerRequestFilter {
// 排除过滤的 uri 地址,nacos自行添加
private final IgnoreWhiteProperties ignoreWhite;
public UserFavoriteFunctionFilter(IgnoreWhiteProperties ignoreWhite) {
this.ignoreWhite = ignoreWhite;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// /gAcDeptController/list
String urlPath = request.getRequestURI();
log.info("Absolute path:{}", urlPath);
// 跳过不需要统计的路径
List<String> whites = ignoreWhite.getWhites();
if (CollUtil.isEmpty(whites)){
filterChain.doFilter(request, response);
return;
}
if (StringUtils.matches(urlPath, whites)) {
log.info("Skip path:{}", urlPath);
filterChain.doFilter(request, response);
return;
}
RemoteSystemService remoteSystemService = SpringUtils.getBean(RemoteSystemService.class);
RedisService redisService = SpringUtils.getBean(RedisService.class);
String prefixKey = "userFavorite:";
BigDecimal userId = SecurityUtils.getUserId();
// 获取uri的前半部分
String[] split = urlPath.split("/");
String ControllerPath = split[1]; // gAcDeptController
// 从 G_AC_PERMISSION 查出当前菜单的 perm_no
ResponseData<String> data = remoteSystemService.getPermNo(ControllerPath);
if (ObjectUtil.isNull(data)){
filterChain.doFilter(request, response);
return;
}
String permNo = data.getData();
// 从redis查询当前的用户菜单点击量
String key = prefixKey+userId;
List<clickCountVo> clickCountVos = redisService.getCacheList(key);
if (CollUtil.isNotEmpty(clickCountVos)){
Map<String, clickCountVo> clickCountMap = clickCountVos.stream()
.collect(Collectors.toMap(
clickCountVo::getName, // 键映射函数
vo -> vo // 值映射函数,直接使用对象本身
));
clickCountVo clickCountVo = clickCountMap.get(permNo);
if (ObjectUtil.isNotNull(clickCountVo)) {
// 当前的点击量
BigDecimal count = clickCountVo.getCount();
AtomicLong atomicLong = new AtomicLong(count.longValue());
long l = atomicLong.incrementAndGet();
clickCountVo.setCount(new BigDecimal(l));
clickCountVo.setTime(new Date());
}else {
clickCountVo clickVo = new clickCountVo();
clickVo.setName(permNo);
clickVo.setTime(new Date());
clickVo.setCount(BigDecimal.ONE);
clickCountVos.add(clickVo);
}
}else {
clickCountVo countVo = new clickCountVo();
countVo.setName(permNo);
countVo.setTime(new Date());
countVo.setCount(BigDecimal.ONE);
clickCountVos.add(countVo);
}
redisService.deleteObject(key);
redisService.setCacheList(key, clickCountVos);
filterChain.doFilter(request, response);
}
}
二、创建一个Vo保存菜单信息和点击量
@Data
public class clickCountVo {
private String name;
private BigDecimal count;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date time;
}
三、有一些路径我们不需要拦截的,可以在nacos配置一下
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties {
private List<String> whites = new ArrayList<>();
public List<String> getWhites()
{
return whites;
}
public void setWhites(List<String> whites)
{
this.whites = whites;
}
}
四、最重要的,将我们自定义的拦截器加到Scurity里,这里还搭配了Oauth2.0
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
AntPathRequestMatcher[] requestMatchers = permitAllUrl.getUrls()
.stream()
.map(AntPathRequestMatcher::new)
.toList()
.toArray(new AntPathRequestMatcher[] {});
http.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers(requestMatchers).permitAll().anyRequest()
.authenticated())
.oauth2ResourceServer(
oauth2 -> oauth2
.authenticationEntryPoint(resourceAuthExceptionEntryPoint)
.bearerTokenResolver(starBearerTokenExtractor).jwt())
.addFilterAfter(new UserFavoriteFunctionFilter(whiteProperties),BearerTokenAuthenticationFilter.class)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
.addFilterAfter(new UserFavoriteFunctionFilter(whiteProperties),BearerTokenAuthenticationFilter.class) 这个是添加我们自定义的过滤器的,熟悉Oauth2.0认证的都熟悉BearerTokenAuthenticationFilter,这个过滤器是当用户每一次想资源服务器请求时都会经过的过滤器,这个过滤器也负责处理token以及将用户认证信息存到SecurityContextHolder中,所以我们在这个过滤器后面加上我们自定义的过滤器UserFavoriteFunctionFilter(这个说起来都是泪,我一点一点debug后才发现addFilterAfter这个方法)
然后你就找一个既又redis又有springWeb依赖的公共模块,将代码放进去就行了。
后面还有一个定时任务的功能,这个主要是为了防止redis数据太多,我们公司是TOB,基本没有什么用户量,也没有高并发什么的,暂时就没有写这个功能。
附带两个查询的方法,返回前端展示
@RestController
@RequestMapping("/userClickController")
@Tag(name = "获取用户常用菜单功能")
public class UserClickController {
@Autowired
private RedisService redisService;
@GetMapping("/getTop10Info")
@Operation(summary = "获取点击量最多的前10个菜单信息")
public List<clickCountVo> getTop10Info(){
String key = "userFavorite:";
BigDecimal userId = SecurityUtils.getUserId();
key = key+userId;
List<clickCountVo> cacheList = redisService.getCacheList(key);
// 按照点击量排序。如果点击量一样就按照时间排序 都是降序
return cacheList.stream()
.sorted(Comparator.comparing(clickCountVo::getCount).reversed().thenComparing(Comparator.comparing(clickCountVo::getTime).reversed()))
.limit(10)
.collect(Collectors.toList());
}
@GetMapping("/getLastWeekInfo")
@Operation(summary = "获取最近一周点击量的菜单信息")
public List<clickCountVo> getLastWeekInfo(){
String key = "userFavorite:";
BigDecimal userId = SecurityUtils.getUserId();
key = key+userId;
List<clickCountVo> cacheList = redisService.getCacheList(key);
if (CollUtil.isNotEmpty(cacheList)){
// 获取上一周的时间
DateTime dateTime = DateUtil.lastWeek();
// 按照点击量排序。如果点击量一样就按照时间排序 都是降序
return cacheList.stream()
.filter(da -> da.getTime().toInstant().isAfter(dateTime.toInstant()))
.sorted(Comparator.comparing(clickCountVo::getCount).reversed().thenComparing(Comparator.comparing(clickCountVo::getTime).reversed()))
.collect(Collectors.toList());
}
return cacheList;
}
}