社区系统项目复盘-7

news2024/9/21 22:53:59

文章目录

      • Spring Security
      • 权限控制
      • 置顶、加精、删除
      • Redis高级数据类型
      • 网站数据统计

使用Spring Security进行权限控制,对登录检查功能进行了重写。对不同的登录账号授予不同的权限,实现了置顶、加精、删除功能。使用Redis高级数据类型HyperLogLog和Bitmap实现了网站UV(独立访客)和DAU(日活跃用户)数据的统计。

Spring Security

  • 简介:Spring Security是一个专注于为Java应用程序提供身份认证和授权的框架,它的强大之处在于它可以轻松扩展以满足自定义的需求。

  • Spring Security底层是基于Filter实现的,Filter与 DispatcherServlet的关系类似于Interceptor与Controller的关系。

    https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/943A9F07-3719-43B2-8785-FD3439CD0658/3B033DBC-6082-41F2-B78A-FF10FCF38070_2/QyH59d1fppA6yfAdoEgdJlGHBwxWFc8Sv3wx0dcMhSwz/Image.png

  • 特征:

    • 对身份的认证和授权提供全面的、可扩展的支持。
    • 防止各种攻击,如会话固定攻击、点击劫持、csrf攻击等。
    • 支持与Servlet API、Spring MVC等Web技术集成。
  • 使用(以登录验证为例)

    • 导入依赖spring-boot-starter-security,添加完该依赖以后,spring security就已经生效了,重启项目,再次打开首页,会自动跳转到一个登录页面,这个登录页面是Spring security自带的,我们可以用自己写的登录页面替换它。Spring security会自动生成一个用户和密码,用于默认登录页面的验证。

      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      
    • 对实体类进行处理,让跟登录相关的实体类实现UserDetails接口中的一些方法,(isAccountNonExpired:判断账号是否过期;isAccountNonLocked:判断账号是否锁定;isCredentialsNonExpired:判断凭证是否过期;isEnabled:判断账号是否可用;getAuthorities:获得用户拥有的权限)

      // 示例
      public class User implements UserDetails {
      
          private int id;
          private String username;
          private String password;
          private String salt;
          private String email;
          private int type;
          private int status;
          private String activationCode;
          private String headerUrl;
          private Date createTime;
      
      	// 此处省略 get方法、set方法、toString方法
      
          // true : 账号未过期
          @Override
          public boolean isAccountNonExpired() {
              return true;
          }
      
          // true : 账号未锁定
          @Override
          public boolean isAccountNonLocked() {
              return true;
          }
      
          // true: 凭证未过期
          @Override
          public boolean isCredentialsNonExpired() {
              return true;
          }
      
          // true: 账号可用
          @Override
          public boolean isEnabled() {
              return true;
          }
      
          // 权限
          @Override
          public Collection<? extends GrantedAuthority> getAuthorities() {
              List<GrantedAuthority> list = new ArrayList<>();
              list.add(new GrantedAuthority() {
                  @Override
                  public String getAuthority() {
                      switch (type){
                          case 1:
                              return "ADMIN";
                          default:
                              return "USER";
                      }
                  }
              });
              return list;
          }
      }
      
    • 将业务层的相关类实现UserDetailsService接口中的loadUserByUsername方法。

      // 示例
      @Service
      public class UserService implements UserDetailsService {
      
          @Autowired
          private UserMapper userMapper;
      
          public User findUserByName(String username) {
              return userMapper.selectByName(username);
          }
      
          @Override
          public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
              return this.findUserByName(username);
          }
      }
      
    • 配置SecurityConfig配置类,该类继承WebSecurityConfigurerAdapter,重写3个config方法。包括认证的逻辑(自定义认证规则),授权的逻辑(登录相关的配置,退出相关的配置,授权配置,增加Filter,添加记住我功能等)。

      // 示例
      @Configuration
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
          @Autowired
          private UserService userService;
      
          @Override
          public void configure(WebSecurity web) throws Exception {
              // 忽略静态资源的访问
              web.ignoring().antMatchers("/resources/**");
          }
      
          // 认证的逻辑
          // AuthenticationManager:认证的核心接口。
          // AuthenticationManagerBuilder:用于构建AuthenticationManager对象的工具。
          // ProviderManager:AuthenticationManager接口的默认实现类。
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              // 内置的认证规则
              // auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));
      
              // 自定义认证规则
              // AuthenticationProvider:ProviderManager持有一组AuthenticationProvider,每个AuthenticationProvider负责一种认证。
              // 委托模式:ProviderManager将认证委托给AuthenticationProvider。
              auth.authenticationProvider(new AuthenticationProvider() {
                  // Authentication:用于封装认证信息的接口,不同的实现类代表不同类型的认证信息。
                  @Override
                  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                      String username = authentication.getName();
                      String password = (String) authentication.getCredentials();
      
                      User user = userService.findUserByName(username);
      
                      if(user == null){
                          throw new UsernameNotFoundException("账号不存在!");
                      }
      
                      password = CommunityUtil.md5(password+user.getSalt());
                      if(!user.getPassword().equals(password)){
                          throw new BadCredentialsException("密码不正确!");
                      }
      
                      // principal:认证的主要信息; credentials:证书; authorities:权限
                      return new UsernamePasswordAuthenticationToken(user,user.getPassword(),user.getAuthorities());
                  }
      
                  //当前的AuthenticationProvider支持哪种类型的认证。
                  @Override
                  public boolean supports(Class<?> aClass) {
                      // UsernamePasswordAuthenticationToken:Authentication接口的常用的实现类,账号密码认证。
                      return UsernamePasswordAuthenticationToken.class.equals(aClass);
                  }
              });
          }
      
          // 授权的逻辑
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              // 登录相关的配置
              http.formLogin()
                      .loginPage("/loginpage")
                      .loginProcessingUrl("/login")
                      .successHandler(new AuthenticationSuccessHandler() {
                          @Override
                          public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                              // 重定向
                              response.sendRedirect(request.getContextPath()+"/index");
                          }
                      })
                      .failureHandler(new AuthenticationFailureHandler() {
                          @Override
                          public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                              // 转发
                              request.setAttribute("error",e.getMessage());
                              request.getRequestDispatcher("/loginpage").forward(request,response);
                          }
                      });
      
              // 退出相关配置
              http.logout()
                      .logoutUrl("/logout")
                      .logoutSuccessHandler(new LogoutSuccessHandler() {
                          @Override
                          public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                              response.sendRedirect(request.getContextPath() + "/index");
                          }
                      });
      
              // 授权配置
              http.authorizeRequests()
                      .antMatchers("/letter").hasAnyAuthority("USER","ADMIN")
                      .antMatchers("/admin").hasAnyAuthority("ADMIN")
                      .and().exceptionHandling().accessDeniedPage("/denied");
      
              // 增加Filter,处理验证码
              http.addFilterBefore(new Filter() {
                  @Override
                  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                      HttpServletRequest request = (HttpServletRequest) servletRequest;
                      HttpServletResponse response = (HttpServletResponse) servletResponse;
                      if(request.getServletPath().equals("/login")){
                          String verifyCode = request.getParameter("verifyCode");
                          System.out.println("验证码:" + verifyCode);
                          if(verifyCode == null || !verifyCode.equalsIgnoreCase("1234")){
                              request.setAttribute("error","验证码错误!");
                              request.getRequestDispatcher("/loginpage").forward(request,response);
                              return ;
                          }
                      }
                      // 让请求继续向下执行
                      filterChain.doFilter(request,response);
                  }
              }, UsernamePasswordAuthenticationFilter.class);
      
              // 记住我
              http.rememberMe()
                      .tokenRepository(new InMemoryTokenRepositoryImpl())
                      .tokenValiditySeconds(3600 * 24)
                      .userDetailsService(userService);
          }
      }
      

      注意⚠️
      认证成功后,结果会通过SecurityContextHolder存入SecurityContext中,访问相关信息的时候,需要SecurityContextHolder。
      Security规定退出登录功能的请求方式必须是post。
      记住我功能前端代码部分name必须是 remember-me。

      重定向与转发示意图:
      https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/943A9F07-3719-43B2-8785-FD3439CD0658/C96EB6FA-121D-4E74-B894-C89DBAA35665_2/U667fU79mftGtD6xOZRkdiJDN7exIBPAF6g4PQVWJHAz/Image.png

