目录
1.1.1 IaaS(Infrastructure as a Service)即(基础设施即服务)
1.1.2 PaaS(Platform-as-a-Service)即(平台即服务)
1.1.3 SaaS(Software-as-a-Service)(软件即服务)
项目亮点
1.权限开发:jwt,shiro
2.企业报表的解决方法:poi(xls) ,jasper(pdf)
3.代码生成工具的制作与解析
4.企业工作流定制:activiti7
5.人工智能:人脸登录
第1章 SAAS-HRM系统概述与搭建环境
- 理解SaaS的基本概念
- 了解SAAS-HRM的基本需求和开发方式
- 掌握Power Designer的用例图
- 完成SAAS-HRM父模块及公共模块的环境搭建
- 完成企业微服务中企业CRUD功能
1 初识SaaS
1.1 云服务的三种模式
1.1.1 IaaS(Infrastructure as a Service)即(基础设施即服务)
只提供基础设备,例如服务器(cpu、内存、网络)
1.1.2 PaaS(Platform-as-a-Service)即(平台即服务)
包含服务器、操作系统、数据库、运行环境库
1.1.3 SaaS(Software-as-a-Service)(软件即服务)
包含服务器、操作系统、数据库、运行环境库、应用
4 工程搭建
4.1 前置知识点的说明
4.2 开发环境要求
4.2.1 lombok 插件

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
- @Data 注解在类上;提供类所有属性的 getting 和 setting 方法,此外还提供了equals、canEqual、hashCode、toString 方法
- @Setter :注解在属性上;为属性提供 setting 方法
- @Getter :注解在属性上;为属性提供 getting 方法
- @NoArgsConstructor :注解在类上;为类提供一个无参的构造方法
- @AllArgsConstructor :注解在类上;为类提供一个全参的构造方法
4.3 构建父工程
1.选择maven,因为父工程不需要什么骨架,直接next创建
2.应为父工程没有代码,所有把src目录删掉
<packaging>pom</packaging>
<name>ihrm_parent</name>
<description>IHRM-黑马程序员</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<fastjson.version>1.2.47</fastjson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<!--编译插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!--单元测试插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
4.4 构建公共子模块
1.返回结果的实体类
2.分布式id生成器
疑问:问什么有了Result还要ResultCode枚举类呢?
其实只要Result也行,不过我们对于多条信息要不断的去get和set值,比较麻烦,所有定义ResultCode枚举类封装信息会比较方便
4.4.1 构建公共子模块ihrm-common

4.4.2 创建返回结果实体类
(1)新建com.ihrm.common.entity包,包下创建类Result,用于控制器类返回结果
package com.ihrm.common.entity;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class Result {
private boolean success;//是否成功
private Integer code;// 返回码
private String message;//返回信息
private Object data;// 返回数据
public Result(ResultCode code) {
this.success = code.success;
this.code = code.code;
this.message = code.message;
}
public Result(ResultCode code,Object data) {
this.success = code.success;
this.code = code.code;
this.message = code.message;
this.data = data;
}
public Result(Integer code,String message,boolean success) {
this.code = code;
this.message = message;
this.success = success;
}
public static Result SUCCESS(){
return new Result(ResultCode.SUCCESS);
}
public static Result ERROR(){
return new Result(ResultCode.SERVER_ERROR);
}
public static Result FAIL(){
return new Result(ResultCode.FAIL);
}
}
(2)创建类PageResult ,用于返回分页结果
package com.ihrm.common.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult<T> {
private Long total;
private List<T> rows;
}
4.4.3 返回码定义类 (是枚举类型)
package com.ihrm.common.entity;
public enum ResultCode {
SUCCESS(true,10000,"操作成功!"),
//---系统错误返回码-----
FAIL(false,10001,"操作失败"),
UNAUTHENTICATED(false,10002,"您还未登录"),
UNAUTHORISE(false,10003,"权限不足"),
SERVER_ERROR(false,99999,"抱歉,系统繁忙,请稍后重试!");
//---用户操作返回码----
//---企业操作返回码----
//---权限操作返回码----
//---其他操作返回码----
//操作是否成功
boolean success;
//操作代码
int code;
//提示信息
String message;
ResultCode(boolean success,int code, String message){
this.success = success;
this.code = code;
this.message = message;
}
public boolean success() {
return success;
}
public int code() {
return code;
}
public String message() {
return message;
}
}
4.4.4 分布式ID生成器
主键id生成
方案一:数据库自增(微服务架构不适合,如果对user表合并就会产生冲突,不推荐)
方案二:uuid全球唯一(缺点:太长32位,数据量太大,无序的)
方案三:借助全局redis(缺点:网络开销太大)
方案四:雪花算法
代码是官方开源:官方下载即用
package com.leyou.common.utils;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
//雪花算法代码实现
public class IdWorker {
// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
private final static long twepoch = 1288834974657L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L;
// 机器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID最大值
private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 毫秒内自增位
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
/* 上次生产id时间戳 */
private static long lastTimestamp = -1L;
// 0,并发控制
private long sequence = 0L;
private final long workerId;
// 数据标识id部分
private final long datacenterId;
public IdWorker(){
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* @param workerId
* 工作机器ID
* @param datacenterId
* 序列号
*/
public IdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
*
* @return
*/
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
return nextId;
}
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* <p>
* 获取 maxWorkerId
* </p>
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuffer mpid = new StringBuffer();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (!name.isEmpty()) {
/*
* GET jvmPid
*/
mpid.append(name.split("@")[0]);
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
/**
* <p>
* 数据标识id部分
* </p>
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1])
| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
} catch (Exception e) {
System.out.println(" getDatacenterId: " + e.getMessage());
}
return id;
}
}
4.5 搭建公共的实体类模块

(2)引入坐标
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>ihrm_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
5 企业微服务-企业CRUD
5.1 模块搭建
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>ihrm_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
server:
port: 9001
spring:
application:
name: ihrm-company #指定服务名
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/ihrm?useUnicode=true&characterEncoding=utf8
username: root
password: 111111
jpa:
database: MySQL
show-sql: true
open-in-view: true
package com.ihrm.company;
import com.ihrm.common.utils.IdWorker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
//1.配置springboot的包扫描
@SpringBootApplication(scanBasePackages = "com.ihrm")
//2.配置jpa注解的扫描
@EntityScan(value="com.ihrm.domain.company")
public class CompanyApplication {
/**
* 启动方法
*/
public static void main(String[] args) {
SpringApplication.run(CompanyApplication.class,args);
}
@Bean
public IdWorker idWorker() {
return new IdWorker();
}
}
5.2 企业管理-CRUD
5.2.1 表结构分析
CREATE TABLE `co_company` (
`id` varchar(40) NOT NULL COMMENT 'ID',
`name` varchar(255) NOT NULL COMMENT '公司名称',
`manager_id` varchar(255) NOT NULL COMMENT '企业登录账号ID',
`version` varchar(255) DEFAULT NULL COMMENT '当前版本',
`renewal_date` datetime DEFAULT NULL COMMENT '续期时间', `expiration_date` datetime DEFAULT NULL COMMENT '到期时间', `company_area` varchar(255) DEFAULT NULL COMMENT '公司地区', `company_address` text COMMENT '公司地址',
`business_license_id` varchar(255) DEFAULT NULL COMMENT '营业执照-图片ID', `legal_representative` varchar(255) DEFAULT NULL COMMENT '法人代表', `company_phone` varchar(255) DEFAULT NULL COMMENT '公司电话',
`mailbox` varchar(255) DEFAULT NULL COMMENT '邮箱',
`company_size` varchar(255) DEFAULT NULL COMMENT '公司规模',
`industry` varchar(255) DEFAULT NULL COMMENT '所属行业',
`remarks` text COMMENT '备注',
`audit_state` varchar(255) DEFAULT NULL COMMENT '审核状态',
`state` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态',
`balance` double NOT NULL COMMENT '当前余额',
`create_time` datetime NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
5.2.2 完成企业增删改查操作
1、实体类
package com.ihrm.domain.company;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
/**
* 实体类代码:
* 属性
* 构造方法
* getter,setter方法
*
* lombok 插件 : 使用注解的形式替换getter setter,构造方法
* 如何使用插件
* 1.安装插件(在工程中引入响应的插件坐标即可)
* <dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
* 2.使用注解配置
* 配置到实体类上
* @setter : setter方法
* @getter :getter方法
* @NoArgsConstructor 无参构造
* @AllArgsConstructor 满参构造
* @Data : setter,getter,构造方法
*
* 使用jpa操作数据
* 配置实体类和数据库表的映射关系:jpa注解
* 1.实体类和表的映射关系
* 2.字段和属性的映射关系
* i。主键属性的映射
* ii。普通属性的映射
*/
@Entity
@Table(name = "co_company")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Company implements Serializable {
private static final long serialVersionUID = 594829320797158219L;
//ID
@Id
private String id;
/**
* 公司名称
*/
private String name;
/**
* 企业登录账号ID
*/
private String managerId;
/**
* 当前版本
*/
private String version;
/**
* 续期时间
*/
private Date renewalDate;
/**
* 到期时间
*/
private Date expirationDate;
/**
* 公司地区
*/
private String companyArea;
/**
* 公司地址
*/
private String companyAddress;
/**
* 营业执照-图片ID
*/
private String businessLicenseId;
/**
* 法人代表
*/
private String legalRepresentative;
/**
* 公司电话
*/
private String companyPhone;
/**
* 邮箱
*/
private String mailbox;
/**
* 公司规模
*/
private String companySize;
/**
* 所属行业
*/
private String industry;
/**
* 备注
*/
private String remarks;
/**
* 审核状态
*/
private String auditState;
/**
* 状态
*/
private Integer state;
/**
* 当前余额
*/
private Double balance;
/**
* 创建时间
*/
private Date createTime;
}
2、DAO层
package com.ihrm.company.dao;
import com.ihrm.domain.company.Company;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* 自定义dao接口继承
* JpaRepository<实体类,主键>
* JpaSpecificationExecutor<实体类>
*/
public interface CompanyDao extends JpaRepository<Company,String> ,JpaSpecificationExecutor<Company> {
}
JpaRepository提供了基本的增删改查 ,JpaSpecificationExecutor用于做复杂的条件查询
3、Service层
package com.ihrm.company.service;
import com.ihrm.common.utils.IdWorker;
import com.ihrm.company.dao.CompanyDao;
import com.ihrm.domain.company.Company;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CompanyService {
@Autowired
private CompanyDao companyDao;
@Autowired
private IdWorker idWorker;
/**
* 保存企业
* 1.配置idwork到工程
* 2.在service中注入idwork
* 3.通过idwork生成id
* 4.保存企业
*/
public void add(Company company) {
//基本属性的设置
String id = idWorker.nextId()+"";
company.setId(id);
//默认的状态
company.setAuditState("0");//0:未审核,1:已审核
company.setState(1); //0.未激活,1:已激活
companyDao.save(company);
}
/**
* 更新企业
* 1.参数:Company
* 2.根据id查询企业对象
* 3.设置修改的属性
* 4.调用dao完成更新
*/
public void update(Company company) {
Company temp = companyDao.findById(company.getId()).get();
temp.setName(company.getName());
temp.setCompanyPhone(company.getCompanyPhone());
companyDao.save(temp);
}
/**
* 删除企业
*/
public void deleteById(String id) {
companyDao.deleteById(id);
}
/**
* 根据id查询企业
*/
public Company findById(String id) {
return companyDao.findById(id).get();
}
/**
* 查询企业列表
*/
public List<Company> findAll() {
return companyDao.findAll();
}
}
4、Controller层
package com.ihrm.company.controller;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.common.exception.CommonException;
import com.ihrm.company.service.CompanyService;
import com.ihrm.domain.company.Company;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
//解决跨域问题
@CrossOrigin
@RestController
@RequestMapping(value="/company")
public class CompanyController {
@Autowired
private CompanyService companyService;
//保存企业
@RequestMapping(value="",method = RequestMethod.POST)
public Result save(@RequestBody Company company) {
//业务操作
companyService.add(company);
return new Result(ResultCode.SUCCESS);
}
//根据id更新企业
/**
* 1.方法
* 2.请求参数
* 3.响应
*/
@RequestMapping(value = "/{id}",method = RequestMethod.PUT)
public Result update(@PathVariable(value="id") String id, @RequestBody Company company ) {
//业务操作
company.setId(id);
companyService.update(company);
return new Result(ResultCode.SUCCESS);
}
//根据id删除企业
@RequestMapping(value="/{id}",method = RequestMethod.DELETE)
public Result delete(@PathVariable(value="id") String id) {
companyService.deleteById(id);
return new Result(ResultCode.SUCCESS);
}
//根据id查询企业
@RequestMapping(value="/{id}",method = RequestMethod.GET)
public Result findById(@PathVariable(value="id") String id) throws CommonException {
Company company = companyService.findById(id);
Result result = new Result(ResultCode.SUCCESS);
result.setData(company);
return result;
}
//查询全部企业列表
@RequestMapping(value="",method = RequestMethod.GET)
public Result findAll() {
List<Company> list = companyService.findAll();
Result result = new Result(ResultCode.SUCCESS);
result.setData(list);
return result;
}
}
5.3公共异常处理
为了使我们的代码更容易维护,同时给用户最好的用户体验,有必要对系统中可能出现的异常进行处理。spring提供了@ControllerAdvice注解和@ExceptionHandler可以很好的在控制层对异常进行统一处理
@ControllerAdvice:全局控制器异常处理,何控制器抛出异常时,注解这个的类可以拦截并处理所抛出的异常
@ExceptionHandler:定义了该方法处理的特定异常类型
(1)添加自定义的异常
package com.ihrm.common.exception;
import com.ihrm.common.entity.ResultCode;
import lombok.Getter;
@Getter
public class CommonException extends RuntimeException {
private static final long serialVersionUID = 1L;//序列化和反序列化操作的版本固定版本
private ResultCode code = ResultCode.SERVER_ERROR;//定义结果集中一个枚举作为作为一个对象
public CommonException(){}
public CommonException(ResultCode resultCode) {
super(resultCode.message());
this.code = resultCode; //枚举对象=客户传入的的报错对象
}
}
(2)配置公共异常处理
package com.ihrm.common.exception;
import com.alibaba.fastjson.JSON;
import com.ihrm.common.entity.Result;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 全局异常处理
*/
@ControllerAdvice
public class BaseExceptionHandler {
@ResponseBody
@ExceptionHandler(value = Exception.class)
public Result error(HttpServletRequest request, HttpServletResponse response,
Exception e) throws IOException {
e.printStackTrace();
if (e.getClass() == CommonException.class) {
CommonException ce = (CommonException) e;
return new Result(ce.getCode());
} else {
return Result.ERROR();
}
}
}
5.4跨域处理
跨域是什么?浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域 。我们是采用前后端分离开发的,也是前后端分离部署的,必然会存在跨域问题。 怎么解决跨域?很简单,
只需要在controller类上添加注解@CrossOrigin 即可
!这个注解其实是CORS的实现。 CORS(Cross-Origin Resource Sharing, 跨源资源共享)是W3C出的一个标准,其思想是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。因此,要想实现CORS进行跨域,需要服务器进行一些设置,同时前 端也需要做一些配置和分析。本文简单的对服务端的配置和前端的一些设置进行分析。
第二章数据库设计与前端框架
知识点:
理解多租户的数据库设计方案
熟练使用PowerDesigner构建数据库模型
理解前端工程的基本架构和执行流程
完成前端工程企业模块开发
1 多租户SaaS平台的数据库方案
1.1 多租户是什么
多租户技术(Multi-TenancyTechnology)又称多重租赁技术:是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离。
1.2 需求分析
传统软件模式,指将软件产品进行买卖,是一种单纯的买卖关系,客户通过买断的方式获取软件的使用权,软件的源码属于客户所有,因此传统软件是部署到企业内部,不同的企业各自部署一套自己的软件系统
Saas模式,指服务提供商提供的一种软件服务,应用统一部署到服务提供商的服务器上,客户可以根据自己的实际需求按需付费。用户购买基于WEB的软件,而不是将软件安装在自己的电脑上,用户也无需对软件进行定期的维护与管理

在SaaS平台里需要使用共用的数据中心以单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且仍可以保障客户的数据正常使用。由此带来了新的挑战,就是如何对应用数据进行设计,以支持多租户,而这种设计的思路,是要在数据的共享、安全隔离和性能间取得平衡。
1.3 多租户的数据库方案分析
目前基于多租户的数据库设计方案通常有如下三种:
独立数据库:隔离,安全,易扩展
共享数据库、独立 Schema:有第一定的隔离性,成本低
共享数据库、共享数据表(使用这一套):成本最低,设计比较复杂,隔离性要求较高
1.3.1 独立数据库
独立数据库:每个租户一个数据库。
优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
1.3.2 共享数据库、独立 Schema
(1) 什么是Schema
oracle数据库:在oracle中一个数据库可以具有多个用户,那么一个用户一般对应一个Schema,表都是建立在Schema中的,(可以简单的理解:在oracle中一个用户一套数据库表)
mysql数据库:mysql数据中的schema比较特殊,并不是数据库的下一级,而是等同于数据库。比如执行create schema test 和执行create database test效果是一模一样的。

共享数据库、独立 Schema:即多个或所有的租户使用同一个数据库服务(如常见的ORACLE或MYSQL数据库),但是每个租户一个Schema。
优点: 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
缺点: 如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据; 如果需要跨租户统计数据,存在一定困难。
这种方案是方案一的变种。只需要安装一份数据库服务,通过不同的Schema对不同租户的数据进行隔离。由于数据库服务是共享的,所以成本相对低廉。
1.3.3 共享数据库、共享数据表
共享数据库、共享数据表:即租户共享同一个Database,同一套数据库表(所有租户的数据都存放在一个数据库的同一套表中)。在表中增加租户ID等租户标志字段,表明该记录是属于哪个租户的。
优点:所有租户使用同一套数据库,所以成本低廉。
缺点:隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量,数据备份和恢复最困难。
这种方案和基于传统应用的数据库设计并没有任何区别,但是由于所有租户使用相同的数据库表,所以需要做好对
每个租户数据的隔离安全性处理,这就增加了系统设计和数据管理方面的复杂程度。
1.4 SAAS-HRM数据库设计
在SAAS-HRM平台中,分为了试用版和正式版。处于教学的目的,试用版采用共享数据库、共享数据表的方式设计。正式版采用基于mysql的共享数据库、独立 Schema设计(后续课程)。
2 数据库设计与建模
2.1 数据库设计的三范式
1.第一范式(1NF):确保每一列的原子性(做到每列不可拆分)
例如:假设我们的表中有一列是地址,里面存的值是诸如:中国北京。那么这样就违反了第一范式,因为中国北京其实可以很好的拆分为中国和北京两个,然后数据库里面可以出现两列:国籍和城市。这样才是符合第一范式的。
2.第二范式(2NF):在第一范式的基础上,非主字段必须依赖于主字段(一个表只做一件事)
假如:我们有一个学生表,里面存的是用户名,密码等,如果再加上 英语成绩,数学成绩等字段,那么就违反了第二范式。因为这样显得学生表不伦不类,不知道到底要存什么样的数据,所以为了满足第二范式,就应该再创建一张成绩表。
3.第三范式(3NF):在第二范式的基础上,消除传递依赖。
例如:创建一个订单表,有订单单价,订单个数,订单总计三个字段,那么这就违反了第三范式,因为总计这列的值完全可以通过单价乘以个数得到,不需要额外去存储。还有一个例子,我们有一个员工表,里面存了员工信息,还有和公司关联的company_id和company_name字段,同样也违反了第三范式,因为我们只要存了company_id,就可以查询企业表,从而得到company_name。
以上说的三范式,出现的年代比较久远了,那个时候服务器的存储的成本还比较高,也就是硬盘还比较贵,所以为了节省硬盘,就应该尽量减少硬盘的使用空间。而现在硬盘已经不是昂贵的东西了,所以就出现了反三范式:
反三范式:
反三范式是基于第三范式所调整的,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。
就拿上面的第三范式的例子来说,如果我们遵守了第三范式,没有存储总计的值,那么如果我们要做统计的时候,每次都要去计算单价乘以个数来得到总计,如果表中有十万数据,就需要计算十万次,这样势必会降低效率。而反三范式就是通过冗余字段,来提高效率,只需要通过查询就可以得到结果,无需再次去逻辑运算,这也就是达到了以空间换时间的目的。
2.2 数据库建模
它主要包括两部分内容:基本的数据结构;对约束建模(外键、组建)。
2.2.1 建模工具
PowerDesigner:他的优势在于:不用使用create table等语句创建表结构,数据库设计人员只关注如何进行数据建模即可,将来的数据库语句,可以自动生成。
2.2.2 使用pd建模
1. 选择新建数据库模型 打开PowerDesigner,文件->建立新模型->model types(选择类型)->Physical DataModel(物理模型)

2. 控制面板

3. 创建数据库表
点即面板按钮中的创建数据库按钮创建数据库模型

切换columns标签,可以对表中的所有字段进行配置

如果基于传统的数据库设计中存在外键则可以使用面版中的Reference配置多个表之间的关联关系,效果如下图



4、导出sql语句
我们之前做的这些操作,都可以进行sql的导出,然后在数据库中执行即可:
菜单栏:Databse——》Genarate Database:

生成的sql文件内容如下:
/*==============================================================*/
/* DBMS name: MySQL 5.0 */
/* Created on: 2019/8/11 14:13:28 */
/*==============================================================*/
drop table if exists co_company;
drop table if exists co_dept;
/*==============================================================*/
/* Table: co_company */
/*==============================================================*/
create table co_company
(
id varchar(40) not null,
name varchar(200),
company_area varchar(200),
primary key (id)
);
/*==============================================================*/
/* Table: co_dept */
/*==============================================================*/
create table co_dept
(
id varchar(40) not null,
name varchar(400),
company_id varchar(40),
primary key (id)
);
alter table co_dept add constraint FK_Reference_1 foreign key (company_id)
references co_company (id) on delete restrict on update restrict;
第四章 权限管理与jwt鉴权
4 常见的认证机制
4.1 HTTP Basic Auth
4.2 Cookie Auth
4.3 OAuth
这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证 权限管理的企业应用。
4.4 Token Auth
- 1. 客户端使用用户名跟密码请求登录
- 2. 服务端收到请求,去验证用户名与密码
- 3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
- 5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

- 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通 过HTTP头传输.
- 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
- 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
- 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你 可以进行Token生成调用即可.
- 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
- CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
- 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
- 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
- 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)
5 HRM中的TOKEN签发与验证
5.1 什么是JWT
5.2 JJWT的快速入门
(1)创建maven工程,引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
/**
* 通过jjwt创建token
*/
public static void main(String[] args) {
JwtBuilder jwtBuilder = Jwts.builder().setId("88").setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "ihrm")
.claim("companyId","123456")
.claim("companyName","小新股份有限公司")
;
String token = jwtBuilder.compact();
System.out.println(token);
}
5.2.2 token的解析
public class ParseJwtTest {
/**
* 解析jwtToken字符串
*/
public static void main(String[] args) {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4OCIsInN1YiI6IuWwj-eZvSIsImlhdCI6MTU0MzMwODM1NiwiY29tcGFueUlkIjoiMTIzNDU2IiwiY29tcGFueU5hbWUiOiLmsZ_oi4_kvKDmmbrmkq3lrqLmlZnogrLogqHku73mnInpmZDlhazlj7gifQ.lacFfiWnBkbCQuHIwsB-S7gRkXxesTx8GOhhtIjALLI";
Claims claims = Jwts.parser().setSigningKey("ihrm").parseClaimsJws(token).getBody();
//私有数据存放在claims
System.out.println(claims.getId());
System.out.println( claims.getSubject());
System.out.println(claims.getIssuedAt());
//解析自定义claim中的内容
String companyId = (String)claims.get("companyId");
String companyName = (String)claims.get("companyName");
System.out.println(companyId + "---" + companyName);
}
}
5.3 JWT工具类
在ihrm_common工程中创建JwtUtil工具类
不可以jwtBuilder.setClaim(map),被覆盖其他的
@Getter
@Setter
@ConfigurationProperties("jwt.config")
public class JwtUtils {
//签名私钥
private String key;
//签名的失效时间
private Long ttl;
/**
* 设置认证token
* id:登录用户id
* subject:登录用户名
*
*/
public String createJwt(String id, String name, Map<String,Object> map) {
//1.设置失效时间
long now = System.currentTimeMillis();//当前毫秒
long exp = now + ttl;
//2.创建jwtBuilder
JwtBuilder jwtBuilder = Jwts.builder().setId(id).setSubject(name)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, key);
//3、直接setClaim(map)会把上边的都覆盖掉
//jwtBuilder.setClaim(map)
//3.根据map设置claims
for(Map.Entry<String,Object> entry : map.entrySet()) {
jwtBuilder.claim(entry.getKey(),entry.getValue());
}
jwtBuilder.setExpiration(new Date(exp));
//4.创建token
String token = jwtBuilder.compact();
return token;
}
/**
* 解析token字符串获取clamis
*/
public Claims parseJwt(String token) {
Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
return claims;
}
}
jwt:
config:
key: saas-ihrm
ttl: 360000
5.4 登录成功签发token
(1)配置JwtUtil。修改ihrm_system工程的启动类
@Bean
public JwtUtil jwtUtil(){
return new util.JwtUtil();
}
/**
* 用户登录
* 1.通过service根据mobile查询用户
* 2.比较password
* 3.生成jwt信息
*
*/
@RequestMapping(value="/login",method = RequestMethod.POST)
public Result login(@RequestBody Map<String,String> loginMap) {
String mobile = loginMap.get("mobile");
String password = loginMap.get("password");
User user = userService.findByMobile(mobile);
//登录失败
if(user == null || !user.getPassword().equals(password)) {
return new Result(ResultCode.MOBILEORPASSWORDERROR);
}else {
//登录成功
Map<String,Object> map = new HashMap<>();
map.put("companyId",user.getCompanyId());
map.put("companyName",user.getCompanyName());
String token = jwtUtils.createJwt(user.getId(), user.getUsername(), map);
return new Result(ResultCode.SUCCESS,token);
}
}
5.5 获取用户信息鉴权
@Setter
@Getter
public class ProfileResult {
private String mobile;
private String username;
private String company;
private Map<String,Object> roles = new HashMap<>();
public ProfileResult(User user) {
this.mobile = user.getMobile();
this.username = user.getUsername();
this.company = user.getCompanyName();
Set<Role> roles = user.getRoles();
Set<String> menus = new HashSet<>();
Set<String> points = new HashSet<>();
Set<String> apis = new HashSet<>();
for (Role role : roles) {
Set<Permission> perms = role.getPermissions();
for (Permission perm : perms) {
String code = perm.getCode();
if(perm.getType() == 1) {
menus.add(code);
}else if(perm.getType() == 2) {
points.add(code);
}else {
apis.add(code);
}
}
}
this.roles.put("menus",menus);
this.roles.put("points",points);
this.roles.put("apis",apis);
}
}
/**
* 用户登录成功之后,获取用户信息
* 1.获取用户id
* 2.根据用户id查询用户
* 3.构建返回值对象
* 4.响应
*/
@RequestMapping(value="/profile",method = RequestMethod.POST)
public Result profile(HttpServletRequest request) throws Exception {
/**
* 从请求头信息中获取token数据
* 1.获取请求头信息:名称=Authorization
* 2.替换Bearer+空格
* 3.解析token
* 4.获取clamis
*/
//1.获取请求头信息:名称=Authorization
String authorization = request.getHeader("Authorization");
if(StringUtils.isEmpty(authorization)) {
throw new CommonException(ResultCode.UNAUTHENTICATED);
}
//2.替换Bearer+空格
String token = authorization.replace("Bearer ","");
//3.解析token
Claims claims = jwtUtils.parseJwt(token);
String userid = claims.getId();
User user = userService.findById(userid);
ProfileResult result = new ProfileResult(user);
return new Result(ResultCode.SUCCESS,result);
}
3 前端框架
此项目采用目前比较流行的前后端分离的方式进行开发。前端是在传智播客研究院开源的前端框架(黑马Admin商用后台模板)的基础上进行的开发。
技术栈
vue 2.5++
elementUI 2.2.2
vuex
axios
vue-router
vue-i18n
前端环境
node 8.++
npm 5.++
3.2 启动与安装
官网上提供了非常基础的脚手架,如果我们使用官网的脚手架需要自己写很多代码比如登陆界面、主界面菜单样式等内容。 课程已经提供了功能完整的脚手架,我们可以拿过来在此基础上开发,这样可以极大节省我们开发的时间。
(1)解压提供的资源包
(2)在命令提示符进入该目录,输入命令:cnpm install
3.3 工程结构
整个前端工程的工程目录结构如下:


