牛客论坛项目-第七章

第七章

Spring Security

对身份认证和授权提供全面、可拓展的支持,防止会话、点击劫持、csrf攻击等

 

权限控制

对所有请求分配访问权限、绕过认证流程

配置SecurityConfig

 @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","/dicuss/add",
                         "/comment/add/**","/letter/**","/notice/**","/like","/follow","/unfollow")
                 .hasAnyAuthority(
                         AUTHORITY_ADMIN,
                         AUTHORITY_USER,
                         AUTHORITY_MODERATOR
                 )
                 .antMatchers("/discuss/top","/discuss/wonderful").hasAnyAuthority(
                         AUTHORITY_MODERATOR
                 )
                 .antMatchers("/discuss/delete",
                         "/data/**").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");
 ​
     }
 }

配置Service

 public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
         User user = this.findUserById(userId);
         List<GrantedAuthority> authorities = new ArrayList<>();
         authorities.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 authorities;
     }

配置LoginTicketInterceptor

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

置顶、加精、删除

版主可以执行指定、加精

管理员可以进行删除

(修改帖子状态)

 //置顶
     @RequestMapping(path = "/top", method = RequestMethod.POST)
     @ResponseBody
     public String setTop(int id) {
         discussPostService.updateType(id, 1);
         //触发发帖事件
         Event event = new Event().setTopic(TOPIC_PUBLISH).setUserId(hostHolder.getUser().getId())
                 .setEntityType(ENTITY_TYPE_POST).setEntityId(id);
         eventProducer.fireEvent(event);
         return CommunityUtil.getJSONString(0);
     }

配置权限SecurityConfig

 .antMatchers("/discuss/top","/discuss/wonderful").hasAnyAuthority(
                         AUTHORITY_MODERATOR
                 )
                 .antMatchers("/discuss/delete",
                         "/data/**").hasAnyAuthority(
                         AUTHORITY_ADMIN
                 )

还可以引入xmlns:sec="http://www.thymeleaf.org/extras/spring-security"

 <button type="button" class="btn btn-danger btn-sm" id="topBtn"
                             th:disabled="${post.type==1}" sec:authorize="hasAnyAuthority('moderator')">置顶</button>
                     <button type="button" class="btn btn-danger btn-sm" id="wonderfulBtn"
                             th:disabled="${post.status==1}" sec:authorize="hasAnyAuthority('moderator')">加精</button>
                     <button type="button" class="btn btn-danger btn-sm" id="deleteBtn"
                             th:disabled="${post.status==2}" sec:authorize="hasAnyAuthority('admin')">删除</button>

异步调用:

  1. @ResponseBody

  2. 在js里编写

 (function (){
     $("#topBtn").click(setTop);
 });
 ​
 //置顶
 function setTop(){
     $.post(
         CONTEXT_PATH + "/discuss/top",
         {"id":$("#postId").val()},
         function (data){
             data = $.parseJSON(data);
             if (data.code == 0){
                 $("#topBtn").attr("disabled","disabled");
             }else {
                 alert(data.msg);
             }
         }
     );
 }

Redis高级数据类型

UV-用户IP,每次访问都要统计--HeperLogLog:采用基数算法,完成独立总数的统计,不准确的统计算法

 @Test
     public void testHyperLogLogUnion(){
         String rediskey = "test:hll:01";
         for (int i = 0; i < 10000; i++) {
             redisTemplate.opsForHyperLogLog().add(rediskey, i);
         }
 ​
         String rediskey1 = "test:hll:01";
         for (int i = 5001; i < 15000; i++) {
             redisTemplate.opsForHyperLogLog().add(rediskey1, i);
         }
         String rediskey2 = "test:hll:01";
         for (int i = 10001; i < 20000; i++) {
             redisTemplate.opsForHyperLogLog().add(rediskey2, i);
         }
 ​
         String rediskeyUnionKey = "test:hll:union";
         for (int i = 0; i < 10000; i++) {
             redisTemplate.opsForHyperLogLog().union(rediskeyUnionKey, rediskey, rediskey1, rediskey2);
         }
 ​
         long size = redisTemplate.opsForHyperLogLog().size(rediskeyUnionKey);
         System.out.println(size);
 ​
     }

