Authority management and jwt authentication

Authority management and jwt authentication

learning target:

Understand the needs of authority management and design ideas to achieve role assignment and authority assignment

Understand common authentication mechanisms

Can use JWT to complete microservice Token issuance and verification

authority management

demand analysis

Complete the basic operations of permissions (menus, buttons (permission points), API interface)

There is a one-to-one relationship between permissions and menus, menus and buttons, and menus and API interfaces. For the convenience of operation, in the table design of the SAAS-HRM system, the one-to-one relationship maintenance is realized based on the shared primary key, and the database is constrained. All relationship maintenance needs to be implemented by the programmer in the code.

backend implementation

Entity class

Create permissions, menus, buttons (permission points), and entity classes of API objects in system microservices

  • Permission entity class 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;

    //是否查询全部权限 0 :查询全部,1 :只查询企业所属权限
    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;
    }


}

Sao Dai understands: pid refers to the parent authority, that is, there may be a set of buttons or APIs under my menu authority, then these are sub-authorities, and their parent authority is the id of this menu. enVisible indicates whether to query all permissions (0: query all, 1: only query the permissions of the enterprise), that is, 0 means more advanced permissions, which cannot even be accessed by the enterprise, and only super administrators can see the permissions

  • Permission menu (permission point) entity class PermissionPoint
@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; //排序号
}
  • Permission menu (permission point) entity class PermissionPoint
@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;
}
  • Permission API entity class PermissionApi
@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;
    private String apiLevel;//权限等级,1为通用接口权限,2为需校验接口权限
}

persistence layer

  • permission persistence class
/**
  * 权限数据访问接口
  */
public interface PermissionDao extends JpaRepository<Permission, String>, 
JpaSpecificationExecutor<Permission> {
    List<Permission> findByTypeAndPid(int type,String pid);
}
  • Permission menu persistence class
public interface PermissionMenuDao extends JpaRepository<PermissionMenu, String>, 
JpaSpecificationExecutor<PermissionMenu> {
}
  • Permission button (dot) persistent class
public interface PermissionPointDao extends JpaRepository<PermissionPoint, String>, JpaSpecificationExecutor<PermissionPoint> {
}
  • Permission API Persistence Class
public interface PermissionApiDao extends JpaRepository<PermissionApi, String>, JpaSpecificationExecutor<PermissionApi> {
}

Business logic

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.domain.Page;
import org.springframework.data.domain.PageRequest;
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>() {
            /**
             * 动态拼接查询条件
             * @return
             */
            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);
        }
    }
}

Sao Dai's understanding: It should be noted that the ids of the three sub-permissions of menu, button and api must be consistent with the parent permission id, that is, the ids of the three classes of PermissionApi, PermissionMenu, and PermissionPoint should be consistent with the Permission class!

Translate the following Java code into SQL for better understanding

if(!StringUtils.isEmpty(map.get("type"))) {
    String ty = (String) map.get("type");
    CriteriaBuilder.In<Object> in = criteriaBuilder.in(root.get("type"));
    //查询全部权限列表type:0:菜单 + 按钮(权限点) 1:菜单2:按钮(权限点)3:API接口
    if("0".equals(ty)) {
        in.value(1).value(2);
    }else{
        in.value(Integer.parseInt(ty));
    }
    list.add(in);
}

This Java code is a conditional statement for constructing JPA dynamic query. Its function is to judge whether a sub-condition needs to be added according to the "type" key value in the map.

The code logic is as follows:

  • Check if the "type" key exists in the map and its value is not empty;
  • If the condition is met, get the value ty of "type", and use CriteriaBuilder to construct an In condition object in;
  • If ty is "0", add the value of in condition to 1 and 2; otherwise, convert ty to int type and add to in condition;
  • Finally, add this in condition to the condition set list.

The following is the SQL statement corresponding to the Java code:

SELECT*FROM 表名 WHERE type IN (1, 2) -- 当map.get("type")中的值为"0"时
SELECT*FROM 表名 WHERE type = ?    -- 当map.get("type")中的值为除"0"以外的值时

This Java code will automatically complete the parameter binding in the generated SQL statement, and the placeholder? in the specific SQL statement will be replaced by the specific value by the framework.

controller

package com.ihrm.system.controller;