3.4 执行流程分析
3.4.1 路由和菜单
路由和菜单是组织起一个后台应用的关键骨架。本项目侧边栏和路由是绑定在一起的,所以你只有在@/router/index.js 下面配置对应的路由,侧边栏就能动态的生成了。大大减轻了手动编辑侧边栏的工作量。当然这样就需要在配置路由的时候遵循很多的约定,这里的路由分为两种, constantRouterMap 和 asyncRouterMap 。
constantRouterMap 代通用页面。
asyncRouterMap 代表那些业务中通过 addRouters 动态添加的页面。

3.4.2 前端数据交互
一个完整的前端 UI 交互到服务端处理流程是这样的:
1. UI 组件交互操作;
2. 调用统一管理的 api service 请求函数;
3. 使用封装的 request.js 发送请求;
4. 获取服务端返回;
5. 更新 data;
从上面的流程可以看出,为了方便管理维护,统一的请求处理都放在 src/api 文件夹中,并且一般按照 model纬度进行拆分文件
api/
frame.js
menus.js
users.js
permissions.js
...
4 企业管理
4.1 需求分析
在通用页面配置企业管理模块,完成企业的基本操作
4.2 搭建环境
4.2.1 新增模块
(1)手动创建
方式一:在src目录下创建文件夹,命名规则:module-模块名称()
在文件夹下按照指定的结构配置assets,components,pages,router,store等文件
(2)使用命令自动创建
安装命令行工具:npm install -g itheima-cli
执行命令:itheima moduleAdd saas-clients `saas-clients` 是新模块的名字
自动创建这些目录和文件
│ ├── module-saas-clients | saas-clients模块主目录
│ │ ├── assets | 资源
│ │ ├── components | 组件
│ │ ├── pages | 页面
│ │ │ └── index.vue | 示例
│ │ ├── router | 路由
│ │ │ └── index.js | 示例
│ │ └── store | 数据
│ │ └── app.js | 示例
每个模块所有的素材、页面、组件、路由、数据,都是独立的,方便大型项目管理,
在实际项目中会有很多子业务项目,它们之间的关系是平行的、低耦合、互不依赖。
注意:创建完模块之后,导致名称和demo模块一样,所以需要修改module-demo/router下面的index.js:
4.2.2 构造模拟数据
(1)在/src/mock 中添加模拟数据company.js
import Mock from 'mockjs'
import { param2Obj } from '@/utils'
const List = []
const count = 100
for (let i = 0; i < 3; i++) {
let data = {
id: "1"+i,
name: "企业"+i,
managerId: "string",
version: "试用版v1.0",
renewalDate: "2018-01-01",
expirationDate: "2019-01-01",
companyArea: "string",
companyAddress: "string",
businessLicenseId: "string",
legalRepresentative: "string",
companyPhone: "13800138000",
mailbox: "string",
companySize: "string",
industry: "string",
remarks: "string",
auditState: "string",
state: "1",
balance: "string",
createTime: "string"
}
List.push(data)
}
export default {
list: () => {
return {
code: 10000,
success: true,
message: "查询成功",
data:List
}
},
sassDetail:() => {
return {
code: 10000,
success: true,
message: "查询成功",
data:{
id: "10001",
name: "测试企业",
managerId: "string",
version: "试用版v1.0",
renewalDate: "2018-01-01",
expirationDate: "2019-01-01",
companyArea: "string",
companyAddress: "string",
businessLicenseId: "string",
legalRepresentative: "string",
companyPhone: "13800138000",
mailbox: "string",
companySize: "string",
industry: "string",
remarks: "string",
auditState: "string",
state: "1",
balance: "string",
createTime: "string"
}
}
}
}
(2)配置模拟API接口拦截规则
在/src/mock/index.js 中配置模拟数据接口拦截规则
import Mock from 'mockjs'
import TableAPI from './table'
import ProfileAPI from './profile'
import LoginAPI from './login'
import CompanyAPI from './company'
Mock.setup({
//timeout: '1000'
})
//如果发送请求的api路径匹配,拦截
//第一个参数匹配的请求api路径,第二个参数匹配请求的方式,第三个参数相应数据如何替换
Mock.mock(/\/table\/list\.*/, 'get', TableAPI.list)
//获取用户信息
Mock.mock(/\/frame\/profile/, 'post', ProfileAPI.profile)
Mock.mock(/\/frame\/login/, 'post', LoginAPI.login)
//配置模拟数据接口
Mock.mock(/\/company\/+/, 'get', CompanyAPI.sassDetail)//根据id查询
Mock.mock(/\/company/, 'get', CompanyAPI.list) //访问企业列表
4.2.3 注册模块
编辑 src/main.js
/*
* 注册 - 业务模块
*/
import dashboard from '@/module-dashboard/' // 面板
import demo from '@/module-demo/' // 面板
import saasClients from '@/module-saas-clients/' //刚新添加的 企业管理
import tools from './utils/common.js'
Vue.prototype.$tools = tools
Vue.use(tools)
Vue.use(dashboard, store)
Vue.use(demo, store)
Vue.use(saasClients, store) ///注册 刚新添加的 企业管理
4.2.4 配置路由菜单
打开刚才自动创建的 /src/module-saas-clients/router/index.js
/*
* @Author: dongwen.zeng <623008719@qq.com>
* @Description: xxx业务模块
* @Date: 2018-04-13 16:13:27
* @Last Modified by: hans.taozhiwei
* @Last Modified time: 2018-09-03 11:12:47
*/
import Layout from '@/module-dashboard/pages/layout'
const _import = require('@/router/import_' + process.env.NODE_ENV)
export default [
{
root: true,
path: '/saas-clients',
component: Layout,
redirect: 'noredirect',
name: 'saas-clients',
meta: {
title: 'xxx业务模块管理',
icon: 'international'
},
children: [
{
path: 'index',
component: _import('saas-clients/pages/index'),
name: 'saas-clients-index',
meta: {title: 'SaaS企业管理', icon: 'international', noCache: true}
}
]
}
]
4.2.5 编写业务页面
创建 /src/module-saas-clients/pages/index.vue
<template>
<div class="dashboard-container">
saas企业管理
</div>
</template>
<script>
export default {
name: 'saasClintList',
components: {},
data() {
return {
}
},
computed: {
},
created() {
}
}
</script>
注意文件名 驼峰格式 首字小写
页面请放在目录 /src/module-saas-clients/pages/
组件请放在目录 /src/module-saas-clients/components/
页面路由请修改 /src/module-saas-clients/router/index.js
4.3 企业操作
4.3.1 创建api
在src/api/base目录下创建企业数据交互的API(saasClient.js)
import {createAPI, createFormAPI} from '@/utils/request' //导入相关工具类,框架自己提供的
//第一个参数/company是请求路径(路径可以是完全路径,也可以是部分路径,因为我们在config/dev.env.js下面有前缀配置),
// BASE_API: '"http://localhost:9001/"' 第二参数是请求方式,第三个参数请求的参数数据
export const list = data => createAPI('/company', 'get', data)
// data代表请求的对象,${data.id}表示从请求的对象中取出id属性
export const detail = data => createAPI(`/company/${data.id}`, 'get', data)
4.3.2 企业列表
想要显示序号只需要吧prop改为type="index"
<!--开关组件
active-value:激活的数据值
active-color:激活的颜色
inactive-value:未激活数据值
inactive-color:未激活的颜色
-->
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card shadow="never">
<!--elementui的table组件
data:数据模型
-->
<el-table :data="dataList" border style="width: 100%">
<!--el-table-column : 构造表格中的每一列
prop: 数组中每个元素对象的属性名
-->
<el-table-column fixed type="index" label="序号" style="width:50px" ></el-table-column>
<el-table-column fixed prop="name" label="企业名称" style="width:100px"></el-table-column>
<el-table-column fixed prop="version" label="版本" style="width:30px"></el-table-column>
<el-table-column fixed prop="companyphone" label="联系电话" style="width:100px">
</el-table-column>
<el-table-column fixed prop="expirationDate" label="截至时间" style="width:150px">
</el-table-column>
<el-table-column fixed prop="state" label="状态" style="width:50px">
<!--scope:传递当前行的所有数据 -->
<template slot-scope="scope">
<!--开关组件
active-value:激活的数据值
active-color:激活的颜色
inactive-value:未激活
inactive-color:未激活的颜色
-->
<el-switch v-model="scope.row.state" inactive-value="0" active-value="1" disabled
active-color="#13ce66" inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" style="width:100px">
<template slot-scope="scope">
<router-link :to="'/saas-clients/details/'+scope.row.id">查看</router-link>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
<script>
import {list} from '@/api/base/saasClient' //导入list方法,从自定义的api中
export default {
name: 'saas-clients-index',
components: {},
data() {
return {
dataList:[]
}
},
methods: {
getList() {
//调用API发起请求
//res=响应数据
list().then(res => {
this.dataList = res.data.data
})
}
},
created() {
this.getList()
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.alert {
margin: 10px 0px 0px 0px;
}
.pagination {
margin-top: 10px;
text-align: right;
}
</style>
4.3.3 企业详情
(1)配置路由
在/src/module-saas-clients/router/index.js 添加新的子路由配置
{
path: 'details/:id', //特别注意这个路径的写法
component: _import('saas-clients/pages/details'),
name: 'saas-clients-details',
meta: {title: 'saas企业详情', icon: 'component', noCache: true}
}
(2)完成详情展示
在/src/module-saas-clients/pages/ 下创建企业详情视图details.vue
// 钩子函数获取地址参数
created() {
var id = this.$route.params.id //获取路径上的参数id的值
this.details(id);
},
//Switch开关
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card shadow="never">
<el-tabs v-model="activeName">
<!--第一个页签的内容-->
<el-tab-pane label="企业信息" name="first">
<!--form表单
model : 双向绑定的数据对象
-->
<el-form ref="form" :model="company" label-width="200px">
<el-form-item label="企业名称" >
<el-input v-model="company.name" style="width:400px" disabled></el-input>
</el-form-item>
<el-form-item label="公司地址">
<el-input v-model="company.companyAddress" style="width:400px" disabled></el-input>
</el-form-item>
<el-form-item label="法人代表">
<el-input v-model="company.legalRepresentative" style="width:400px" disabled></el-input>
</el-form-item>
<el-form-item label="当前余额">
<el-input v-model="company.balance" style="width:400px" disabled></el-input>
</el-form-item>
<el-form-item label="创建时间">
<el-input v-model="company.createTime" style="width:400px" disabled></el-input>
</el-form-item>
<el-form-item label="公司电话">
<el-input v-model="company.companyPhone" style="width:400px" disabled></el-input>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="company.mailbox" style="width:400px" disabled></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="company.remark" style="width:400px" ></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary">审核</el-button>
<el-button>拒绝</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!--第一个页签的内容结束-->
<el-tab-pane label="账户信息" name="second">账户信息</el-tab-pane>
<el-tab-pane label="交易记录" name="third">交易记录</el-tab-pane>
</el-tabs>
</el-card>
</div>
</div>
</template>
<script>
import {details} from '@/api/base/saasClient'
export default {
name: 'saas-clients-table-details',
data() {
return {
activeName: 'first', //定义默认显示第一个页签
company: {}
}
},
methods: {
details(id){
//调用api方法查询公司详细信息
details({id:id}).then(res => {
this.company = res.data.data;
console.log(id);
console.log(res.data.data)
});
}
},
// 创建完毕状态
created() {
var id = this.$route.params.id //获取路径上的参数id的值
this.details(id);
},
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.alert {
margin: 10px 0px 0px 0px;
}
.pagination {
margin-top: 10px;
text-align: right;
}
</style>
4.4 与后台对接测试
(1)启动第一天的企业微服务服务(ihrm_company);
(2)注释掉src/mock目录下index.js下面的:
//配置模拟数据接口
//Mock.mock(/\/company\/+/, 'get', CompanyAPI.sassDetail)//根据id查询
//Mock.mock(/\/company/, 'get', CompanyAPI.list) //访问企业列表
(3)在config/dev.env.js 中配置请求地址
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"http://localhost:9001/"'
})
第3章:SaaS系统用户权限设计
学习目标:
理解RBAC模型的基本概念及设计思路
了解SAAS-HRM中权限控制的需求及表结构分析
完成组织机构的基本CRUD操作
1 组织机构管理
1.1 需求分析
1.1.1 需求分析
实现企业组织结构管理,实现部门的基本CRUD操作

1.1.2 数据库表设计
CREATE TABLE `co_department` (
`id` varchar(40) NOT NULL,
`company_id` varchar(255) NOT NULL COMMENT '企业ID',
`parent_id` varchar(255) DEFAULT NULL COMMENT '父级部门ID',
`name` varchar(255) NOT NULL COMMENT '部门名称',
`code` varchar(255) NOT NULL COMMENT '部门编码',
`category` varchar(255) DEFAULT NULL COMMENT '部门类别',
`manager_id` varchar(255) DEFAULT NULL COMMENT '负责人ID',
`city` varchar(255) DEFAULT NULL COMMENT '城市',
`introduce` text COMMENT '介绍',
`create_time` datetime NOT NULL COMMENT '创建时间',
`manager` varchar(40) DEFAULT NULL COMMENT '部门负责人',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1.2 微服务实现
1.2.1 抽取公共代码
ihrm_commoncom.模块下的ihrm.common.controller 包下添加公共controller
@ModelAttribute //在所有controller层前之前执行的方法
import org.springframework.web.bind.annotation.ModelAttribute;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 公共controller
* 获取request,response
* 获取企业id,获取企业名称
*/
public class BaseController {
protected HttpServletRequest request;
protected HttpServletResponse response;
protected String companyId; //暂定假设值
protected String companyName;
@ModelAttribute //在所有controller层前之前执行的方法
public void setResAnReq(HttpServletRequest request,HttpServletResponse response) {
this.request = request;
this.response = response;
/**
* 目前使用 companyId = 1
* companyName = "传智播客"
*/
}
//企业id,(暂时使用1,以后会动态获取)
public String parseCompanyId() {
return "1";
}
public String parseCompanyName() {
return "江苏传智播客教育股份有限公司";
}
}
(2) 公共service
ihrm_commoncom.模块下的ihrm.common.service 包下添加公共BaseService
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
</dependency>
</dependencies>
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
public class BaseService<T> {
protected Specification<T> getSpec(String companyId) {
Specification<T> spect = new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder cb) {
//根据企业id查询
return cb.equal(root.get("companyId").as(String.class),companyId);
}
};
return spect;
}
}
1.2.2 实现基本CRUD操作
在com.ihrm.domain.company 包下创建Department实体类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* (Department)实体类
*/
@Entity
@Table(name = "co_department")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department implements Serializable {
private static final long serialVersionUID = -9084332495284489553L;
//ID
@Id
private String id;
/**
* 父级ID
*/
private String pid;
/**
* 企业ID
*/
private String companyId;
/**
* 部门名称
*/
private String name;
/**
* 部门编码,同级部门不可重复
*/
private String code;
/**
* 负责人ID
*/
private String managerId;
/**
* 负责人名称
*/
private String manager;
/**
* 介绍
*/
private String introduce;
/**
* 创建时间
*/
private Date createTime;
}
(2)持久化层
在com.ihrm.company.dao 包下创建DepartmentDao
import com.ihrm.domain.company.Department;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* /**
* 部门操作持久层:参数类型:函数JpaRepository
参数一:对象+对象主键类型
函数JpaSpecificationExecutor
参数二:对象
*/
*/
public interface DepartmentDao extends JpaRepository<Department,String> ,JpaSpecificationExecutor<Department> {
}
(3)业务层
在com.ihrm.company.service 包下创建DepartmentService
import com.ihrm.common.service.BaseService;
import com.ihrm.common.utils.IdWorker;
import com.ihrm.company.dao.DepartmentDao;
import com.ihrm.domain.company.Department;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.List;
@Service
public class DepartmentService extends BaseService {
@Autowired
private DepartmentDao departmentDao;
@Autowired
private IdWorker idWorker;
/**
* 1.保存部门
*/
public void save(Department department) {
//设置主键的值
String id = idWorker.nextId()+"";
department.setId(id);
//调用dao保存部门
departmentDao.save(department);
}
/**
* 2.更新部门
*/
public void update(Department department) {
//1.根据id查询部门
Department dept = departmentDao.findById(department.getId()).get();
//2.设置部门属性
dept.setCode(department.getCode());
dept.setIntroduce(department.getIntroduce());
dept.setName(department.getName());
//3.更新部门
departmentDao.save(dept);
}
/**
* 3.根据id查询部门
*/
public Department findById(String id) {
return departmentDao.findById(id).get();
}
/**
* 4.查询全部部门列表
*/
public List<Department> findAll(String companyId) {
/**
* 用户构造查询条件
* 1.只查询companyId
* 2.很多的地方都需要根据companyId查询
* 3.很多的对象中都具有companyId
*
*/
// Specification<Department> spec = new Specification<Department>() {
// /**
// * 用户构造查询条件
// * root :包含了所有的对象数据
// * cq :一般不用
// * cb :构造查询条件
// */
// public Predicate toPredicate(Root<Department> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
// //根据企业id查询
// return cb.equal(root.get("companyId").as(String.class),companyId);
// }
// };
return departmentDao.findAll(getSpec(companyId));
}
/**
* 5.根据id删除部门
*/
public void deleteById(String id) {
departmentDao.deleteById(id);
}
}
(4)控制层
在ihrm.company.controller 创建控制器类DepartmentController
package com.ihrm.company.controller;
import com.ihrm.common.controller.BaseController;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.company.service.CompanyService;
import com.ihrm.company.service.DepartmentService;
import com.ihrm.domain.company.Company;
import com.ihrm.domain.company.Department;
import com.ihrm.domain.company.response.DeptListResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
//1.解决跨域
@CrossOrigin
//2.声明restContoller
@RestController
//3.设置父路径
@RequestMapping(value="/company") // company/deparment
public class DepartmentController extends BaseController{
@Autowired
private DepartmentService departmentService;
@Autowired
private CompanyService companyService;
/**
* 保存
*/
@RequestMapping(value="/department",method = RequestMethod.POST)
public Result save(@RequestBody Department department) {
//1.设置保存的企业id
/**
* 企业id:目前使用固定值1,以后会解决
*/
department.setCompanyId(companyId);
//2.调用service完成保存企业
departmentService.save(department);
//3.构造返回结果
return new Result(ResultCode.SUCCESS);
}
/**
* 查询企业的部门列表
* 指定企业id
*/
@RequestMapping(value="/department",method = RequestMethod.GET)
public Result findAll() {
//1.指定企业id
Company company = companyService.findById(companyId);
//2.完成查询
List<Department> list = departmentService.findAll(companyId);
//3.构造返回结果
DeptListResult deptListResult = new DeptListResult(company,list);
return new Result(ResultCode.SUCCESS,deptListResult);
}
/**
* 根据ID查询department
*/
@RequestMapping(value="/department/{id}",method = RequestMethod.GET)
public Result findById(@PathVariable(value="id") String id) {
Department department = departmentService.findById(id);
return new Result(ResultCode.SUCCESS,department);
}
/**
* 修改Department
*/
@RequestMapping(value="/department/{id}",method = RequestMethod.PUT)
public Result update(@PathVariable(value="id") String id,@RequestBody Department department) {
//1.设置修改的部门id
department.setId(id);
//2.调用service更新
departmentService.update(department);
return new Result(ResultCode.SUCCESS);
}
/**
* 根据id删除
*/
@RequestMapping(value="/department/{id}",method = RequestMethod.DELETE)
public Result delete(@PathVariable(value="id") String id) {
departmentService.deleteById(id);
return new Result(ResultCode.SUCCESS);
}
}
创建返回值对象DeptListResult
package com.ihrm.domain.company.response;
import com.ihrm.domain.company.Company;
import com.ihrm.domain.company.Department;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class DeptListResult {
private String companyId; //交叉部分元素公司与部门交叉部分
private String companyName;
private String companyManage;
private List<Department> depts; //对象等于department
public DeptListResult(Company company, List depts){
this.companyId=company.getId();
this.companyName= company.getName();
this.companyManage= company.getLegalRepresentative(); //公司联系人
}
}
1.3 前端实现
1.3.1 创建模块
(1)使用命令行创建module-departments模块并引入到工程中
itheima moduleAdd departments1.
(2) 在src/main.js 中注册模块
import departments from '@/module-departments/' // 组织机构管理
Vue.use(departments, store)1.2.
(3)在 /module-departments/router/index.js 配置路由
import Layout from '@/module-dashboard/pages/layout'
const _import = require('@/router/import_' + process.env.NODE_ENV)
export default [
{
root: true,
path: '/departments',
component: Layout,
redirect: 'noredirect',
name: 'departments',
meta: {
title: '组织架构管理',
icon: 'architecture'
},
children: [
{
path: 'index',
component: _import('departments/pages/index'),
name: 'organizations-index',
meta: {title: '组织架构', icon: 'architecture', noCache: true}
}
]
}
1.3.2 配置请求API
在 /src/api/base/ 创建departments.js作为组织机构管理的API公共接口方法
import {
createAPI, createFileAPI
} from '@/utils/request'
export const organList = data => createAPI('/company/departments', 'get', data)
export const add = data => createAPI('/company/departments', 'post', data)
export const update = data => createAPI(`/company/departments/${data.id}`, 'put', data)
export const detail = data => createAPI(`/company/departments/${data.id}`, 'get', data)
export const remove = data => createAPI(`/company/departments/${data.id}`, 'delete',
data)
export const changeDept = data => createAPI(`/company/departments/changeDept`, 'put',
data)
export const saveOrUpdate = data => {return data.id?update(data):add(data)}
课堂讲解的代码
import {createAPI} from '@/utils/request'
//查询部门列表
export const list = data => createAPI('/company/department', 'get', data)
//保存部门
//data {id:“”,name:“”}
export const save = data => createAPI('/company/department', 'post', data)
//根据id查询部门 {id:“”}
export const find = data => createAPI(`/company/department/${data.id}`, 'get', data)
//根据id删除部门 {id:""}
export const deleteById = data => createAPI(`/company/department/${data.id}`, 'delete', data)
//根据id更新部门 {id:"",name:"",code:""}
export const update = data => createAPI(`/company/department/${data.id}`, 'put', data)
//保存或更新的方法
export const saveOrupdate = data => {return data.id?update(data):save(data)}
1.3.3 构造列表
找到 /module-departments/page/index.vue ,使用element-ui提供的Card组件构造卡片式容器
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card shadow="never">
<div class='organization-index'>
<div class='organization-index-top'>
<div class='main-top-title'>
<el-tabs v-model="activeName">
<el-tab-pane label="组织结构" name="first"></el-tab-pane>
<div class="el-tabs-report">
<a class="el-button el-button--primary el-button--mini" title="导 出" >导入</a>
<a class="el-button el-button--primary el-button--mini" title="导 出" >导出</a>
</div>
</el-tabs>
</div>
</div>
<div style="overflow: scroll;white-space:nowrap" class="treBox">
<div class="treeCon clearfix">
<span>
<i class="fa fa-university" aria-hidden="true"></i>
<span ><strong>{{departData.name}}</strong></span>
</span>
<div class="fr">
<div class="treeRinfo">
<span>负责人</span>
<span>在职 <em class="colGreen" title="在职人数">---
</em> (<em class="colGreen" title="正式员工">---</em> / <em
class="colRed" title="非正式员工">---</em>)</span>
</div>
<div class="treeRinfo">
<el-dropdown class="item">
<span class="el-dropdown-link">
操作<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>
<el-button type="text" @click="handlAdd('')">添加子部门
</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button type="text"
@click="handleList(organizationTree,0)">查看待分配员工</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
<!--
构造树形列表
-->
</div>
</div>
</el-card>
</div>
</div>
</template>
<style rel="stylesheet/scss" lang="scss">
.el-dropdown {
color: #000000
}
.el-tree-node__content>.el-tree-node__expand-icon {
padding:0px; }
.el-tree-node__expand-icon {
color:#ffffff
}
.generalClassNode {
padding-left: 20px; }
.el-tree-node__content{
font-size: 16px;
line-height: 36px;
height:36px; }
.custom-tree-node{
padding-left: 20px; }
.objectTree {
overflow: auto;
z-index: 100;
width: 300px;
border: 1px solid #dcdfe6;
margin-top: 5px;
left: 70px; }
.el-tabs__content {
overflow: initial; }
.boxpad {
margin-left: -40px; }
.boxpad > div:first-child,
.objectTree > div:first-child.el-tree-node > div:first-child {
display: none; }
</style>
<style rel="stylesheet/scss" lang="scss" scoped>
.el-tree-node__expand-icon{ }
.el-icon-caret-right{}
.el-tree-node__content{
font-size: 14px;
line-height: 36px; }
.generalClass {
font-size: 14px;
line-height: 36px;
color:#000000
}
.all {
position: relative;
min-height: 100%;
padding-bottom: 200px; }
.organization-main:after,
.organization-index-top:after {
display: block;
clear: both;
content: '';
visibility: hidden;
height: 0; }
.organization-main {
font-size: 14px;
font-size: 14px; }
.organization-index {
padding-bottom: 20px;
margin-left: 20px; }
.main-top-title {
padding-left: 20px;
padding-top: 20px;
text-align: left; }
::-webkit-scrollbar-thumb {
background-color: #018ee8;
height: 50px;
outline-offset: -2px;
outline: 8px solid #fff;
-webkit-border-radius: 4px; }
::-webkit-scrollbar-track-piece {
background-color: #fff;
-webkit-border-radius: 0; }
::-webkit-scrollbar {
width: 8px;
height: 8px; }
::-webkit-scrollbar-thumb:hover {
background-color: #fb4446;
height: 50px;
-webkit-border-radius: 4px; }
.modal-total {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
background: #000;
z-index: 90;
opacity: 0.2; }
.modal {
width: 400px;
height: 300px;
background-color: #ffffff;
z-index: 999;
position: absolute;
left: 45%;
top: 20%;
text-align: center; }
.treBox {
padding: 30px 120px 0;
}
.organization-index-top {
position: relative;
.el-tabs-report {
position: absolute;
top: -50px;
right: 15px;
}
}
.treeCon {
border-bottom: 1px solid #cfcfcf;
padding: 10px 0;
margin-bottom: 10px;
.el-dropdown {
color: #333;
}
}
.treeRinfo {
display: inline-block; }
.treeRinfo span {
padding-left: 30px; }
2)树形机构列表
<!--
构造树形列表
叶子 <i class="fa fa-male"></i>
非叶子
展开 <i class="fa fa-minus-square-o">
闭合 <i class="fa fa-plus-square-o">
<div class="generalClass" slot-scope="{node,data}" style="width:99%">
-->
<div class="custom-tree-node" slot-scope="{ node, data }">
<!--
node:是否展开,是否是叶子节点
data:部门对象
-->
<span>
<i v-if="node.isLeaf" class="fa fa-male"></i>
<i v-else :class="node.expanded?'fa fa-plus-square-o':'fa fa-minus-square-o'"></i>
<span>{{ node.label }}</span>
</span>
<el-tree
:data="depts"
node-key="id"
default-expand-all
:props="{label:'name'}"
>
<div class="custom-tree-node" slot-scope="{ node, data }">
<!--
node:是否展开,是否是叶子节点
data:部门对象
-->
<span>
<i v-if="node.isLeaf" class="fa fa-male"></i>
<i v-else :class="node.expanded?'fa fa-plus-square-o':'fa fa-minus-square-o'"></i>
<span>{{ node.label }}</span>
</span>
<div class="fr">
<span class="treeRinfo">
<div class="treeRinfo">
<span>责任人:{{departData.companyManage}}</span>
<span>在职 <em class="colGreen" title="在职人数">---</em> (<em class="colGreen" title="正式员工">---</em> / <em class="colRed" title="非正式员工">---</em>)</span>
</div>
<div class="treeRinfo">
<el-dropdown class="item">
<span class="el-dropdown-link">
操作<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>
<el-button type="text" @click="handlAdd('')">添加子部门</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button type="text" @click="handleList()">查看待分配员工</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</span>
</div>
</div>
</el-tree>
(3) 构造数据
//数据绑定模型
data() {
return {
activeName: 'first', //激活pane的名称
dialogFormVisible:false,//是否显示弹出层标识
parentId:'', //父id
departData:{}, //部门列表
formData:{} //表单提交数据
}
},
//自定义方法
methods: {
getObject(params) {
organList().then(res => {
this.departData = res.data.data
})
}
},
//钩子函数
created: function() {
this.getObject()
},
1.3.4 组织机构的增删改查
使用element-ui提供的dialog的弹出层构造弹出添加页面
<el-dialog title="编辑部门" :visible.sync="dialogFormVisible">
<el-form ref="dataForm" :model="formData" label-width="120px">
<el-form-item label="部门名称">
<el-input v-model="formData.name" placeholder='请输入部门名称'></el-input>
</el-form-item>
<el-form-item label="部门编码">
<el-input v-model="formData.code" placeholder='请输入部门编码'></el-input>
</el-form-item>
<el-form-item label="部门负责人">
<el-input v-model="formData.manager" placeholder='请输入负责人'></el-input>
</el-form-item>
<el-form-item label="部门介绍">
<el-input v-model="formData.introduce" placeholder='请输入介绍'></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="createData">确定</el-button>
<el-button @click="dialogFormVisible=false">取消</el-button>
</div>
</el-dialog>
配置保存方法
createData:function() {
this.formData.parentId = this.parentId
saveOrUpdate(this.formData)
.then(res => {
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
location.reload()
this.dialogFormVisible=false
})
}
(2)修改部门
- 根据id查询部门
handleEdit(id) {
detail({id}).then( res=> {
this.formData = res.data.data
this.dialogFormVisible = true
this.parentId = res.data.data.parentId
})
}
- 调用方法更新部门
handleDelete(obj) {
this.$confirm(
`本次操作将删除${obj.name},删除后将不可恢复,您确认删除吗?`
).then(() => {
remove({id:obj.id}).then( res=> {
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
location.reload()
})
})
}
1.3.5 抽取组件
组件(Component) 是Vue.js 最强大的功能。可以通过将不同的业务拆分为不同的组件进行开发,让代码更加优雅提供可读性。当然页可以封装可重用的代码,通过传入对象的不同,实现组件的复用。
(1)抽取新增/修改页面到 /module-departments/components/add.vue 中
<template>
<el-dialog title="编辑部门" :visible.sync="dialogFormVisible">
<el-form ref="dataForm" :model="formData" label-width="120px">
<el-form-item label="部门名称">
<el-input v-model="formData.name" placeholder='请输入部门名称'></el-input>
</el-form-item>
<el-form-item label="部门编码">
<el-input v-model="formData.code" placeholder='请输入部门编码'></el-input>
</el-form-item>
<el-form-item label="部门负责人">
<el-input v-model="formData.manager" placeholder='请输入部门负责人'></el-input>
</el-form-item>
<el-form-item label="部门介绍">
<el-input v-model="formData.introduce" placeholder='请输入部门介绍'></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="createData">确定</el-button>
<el-button @click="dialogFormVisible=false">取消</el-button>
</div>
</el-dialog>
</template>
<script>
import { saveOrUpdate } from '@/api/base/departments'
export default {
name: 'dept-add',
data() {
return {
dialogFormVisible:false,
formData:{},
parentId:''
}
},
methods: {
createData:function() {
this.formData.parentId = this.parentId
saveOrUpdate(this.formData)
.then(res => {
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
location.reload()
this.dialogFormVisible=false
})
}
}
}</script>
(2) 在 /module-departments/page/index.vue 中引用组件
- 导入组件
import deptAdd from './../components/add' //导入组件
export default {
//声明引用组件
components: { deptAdd }, //声明组件
data() {
return {
deptAdd: 'deptAdd', //配置组件别名
activeName: 'first',
departData:{},
}
},
}
使用组件
//v-bind:is (绑定的组件名称)
//ref : 引用子组件中内容的别名
<component v-bind:is="deptAdd" ref="deptAdd"></component>1.2.3.
- 改造新增修改方法
复制
handlAdd(parentId) {
//对子组件中的属性复制
this.$refs.deptAdd.formData = {};
this.$refs.deptAdd.parentId = parentId
this.$refs.deptAdd.dialogFormVisible = true;
},
handleEdit(id) {
detail({id}).then( res=> {
this.$refs.deptAdd.formData = res.data.data
this.$refs.deptAdd.dialogFormVisible = true
this.$refs.deptAdd.parentId = res.data.data.parentId
})
},
2 RBAC模型
2.1 什么是RBAC
RBAC(全称:Role-Based Access Control)基于角色的权限访问控制,作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注。 所谓的RBAC就是在用户和权限中加了一个角色的概念,通过角色去关联用户关联权限
2.2 基于RBAC的设计思路
基于角色的访问控制基本原理是在用户和访问权限之间加入角色这一层,实现用户和权限的分离,用户只有通过激活角色才能获得访问权限。通过角色对权限分组,大大简化了用户权限分配表,间接地实现了对用户的分组,提高了权限的分配效率。且加入角色层后,访问控制机制更接近真实世界中的职业分配,便于权限管理。

