本人用的框架?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;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// /gAcDeptController/listString 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_noResponseData<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 {@Autowiredprivate 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;}}