import com.ihrm.common.entity.PageResult;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.domain.system.Permission;
import com.ihrm.domain.system.User;
import com.ihrm.system.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
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);
    }
}

Sao Dai understands: Here I made a mistake at the beginning. I thought that the following front-end Permission entity corresponds to the back-end Permission object, but it turned out not! As a result, I have been unable to understand why the backend needs to be encapsulated into a map to accept parameters instead of directly receiving the Permission object. The Permission entity of the frontend is actually a combination of Permission, PermissionApi, PermissionMenu, and PermissionPoint at the backend.

Therefore, the front-end Permission entity can be received by map, that is, through such a front-end Permission entity to realize the management of menus, components, and APIs, and judge what to operate according to the type passed from the front end, which is why there is only PermissionController This controller can add, delete, modify and check menus, components, and APIs

Front-end implementation

Introduce permission management module

Import the data module-permissions into the /src folder of the project, and complete the module registration in /src/main.js

import permissions from '@/module-permissions/' // 权限管理
Vue.use(permissions, store)

configuration API

Create permissions.js in the /src/api/base/ directory

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)}

Implement the permissions page

<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>
                    </template>
                </el-table-column>
                <el-table-column fixed prop="code" label="权限标识" width="200"></el-table-column>
                <el-table-column fixed prop="description" label="描述" width="200"></el-table-column>        
                <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 v-model="formData.name" autocomplete="off" style="width:90%"></el-input>
            </el-form-item>
            <el-form-item label="权限标识">
              <el-input v-model="formData.code" autocomplete="off" style="width:90%"></el-input>
            </el-form-item>
            <el-form-item label="权限描述">
              <el-input v-model="formData.description" autocomplete="off" style="width:90%"></el-input>
            </el-form-item>
            <el-form-item label="企业可见">
              <el-switch
                v-model="formData.enVisible"
                active-value="1"
                inactive-value="0"
                active-text="可见"
                inactive-text="不可见">
              </el-switch>
            </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>
                <el-table-column fixed prop="code" label="权限标识" width="200"></el-table-column>
                <el-table-column fixed prop="description" label="描述" width="200"></el-table-column>        
                <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>

<script>
import {saveOrUpdate,list,detail,remove} from "@/api/base/permissions"
export default {
  name: 'permissions-table-index',
  data() {
    return {
      MenuList: 'menuList',
      type:0,
      pid:"",
      dialogFormVisible:false,
      apiDialogVisible:false,
      formData:{},
      dataList:[],
      apiList:[],
      pointEnable:{}
    }
  },
  methods: {
    setPid(type,pid){
      this.pid = pid;
      this.type = type
    },
    handleCreate(id) {
      if(id && id !=undefined) {
        detail({id}).then(res => {
          this.formData = res.data.data
          this.dialogFormVisible=true
        })
      }else{
        this.formData = {}
        this.dialogFormVisible=true
      }
    },
    saveOrUpdate() {
      this.formData.type = this.type
      this.formData.pid = this.pid
      saveOrUpdate(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;
        }
        if(this.type ==3){
          this.handlerApiList(this.pid);
        }else{
          this.getList();
          this.pointEnable = {}
        }
      })
    },
    handleDelete(id) {
      remove({id}).then(res=> {
        this.$message({message:res.data.message,type:res.data.success?"success":"error"});
      })
    },
    getList() {
      list({type:1,pid:0}).then(res=> {
          this.dataList = res.data.data
      })
    },
    show(index,id) {
        if(!this.pointEnable[id] == null || this.pointEnable[id]==undefined){
            list({type:2,pid:id}).then(res=> {
                if(res.data.data.length <=0) {
                  this.$message.error("无子权限")
                }else{
                  for(var i = 0 ; i <res.data.data.length;i++) {
                      this.dataList.splice(index+1,0,res.data.data[i]);
                  }
                  this.pointEnable[id] = res.data.data.length;
                }
            })
        }else{
            this.dataList.splice(index+1,this.pointEnable[id])
            this.pointEnable[id] = null;
        }
    },
    handlerApiList(id) {
      this.pid = id;
      list({type:3,pid:id}).then(res=> {
          this.apiList = res.data.data
          this.apiDialogVisible = true;
      })
    }
  },
  created () {
    this.getList();
  }
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.alert {
  margin: 10px 0px;
}
.pagination {
  margin-top: 10px;
  // text-align: right;
}
</style>