在RBAC模型中,角色是系统根据管理中相对稳定的职权和责任来划分,每种角色可以完成一定的职能。用户通过饰演不同的角色获得角色所拥有的权限,一旦某个用户成为某角色的成员,则此用户可以完成该角色所具有的职能。通过将权限指定给角色而不是用户,在权限分派上提供了极大的灵活性和极细的权限指定粒度。
2.3 表结构分析

一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。
SaaS-用户管理
完成用户管理的基本CRUD操作
完成角色管理的基本CRUD操作
3 SAAS-HRM中的权限设计
3.1 需求分析
3.1.1 SAAS平台的基本元素

SAAS平台管理员:负责平台的日常维护和管理,包括用户日志的管理、租户账号审核、租户状态管理、租户费用的管理,要注意的是平台管理员不能对租户的具体业务进行管理
企业租户:指访问SaaS平台的用户企业,在SaaS平台中各租户之间信息是独立的。
租户管理员:为租户角色分配权限和相关系统管理、维护。
租户角色:根据业务功能租户管理员进行角色划分,划分好角色后,租户管理员可以对相应的角色进行权限分配租户用户:需对租户用户进行角色分配,租户用户只能访问授权的模块信息。
3.1.2 需求分析
在应用系统中,权限是以什么样的形式展现出来的?对菜单的访问,页面上按钮的可见性,后端接口的控制,都要进行充分考虑
前端
前端菜单:根据是否有请求菜单权限进行动态加载
按钮:根据是否具有此权限点进行显示/隐藏的控制
后端
前端发送请求到后端接口,有必要对接口的访问进行权限的验证
3.2 权限设计
针对这样的需求,在有些设计中可以将菜单,按钮,后端API请求等作为资源,这样就构成了基于RBAC的另一种授权模型(用户-角色-权限-资源)。在SAAS-HRM系统的权限设计中我们就是才用了此方案

针对此种权限模型,其中权限究竟是属于菜单,按钮,还是API的权限呢?那就需要在设计数据库权限表的时候添加类型加以区分(如权限类型 1为菜单 2为功能 3为API)。