DAU-日活跃用户,访问一次就为活跃,Bitmap:按位存储,可看作byte数组

 @Test
     public void testBitMap(){
         String redisKey = "test:bm:01";
 ​
         redisTemplate.opsForValue().setBit(redisKey, 1, true);
         redisTemplate.opsForValue().setBit(redisKey, 4, true);
         redisTemplate.opsForValue().setBit(redisKey, 7, true);
 ​
         System.out.println(redisTemplate.opsForValue().getBit(redisKey, 0));
         System.out.println(redisTemplate.opsForValue().getBit(redisKey, 1));
         System.out.println(redisTemplate.opsForValue().getBit(redisKey, 2));
 ​
         Object obj = redisTemplate.execute(new RedisCallback() {
             @Override
             public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                 return redisConnection.bitCount(redisKey.getBytes());
             }
         });
         System.out.println(obj);
     }

管理员通过命令直接访问 xxxx/commuity/data

任务执行和调度

 //JDK普通线程池
     private ExecutorService executorService = Executors.newFixedThreadPool(5);
     
 //JDK普通线程池
     @Test
     public void testExectorService() throws InterruptedException {
         Runnable task = new Runnable() {
             @Override
             public void run() {
                 logger.debug("hello testExectorService");
             }
         };
         for (int i = 0; i < 10; i++) {
             executorService.submit(task);
         }
         sleep(10000);
     }
 //JDK可执行定时任务的线程池
     private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
 @Test
     public void testScheduleExectorService() throws InterruptedException {
         Runnable task = new Runnable() {
             @Override
             public void run() {
                 logger.debug("hello testScheduleExectorService");
             }
         };
         scheduledExecutorService.scheduleAtFixedRate(
         task, 10000, 1000, TimeUnit.SECONDS);
         sleep(30000);
     }    
 //Spring普通线程池
     @Autowired
     private ThreadPoolTaskExecutor taskExecutor;
 ​
 //spring可执行任务线程池
     @Autowired
     private ThreadPoolTaskScheduler taskScheduler;
 ​
 //spring普通线程池
     @Test
     public void testThreadPoolTaskExecutor() throws InterruptedException {
         Runnable task = new Runnable() {
             @Override
             public void run() {
                 logger.debug("hello ThreadPoolTaskExecutor");
             }
         };
             for (int i = 0; i < 10; i++) {
                 taskExecutor.submit(task);
             }
             sleep(10000);
     }
 //spring定时任务线程池
     @Test
     public void testThreadPoolTaskScheduler() throws InterruptedException {
         Runnable task = new Runnable() {
             @Override
             public void run() {
                 logger.debug("hello ThreadPoolTaskScheduler");
             }
         };
         Date startTime = new Date(System.currentTimeMillis() + 10000);
         taskScheduler.scheduleAtFixedRate(task, startTime, 1000);
         sleep(30000);
     }

定义任务

配置quartz 中 PostScoreRefreshJob implements Job, CommunityConstant

 @Override
     public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
         String redisKey = RedisKeyUtil.getPostScoreKey();
         BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);
         if (operations.size() == 0){
             logger.info("任务取消,没有需要刷新的帖子!");
             return;
         }
         logger.info("任务开始 正在刷新帖子分数:" + operations.size());
         while (operations.size() > 0){
             this.refresh((Integer) operations.pop());
         }
         logger.info("任务结束 帖子分数刷新完毕");
     }
     private void refresh(int postId) {
         DiscussPost discussPost = discussPostService.findDiscussPostById(postId);
         if (discussPost == null) {
             logger.info("该帖子不存在:id" + postId);
         }
         //是否精华
         boolean wonderful = discussPost.getStatus() == 1;
         //评论数量
         int CommentCount = discussPost.getCommentCount();
         //点赞数量
         long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);
 ​
         //计算权重
         double w = (wonderful ? 75 : 0) + CommentCount * 10 + likeCount * 2;
         //分数
         double score = Math.log10(Math.max(w, 1)) + (discussPost.getCreateTime().getTime() - epoch.getTime()) / (3600 * 1000 * 24);
         //跟新帖子分数
         discussPostService.updateScore(postId, score);
         //同步搜索数据
         elasticsearchService.saveDiscusspost(discussPost);
     }

配置 JobDetail和Trigger

config - QuartzConfig

 @Configuration
 public class QuartzConfig {
 ​
     //更新帖子分数任务
     @Bean
     public JobDetailFactoryBean postScorefreshJobDetail(){
         JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
         factoryBean.setJobClass(PostScoreRefreshJob.class);
         factoryBean.setName("postScoreRefreshJob");
         factoryBean.setGroup("communityJobGroup");
         factoryBean.setDurability(true); //长久保存
         factoryBean.setRequestsRecovery(true); //任务是否可恢复
         return factoryBean;
     }
 ​
     @Bean
     public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) {
         SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
         factoryBean.setJobDetail(postScoreRefreshJobDetail);
         factoryBean.setName("postScoreRefreshTrigger");
         factoryBean.setGroup("communityTriggerGroup");
         factoryBean.setRepeatInterval(1000 * 60 * 5);
         factoryBean.setJobDataMap(new JobDataMap());
         return factoryBean;
     }
 }

生产长图

工具:wkhtmltopdf

wkhtmltoimage --quality 75 牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网 e:/Study/workspace/data/wk-images/2.png

定义controller

  @RequestMapping(path = "/share", method = RequestMethod.GET)
     @ResponseBody
     public String share(String htmlUrl){
         //文件名
         String fileName = CommunityUtil.generateUUID();
         //异步生成长图
         Event event = new Event().setTopic(TOPIC_SHARE)
                 .setData("htmlUrl", htmlUrl)
                 .setData("fileName", fileName)
                 .setData("suffix", ".png");
         eventProducer.fireEvent(event);
 ​
         Map<String, Object> map = new HashMap<>();
         map.put("shareUrl", domain + contextPath + "/share/image" + fileName);
 ​
         //返回访问路径
         return CommunityUtil.getJSONString(0, null, map);
     }
 ​
     //获取长图
     @RequestMapping(path = "/share/image/{filename}",method = RequestMethod.GET)
     public void getShareImage(@PathVariable("fileName") String filename, HttpServletResponse response){
         if (StringUtils.isBlank(filename)){
             throw new IllegalArgumentException("文件名不能为空");
         }
         response.setContentType("image/png");
         File file = new File(wkImageStorage + "/" + filename + ".png");
         try {
             OutputStream os = response.getOutputStream();
             FileInputStream fis  = new FileInputStream(file);
             byte[] buffer = new byte[1024];
             int b = 0;
             while ((b=fis.read(buffer)) != -1){
                 os.write(buffer,0, b);
             }
         }catch (IOException e){
             logger.info("获取长图失败" + e.getMessage());
         }
     }

消费者

 //消费分享事件
     @KafkaListener(topics = TOPIC_SHARE)
     public void handleShareMessage(ConsumerRecord record){
         if (record == null && record.value() == null){
             logger.error("消息内容不能为空");
             return;
         }
         Event event = JSONObject.parseObject(record.value().toString(), Event.class);
         if (event == null){
             logger.error("消息格式错误" );
             return;
         }
         String htmUrl = (String) event.getData().get("htmlUrl");
         String fileName = (String) event.getData().get("fileName");
         String suffix = (String) event.getData().get("suffix");
         String cmd = wkImageCommand + " --quality 75 " +
                 htmUrl + " " + wkImageStorage + "/" + fileName +suffix;
         try {
             Runtime.getRuntime().exec(cmd);
             logger.info("生成长图成功" + cmd);
         } catch (IOException e) {
            logger.info("生成长图失败" +  e.getMessage());
         }
     }

云服务器