<style>
.el-table th {
  background-color: #fafafa;
}
.el-table th.is-leaf {
  border-bottom: 2px solid #e8e8e8;
}
.el-table__row i{ font-style:normal}
</style>

Assigning Roles

demand analysis

Since the RBAC model is used to manage permissions uniformly, each user of the SAAS-HRM platform should have role information. Then complete the identification of permissions through roles. As we all know, a user can have many roles, and a role can be assigned to different users. So there is a many-to-many relationship between users and roles.

Server code implementation

  • Transform the user entity class, add the id collection attribute of the role, and indicate that a user has multiple role ids

Add a many-to-many relationship with roles in the com.ihrm.system.domain.User user entity class and configure JPA

@ManyToMany
@JsonIgnore
@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>();//用户与角色   多对多

Configure the many-to-many relationship between roles and users in the com.ihrm.system.domain.Role role entity class and perform JPA configuration

@JsonIgnore
@ManyToMany(mappedBy="roles")
private Set<User> users = new HashSet<User>(0);//角色与用户   多对多

Sao Dai understands: By configuring this way in the entity class, multiple table associations can be automatically modified, that is, you only need to modify the entity class, and it will automatically maintain and modify the associated tables and intermediate tables in the database

  • In com.ihrm.system.controller.UserController, add the controller method implementation of assigning roles
    /**
     * 分配角色
     */
    @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);
   }
  • The business logic layer adds business methods for assigning roles
    /**
     * 分配角色
     */
    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);
   }

Front-end code implementation

  • \src\module-employees add components that assign roles
<template>
  <div class="add-form">
    <el-dialog title="分配角色" :visible.sync="roleFormVisible" style="height:500px">
      <el-form :model="formBase"  label-position="left" label-width="120px" style='margin-left:120px;'>
          <el-checkbox-group 
            v-model="allRoles">
            <el-checkbox v-for="(item,index) in roles" :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,detail} from "@/api/base/users"
export default {
    data () {
        return {
            roleFormVisible:false,
            formBase:{},
            allRoles:[],
            data:[],
            roles:[],
            id:null
        }
    },
    methods: {
        toAssignPrem(id) {
            detail({id:id}).then(res1 => {
                this.allRoles = res1.data.data.roleIds;
                findAll().then(res => {
                    this.id = id;
                    this.roles = res.data.data
                    this.roleFormVisible=true
                })
            })
        },
        createData() {
            assignRoles({id:this.id,roleIds:this.allRoles}).then(res => {
                console.log(this.allRoles)
                this.$message({message:res.data.message,type:res.data.success?"success":"error"});
                this.roleFormVisible=false
            })
        }
    }
}
</script>
  • \src\module-employees\pages\employees-list.vue import component
<!--分配角色组件 -->
<component v-bind:is="addRole" ref="addRole"></component>

assign permissions

demand analysis

Complete the assignment of role permissions.

Server code implementation

  • Add a many-to-many relationship with permissions in the role entity class and perform JPA configuration
@JsonIgnore //忽略json转化
@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);//角色与模块 多对多
  • Controller class ( com.ihrm.system.controller.RoleController ) to add permission assignment
    /**
     * 分配权限
     */
    @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);
   }
  • Add the method of assigning permissions in the service layer
   /**
     * 分配权限
     */
    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);
   }

Sao Dai understands: the above logic of assigning permissions is basically the same as the logic of assigning roles, the difference lies in the following two lines of code, if this permission is a button, then this button must be bound to an api permission, so if the front end Assign button permissions, then api permissions should also be given together. At first, I thought that button permissions and api permissions were one-to-one, so I couldn’t understand why List was used to receive them. Later, I found that sometimes a button would send multiple requests. , for example the "Assign Role" button will send two requests

List<Permission> apiList =  permissionDao.findByTypeAndPid(PermissionConstants.PERMISSION_API, permission.getId());
perms.addAll(apiList);//自定赋予API权限
  • data layer
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 org.springframework.data.jpa.repository.Query;
import java.util.List;

/**
 * 权限
 */
public interface PermissionDao extends JpaRepository<Permission, String>, JpaSpecificationExecutor<Permission> {
    List<Permission> findByTypeAndPid(int type,String pid);
}