这里要注意的是,权限表与权限菜单表、页面元素表与API接口表都是一对一的关系与传统的RBAC模型对比不难发现此种设计的好处:
1. 不需要区分哪些是操作,哪些是资源
2. 方便扩展,当系统要对新的东西进行权限控制时,我只需要建立一个新的资源表,并确定这类权限的权限类型标识即可。
4 用户管理
4.1 需求分析
用户其实就是saas企业访问的员工,对企业员工完成基本的CRUD操作
表结构如下:
CREATE TABLE `bs_user` (
`id` varchar(40) NOT NULL COMMENT 'ID',
`mobile` varchar(40) NOT NULL COMMENT '手机号码',
`username` varchar(255) NOT NULL COMMENT '用户名称',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`enable_state` int(2) DEFAULT '1' COMMENT '启用状态 0是禁用,1是启用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`department_id` varchar(40) DEFAULT NULL COMMENT '部门ID',
`time_of_entry` datetime DEFAULT NULL COMMENT '入职时间',
`form_of_employment` int(1) DEFAULT NULL COMMENT '聘用形式',
`work_number` varchar(20) DEFAULT NULL COMMENT '工号',
`form_of_management` varchar(8) DEFAULT NULL COMMENT '管理形式',
`working_city` varchar(16) DEFAULT NULL COMMENT '工作城市',
`correction_time` datetime DEFAULT NULL COMMENT '转正时间',
`in_service_status` int(1) DEFAULT NULL COMMENT '在职状态 1.在职 2.离职',
`company_id` varchar(40) DEFAULT NULL COMMENT '企业ID',
`company_name` varchar(40) DEFAULT NULL,
`department_name` varchar(40) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_phone` (`mobile`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
注意:用户表中有company_id、company_name用来关联企业,有department_id、department_name来关联部门。
角色表 pe_role 表结构如下:
CREATE TABLE pe_role(
id VARCHAR(40) NOT NULL COMMENT 'ID',
NAME VARCHAR(200) NOT NULL COMMENT '角色名称',
description VARCHAR(500) COMMENT '说明',
company_id VARCHAR(40) COMMENT '企业ID',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4
用户和角色关联表 pe_user_role 表结构如下:
CREATE TABLE pe_user_role(
user_id VARCHAR(40) NOT NULL COMMENT '用户ID',
role_id VARCHAR(200) NOT NULL COMMENT '角色ID'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4
权限表 pe_permission 结构如下:
CREATE TABLE pe_permission(
id VARCHAR(40) NOT NULL COMMENT 'ID',
NAME VARCHAR(40) NOT NULL COMMENT '权限名称(菜单名称,按钮名称,api名称)',
TYPE INT NOT NULL COMMENT '权限类型 1为菜单 2为功能 3为API',
CODE VARCHAR(40) NOT NULL COMMENT '权限编码,权限控制根据这个值来判断',
description VARCHAR(500) COMMENT '权限描述',
pid VARCHAR(40) COMMENT '父id,比如菜单就是按钮的父',
en_visible INT COMMENT '是否可见,1可见,0不可见(表示不让企业看到)',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4
角色和权限关系表 pe_role_permission 结构如下:
CREATE TABLE pe_role_permission(
role_id VARCHAR(40) NOT NULL COMMENT '角色ID',
permission_id VARCHAR(40) NOT NULL COMMENT '权限ID'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4
菜单资源表:pe_permission_menu
CREATE TABLE pe_permission_menu(
id VARCHAR(40) NOT NULL COMMENT 'ID',
menu_icon VARCHAR(200) COMMENT '菜单图标',
menu_order VARCHAR(10) COMMENT '排序号',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4
按钮(页面元素)资源表:pe_permission_point
CREATE TABLE pe_permission_point(
id VARCHAR(40) NOT NULL COMMENT 'ID',
point_class VARCHAR(200) COMMENT '菜单图标',
point_icon VARCHAR(200) COMMENT '排序号',
point_status VARCHAR(20) COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4
API资源表:
CREATE TABLE pe_permission_api(
id VARCHAR(40) NOT NULL COMMENT 'ID',
api_url VARCHAR(500) COMMENT '请求链接',
api_method VARCHAR(10) COMMENT '请求类型',
api_level VARCHAR(50) COMMENT '权限等级,1为通用接口权限,2为需校验接口权限',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4
4.2 配置系统微服务
(1)搭建系统微服务模块(ihrm_system),pom引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.ihrm</groupId>
<artifactId>ihrm_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
(2)配置application.yml
server:
port: 9002
spring:
application:
name: ihrm-system #指定服务名
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/ihrm?useUnicode=true&characterEncoding=utf8
username: root
password: root
jpa:
database: MySQL
show-sql: true
open-in-view: true
(3)配置启动类
import com.ihrm.common.utils.IdWorker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
//1.配置springboot的包扫描
@SpringBootApplication(scanBasePackages = "com.ihrm")
//2.配置jpa注解的扫描
@EntityScan(value="com.ihrm.domain.system")
public class SystemApplication {
/**
* 启动方法
*/
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class,args);
}
@Bean
public IdWorker idWorker() {
return new IdWorker();
}
}
4.3 后端用户基本操作
(1)实体类
/**
* JsonIgnore:忽略json转化
* 多对多关系是要忽略json转换,因为对于前端来说会对实体类自动转换为字符串,这里对会Role进行json转换,
* 因为是多对多的关系,Role里面有user,就会产生死循环问题
*
*ManyToMany:多对多关系
*@JoinTable:中间表
*/
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
/**
* 用户实体类
*/
@Entity
@Table(name = "bs_user")
@Getter
@Setter
public class User implements Serializable {
private static final long serialVersionUID = 4297464181093070302L;
/**
* ID
*/
@Id
private String id;
/**
* 手机号码
*/
private String mobile;
/**
* 用户名称
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 启用状态 0为禁用 1为启用
*/
private Integer enableState;
/**
* 创建时间
*/
private Date createTime;
private String companyId;
private String companyName;
/**
* 部门ID
*/
private String departmentId;
/**
* 入职时间
*/
private Date timeOfEntry;
/**
* 聘用形式
*/
private Integer formOfEmployment;
/**
* 工号
*/
private String workNumber;
/**
* 管理形式
*/
private String formOfManagement;
/**
* 工作城市
*/
private String workingCity;
/**
* 转正时间
*/
private Date correctionTime;
/**
* 在职状态 1.在职 2.离职
*/
private Integer inServiceStatus;
private String departmentName;
/**
* JsonIgnore
* : 忽略json转化
*/
@JsonIgnore //表示防止死循环
@ManyToMany //不维护中间表
@JoinTable(name="pe_user_role",joinColumns={@JoinColumn(name="user_id",referencedColumnName="id")},
inverseJoinColumns={@JoinColumn(name="role_id",referencedColumnName="id")}
)
private Set<Role> roles = new HashSet<Role>();//用户与角色 多对多
}
@JsonIgnore
@ManyToMany(mappedBy="roles"):表示不对中间表维护,由上边的user去维护
package com.ihrm.domain.system;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "pe_role")
@Getter
@Setter
public class Role implements Serializable {
private static final long serialVersionUID = 594829320797158219L;
@Id
private String id;
/**
* 角色名
*/
private String name;
/**
* 说明
*/
private String description;
/**
* 企业id
*/
private String companyId;
@JsonIgnore
@ManyToMany(mappedBy="roles")
private Set<User> users = new HashSet<User>(0);//角色与用户 多对多
@JsonIgnore
@ManyToMany
@JoinTable(name="pe_role_permission",
joinColumns={@JoinColumn(name="role_id",referencedColumnName="id")},
inverseJoinColumns={@JoinColumn(name="permission_id",referencedColumnName="id")})
private Set<Permission> permissions = new HashSet<Permission>(0);//角色与模块 多对多
}
Permission
package com.ihrm.domain.system;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "pe_permission")
@Getter
@Setter
@NoArgsConstructor
@DynamicInsert(true)
@DynamicUpdate(true)
public class Permission implements Serializable {
private static final long serialVersionUID = -4990810027542971546L;
/**
* 主键
*/
@Id
private String id;
/**
* 权限名称
*/
private String name;
/**
* 权限类型 1为菜单 2为功能 3为API
*/
private Integer type;
private String code;
/**
* 权限描述
*/
private String description;
private String pid;
private Integer enVisible;
public Permission(String name, Integer type, String code, String description) {
this.name = name;
this.type = type;
this.code = code;
this.description = description;
}
}
api
package com.ihrm.domain.system;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
*/
@Entity
@Table(name = "pe_permission_api")
@Getter
@Setter
public class PermissionApi implements Serializable {
private static final long serialVersionUID = -1803315043290784820L;
/**
* 主键
*/
@Id
private String id;
/**
* 链接
*/
private String apiUrl;
/**
* 请求类型
*/
private String apiMethod;
/**
* 权限等级,1为通用接口权限,2为需校验接口权限
*/
private String apiLevel;
}
menu
package com.ihrm.domain.system;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* Created with IDEA
* Author:xzengsf
* Date:2018/3/22 10:24
* Description: 菜单权限实体类
*/
@Entity
@Table(name = "pe_permission_menu")
@Getter
@Setter
public class PermissionMenu implements Serializable {
private static final long serialVersionUID = -1002411490113957485L;
/**
* 主键
*/
@Id
private String id;
//展示图标
private String menuIcon;
//排序号
private String menuOrder;
}
point==按钮资源
package com.ihrm.domain.system;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* Created with IDEA
* Author:xzengsf
* Date:2018/3/22 10:24
* Description: 菜单权限实体类
*/
@Entity
@Table(name = "pe_permission_point")
@Getter
@Setter
public class PermissionPoint implements Serializable {
private static final long serialVersionUID = -1002411490113957485L;
/**
* 主键
*/
@Id
private String id;
/**
* 权限代码
*/
private String pointClass;
private String pointIcon;
private String pointStatus;
}
(2)持久化层
import com.ihrm.domain.system.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* 企业数据访问接口
*/
public interface UserDao extends JpaRepository<User,String>,JpaSpecificationExecutor<User> {
}
(3)业务逻辑层
import com.ihrm.common.utils.IdWorker;
import com.ihrm.domain.system.User;
import com.ihrm.system.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 部门操作业务逻辑层
*/
@Service
public class UserService {
@Autowired
private UserDao userDao;
@Autowired
private IdWorker idWorker;
/**
* 1.保存用户
*/
public void save(User user) {
//设置主键的值
String id = idWorker.nextId()+"";
user.setPassword("123456");//设置初始密码
user.setEnableState(1);
user.setId(id);
//调用dao保存部门
userDao.save(user);
}
/**
* 2.更新用户
*/
public void update(User user) {
//1.根据id查询部门
User target = userDao.findById(user.getId()).get();
//2.设置部门属性
target.setUsername(user.getUsername());
target.setPassword(user.getPassword());
target.setDepartmentId(user.getDepartmentId());
target.setDepartmentName(user.getDepartmentName());
//3.更新部门
userDao.save(target);
}
/**
* 3.根据id查询用户
*/
public User findById(String id) {
return userDao.findById(id).get();
}
/**
* 4.查询全部用户列表
* 参数:map集合的形式
* hasDept
* departmentId
* companyId
*
*/
public Page findAll(Map<String,Object> map,int page, int size) {
//1.需要查询条件
Specification<User> spec = new Specification<User>() {
/**
* 动态拼接查询条件
* @return
*/
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> list = new ArrayList<>();
//根据请求的companyId是否为空构造查询条件
if(!StringUtils.isEmpty(map.get("companyId"))) {
list.add(criteriaBuilder.equal(root.get("companyId").as(String.class),(String)map.get("companyId")));
}
//根据请求的部门id构造查询条件
if(!StringUtils.isEmpty(map.get("departmentId"))) {
list.add(criteriaBuilder.equal(root.get("departmentId").as(String.class),(String)map.get("departmentId")));
}
if(!StringUtils.isEmpty(map.get("hasDept"))) {
//根据请求的hasDept判断 是否分配部门 0未分配(departmentId = null),1 已分配 (departmentId != null)
if("0".equals((String) map.get("hasDept"))) {
list.add(criteriaBuilder.isNull(root.get("departmentId")));
}else {
list.add(criteriaBuilder.isNotNull(root.get("departmentId")));
}
}
return criteriaBuilder.and(list.toArray(new Predicate[list.size()]));
}
};
//2.分页
Page<User> pageUser = userDao.findAll(spec, new PageRequest(page-1, size));
return pageUser;
}
/**
* 5.根据id删除用户
*/
public void deleteById(String id) {
userDao.deleteById(id);
}
}
(4)控制器层
import com.ihrm.common.controller.BaseController;
import com.ihrm.common.entity.PageResult;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.domain.system.User;
import com.ihrm.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import javax.websocket.server.PathParam;
import java.util.List;
import java.util.Map;
//1.解决跨域
@CrossOrigin
//2.声明restContoller
@RestController
//3.设置父路径
@RequestMapping(value="/sys")
public class UserController extends BaseController {
@Autowired
private UserService userService;
/**
* 保存
*/
@RequestMapping(value = "/user", method = RequestMethod.POST)
public Result save(@RequestBody User user) {
//1.设置保存的企业id
user.setCompanyId(companyId);
user.setCompanyName(companyName);
//2.调用service完成保存企业
userService.save(user);
//3.构造返回结果
return new Result(ResultCode.SUCCESS);
}
/**
* 查询企业的部门列表
* 指定企业id
*/
@RequestMapping(value = "/user", method = RequestMethod.GET)
public Result findAll(int page, int size, @RequestParam Map map) {
//1.获取当前的企业id
map.put("companyId",companyId);
//2.完成查询
Page<User> pageUser = userService.findAll(map,page,size);
//3.构造返回结果
PageResult pageResult = new PageResult(pageUser.getTotalElements(),pageUser.getContent());
return new Result(ResultCode.SUCCESS, pageResult);
}
/**
* 根据ID查询user
*/
@RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
public Result findById(@PathVariable(value = "id") String id) {
User user = userService.findById(id);
return new Result(ResultCode.SUCCESS, user);
}
/**
* 修改User
*/
@RequestMapping(value = "/user/{id}", method = RequestMethod.PUT)
public Result update(@PathVariable(value = "id") String id, @RequestBody User user) {
//1.设置修改的部门id
user.setId(id);
//2.调用service更新
userService.update(user);
return new Result(ResultCode.SUCCESS);
}
/**
* 根据id删除
*/
@RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
public Result delete(@PathVariable(value = "id") String id) {
userService.deleteById(id);
return new Result(ResultCode.SUCCESS);
}
}
4.4 前端用户基本操作
4.4.2 配置接口请求路径
在 config/index.js 中通过proxyTable配置代理转发的请求后端地址
'/api/sys': {
target: 'http://localhost:9002/sys',
changeOrigin: true,
pathRewrite: {
'^/api/sys': ''
}
},
4.4.1 导入员工模块
注册模块
import employees from '@/module-employees/' // 员工管理
Vue.use(employees, store)1.2.
在 /src/api/base/ 下配置API(user.js)
import {createAPI} from '@/utils/request'
export const list = data => createAPI('/sys/user', 'get', data)
export const simple = data => createAPI('/sys/user/simple', 'get', data)
export const add = data => createAPI('/sys/user', 'post', data)
export const update = data => createAPI(`/sys/user/${data.id}`, 'put', data)
export const remove = data => createAPI(`/sys/user/${data.id}`, 'delete', data)
export const detail = data => createAPI(`/sys/user/${data.id}`, 'get', data)
4.4.2 用户列表展示
(1) 页面代码
<el-table :data="dataList" fit style="width: 100%;" border>
<el-table-column type="index" :index="1" label="序号" width="150"> </el-tablecolumn>
<el-table-column sortable prop="username" label="姓名" width="150"></el-tablecolumn>
<el-table-column sortable prop="mobile" label="手机号" width="150"></el-tablecolumn>
<el-table-column sortable prop="workNumber" label="工号" width="120"></eltable-column>
<el-table-column sortable prop="formOfEmployment" label="聘用形势"
width="200"></el-table-column>
<el-table-column sortable prop="departmentName" label="部门" width="200"></eltable-column>
<el-table-column sortable prop="timeOfEntry" label="入职时间" width="150">
</el-table-column>
<el-table-column sortable label="状态" width="120">
<template slot-scope="scope">
<el-switch
v-model="scope.row.accountStatus"
active-color="#13ce66"
inactive-color="#ff4949"
@change="handleStatus(scope.row)">
</el-switch>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" align="center" width="220">
<template slot-scope="scope">
<router-link :to="{'path':'/employees/details/' + scope.row.id}"
class="el-button el-button--text el-button--small">
查看
</router-link>
<el-button @click="handleDelete(scope.row)" type="text" size="small">删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<PageTool :paginationPage="requestParameters.page"
:paginationPagesize="requestParameters.pagesize" :total="counts"
@pageChange="handleCurrentChange" @pageSizeChange="handleSizeChange">
</PageTool>
</div>
(2) js构造数据
import constantApi from '@/api/constant/employees'
import {list,remove} from "@/api/base/users"
import PageTool from './../../components/page/page-tool'
import employeesAdd from './../components/add'
var _this = null
export default {
name: 'employeesList',
components: {
PageTool,employeesAdd
},
data() {
return {
employeesAdd: 'employeesAdd',
baseData: constantApi,
dataList: [],
counts: '',
requestParameters:{
page: 1,
pagesize: 10,
}
}
},
methods: {
// 业务方法
doQuery(params) {
list(this.requestParameters).then(res => {
this.dataList = res.data.data.rows
this.counts = res.data.data.total
})
}
},
// 创建完毕状态
created: function() {
this.doQuery()
},
}
4.4.4 用户详情
(1) 配置路由
{
path: 'details/:id',
component: _import('employees/pages/employees-details'),
// hidden: true // 是否显示在左侧菜单
name: 'details',
meta: {
title: '详情'
}
}
(2) 完成用户详情页面
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card :style="{minHeight:boxHeight}">
<el-tabs v-model="activeName" class="infoPosin">
<el-tab-pane name="first" class="rInfo">
<span slot="label">登录账户设置</span>
<component v-bind:is="accountInfo" :objId='objId' ref="user"></component>
</el-tab-pane>
<el-tab-pane name="two" class="rInfo">
<span slot="label">个人详情</span>
</el-tab-pane>
<el-tab-pane name="third" class="rInfo">
<span slot="label">岗位信息</span>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</div>
</template>
<script>
import accountInfo from './../components/details-account-info'
export default {
name: 'employeesDetails',
components: { accountInfo},
data() {
return {
accountInfo:'accountInfo',
activeName: 'first',
objId: this.$route.params.id,
dataList: []
}
}
}</script>
(3) 用户信息组件
复制
<template>
<div class="boxInfo">
<!-- 表单内容 -->
<div class="formInfo">
<div>
<!-- 头部信息 -->
<div class="userInfo">
<div class="headInfo clearfix">
<div class="headText">
<el-form ref="formData" :model="formData" label-width="215px">
<el-form-item label="姓名:">
<el-input v-model="formData.username" placeholder="请输入"
class="inputW"></el-input>
</el-form-item>
<el-form-item label="密码:">
<el-input v-model="formData.password" placeholder="请输入"
class="inputW"></el-input>
</el-form-item>
<el-form-item label="部门:">
<el-input
placeholder="请选择"
v-model="formData.departmentName"
icon="caret-bottom"
class="inputW"
@click.native="isShowSelect = !isShowSelect">
</el-input>
<input v-model="formData.departmentId" type="hidden" >
<el-tree v-if="isShowSelect"
:expand-on-click-node="false"
:data="inspectionObjectOptions"
:props="{label:'name'}"
default-expand-all
:filter-node-method="filterNode"
@node-click="handleNodeClick"
class="objectTree"
ref="tree2">
</el-tree>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveData">更新</el-button>
<router-link :to="{'path':'/employees/index'}" class="el-button
el-button--text el-button--small">
取消
</router-link>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import constantApi from '@/api/constant/employees'
import {detail,update} from "@/api/base/users"
import { organList } from '@/api/base/departments'
export default {
name: 'accountInfo',
props: ['objId'],
data() {
return {
baseData: constantApi,
inspectionObjectOptions: [],
isShowSelect:false,
formData: {
id: this.objId,
}
}
},
methods: {
handleNodeClick(data) {
this.formData.departmentName = data.name
this.formData.departmentId = data.id
this.isShowSelect = false
},
// 获取详情
getObjInfo() {
detail({ id: this.objId }).then(res => {
this.formData = res.data.data
})
},
saveData(obj) {
update(this.formData)
.then(res => {
this.formData = res.data
this.$message.success('保存成功!')
this.getObjInfo()
})
},
},
// 创建完毕状态
created: function() {
this.getObjInfo()
organList().then(ret => {
this.inspectionObjectOptions.push(ret.data.data)
})
}
}</script>
4.4.3 用户的新增
和组织机构的增删改查大同小异,可以参照代码自行实现
第4章 权限管理与jwt鉴权
学习目标:
理解权限管理的需求以及设计思路实现角色分配和权限分配
理解常见的认证机制
能够使用JWT完成微服务Token签发与验证
1权限管理
1.1需求分析
完成权限(菜单,按钮(权限点),API接口)的基本操作

权限与菜单,菜单与按钮,菜单与API接口都是一对一关系。为了方便操作,在SAAS-HRM系统的表设计中,采用基于共享主键的形式实现一对一关系维护,并且数据库约束,一切的关系维护需要程序员在代码中实现。
1.2后端实现
1.2.1实体类
在系统微服务中创建权限,菜单,按钮(权限点),API对象的实体类
(1)权限实体类Permission
package com.ihrm.common.utils;
import org.springframework.cglib.beans.BeanMap;
import java.util.HashMap;
import java.util.Map;public class BeanMapUtils {
/**
* 将对象属性转化为map结合
*/
public static <T> Map<String, Object> beanToMap(T bean) {
Map<String, Object> map = new HashMap<>();
if (bean != null) {
BeanMap beanMap = BeanMap.create(bean);
for (Object key : beanMap.keySet()) {
map.put(key+"", beanMap.get(key));
}
}
return map;
}/**
* 将map集合中的数据转化为指定对象的同名属性中
*/
public static <T> T mapToBean(Map<String, Object> map,Class<T> clazz) throws Exception {
T bean = clazz.newInstance();
BeanMap beanMap = BeanMap.create(bean);
beanMap.putAll(map);
return bean;
}
}
2、权限菜单实体类PermissionMenu
package com.ihrm.common.utils;
public class PermissionConstants {
/**
* 权限类型 1为菜单 2为功能 3为API
*/
public static final int PERMISSION_MENU = 1;
public static final int PERMISSION_POINT = 2;
public static final int PERMISSION_API = 3;
}
3、权限菜单(权限点)实体类PermissionPoint
package com.ihrm.domain.system;
import lombok.Getter;
import lombok.Setter;import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;/**
* Description: 菜单权限实体类
*/
@Entity
@Table(name = "pe_permission_point")
@Getter
@Setter
public class PermissionPoint implements Serializable {
private static final long serialVersionUID = -1002411490113957485L;/**
* 主键
*/
@Id
private String id;/**
* 权限代码
*/
private String pointClass;private String pointIcon;
private String pointStatus;
}
4、权限API实体类PermissionApi
package com.ihrm.domain.system;
import lombok.Getter;
import lombok.Setter;import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;@Entity
@Table(name = "pe_permission_api")
@Getter
@Setter
public class PermissionApi implements Serializable {
private static final long serialVersionUID = -1803315043290784820L;
/**
* 主键
*/
@Id
private String id;
/**
* 链接
*/
private String apiUrl;
/**
* 请求类型
*/
private String apiMethod;
/**
* 权限等级,1为通用接口权限,2为需校验接口权限
*/
private String apiLevel;
}
(2)、DAO层
1、权限DAO层
package com.ihrm.system.dao;
import com.ihrm.domain.system.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;import java.util.List;
/**
* 权限数据访问接口
*/
public interface PermissionDao extends JpaRepository<Permission, String>, JpaSpecificationExecutor<Permission> {
List<Permission> findByTypeAndPid(int type,String pid);
}
2、权限菜单DAO层
package com.ihrm.system.dao;
import com.ihrm.domain.system.PermissionMenu;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;/**
* 企业数据访问接口
*/
public interface PermissionMenuDao extends JpaRepository<PermissionMenu, String>, JpaSpecificationExecutor<PermissionMenu> {}
3、权限按钮(点)DAO层
package com.ihrm.system.dao;
import com.ihrm.domain.system.PermissionPoint;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;/**
* 企业数据访问接口
*/
public interface PermissionPointDao extends JpaRepository<PermissionPoint, String>, JpaSpecificationExecutor<PermissionPoint> {}
4、权限Api DAO层
package com.ihrm.system.dao;
import com.ihrm.domain.system.PermissionApi;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;/**
* 企业数据访问接口
*/
public interface PermissionApiDao extends JpaRepository<PermissionApi, String>, JpaSpecificationExecutor<PermissionApi> {}
(3)、Service层
package com.ihrm.system.service;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.common.exception.CommonException;
import com.ihrm.common.utils.BeanMapUtils;
import com.ihrm.common.utils.IdWorker;
import com.ihrm.common.utils.PermissionConstants;
import com.ihrm.domain.system.*;
import com.ihrm.system.dao.*;
import com.ihrm.system.dao.PermissionDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;@Service
@Transactional
public class PermissionService {@Autowired
private PermissionDao permissionDao;@Autowired
private PermissionMenuDao permissionMenuDao;@Autowired
private PermissionPointDao permissionPointDao;@Autowired
private PermissionApiDao permissionApiDao;@Autowired
private IdWorker idWorker;/**
* 1.保存权限
*/
public void save(Map<String,Object> map) throws Exception {
//设置主键的值
String id = idWorker.nextId()+"";
//1.通过map构造permission对象
Permission perm = BeanMapUtils.mapToBean(map,Permission.class);
perm.setId(id);
//2.根据类型构造不同的资源对象(菜单,按钮,api)
int type = perm.getType();
switch (type) {
case PermissionConstants.PERMISSION_MENU:
PermissionMenu menu = BeanMapUtils.mapToBean(map,PermissionMenu.class);
menu.setId(id);
permissionMenuDao.save(menu);
break;
case PermissionConstants.PERMISSION_POINT:
PermissionPoint point = BeanMapUtils.mapToBean(map,PermissionPoint.class);
point.setId(id);
permissionPointDao.save(point);
break;
case PermissionConstants.PERMISSION_API:
PermissionApi api = BeanMapUtils.mapToBean(map,PermissionApi.class);
api.setId(id);
permissionApiDao.save(api);
break;
default:
throw new CommonException(ResultCode.FAIL);
}
//3.保存
permissionDao.save(perm);
}/**
* 2.更新权限
*/
public void update(Map<String,Object> map) throws Exception {
Permission perm = BeanMapUtils.mapToBean(map,Permission.class);
//1.通过传递的权限id查询权限
Permission permission = permissionDao.findById(perm.getId()).get();
permission.setName(perm.getName());
permission.setCode(perm.getCode());
permission.setDescription(perm.getDescription());
permission.setEnVisible(perm.getEnVisible());
//2.根据类型构造不同的资源
int type = perm.getType();
switch (type) {
case PermissionConstants.PERMISSION_MENU:
PermissionMenu menu = BeanMapUtils.mapToBean(map,PermissionMenu.class);
menu.setId(perm.getId());
permissionMenuDao.save(menu);
break;
case PermissionConstants.PERMISSION_POINT:
PermissionPoint point = BeanMapUtils.mapToBean(map,PermissionPoint.class);
point.setId(perm.getId());
permissionPointDao.save(point);
break;
case PermissionConstants.PERMISSION_API:
PermissionApi api = BeanMapUtils.mapToBean(map,PermissionApi.class);
api.setId(perm.getId());
permissionApiDao.save(api);
break;
default:
throw new CommonException(ResultCode.FAIL);
}
//3.保存
permissionDao.save(permission);
}/**
* 3.根据id查询
* //1.查询权限
* //2.根据权限的类型查询资源
* //3.构造map集合
*/
public Map<String, Object> findById(String id) throws Exception {
Permission perm = permissionDao.findById(id).get();
int type = perm.getType();Object object = null;
if(type == PermissionConstants.PERMISSION_MENU) {
object = permissionMenuDao.findById(id).get();
}else if (type == PermissionConstants.PERMISSION_POINT) {
object = permissionPointDao.findById(id).get();
}else if (type == PermissionConstants.PERMISSION_API) {
object = permissionApiDao.findById(id).get();
}else {
throw new CommonException(ResultCode.FAIL);
}Map<String, Object> map = BeanMapUtils.beanToMap(object);
map.put("name",perm.getName());
map.put("type",perm.getType());
map.put("code",perm.getCode());
map.put("description",perm.getDescription());
map.put("pid",perm.getPid());
map.put("enVisible",perm.getEnVisible());
return map;
}/**
* 4.查询全部
* type : 查询全部权限列表type:0:菜单 + 按钮(权限点) 1:菜单2:按钮(权限点)3:API接口
* enVisible : 0:查询所有saas平台的最高权限,1:查询企业的权限
* pid :父id
*/
public List<Permission> findAll(Map<String, Object> map) {
//1.需要查询条件
Specification<Permission> spec = new Specification<Permission>() {
/**
* 动态拼接查询条件
*/
public Predicate toPredicate(Root<Permission> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> list = new ArrayList<>();
//根据父id查询
if(!StringUtils.isEmpty(map.get("pid"))) {
list.add(criteriaBuilder.equal(root.get("pid").as(String.class),(String)map.get("pid")));
}
//根据enVisible查询
if(!StringUtils.isEmpty(map.get("enVisible"))) {
list.add(criteriaBuilder.equal(root.get("enVisible").as(String.class),(String)map.get("enVisible")));
}
//根据类型 type
if(!StringUtils.isEmpty(map.get("type"))) {
String ty = (String) map.get("type");
CriteriaBuilder.In<Object> in = criteriaBuilder.in(root.get("type"));
if("0".equals(ty)) {
in.value(1).value(2);
}else{
in.value(Integer.parseInt(ty));
}
list.add(in);
}
return criteriaBuilder.and(list.toArray(new Predicate[list.size()]));
}
};
return permissionDao.findAll(spec);
}/**
* 5.根据id删除
* //1.删除权限
* //2.删除权限对应的资源
*
*/
public void deleteById(String id) throws Exception {
//1.通过传递的权限id查询权限
Permission permission = permissionDao.findById(id).get();
permissionDao.delete(permission);
//2.根据类型构造不同的资源
int type = permission.getType();
switch (type) {
case PermissionConstants.PERMISSION_MENU:
permissionMenuDao.deleteById(id);
break;
case PermissionConstants.PERMISSION_POINT:
permissionPointDao.deleteById(id);
break;
case PermissionConstants.PERMISSION_API:
permissionApiDao.deleteById(id);
break;
default:
throw new CommonException(ResultCode.FAIL);
}
}
}
(4)、Controller层
package com.ihrm.system.controller;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.domain.system.Permission;
import com.ihrm.system.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;
import java.util.Map;//1.解决跨域
@CrossOrigin
//2.声明RestContoller
@RestController
//3.设置父路径
@RequestMapping(value="/sys")
public class PermissionController {@Autowired
private PermissionService permissionService;/**
* 保存
*/
@RequestMapping(value = "/permission", method = RequestMethod.POST)
public Result save(@RequestBody Map<String,Object> map) throws Exception {
permissionService.save(map);
return new Result(ResultCode.SUCCESS);
}/**
* 修改
*/
@RequestMapping(value = "/permission/{id}", method = RequestMethod.PUT)
public Result update(@PathVariable(value = "id") String id, @RequestBody Map<String,Object> map) throws Exception {
//构造id
map.put("id",id);
permissionService.update(map);
return new Result(ResultCode.SUCCESS);
}/**
* 查询列表
*/
@RequestMapping(value = "/permission", method = RequestMethod.GET)
public Result findAll(@RequestParam Map map) {
List<Permission> list = permissionService.findAll(map);
return new Result(ResultCode.SUCCESS,list);
}/**
* 根据ID查询
*/
@RequestMapping(value = "/permission/{id}", method = RequestMethod.GET)
public Result findById(@PathVariable(value = "id") String id) throws Exception {
Map map = permissionService.findById(id);
return new Result(ResultCode.SUCCESS,map);
}/**
* 根据id删除
*/
@RequestMapping(value = "/permission/{id}", method = RequestMethod.DELETE)
public Result delete(@PathVariable(value = "id") String id) throws Exception {
permissionService.deleteById(id);
return new Result(ResultCode.SUCCESS);
}
}
<3>、前端实现
1、引入权限管理模块
将module-permissions 引入到工程的 /src 文件夹下,在 /src/main.js 完成模块注册
import permissions from '@/module-permissions/' // 权限管理
Vue.use(permissions, store)
12
2、配置API
在 /src/api/base/ 目录下创建 permissions.js
import {createAPI} from '@/utils/request'
const api = "/sys/permission"
export const list = data => createAPI(`${api}`, 'get', data)
export const add = data => createAPI(`${api}`, 'post', data)
export const update = data => createAPI(`${api}/${data.id}`, 'put', data)
export const remove = data => createAPI(`${api}/${data.id}`, 'delete', data)
export const detail = data => createAPI(`${api}/${data.id}`, 'get', data)
export const saveOrUpdate = data => {return data.id?update(data):add(data)}
12345678
3、实现权限页面
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card shadow="never">
<el-button class="filter-item fr" size="small" style="margin-left: 10px;" @click="handleCreate(null,1);setPid(1,'0')" type="primary" icon="el-icon-edit">添加菜单 </el-button>
<el-table :data="dataList" fit style="width: 100%;" highlight-current-row> <el-table-column fixed prop="name" label="菜单名称" width="200px">
<template slot-scope="scope">
<i :class="scope.row.type==1?'ivu-icon fa fa-folder-open-o fa-
fw':'ivu-icon el-icon-view'"
:style="scope.row.type==1?'margin-left: 0px':'margin-left:
20px'"></i>
<span @click="show(scope.$index,scope.row.id)">
{{scope.row.name}}</span>
table-column>
table-column>
</template>
</el-table-column>
<el-table-column fixed prop="code" label="权限标识" width="200"></el-
<el-table-column fixed prop="description" label="描述" width="200"></el-
<el-table-column fixed="right" label="操作"> <template slot-scope="scope">
<el-button v-if="scope.row.type==1" @click="handleCreate(null,2);setPid(2,scope.row.id)" type="text" size="small">添加权限点
</el-button>
<el-button @click="handlerApiList(scope.row.id)" type="text" size="small">查看api权限</el-button>
<el-button
@click="handleCreate(scope.row.id,scope.row.type);setPid(scope.row.type,scope.row.pid)" type="text" size="small">查看</el-button>
<el-button @click="handleDelete(scope.row.id)" type="text" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<el-dialog title="编辑权限" :visible.sync="dialogFormVisible" style="hight:100px;line-height:1px">
<el-form :model="formData" label-width="90px" style="margin-top:20px"> <el-form-item label="权限名称">
</el-input>
</el-input>
<el-input v-model="formData.name" autocomplete="off" style="width:90%">
</el-form-item>
<el-form-item label="权限标识">
<el-input v-model="formData.code" autocomplete="off" style="width:90%">
</el-form-item>
<el-form-item label="权限描述">
<el-input v-model="formData.description" autocomplete="off"
style="width:90%"></el-input>
</el-form-item>
<div v-if="type==1">
<el-form-item label="菜单顺序">
<el-input v-model="formData.menuOrder" autocomplete="off"
style="width:90%"></el-input>
</el-form-item>
<el-form-item label="菜单icon">
<el-input v-model="formData.menuIcon" autocomplete="off"
style="width:90%"></el-input>
</el-form-item>
</div>
<div v-else-if="type==2">
<el-form-item label="按钮样式">
<el-input v-model="formData.pointClass" autocomplete="off"
style="width:90%"></el-input>
</el-form-item>
<el-form-item label="按钮icon">
<el-input v-model="formData.pointIcon" autocomplete="off"
style="width:90%"></el-input>
</el-form-item>
<el-form-item label="按钮状态">
<el-input v-model="formData.pointStatus" autocomplete="off"
style="width:90%"></el-input>
</el-form-item>
</div>
<div v-else-if="type==3">
<el-form-item label="api请求地址">
<el-input v-model="formData.apiUrl" autocomplete="off"
style="width:90%"></el-input>
</el-form-item>
<el-form-item label="api请求方式">
<el-input v-model="formData.apiMethod" autocomplete="off"
style="width:90%"></el-input>
</el-form-item>
<el-form-item label="api类型">
<el-input v-model="formData.apiLevel" autocomplete="off"
style="width:90%"></el-input>
</el-form-item>
</div>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate">确 定</el-button> </div>
</el-dialog>
<el-dialog title="API权限列表" :visible.sync="apiDialogVisible" style="hight:400px;line-height:1px">
<el-button class="filter-item fr" size="small" style="margin-left: 10px;" @click="handleCreate(null,1);setPid(3,pid)" type="primary" icon="el-icon-edit">添加api权 限</el-button>
<el-table :data="apiList" fit style="width: 100%;" max-height="250" > <el-table-column fixed prop="name" label="菜单名称" width="120px"></el-
table-column>
table-column>
table-column>
<el-table-column fixed prop="code" label="权限标识" width="200"></el- <el-table-column fixed prop="description" label="描述" width="200"></el-
<el-table-column fixed="right" label="操作" width="200"> <template slot-scope="scope">
<el-button
@click="handleCreate(scope.row.id,scope.row.type);setPid(scope.row.type,scope.row.pid)" type="text" size="small">查看</el-button>
<el-button @click="handleDelete(scope.row.id);handlerApiList(pid)" type="text" size="small">删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
2、分配角色
由于使用了RBAC模型对权限进行统一管理,所以每个SaaS-HRM平台的用户都应该具有角色的信息,进而通过角色完成对权限的识别。重所周知,一个用户可以具有很多的角色,一个角色可以被分配给不同的用户。所以用户和角色之间是多对多关系。

<2>、服务端功能实现
1、改造用户实体类,添加角色的id集合属性,表明一个用户具有的多个角色id
在 com.ihrm.system.domain.User 用户实体类中添加与角色的多对多关系并进行JPA的配置
@JsonIgnore
@ManyToMany
@JoinTable(name="pe_user_role",joinColumns={@JoinColumn(name="user_id",referencedColumnName="id")},
inverseJoinColumns={@JoinColumn(name="role_id",referencedColumnName="id")}
)
private Set<Role> roles = new HashSet<Role>();//用户与角色 多对多
在com.ihrm.system.domain.Role角色实体类中配置角色与用户的多对多关系并进行JPA配置
@JsonIgnore
@ManyToMany(mappedBy="roles") //不维护中间表
private Set<User> users = new HashSet<User>(0);//角色与用户 多对多
2、 在 com.ihrm.system.controller.UserController 添加分配角色的控制器方法实现
/**
* 分配角色
*/
@RequestMapping(value = "/user/assignRoles", method = RequestMethod.PUT)
public Result assignRoles(@RequestBody Map<String,Object> map) {
//1.获取被分配的用户id
String userId = (String) map.get("id");
//2.获取到角色的id列表
List<String> roleIds = (List<String>) map.get("roleIds");
//3.调用service完成角色分配
userService.assignRoles(userId,roleIds);
return new Result(ResultCode.SUCCESS);
}
3、在Service层添加分配角色的方法
/**
* 分配角色
*/
public void assignRoles(String userId,List<String> roleIds) {
//1.根据id查询用户
User user = userDao.findById(userId).get();
//2.设置用户的角色集合
Set<Role> roles = new HashSet<>();
for (String roleId : roleIds) {
Role role = roleDao.findById(roleId).get();
roles.add(role);
}
//设置用户和角色集合的关系
user.setRoles(roles);
//3.更新用户
userDao.save(user);
}
<3>、前端实现
1、在src/module-employees 添加分配角色的组件
<template>
<div class="add-form">
<el-dialog title="分配角色" :visible.sync="roleFormVisible" style="height:300px">
<el-form :model="formBase" label-position="left" label-width="120px"
style='margin-left:120px; width:500px;'>
<el-checkbox-group
v-model="checkedCities1">
<el-checkbox v-for="(item,index) in cities" :label="item.id" :key="index">
{{item.name}}</el-checkbox>
</el-checkbox-group>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="createData">提交</el-button>
<el-button @click="roleFormVisible=false">取消</el-button> </div>
</el-dialog>
</div>
</template>
<script>
import {findAll} from "@/api/base/role"
import {assignRoles} from "@/api/base/users"
export default {
data () {
return {
roleFormVisible:false,
formBase:{},
checkedCities1:[],
data:[],
cities:[],
id:null
}
},
methods: {
toAssignPrem(id) {
findAll().then(res => {
this.id = id;
this.cities = res.data.data
this.roleFormVisible=true
})
},
createData() {
assignRoles({id:this.id,ids:this.checkedCities1}).then(res => {
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
this.roleFormVisible=false
})
}
}
}
</script>
2、在src/module-employees/pages/employees-list.vue 引入组件
<!--分配角色组件 -->
<component v-bind:is="addRole" ref="addRole"></component>
12
3、分配权限
完成对角色权限的分配

<2>、服务端实现
1、角色实体类中添加与权限的多对多关系并且进行JPA配置
@JsonIgnore
@ManyToMany
@JoinTable(name="pe_role_permission",
joinColumns={@JoinColumn(name="role_id",referencedColumnName="id")},
inverseJoinColumns={@JoinColumn(name="permission_id",referencedColumnName="id")})
private Set<Permission> permissions = new HashSet<Permission>(0);//角色与模块 多对多
2、控制器类( com.ihrm.system.controller.RoleController )添加权限分配
/**
* 分配权限
*/
@RequestMapping(value = "/role/assignPrem", method = RequestMethod.PUT)
public Result assignPrem(@RequestBody Map<String,Object> map) {
//1.获取被分配的角色的id
String roleId = (String) map.get("id");
//2.获取到权限的id列表
List<String> permIds = (List<String>) map.get("permIds");
//3.调用service完成权限分配
roleService.assignPerms(roleId,permIds);
return new Result(ResultCode.SUCCESS);
}
3、在Service层添加分配权限的方法
/**
* 分配权限
*/
public void assignPerms(String roleId,List<String> permIds) {
//1.获取分配的角色对象
Role role = roleDao.findById(roleId).get();
//2.构造角色的权限集合
Set<Permission> perms = new HashSet<>();
for (String permId : permIds) {
Permission permission = permissionDao.findById(permId).get();
//需要根据父id和类型查询API权限列表
List<Permission> apiList = permissionDao.findByTypeAndPid(PermissionConstants.PERMISSION_API, permission.getId());
perms.addAll(apiList);//自定赋予API权限
perms.add(permission);//当前菜单或按钮的权限
}
System.out.println(perms.size());
//3.设置角色和权限的关系
role.setPermissions(perms);
//4.更新角色
roleDao.save(role);
}
4、在DAO层添加方法
public interface PermissionDao extends JpaRepository<Permission, String>, JpaSpecificationExecutor<Permission> {
List<Permission> findByTypeAndPid(int type,String pid);
}
<3>、前端实现
1、在src/module-settings/components/role-list.vue 绑定权限按钮
<el-table-column fixed="right" label="操作" align="center" width="250">
<template slot-scope="scope">
<el-button @click="handlerPerm(scope.row)" type="text" size="small">分配权限</el- button>
<el-button @click="handleUpdate(scope.row)" type="text" size="small">修改</el- button>
<el-button @click="handleDelete(scope.row)" type="text" size="small">删除</el- button>
</template>
</el-table-column>
2、在src/api/base/role.js 中添加分配权限的API方法
export const assignPrem = data => createAPI(`/sys/role/assignPrem`, 'put', data)
3、src/module-settings/components/role-list.vue 使用Element-UI构造权限树
<el-dialog :title="'为【'+formData.name+'】分配权限'" :visible.sync="permFormVisible" style="hight:100px;line-height:1px">
<el-tree
:data="treeData"
default-expand-all
show-checkbox
node-key="id"
ref="tree"
:default-checked-keys="checkNodes"
:props="{label:'name'}">
</el-tree>
<div slot="footer" class="dialog-footer">
<el-button @click="permFormVisible = false">取 消</el-button>
<el-button type="primary" @click="assignPrem">确 定</el-button>
</div>
</el-dialog>
4、完成添加权限
import {list,add,update,remove,detail,assignPrem} from "@/api/base/role"
import * as permApi from "@/api/base/permissions"
import commonApi from "@/utils/common"
import PageTool from './../../components/page/page-tool'
var _this = null
export default {
name: 'roleList',
components: {PageTool},
props: ['objId'],
data() {
return {
formData:{},
treeData:[],
checkNodes:[],
dialogFormVisible: false,
permFormVisible:false,
dataList:[],
counts:0,
requestParameters:{
page: 1,
pagesize: 10
}
}
},
methods: {
assignPrem() {
assignPrem({roleId:this.formData.id,ids:this.$refs.tree.getCheckedKeys()}).then(res =>{
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
this.permFormVisible=false
})
},
handlerPerm(obj) {
detail({id:obj.id}).then(res=>{
this.formData = res.data.data;
if(this.formData.menusIds != null) {
this.checkNodes = this.formData.menusIds.split(",")
}
if(this.formData.pointIds != null) {
this.checkNodes.push(this.formData.pointIds.split(","))
}
permApi.list({type:0,pid:null}).then(res => {
this.treeData = commonApi.transformTozTreeFormat(res.data.data)
this.permFormVisible=true
})
})
},
handlerAdd() {
this.formData={}
this.dialogFormVisible = true
},
handleDelete(obj) {
this.$confirm(
`本次操作将删除${obj.name},删除后角色将不可恢复,您确认删除吗?` ).then(() => {
remove({id: obj.id}).then(res => {
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
this.doQuery()
})
})
},
handleUpdate(obj) {
detail({id:obj.id}).then(res=>{
this.formData = res.data.data;
this.formData.id = obj.id;
this.dialogFormVisible = true
})
},
saveOrUpdate() {
if(this.formData.id == null || this.formData.id == undefined) {
this.save()
}else{
this.update();
}
},
update(){
update(this.formData).then(res=>{
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
if(res.data.success){
this.formData={};
this.dialogFormVisible=false;
this.doQuery();
}
})
},
save() {
add(this.formData).then(res=>{
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
if(res.data.success){
this.formData={};
this.dialogFormVisible=false;
this.doQuery();
}
})
},
// 获取详情
doQuery() {
list(this.requestParameters).then(res => {
this.dataList = res.data.data.rows
this.counts = res.data.data.total
})
},
// 每页显示信息条数
handleSizeChange(pageSize) {
this.requestParameters.pagesize = pageSize
if (this.requestParameters.page === 1) {
_this.doQuery(this.requestParameters)
}
},
// 进入某一页
handleCurrentChange(val) {
this.requestParameters.page = val
_this.doQuery()
},
},
// 挂载结束
mounted: function() {},
// 创建完毕状态
created: function() {
_this = this
this.doQuery()
},
// 组件更新
updated: function() {}
}
</script>
4、常见的认证机制
<1>、HTTP Basic Auth
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合 RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于存在将用户名密码暴露给第三方客户端的风险,因此在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth
<2>、Cookie Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上的Cookie对象来与服务器端的Session对象匹配来实现状态管理的。默认,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie的expire time使cookie在一定时间内有效
<3>、OAuth
OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样, OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容

这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等等应用,但是不太适合拥有自有认证权限管理的企业应用。
<4>、Token Auth
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
客户端使用用户名跟密码请求登录
服务端收到请求,去验证用户名与密码
验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
Token Auth的优点
支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的
(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理. 基于标准化:你的API可以采用标准化的 JSON Web Token ( JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft
5HRM中的TOKEN签发与验证
5.1什么是JWT
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。在Java世界中通过JJWT实现JWT创建和验证。
5.2JJWT的快速入门
5.2.1token的创建
(1)在ihrm-common工程,引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2、创建CreateJwtTest,用于生成token
package com.ihrm.demo;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import java.util.Date;
public class CreateJwtTest {
/**
* 通过jjwt创建token
*/
public static void main(String[] args) {
JwtBuilder jwtBuilder = Jwts.builder().setId("88").setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "ihrm");
String token = jwtBuilder.compact();
System.out.println(token);
}
}
3、运行结果如下:
yJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTM0NTh9.gq0JcOM_qCNqU_s-d_IrRytaNenesPmqAIhQpYXHZk
5.2.2token的解析
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息 (例如用户id),根据这些信息查询数据库返回相应的结果。
package com.ihrm.demo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;public class ParseJwtTest {
/**
* 解析jwtToken字符串
*/
public static void main(String[] args) {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4OCIsInN1YiI6IuWwj-eZvSIsImlhdCI6MTU2Mjc0MTA3NH0._yMPiB0x2793K4BmCsodcwpkSuJK_76Av73p8XR-kDs";
Claims claims = Jwts.parser().setSigningKey("ihrm").parseClaimsJws(token).getBody();//私有数据存放在claims
System.out.println(claims.getId());
System.out.println( claims.getSubject());
System.out.println(claims.getIssuedAt());
}
}
试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证toke
5.2.3自定义claims
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims
(1)创建CreateJwtTest3,并存储指定的内容
package com.ihrm.demo;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import java.util.Date;
public class CreateJwtTest {
/**
* 通过jjwt创建token
*/
public static void main(String[] args) {
JwtBuilder jwtBuilder = Jwts.builder().setId("88").setSubject("小白")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "ihrm")
.claim("companyId","123456")
.claim("companyName","gongda")
;
String token = jwtBuilder.compact();
System.out.println(token);
}
}
2、修改ParseJwtTest,获取指定信息
public class ParseJwtTest {
public static void main(String[] args) {
String compactJws="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MT czMjMsImV4cCI6MTUyMzQxNzM4Mywicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJsb2dvLnBuZyJ9.b11p4g4rE94r qFhcfzdJTPCORikqP_1zJ1MP8KihYTQ";
Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJws).getBody();
System.out.println("id:"+claims.getId()); System.out.println("subject:"+claims.getSubject()); System.out.println("roles:"+claims.get("roles")); System.out.println("logo:"+claims.get("logo"));
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println("签发时间:"+sdf.format(claims.getIssuedAt())); System.out.println("过期时间:"+sdf.format(claims.getExpiration())); System.out.println("当前时间:"+sdf.format(new Date()) );
}
}
5.3JWT工具类
在ihrm_common工程中创建JwtUtil工具类
@ConfigurationProperties("jwt.config") public class JwtUtil {
private String key; private long ttl;
public String getKey() { return key;
}
public void setKey(String key) { this.key = key;
}
public long getTtl() { return ttl;
}
public void setTtl(long ttl) { this.ttl = ttl;
}
/**
* 签 发 token
*/
public String createJWT(String id, String subject,Map<String,Object> map){ long now=System.currentTimeMillis();
long exp=now+ttl;
JwtBuilder jwtBuilder = Jwts.builder().setId(id)
.setSubject(subject).setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, key); for(Map.Entry<String,Object> entry:map.entrySet()) {
jwtBuilder.claim(entry.getKey(),entry.getValue());
}
if(ttl>0){
jwtBuilder.setExpiration( new Date(exp));}
/**}
String token = jwtBuilder.compact(); return token;
*解析JWT
*@param token
*@return
*/
public Claims parseJWT(String token){ Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token).getBody();
}catch (Exception e){}
return claims;
}
}
(3)修改ihrm_common工程的application.yml, 添加配置
jwt: config:
key: saas-ihrm ttl: 360000
5.4登录成功签发token
(1)配置JwtUtil。修改ihrm_system工程的启动类
@Bean
public JwtUtil jwtUtil(){ return new util.JwtUtil();
}
(2)添加登录方法
/**
*用户登录
*1.通过service根据mobile查询用户
*2.比较password
*3.生成jwt信息
*
*/
@RequestMapping(value="/login",method = RequestMethod.POST) public Result login(@RequestBody Map<String,String> loginMap) {
String mobile = loginMap.get("mobile"); String password = loginMap.get("password");
User user = userService.findByMobile(mobile);
//登录失败
if(user == null || !user.getPassword().equals(password)) { return new Result(ResultCode.MOBILEORPASSWORDERROR);
}else {
//登录成功
Map<String,Object> map = new HashMap<>(); map.put("companyId",user.getCompanyId()); map.put("companyName",user.getCompanyName());
String token = jwtUtils.createJwt(user.getId(), user.getUsername(), map); return new Result(ResultCode.SUCCESS,token);
}
}
(3)测试运行结果

使用postman验证登录返回:
{"success":true,"code":10000,"message":"操作成
功!","data":"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDYyNjYxODkxNjE4Mzc3NzI4Iiwic3ViIjoiemhhb
mdzYW4iLCJpYXQiOjE1NDI0NjgzNzcsImNvbXBhbnlJZCI6IjEiLCJjb21wYW55TmFtZSI6IuS8oOaZuuaSreWu oiIsImV4cCI6MTU0MjU1NDc3N30.J-8uv8jOp2GMLpBwrUOksnErjA4-DOJ_qvy7tsJbsa8"}
5.5获取用户信息鉴权
需求:用户登录成功之后,会发送一个新的请求到服务端,获取用户的详细信息。获取用户信息的过程中必须登录 才能,否则不能获取。
前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格+token
(1)添加响应值对象
@Getter @Setter
@NoArgsConstructor
public class ProfileResult {private String mobile; private String username;
private String company; private Map roles;public ProfileResult(User user) { this.mobile = user.getMobile(); this.username = user.getUsername(); this.company = user.getCompanyName();
//角色数据
Set<String> menus = new HashSet<>(); Set<String> points = new HashSet<>(); Set<String> apis = new HashSet<>(); Map rolesMap = new HashMap<>();
for (Role role : user.getRoles()) {
for (Permission perm : role.getPermissions()) { String code = perm.getCode(); if(perm.getType() == 1) {
menus.add(code);
}else if(perm.getType() == 2) { points.add(code);
}else {
apis.add(code);
}
}
}
rolesMap.put("menus",menus); rolesMap.put("points",points); rolesMap.put("apis",points); this.roles = rolesMap;
}
}
(2)添加profile方法
/**
* 获取个人信息
*/
@RequestMapping(value = "/profile", method = RequestMethod.POST) public Result profile(HttpServletRequest request) throws Exception {
//临时使用
String userId = "1";
User user = userService.findById(userId);
return new Result(ResultCode.SUCCESS,new ProfileResult(user));
}
(3)验证token
思路:从请求中获取key为Authorization的token信息,并使用jwt验证,验证成功后获取隐藏信息。 修改profile方法添加如下代码
@RequestMapping(value = "/profile", method = RequestMethod.POST) public Result profile(HttpServletRequest request) throws Exception {
//请求中获取key为Authorization的头信息
String authorization = request.getHeader("Authorization"); if(StringUtils.isEmpty(authorization)) {
throw new CommonException(ResultCode.UNAUTHENTICATED);
}
//前后端约定头信息内容以 Bearer+空格+token 形式组成
String token = authorization.replace("Bearer ", "");
//比较并获取claims
Claims claims = jwtUtil.parseJWT(token); if(claims == null) {
throw new CommonException(ResultCode.UNAUTHENTICATED);
}
String userId = claims.getId();
User user = userService.findById(userId);
return new Result(ResultCode.SUCCESS,new ProfileResult(user));
}
第五章 权限管理与shiro入门
1、前端权限控制
<1>、需求分析
(1)、需求说明
基于前后端分离的开发模式中,权限控制分为前端页面可见性权限与后端API接口可访问权限。前端的权限控制主要围绕在菜单是否可见,以及菜单中按钮是否可见两方面展开的。
(2)、实现方案
在Vue工程中,菜单可以简单的理解为Vue中的路由,只需要根据登录用户的权限信息动态的加载路由列表就可以动态的构造出访问菜单。
登录成功后获取用户信息,包含权限列表(菜单权限,按钮权限)
根据用户菜单权限列表,动态构造路由(根据路由名称和权限标识比较)
页面按钮权限通过自定义方法控制可见性

<2>、服务端实现
对系统微服务的FrameController的profile方法(获取用户信息接口)进行修改,添加权限信息
/**
* 获取个人信息
*/
@RequestMapping(value = "/profile", method = RequestMethod.POST)
public Result profile(HttpServletRequest request) throws Exception {
//请求中获取key为Authorization的头信息
String authorization = request.getHeader("Authorization");
if(StringUtils.isEmpty(authorization)) {
throw new CommonException(ResultCode.UNAUTHENTICATED);
}
//前后端约定头信息内容以 Bearer+空格+token 形式组成
String token = authorization.replace("Bearer ", "");
//比较并获取claims
Claims claims = jwtUtil.parseJWT(token);
if(claims == null) {
throw new CommonException(ResultCode.UNAUTHENTICATED);
}
//查询用户
User user = userService.findById(userId);
ProfileResult result = null;
if("user".equals(user.getLevel())) {
result = new ProfileResult(user);
}else {
Map map = new HashMap();
if("coAdmin".equals(user.getLevel())) {
map.put("enVisible","1");
}
List<Permission> list = permissionService.findAll(map);
result = new ProfileResult(user,list);
}
return new Result(ResultCode.SUCCESS,result);
}
<3>、前端实现
(1)、路由钩子函数
vue路由提供的钩子函数(beforeEach)主要用来在加载之前拦截导航,让它完成跳转或取消。可以在路由钩子函数中进行校验是否对某个路由具有访问权限
vue路由提供的钩子函数(beforeEach)主要用来在加载之前拦截导航,让它完成跳转或取消。可以在路由钩子函数中进行校验是否对某个路由具有访问权限
router.beforeEach((to, from, next) => {
NProgress.start() // start progress bar
if (getToken()) {
// determine if there has token
/* has token */
if (to.path === '/login') {
next({path: '/'})
NProgress.done() // if current page is dashboard will not trigger afterEach hook,
so manually handle it
} else {
if (store.getters.roles.length === 0) {
// 判断当前用户是否已拉取完user_info信息
store
.dispatch('GetUserInfo')
.then(res => {
// 拉取user_info
const roles = res.data.data.roles // note: roles must be a array! such as:
['editor','develop']
store.dispatch('GenerateRoutes', {roles}).then(() => {
// 根据roles权限生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({...to, replace: true}) // hack方法 确保addRoutes已完成 ,set the
replace: true so the navigation will not leave a history record
})
})
.catch(() => {
store.dispatch('FedLogOut').then(() => {
Message.error('验证失败, 请重新登录')
next({path: '/login'})
})
})
} else {
next()
}
}
} else {
/* has no token */
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next('/login') // 否则全部重定向到登录页
NProgress.done() // if current page is login will not trigger afterEach hook, so
manually handle it
}
}
})
(2)、配置菜单权限
在src/module-dashboard/store/permission.js 下进行修改,开启路由配置
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data
//动态构造权限列表
let accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
commit('SET_ROUTERS', accessedRouters)
//commit('SET_ROUTERS', asyncRouterMap) // 调试开启全部路由
resolve()
})
}
}
(3)、配置验证权限的方法
在src/utils/permission.js 配置验证是否具有权限的验证方法
import store from '@/store'
// 检查是否有权限
export function hasPermission(roles, route) {
if (roles.menus && route.name) {
return roles.menus.some(role => {
return route.name.toLowerCase() === role.toLowerCase()
})
} else {
return false
}
}
// 检查是否有权限点
export function hasPermissionPoint(point) {
let points = store.getters.roles.points
if (points) {
return points.some(it => it.toLowerCase() === point.toLowerCase())
} else {
return false
}
}
(4)、修改登录和获取信息的请求接口
关闭模拟测试接口
mock/index.js 中不加载登录(login)以及(profile)的模拟测试
import Mock from 'mockjs'
import TableAPI from './table'
import ProfileAPI from './profile'
import LoginAPI from './login'
Mock.setup({
//timeout: '1000'
})
Mock.mock(/\/table\/list\.*/, 'get', TableAPI.list)
//Mock.mock(/\/frame\/profile/, 'post', ProfileAPI.profile)
//Mock.mock(/\/frame\/login/, 'post', LoginAPI.login)
1.4 权限测试
<4>、权限测试
1、菜单测试
分配好权限之后,重新登录前端页面,左侧菜单发生了变化
2、对需要进行权限控制的(权限点)验证测试
页面添加校验方法
methods: {
checkPoint(point){
return hasPermissionPoint(point);
}
}
使用v-if验证权限是否存在,其中参数为配置的权限点标识
<el-button type="primary" v-if="checkPoint('POINT-USER-ADD')" size="mi
2、有状态服务和无状态服务
<1>、什么是服务中的状态
有状态和无状态服务是两种不同的服务架构,两者的不同之处在于对于服务状态的处理。服务状态是服务请求所需要的数据,它可以是一个变量或者一个数据结构。无状态服务不会记录服务状态,不同请求之间也是没有任何关系; 而有状态服务则反之。对服务器程序来说,究竟是有状态服务,还是无状态服务,其判断依据——两个来自相同发起者的请求在服务器端是否具备上下文关系。
<2>、无状态服务
无状态请求,服务器端所能够处理的数据全部来自于请求所携带的信息,无状态服务对于客户端的单次请求的处理,不依赖于其他请求,处理一次请求的信息都包含在该请求里。最典型的就是通过Cookie保存token的方式传输请求数据。也可以理解为Cookie是通过客户端保持状态的解决方案。

<3>、有状态服务
有状态服务则相反,服务会存储请求上下文相关的数据信息,先后的请求是可以有关联的。例如,在Web 应用中,经常会使用Session来维系登录用户的上下文信息。虽然http协议是无状态的,但是借助Session,可以使http服务转换为有状态服务

3、基于JWT的API鉴权
3.1、基于拦截器的token与鉴权
如果我们每个方法都去写一段代码,冗余度太高,不利于维护,那么如何做才能使代码看起来更加简洁,可以将这段代码放入拦截器去实现。
<1>Spring中的拦截器
Spring为我们提供了org.springframework.web.servlet.handler.HandlerInterceptorAdapter这个适配器,继承此类,可以非常方便的实现自己的拦截器。它有三个方法:分别实现预处理、后处理(调用了Service并返回 ModelAndView,但未进行页面渲染)、返回处理(已经渲染了页面)
1、在preHandle中,可以进行编码、安全控制等处理;
2、在postHandle中,有机会修改ModelAndView;
3、在afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录
<2>、签发用户API权限
在系统微服务的 com.ihrm.system.controller.UserController 修改签发token的登录服务添加API权限
/**
* 用户登录
* 1.通过service根据mobile查询用户
* 2.比较password
* 3.生成jwt信息
*
*/
@RequestMapping(value="/login",method = RequestMethod.POST)
public Result login(@RequestBody Map<String,String> loginMap) {
String mobile = loginMap.get("mobile");
String password = loginMap.get("password");
User user = userService.findByMobile(mobile);
//登录失败
if(user == null || !user.getPassword().equals(password)) {
return new Result(ResultCode.MOBILEORPASSWORDERROR);
}else {
//登录成功
//api权限字符串
StringBuilder sb = new StringBuilder();
//获取到所有的可访问API权限
for (Role role : user.getRoles()) {
for (Permission perm : role.getPermissions()) {
if(perm.getType() == PermissionConstants.PERMISSION_API) {
sb.append(perm.getCode()).append(",");
}
}
}
Map<String,Object> map = new HashMap<>();
map.put("apis",sb.toString());//可访问的api权限字符串
map.put("companyId",user.getCompanyId());
map.put("companyName",user.getCompanyName());
String token = jwtUtils.createJwt(user.getId(), user.getUsername(), map);
return new Result(ResultCode.SUCCESS,token);
}
}
<3>、拦截器中鉴权
1、在ihrm-common下添加拦截器 JwtInterceptor
package com.ihrm.common.interceptor;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.common.exception.CommonException;
import com.ihrm.common.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/**
* 自定义拦截器
* 继承HandlerInterceptorAdapter
*
* preHandle:进入到控制器方法之前执行的内容
* boolean:
* true:可以继续执行控制器方法
* false:拦截
* posthandler:执行控制器方法之后执行的内容
* afterCompletion:响应结束之前执行的内容
*
* 1.简化获取token数据的代码编写
* 统一的用户权限校验(是否登录)
* 2.判断用户是否具有当前访问接口的权限
*
*/
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {/**
* 简化获取token数据的代码编写(判断是否登录)
* 1.通过request获取请求token信息
* 2.从token中解析获取claims
* 3.将claims绑定到request域中
*/@Autowired
private JwtUtils jwtUtils;@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.通过request获取请求token信息
String authorization = request.getHeader("Authorization");
//判断请求头信息是否为空,或者是否已Bearer开头
if(!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")) {
//获取token数据
String token = authorization.replace("Bearer ","");
//解析token获取claims
Claims claims = jwtUtils.parseJwt(token);
if(claims != null) {
//通过claims获取到当前用户的可访问API权限字符串
String apis = (String) claims.get("apis"); //api-user-delete,api-user-update
//通过handler,指的是接下来要执行的方法,就是要执行的接口HandlerMethod h = (HandlerMethod) handler;
//获取接口上的reqeustmapping注解
RequestMapping annotation = h.getMethodAnnotation(RequestMapping.class);
//获取当前请求接口中的name属性
String name = annotation.name();
//判断当前用户是否具有响应的请求权限
if(apis.contains(name)) {
request.setAttribute("user_claims",claims);
return true;
}else {
throw new CommonException(ResultCode.UNAUTHORISE);
}
}
}
throw new CommonException(ResultCode.UNAUTHENTICATED);
}
}
2、修改UserController中的profile方法
/**
* 用户登录成功之后,获取用户信息
* 1.获取用户id
* 2.根据用户id查询用户
* 3.构建返回值对象
* 4.响应
*/
@RequestMapping(value="/profile",method = RequestMethod.POST)
public Result profile(HttpServletRequest request) throws Exception {
String userid = claims.getId();
//获取用户信息
User user = userService.findById(userid);
//根据不同的用户级别获取用户权限
ProfileResult result = null;
if("user".equals(user.getLevel())) {
result = new ProfileResult(user);
}else {
Map map = new HashMap();
if("coAdmin".equals(user.getLevel())) {
map.put("enVisible","1");
}
List<Permission> list = permissionService.findAll(map);
result = new ProfileResult(user,list);
}
return new Result(ResultCode.SUCCESS,result);
}
}
3、配置拦截器,创建com.ihrm.system.SystemConfig
package com.ihrm.system;
import com.ihrm.common.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;@Configuration
public class SystemConfig extends WebMvcConfigurationSupport {@Autowired
private JwtInterceptor jwtInterceptor;/**
* 添加拦截器的配置
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
//1.添加自定义拦截器
registry.addInterceptor(jwtInterceptor).
addPathPatterns("/**").//2.指定拦截器的url地址
excludePathPatterns("/sys/login","/frame/register/**");//3.指定不拦截的url地址
}
}
4、Shiro安全框架
<1>、Shiro概述
(1)、什么是Shiro
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。 使用Shiro的易于理解的API,可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
Apache Shiro 的首要目标是易于使用和理解。安全有时候是很复杂的,甚至是痛苦的,但它没有必要这样。框架应该尽可能掩盖复杂的地方,露出一个干净而直观的 API,来简化开发人员在使他们的应用程序安全上的努力。以 下是你可以用 Apache Shiro 所做的事情:
验证用户来核实他们的身份
对用户执行访问控制,如:判断用户是否被分配了一个确定的安全角色,判断用户是否被允许做某些事情
在任何环境下使用Session API,即使没有Web或EJB容器
在身份验证,访问控制期间或在会话的生命周期,对事件作出反应。
聚集一个或多个用户安全数据的数据源,并作为一个单一的复合用户“视图”。
启用单点登录(SSO)功能。
为没有关联到登录的用户启用"Remember Me"服务
(2)、与Spring Security的对比
Shiro:
Shiro较之 Spring Security,Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。
易于理解的 Java Security API;
简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
对角色的简单的签权(访问控制),支持细粒度的签权;
支持一级缓存,以提升应用程序的性能;
内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
异构客户端会话访问;
非常简单的加密 API;
不跟任何的框架或者容器捆绑,可以独立运行
Spring Security:
除了不能脱离Spring,shiro的功能它都有。而且Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。Spring Security的权限细粒度更高。
Spring Security:
除了不能脱离Spring,shiro的功能它都有。而且Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。Spring Security的权限细粒度更高。
(3)、Shiro的功能模块
Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助 我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这正是我们需要的,而且Shiro的API也是非常简单;其基本功能点如下图所示:

- Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情。
- Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话 中;会话可以是普通JavaSE环境的,也可以是如Web环境的。
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
- Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。
- Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。
- Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。
- “Run As”:一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
- “Remember Me”:记住我。
<2>、Shiro的内部结构
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心 脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会 话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实 现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的 哪些功能;
Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可 以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储; 所以我们一般在应用中都需要实现自己的Realm;
SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个 组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、E JB等环境; 所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;
SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可 以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的 Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到 缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
<3>、应用程序使用Shiro

也就是说对于我们而言,最简单的一个Shiro应用:
1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判 断。
从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。
<4>、Shiro入门

(1)、搭建基于ini的运行环
创建maven工程并导入相关依赖
<!--shiro核心包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
(2)、用户认证
认证:身份认证/登录,验证用户是不是拥有相应的身份,基于Shiro的认证,通过subject的login方法完成用户认证工作的
1、在resource目录下面创建shiro的ini配置文件构造模拟数据(shiro-auth.ini)
shiro-test-1.ini
[users]
#模拟从数据库查询的用户
#数据格式 用户名=密码
zhangsan=123456
lisi=654321
2、测试用户认证
//7.验证用户是否登录成功
System.out.println("用户是否登录成功="+subject.isAuthenticated());
//8.获取登录成功的数据
System.out.println(subject.getPrincipal());
package cn.itcast.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.junit.Test;public class ShiroTest01 {
/**
* 测试用户认证:
* 认证:用户登录
*
* 1.根据配置文件创建SecurityManagerFactory
* 2.通过工厂获取SecurityManager
* 3.将SecurityManager绑定到当前运行环境
* 4.从当前运行环境中构造subject
* 5.构造shiro登录的数据
* 6.主体登陆
*/@Test
public void testLogin() {
//1.根据配置文件创建SecurityManagerFactory
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-1.ini");
//2.通过工厂获取SecurityManager
SecurityManager securityManager = factory.getInstance();
//3.将SecurityManager绑定到当前运行环境
SecurityUtils.setSecurityManager(securityManager);
//4.从当前运行环境中构造subject
Subject subject = SecurityUtils.getSubject();
//5.构造shiro登录的数据
String username = "zhangsan";
String password = "1234561";
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
//6.主体登陆
subject.login(token);
//7.验证用户是否登录成功
System.out.println("用户是否登录成功="+subject.isAuthenticated());
//8.获取登录成功的数据
System.out.println(subject.getPrincipal());
}
}
(3)、用户授权
授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
1、在resource目录下创建shiro的ini配置文件构造模拟数据(shiro-prem.ini)
shiro-test-2.ini
[users]
#用户名=密码,角色名
zhangsan=123456,role1,role2
lisi=123456,role2
[roles]
#角色
#角色名=权限列表
role1=user:save,user:update
role2=user:find
2、完成用户授权
//授权:检验当前登录用户是否具有操作权限,是否具有某个角色
System.out.println(subject.hasRole("role1"));
System.out.println(subject.isPermitted("user:save"));
package cn.itcast.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Before;
import org.junit.Test;public class ShiroTest02 {
private SecurityManager securityManager;
@Before
public void init() {
//1.根据配置文件创建SecurityManagerFactory
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-2.ini");
//2.通过工厂获取SecurityManager
securityManager = factory.getInstance();
//3.将SecurityManager绑定到当前运行环境
SecurityUtils.setSecurityManager(securityManager);
}@Test
public void testLogin() {
Subject subject = SecurityUtils.getSubject();
String username = "lisi";
String password = "123456";
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
subject.login(token);//登录成功之后,完成授权
//授权:检验当前登录用户是否具有操作权限,是否具有某个角色
System.out.println(subject.hasRole("role1"));
System.out.println(subject.isPermitted("user:save"));
}
}
(4)、自定义Realm
Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源
1、自定义Realm
package cn.itcast.shiro;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;import java.util.ArrayList;
import java.util.List;/**
* 自定义realms对象
* 继承AuthorizingRealm
* 重写方法
* doGetAuthorizationInfo:授权
* 获取到用户的授权数据(用户的权限数据)
* doGetAuthenticationInfo:认证
* 根据用户名密码登录,将用户数据保存(安全数据)
*
*/
public class PermissionRealm extends AuthorizingRealm {/**
* 自定义realm名称
*/
public void setName(String name) {
super.setName("permissionRealm");
}//授权:授权的主要目的就是根据认证数据获取到用户的权限信息
/**
* principalCollection:包含了所有已认证的安全数据
* AuthorizationInfoInfo:授权数据
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权方法");
//1.获取安全数据 username,用户id
String username = (String)principalCollection.getPrimaryPrincipal();
//2.根据id或者名称查询用户
//3.查询用户的角色和权限信息
List<String> perms = new ArrayList<>();
perms.add("user:save");
perms.add("user:update");
List<String> roles = new ArrayList<>();
roles.add("role1");
roles.add("role2");
//4.构造返回
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//设置权限集合
info.addStringPermissions(perms);
//设置角色集合
info.addRoles(roles);
return info;
}//认证:认证的主要目的,比较用户名和密码是否与数据库中的一致
//将安全数据存入到shiro进行保管
//参数:authenticationToken登录构造的usernamepasswordtoken
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证方法");
//1.构造uptoken
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
//2.获取输入的用户名密码
String username = upToken.getUsername();
String password = new String(upToken.getPassword());
//3.根据用户名查询数据库,正式系统查询
//4.比较密码和数据库中的密码是否一致(密码可能需要加密)
if("123456".equals(password)) {
//5.如果成功,向shiro存入安全数据
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,password,getName());//1.安全数据,2.密码。3。当前realm域名称
return info;
}else{
//6.失败,抛出异常或返回null
throw new RuntimeException("用户名或密码错误");
}
}
}
2、配置shiro的ini配置文件(shiro-realm.ini)
shiro-test-3.ini
[main]
#声明realm
permReam=cn.itcast.shiro.PermissionRealm
#注册realm到securityManager中
securityManager.realms=$permReam
3、验证方法
package cn.itcast.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Before;
import org.junit.Test;public class ShiroTest03 {
private SecurityManager securityManager;
@Before
public void init() {
//1.根据配置文件创建SecurityManagerFactory
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-3.ini");
//2.通过工厂获取SecurityManager
securityManager = factory.getInstance();
//3.将SecurityManager绑定到当前运行环境
SecurityUtils.setSecurityManager(securityManager);
}@Test
public void testLogin() {
Subject subject = SecurityUtils.getSubject();
String username = "zhangsan";
String password = "123456";
UsernamePasswordToken token = new UsernamePasswordToken(username,password);//执行login-->realm域中的认证方法
subject.login(token);//授权:-->realm域中的授权方法
System.out.println(subject.hasRole("role1"));
System.out.println(subject.isPermitted("user:save"));
}
}
(5)、认证与授权的执行流程分析
[1]、认证流程

- 首先调用Subject.login(token)进行登录,然后自动委托给Security Manager,
- 调用之前必须通过 SecurityUtils. setSecurityManager()将Security Manager绑定到当前的运行环境中;
- SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自定义的实现;Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
[2]、授权流程
- 首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer;
- Authorizer是真正的授权者,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver 把字符串转换成相应的Permission实例;
- 在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;
- Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted/hasRole会返回true,否则返回false表示授权失败。
第六章:Shiro高级及SaaS-HRM的认证授权
学习目标:
-
理解基于shiro的登录方式
-
理解shiro的拦截器
-
掌握自定义realm
-
理解shiro的会话管理
-
实现基于saas-hrm中基于shiro的认证和授权
1Shiro在SpringBoot工程的应用
Apache Shiro是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。越来越多的企业使用Shiro作为项目的安全框架,保证项目的平稳运行。
在之前的讲解中只是单独的使用shiro,方便学员对shiro有一个直观且清晰的认知,我们今天就来看一下shiro在
springBoot工程中如何使用以及其他特性
1.1案例说明
使用springBoot构建应用程序,整合shiro框架完成用户认证与授权。

工程结构
导入资料中准备的基本工程代码,此工程中实现了基本用户角色权限的操作。我们只需要在此工程中添加Shiro相 关的操作代码即可
1.2整合Shiro
1.2.1spring和shiro的整合依赖
<!--shiro与spring整合 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
1.2.2修改登录方法
认证:身份认证/登录,验证用户是不是拥有相应的身份。基于shiro的认证,shiro需要采集到用户登录数据使用
subject的login方法进入realm完成认证工作。
@RequestMapping(value = "/login")
public String login(String username, String password) {
try {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken uptoken = new
UsernamePasswordToken(username, password);
subject.login(uptoken);
return "登录成功";
} catch (Exception e) {
return "用户名或密码错误";
}
}
1.2.3自定义realm
Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么 它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源
package cn.itcast.shiro.realm;
import cn.itcast.shiro.domain.Permission;
import cn.itcast.shiro.domain.Role;
import cn.itcast.shiro.domain.User;
import cn.itcast.shiro.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Set;
/**
* 自定义的realm人域
*/
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 给realm起个唯一标识
* @param name
*/
@Override
public void setName(String name) {
super.setName("customRealm");
}
/**
* 授权方法
* 操作的时候判断用户是否具有相应的权限
* 先认证--》安全数据
* 再授权--》根据安全数据获取用户具有的所有操作权限
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1.获取认证的用户数据
//2.构造认证数据
//3.添加角色信息info.addRole(role.getName)
//4.添加权限信息info.addStringPermission(permission.getCode)
User user = (User)principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<Role> roles = user.getRoles();
for (Role role : roles) {
info.addRole(role.getName());
Set<Permission> permissions = role.getPermissions();
for (Permission permission : permissions) {
info.addStringPermission(permission.getCode());
}
}
return info;
}
/**
* 认证方法
* 执行subject.login()时,用户名和密码会传到这里
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取subject.login()传过来的用户名和密码
//2.根据用户名查询数据库
//3.判断用户是否存在或者密码是否错误
//4.如果一致返回安全数据
//5.不一致返回null,返回null相当抛出异常
UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken;
String username = upToken.getUsername();
String password = new String(upToken.getPassword());
User user = userService.findByName(username);
if (user!=null && password.equals(password)){
//构造方法:安全数据,密码,realm域名
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
return info;
}
return null;
}
}
1.3Shiro的配置
SecurityManager 是 Shiro 架构的心脏,用于协调内部的多个组件完成全部认证授权的过程。例如通过调用realm 完成认证与登录。使用基于springboot的配置方式完成SecurityManager,Realm的装配
@Configuration
public class ShiroConfiguration {
//配置自定义的Realm @Bean
public CustomRealm getRealm() {
return new CustomRealm();
}
//配置安全管理器@Bean
public SecurityManager securityManager(CustomRealm realm) {
//使用默认的安全管理器
DefaultWebSecurityManager securityManager = new
DefaultWebSecurityManager(realm);
//将自定义的realm交给安全管理器统一调度管理securityManager.setRealm(realm); return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
//1.创建shiro过滤器工厂
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
//2.设置安全管理器filterFactory.setSecurityManager(securityManager);
//3. 通 用 配 置 ( 配 置 登 录 页 面 , 登 录 成 功 页 面 , 验 证 未 成 功 页 面 ) filterFactory.setLoginUrl("/autherror?code=1"); //设置登录页面filterFactory.setUnauthorizedUrl("/autherror?code=2"); //授权失败跳转页面
//4.配置过滤器集合
/**
*key :访问连接
*支持通配符的形式
*value:过滤器类型
*shiro常用过滤器
*anno :匿名访问(表明此链接所有人可以访问)
*authc :认证后访问(表明此链接需登录认证成功之后可以访问)
*/
Map<String, String> filterMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断filterMap.put("/user/home", "anon"); filterMap.put("/user/**", "authc");
//5.设置过滤器filterFactory.setFilterChainDefinitionMap(filterMap); return filterFactory;
}
//配置shiro注解支持@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
1.4shiro中的过滤器

意:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器
(例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url )
1.5授权
授权:即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情
shiro支持基于过滤器的授权方式也支持注解的授权方式
1.5.1基于配置的授权
在shiro中可以使用过滤器的方式配置目标地址的请求权限
//配置请求连接过滤器配置
//匿名访问(所有人员可以使用)
filterMap.put("/user/home", "anon");
//具有指定权限访问
filterMap.put("/user/find", "perms[user-find]");
//认证之后访问(登录之后可以访问) filterMap.put("/user/**", "authc");
//具有指定角色可以访问
filterMap.put("/user/**", "roles[系统管理员]");
基于配置的方式进行授权,一旦操作用户不具备操作权限,目标地址不会被执行。会跳转到指定的url连接地 址。所以需要在连接地址中更加友好的处理未授权的信息提示
1.5.2基于注解的授权
(1)RequiresPermissions
配置到方法上,表明执行此方法必须具有指定的权限
//查询
@RequiresPermissions(value = "user-find")
public String find() { return "查询用户成功";
}
(2)RequiresRoles
配置到方法上,表明执行此方法必须具有指定的角色
//查询
@RequiresRoles(value = "系统管理员")
public String find() { return "查询用户成功";
}
基于注解的配置方式进行授权,一旦操作用户不具备操作权限,目标方法不会被执行,而且会抛出
AuthorizationException 异常。所以需要做好统一异常处理完成未授权处理
2Shiro中的会话管理
在shiro里所有的用户的会话信息都会由Shiro来进行控制,shiro提供的会话可以用于JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。通过Shiro的会话管理器(SessionManager)进行统一的 会话管理
2.1什么是shiro的会话管理
SessionManager(会话管理器):管理所有Subject的session包括创建、维护、删除、失效、验证等工作。SessionManager是顶层组件,由SecurityManager管理
shiro提供了三个默认实现:
1.DefaultSessionManager:用于JavaSE环境
2.ServletContainerSessionManager:用于Web环境,直接使用servlet容器的会话。
3.DefaultWebSessionManager:用于web环境,自己维护会话(自己维护着会话,直接废弃了Servlet容器的会话管理)。
在web程序中,通过shiro的Subject.login()方法登录成功后,用户的认证信息实际上是保存在HttpSession中的通过如下代码验证。
//登录成功后,打印所有session内容@RequestMapping(value="/show")
public String show(HttpSession session) {
// 获取session中所有的键值
Enumeration<?> enumeration = session.getAttributeNames();
// 遍历enumeration中的
while (enumeration.hasMoreElements()) {
// 获取session键值
String name = enumeration.nextElement().toString();
// 根据键值取session中的值
Object value = session.getAttribute(name);
// 打印结果
System.out.println("<B>" + name + "</B>=" + value + "<br>/n");
}
return "查看session成功";
}
2.2应用场景分析
在分布式系统或者微服务架构下,都是通过统一的认证中心进行用户认证。如果使用默认会话管理,用户信息只会 保存到一台服务器上。那么其他服务就需要进行会话的同步。

2.3 Shiro结合redis的统一会话管理
2.3.1 步骤分析

2.3.2构建环境
(1)使用开源组件Shiro-Redis可以方便的构建shiro与redis的整合工程。
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.0.0</version>
</dependency>
(2)在springboot配置文件中添加redis配置
redis:
host: 127.0.0.1
port: 6379
2.3.3自定义shiro会话管理器
/**
* 自定义的sessionManager
*/
public class CustomSessionManager extends DefaultWebSessionManager {
/**
*头信息中具有sessionid
*请求头:Authorization: sessionid
*
*指定sessionId的获取方式
*/
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {//获取请求头Authorization中的数据
String id = WebUtils.toHttp(request).getHeader("Authorization");
if(StringUtils.isEmpty(id)) {
//如果没有携带,生成新的sessionId
return super.getSessionId(request,response);
}else{
// 返 回 sessionId; request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
"header");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}
2.3.4配置Shiro基于redis的会话管理
在Shiro配置类 配置
1.配置shiro的RedisManager,通过shiro-redis包提供的RedisManager统一对redis操作
@Value("${spring.redis.host}") private String host;@Value("${spring.redis.port}") private int port;
//配置shiro redisManager
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port);
return redisManager;
}
2.Shiro内部有自己的本地缓存机制,为了更加统一方便管理,全部替换redis实现
//配置Shiro的缓存管理器
//使用redis实现
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
3.配置SessionDao,使用shiro-redis实现的基于redis的sessionDao
/**
*RedisSessionDAO shiro sessionDao层的实现 通过redis
*使用的是shiro-redis开源插件
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
4.配置会话管理器,指定sessionDao的依赖关系
/**
* 3.会话管理器
*/
public DefaultWebSessionManager sessionManager() { CustomSessionManager sessionManager = new CustomSessionManager(); sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
5.统一交给SecurityManager管理
//配置安全管理器@Bean
public SecurityManager securityManager(CustomRealm realm) {
//使用默认的安全管理器
DefaultWebSecurityManager securityManager = new
DefaultWebSecurityManager(realm);
// 自 定 义 session 管 理 使 用 redis securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
//将自定义的realm交给安全管理器统一调度管理securityManager.setRealm(realm); return securityManager;
}
3SaaS-HRM中的认证授权
3.1需求分析
实现基于Shiro的SaaS平台的统一权限管理。我们的SaaS-HRM系统是基于微服务构建,所以在使用Shiro鉴权的时 候,就需要将认证信息保存到统一的redis服务器中完成。这样,每个微服务都可以通过指定cookie中的sessionid 获取公共的认证信息。
3.2搭建环境
3.2.1导入依赖
父工程导入Shiro的依赖
<!--shiro和spring整合-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!--shiro核心包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<!--shiro与redis整合-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.0.0</version>
</dependency>
3.2.2配置值对象
不需要存入redis太多的用户数据,和获取用户信息的返回对象一致即可,需要实现AuthCachePrincipali接口
@Setter @Getter
public class ProfileResult implements Serializable,AuthCachePrincipal {private String mobile;
private String username;private String company;
private String companyId;
private Map<String,Object> roles = new HashMap<>();
//省略
}
3.2.3配置未认证controller
为了在多个微服务中使用,配置公共的未认证未授权的Controller
@RestController @CrossOrigin
public class ErrorController {//公共错误跳转@RequestMapping(value="autherror") public Result autherror(int code) {
return code ==1?new Result(ResultCode.UNAUTHENTICATED):new Result(ResultCode.UNAUTHORISE);
}}
3.2.4自定义realm授权
ihrm-common模块下创建公共的认证与授权realm,需要注意的是,此realm只处理授权数据即可,认证方法需要 在登录模块中补全。
public class IhrmRealm extends AuthorizingRealm {@Override
public void setName(String name) { super.setName("ihrmRealm");
}//授权方法
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection
principalCollection) {
//1.获取安全数据ProfileResult result =
(ProfileResult)principalCollection.getPrimaryPrincipal();
//2.获取权限信息
Set<String> apisPerms = (Set<String>)result.getRoles().get("apis");
//3.构造权限数据,返回值
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setStringPermissions(apisPerms);
return info;
}/**
* 认证方法
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
3.3.5 自定义会话管理器
之前的程序使用jwt的方式进行用户认证,前端发送后端的是请求头中的token。为了适配之前的程序,在shiro中需要更改sessionId的获取方式。很好解决,在shiro的会话管理中,可以轻松的使用请求头中的内容作为sessionid
public class IhrmWebSessionManager extends DefaultWebSessionManager {private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public IhrmWebSessionManager(){ super();
}protected Serializable getSessionId(ServletRequest request, ServletResponse response){
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if(StringUtils.isEmpty(id)){
//如果没有携带id参数则按照父类的方式在cookie进行获取return super.getSessionId(request, response);
}else{
id = id.replace("Bearer ", "");
//如果请求头中有 authToken 则其值为sessionIdrequest.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_S ESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TR UE);
return id;
}
}
}
3.3用户认证
3.3.1配置用户登录
//用户名密码登录
@RequestMapping(value="/login",method = RequestMethod.POST) public Result login(@RequestBody Map<String,String> loginMap) {
String mobile = loginMap.get("mobile"); String password = loginMap.get("password");
try {
//1.构造登录令牌 UsernamePasswordToken
//加密密码
password = new Md5Hash(password,mobile,3).toString(); //1.密码,盐,加密次数
UsernamePasswordToken upToken = new UsernamePasswordToken(mobile,password);
//2.获取subject
Subject subject = SecurityUtils.getSubject();
//3.调用login方法,进入realm完成认证
subject.login(upToken);
//4.获取sessionId
String sessionId = (String)subject.getSession().getId();
//5.构造返回结果
return new Result(ResultCode.SUCCESS,sessionId);
}catch (Exception e) {
return new Result(ResultCode.MOBILEORPASSWORDERROR);
}
}
3.3.2shiro认证
配置用户登录认证的realm域,只需要继承公共的IhrmRealm补充其中的认证方法即可
public class UserIhrmRealm extends IhrmRealm {
@Override
public void setName(String name) { super.setName("customRealm");
}
@Autowired
private UserService userService;//认证方法
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken
authenticationToken) throws AuthenticationException {
//1.获取用户的手机号和密码
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken; String mobile = upToken.getUsername();
String password = new String( upToken.getPassword());
//2.根据手机号查询用户
User user = userService.findByMobile(mobile);
//3.判断用户是否存在,用户密码是否和输入密码一致
if(user != null && user.getPassword().equals(password)) {
//4.构造安全数据并返回(安全数据:用户基本数据,权限信息 profileResult) ProfileResult result = null; if("user".equals(user.getLevel())) {
result = new ProfileResult(user);
}else {
Map map = new HashMap(); if("coAdmin".equals(user.getLevel())) {
map.put("enVisible","1");
}
List<Permission> list = permissionService.findAll(map); result = new ProfileResult(user,list);
}
//构造方法:安全数据,密码,realm域名SimpleAuthenticationInfo info = new
SimpleAuthenticationInfo(result,user.getPassword(),this.getName()); return info;
}
//返回null,会抛出异常,标识用户名和密码不匹配
return null;
}
}
3.3.3获取session数据
baseController中使用shiro从redis中获取认证数据
//使用shiro获取@ModelAttribute
public void setResAnReq(HttpServletRequest request,HttpServletResponse response) { this.request = request;
this.response = response;//获取session中的安全数据
Subject subject = SecurityUtils.getSubject();
//1.subject获取所有的安全数据集合
PrincipalCollection principals = subject.getPrincipals();
if(principals != null && !principals.isEmpty()){
//2.获取安全数据
ProfileResult result = (ProfileResult)principals.getPrimaryPrincipal(); this.companyId = result.getCompanyId();
this.companyName = result.getCompany();
}
}
3.4用户授权
在需要使用的接口上配置@RequiresPermissions(“API-USER-DELETE”)
3.5配置
构造shiro的配置类
@Configuration
public class ShiroConfiguration {@Value("${spring.redis.host}") private String host;
@Value("${spring.redis.port}") private int port;
//配置自定义的Realm @Bean
public IhrmRealm getRealm() { return new UserIhrmRealm();
}//配置安全管理器
@Bean
public SecurityManager securityManager() {
//使用默认的安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自 定 义 session 管 理 使 用 redis securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
//将自定义的realm交给安全管理器统一调度管理securityManager.setRealm(getRealm()); return securityManager;
}//Filter工厂,设置对应的过滤条件和跳转条件@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
//1.创建shiro过滤器工厂
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
//2.设置安全管理器filterFactory.setSecurityManager(securityManager);
//3. 通 用 配 置 ( 配 置 登 录 页 面 , 登 录 成 功 页 面 , 验 证 未 成 功 页 面 ) filterFactory.setLoginUrl("/autherror?code=1"); //设置登录页面filterFactory.setUnauthorizedUrl("/autherror?code=2"); //授权失败跳转页面
//4.配置过滤器集合
/**
*key :访问连接
*支持通配符的形式
*value:过滤器类型
*shiro常用过滤器
*anno :匿名访问(表明此链接所有人可以访问)
*authc :认证后访问(表明此链接需登录认证成功之后可以访问)
*/
Map<String,String> filterMap = new LinkedHashMap<String,String>();
//配置请求连接过滤器配置
//匿名访问(所有人员可以使用)
filterMap.put("/frame/login", "anon");
filterMap.put("/autherror", "anon");
//认证之后访问(登录之后可以访问) filterMap.put("/**", "authc");//5.设置过滤器filterFactory.setFilterChainDefinitionMap(filterMap); return filterFactory;
}//配置shiro注解支持@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager); return advisor;
}//配置shiro redisManager
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port);
return redisManager;
}//cacheManager缓存 redis实现
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
*RedisSessionDAO shiro sessionDao层的实现 通过redis
*使用的是shiro-redis开源插件
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
*shiro session的管理
*/
public DefaultWebSessionManager sessionManager() { IhrmWebSessionManager sessionManager = new IhrmWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
}
第14章 API网关与社保模块
- 理解zuul网关的作用
- 完成zuul网关的搭建
- 实现社保模块的代码开发
1 zuul网关
在学习完前面的知识后,微服务架构已经初具雏形。但还有一些问题:不同的微服务一般会有不同的网络地址,客户端在访问这些微服务时必须记住几十甚至几百个地址,这对于客户端方来说太复杂也难以
维护。如下图:

如果让客户端直接与各个微服务通讯,可能会有很多问题:
1)客户端会请求多个不同的服务,需要维护不同的请求地址,增加开发难度
2)在某些场景下存在跨域请求的问题
3)加大身份认证的难度,每个微服务需要独立认证
因此,我们需要一个微服务网关,介于客户端与服务器之间的中间层,所有的外部请求都会先经过微服务网关。客户端只需要与网关交互,只知道一个网关地址即可,这样简化了开发还有以下优点: 1、易于监控 2、易于认证 3、减少了客户端与各个微服务之间的交互次数

API网关是一个服务器,是系统对外的唯一入口。API网关封装了系统内部架构,为每个客户端提供一个定制的API。API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务
1.1 什么是zuul网关

ZUUL是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用,Zuul组件的
核心是一系列的过滤器,这些过滤器可以完成以下功能:
1)动态路由:动态将请求路由到不同后端集群
2)压力测试:逐渐增加指向集群的流量,以了解性能
3)负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
4)静态响应处理:边缘位置进行响应,避免转发到内部集群
5)身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求。Spring Cloud对Zuul进行
了整合和增强。
Spring Cloud对Zuul进行了整合和增强
1.2 Zuul加入后的架构

不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
1.3.快速入门
1.3.1 工程搭建
创建工程ihrm_gate ,并导入zuul网关的响应依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
1.3.2 编写启动类
在网关工程中的com.ihrm.gate 下创建启动类GateApplication
@SpringBootApplication
@EnableZuulProxy //开启网关功能
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class);
}
}
1.3.3 编写配置文件
server:
port: 10010 #服务端口
spring:
application:
name: api-gateway #指定服务名
1.3.4 配置路由规则
#映射规则
zuul:
routes:
user-server: #这里是路由的id,只保证唯一性即可,不要求格式
path: /user-server/** #这里是映射的路径
url: http://127.0.0.1:8081 #这里是映射路径的实际url地址
1.4 路由配置
在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行 动态路由才对!
1.4.1 添加Eureka客户端发现功能
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class);
}
}
1.4.2 修改配置文件
eureka:
client:
registry-fetch-interval-seconds: 5 #获取服务列表的周期
service-url:
defaultZone: http://127.0.0.1:10087/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
(2)修改映射配置,通过服务名称获取
#映射规则
zuul:
routes:
user-server: #这里是路由的id,只保证唯一性即可,不要求格式
path: /user-server/** #这里是映射的路径
url: user-server
1.4.3 简化配置
刚才的配置中:
- zuul.routes.<route>.path=/xxx/** :来指定映射路径,<route>是自定义的路由名;
- zuul.routes.<route>.serviceId=/user-server:来指定服务名;
而在大多数情况下,我们的路由名称往往和服务名写成一样的,因此Zuul就提供一种简化的配置语法
所以我们可以这样写:
zuul:
routes:
user-server: /user-server/**
1.4.4 默认的路由规则
在使用Zuul的过程中,上面讲述的规则已经大大简化了配置项,但是当服务较多时,配置也是比较繁琐的。因此Zuul就只定了默认的路由规则:
默认情况下,一切服务的映射路径就是服务名本身,比如:服务名为user-server,默认的映射路径就是:/user-server/** ,并且在网关启动后,会自动从Eureka中拉取所有的服务信息
这意思就说,我们完全可以不用配置路由规则。
但是对于某些服务来说只能进行服务间调用,这样拉取全部服务信息反而会降低效率,而且有的服务不希望将路径暴露给网关。
#映射规则
zuul:
routes:
user-server: /user-server/**
ignored-services:
- user-consumer
可以使用这种配置来指定不需要网关映射的服务。
2 基于Zuul的统一鉴权
spring cloud Zuul包含了对请求的路由和过滤2个功能。路由功能负责将请求转发到具体的微服务上,而过滤器负责对请求的处理过程进行干预,是实现权限校验、服务聚合等功能的基础。
2.1 Zuul的过滤器
2.1.1 ZuulFilter
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter {
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
shouldFilter :返回一个Boolean 值,判断该过滤器是否需要执行。返回true执行,返回false
不执行。
run :过滤器的具体业务逻辑。
filterType :返回字符串,代表过滤器的类型。包含以下4种:
- pre :请求在被路由之前执行
- routing :在路由请求时调用
- post :在routing和errror过滤器之后调用
- error :处理请求时发生错误调用
filterOrder :通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
2.1.2 自定义过滤器
接下来我们来自定义一个过滤器,用于深入理解zuul过滤器的执行过程
package com.ihrm.gate.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义过滤器
*/
//@Component
public class LoginFilter extends ZuulFilter {
/**
* String类型的返回值
* 定义过滤器类型的
* pre : 在执行路由请求之前执行
* routing : 在路由请求是调用
* post : 在routing和error过滤器之后执行
* error : 处理请求出现异常的时候执行
*/
public String filterType() {
return "pre";
}
/**
* int类型的返回值
* 定义过滤器的优先级 : 数字越小,优先级越高
*/
public int filterOrder() {
return 2;
}
/**
* boolean类型的返回值
* 判断过滤器是否需要执行
*
*/
public boolean shouldFilter() {
//对某些请求过滤(不执行过滤器)
return true;
}
/**
* run方法 : 过滤器中负责的具体业务逻辑
* 使用过滤器进行jwt的鉴权
*/
public Object run() throws ZuulException {
System.out.println("执行了LoginFilter的run方法");
return null;
}
}
2.1.3 过滤器执行生命周期
这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。

正常流程:
- 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
异常流程:
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
所有内置过滤器列表:

2.1.4 使用场景
场景非常多:
- 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
- 异常处理:一般会在error类型和post类型过滤器中结合来处理。
- 服务调用时长统计:pre和post结合使用。
2.2 统一鉴权
2.2.1 基于JWT的统一鉴权
在某些应用中,往往使用JWT的形式进行无状态的用户鉴权。对于JWT的鉴权,只需要使用Zuul的自定义过滤器,在过滤器中判断是否携带JWT的token信息即可。
package com.ihrm.gate.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义过滤器
*/
@Component
public class LoginFilter extends ZuulFilter {
/**
* String类型的返回值
* 定义过滤器类型的
* pre : 在执行路由请求之前执行
* routing : 在路由请求是调用
* post : 在routing和error过滤器之后执行
* error : 处理请求出现异常的时候执行
*/
public String filterType() {
return "pre";
}
/**
* int类型的返回值
* 定义过滤器的优先级 : 数字越小,优先级越高
*/
public int filterOrder() {
return 2;
}
/**
* boolean类型的返回值
* 判断过滤器是否需要执行
*
*/
public boolean shouldFilter() {
//对某些请求过滤(不执行过滤器)
return true;
}
/**
* run方法 : 过滤器中负责的具体业务逻辑
* 使用过滤器进行jwt的鉴权
*/
public Object run() throws ZuulException {
//System.out.println("执行了LoginFilter的run方法");
//1.获取请求对象request
//1.1 获取Zuul提供的请求上下文的对象(工具类)
RequestContext rc = RequestContext.getCurrentContext();
//1.2 从上下文对象获取request对象
HttpServletRequest request = rc.getRequest();
//2.从request中获取Authorization的头信息
String token = request.getHeader("Authorization");
//3.判断
if(token == null || "".equals(token)) {
//没有传递token信息,需要登录,拦截
rc.setSendZuulResponse(false);
//返回错误的401状态码
rc.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}
2.2.2 基于Shiro的统一鉴权
由于我们的系统使用shiro结合自定义session的形式,相当于将用户数据存储到了分布式缓存redis中。
那么只需要再zuul中使用shiro即可完成统一用户权限校验
(1)引入shiro依赖
<dependency>
<groupId>com.ihrm</groupId>
<artifactId>ihrm_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
(2)配置shiro
package com.ihrm.gate;
import com.ihrm.common.shiro.realm.IhrmRealm;
import com.ihrm.common.shiro.session.CustomSessionManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfiguration {
//1.创建realm
@Bean
public IhrmRealm getRealm() {
return new IhrmRealm();
}
//2.创建安全管理器
@Bean
public SecurityManager getSecurityManager(IhrmRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
//将自定义的会话管理器注册到安全管理器中
securityManager.setSessionManager(sessionManager());
//将自定义的redis缓存管理器注册到安全管理器中
securityManager.setCacheManager(cacheManager());
return securityManager;
}
//3.配置shiro的过滤器工厂
/**
* 再web程序中,shiro进行权限控制全部是通过一组过滤器集合进行控制
*
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
//1.创建过滤器工厂
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
//2.设置安全管理器
filterFactory.setSecurityManager(securityManager);
//3.通用配置(跳转登录页面,未授权跳转的页面)
filterFactory.setLoginUrl("/autherror?code=1");//跳转url地址
filterFactory.setUnauthorizedUrl("/autherror?code=2");//未授权的url
//4.设置过滤器集合
Map<String,String> filterMap = new LinkedHashMap<>();
//anon -- 匿名访问
filterMap.put("/sys/login","anon");
filterMap.put("/autherror","anon");
//注册
//authc -- 认证之后访问(登录)
filterMap.put("/**","authc");
//perms -- 具有某中权限 (使用注解配置授权)
filterFactory.setFilterChainDefinitionMap(filterMap);
return filterFactory;
}
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
/**
* 1.redis的控制器,操作redis
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
return redisManager;
}
/**
* 2.sessionDao
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO sessionDAO = new RedisSessionDAO();
sessionDAO.setRedisManager(redisManager());
return sessionDAO;
}
/**
* 3.会话管理器
*/
public DefaultWebSessionManager sessionManager() {
CustomSessionManager sessionManager = new CustomSessionManager();
//sessionManager.setSessionIdCookieEnabled(false);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* 4.缓存管理器
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
//开启对shior注解的支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
由于再zuul网关中已经进行了统一的权限校验,那么其它微服务的权限校验就可以关闭了。
2.2.3 传递敏感header
经过测试可以发现在网关中明明已经具备了权限,可以在具体的微服务中还是会告知权限不足或者没有找到相关用户。这是因为在Zuul进行请求转发的时候,会把header清空,为了传递原始的header信息到最终的微服务,在配置加上:
zuul:
host:
connect-timeout-millis: 15000
socket-timeout-millis: 60000
routes:
#薪资模块的路由
ihrm-salarys: #工资
path: /salarys/** #配置请求URL的请求规则
serviceId: ihrm-salarys #指定Eureka注册中心中的服务id
strip-prefix: false
sentiviteHeaders:
customSensitiveHeaders: true
#路由id,随便写
ihrm-company:
path: /company/** #需要映射的路径地址
#url: http://127.0.0.1:9001 #映射路径对应的实际微服务的url路径
serviceId: ihrm-company
#zuul 自动的会删除请求的前缀 http://127.0.0.1:9001/company
stripPrefix: false #不删除请求前缀
#处理敏感头信息
sentiviteHeaders: #将指定路由的敏感头设置为空
customSensitiveHeaders: true #对指定路由开启自定义敏感头
3 社保管理
3.1 需求分析
完成社保模板相关代码开发:
企业员工参保设置
企业月度社保明细
企业社保归档数据
3.2 数据库表
(1)社保归档表
(2)社保-归档详情表
zuul.routes.xxx.sentiviteHeaders: #将指定路由的敏感头设置为空
zuul.routes.xxx.customSensitiveHeaders: true #对指定路由开启自定义敏感头
CREATE TABLE `ss_archive` (
`id` varchar(40) NOT NULL COMMENT 'id',
`company_id` varchar(40) NOT NULL COMMENT '企业id',
`years_month` varchar(255) NOT NULL COMMENT '年月',
`creation_time` date NOT NULL COMMENT '创建时间',
`enterprise_payment` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '企业缴纳',
`personal_payment` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '个人缴纳',
`total` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '合计',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='社保-归档表'
CREATE TABLE `ss_archive_detail` (
`id` varchar(40) NOT NULL COMMENT 'id',
`archive_id` varchar(40) NOT NULL COMMENT '归档id',
`user_id` varchar(40) DEFAULT NULL COMMENT '用户id',
`username` varchar(255) DEFAULT NULL COMMENT '用户名称',
`time_of_entry` varchar(255) DEFAULT NULL COMMENT '入职时间',
`mobile` varchar(255) DEFAULT NULL COMMENT '手机号',
`id_number` varchar(255) DEFAULT NULL COMMENT '身份证号',
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090
`the_highest_degree_of_education` varchar(255) DEFAULT NULL COMMENT '学历',
`opening_bank` varchar(255) DEFAULT NULL COMMENT '开户行',
`bank_card_number` varchar(255) DEFAULT NULL COMMENT '银行卡号',
`first_level_department` varchar(255) DEFAULT NULL COMMENT '一级部门',
`two_level_department` varchar(255) DEFAULT NULL COMMENT '二级部门',
`working_city` varchar(255) DEFAULT NULL COMMENT '工作城市',
`social_security_computer_number` varchar(255) DEFAULT NULL COMMENT '社保电脑
号',
`provident_fund_account` varchar(255) DEFAULT NULL COMMENT '公积金账号',
`leave_date` varchar(255) DEFAULT NULL COMMENT '离职时间',
`household_registration_type` varchar(255) DEFAULT NULL COMMENT '户籍类型',
`participating_in_the_city` varchar(255) DEFAULT NULL COMMENT '参保城市',
`social_security_month` varchar(255) DEFAULT NULL COMMENT '社保月份',
`social_security_base` decimal(10,2) DEFAULT NULL COMMENT '社保基数',
`social_security` decimal(10,2) DEFAULT NULL COMMENT '社保合计',
`social_security_enterprise` decimal(10,2) DEFAULT NULL COMMENT '社保企业',
`social_security_individual` decimal(10,2) DEFAULT NULL COMMENT '社保个人',
`provident_fund_city` varchar(255) DEFAULT NULL COMMENT '公积金城市',
`provident_fund_month` varchar(255) DEFAULT NULL COMMENT '公积金月份',
`provident_fund_base` decimal(10,2) DEFAULT NULL COMMENT '公积金基数',
`accumulation_fund_enterprise_base` decimal(10,2) DEFAULT NULL COMMENT '公积金
企业基数',
`proportion_of_provident_fund_enterprises` decimal(10,2) DEFAULT NULL COMMENT
'公积金企业比例',
`individual_base_of_provident_fund` decimal(10,2) DEFAULT NULL COMMENT '公积金
个人基数',
`personal_ratio_of_provident_fund` decimal(10,2) DEFAULT NULL COMMENT '公积金个
人比例',
`total_provident_fund` decimal(10,2) DEFAULT NULL COMMENT '公积金合计',
`provident_fund_enterprises` decimal(10,2) DEFAULT NULL COMMENT '公积金企业',
`provident_fund_individual` decimal(10,2) DEFAULT NULL COMMENT '公积金个人',
`pension_enterprise_base` decimal(10,2) DEFAULT NULL COMMENT '养老企业基数',
`proportion_of_pension_enterprises` decimal(10,2) DEFAULT NULL COMMENT '养老企
业比例',
`pension_enterprise` decimal(10,2) DEFAULT NULL COMMENT '养老企业',
`personal_pension_base` decimal(10,2) DEFAULT NULL COMMENT '养老个人基数',
`personal_pension_ratio` decimal(10,2) DEFAULT NULL COMMENT '养老个人比例',
`old_age_individual` decimal(10,2) DEFAULT NULL COMMENT '养老个人',
`unemployment_enterprise_base` decimal(10,2) DEFAULT NULL COMMENT '失业企业基
数',
`proportion_of_unemployed_enterprises` decimal(10,2) DEFAULT NULL COMMENT '失业
企业比例',
`unemployed_enterprise` decimal(10,2) DEFAULT NULL COMMENT '失业企业',
`the_number_of_unemployed_individuals` decimal(10,2) DEFAULT NULL COMMENT '失业
个人基数',
`percentage_of_unemployed_individuals` decimal(10,2) DEFAULT NULL COMMENT '失业
个人比例',
`unemployed_individual` decimal(10,2) DEFAULT NULL COMMENT '失业个人',
`medical_enterprise_base` decimal(10,2) DEFAULT NULL COMMENT '医疗企业基数',
`proportion_of_medical_enterprises` decimal(10,2) DEFAULT NULL COMMENT '医疗企
业比例',
`medical_enterprise` decimal(10,2) DEFAULT NULL COMMENT '医疗企业',
`medical_personal_base` decimal(10,2) DEFAULT NULL COMMENT '医疗个人基数',
`medical_personal_ratio` decimal(10,2) DEFAULT NULL COMMENT '医疗个人比例',
`medical_individual` decimal(10,2) DEFAULT NULL COMMENT '医疗个人',
`base_of_industrial_injury_enterprises` decimal(10,2) DEFAULT NULL COMMENT '工
伤企业基数',
(3)社保-城市与缴费项目关联表
(4)社保-企业设置信息
(5)社保-缴费项目
`proportion_of_industrial_injury_enterprises` decimal(10,2) DEFAULT NULL
COMMENT '工伤企业比例',
`industrial_injury_enterprise` decimal(10,2) DEFAULT NULL COMMENT '工伤企业',
`fertility_enterprise_base` decimal(10,2) DEFAULT NULL COMMENT '生育企业基数',
`proportion_of_fertility_enterprises` decimal(10,2) DEFAULT NULL COMMENT '生育
企业比例',
`childbearing_enterprise` decimal(10,2) DEFAULT NULL COMMENT '生育企业',
`base_of_serious_illness` decimal(10,2) DEFAULT NULL COMMENT '大病企业基数',
`proportion_of_seriously_ill_enterprises` decimal(10,2) DEFAULT NULL COMMENT
'大病企业比例',
`big_disease_enterprise` decimal(10,2) DEFAULT NULL COMMENT '大病企业',
`personal_base_of_serious_illness` decimal(10,2) DEFAULT NULL COMMENT '大病个人
基数',
`personal_proportion_of_serious_illness` decimal(10,2) DEFAULT NULL COMMENT
'大病个人比例',
`a_person_of_great_disease` decimal(10,2) DEFAULT NULL COMMENT '大病个人',
`provident_fund_notes` text COMMENT '公积金备注',
`social_security_notes` text COMMENT '社保备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='社保-归档详情'
CREATE TABLE `ss_city_payment_item` (
`id` varchar(40) NOT NULL,
`city_id` varchar(40) NOT NULL COMMENT '城市id',
`payment_item_id` varchar(40) NOT NULL COMMENT '缴费项目id',
`switch_company` tinyint(1) NOT NULL COMMENT '企业是否缴纳开关,0为禁用,1为启用',
`scale_company` decimal(6,2) DEFAULT NULL COMMENT '企业比例',
`switch_personal` tinyint(1) NOT NULL COMMENT '个人是否缴纳开关,0为禁用,1为启用',
`scale_personal` decimal(6,2) DEFAULT NULL COMMENT '个人比例',
PRIMARY KEY (`id`),
UNIQUE KEY `UK_CID_PIID` (`city_id`,`payment_item_id`) USING BTREE COMMENT '城
市id与缴费项目id组合唯一'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='社保-城市与缴费项目关联表'
CREATE TABLE `ss_company_settings` (
`company_id` varchar(40) NOT NULL COMMENT '企业id',
`is_settings` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0是未设置,1是已设置',
`data_month` varchar(40) NOT NULL COMMENT '当前显示报表月份',
PRIMARY KEY (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='社保-企业设置信息'
(6)社保-用户社保信息表
3.3 搭建环境
(1)使用代码生成工具,根据数据库表生成最基本的实体类,dao接口和service层代码。
略
(2)创建社保管理模块ihrm_social_securitys,并将自动生成的代码依次copy到响应的包下。
(3)修改zuul网关,添加社保相关的请求转发
CREATE TABLE `ss_payment_item` (
`id` varchar(40) NOT NULL COMMENT 'id',
`name` varchar(255) NOT NULL COMMENT '缴费项目名称',
`switch_company` tinyint(1) NOT NULL DEFAULT '0' COMMENT '企业是否缴纳开关,0为禁
用,1为启用',
`scale_company` decimal(6,2) NOT NULL DEFAULT '0.00' COMMENT '企业比例',
`switch_personal` tinyint(1) NOT NULL DEFAULT '0' COMMENT '个人是否缴纳开关,0为禁
用,1为启用',
`scale_personal` decimal(6,2) NOT NULL DEFAULT '0.00' COMMENT '个人比例',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='社保-缴费项目'
CREATE TABLE `ss_user_social_security` (
`user_id` varchar(40) NOT NULL COMMENT '用户id',
`enterprises_pay_social_security_this_month` tinyint(1) NOT NULL DEFAULT '0'
COMMENT '本月是否缴纳社保 0为不缴纳 1为缴纳',
`enterprises_pay_the_provident_fund_this_month` tinyint(1) NOT NULL DEFAULT
'0' COMMENT '本月是否缴纳公积金 0为不缴纳 1为缴纳',
`participating_in_the_city_id` varchar(40) NOT NULL COMMENT '参保城市id',
`social_security_type` tinyint(1) NOT NULL DEFAULT '2' COMMENT '参保类型 1为首
次开户 2为非首次开户',
`household_registration_type` tinyint(1) NOT NULL COMMENT '户籍类型 1为本市城镇 2
为本市农村 3为外埠城镇 4为外埠农村',
`social_security_base` int(8) NOT NULL COMMENT '社保基数',
`industrial_injury_ratio` decimal(6,2) DEFAULT NULL COMMENT '工伤比例',
`social_security_notes` varchar(300) DEFAULT NULL COMMENT '社保备注',
`provident_fund_city_id` varchar(40) NOT NULL COMMENT '公积金城市id',
`provident_fund_base` int(8) NOT NULL COMMENT '公积金基数',
`enterprise_proportion` decimal(6,2) NOT NULL COMMENT '公积金企业比例',
`personal_proportion` decimal(6,2) NOT NULL COMMENT '公积金个人比例',
`enterprise_provident_fund_payment` decimal(8,2) NOT NULL COMMENT '公积金企业缴
纳数额',
`personal_provident_fund_payment` decimal(8,2) NOT NULL COMMENT '公积金个人缴纳数
额',
`provident_fund_notes` varchar(300) DEFAULT NULL COMMENT '公积金备注',
`create_time` datetime NOT NULL COMMENT '创建时间',
`last_modify_time` datetime NOT NULL COMMENT '最后修改时间',
`social_security_switch_update_time` datetime NOT NULL COMMENT '社保是否缴纳变更
时间',
`provident_fund_switch_update_time` datetime NOT NULL COMMENT '公积金是否缴纳变更
时间',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='社保-用户社保信息表'
3.4 社保列表
3.4.1 企业社保设置
企业第一次进入社保页面,会要求输入制作社保记录的日期
3.4.2 查询所有参保人员数据列表
ihrm-social-securitys: #企业
path: /social_securitys/** #配置请求URL的请求规则
serviceId: ihrm-social-securitys #指定Eureka注册中心中的服务id
strip-prefix: false
sentiviteHeaders:
customSensitiveHeaders: true
/**
* 获取企业是否设置社保
* @return
*/
@RequestMapping(value = "/settings", method = RequestMethod.GET)
public Result getSettings() throws Exception {
CompanySettings companySettings =
companySettingsService.findById(companyId);
return new Result(ResultCode.SUCCESS, companySettings);
}
/**
* 保存企业社保设置
*/
@RequestMapping(value = "/settings", method = RequestMethod.POST)
public Result saveSettings(@RequestBody CompanySettings companySettings){
companySettings.setCompanyId(companyId);
companySettingsService.save(companySettings);
return new Result(ResultCode.SUCCESS);
}
3.5 社保设置
3.5.1 获取不同城市的社保缴费项目
3.5.2 展示员工社保数据
3.5.3 设置员工参保数据
/**
* 获取社保列表
*/
@RequestMapping(value = "/list", method = RequestMethod.POST)
public Result list(@RequestBody SearchListVo
searchListVo,@RequestParam(defaultValue = "1") int
page,@RequestParam(defaultValue = "1") int pageSize) throws Exception {
Page<UserSocialSecurityItem> itemPage = userSocialService.findAll(page,
pageSize, companyId, searchListVo.getDepartmentChecks(),
searchListVo.getSocialSecurityChecks(), searchListVo.getProvidentFundChecks());
PageResult pageResult = new PageResult(itemPage.getTotalElements(),
itemPage.getContent());
return new Result(ResultCode.SUCCESS, pageResult);
}
/**
* 根据城市id获取社保缴费项目
*/
@RequestMapping(value = "/payment_item/{cityId}", method = RequestMethod.GET)
public Result findPaymentItemByCityId(@PathVariable(value = "cityId") String
cityId) {
List<CityPaymentItem> cityPaymentItemList =
paymentItemService.findAllByCityId(cityId);
return new Result(ResultCode.SUCCESS, cityPaymentItemList);
}
/**
* 获取社保信息
*/
@RequestMapping(value = "/{userId}", method = RequestMethod.GET)
public Result findById(@PathVariable(value = "userId") String userId) throws
Exception {
//获取用户数据
User user = (User) systemFeignClient.findById(userId).getData();
//查询用户的社保数据
UserSocialSecurity userSocialSecurity =
userSocialService.findByUserId(userId);
Map map = new HashMap<>();
map.put("user",user);
map.put("userSocialSecurity",userSocialSecurity);
return new Result(ResultCode.SUCCESS, map);
}
3.5.4 批量导入员工参保数据
3.6 月报表
3.6.1 展示月报表数据
3.6.2 导出月报表
基于vue前端的Excel数据导出
/**
* 保存用户社保信息
*/
@ApiOperation(value="保存用户社保信息",httpMethod = "PUT")
@RequestMapping(value = "/{userId}", method = RequestMethod.PUT)
public Result save(@RequestBody UserSocialSecurity userSocialSecurity) {
userSocialService.save(userSocialSecurity);
return new Result(ResultCode.SUCCESS);
}
@RequestMapping(value = "/import", method = RequestMethod.POST)
public Result importSocialSecurity(@RequestParam(name = "file") MultipartFile
file) throws Exception {
List<UserSocialSecurity> list = new
ExcelImportUtil(UserSocialSecurity.class).readExcel(file.getInputStream(), 1,
0);
for (UserSocialSecurity item : list) {
UserSocialSecurity us =
userSocialService.findByUserId(item.getUserId());
if (us == null) {
userSocialService.save(item);
}
}
return new Result(ResultCode.SUCCESS);
}
@RequestMapping(value = "/historys/{yearMonth}", method = RequestMethod.GET)
public Result histories(@ApiParam(value = "年月", required = true)
@PathVariable(value = "yearMonth") String yearMonth, @RequestParam(value =
"opType") Integer opType) {
List<ArchiveDetail> reportVoList = new ArrayList<>();
if (opType == 1) {
reportVoList.addAll(archiveService.getReports(yearMonth,companyId));
} else {
Archive archive = archiveService.findArchive(companyId, yearMonth);
if (archive != null) {
reportVoList =
archiveService.findAllDetailByArchiveId(archive.getId());
}
}
return new Result(ResultCode.SUCCESS, reportVoList);
}
3.7 历史归档
归档:同义词为存档,指将处理完并且具有保存价值的事情或文件经系统整理后交档案室(馆)保存备
案(备查)的过程。
3.7.1 归档月报表数据
handleExport(index) {
let xlsxParam = { raw: true }
let getName = "社保报表"
let xxxs = XLSX.utils.table_to_book(
document.querySelector("#item"),
xlsxParam
);
getBlob(getName, xxxs, XLSX.write, FileSaver.saveAs);
this.$message.success("导出报表成功!");
this.$forceUpdate()
},
@RequestMapping(value = "/historys/{yearMonth}/archive", method =
RequestMethod.POST)
public Result archive(@PathVariable(value = "yearMonth") String yearMonth)
throws Exception {
//构造归档详情数据列表
List<ArchiveDetail> reportVoList =
archiveService.getReports(yearMonth,companyId);
//企业费用
BigDecimal enterprisePayment = new BigDecimal(0);
//个人费用
BigDecimal personalPayment = new BigDecimal(0);
//循环计算企业与个人部分费用总和
for (ArchiveDetail vo : reportVoList) {
enterprisePayment =
enterprisePayment.add(vo.getProvidentFundEnterprises().add(vo.getSocialSecurityE
nterprise()));
personalPayment =
personalPayment.add(vo.getSocialSecurityIndividual().add(vo.getProvidentFundIndi
vidual()));
}
Archive archive = archiveService.findArchive(companyId, yearMonth);
if (archive == null) {
archive = new Archive(companyId,yearMonth);
}
archive.setEnterprisePayment(enterprisePayment);
archive.setPersonalPayment(personalPayment);
archive.setTotal(enterprisePayment.add(personalPayment));
archiveService.save(archive);
archiveService.batchSaveDetail(archive.getId(), reportVoList);
return new Result(ResultCode.SUCCESS);
}
3.7.2 历史归档列表
3.7.3 历史归档详情
@RequestMapping(value = "/historys/{year}/list", method = RequestMethod.GET)
public Result historiesList(@PathVariable(value = "year") String year) throws
Exception {
List<Archive> archiveVoList = new ArrayList<>();
List<String> yearMonths = DateUtil.getYearMonths();
for (String yearMonth : yearMonths) {
Archive archive = archiveService.findArchive(companyId, yearMonth);
if (archive == null) {
archive = new Archive("0", companyId, yearMonth, null, new
BigDecimal(0), new BigDecimal(0), new BigDecimal(0));
}
archiveVoList.add(archive);
}
return new Result(ResultCode.SUCCESS, archiveVoList);
}
@RequestMapping(value = "/historys/{yearMonth}", method = RequestMethod.GET)
public Result histories(@PathVariable(value = "yearMonth") String
yearMonth,@RequestParam(value = "opType") Integer opType) {
List<ArchiveDetail> reportVoList = new ArrayList<>();
if (opType == 1) {
reportVoList.addAll(archiveService.getReports(yearMonth,companyId));
} else {
Archive archive = archiveService.findArchive(companyId, yearMonth);
if (archive != null) {
reportVoList =
archiveService.findAllDetailByArchiveId(archive.getId());
}
}
return new Result(ResultCode.SUCCESS, reportVoList);
}




1863

被折叠的 条评论
为什么被折叠?



