博客后台模块续更(四)

八、博客后台模块-Excel表格

1. 接口分析

在分类管理中点击导出按钮可以把所有的分类导出到Excel文件

请求方式

请求地址

请求头

GET

/content/category/export

需要token请求头

响应体:

 直接导出一个Excel文件

失败的话响应体如下:

{
	"code":500,
	"msg":"出现错误"
}

2. EasyExcel入门

使用easyExcel实现Excel的导出操作

官方地址: http:// https://github.com/alibaba/easyexcel

快速开始 https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write#%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81-1

分析: 把数据库的分类数据查询出来,然后写入到Excel文件中,然后下载这个Excel文件,重点就是怎么往Excel里面写入数据,点击上面提供的快速开始的链接,点击左侧的 '写Excel',就能看到实现的代码了,重点看右侧小导航栏的 'web中的写并且失败的时候返回json'

3. 代码实现

第一步:在keke-framework工程的pom.xml添加如下



	com.alibaba
	easyexcel

第二步: 把keke-framework工程的WebUtils类修改为如下

package com.keke.utils;

import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class WebUtils {
    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static void renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }


    //easyExcel文件导出
    public static void setDownLoadHeader(String filename, HttpServletResponse response) throws UnsupportedEncodingException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fname= URLEncoder.encode(filename,"UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition","attachment; filename="+fname);
    }
}

第三步: 在keke-framework工程的vo目录新建ExcelCategoryVo类,写入如下,用于作为Excel表格的列头

package com.keke.domain.vo;


import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExcelCategoryVo {
     @ExcelProperty("分类名")
     private String name;

     @ExcelProperty("描述")
     private String description;

     @ExcelProperty("状态:0正常,1禁用")
     private String status;
}

第四步: 把keke-admin工程的CategoryController类修改为如下,增加了easyExcel文件导出的具体代码实现

package com.keke.controller;


import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson.JSON;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.ExcelCategoryVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.WebUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.util.List;

@RestController
@RequestMapping("/content/category")
@Api(tags = "后台标签相关接口")
public class CategoryController {

     @Autowired
     private CategoryService categoryService;

     @GetMapping("/listAllCategory")
     public ResponseResult listAllCategory(){
          return categoryService.listAllCategory();
     }

     @GetMapping("/export")
     public void export(HttpServletResponse response){

          try {
               //设置下载文件的请求头
               WebUtils.setDownLoadHeader("分类.xlsx",response);
               //获取需要导出的数据
               List categoryList = categoryService.list();
               List excelCategoryVos = BeanCopyUtils.copyBeanList(categoryList, ExcelCategoryVo.class);
               //把数据写入Excel中
               EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("分类导出")
                       .doWrite(excelCategoryVos);
          } catch (Exception e) {
               //如果出现异常,就返回失败的json数据给前端
               ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
               //WebUtils是我们在keke-framework工程写的类,里面的renderString方法是将json字符串写入到请求体,然后返回给前端
               WebUtils.renderString(response, JSON.toJSONString(result));
          }
     }
}

第五步:测试,启动后台工程,redis,前端工程,点击导出按钮

博客后台模块续更(四)_第1张图片

博客后台模块续更(四)_第2张图片

导出成功 

博客后台模块续更(四)_第3张图片

博客后台模块续更(四)_第4张图片

九、SpringSecurity权限控制

由于后台的用户对应不同的角色,所以有不同的权限,例如上面实现的导出excel功能,是超级管理员才有的功能,但是如果普通用户登录,拿着token去调导出excel的接口,也是可以成功的,这里我们就需要用到安全框架中的权限控制

1. 案例

比如我们登录普通用户,拿到token,访问导出excel表格接口,依旧可以导出,但是此用户是没有该权限的

博客后台模块续更(四)_第5张图片

2. 代码实现

第一步: 把keke-framework工程的SystemCanstants类修改为如下,增加了是否是管理员用户的判断常量

package com.keke.constants;


//字面值(代码中的固定值)处理,把字面值都在这里定义成常量
public class SystemConstants {

    /**
     * 文章是草稿
     */
    public static final int ARTICLE_STATUS_DRAFT = 1;

    /**
     * 文章是正常发布状态
     */
    public static final int ARTICLE_STATUS_NORMAL = 0;

    /**
     * 文章列表当前查询页数
     */
    public static final int ARTICLE_STATUS_CURRENT = 1;

    /**
     * 文章列表每页显示的数据条数
     */
    public static final int ARTICLE_STATUS_SIZE = 10;

    /**
     * 分类表的分类状态是正常状态
     */
    public static final String STATUS_NORMAL = "0";

    /**
     * 友联审核通过
     */
    public static final String Link_STATUS_NORMAL = "0";

    /**
     * 评论区的某条评论是根评论
     */
    public static final String COMMENT_ROOT = "-1";

    /**
     * 文章评论
     */
    public static final String ARTICLE_COMMENT = "0";

    /**
     * 友链评论
     */
    public static final String LINK_COMMENT = "1";

    /**
     * redis中的文章浏览量key
     */
    public static final String REDIS_ARTICLE_KEY = "article:viewCount";

    /**
     * 浏览量自增1
     */
    public static final int REDIS_ARTICLE_VIEW_COUNT_INCREMENT = 1;


    /**
     * 菜单权限
     */
    public static final String MENU = "C";

    /**
     * 按钮权限
     */
    public static final String BUTTON = "F";

    /**
     * 后台管理员用户
     */
    public static final String ADMIN = "1";

}

第二步:keke-framework中domain/entity LoginUser修改如下,增加权限信息成员变量