Sao Dai’s understanding: Here the findByTypeAndPid method is directly written in PermissionDao, but no specific sql is written. I wondered how it was implemented at the time. This is actually implemented according to the naming of this method. FindByTypeAndPid will be based on this method. The name is parsed to get the Type and Pid fields, and then spliced ​​into a sql statement with and, the effect is to query according to these two values

Front-end code implementation

  • Bind permission button in \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">分配权限</elbutton>
      <el-button @click="handleUpdate(scope.row)" type="text" size="small">修改</elbutton>
      <el-button @click="handleDelete(scope.row)" type="text" size="small">删除</elbutton>
   </template>
</el-table-column>
  • Add the API method for assigning permissions in \\src\api\base\role.js

export const assignPrem = data => createAPI(`/sys/role/assignPrem`, 'put', data)

  • \src\module-settings\components\role-list.vue uses Element-UI to construct a permission tree
 <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>
  • Finish adding permissions

Common Authentication Mechanisms

HTTP Basic Auth

The simple explanation of HTTP Basic Auth is to provide the user's username and password every time the API is requested. The risk of exposing username and password to third-party clients is less and less used in production environments. Therefore, when developing an open RESTful API, try to avoid using HTTP Basic Auth

Cookie Auth

The Cookie authentication mechanism is to create a Session object on the server side for a request authentication, and create a Cookie object on the client's browser side at the same time; the Cookie object is brought up by the client to match the session object on the server side to achieve state management. By default, when we close the browser, the cookie will be deleted. But you can make the cookie valid for a certain period of time by modifying the expire time of the cookie

OAuth

OAuth (Open Authorization) is an open authorization standard that allows users to allow third-party applications to access private resources (such as photos, videos, contact lists) stored by the user on a web service without providing usernames and passwords. to third-party applications. OAuth allows users to provide a token, rather than a username and password, to access their data with a particular service provider. Each token authorizes a specific third-party system (eg, a video editing website) to access a specific resource (eg, only videos in a certain album) within a specific period of time (eg, within the next 2 hours). In this way, OAuth allows users to authorize third-party websites to access some specific information they store in another service provider, but not all content

This OAuth-based authentication mechanism is suitable for individual consumer Internet products, such as social apps, but not suitable for enterprise applications with their own authentication authority management.

Token Auth

Using Token-based authentication methods, there is no need to store user login records on the server side. The approximate process is as follows:

      • Client requests login with username and password
      • The server receives the request to verify the username and password
      • After the verification is successful, the server will issue a Token, and then send the Token to the client
      • After receiving the Token, the client can store it, such as putting it in a cookie
      • Every time the client requests resources from the server, it needs to bring the Token issued by the server
      • The server receives the request, and then verifies the Token carried in the client request. If the verification is successful, it returns the requested data to the client.

Advantages of Token Auth

  • Support cross-domain access: Cookies do not allow cross-domain access, which does not exist for the Token mechanism, provided that the transmitted user authentication information is transmitted through the HTTP header.
  • Stateless (also known as: server-side extensible row): Token mechanism does not need to store session information on the server side, because Token itself contains information of all logged-in users, and only needs to store state information in the client's cookie or local media.
  • More suitable for CDN: You can request all the data of your server (such as: javascript, HTML, pictures, etc.) through the content distribution network, and your server only needs to provide API.
  • Decoupling: No need to be tied to a specific authentication scheme. Token can be generated anywhere, as long as you can make a Token generation call when your API is called.
  • More suitable for mobile applications: when your client is a native platform (iOS, Android, Windows 8, etc.), cookies are not supported
  • (You need to process it through the Cookie container), then it will be much simpler to use the Token authentication mechanism.
  • CSRF: Since you no longer rely on cookies, you don't need to consider CSRF (cross-site request forgery) prevention.
  • Performance: A network round-trip time (querying session information through the database) is much more time-consuming than doing a Token verification and parsing for HMACSHA256 calculation.
  • No need to do special processing for the login page: If you use Protractor for functional testing, you no longer need to do special processing for the login page. Based on standardization: your API can use the standardized JSON Web Token ( JWT ). This standard already exists Multiple backend libraries (.NET, Ruby, Java,Python, PHP) and support from multiple companies (eg: Firebase, Google, Microsoft).

Token issuance and verification in HRM

What is JWT

JSON Web Token (JWT) is a very lightweight specification. This specification allows us to use JWT to pass secure and reliable information between the user and the server. JWT creation and verification is implemented by JJWT in the Java world.