权限控制

  • 登录检查:之前采用拦截器实现了登录检查,这是简单的权限管理方案,现将其废弃。

    实现:将WebMvcConfig配置类中跟登录检查相关的代码注释掉。

  • 授权配置:对当前系统内包含的所有的请求,分配访问权限(普通用户user,版主moderator,管理员admin)。

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/resources/**");
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 授权
            http.authorizeRequests()
                    .antMatchers(
                            "/user/setting",
                            "/user/upload",
                            "/discuss/add",
                            "/comment/add/**",
                            "/letter/**",
                            "/notice/**",
                            "/like",
                            "/follow",
                            "/unfollow"
                    ).hasAnyAuthority(
                            AUTHORITY_USER,
                            AUTHORITY_ADMIN,
                            AUTHORITY_MODERATOR
                    ).antMatchers(
                            "/discuss/top",
                            "/discuss/wonderful"
                    )
                    .hasAnyAuthority(
                            AUTHORITY_MODERATOR
                    ).antMatchers(
                            "/discuss/delete",
                            "/data/**",
                            "/actuator/**"
                    )
                    .hasAnyAuthority(
                            AUTHORITY_ADMIN
                    )
                    .anyRequest().permitAll()
                    .and().csrf().disable();
    
            // 权限不够时的处理
            http.exceptionHandling()
                    .authenticationEntryPoint(new AuthenticationEntryPoint() {
                        // 没有登录时的处理
                        @Override
                        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                            String xRequestedWith = request.getHeader("x-requested-with");
                            if("XMLHttpRequest".equals(xRequestedWith)){
                                // 异步时的处理
                                response.setContentType("application/plain;charset=utf-8");
                                PrintWriter writer = response.getWriter();
                                writer.write(CommunityUtil.getJSONString(403,"你还没有登录哦!"));
                            }else{
                                response.sendRedirect(request.getContextPath() + "/login");
                            }
                        }
                    })
                    .accessDeniedHandler(new AccessDeniedHandler() {
                        // 权限不足时的处理
                        @Override
                        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
                            String xRequestedWith = request.getHeader("x-requested-with");
                            if("XMLHttpRequest".equals(xRequestedWith)){
                                // 异步时的处理
                                response.setContentType("application/plain;charset=utf-8");
                                PrintWriter writer = response.getWriter();
                                writer.write(CommunityUtil.getJSONString(403,"你没有访问此功能的权限!"));
                            }else{
                                response.sendRedirect(request.getContextPath() + "/denied");
                            }
                        }
                    });
    
            // Security底层默认会拦截/logout请求,进行退出处理.
            // 覆盖它默认的逻辑,才能执行我们自己的退出代码。
            http.logout().logoutUrl("/securitylogout");
        }
    }
    
  • 认证方案:绕过Security认证流程,采用系统原来的认证方案。

    在UserService业务层代码中对user进行授权

    public Collection<? extends GrantedAuthority> getAuthorities(int userId){
        User user = this.findUserById(userId);
        List<GrantedAuthority> list = new ArrayList<>();
        list.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                switch (user.getType()){
                    case 1:
                        return AUTHORITY_ADMIN;
                    case 2:
                        return AUTHORITY_MODERATOR;
                    default:
                        return AUTHORITY_USER;
                }
            }
        });
        return list;
    }
    

    因为绕过了Security的认证流程(Serucity认证时会将认证结果存入SecurityContext中),所要构建用户认证的结果,并存入SecurityContext,以便于Security进行授权。在检查登录凭证的拦截器preHandle()方法中处理这一块逻辑。

    // 构建用户认证的结果,并存入SecurityContext,以便于Security进行授权。
    Authentication authentication = new UsernamePasswordAuthenticationToken(
            user,user.getPassword(),userService.getAuthorities(user.getId()));
    SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
    

    请求结束时,将SecurityContext中保存的权限进行清理,实现:在拦截器afterCompletion()方法以及退出登录时,删除用户认证结果。

    SecurityContextHolder.clearContext();
    
  • CSRF配置:防止CSRF攻击的基本原理,以及表单、AJAX相关的配置。

    CSRF:Cross Site Request Forgery( 跨站请求伪造)

    **CSRF攻击:**大部分的网站应用都是采用cookie或session的方式进行登入验证,当通过登入验证之后,网站就会给你一个通行证存在cookie或seesion中,代表之后的动作中都不需要重复验证身份了。当在登录一个网站以后,中途去逛了其他网页,刚好遇到了恶意网页,窃取了cookies,那么它就可以凭借该身份访问网站,这就是CSRF攻击。

    防御CSRF的方式有两种,1.检查referer栏位,http协定中就有一个referer栏位记录着这个请求是从哪个网站发出来的,从而确认请求来源。2.加入验证token,这个token由服务端产生,加密存在session中,无法仿造。Spring Security就是采用的第二种方法。

    ⚠️:Spring Security默认防御CSRF攻击,但是对于异步请求不可以,因为异步请求根本不提交表单数据,因此,我们需要单独处理。

    发布帖子就是一个异步请求,以此为例:

  • 通过标签要求服务器把csrf凭证生成在header里,发请求的时候直接从header里取。

    <!-- 访问该页面时,在此处生成CSRF令牌. -->
    <meta name="_csrf" th:content="${_csrf.token}">
    <meta name="_csrf_header" th:content="${_csrf.headerName}">
    
  • 发送AJAX请求之前,将CSRF令牌设置到请求消息头中。

    // 发送AJAX请求之前,将CSRF令牌设置到请求消息头中.
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function (e,xhr,options){
    	xhr.setRequestHeader(header,token);
    });
    

    当Spring Security启用防御CSRF后,对于所有的异步请求都需要处理。也可以禁用security防御CSRF攻击的功能,在授权时加上 .and().csrf().disable(); 即可。

    https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/943A9F07-3719-43B2-8785-FD3439CD0658/905D6E0E-38C2-48B0-8562-95B008A1F6F3_2/Fs2FdoqqTybTC5KxAdFxmzgKH4ldDSgTEdy6ENHqx0Ez/Image.png

置顶、加精、删除

  • 功能实现

    • 点击 “置顶”,修改帖子的类型。0-普通、1-置顶
    • 点击 “加精”、“删除”、修改帖子的状态。 0-正常、1-精华、2-拉黑
  • 权限管理

    • 版主可以执行“置顶”、“加精”操作
    • 管理员可以执行“删除”操作
  • 按钮显示

    • 版主可以看到“置顶”、“加精”按钮
    • 管理员可以看到“删除”按钮

    https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/943A9F07-3719-43B2-8785-FD3439CD0658/AF928AB6-2E26-44E5-93D5-65ED8DD58AB3_2/m8xWcIpO1paJRtfy2zcDufmzIMQqGzwXT9reamR47eQz/Image.png

Redis高级数据类型

  • HyperLogLog(超级日志),可以对数据统计,也可以对数据合并

    • 采用一种基数算法,用于完成独立总数的统计。
    • 占据空间小,无论统计多少个数据,只占12K的内存空间。
    • 不精确的统计算法,标准误差为0.81%。
    // 示例
    // 添加数据
    redisTemplate.opsForHyperLogLog().add(redisKey,value);
    
    // 统计数据
    Long size = redisTemplate.opsForHyperLogLog().size(redisKey);
    
    // 合并数据
    redisTemplate.opsForHyperLogLog().union(unionKey,redisKey2,redisKey3,redisKey4);
    
  • Bitmap(位图),可以统计一组数据的布尔值

    • 不是一种独立的数据结构, 实际上就是字符串。
    • 支持安慰存取数据,可以将其看成是byte数组。
    • 适合存储检索大量的连续的数据的布尔值。
    // 示例
    // 记录
    redisTemplate.opsForValue().setBit(redisKey,index,value); // value:true or false
    
    // 查询
    System.out.println(redisTemplate.opsForValue().getBit(redisKey,index));
    
    // 统计
    Object obj = redisTemplate.execute(new RedisCallback() {
        @Override
        public Object doInRedis(RedisConnection connection) throws DataAccessException {
            return connection.bitCount(redisKey.getBytes());
        }
    });
    
    System.out.println(obj);
    
    // 运算 以or为例
    Object obj = redisTemplate.execute(new RedisCallback() {
        @Override
        public Object doInRedis(RedisConnection connection) throws DataAccessException {
            connection.bitOp(RedisStringCommands.BitOperation.OR,redisKey.getBytes(),
                    redisKey2.getBytes(),redisKey3.getBytes(),redisKey4.getBytes());
            return connection.bitCount(redisKey.getBytes());
        }
    });
    
    System.out.println(obj);
    

网站数据统计

  • UV(Unique Visitor)

    • 独立访客,需通过用户IP排重统计数据。为什么用IP而不是用userId统计呢,因为我们希望将非注册用户(游客)也统计进来。
    • 每次访问都要进行统计。
    • HyperLogLog,性能好,且存储空间小。
  • DAU(Daily Active User)

    • 日活跃用户,需通过用户ID排重统计数据。
    • 访问过一次,则认为其活跃。
    • Bitmap,性能好、且可以统计精确的结果。

    https://res.craft.do/user/full/fd148a50-4a5b-9a85-bec3-e1645571e2c7/doc/943A9F07-3719-43B2-8785-FD3439CD0658/CCDC0F95-8D44-4EED-955B-9DFFEE5EE1AD_2/NAY3fh0DxlnEeWf10CQn8cSe73g7J0PyXmo6S80WBEkz/Image.png

具体实现:

1.定义RedisKey

public class RedisKeyUtil {
    private static final String SPLIT = ":";
    private static final String PREFIX_UV = "uv";
    private static final String PREFIX_DAU = "dau";

    // 单日UV
    public static String getUVKey(String date){
        return PREFIX_UV + SPLIT + date;
    }

    // 区间UV
    public static String getUVKey(String startDate,String endDate){
        return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;
    }

    // 单日活跃用户
    public static String getDAUKey(String date){
        return PREFIX_DAU + SPLIT + date;
    }

    // 区间活跃用户
    public static String getDAUKey(String startDate,String endDate){
        return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate;
    }
}

2.业务层逻辑

@Service
public class DataService {

    @Autowired
    private RedisTemplate redisTemplate;

    private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");

    // 将指定的IP计入UV
    public void recordUV(String ip){
        String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
        redisTemplate.opsForHyperLogLog().add(redisKey,ip);
    }

    // 统计指定日期范围内的UV
    public long calculateUV(Date start,Date end){
        if(start == null || end == null){
            throw new IllegalArgumentException("参数不能为空!");
        }

        // 整理该日期范围内的key
        List<String> keyList = new ArrayList<>();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(start);
        while (!calendar.getTime().after(end)){
            String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));
            keyList.add(key);
            calendar.add(calendar.DATE,1);
        }

        // 合并这些数据
        String redisKey = RedisKeyUtil.getUVKey(df.format(start),df.format(end));
        redisTemplate.opsForHyperLogLog().union(redisKey,keyList.toArray());

        // 返回统计结果
        return redisTemplate.opsForHyperLogLog().size(redisKey);
    }

    // 将指定用户计入DAU
    public void recordDAU(int userId){
        String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));
        redisTemplate.opsForValue().setBit(redisKey,userId,true);
    }

    // 统计指定日期范围内的DAU
    public long calculateDAU(Date start,Date end){
        if(start == null || end == null){
            throw new IllegalArgumentException("参数不能为空!");
        }

        // 整理该日期范围内的key
        List<byte[]> keyList = new ArrayList<>();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(start);
        while (!calendar.getTime().after(end)){
            String key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime()));
            keyList.add(key.getBytes());
            calendar.add(calendar.DATE,1);
        }

        // 进行OR运算
        return (long) redisTemplate.execute(new RedisCallback() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                String redisKey = RedisKeyUtil.getDAUKey(df.format(start),df.format(end));
                connection.bitOp(RedisStringCommands.BitOperation.OR,
                        redisKey.getBytes(),keyList.toArray(new byte[0][0]));
                return connection.bitCount(redisKey.getBytes());
            }
        });
    }
}

3.使用拦截器进行访问数据的记录

  • 定义拦截器DataInterceptor
@Component
public class DataInterceptor implements HandlerInterceptor {
    @Autowired
    private DataService dataService;
    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 统计UV
        String ip = request.getRemoteHost();
        dataService.recordUV(ip);

        // 统计DAU
        User user = hostHolder.getUser();
        if(user != null){
            dataService.recordDAU(user.getId());
        }

        return true;
    }
}
  • 配置拦截器WebMvcConfig
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private DataInterceptor dataInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(dataInterceptor)
                .excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg");

    }
}

4.表现层逻辑

注意⚠️:@DateTimeFormat注解的作用是入参格式化,前台传string类型的时间字符串,此注解将字符串转换为Date类型

@Controller
public class DataController {

    @Autowired
    private DataService dataService;

    // 统计页面
    @RequestMapping(path = "/data",method = {RequestMethod.GET,RequestMethod.POST})
    public String getDataPage(){
        return "/site/admin/data";
    }

    // 统计网站UV
    @RequestMapping(path = "/data/uv",method = RequestMethod.POST)
    public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
                        @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model){
        long uv = dataService.calculateUV(start,end);
        model.addAttribute("uvResult",uv);
        model.addAttribute("uvStartDate",start);
        model.addAttribute("uvEndDate",end);
        return "forward:/data";
    }

    // 统计活跃用户
    @RequestMapping(path = "/data/dau",method = RequestMethod.POST)
    public String getDAU(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
                        @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model){
        long dau = dataService.calculateDAU(start,end);
        model.addAttribute("dauResult",dau);
        model.addAttribute("dauStartDate",start);
        model.addAttribute("dauEndDate",end);
        return "forward:/data";
    }
}

5.权限管理SecutiryConfig

// 部分授权代码
http.authorizeRequests()
        .antMatchers(
                "/data/**"
        )
        .hasAnyAuthority(
                AUTHORITY_ADMIN
        )
        .anyRequest().permitAll()
        .and().csrf().disable();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/61606.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2-分类问题 SVM 核函数

目录 一&#xff0c;核函数的引入 二&#xff0c;核函数的定义 三&#xff0c;核函数介绍&#xff1a; 四&#xff0c;核函数总结&#xff1a; 一&#xff0c;核函数的引入 世界上本来没有两个完全一样的物体&#xff0c;对于所有的两个物体&#xff0c;我们可以通过增加维…

ES6 Reflect

前言 此文总结了Reflect对象的部分语法&#xff0c;对比了与Object方法的差异性&#xff0c;希望对你有用。 语法 Reflect与Math类似&#xff0c;都是JavaScript内置对象&#xff0c;提供了工具方法。 typeof Reflect // objectget Reflect.get(target, property, receiver) …

冷热电气多能互补的微能源网鲁棒优化调度(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Linux——VIM编辑器(详细)

目录 一、基本简介 1.1 基本简介 1.2 我们怎么使用终端进入到桌面&#xff1f; 1.3 模式间转换 二、一般模式 2.1 操作指令 2.2 什么情况下是一般模式呢&#xff1f; 2.3 怎么进行光标的快速移动&#xff1f; 2.4 复制粘贴操作 2.5 删除操作 三、编辑模式 四、命令行…

Java项目:SSM教师师资管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 本项目包含管理员与教师两种角色&#xff1b; 管理员角色包含以下功能&#xff1a; 管理员角色登录,教师管理,教师授课管理,审批教师的项目开…

【前沿技术RPA】 一文了解UiPath 文件与文件夹自动化功能

&#x1f40b;作者简介&#xff1a;博主是一位.Net开发者&#xff0c;同时也是RPA和低代码平台的践行者。 &#x1f42c;个人主页&#xff1a;会敲键盘的肘子 &#x1f430;系列专栏&#xff1a;UiPath &#x1f980;专栏简介&#xff1a;UiPath在传统的RPA&#xff08;Robotic…

[YOLOv7/YOLOv5系列改进NO.40]融入适配GPU的轻量级 G-GhostNet

文章目录前言一、解决问题二、基本原理三、​添加方法四、总结前言 作为当前先进的深度学习目标检测算法YOLOv7&#xff0c;已经集合了大量的trick&#xff0c;但是还是有提高和改进的空间&#xff0c;针对具体应用场景下的检测难点&#xff0c;可以不同的改进方法。此后的系列…

头歌计算机组成原理汉字字库存储芯片扩展实验

全部答案点击底部 <?xml version"1.0" encoding"UTF-8" standalone"no"?> <project source"2.15.0.2.exe" version"1.0"> This file is intended to be loaded by Logisim http://logisim.altervista.org &…

如何在 Spring 或 Spring Boot 中使用键集分页

介绍 在本文中&#xff0c;我将向您展示如何在 Spring 或 Spring Boot 中使用键集分页技术。 虽然 Spring DataPagingAndSortingRepository提供的基于偏移量的默认分页在许多情况下很有用&#xff0c;但如果您必须迭代大型结果集&#xff0c;那么键集分页或查找方法技术可以提…

使用awk聚合和排序

用awk聚合和排序 文章目录用awk聚合和排序一、需求1.1 源文件格式1.2 需求二、用awk实现2.1 写法2.2 效果一、需求 1.1 源文件格式 一份csv文件&#xff08;默认逗号分隔&#xff09;一共五列&#xff0c;其中一列是用户名文件名&#xff1a;日志文件.csv type日记idusernam…

Android使用ListView,DrawerLayout实现简单注册功能界面

1.效果展示 2.实现 1.主页面activity_main.xml 主页面就是简单的几个TextView和EditText以及单选框组成的一个注册表单。 <?xml version"1.0" encoding"utf-8"?> <LinearLayoutxmlns:android"http://schemas.android.com/apk/res/andro…

[附源码]JAVA毕业设计口腔医院网站(系统+LW)

[附源码]JAVA毕业设计口腔医院网站&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

java通过lock实现同步锁

这里我们是一个卖票的演示代码 其实 同步锁 远不止一个synchronized 它本身有一个 加上锁 和释放锁的过程 为了 让我们更好的理解这个过程 JDK5之后 为我们提供了一个单独的锁工具 lock lock是一个接口 他提供了 synchronized 方法 和 更广泛的语句操作 lock方法 获得锁 unl…

【C语言】函数传参与指针理解

文章目录指针与变量注意指针的本质指针和变量的用法函数与传参传变量与传指针的区别传变量与传指针的时机指针与变量 大三&#xff0c;但是C语言。目标&#xff1a;高屋建瓴&#xff0c;深入浅出。 注意 所有人在最开始学C语言的时候&#xff0c;老师都会和你说指针指向一个…

[附源码]JAVA毕业设计课程答疑系统(系统+LW)

[附源码]JAVA毕业设计课程答疑系统&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

LLM.int8()——自适应混合精度量化方法

Paper地址&#xff1a;https://arxiv.org/abs/2208.07339 GitHub链接&#xff1a;GitHub - TimDettmers/bitsandbytes: 8-bit CUDA functions for PyTorch 随着模型参数规模的增加&#xff0c;大模型&#xff08;如GPT-3&#xff0c;OPT-175B等NLP稠密大模型&#xff09;的实际…

某验三代滑块流程分析

一、请求流程 slide-float.html 首先请求了个HTML文本jquery.js 拿回一个jQuery的jsgt.js 拿回gt.js 像是某验的网址信息register-slide?t1669432270469 一个请求、携带时间戳。返回challenge gt等信息gettype.php 获取验证码类型 携带gtfullpage.9.1.0.js 滑块js代码get.php …

【Java开发】 Spring 08 :访问 Web 资源( 借助 RestTemplate 或 WebClient )

web 资源就是运行在服务器上的资源&#xff0c;比如放到 web 下的页面 js 文件、图片、css等&#xff0c;web资源分为静态web资源和动态web资源两类&#xff0c;接下来访问的就是动态资源&#xff08;页面返回的数据是动态的&#xff0c;由后端程序产生&#xff09;&#xff0…

Rust权威指南之编写自动化测试

一. 简述 虽然Rust的类型系统为我们提供了相当多的安全保障&#xff0c;但是还是不足以防止所有的错误。因此&#xff0c;Rust在语言层面内置了编写测试代码、执行自动化测试任务的功能。 测试是一门复杂的技术&#xff0c;本章覆盖关于如何编写优秀测试的每一个细节&#xf…

[LeetCode周赛复盘] 第 322 场周赛20221204

[LeetCode周赛复盘] 第 322 场周赛20221204 一、本周周赛总结二、 [Easy] 6253. 回环句1. 题目描述2. 思路分析3. 代码实现三、[Medium] 6254. 划分技能点相等的团队1. 题目描述2. 思路分析3. 代码实现四、[Medium] 6255. 两个城市间路径的最小分数1. 题目描述2. 思路分析3. 代…