八、博客后台模块-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添加如下
<!--easyExcel的依赖-->
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId>
</dependency>
第二步: 把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 {@Autowiredprivate CategoryService categoryService;@GetMapping("/listAllCategory")public ResponseResult listAllCategory(){return categoryService.listAllCategory();}@GetMapping("/export")public void export(HttpServletResponse response){try {//设置下载文件的请求头WebUtils.setDownLoadHeader("分类.xlsx",response);//获取需要导出的数据List<Category> categoryList = categoryService.list();List<ExcelCategoryVo> 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,前端工程,点击导出按钮
导出成功
九、SpringSecurity权限控制
由于后台的用户对应不同的角色,所以有不同的权限,例如上面实现的导出excel功能,是超级管理员才有的功能,但是如果普通用户登录,拿着token去调导出excel的接口,也是可以成功的,这里我们就需要用到安全框架中的权限控制
1. 案例
比如我们登录普通用户,拿到token,访问导出excel表格接口,依旧可以导出,但是此用户是没有该权限的
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<String> permissions;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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过滤器@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@AutowiredAuthenticationEntryPoint authenticationEntryPoint;@AutowiredAccessDeniedHandler accessDeniedHandler;@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Bean//把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoderpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected 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 {@Autowiredprivate UserMapper userMapper;@Autowiredprivate MenuMapper menuMapper;@Overridepublic UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {//因为我们自己创建的UserDetailsService注入到容器中,所以会调用我们自己创建的//根据用户名从数据库查询用户信息,这里注入userMapper进行查询LambdaQueryWrapper<User> 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<String> 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<String> permissions = SecurityUtils.getLoginUser().getPermissions();//contains方法是 'List集合官方' 提供的方法,返回值是布尔值,如果用户具有对应权限就返回truereturn 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 {@Autowiredprivate 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<Category> categoryList = categoryService.list();List<ExcelCategoryVo> 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表格的
普通用户访问
先登录拿到普通用户的token
访问接口,没有权限
下载后的文件,是失败格式的json响应体