Quick Start with JJWT

token creation

  • Create a maven project and introduce dependencies
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.6.0</version>
</dependency>
  • Create class CreateJwtTest for generating token
public class CreateJwtTest {
    public static void main(String[] args) {
        JwtBuilder builder= Jwts.builder().setId("888")
                .setSubject("小白")
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256,"itcast");
        System.out.println( builder.compact() );
    }
}
  • The test runs and the output is as follows:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTM0NTh9.gq0J- cOM_qCNqU_s-d_IrRytaNenesPmqAIhQpYXHZk

token analysis

We have just created the token. In the web application, this operation is performed by the server and then sent to the client. The client needs to carry this token when it sends a request to the server next time (this is like holding a ticket) , then the server should parse out the information in the token (such as user id) after receiving the token, and query the database according to the information to return the corresponding results.

Create ParseJwtTest

public class ParseJwtTest {
    public static void main(String[] args) {
        String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTM0NTh9.gq0J-cOM_qCNqU_s-d_IrRytaNenesPmqAIhQpYXHZk";
        Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(token).getBody();
        System.out.println("id:"+claims.getId());
        System.out.println("subject:"+claims.getSubject());
        System.out.println("IssuedAt:"+claims.getIssuedAt());
    }
}

Try to tamper with the token or the signature key, and you will find that an error will be reported at runtime, so parsing the token means verifying the token

Custom claims

Our previous example only stored the id and subject information, if you want to store more information (such as roles) you can define custom claims

  • Create CreateJwtTest3 and store the specified content
public class CreateJwtTest3 {
    public static void main(String[] args) {
        //为了方便测试,我们将过期时间设置为1分钟
        long now = System.currentTimeMillis();//当前时间
        long exp = now + 1000*60;//过期时间为1分钟
        JwtBuilder builder= Jwts.builder().setId("888")
                .setSubject("小白")
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256,"itcast")
                .setExpiration(new Date(exp))
                .claim("roles","admin") //自定义claims存储数据
                .claim("logo","logo.png");
        System.out.println( builder.compact() );
    }
}
  • Modify ParseJwtTest to obtain specified information
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()) );
    }
}

JWT tool class

Create JwtUtil tool class in ihrm_common project

package com.ihrm.common.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Date;
import java.util.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.根据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;
    }

}
  • Modify application.yml of ihrm_common project, add configuration
jwt:
 config:
    key: saas-ihrm
    ttl: 360000

Sign in successfully and issue a token

  • Configure JwtUtil. Modify the startup class of the ihrm_system project
@Bean    
public JwtUtil jwtUtil(){    
     return new util.JwtUtil();        
}
  • Add login method in UserController.java
    /**
     * 用户登录
     * 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);
       }
   }
  • Test run results

Using postman to authenticate login returns:

{"success":true,"code":10000,"message":"操作成
功!","data":"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDYyNjYxODkxNjE4Mzc3NzI4Iiwic3ViIjoiemhhb
mdzYW4iLCJpYXQiOjE1NDI0NjgzNzcsImNvbXBhbnlJZCI6IjEiLCJjb21wYW55TmFtZSI6IuS8oOaZuuaSreWu oiIsImV4cCI6MTU0MjU1NDc3N30.J-8uv8jOp2GMLpBwrUOksnErjA4-DOJ_qvy7tsJbsa8"}

Obtain user information authentication

Requirement: After the user logs in successfully, a new request will be sent to the server to obtain the user's detailed information. In the process of obtaining user information, you must log in, otherwise you cannot obtain it.

Front-end and back-end agreement: when the front-end requests microservices, the header information Authorization needs to be added, and the content is Bearer+space+token

  • Add response value object ProfileResult
package com.ihrm.domain.system.response;

import com.ihrm.domain.system.Permission;
import com.ihrm.domain.system.Role;
import com.ihrm.domain.system.User;
import lombok.Getter;
import lombok.Setter;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@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);
    }
}
  • Add profile method in UserController.java
/**
* 获取个人信息
*/
@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));
}
  • verify token

Idea: Obtain the token information whose key is Authorization from the request, and use jwt verification to obtain hidden information after successful verification. Modify the profile method to add the following code

@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));
}

Guess you like

Origin blog.csdn.net/qq_50954361/article/details/130373155