社区系统项目复盘-7


使用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();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值