权限通用项目
权限通用项目
Maven-聚合工程
-
guigu-auth-parent
: 根目录,管理子模块 common
:公共类父模块 common-log
: 系统操作日志模块common-util
: 核心工具类service-util
:service
模块工具类 spring-security
业务模块
model
: 实体类模块service-system
: 系统权限模块
目录结构
Knife4j
-
文档
-
是什么
Knife4j
是一个集 Swagger2
和OpenAPI3
为一体的增强解决方案 -
快速开始
-
第一步:创建
Spring Boot
项目, 并且在 pom.xml
中引入 Knife4j
的依赖包, Maven
坐标如下 1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.10.5</version>
<scope>compile</scope>
</dependency> -
创建
Swagger
配置依赖,代码如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63package com.example.system.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
import java.util.ArrayList;
import java.util.List;
/**
* knife4j 配置信息
*/
public class Knife4jConfig {
public Docket adminApiConfig() {
List<Parameter> pars = new ArrayList<>();
ParameterBuilder tokenPar = new ParameterBuilder();
tokenPar.name("token")
// 描述字段支持 Markdown 语法
.description("用户token")
.defaultValue("")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false)
.build();
pars.add(tokenPar.build());
// 指定使用 Swagger2 规范
Docket adminApi = new Docket(DocumentationType.SWAGGER_2)
// 分组名称
.groupName("adminApi")
.apiInfo(adminApiInfo())
.select()
// 这里指定 Controller 扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.example"))
.paths(PathSelectors.regex("/admin/.*"))
.build()
.globalOperationParameters(pars);
return adminApi;
}
private ApiInfo adminApiInfo() {
return new ApiInfoBuilder()
.title("后台管理系统-API文档")
.description("本文档描述了后台管理系统微服务接口定义")
.version("1.0")
.contact(new Contact("coder-itl", "http://coderitl.github.io", "3327511395@qq.com"))
.build();
}
} -
Controller
添加注解 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48package com.example.system.controller;
import com.example.model.system.SysRole;
import com.example.system.service.SysRoleService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
public class SysRoleController {
private SysRoleService sysRoleService;
/**
* 查询所有
*
* @return
*/
public List<SysRole> findAll() {
log.info("findAll:..............................");
List<SysRole> list = sysRoleService.list();
return list;
}
/**
* 逻辑删除
*
* @param id
* @return
*/
public boolean removeRole( { Long id)
log.info("PathVariable Id value:{}", id);
return sysRoleService.removeById(id);
}
} -
启动,访问
-
定义统一返回结果对象
-
创建
统一返回结果状态信息类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35package com.example.common.result;
import lombok.Getter;
/**
* 统一返回结果状态信息类
*/
public enum ResultCodeEnum {
SUCCESS(200, "成功"),
FAIL(201, "失败"),
SERVICE_ERROR(2012, "服务异常"),
DATA_ERROR(204, "数据异常"),
ILLEGAL_REQUEST(205, "非法请求"),
REPEAT_SUBMIT(206, "重复提交"),
ARGUMENT_VALID_ERROR(210, "参数校验异常"),
LOGIN_AUTH(208, "未登陆"),
PERMISSION(209, "没有权限"),
ACCOUNT_ERROR(214, "账号不正确"),
PASSWORD_ERROR(215, "密码不正确"),
LOGIN_MOBLE_ERROR(216, "账号不正确"),
ACCOUNT_STOP(217, "账号已停用"),
NODE_ERROR(218, "该节点下有子节点,不可以删除");
private Integer code;
private String message;
private ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
} -
全局统一返回结果类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87package com.example.common.result;
import lombok.Data;
/**
* 全局统一返回结果类
*/
public class Result<T> {
//返回码
private Integer code;
//返回消息
private String message;
//返回数据
private T data;
public Result() {
}
// 返回数据
protected static <T> Result<T> build(T data) {
Result<T> result = new Result<T>();
if (data != null)
result.setData(data);
return result;
}
public static <T> Result<T> build(T body, Integer code, String message) {
Result<T> result = build(body);
result.setCode(code);
result.setMessage(message);
return result;
}
public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
Result<T> result = build(body);
result.setCode(resultCodeEnum.getCode());
result.setMessage(resultCodeEnum.getMessage());
return result;
}
public static <T> Result<T> ok() {
return Result.ok(null);
}
/**
* 操作成功
*
* @param data baseCategory1List
* @param <T>
* @return
*/
public static <T> Result<T> ok(T data) {
Result<T> result = build(data);
return build(data, ResultCodeEnum.SUCCESS);
}
public static <T> Result<T> fail() {
return Result.fail(null);
}
/**
* 操作失败
*
* @param data
* @param <T>
* @return
*/
public static <T> Result<T> fail(T data) {
Result<T> result = build(data);
return build(data, ResultCodeEnum.FAIL);
}
public Result<T> message(String msg) {
this.setMessage(msg);
return this;
}
public Result<T> code(Integer code) {
this.setCode(code);
return this;
}
} -
修改控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55package com.example.system.controller;
import com.example.common.result.Result;
import com.example.model.system.SysRole;
import com.example.system.service.SysRoleService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
public class SysRoleController {
private SysRoleService sysRoleService;
/**
* 查询所有
*
* @return
*/
public Result findAll() {
log.info("findAll:..............................");
List<SysRole> list = sysRoleService.list();
return Result.ok(list);
}
/**
* 逻辑删除
*
* @param id
* @return
*/
public Result removeRole( { Long id)
log.info("PathVariable Id value:{}", id);
boolean removeById = sysRoleService.removeById(id);
if (removeById) {
return Result.ok();
} else {
return Result.fail();
}
}
}
MybatisPlus-条件分页查询
-
第一步: 配置分页插件,
通过配置类实现 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27package com.example.system.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
public class MybatisPlusConfig {
/**
* @return
*/
public MybatisPlusInterceptor addPaginationInnerInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 向 Mybatis 过滤器链中添加分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
} -
第二步: 创建
Controller
方法、创建 Service
方法、创建 Mapper
方法 1
2
3
4
5
6
7
8
9
10// controller
public Result findPageQueryRole( { Long page, Long limit, SysRoleQueryVo sysRoleQueryVo)
// 创建 page 对象
Page<SysRole> pageparam = new Page<>(page, limit);
IPage<SysRole> pageModel = sysRoleService.queryPage(pageparam, sysRoleQueryVo);
return Result.ok(pageModel);
}
1
2
3
4
5
6
7// service && serviceImpl
// 条件分页查询
public IPage<SysRole> queryPage(Page<SysRole> pageparam, SysRoleQueryVo sysRoleQueryVo) {
IPage<SysRole> pageModel = baseMapper.queryPage(pageparam, sysRoleQueryVo);
return pageModel;
}1
2// mapper
IPage<SysRole> queryPage(Page<SysRole> pageparam, ; SysRoleQueryVo sysRoleQueryVo) -
第三步: 创建
mapper
的 xml
配置文件. 编写 sql
语句实现 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<mapper namespace="com.example.system.mapper.SysRoleMapper">
<!-- public IPage<SysRole> selectPage(Page<SysRole> pageparam, SysRoleQueryVo sysRoleQueryVo); -->
<resultMap id="roleMap" type="com.example.model.system.SysRole" autoMapping="true"></resultMap>
<sql id="columns">
id,role_name,role_code,description,create_time,update_time,is_deleted
</sql>
<select id="queryPage" resultMap="roleMap">
select
<include refid="columns"/>
from sys_role
<where>
<if test="vo.roleName != null and vo.roleName != ''">
and role_name like CONCAT('%',#{vo.roleName},'%')
</if>
and is_deleted = 0
</where>
order by id desc
</select>
</mapper> -
第四步: 修改配置文件关于
mapper
路径映射 1
2
3
4mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:com/example/mapper/*Mapper.xml
统一异常处理
全局异常处理
-
只要出现异常,
就执行这个处理 -
实现
1
2
3
4
5
6
7
8
9
public class GlobalExceptionHandle {
public Result error(Exception e) {
e.printStackTrace();
return Result.fail().message("执行了全局异常处理");
}
}全局异常捕获处理
特定异常处理
-
针对特定异常处理
-
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.example.system.exception;
import com.example.common.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
public class GlobalExceptionHandle {
public Result error(Exception e) {
e.printStackTrace();
return Result.fail().message("执行了全局异常处理");
}
// 特定异常处理
public Result error(ArithmeticException e) {
e.printStackTrace();
return Result.fail().message("执行了特定异常");
}
}处理特定异常
自定义异常处理
-
自己编写异常类,
手动抛出异常 -
实现
1
2
3
4
5
6
7
8
9
10
public class coderItlException extends RuntimeException {
// 状态码
private Integer code;
// 消息
private String message;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30package com.example.system.exception;
import com.example.common.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
public class GlobalExceptionHandle {
public Result error(Exception e) {
e.printStackTrace();
return Result.fail().message("执行了全局异常处理");
}
public Result error(ArithmeticException e) {
e.printStackTrace();
return Result.fail().message("执行了特定异常");
}
// 自定义异常处理
public Result error(coderItlException e) {
return Result.fail().code(e.getCode()).message(e.getMessage());
}
}1
2
3
4
5
6// 在需要的地方主动抛出异常
try {
System.out.println(1 / 0);
} catch (Exception e) {
throw new coderItlException(10010, "执行了自定义异常");
}执行自定义异常
代码生成器使用
-
添加依赖
1
2
3
4
5
6
7
8
9
10
11
12<!-- mybatis-plus 3.4.1 使用旧版本 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency> -
创建测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68package com.example.system;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
public class CodeGet {
public static void main(String[] args) {
// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2、全局配置
// 全局配置
GlobalConfig gc = new GlobalConfig();
// 使用时需要修改
gc.setOutputDir("E:\\auth-parent\\service-system" + "/src/main/java");
gc.setServiceName("%sService"); //去掉 Service 接口的首字母 I
gc.setAuthor("coder-itl");
gc.setOpen(false);
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
// 数据库名称需要修改
dsc.setUrl("jdbc:mysql://localhost:3306/guigu-auth?serverTimezone=GMT%2B8&useSSL=false");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.example");
pc.setModuleName("system"); // 模块名
pc.setController("controller");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
// 要生成的表
strategy.setInclude("sys_user");
strategy.setNaming(NamingStrategy.underline_to_camel);// 数据库表映射到实体的命名策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel);// 数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter 链式操作
strategy.setRestControllerStyle(true); //restful api风 格控制器
strategy.setControllerMappingHyphenStyle(true); // url中驼峰转连字符
mpg.setStrategy(strategy);
// 6、执行
mpg.execute();
}
}实现效果 (局部修改)
Token
-
介绍
JWT
是 JSON Web Token
的缩写,即 JSON Web
令牌,是一种自包含令牌。 是为了在网络应用环境间传递声明而执行的一种基于 JSON
的开放标准。 JWT
的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。 JWT
最重要的作用就是对 token
信息的防伪作用。 -
有效载荷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18iss: jwt
签发者
sub: 主题
aud: 接收jwt 的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt 都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性 token, 从而回避重放攻击。
// 除以上默认字段外,我们还可以自定义私有字段,如下例:
{
"name": "Helen",
"role": "editor",
"avatar": "helen.jpg"
}
// 请注意,默认情况下JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。JSON 对象也使用 Base64 URL 算法转换为字符串保存。 -
使用步骤
-
添加依赖
1
2
3
4<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency> -
创建工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63package com.example.common.utils;
import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;
import java.util.Date;
/**
* 生成JSON Web 令牌的工具类
*/
public class JwtHelper {
private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;
private static String tokenSignKey = "123456";
public static String createToken(Long userId, String username) {
String token = Jwts.builder()
.setSubject("AUTH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("userId", userId)
.claim("username", username)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
public static String getUserId(String token) {
try {
if (StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
String userId = (String) claims.get("userId");
return userId;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String getUsername(String token) {
try {
// StringUtils: 是 web 包下的
if (StringUtils.isEmpty(token)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String) claims.get("username");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// 测试
public static void main(String[] args) {
String token = JwtHelper.createToken("1", "admin");
System.out.println(token);
System.out.println(JwtHelper.getUserId(token));
System.out.println(JwtHelper.getUsername(token));
}
}
-
-
MD5
加密工具类 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32package com.example.common.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class MD5 {
public static String encrypt(String strSrc) {
try {
char hexChars[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f'};
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出错!!+" + e);
}
}
}
前端
vue-admin-template
-
下载
-
解压
-
依赖下载
1
2
3
4
5
6# 配置镜像
npm config set registry http://registry.npmmirror.com
yarn config set registry http://registry.npmmirror.com
# 切换回默认镜像源
npm config set registryhttps://registry.npmjs.org1
2
3
4# npm
npm install
# yarn
yarn install -
启动
1
2
3
4# npm 启动
npm run dev
# yarn 启动
yarn run dev通过 package.json
查看 scripts
确定启动命令 -
访问
访问首页 -
依赖下载出错问题
提示下载,直接下载无效 -
解决方案
-
先删除
node_modules
-
在下载如下依赖
1
npm install --save core-js
-
再次执行
1
2# /: 或者
npm install / yarn install
-
-
Vue-admin-template 文件目录
-
文件信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56|-dist 生产环境打包生成的打包项目
|-mock 产生模拟数据
|-public 包含会被自动打包到项目根路径的文件夹
|-index.html 唯一的页面
|-src
|-api 包含接口请求函数模块
|-table.js 表格列表mock 数据接口的请求函数
|-user.js 用户登陆相关mock 数据接口的请求函数
|-assets 组件中需要使用的公用资源
|-404_images 404页面的图片
|-components 非路由组件
|-SvgIcon svg图标组件
|-Breadcrumb 面包屑组件(头部水平方向的层级组件)
|-Hamburger 用来点击切换左侧菜单导航的图标组件
|-icons
|-svg 包含一些svg 图片文件
|-index.js 全局注册SvgIcon 组件, 加载所有 svg 图片并暴露所有 svg 文件名的数组
|-layout
|-components 组成整体布局的一些子组件
|-mixin 组件中可复用的代码
|-index.vue 后台管理的整体界面布局组件
|-router
|-index.js 路由器
|-store
|-modules
|-app.js 管理应用相关数据
|-settings.js 管理设置相关数据
|-user.js 管理后台登陆用户相关数据
|-getters.js 提供子模块相关数据的getters 计算属性
|-index.js vuex的 store
|-styles
|-xxx.scss 项目组件需要使用的一些样式(使用 scss)
|-utils 一些工具函数
|-auth.js 操作登陆用户的token cookie
|-get-page-title.js 得到要显示的网页title
|-request.js axios二次封装的模块
|-validate.js 检验相关工具函数
|-index.js 日期和请求参数处理相关工具函数
|-views 路由组件文件夹
|-dashboard 首页
|-login 登陆
|-App.vue 应用根组件
|-main.js 入口js
|-permission.js 使用全局守卫实现路由权限控制的模块
|-settings.js 包含应用设置信息的模块
|-.env.development 指定了开发环境的代理服务器前缀路径
|-.env.production 指定了生产环境的代理服务器前缀路径
|-.eslintignore eslint的忽略配置
|-.eslintrc.js eslint的检查配置
|-.gitignore git的忽略配置
|-.npmrc 指定npm 的淘宝镜像和 sass 的下载地址
|-babel.config.js babel的配置
|-jsconfig.json 用于vscode 引入路径提示的配置
|-package.json 当前项目包信息
|-package-lock.json 当前项目依赖的第三方包的精确信息
|-vue.config.js webpack相关配置 (如: 代理服务器)
登录退出改造
-
通过
network
查看到登录后发起的请求 -
login
1
2
3
4
5
6
7// POST
{
"code":20000,
"data":{
"token":"admin-token"
}
} -
info
1
2
3
4
5
6
7
8
9
10
11
12// GET
{
"code":20000,
"data":{
"roles":[
"admin"
],
"introduction":"I am a super administrator",
"avatar":"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif",
"name":"Super Admin"
}
}分析
-
-
通过上述分析,
后端模拟实现 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38package com.example.system.controller;
import com.example.common.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
public class IndexController {
public Result login() {
Map<String, Object> map = new HashMap<>();
map.put("token", "admin-token");
return Result.ok(map);
}
public Result info() {
Map<String, Object> map = new HashMap<>();
map.put("roles", "[admin]");
map.put("introduction", "I am a super administrator");
map.put("avatar", "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
map.put("name", "Super Admin");
return Result.ok(map);
}
} -
配置后端路径
IP
+ 端口号-
修改
vue.config.js
1
2
3
4
5
6
7
8
9
10// before: require('./mock/mock-server.js')
proxy: {
'/dev-api': { // 匹配所有以 '/dev-api'开头的请求路径
target: 'http://localhost:8800',
changeOrigin: true, // 支持跨域
pathRewrite: { // 重写路径: 去掉路径中开头的'/dev-api'
'^/dev-api': ''
}
}
} -
修改
src/utils
里面的 request.js
文件 1
2
3
4
5
6
7
8
9
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 20000 修改为自己所需要的值信息
-
-
修改登录接口和提交方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// src/api/user.js
export function login(data) {
return request({
// 后端提供的接口
url: "/admin/system/index/login",
method: "post",
data,
});
}
export function getInfo(token) {
return request({
url: "/admin/system/index/info",
method: "get",
params: { token },
});
} -
退出实现
-
实现一: 后端实现,前端调用接口
1
2
3
4
5
public Result logout() {
return Result.ok();
}1
2
3
4
5
6
7
8
9// src/api/user
export function logout() {
return request({
url: "/admin/system/index/logout",
method: "post",
});
} -
实现二: 修改前端信息
src/store/modules/user.js
1
2
3
4
5
6
7
8
9login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
})
},
-
角色列表实现
-
实现步骤
-
添加角色管理路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* sysRole start */
{
path: "/system",
component: Layout,
redirect: "/system/sysUser",
name: "System",
meta: { title: "系统管理", icon: "el-icon-s-help" },
children: [
{
path: "sysUser",
name: "SysUser",
component: () => import("@/views/system/sysUser/list"),
meta: { title: "用户管理", icon: "tree" },
},
{
path: "sysRole",
name: "SysRole",
component: () => import("@/views/system/sysRole/list"),
meta: { title: "角色管理", icon: "table" },
},
],
},
/* sysRole end */ -
创建路由对应的页面
1
2
3
4
5
6
7
8
9<template>
<div class="app-container">角色列表 </div>
</template>
<script>
export default {}
</script>
<style>
</style>根据路由创建页面 -
在
api
目录创建 js
文件, 定义角色管理接口 1
2
3
4
5
6
7
8
9
10
11
12import request from "@/utils/request";
const API_PATH = "/admin/system/sysRole/";
export default {
// 分页查询
getPageList(page, limit, searchObj) {
return request({
url: `${API_PATH}/${page}/${limit}`,
method: "get",
params: searchObj,
});
},
}; -
在具体功能页面调用
api
定义的方法获取接口返回数据 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<script>
// 引入定义接口的 js 文件
import api from '@/api/role/role'
export default {
data() {
return {
list: [], // 角色列表
total: 0, // 总记录数
page: 1, // 当前页
limit: 3, // 每页显示记录数
searchObj: {}, // 条件查询封装对象
}
},
// 页面渲染之前执行
created() {
this.fetchData()
},
methods: {
// 条件分页查询列表,pageNum=1 代表默认值
async fetchData(pageNum = 1) {
// 页数赋值
this.page = pageNum
// 发起 ajax 调用
const { data: res } = await api.getPageList(
this.page,
this.limit,
this.searchObj
)
console.log(res)
// 角色列表赋值
this.list = res.records
this.total = res.total
},
},
}
</script>数据模型 -
把接口返回的数据进行处理,
使用 ElementUI
显示 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79<template>
<div class="app-container">
<!-- 表格 -->
<el-table
v-loading="listLoading"
:data="list"
stripe
border
style="width: 100%; margin-top: 10px"
>
<el-table-column label="序号" width="70" align="center">
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="roleName" label="角色名称" />
<el-table-column prop="roleCode" label="角色编码" />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button
type="primary"
icon="el-icon-edit"
size="mini"
@click="edit(scope.row.id)"
title="修改"
/>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="removeDataById(scope.row.id)"
title="删除"
/>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
// 引入定义接口的 js 文件
import api from '@/api/role/role'
export default {
data() {
return {
list: [], // 角色列表
total: 0, // 总记录数
page: 1, // 当前页
limit: 3, // 每页显示记录数
searchObj: {}, // 条件查询封装对象
}
},
// 页面渲染之前执行
created() {
this.fetchData()
},
methods: {
// 条件分页查询列表,pageNum=1 代表默认值
async fetchData(pageNum = 1) {
// 页数赋值
this.page = pageNum
// 发起 ajax 调用
const { data: res } = await api.getPageList(
this.page,
this.limit,
this.searchObj
)
console.log(res)
// 角色列表赋值
this.list = res.records
this.total = res.total
},
},
}
</script>
<style>
</style>
-
-
SysRole
页面实现 角色管理页面 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127<template>
<div class="app-container">
<!--查询表单-->
<div class="search-div">
<el-form label-width="70px" size="small">
<el-row>
<el-col :span="24">
<el-form-item label="角色名称">
<el-input
style="width: 100%"
v-model="searchObj.roleName"
placeholder="角色名称"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row style="display: flex">
<el-button
type="primary"
icon="el-icon-search"
size="mini"
@click="fetchData()"
>搜索
>
<el-button icon="el-icon-refresh" size="mini" @click="resetData"
>重置
>
</el-row>
</el-form>
</div>
<!-- 表格 -->
<el-table
v-loading="listLoading"
:data="list"
stripe
border
style="width: 100%; margin-top: 10px"
>
<el-table-column label="序号" width="70" align="center">
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="roleName" label="角色名称" />
<el-table-column prop="roleCode" label="角色编码" />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button
type="primary"
icon="el-icon-edit"
size="mini"
@click="edit(scope.row.id)"
title="修改"
/>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="removeDataById(scope.row.id)"
title="删除"
/>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
:current-page="page"
:total="total"
:page-size="limit"
style="padding: 30px 0; text-align: center"
layout="total, prev, pager, next, jumper"
@current-change="fetchData"
/>
</div>
</template>
<script>
// 引入定义接口的 js 文件
import api from '@/api/role/role'
export default {
data() {
return {
listLoading: true, // 是否显示加载图标
list: [], // 角色列表
total: 0, // 总记录数
page: 1, // 当前页
limit: 3, // 每页显示记录数
searchObj: {}, // 条件查询封装对象
}
},
// 页面渲染之前执行
created() {
this.fetchData()
},
methods: {
// 条件分页查询列表,pageNum=1 代表默认值
async fetchData(pageNum = 1) {
// 页数赋值
this.page = pageNum
// 发起 ajax 调用
const { data: res } = await api.getPageList(
this.page,
this.limit,
this.searchObj
)
this.listLoading = false
console.log(res)
// 角色列表赋值
this.list = res.records
this.total = res.total
},
// 重置
resetData() {
// 清空表单
this.searchObj = {}
// 显示所有数据
this.fetchData()
},
},
}
</script>
<style>
</style> -
角色删除
-
前端请求接口定义
1
2
3
4
5
6
7
8// api
// 根据 id 删除
removeById(id) {
return request({
url: `${API_PATH}/remove/${id}`,
method: "delete",
});
}, -
事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 根据 id 删除
removeDataById(id) {
// debugger
this.$confirm('此操作将永久删除该角色, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
// promise
// 点击确定,远程调用ajax
return api.removeById(id)
})
.then((response) => {
this.fetchData(this.page)
this.$message.success(response.message || '删除成功')
})
}, -
Javascript
处理数值问题 - 由于
Javascript
对数值的精度问题,可能导致错误 - 解决方案: 修改实体类中数据类型
- 由于
-
-
添加角色实现
-
添加按钮
1
2
3
4
5
6
7
8
9
10
11
12
13<div class="tools-div">
<el-button type="success" icon="el-icon-plus" size="mini" @click="add">添 加 </el-button>
</div>
// 放在全局样式
<style>
.search-div {
padding:10px;border: 1px solid #EBEEF5;border-radius:3px;
}
.tools-div {
margin-top: 10px;padding:10px;border: 1px solid #EBEEF5;border-radius:3px;
}
</style> -
api
1
2
3
4
5
6
7
8
9// 添加角色
saveRole(role) {
return request({
url: `${API_PATH}/save`,
method: "post",
// 后端存在 @RequestBody 注解时,需要前端传递 json 数据
data: role,
});
}, -
element-ui
弹窗 1
2
3
4
5
6
7
8
9
10
11
12
13
14<el-dialog title="添加
/ 修改" :visible.sync="dialogVisible" width="40%" >
<el-form ref="dataForm" :model="sysRole" label-width="150px" size="small" style="padding-right: 40px;">
<el-form-item label="角色名称">
<el-input v-model="sysRole.roleName"/>
</el-form-item>
<el-form-item label="角色编码">
<el-input v-model="sysRole.roleCode"/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false" size="small" icon="el-icon-refresh-right">取 消 </el-button>
<el-button type="primary" icon="el-icon-check" @click="saveOrUpdate()" size="small">确 定 </el-button>
</span>
</el-dialog> -
事件
1
2
3// 初始值
sysRole: {}, // 封装添加的表单数据
dialogVisible: false, // 添加角色弹框1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// 添加添加按钮出现弹窗
add() {
this.dialogVisible = true
// 清空原始数据,便于添加
this.sysRole = {}
},
// 添加或更新事件
saveOrUpdate() {
// 判断添加是修改还是更新
if (!this.sysRole.id) {
// 添加
this.saveRole()
} else {
this.updateRole()
}
},
// 添加角色事件
saveRole() {
api.saveRole(this.sysRole).then((res) => {
// 1. 出现成功或失败的提示信息
this.$message({
type: 'success',
message: '添加成功',
})
// 2. 关闭弹窗
this.dialogVisible = false
// 3. 刷新数据
this.fetchData()
})
},
-
-
编辑按钮事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 根据 id 删除
removeDataById(id) {
// debugger
this.$confirm('此操作将永久删除该角色, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// promise
// 点击确定,远程调用 ajax
api.removeById(id).then((res) => {
this.$message.success(res.message || '删除成功')
})
this.fetchData(this.page)
})
}, -
更新按钮事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 更新角色事件
updateRole() {
api.update(this.sysRole).then((res) => {
// 1. 出现成功或失败的提示信息
this.$message({
type: 'success',
message: '更新成功',
})
// 2. 关闭弹窗
this.dialogVisible = false
// 3. 刷新数据
this.fetchData()
})
},
// 修改-回显数据
edit(id) {
// 显示弹窗
this.dialogVisible = true
// 修改数据
api.getRoleId(id).then((res) => {
// 将获取到的数据赋值,通过双向绑定回显数据
this.sysRole = res.data
})
},添加 / 修改弹窗 编辑回显数据 -
批量删除
-
定义
api
1
2
3
4
5
6
7
8// 根据 id 批量删除
batchRemove(idList) {
return request({
url: `${API_PATH}/batchRemove`,
method: "delete",
data: idList,
});
}, -
前后端参数数据模型
数据模型 -
复选框实现
1
2// 在 table 表头内第一行添加
<el-table-column type="selection" />添加位置 实现效果 -
复选框事件
1
2
3
4
5
6
7
8
9// el-table 添加
<el-table
v-loading="listLoading"
:data="list"
stripe
border
style="width: 100%; margin-top: 10px"
@selection-change="handleSelectionChange"
>1
2// 初始值
selectValue: [], // 复选框选择内容封装1
2
3
4// 复选框发生变化
(选中时) 时执行
handleSelectionChange(selection) {
this.selectValue = selection
}, -
批量删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 批量删除实现
batchRemove() {
// 判断是否可以删除
if (this.selectValue.length == 0) {
this.$message.warning('请选择要删除的记录')
return
}
this.$confirm('此操作将永久删除该角色, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// promise
var idList = []
this.selectValue.forEach((item) => {
idList.push(item.id)
})
// 点击确定,远程调用 ajax
api.batchRemove(idList).then((res) => {
this.$message.success(res.message || '批量删除成功')
// 刷新页面数据
this.fetchData(this.page)
})
})
},
-
用户管理模块
-
条件分页查询用户
1
2
3
4
5
6
7
8// 条件分页查询用户
public Result list( { Long page, Long limit, SysUserQueryVo sysUserQueryVo)
Page<SysUser> pageParam = new Page<>(page, limit);
IPage<SysUser> pageModel = sysUserService.queryUserPage(pageParam, sysUserQueryVo);
return Result.ok(pageModel);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<mapper namespace="com.example.system.mapper.SysUserMapper">
<!-- IPage<SysUser> queryUserPage(Page pageParam, SysUserQueryVo sysUserQueryVo); -->
<resultMap id="sysUserMap" type="com.example.model.system.SysUser" autoMapping="true"></resultMap>
<sql id="columns">
id ,
username ,
password ,
name ,
phone ,
head_url ,
dept_id ,
post_id ,
description ,
status ,
create_time ,
update_time ,
is_deleted
</sql>
<select id="queryUserPage" resultMap="sysUserMap">
select
<include refid="columns"/>
from sys_user
<where>
<if test="vo.keyword != null and vo.keyword != ''">
and (username like CONCAT('%',#{vo.keyword},'%') or name like CONCAT('%',#{vo.keyword},'%') or phone
like CONCAT('%',#{vo.keyword},'%'))
</if>
<if test="vo.createTimeBegin != null and vo.createTimeBegin != ''">
and create_time >= #{vo.createTimeBegin}
</if>
<if test="vo.createTimeEnd != null and vo.createTimeEnd != ''">
and create_time <= #{vo.createTimeEnd}
</if>
and is_deleted = 0
</where>
order by id desc
</select>
</mapper> -
用户添加
1
2
3
4
5
6
7
8
9
10
11// 用户添加
public Result save( { SysUser sysUser)
boolean isSuccess = sysUserService.save(sysUser);
if (isSuccess) {
return Result.ok();
} else {
return Result.fail();
}
}1
2
3
4
5
6
7
8save(role) {
return request({
url: `${api_name}/save`,
method: "post",
data: role,
});
},
-
用户修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 用户修改
// 1. 1 根据 id 查询
public Result getUserById( { Long id)
SysUser sysUser = sysUserService.getById(id);
return Result.ok(sysUser);
}
// 1.2 更新
public Result update( { SysUser sysUser)
boolean isSuccess = sysUserService.updateById(sysUser);
if (isSuccess) {
return Result.ok();
} else {
return Result.fail();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
getById(id) {
return request({
url: `${api_name}/get/${id}`,
method: "get",
});
},
update(role) {
return request({
url: `${api_name}/update`,
method: "put",
data: role,
});
}, -
用户删除
1
2
3
4
5
6
7
8
9
10
11
12// 用户删除
public Result removeById( { Long id)
boolean isSuccess = sysUserService.removeById(id);
if (isSuccess) {
return Result.ok();
} else {
return Result.fail();
}
}1
2
3
4
5removeById(id) {
return request({
url: `${api_name}/remove/${id}`,
method: "delete",
}); -
为用户分配角色
一个用户可以有多个角色
一个角色可以有多个用户
关系描述 -
接口分析
- 进入分配页面: 获取已分配角色与全部角色,
进行页面展示 - 查询用户已经分配的角色和全部角色,页面显示
- 保存分配角色: 删除之前分配的角色和保存现在分配的角色
- 删除之前已经分配的角色
- 保存新分配的角色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// TODO: 2022-12-15 16:54 添加角色分配接口
public Result toAssign( { String userId)
Map<String, Object> roleMap = sysRoleService.getRolesByUserId(userId);
return Result.ok(roleMap);
}
// TODO: 用户重新分配角色
public Result doAssign( { AssginRoleVo assginRoleVo)
// 给用户分配角色
sysRoleService.doAssign(assginRoleVo);
return Result.ok();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58package com.example.system.service.impl;
public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {
private SysUserRoleMapper sysUserRoleMapper;
// 条件分页查询
public IPage<SysRole> queryPage(Page<SysRole> pageparam, SysRoleQueryVo sysRoleQueryVo) {
IPage<SysRole> pageModel = baseMapper.queryPage(pageparam, sysRoleQueryVo);
return pageModel;
}
// TODO: 难点一
public Map<String, Object> getRolesByUserId(String userId) {
// 获取所有角色
List<SysRole> roleList = baseMapper.selectList(null);
// 根据用户 id 查询,也就是查询用户已经分配的角色
QueryWrapper<SysUserRole> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
List<SysUserRole> userRoles = sysUserRoleMapper.selectList(wrapper);
// 从 userRoles 集合中获取所有角色 id
List<String> userRoleIds = new ArrayList<>();
userRoles.forEach(userRole -> {
String roleId = userRole.getRoleId();
userRoleIds.add(roleId);
});
// 封装到 map 集合
Map<String, Object> returnMap = new HashMap<>();
// 所有角色
returnMap.put("allRoles", roleList);
// 用户分配角色 id 集合
returnMap.put("userRoleIds", userRoleIds);
return returnMap;
}
// 给用户分配角色
public void doAssign(AssginRoleVo assginRoleVo) {
// 根据用户id 删除之前分配角色
QueryWrapper<SysUserRole> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", assginRoleVo.getUserId());
sysUserRoleMapper.delete(wrapper);
// 获取所有角色 id,添加角色用户关系表
// 角色 id 列表
List<String> roleIdList = assginRoleVo.getRoleIdList();
for (String roleId : roleIdList) {
SysUserRole sysUserRole = new SysUserRole();
sysUserRole.setUserId(assginRoleVo.getUserId());
sysUserRole.setRoleId(roleId);
sysUserRoleMapper.insert(sysUserRole);
}
}
}分配角色 - 进入分配页面: 获取已分配角色与全部角色,
-
菜单管理模块
-
接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public Result findNodes() {
List<SysMenu> list = sysMenuService.findNodes();
return Result.ok(list);
}
public Result remove( { String id)
sysMenuService.removeMenuById(id);
return Result.ok();
}
// 添加菜单
public Result save( { SysMenu sysMenu)
boolean isSuccess = sysMenuService.save(sysMenu);
if (isSuccess) {
return Result.ok().message("添加成功");
}
return Result.fail().message("添加失败");
}
// 根据 id 查询
// 添加菜单
public Result save( { String id)
SysMenu sysMenu = sysMenuService.getById(id);
return Result.ok(sysMenu);
}
// 修改
public Result update( { SysMenu sysMenu)
boolean isSuccess = sysMenuService.updateById(sysMenu);
if (isSuccess) {
return Result.ok().message("更新成功");
}
return Result.fail().message("更新失败");
}
public Result remove( { String id)
boolean isSuccess = sysMenuService.removeById(id);
if (isSuccess) {
return Result.ok().message("删除成功");
}
return Result.fail().message("删除失败");
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.example.system.service.impl;
/**
* <p>
* 菜单表 服务实现类
* </p>
*
* @author coder-itl
* @since 2022-12-17
*/
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
/**
* 菜单列表,树形显示
*
* @return
*/
public List<SysMenu> findNodes() {
// 获取所有菜单
List<SysMenu> sysMenuList = baseMapper.selectList(null);
// 所有菜单数据转换为要求的数据格式
List<SysMenu> resultList = MenuHelper.buildTree(sysMenuList);
return resultList;
}
/**
* 删除菜单
*
* @param id
*/
public void removeMenuById(String id) {
// 查询当前删除菜单下面是否有子菜单
// 根据 id==parentId
QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
wrapper.eq("parent_id", id);
Integer count = baseMapper.selectCount(wrapper);
if (count > 0) {
// 子菜单存在
throw new coderItlException(201, "请先删除子菜单");
}
// 调用删除
baseMapper.deleteById(id);
}
} -
前端路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18{
name: "sysMenu",
path: "sysMenu",
component: () => import("@/views/system/sysMenu/list"),
meta: {
title: "菜单管理",
icon: "el-icon-s-unfold",
},
},
{
path: "assignAuth",
component: () => import("@/views/system/sysRole/assignAuth"),
meta: {
activeMenu: "/system/sysRole",
title: "角色授权",
},
hidden: true,
}, -
工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54package com.example.system.utils;
import com.example.model.system.SysMenu;
import java.util.ArrayList;
import java.util.List;
public class MenuHelper {
// 构建树形结构
public static List<SysMenu> buildTree(List<SysMenu> sysMenuList) {
// 创建集合封装最终数据
List<SysMenu> trees = new ArrayList<>();
// 遍历所有菜单集合
for (SysMenu sysMenu : sysMenuList) {
// 找到递归入口
if (sysMenu.getParentId().longValue() == 0) {
trees.add(findChildren(sysMenu, sysMenuList));
}
}
return trees;
}
/**
* 从根节点进行递归查询,查询是否存在子节点
* 判断 id == parentId 是否相同,如果相同是子节点, 进行数据封装
*
* @param sysMenu
* @param treeNodes
* @return
*/
private static SysMenu findChildren(SysMenu sysMenu, List<SysMenu> treeNodes) {
// 数据初始化
sysMenu.setChildren(new ArrayList<SysMenu>());
// 遍历递归查找
for (SysMenu it : treeNodes) {
// 获取当前菜单 id
// String id = sysMenu.getId();
// 数据类型转换处理
// long lid = Long.parseLong(id);
// 获取所有菜单的 parentId
// Long parentId = it.getParentId();
// 比对
if (Long.parseLong(sysMenu.getId()) == it.getParentId()) {
if (sysMenu.getChildren() == null) {
sysMenu.setChildren(new ArrayList<SysMenu>());
}
// 递归查找
sysMenu.getChildren().add(findChildren(it, treeNodes));
}
}
return sysMenu;
}
} -
前端接口定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52// sysMenu.js
import request from '@/utils/request'
/*菜单管理相关的 API 请求函数
*/
const api_name = '/admin/system/sysMenu'
export default {
/*
获取权限(菜单 / 功能) 列表
*/
findNodes() {
return request({
url: `${api_name}/findNodes`,
method: 'get'
})
},
/*
删除
*/
removeById(id) {
return request({
url: `${api_name}/remove/${id}`,
method: "delete"
})
},
/*
保存
*/
save(sysMenu) {
return request({
url: `${api_name}/save`,
method: "post",
data: sysMenu
})
},
/*
更新
*/
updateById(sysMenu) {
return request({
url: `${api_name}/update`,
method: "put",
data: sysMenu
})
}
} -
渲染
页面渲染 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376<template>
<div class="app-container">
<!-- 工具条 -->
<div class="tools-div">
<el-button type="success" icon="el-icon-plus" size="mini" @click="add()"
>添 加
>
</div>
<el-table
:data="sysMenuList"
style="width: 100%; margin-bottom: 20px; margin-top: 10px"
row-key="id"
border
:default-expand-all="false"
:tree-props="{ children: 'children' }"
>
<el-table-column prop="name" label="菜单名称" width="160" />
<el-table-column label="图标">
<template slot-scope="scope">
<i :class="scope.row.icon"></i>
</template>
</el-table-column>
<el-table-column prop="perms" label="权限标识" width="160" />
<el-table-column prop="path" label="路由地址" width="120" />
<el-table-column prop="component" label="组件路径" width="160" />
<el-table-column prop="sortValue" label="排序" width="60" />
<el-table-column label="状态" width="80">
<template slot-scope="scope">
<el-switch v-model="scope.row.status === 1" disabled="true">
</el-switch>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="180" align="center" fixed="right">
<template slot-scope="scope">
<el-button
type="success"
v-if="scope.row.type !== 2"
icon="el-icon-plus"
size="mini"
@click="add(scope.row)"
title="添加下级节点"
/>
<el-button
type="primary"
icon="el-icon-edit"
size="mini"
@click="edit(scope.row)"
title="修改"
/>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="removeDataById(scope.row.id)"
title="删除"
:disabled="scope.row.children.length > 0"
/>
</template>
</el-table-column>
</el-table>
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="40%">
<el-form
ref="dataForm"
:model="sysMenu"
label-width="150px"
size="small"
style="padding-right: 40px"
>
<el-form-item label="上级部门" v-if="sysMenu.id === ''">
<el-input v-model="sysMenu.parentName" disabled="true" />
</el-form-item>
<el-form-item label="菜单类型" prop="type">
<el-radio-group v-model="sysMenu.type" :disabled="typeDisabled">
<el-radio :label="0" :disabled="type0Disabled">目录 </el-radio>
<el-radio :label="1" :disabled="type1Disabled">菜单 </el-radio>
<el-radio :label="2" :disabled="type2Disabled">按钮 </el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单名称" prop="name">
<el-input v-model="sysMenu.name" />
</el-form-item>
<el-form-item label="图标" prop="icon" v-if="sysMenu.type !== 2">
<el-select v-model="sysMenu.icon" clearable>
<el-option
v-for="item in iconList"
:key="item.class"
:label="item.class"
:value="item.class"
>
<span style="float: left">
<i :class="item.class"></i>
<!-- 如果动态显示图标,这里添加判断 -->
</span>
<span style="padding-left: 6px">{{ item.class }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="排序">
<el-input-number
v-model="sysMenu.sortValue"
controls-position="right"
:min="0"
/>
</el-form-item>
<el-form-item prop="path">
<span slot="label">
<el-tooltip content="访问的路由地址,如:`sysUser`" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
路由地址
</span>
<el-input v-model="sysMenu.path" placeholder="请输入路由地址" />
</el-form-item>
<el-form-item prop="component" v-if="sysMenu.type !== 0">
<span slot="label">
<el-tooltip
content="访问的组件路径,如:`system/user/index`,默认在`views`目录下"
placement="top"
>
<i class="el-icon-question"></i>
</el-tooltip>
组件路径
</span>
<el-input v-model="sysMenu.component" placeholder="请输入组件路径" />
</el-form-item>
<el-form-item v-if="sysMenu.type === 2">
<el-input
v-model="sysMenu.perms"
placeholder="请输入权限标识"
maxlength="100"
/>
<span slot="label">
<el-tooltip
content="控制器中定义的权限字符,如:@PreAuthorize(hasAuthority('bnt.sysRole.list'))"
placement="top"
>
<i class="el-icon-question"></i>
</el-tooltip>
权限字符
</span>
</el-form-item>
<el-form-item label="状态" prop="type">
<el-radio-group v-model="sysMenu.status">
<el-radio :label="1">正常 </el-radio>
<el-radio :label="0">停用 </el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button
@click="dialogVisible = false"
size="small"
icon="el-icon-refresh-right"
>取 消
>
<el-button
type="primary"
icon="el-icon-check"
@click="saveOrUpdate()"
size="small"
>确 定
>
</span>
</el-dialog>
</div>
</template>
<script>
import api from '@/api/system/sysMenu'
const defaultForm = {
id: '',
parentId: '',
name: '',
type: 0,
path: '',
component: '',
perms: '',
icon: '',
sortValue: 1,
status: 1,
}
export default {
// 定义数据
data() {
return {
sysMenuList: [],
expandKeys: [], // 需要自动展开的项
typeDisabled: false,
type0Disabled: false,
type1Disabled: false,
type2Disabled: false,
dialogTitle: '',
dialogVisible: false,
sysMenu: defaultForm,
saveBtnDisabled: false,
iconList: [
{
class: 'el-icon-s-tools',
},
{
class: 'el-icon-s-custom',
},
{
class: 'el-icon-setting',
},
{
class: 'el-icon-user-solid',
},
{
class: 'el-icon-s-help',
},
{
class: 'el-icon-phone',
},
{
class: 'el-icon-s-unfold',
},
{
class: 'el-icon-s-operation',
},
{
class: 'el-icon-more-outline',
},
{
class: 'el-icon-s-check',
},
{
class: 'el-icon-tickets',
},
{
class: 'el-icon-s-goods',
},
{
class: 'el-icon-document-remove',
},
{
class: 'el-icon-warning',
},
{
class: 'el-icon-warning-outline',
},
{
class: 'el-icon-question',
},
{
class: 'el-icon-info',
},
],
}
},
//当页面加载时获取数据
created() {
this.fetchData()
},
methods: {
//调用 api 层获取数据库中的数据
fetchData() {
console.log('加载列表')
api.findNodes().then((response) => {
this.sysMenuList = response.data
console.log(this.sysMenuList)
})
},
//根据 id 删除数据
removeDataById(id) {
// debugger
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
// promise
// 点击确定,远程调用ajax
return api.removeById(id)
})
.then((response) => {
this.fetchData()
this.$message({
type: 'success',
message: response.message,
})
})
.catch(() => {
this.$message.info('取消删除')
})
},
//弹出添加或更新的表单
add(row) {
debugger
this.typeDisabled = false
this.dialogTitle = '添加下级节点'
this.dialogVisible = true
this.sysMenu = Object.assign({}, defaultForm)
this.sysMenu.id = ''
if (row) {
this.sysMenu.parentId = row.id
this.sysMenu.parentName = row.name
//this.sysMenu.component = 'ParentView'
if (row.type === 0) {
this.sysMenu.type = 1
this.typeDisabled = false
this.type0Disabled = false
this.type1Disabled = false
this.type2Disabled = true
} else if (row.type === 1) {
this.sysMenu.type = 2
this.typeDisabled = true
}
} else {
this.dialogTitle = '添加目录节点'
this.sysMenu.type = 0
this.sysMenu.component = 'Layout'
this.sysMenu.parentId = 0
this.typeDisabled = true
}
},
//编辑
edit(row) {
debugger
this.dialogTitle = '修改菜单'
this.dialogVisible = true
this.sysMenu = Object.assign({}, row)
this.typeDisabled = true
},
//添加或更新
saveOrUpdate() {
if (this.sysMenu.type === 0 && this.sysMenu.parentId !== 0) {
this.sysMenu.component = 'ParentView'
}
this.$refs.dataForm.validate((valid) => {
if (valid) {
this.saveBtnDisabled = true // 防止表单重复提交
if (!this.sysMenu.id) {
this.save()
} else {
this.update()
}
}
})
},
//添加
save() {
api.save(this.sysMenu).then((response) => {
this.$message.success(response.message || '操作成功')
this.dialogVisible = false
this.fetchData(this.page)
})
},
//更新
update() {
api.updateById(this.sysMenu).then((response) => {
this.$message.success(response.message || '操作成功')
this.dialogVisible = false
this.fetchData()
})
},
},
}
</script> -
给角色分配权限
- 接口分析
- 进入分配页面: 获取全部菜单的以及菜单按钮,选中已选复选框,
进行页面展示 - 保存分配权限: 删除之前分配的权限和保存现在分配的权限
- 进入分配页面: 获取全部菜单的以及菜单按钮,选中已选复选框,
- 接口分析
权限管理
-
菜单权限
菜单权限就是对页面的控制,
就是有这个权限的用户才能访问这个页面, 没这个权限的用户就无法访问, 它是以整个页面为维度, 对权限的控制并没有那么细, 所以是一种 粗颗粒权限
-
权限管理设计思路
在用户管理、角色管理以及菜单管理,我们把菜单权限分配给角色,把角色分配给用户,
那么用户就拥有了角色的所有权限 (权限包括: 菜单权限和按钮权限) 用户所拥有的角色 角色所拥有的权限 -
用户接口修改密码加密
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 用户添加
public Result save( { SysUser sysUser)
// TODO: 将用户输入的密码进行 MD5 加密
String encrypt = MD5.encrypt(sysUser.getPassword());
sysUser.setPassword(encrypt);
boolean isSuccess = sysUserService.save(sysUser);
if (isSuccess) {
return Result.ok();
} else {
return Result.fail();
}
} -
修改用户登录接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public Result login( { LoginVo loginVo)
// 根据 username 查询数据
SysUser sysUser = sysUserService.getUserInfobyUserName(loginVo.getUsername());
// 如果查询为空
if (sysUser == null) {
throw new coderItlException(20001, "用户不存在");
}
// 判断密码是否一致
String password = loginVo.getPassword();
String md5Password = MD5.encrypt(password);
if (!sysUser.getPassword().equals(md5Password)) {
throw new coderItlException(20001, "密码不正确!");
}
// 判断用户是否可用
if (sysUser.getStatus().intValue() == 0) {
throw new coderItlException(20001, "用户已禁用!");
}
// 根据 userid 和 username 生成 token 字符串,返回 map
String token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());
Map<String, Object> map = new HashMap<>();
map.put("token", token);
return Result.ok(map);
}1
2
3
4
5
6
7
8// dao 根据用户名查询
public SysUser getUserInfobyUserName(String username) {
QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
SysUser sysUser = sysUserMapper.selectOne(wrapper);
return sysUser;
} -
修改用户信息接口
1
2
3
4
5
6
7
8
9
10
11
12
13
public Result info(HttpServletRequest request) {
// 获取请求头中的 token 字符串
String token = request.getHeader("token");
// 从 token 中获取(用户名称 /id)
String username = JwtHelper.getUsername(token);
// 根据用户名称获取用户信息(基本信息和菜单权限和按钮权限数据)
Map<String, Object> map = sysUserService.getUserInfo(username);
return Result.ok(map);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// dao
public Map<String, Object> getUserInfo(String username) {
// 根据用户名查询用户基本信息
SysUser sysUser = this.getUserInfobyUserName(username);
// 根据 userid查询菜单权限值
List<RouterVo> routerVoList = sysMenuService.getUserMenuList(sysUser.getId());
// 根据 userid 查询按钮权限值
List<String> permsList = sysMenuService.getUserButtonList(sysUser.getId());
Map<String, Object> map = new HashMap<>();
map.put("roles", "[admin]");
map.put("name", "admin");
map.put("avatar", "https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
map.put("buttons", permsList);
map.put("routers", routerVoList);
return map;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 根据 userid 查询菜单权限值
public List<RouterVo> getUserMenuList(String userId) {
// admin 是超级管理员,可以操作所有内容
List<SysMenu> sysMenuList = null;
// 判断 userid 的值是 1(代表超级管理员),查询所有权限数据
if ("1".equals((userId))) {
QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
wrapper.eq("status", 1);
wrapper.orderByAsc("sort_value");
sysMenuList = baseMapper.selectList(wrapper);
} else {
// 如果 userid 不是 1,是其他类型用户, 查询这个用户的权限
sysMenuList = baseMapper.findMenuListUserId(userId);
}
// 构建树形结构
List<SysMenu> sysMenuTreeList = MenuHelper.buildTree(sysMenuList);
// 转换成前端路由要求的格式数据
List<RouterVo> routerVoList = RouterHelper.buildRouters(sysMenuTreeList);
return routerVoList;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 根据 userid 查询按钮权限值
public List<String> getUserButtonList(String userId) {
List<SysMenu> sysMenuList = null;
// 判断是否是管理员
if ("1".equals(userId)) {
sysMenuList = baseMapper.selectList(new QueryWrapper<SysMenu>().eq("status", 1));
} else {
sysMenuList = baseMapper.findMenuListUserId(userId);
}
List<String> permissionList = new ArrayList<>();
for (SysMenu sysMenu : sysMenuList) {
if (sysMenu.getType().intValue() == 2) {
String perms = sysMenu.getPerms();
permissionList.add(perms);
}
}
// 返回按钮的权限
return permissionList;
} -
工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74// 构建动态路由
package com.example.system.utils;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.example.model.system.SysMenu;
import com.example.model.vo.MetaVo;
import com.example.model.vo.RouterVo;
import org.springframework.util.StringUtils;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 根据菜单数据构建路由的工具类
*/
public class RouterHelper {
/**
* 根据菜单构建路由
*
* @param menus
* @return
*/
public static List<RouterVo> buildRouters(List<SysMenu> menus) {
List<RouterVo> routers = new LinkedList<RouterVo>();
for (SysMenu menu : menus) {
RouterVo router = new RouterVo();
router.setHidden(false);
router.setAlwaysShow(false);
router.setPath(getRouterPath(menu));
router.setComponent(menu.getComponent());
router.setMeta(new MetaVo(menu.getName(), menu.getIcon()));
List<SysMenu> children = menu.getChildren();
//如果当前是菜单,需将按钮对应的路由加载出来,如:“角色授权” 按钮对应的路由在 “系统管理” 下面
if (menu.getType().intValue() == 1) {
List<SysMenu> hiddenMenuList = children.stream().filter(item -> !StringUtils.isEmpty(item.getComponent())).collect(Collectors.toList());
for (SysMenu hiddenMenu : hiddenMenuList) {
RouterVo hiddenRouter = new RouterVo();
hiddenRouter.setHidden(true);
hiddenRouter.setAlwaysShow(false);
hiddenRouter.setPath(getRouterPath(hiddenMenu));
hiddenRouter.setComponent(hiddenMenu.getComponent());
hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon()));
routers.add(hiddenRouter);
}
} else {
if (!CollectionUtils.isEmpty(children)) {
if (children.size() > 0) {
router.setAlwaysShow(true);
}
router.setChildren(buildRouters(children));
}
}
routers.add(router);
}
return routers;
}
/**
* 获取路由地址
*
* @param menu 菜单信息
* @return 路由地址
*/
public static String getRouterPath(SysMenu menu) {
String routerPath = "/" + menu.getPath();
if (menu.getParentId().intValue() != 0) {
routerPath = menu.getPath();
}
return routerPath;
}
} -
权限测试
-
创建新用户,分配权限与角色
添加新用户,分配权限 对角色分配权限 -
修改前端页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14<!-- 工具条 -->
<div class="tools-div">
<el-button
type="success"
icon="el-icon-plus"
size="mini"
@click="add"
:disabled="$hasBP('bnt.sysUser.add') === false"
>添 加
>
</div>
<!-- :disabled="$hasBP('bnt.sysUser.add') === false" --> -
登录查看
实现客户端按钮的权限控制
-
-
前端权限对接
-
修改
axios
对请求头 token
的设置 1
2
3
4
5
6
7
8
9
10
11
12service.interceptors.request.use(
(config) => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers["token"] = getToken();
}
return config;
},
-
SpringSecurity
-
创建项目
-
添加依赖
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency> -
用户认证流程
用户认证流程 -
用户认证
-
自定义密码组件
( 采用 MD5
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.example.system.customer;
import com.example.common.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
// 自定义密码组件
public class CustomerMD5Paddword implements PasswordEncoder {
public String encode(CharSequence charSequence) {
// MD5 是自定义的工具类
return MD5.encrypt(charSequence.toString());
}
public boolean matches(CharSequence charSequence, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
}
} -
自定义用户对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class SysUser extends BaseEntity {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private String name;
private String phone;
private String headUrl;
private Long deptId;
private Long postId;
private String description;
private Integer status;
private List<SysRole> roleList;
//岗位
private String postName;
//部门
private String deptName;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29package com.example.system.customer;
import com.example.model.system.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class CustomerUser extends User {
/**
* 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象
*/
private SysUser sysUser;
public CustomerUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
super(sysUser.getUsername(), sysUser.getPassword(), authorities);
this.sysUser = sysUser;
}
public SysUser getSysUser() {
return sysUser;
}
public void setSysUser(SysUser sysUser) {
this.sysUser = sysUser;
}
} -
创建方法根据用户名查询用户信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32package com.example.system.service.impl;
import com.example.model.system.SysUser;
import com.example.system.customer.CustomerUser;
import com.example.system.service.SysUserService;
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.Collections;
public class UserDetailServiceImpl implements UserDetailsService {
private SysUserService sysUserService;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getUserInfobyUserName(username);
if (sysUser == null) {
// UsernameNotFoundException: 是 security 中提供的
throw new UsernameNotFoundException("用户不存在");
}
if (sysUser.getStatus().intValue() == 0) {
throw new RuntimeException("用户已禁用!");
}
return new CustomerUser(sysUser, Collections.emptyList());
}
} -
自定义认证过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99package com.example.system.filter;
import com.example.common.result.Result;
import com.example.common.result.ResultCodeEnum;
import com.example.common.utils.JwtHelper;
import com.example.model.vo.LoginVo;
import com.example.system.customer.CustomerUser;
import com.example.system.utils.ResponseUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 登录过滤器,继承 UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
public TokenLoginFilter(AuthenticationManager authenticationManager) {
this.setAuthenticationManager(authenticationManager);
this.setPostOnly(false);
// 指定登录接口及提交方式,可以指定任意路径
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login", "POST"));
}
/**
* 登录认证
*
* @param req
* @param res
* @return
* @throws AuthenticationException
*/
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 登录成功
*
* @param request
* @param response
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
CustomerUser customUser = (CustomerUser) auth.getPrincipal();
String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
Map<String, Object> map = new HashMap<>();
map.put("token", token);
ResponseUtil.out(response, Result.ok(map));
}
/**
* 登录失败
*
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
if (e.getCause() instanceof RuntimeException) {
ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
}
}
} -
返回信息工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 响应工具类
package com.example.system.utils;
import com.example.common.result.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ResponseUtil {
public static void out(HttpServletResponse response, Result r) {
ObjectMapper mapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try {
mapper.writeValue(response.getWriter(), r);
} catch (IOException e) {
e.printStackTrace();
}
}
} -
认证解析过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61package com.example.system.filter;
import com.example.common.result.Result;
import com.example.common.result.ResultCodeEnum;
import com.example.common.utils.JwtHelper;
import com.example.system.utils.ResponseUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
/**
* 认证解析 token 过滤器
*/
public class TokenAuthenticationFilter extends OncePerRequestFilter {
public TokenAuthenticationFilter() {
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("uri:" + request.getRequestURI());
// 如果是登录接口,直接放行
if ("/admin/system/index/login".equals(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if (null != authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} else {
ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// token 置于 header 里
String token = request.getHeader("token");
logger.info("token:" + token);
if (!StringUtils.isEmpty(token)) {
String useruame = JwtHelper.getUsername(token);
log.info("useruame:" + useruame);
if (!StringUtils.isEmpty(useruame)) {
return new UsernamePasswordAuthenticationToken(useruame, null, Collections.emptyList());
}
}
return null;
}
} -
配置用户认证全局信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73package com.example.system.config;
import com.example.system.customer.CustomMd5PasswordEncoder;
import com.example.system.filter.TokenAuthenticationFilter;
import com.example.system.filter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
private CustomMd5PasswordEncoder customMd5PasswordEncoder;
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
protected void configure(HttpSecurity http) throws Exception {
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http
//关闭 csrf
.csrf().disable()
// 开启跨域以便前端调用接口
.cors().and()
.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
.antMatchers("/admin/system/index/login").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and()
//TokenAuthenticationFilter放到 UsernamePasswordAuthenticationFilter 的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用 token 进行认证。
.addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilter(new TokenLoginFilter(authenticationManager()));
//禁用 session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService 和加密器
auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);
}
/**
* 配置哪些请求不拦截
* 排除swagger 相关请求
*
* @param web
* @throws Exception
*/
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/favicon.ico", "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
}
}前后端分离项目,使用
jwt
生成 token
,即用户状态保存在客户端中,前后端交互通过api
接口 无 session
生成,所以我们不需要配置 formLogin
,session
禁用
-
-
用户授权
在
SpringSecurity
中,会使用默认的 FilterSecurityInterceptor
来进行权限校验。在 FilterSecurityInterceptor
中会从 SecurityContextHolder
获取其中的 Authentication
,然后获取其中的权限信息。判断当前用户是否拥有访问当前资源所需的权限。-
修改
loadUserByUsername
,查询用户权限操作数据返回1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getUserInfobyUserName(username);
if (sysUser == null) {
// UsernameNotFoundException: 是 security 中提供的
throw new UsernameNotFoundException("用户不存在");
}
if (sysUser.getStatus().intValue() == 0) {
throw new RuntimeException("用户已禁用!");
}
// 根据 userid 查询操作权限数据
List<String> userPermsList = sysMenuService.getUserButtonList(sysUser.getId());
// 转换成 security 要求的格式
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String perm : userPermsList) {
authorities.add(new SimpleGrantedAuthority(perm.trim()));
}
return new CustomerUser(sysUser, authorities);
} -
配置
Redis
,存储权限数据 -
修改过滤器
- 认证过滤器
- 解析过滤器
-
在项目配置
redis
,完成Controller
相关代码
-
日志模块
-
获取日志工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83package com.example.system.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 获取ip 地址
*/
public class IpUtil {
public static String getIpAddress(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
log.info("inet: {}", inet.toString());
} catch (UnknownHostException e) {
log.error("UnknownHostException: {}", e);
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP 为客户端真实 IP, 多个 IP 按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
public static String getGatwayIpAddress(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
String ip = headers.getFirst("x-forwarded-for");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip 值,第一个 ip 才是真实 ip
if (ip.indexOf(",") != -1) {
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = headers.getFirst("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddress().getAddress().getHostAddress();
}
return ip;
}
} -
简单使用
1
2
3
4
5
6
7
8// 获取 IPV4 地址
try {
InetAddress inet = InetAddress.getLocalHost();
String ipAdddr = inet.getHostAddress();
System.out.println(ipAdddr);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
} -
表结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `sys_login_log`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '访问ID',
`username` varchar(50) DEFAULT '' COMMENT '用户账号',
`ipaddr` varchar(128) DEFAULT '' COMMENT '登录IP 地址',
`status` tinyint(1) DEFAULT '0' COMMENT '登录状态(0成功 1 失败)',
`msg` varchar(255) DEFAULT '' COMMENT '提示信息',
`access_time` datetime DEFAULT NULL COMMENT '访问时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记(0:可用 1: 已删除)',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 11
DEFAULT CHARSET = utf8mb3 COMMENT ='系统访问记录'
项目部署
-
后端打包
-
在主启动类的项目中的
pom.xml
添加 build
1
2
3
4
5
6
7
8
9<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> -
删除父工程的
build
-
目录结构预览
目录结构预览 -
在主启动的项目中点击
maven
的 package
打包 -
打包成功
打包结果
-