package com.keke.domain.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

     private User user;
     //用于返回权限信息。现在我们正在实现'认证','权限'后面才用得到。所以返回null即可
     //当要查询用户信息的时候,我们不能单纯返回null,要重写这个方法,作用是返回权限信息
     private List permissions;
     @Override
     public Collection getAuthorities() {
          return null;
     }

     @Override
     public String getPassword() {
          return user.getPassword();
     }

     @Override
     public String getUsername() {
          return user.getUserName();
     }

     @Override
     public boolean isAccountNonExpired() {
          return true;
     }

     @Override
     public boolean isAccountNonLocked() {
          return true;
     }

     @Override
     public boolean isCredentialsNonExpired() {
          return true;
     }

     @Override
     public boolean isEnabled() {
          return true;
     }
}

第三步: 把keke-admin工程的SecurityConfig修改为如下,增加了@EnableGlobalMethodSecurity注解

package com.keke.config;

import com.keke.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//WebSecurityConfigurerAdapter是Security官方提供的类
@EnableGlobalMethodSecurity(prePostEnabled = true)//权限控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入我们在keke-blog工程写的JwtAuthenticationTokenFilter过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;



    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    //把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                 //对于后台登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                //其他接口均需要认证
                .anyRequest().authenticated();

        //配置我们自己写的认证和授权的异常处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);


        //关闭security默认的退出登录功能
        http.logout().disable();
        //将自定义filter加入security过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

}

第四步: 把keke-framework工程的UserDetailsServiceImpl类修改为如下,增加了权限信息的相关实现代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.keke.constants.SystemConstants;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.User;
import com.keke.mapper.MenuMapper;
import com.keke.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

     @Autowired
     private UserMapper userMapper;

     @Autowired
     private MenuMapper menuMapper;

     @Override
     public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
          //因为我们自己创建的UserDetailsService注入到容器中,所以会调用我们自己创建的
          //根据用户名从数据库查询用户信息,这里注入userMapper进行查询
          LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
          lambdaQueryWrapper.eq(User::getUserName,userName);
          //这里可以看到userMapper也可以传入wrapper进行条件查询
          User user = userMapper.selectOne(lambdaQueryWrapper);
          //判断是否查到用户,如果没查到,抛出异常
          if(Objects.isNull(user)){
               throw new RuntimeException("用户不存在");
          }
          //返回用户信息


          //TODO查询权限信息封装(后台)
          if(user.getType().equals(SystemConstants.ADMIN)){
               //如果是后台管理员,查询权限信息,封装到LoginUser中
               List menuList = menuMapper.selectPermsByUserId(user.getId());
               LoginUser loginUser = new LoginUser(user,menuList);
               return loginUser;
          }
          return new LoginUser(user,null);
     }
}

第五步: 在keke-framework工程的service目录创建impl.PermissionService类,写入如下

package com.keke.service.impl;

import com.keke.utils.SecurityUtils;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("ps")
public class PermissionService {


     /**
      * 判断当前用户是否具有permission
      * @param permission
      * @return
      */

     //否则  获取当前登录用户所具有的权限列表 如何判断是否存在permission
     // 这个permission其实就是sys_menu表的perms字段的值
     public boolean hasPermission(String permission){
          //如果是管理员,直接有权限
          if(SecurityUtils.isAdmin()){
               return true;
          }
          List permissions = SecurityUtils.getLoginUser().getPermissions();
          //contains方法是 'List集合官方' 提供的方法,返回值是布尔值,如果用户具有对应权限就返回true
          return permissions.contains(permission);
     }
}

第六步: 把huanf-admin工程的CategoryController类修改为如下,在export方法的上面添加了@PreAuthorize注解

package com.keke.controller;


import com.alibaba.excel.EasyExcel;
import com.alibaba.fastjson.JSON;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Category;
import com.keke.domain.vo.ExcelCategoryVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.service.CategoryService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.WebUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.util.List;

@RestController
@RequestMapping("/content/category")
@Api(tags = "后台标签相关接口")
public class CategoryController {

     @Autowired
     private CategoryService categoryService;

     @GetMapping("/listAllCategory")
     public ResponseResult listAllCategory(){
          return categoryService.listAllCategory();
     }

     //权限控制,ps是PermissionService类的bean名称
     @PreAuthorize("@ps.hasPermission('content:category:export')")
     @GetMapping("/export")
     public void export(HttpServletResponse response){

          try {
               //设置下载文件的请求头
               WebUtils.setDownLoadHeader("分类.xlsx",response);
               //获取需要导出的数据
               List categoryList = categoryService.list();
               List excelCategoryVos = BeanCopyUtils.copyBeanList(categoryList, ExcelCategoryVo.class);
               //把数据写入Excel中
               EasyExcel.write(response.getOutputStream(), ExcelCategoryVo.class).autoCloseStream(Boolean.FALSE).sheet("分类导出")
                       .doWrite(excelCategoryVos);
          } catch (Exception e) {
               //如果出现异常,就返回失败的json数据给前端
               ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR);
               //WebUtils是我们在keke-framework工程写的类,里面的renderString方法是将json字符串写入到请求体,然后返回给前端
               WebUtils.renderString(response, JSON.toJSONString(result));
          }
     }
}

3. 测试

管理员访问

正常用管理员登录,拿到token是可以导出excel表格的

博客后台模块续更(四)_第6张图片

普通用户访问

先登录拿到普通用户的token

访问接口,没有权限

博客后台模块续更(四)_第7张图片

下载后的文件,是失败格式的json响应体

博客后台模块续更(四)_第8张图片

博客后台模块续更(四)_第9张图片

你可能感兴趣的:(KekeBlog,java,spring,boot,intellij-idea,spring,maven)