头像上传至七牛云

 qiniu.key.access=ktu4EncQ52sVKYA4MKv96PcsVe5wHJWkolyFCfYr
 qiniu.key.secret=yIFPUKcVWIbU77z-qPthZLYASwK23rgFXgeaNbmr
 @LoginRequired
     @RequestMapping(path = "/setting", method = RequestMethod.GET)
     public String getSettingPage(Model model) {
         //上传文件名称
         String fileName = CommunityUtil.generateUUID();
         //设置响应信息
         StringMap policy = new StringMap();
         policy.put("returnBody",CommunityUtil.getJSONString(0));
         //生成上传凭证
         Auth auth = Auth.create(accessKey, secretKey);
         String uploadToken = auth.uploadToken(headerBucketName, fileName, 3600, policy);
 ​
         model.addAttribute("uploadToken", uploadToken);
         model.addAttribute("fileName", fileName);
 ​
         return "/site/setting";
     }
 ​
     //更新头像的路径
     @RequestMapping(path = "/header/url",method = RequestMethod.POST)
     @ResponseBody
     public String updateHeaderUrl(String fileName){
         if (StringUtils.isBlank(fileName)){
             return CommunityUtil.getJSONString(1, "文件名不能为空");
         }
         String url = headerBucketUrl + "/" + fileName;
         userService.updateHeader(hostHolder.getUser().getId(), url);
         return CommunityUtil.getJSONString(0);
     }
 $(function (){
     $("#uploadForm").submit(upload);
 });
 ​
 function upload(){
     $.ajax({
         url:"http://upload-z1.qiniup.com",
         method:"post",
         processData:false,
         contentType:false,
         data: new FormData($("#uploadForm")[0]),
         success:function (data){
             if (data && data.code == 0){
                 //更新头像访问路径
                 $.post(
                     CONTEXT_PATH + "/user/header/url",
                     {"fileName":$("input[name='key']").val()},
                     function (data){
                         data = $.parseJSON(data);
                         if (data.code == 0){
                             window.location.reload();
                         }else {
                             alert(data.mag);
                         }
                     }
                 );
             }else {
                 alert("上传失败!");
             }
         }
     });
 ​
     return false;
 }

性能优化

本地缓存:数据缓存到服务器上,常用工具Caffeine

分布式缓存:缓存到NoSQL数据库上,如Redis

多级缓存:一级(本地) > 二级(分布式) > DB

优化service方法

Caffeine

 //caffeine核心接口:Cache, loadingCache, AsyncLoadingCache
     //帖子列表缓存
     private LoadingCache<String, List<DiscussPost>> postListCache;
 ​
     //帖子总数缓存
     private LoadingCache<Integer, Integer> postRowsCache;
 ​
     @PostConstruct
     public void init(){
         //初始化帖子列表缓存
         postListCache = Caffeine.newBuilder()
                 .maximumSize(maxSize)
                 .expireAfterWrite(expiredSeconds, TimeUnit.SECONDS)
                 .build(new CacheLoader<String, List<DiscussPost>>(){
                     @Nullable
                     @Override
                     public List<DiscussPost> load(@NonNull String key) throws Exception{
                         if (key == null || key.length() == 0){
                             throw new IllegalArgumentException("参数错误");
                         }
 ​
                         String[] params = key.split(":");
                         if (params == null || params.length != 2){
                             throw new IllegalArgumentException("参数错误");
                         }
                         int offset = Integer.valueOf(params[0]);
                         int limit = Integer.valueOf(params[1]);
 ​
                         //二级缓存
 ​
                         return discussPostMapper.selectDiscussPosts(0, offset, limit, 1);
                     }
                 });
 ​
         //初始化帖子总数缓存
         postRowsCache = Caffeine.newBuilder()
                 .maximumSize(maxSize)
                 .expireAfterWrite(expiredSeconds, TimeUnit.SECONDS)
                 .build(new CacheLoader<Integer, Integer>(){
                     @Nullable
                     @Override
                     public Integer load(@NonNull Integer key) throws Exception{
                         logger.debug("load post rows from DB");
                         return discussPostMapper.selectDiscussPostRows(key);
                     }
                 });
     }
     
 public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode){
        if (userId == 0 && orderMode == 1){
                return postListCache.get(offset+ ":" + limit);
        }
 ​
        logger.debug("load post list from DB");
         return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode);
     }
 ​
     public int findDiscussPostRows(int userId){
         if (userId == 0){
                 return postRowsCache.get(userId);
         }
         logger.debug("load post rows from DB");
         return discussPostMapper.selectDiscussPostRows(userId);
     }

压力测试

JMeter 点击xxx.bat

模拟100个线程

吞吐量:9.5/sec -> 188/sec

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值