效果图:
1.前言
从零开始搭建一个项目最重要的是选择一个自己熟悉的框架,此项目使用Springboot框架来构建后端结构,使用vue来构建前端页面。数据层我们常用的是Mybatis,这里我大部分使用了Mybatis-plus简化配置,在涉及到多表联合查询的时候使用了Mybatis。登录功能使用的单点登录,使用jwt作为我们的用户身份验证。引入了SpringSecurity安全框架作为我们的权限控制和会话控制。
技术栈:
- Springboot
- mybatis-plus
- spring-security
- lombok
- jwt
2.新建Springboot项目,注意版本
新建功能比骄简单,就不截图了。
开发工具与环境:
- idea
- mysql5.7
- jdk8
- maven3.5.0
第一步:pom依赖导入如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/>
</parent>
<groupId>com.tjise</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--web 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok 依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--导入导出-->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!--日志信息-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!--下载文件-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis-plus 依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!-- swagger2 依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Swagger第三方ui依赖 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<!--security 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--JWT 依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--验证码-->
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
</project>
第二步:项目的结构
仅供参考!
第三步:去写配置文件
# 服务端口
server.port=8001
# 服务名
spring.application.name=service-employ
# 环境设置:dev、test、prod
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/employ?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#逻辑删除
#已删除
mybatis-plus.global-config.db-config.logic-delete-value=1
#未删除
mybatis-plus.global-config.db-config.logic-not-delete-value=0
#开启mutipart
spring.servlet.multipart.enabled=true
#上传文件默认的大小
#设置单个文件上传的大小
spring.servlet.multipart.max-file-size=20MB
#设置总上传文件的大小
spring.servlet.multipart.max-request-size=100MB
spring.resources.static-locations=classpath:/resources/,classpath:/static/,classpath:/public/
#jwt配置
#jwt存储请求头
jwt.tokenHeader=Authorization
#jwt加密使用的密钥
jwt.secret=yed-secret
#jwt的超期限时间
jwt.expiration=604800
#jwt负载中拿到开头
jwt.tokenHead=Bearer
上边除了数据库的配置信息之外,还配置了逻辑删除的配置、jwt的配置、文件上传的配置。端口号前端为8080,所以后端我们就设置为8001.
第三步:开启mapper接口扫描,添加分页、逻辑删除的插件
package com.tjise;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.tjise.employ.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
第四步:创建数据库和表
主要的表有:用户表,用户角色表、菜单表、菜单权限表、菜单表和员工表
3.结果封装
因为是前后端分离项目,所以我们必须要有一个统一的结果返回封装
封装结果如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
private long code;
private String message;
private Object obj;
/**
* 成功返回结果
* @param message
* @return
*/
public static RespBean success(String message){
return new RespBean(200,message,null);
}
/**
* 成功返回结果
* @param message
* @param obj
* @return
*/
public static RespBean success(String message,Object obj){
return new RespBean(200,message,obj);
}
/**
* 失败返回结果
* @param message
* @return
*/
public static RespBean error(String message){
return new RespBean(500,message,null);
}
/**
* 失败返回结果
* @param message
* @param obj
* @return
*/
public static RespBean error(String message,Object obj){
return new RespBean(500,message,obj);
}
}
4.整合SpringSecurity
首先了解一下security的原理
流程说明:
1.客户端发起一个请求,进入 Security 过滤器链。
2.当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
3.当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
4.当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
Security 配置
配置类伪代码:
@Override
public void configure(WebSecurity web) throws Exception {
//放行静态资源
web.ignoring().antMatchers(
"/login",
"/logout",
"/css/**",
"/js/**",
"favicon.ico",
"/index.html",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/captcha",
"/ws/**",
"/employ/upload",
"/employ/export",
"/resources/**"
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用jwt,不需要用csrf
http.csrf()
.disable()
//基于token不需要用session
.sessionManagement()
.and()
.authorizeRequests()
.antMatchers("/众安保险.pdf")
.permitAll()
.anyRequest()
.authenticated()
//动态权限控制
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilter);
return object;
}
})
.and()
//允许跨域访问
.cors()
.and()
//禁用缓存
.headers().frameOptions().disable()
.cacheControl();
//添加jwt 登录授权过滤器
http.addFilterBefore(jwtAutjencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录的结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
@Override
@Bean
public UserDetailsService userDetailsService() {
return username -> {
Admin admin = adminService.getAdminByUserName(username);
if (null != admin) {
admin.setRoles(adminService.getRoles(admin.getId()));
return admin;
}
return null;
};
}
配置类简介:
- configure(AuthenticationManagerBuilder auth)
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager;如果你想要使用该功能你就得配置一个UserDetailService和PasswordEncord。UserDetailService用于在认证器中根据用户传过来的用户查找一个用户,PasswordEncord用于密码的加密与对比,我们存储密码的时候用PasswordEncord.encord()加密存储,在认证器里会调用PasswordEncord.matchs()方法进行密码的比对。
- configure(WebSecurity web)
public void configure(WebSecurity web) throws Exception {
//放行静态资源
web.ignoring().antMatchers(
"/login",
"/logout",
"/css/**",
"/js/**",
"favicon.ico",
"/index.html",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/captcha",
"/ws/**",
"/employ/upload",
"/employ/export",
"/resources/**"
);
}
这个配置方法用于配置静态资源的处理方式,可以使用Ant匹配规则
- configure(HttpSecurity http)
protected void configure(HttpSecurity http) throws Exception {
//使用jwt,不需要用csrf
http.csrf()
.disable()
//基于token不需要用session
.sessionManagement()
.and()
.authorizeRequests()
.antMatchers("/众安保险.pdf")
.permitAll()
.anyRequest()
.authenticated()
//动态权限控制
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilter);
return object;
}
})
.and()
//允许跨域访问
.cors()
.and()
//禁用缓存
.headers().frameOptions().disable()
.cacheControl();
//添加jwt 登录授权过滤器
http.addFilterBefore(jwtAutjencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录的结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
这个配置方法是最关键的方法,也是最复杂的方法。
用户认证
首先我们来解决用户认证的问题,用户认证分为首次登录和二次认证。
- 首次登录认证:用户名、密码、验证码完成登录。
- 二次token认证:请求头携带jwt进行身份认证。
生成验证码
首先我们先生成验证码,我们先配置一下图片验证码的生成规则
@Configuration
public class CaptchaConfig {
@Bean
public DefaultKaptcha defaultKaptcha(){
//验证码生成器
DefaultKaptcha defaultKaptcha=new DefaultKaptcha();
//配置
Properties properties = new Properties();
//是否有边框
properties.setProperty("kaptcha.border", "yes");
//设置边框颜色
properties.setProperty("kaptcha.border.color", "105,179,90");
//边框粗细度,默认为1
// properties.setProperty("kaptcha.border.thickness","1");
//验证码
properties.setProperty("kaptcha.session.key","code");
//验证码文本字符颜色 默认为黑色
properties.setProperty("kaptcha.textproducer.font.color", "blue");
//设置字体样式
properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
//字体大小,默认40
properties.setProperty("kaptcha.textproducer.font.size", "30");
//验证码文本字符内容范围 默认为abced2345678gfynmnpwx
// properties.setProperty("kaptcha.textproducer.char.string", "");
//字符长度,默认为5
properties.setProperty("kaptcha.textproducer.char.length", "4");
//字符间距 默认为2
properties.setProperty("kaptcha.textproducer.char.space", "4");
//验证码图片宽度 默认为200
properties.setProperty("kaptcha.image.width", "100");
//验证码图片高度 默认为40
properties.setProperty("kaptcha.image.height", "40");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
上边定义了图片验证码的长宽颜色等
然后我们通过控制器提供生成验证码的方法。
@RestController
public class CaptchaController {
@Autowired
private DefaultKaptcha defaultKaptcha;
@GetMapping(value = "/captcha",produces = "image/jpeg")
public void captcha(HttpServletRequest request, HttpServletResponse response) {
// 定义response输出类型为image/jpeg类型
response.setDateHeader("Expires", 0);
// Set standard HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// Set standard HTTP/1.0 no-cache header.
response.setHeader("Pragma", "no-cache");
// return a jpeg
response.setContentType("image/jpeg");
//-------------------生成验证码 begin --------------------------
//获取验证码文本内容
String text = defaultKaptcha.createText();
System.out.println("验证码内容:" + text);
//将验证码文本内容放入session
request.getServletContext().setAttribute("captcha", text);
//根据文本验证码内容创建图形验证码
BufferedImage image = defaultKaptcha.createImage(text);
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
//输出流输出图片,格式为jpg
ImageIO.write(image, "jpg", outputStream);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
因为是前后端分离的项目,我们禁用了session,所以我们将验证码放在了ServletContext中,然后以二进制流的形式将图片验证码输出。
验证码校验
//校验验证码
String captcha = (String) request.getServletContext().getAttribute("captcha");
if (StringUtils.isEmpty(code)||!captcha.equalsIgnoreCase(code)){
return RespBean.error("验证码输入错误,请重新输入!");
}
登录成功之后,更新security登录用户对象,生成token,然后将token作为请求头返回回去,名称就叫作Authorization。我们需要在配置文件中配置一些jwt的一些信息:
#jwt存储请求头
jwt.tokenHeader=Authorization
#jwt加密使用的密钥
jwt.secret=yed-secret
#jwt的超期限时间
jwt.expiration=604800
#jwt负载中拿到开头
jwt.tokenHead=Bearer
我们去swagger里边进行测试
上边我们可以看到,我们已经登录成功,然后我们的token也可以看到。
身份认证-1:
登录成功之后前端就可以获取到token的信息,前端中我们是保存在了sessionStorage中,然后每次axios请求之前,我们都会添加上我们的请求头信息。这样携带请求头就可以正常访问我们的接口了。
身份认证-2:
我们的用户必须是存储在数据库里边,密码也是经过加密的,所以我们先来解决这个问题。这里我们使用了Security内置的BCryPasswordEncoder,里边就有生成和匹配密码是否正确的方法,也就是加密和验证的策略。因此我们需要在SecurityConfig中进行配置
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
因为我们登录过程中系统不是从我们数据库中获取数据的,因此我们需要重新定义这个查用户数据的过程,重写UserDetailsService接口
@Override
@Bean
public UserDetailsService userDetailsService() {
return username -> {
Admin admin = adminService.getAdminByUserName(username);
if (null != admin) {
admin.setRoles(adminService.getRoles(admin.getId()));
return admin;
}
return null;
};
}
解决授权
然后就是关于权限部分,也是security的重要功能,当用户认证成功之后,我们就知道是谁在访问系统接口,就是这个用户有没有权限去访问这个接口,要解决这个问题的话我们需要知道用户有哪些权限,哪些角色,这样security才能为我们做出权限的判断。
之前我们定义过几张表,用户、角色、菜单、以及一些关联表,一般情况下当权限粒度比较细的时候,我们通过判断用户有没有此菜单的操作权限,而不是通过角色判断。而用户和菜单不直接做关联的,是通过用户拥有哪些角色,角色拥有哪些菜单这样来获取的。
问题:我们在哪里赋予用户的权限
用户登录的时候
@Bean
public UserDetailsService userDetailsService() {
return username -> {
Admin admin = adminService.getAdminByUserName(username);
if (null != admin) {
admin.setRoles(adminService.getRoles(admin.getId()));
return admin;
}
return null;
};
}
我们再来整体梳理一下授权、验证权限的流程:
用户登录的时候识别到用户,并获取用户的权限信息
Security通过FilterSecurityInterceptor匹配url和权限是否匹配
有权限则可以访问接口,当没有权限的时候返回异常
*/
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
//当前url所需角色
String needRole = configAttribute.getAttribute();
//判断角色是否登录即可访问的角色,此角色在CustomFilter中设置
if ("ROLE_LOGIN".equals(needRole)){
//判断是否登录
if (authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("尚未登录,请登录!");
}else {
return;
}
}
//判断用户角色是否为url所需角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)){
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员!");
}
员工管理的接口的开发:
我们首先来开发员工管理的接口,主要功能就是实现对员工的增删改查,数据的导入和导出,分页显示和逻辑删除。
首先看一下员工的实体类:
public class User {
@Excel(name="编号")
private Integer id; //员工的编号
@Excel(name="姓名")
private String name; //员工姓名
@Excel(name="性别")
private String sex; //员工性别
@Excel(name="籍贯")
private String provices;//员工籍贯
@Excel(name="年龄")
private Integer age; //员工年龄
@Excel(name="薪水")
private double salary; //员工薪水
@Excel(name="地址")
private String address;//员工的地址
@Excel(name="院校")
private String school;//毕业院校
@TableLogic
private Integer isdelete=0;//逻辑删除
}
接着在controller层编写对员工的分页显示的接口:
@PostMapping("pageCodition/{current}/{size}")
public RespPage pageEmployCodition(@PathVariable long current,
@PathVariable long size,
@RequestBody(required = false) Employ employ) {
//创建page对象,开启分页
Page<User> userPage = new Page<>(current, size);
//构建条件
QueryWrapper<User> wrapper = new QueryWrapper<>();
String name = employ.getName();
Integer id = employ.getId();
String provices = employ.getProvices();
//判断条件是否为空
if (!StringUtils.isEmpty(name)) {
wrapper.like("name", name);
}
if (!StringUtils.isEmpty(id)) {
wrapper.eq("id", id);
}
if (!StringUtils.isEmpty(provices)) {
wrapper.like("provices", provices);
}
//调用方法实现分页
//调用方法的时候,底层封装,把分页所有数据封装到page里边
employService.page(userPage, wrapper);
long total = userPage.getTotal();//总记录数
List<User> records = userPage.getRecords();//数据List集合
return RespPage.ok().data("total", total).data("rows", records);
}
要实现分页查询,首先得传两个参数,当前页和每页大小(current、size)
分页显示的步骤:
首先开启分页,传入当前页和每页大小。
Page<User> userPage = new Page<>(current, size);
接着创建条件构造器,
QueryWrapper<User> wrapper = new QueryWrapper<>();
最后调用分页查询的方法。调用方法的时候底层封装,将所有分页的数据封装到page里边。
employService.page(userPage, wrapper);
添加员工的接口:
@PostMapping("add")
public boolean adduser(@RequestBody User user) {
boolean save = employService.save(user);
if (save != false) {
return true;
} else {
return false;
}
}
添加员工的接口就比较简单了,首先用post请求,方法中传入实体类对象,调用mybatisplus自带的添加方法.save(),返回值是boolean类型的。
逻辑删除员工的接口:
在实现逻辑删除之前,我们首先看一下逻辑删除和物理删除,它们有什么区别呢?
物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除数据。
逻辑删除:假删除,将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录。
在我们日常开发中,为了保留数据,经常会使用逻辑删除的方式进行数据删除,下面我们就来看看逻辑删除怎么实现的吧
1.数据库的修改,添加isdelete字段:
2.实体类的修改,添加对应的字段,并添加@TableLogic注解.
public class User {
@TableLogic
private Integer isdelete=0;//逻辑删除
}
3.添加全局配置:
#逻辑删除
#已删除
mybatis-plus.global-config.db-config.logic-delete-value=1
#未删除
mybatis-plus.global-config.db-config.logic-not-delete-value=0
0表示未删除,1表示已删除。
4.controller实现逻辑删除的方法:
//3.逻辑删除员工
@DeleteMapping("{id}")
public boolean removeEmpoy(@PathVariable Integer id) {
boolean b = employService.removeById(id);
return b;
}
根据员工的id删除员工,调用service层的remove方法进行删除。
批量删除员工的接口:
所谓批量删除就是能同时删除多个,调用mybatisplus自带的批量删除的方法
@DeleteMapping("/ids")
public boolean removeEmpployids(Integer[] ids) {
boolean b = employService.removeByIds(Arrays.asList(ids));
return b;
}
因为是删除多个,所以这里我们定义一个数组,然后调用removeByIds方法,需要注意的是,方法里边需要一个数,所以需要将数组转换成数。
修改员工信息的接口:
@PostMapping("update")
public Boolean update(@RequestBody User user) {
boolean b = employService.updateById(user);
if (b != false) {
return true;
} else {
return false;
}
}
使用post请求,直接调用mybatisplus自带的修改方法.updateById()通过用户的id修改用户的信息,返回值是一个boolean类型。
导入员工的数据:
在实现导入导出数据之前我们需要添加相关的依赖:
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
这里我们用的是easy-poi;
添加完依赖之后需要对要导出的字段添加对应的注解:
public class User {
@Excel(name="编号")
private Integer id; //员工的编号
@Excel(name="姓名")
private String name; //员工姓名
@Excel(name="性别")
private String sex; //员工性别
@Excel(name="籍贯",width = 15)
private String provices;//员工籍贯
@Excel(name="年龄")
private Integer age; //员工年龄
@Excel(name="薪水",width = 15)
private double salary; //员工薪水
@Excel(name="地址",width = 15)
private String address;//员工的地址
@Excel(name="院校",width = 20)
private String school;//毕业院校
}
controller层实现导入的接口:
@PostMapping("/import")
public RespPage importEmploy(MultipartFile file) {
//准备导入的参数
ImportParams params = new ImportParams();
//去掉标题行
params.setTitleRows(1);
try {
List<User> list = ExcelImportUtil.importExcel(file.getInputStream(), User.class, params);
if (employService.saveBatch(list)) ;
{
return RespPage.ok();
}
} catch (Exception e) {
e.printStackTrace();
}
return RespPage.error();
}
首先准备导入的参数
ImportParams params = new ImportParams();
去掉标题行,因为标题不能导入
params.setTitleRows(1);
以流的形式导入:
List<User> list = ExcelImportUtil.importExcel(file.getInputStream(), User.class, params);
导入数据成功之后更新数据:
if (employService.saveBatch(list)) ;
{
return RespPage.ok();
}
} catch (Exception e) {
e.printStackTrace();
}
return RespPage.error();
}
导出数据:
//7.导出员工信息
@GetMapping(value = "/export",produces = "application/octet-stream")
public void exportEmploy(HttpServletResponse response) {
//获取所有员工的数据
List<User> list = employService.list(null);
//准备导出参数
ExportParams params = new ExportParams("员工表", "员工表", ExcelType.HSSF);
Workbook workbook = ExcelExportUtil.exportExcel(params, User.class, list);
ServletOutputStream out = null;
try {
//流形式
response.setHeader("content-type","application/octet-stream");
//防止中文乱码
response.setHeader("content-disposition","attachment;filename="+
URLEncoder.encode("员工表.xls","UTF-8"));
out = response.getOutputStream();
workbook.write(out);
} catch (IOException e) {
e.printStackTrace();
}finally {
if (null!=out){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
首先获取到所有员工的数据,然后准备导出参数,调用导出的方法。
文件的上传和下载:
文件上传的方法
public int Upload(@RequestParam("file") MultipartFile file,File file1) {
if(!file.isEmpty()) {
// 获取文件名称,包含后缀
String fileName = file.getOriginalFilename();
//随机生成uuid作为文件的id
String id=UUID.randomUUID().toString();
// 存放在这个路径下:该路径是该工程目录下的static文件下
// 放在static下的原因是,存放的是静态文件资源,即通过浏览器输入本地服务器地址,加文件
名时是可以访问到的
String path = "D:\\java项目
\\svnbucket\\workspace\\employ\\springboootemploy\\src\\main\\resources\\static\\";
try {
// 该方法是对文件写入的封装,在util类中,导入该包即可使用
FileUtils.uploadFile(file.getBytes(), path, fileName);
} catch (Exception e) {
e.printStackTrace();
}
// 接着创建对应的实体类,将以下路径进行添加,然后通过数据库操作方法写入
file1.setFilepath("http://localhost:8001/"+fileName);
file1.setFilename(fileName);
fileMapper.insert(file1);
}
return 0;
}
文件上传的接口
@PostMapping("/upload")
public RespPage Upload(@RequestParam("file") MultipartFile file, com.tjise.employ.entity.File file1) {
fileService.Upload(file, file1);
String filename = file1.getFilename();
String filepath = file1.getFilepath();
Long id = file1.getId();
return RespPage.ok().data("id", id).data("filename", filename).data("filepath", filepath);
}
post请求,调用上传文件的接口
文件下载:
@GetMapping("/download/{filename}")
@ResponseBody
public RespPage download(HttpServletResponse response, @PathVariable("filename")
String filename) {
//下载后的文件名
String fileName = "文件下载.pdf";
//使用流的形式下载文件
try {
//加载文件
File file = new File("D:\\java项目\\workspace\\springboot02\\src\\main\\resources\\static\\"+filename);
InputStream is = new BufferedInputStream(new FileInputStream(file));
byte[] buffer = new byte[is.available()];
is.read(buffer);
is.close();
// 清空response
response.reset();
// 设置response的Header
response.addHeader("Content-Disposition", "attachment" + new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
response.addHeader("Content-Length", "" + file.length());
OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
response.setContentType("application/octet-stream");
toClient.write(buffer);
toClient.flush();
toClient.close();
return RespPage.ok();
} catch (Exception e) {
e.printStackTrace();
return RespPage.ok();
}
}
以二进制流的形式下载文件
员工管理接口的所有功能都写完了,下边我们实现一下权限管理模块。
菜单接口的开发
我们先来开发菜单接口,因为它不需要通过其他的表来获取信息的。比如用户表需要关联角色表,角色表需要关联菜单表,因此菜单表的增删改是比较简单的。
获取菜单导航和权限的链接是这样的/menu,然后我们的菜单导航的json数据应该是这样的:
注意导航中有个children,也就是子菜单,是个树形结构
所以在打代码的时候一定要注意这个关系的关联,我们的Menu实体类中有一个parentId,但是没有children,因此我们可以在Menu中添加一个children
com.tjise.employ.Menu
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("menu")
@Accessors(chain = true)
public class Menu {
private Integer id;
private String url;
private String path;
private String component;
private String name;
private String iconCls;
private Boolean requireAuth;
private Integer parentId;
@TableField(exist = false)
private List<Menu> children;
@TableField(exist = false)
private List<Role> roles;
@TableField(exist = false)
private List<Menu> children;
}
ok,我们开始编码:
public class MenuController {
@Autowired
private MenuService menuService;
//根据用户的id查询菜单列表
@GetMapping("/menu")
public List<Menu> getMenusByAdminId(){
return menuService.getMenusByAdminId();
}
}
根据用户的id查询菜单的信息
角色接口开发:
角色接口主要实现的功能就是对角色的增删改查和对角色分配菜单
角色的增加
//添加角色
@PostMapping("/add")
public RespBean addRole(@RequestBody Role role){
if (!role.getName().startsWith("ROLE_")){
role.setName("ROLE_"+role.getName());
}
if (roleService.save(role)){
return RespBean.success("添加成功!");
}
return RespBean.error("添加失败!");
}
添加角色需要注意的是,角色的统一编码,开头是以ROLE_开头的
角色的删除
//删除角色
@DeleteMapping("/delete/{rid}")
public RespBean deleteRole(@PathVariable Integer rid){
if(roleService.removeById(rid)){
return RespBean.success("删除成功!");
}
return RespBean.error("删除失败!");
}
根据角色的id删除角色
角色信息的修改
//修改角色信息
@PostMapping("/update")
public Boolean update(@RequestBody Role role) {
boolean b = roleService.updateById(role);
if (b != false) {
return true;
} else {
return false;
}
}
根据角色的名称查询角色
//根据角色的名称查询角色的信息
@PostMapping("/findByname")
public RespBean findByName(@RequestBody Role role){
Page<Role> page=new Page<>();
//构建条件
QueryWrapper<Role> wrapper = new QueryWrapper<>();
String zname = role.getZname();
//判断条件是否为空
if (!StringUtils.isEmpty(zname)) {
wrapper.like("zname", zname);
}
Page<Role> page1 = roleService.page(page, wrapper);
return RespBean.success("查询成功!",page1);
}
根据角色的id获取菜单的id
//根据角色id获取菜单的id
@GetMapping("/mid/{rid}")
public List<Integer> getMidByRid(@PathVariable Integer rid){
List<MenuRole> rid1 = menuRoleService.list(new QueryWrapper<MenuRole>().eq("rid", rid));
List<Integer> collect = rid1.stream().map(MenuRole::getMid)
.collect(Collectors.toList());
return collect;
}
给角色分配权限
mid方法
获取角色信息,因为我们不仅仅是在编辑角色的时候会用到这个方法,在回显关联菜单的时候也要被调用,因此我们需要把角色关联的所有菜单的id全部查询出来,也就是分配权限的功能。对应到前端,点击分配权限会弹出所有的菜单列表,然后根据角色已经关联的id回显勾选上已经关联过的,效果如下:
然后点击保存的时候,我们需要将角色的id和所有已经勾选上的菜单的id的数组一起传过来,如下:
//更新角色的菜单
@PutMapping("/")
public RespBean updateMenuRole(Integer rid,Integer[] mids){
return menuRoleService.updateMenuRole(rid,mids);
}
Service层
public interface MenuRoleService extends IService<MenuRole> {
/**
* 更新角色菜单
*/
RespBean updateMenuRole(Integer rid, Integer[] mids);
}
ServiceImpl层
public class MenuRoleServiceImpl extends ServiceImpl<MenuRoleMapper, MenuRole> implements MenuRoleService {
@Autowired
private MenuRoleMapper menuRoleMapper;
/**
* 更新角色菜单
*/
@Override
@Transactional
public RespBean updateMenuRole(Integer rid, Integer[] mids) {
menuRoleMapper.delete(new QueryWrapper<MenuRole>().eq("rid", rid));
if (null == mids || 0 == mids.length) {
return RespBean.success("更新成功!");
}
Integer result = menuRoleMapper.insertRecord(rid, mids);
if (result==mids.length){
return RespBean.success("更新成功!");
}
return RespBean.error("更新失败!");
}
}
首先执行删除的方法,删除角色对应的菜单,然后判断是否有对应的菜单,如果没有则执行插入的方法。
ok,角色管理到这儿就已经结束了。
用户接口的开发:
用户管理里边有个用户关联角色的操作和角色关联菜单的操作差不多,其他的增删改查的操作也都一样,多了一个重置密码的操作。
UserController
public class UserController {
@Autowired
private AdminService adminService;
@Autowired
private RoleService roleService;
@Autowired
private HttpServletRequest req;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private AdminRoleService adminRoleService;
//获取所有的角色
@GetMapping("/roles")
public List<Role> getAllRoles(){
return roleService.list();
}
//根据角色id获取菜单的id
@GetMapping("/rid/{adminid}")
public List<Integer> getMidByRid(@PathVariable Integer adminid){
List<AdminRole> rid1 = adminRoleService.list(new QueryWrapper<AdminRole>().eq("adminid", adminid));
List<Integer> collect = rid1.stream().map(AdminRole::getRid)
.collect(Collectors.toList());
return collect;
}
//根据用户名分页显示用户的信息
@GetMapping("/list")
public RespBean list(String username){
Page<Admin> pageData = adminService.page(getPage(), new QueryWrapper<Admin>()
.like(StrUtil.isNotBlank(username), "username", username));
pageData.getRecords().forEach(u -> {
u.setRoles(roleService.listRoleByUserId(Long.valueOf(u.getId())));
});
return RespBean.success("",pageData);
}
//添加用户
@PostMapping("/add")
public RespBean addUser(@RequestBody Admin admin){
//初始化密码
String password=passwordEncoder.encode(Rsultcode.DEFAULT_PASSWORD);
admin.setPassword(password);
if (adminService.save(admin)){
return RespBean.success("添加成功!");
}
return RespBean.error("添加失败!");
}
//删除用户
@DeleteMapping("/delete/{id}")
public RespBean deleteRole(@PathVariable Integer id){
if(adminService.removeById(id)){
return RespBean.success("删除成功!");
}
return RespBean.error("删除失败!");
}
//修改用户的信息
@PostMapping("/update")
public Boolean update(@RequestBody Admin admin) {
boolean b = adminService.updateById(admin);
if (b != false) {
return true;
} else {
return false;
}
}
//给用户分配角色
@PutMapping("/")
public RespBean rolePerm( Integer adminid,Integer[] rolesIds){
return adminRoleService.updateAdminRole(adminid,rolesIds);
}
//重置密码
@PostMapping("/repass/{adminid}")
public RespBean repass(@PathVariable Long adminid){
Admin admin=adminService.getById(adminid);
admin.setPassword(passwordEncoder.encode(Rsultcode.DEFAULT_PASSWORD));
adminService.updateById(admin);
return RespBean.success("");
}
最后的效果如下: