八、博客后台模块-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 {
@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<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;
@Override
public Collection<? extends GrantedAuthority> 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<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集合官方' 提供的方法,返回值是布尔值,如果用户具有对应权限就返回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<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响应体