权限管理与jwt鉴权

 权限管理与jwt鉴权

学习目标:

理解权限管理的需求以及设计思路实现角色分配和权限分配

理解常见的认证机制

能够使用JWT完成微服务Token签发与验证

权限管理

需求分析

完成权限(菜单,按钮(权限点),API接口)的基本操作

权限与菜单,菜单与按钮,菜单与API接口都是一对一关系。为了方便操作,在SAAS-HRM系统的表设计中,采用基于共享主键的形式实现一对一关系维护,并且数据库约束,一切的关系维护需要程序员在代码中实现。

后端实现

实体类

在系统微服务中创建权限,菜单,按钮(权限点),API对象的实体类

  • 权限实体类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;/*** 主键*/@Idprivate 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;}}

骚戴理解:pid是指的父权限,就是我菜单权限下面可能有一套按钮或者API,那么这些就是子权限,那么它们的父权限就是这个菜单的id。enVisible表示是否查询全部权限(0 :查询全部,1 :只查询企业所属权限)也就是0表示更加高级的权限,连企业都访问不了的,只有超级管理员可以看到的权限

  • 权限菜单(权限点)实体类PermissionMenu
@Entity
@Table(name = "pe_permission_menu")
@Getter
@Setter
public class PermissionMenu implements Serializable {private static final long serialVersionUID = -1002411490113957485L;@Idprivate String id; //主键private String menuIcon; //展示图标private String menuOrder; //排序号
}
  • 权限按钮实体类 PermissionPoint
@Entity
@Table(name = "pe_permission_point")
@Getter
@Setter
public class PermissionPoint implements Serializable {private static final long serialVersionUID = -1002411490113957485L;@Idprivate String id;private String pointClass;private String pointIcon;private String pointStatus;
}
  • 权限API实体类 PermissionApi
@Entity
@Table(name = "pe_permission_api")
@Getter
@Setter
public class PermissionApi implements Serializable {private static final long serialVersionUID = -1803315043290784820L;@Idprivate String id;private String apiUrl;private String apiMethod;private String apiLevel;//权限等级,1为通用接口权限,2为需校验接口权限
}

持久层

  • 权限持久化类
/*** 权限数据访问接口*/
public interface PermissionDao extends JpaRepository<Permission, String>, 
JpaSpecificationExecutor<Permission> {List<Permission> findByTypeAndPid(int type,String pid);
}
  • 权限菜单持久化类
public interface PermissionMenuDao extends JpaRepository<PermissionMenu, String>, 
JpaSpecificationExecutor<PermissionMenu> {
}
  • 权限按钮(点)持久化类
public interface PermissionPointDao extends JpaRepository<PermissionPoint, String>, JpaSpecificationExecutor<PermissionPoint> {
}
  • 权限API持久化类
public interface PermissionApiDao extends JpaRepository<PermissionApi, String>, JpaSpecificationExecutor<PermissionApi> {
}

业务逻辑

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 {@Autowiredprivate PermissionDao permissionDao;@Autowiredprivate PermissionMenuDao permissionMenuDao;@Autowiredprivate PermissionPointDao permissionPointDao;@Autowiredprivate PermissionApiDao permissionApiDao;@Autowiredprivate 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")));}//根据类型 typeif(!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);}}
}

骚戴理解:需要注意的是菜单、按钮、api这三个子权限的id要和父权限id保持一致,也就是PermissionApi、PermissionMenu、PermissionPoint这三个类的id和Permission类保持一致!

把下面这段Java代码翻译成SQL更好理解

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

这段Java代码是用于构建JPA动态查询的条件语句。它的作用是根据map中的"type"键值来判断是否需要加入一个子条件。

代码逻辑如下:

  • 检查map中是否存在"type"键,并且其值不为空;
  • 如果符合条件,则获取"type"的值ty,并且使用CriteriaBuilder构造一个In条件对象in;
  • 如果ty为"0",则添加in条件的取值为1和2;否则将ty转换为int类型并添加到in条件中;
  • 最后将这个in条件添加到条件集合list中。

以下是该Java代码对应的SQL语句:

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

这段Java代码会在生成的SQL语句中自动完成参数的绑定,具体SQL语句中的占位符?由框架来替换成具体的值。

控制器

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 {@Autowiredprivate 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 {//构造idmap.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);}
}

骚戴理解:这里我一开始搞错了,我以为下面的这个前端的Permission实体就是对应这后端的Permission对象,结果不是!导致我一直不能理解为什么后端要封装成一个map来接受参数而不是直接用Permission对象接收,这个前端的Permission实体其实是后端的Permission、PermissionApi、PermissionMenu、PermissionPoint的合体集合

所以这个前端的Permission实体可以用map来接收,也就是通过这么一个前端的Permission实体实现菜单、组件、API的管理,根据前端传过来的type来判断到底要操作什么,这也就是为什么就只有PermissionController这一个控制器却可以实现菜单、组件、API的增删改查操作

前端实现

引入权限管理模块

将资料module-permissions引入到工程的/src文件夹下,在/src/main.js完成模块注册

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

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

实现权限页面

<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-switchv-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.datathis.dialogFormVisible=true})}else{this.formData = {}this.dialogFormVisible=true}},saveOrUpdate() {this.formData.type = this.typethis.formData.pid = this.pidsaveOrUpdate(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.datathis.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>

分配角色

需求分析

由于使用了RBAC模型对权限进行统一管理,所以每个SAAS-HRM平台的用户都应该具有角色的信息。进而通过角 色完成对权限的识别。众所周知,一个用户可以具有很多的角色,一个角色可以被分配给不同的用户。所以用户和角色之间是多对多关系。

服务端代码实现

  • 改造用户实体类,添加角色的id集合属性,表明一个用户具有的多个角色id

在com.ihrm.system.domain.User用户实体类中添加与角色的多对多关系并进行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>();//用户与角色   多对多

在com.ihrm.system.domain.Role角色实体类中配置角色与用户的多对多关系并进行JPA配置

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

骚戴理解:通过在实体类里这样配置就可以实现多个表关联自动修改,也就是你只需要修改实体类他就会自动去维护修改数据库中的关联表和中间表

  • 在com.ihrm.system.controller.UserController添加分配角色的控制器方法实现
    /*** 分配角色*/@RequestMapping(value = "/user/assignRoles", method = RequestMethod.PUT)public Result assignRoles(@RequestBody Map<String,Object> map) {//1.获取被分配的用户idString 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);}
  • 业务逻辑层添加分配角色的业务方法
    /*** 分配角色*/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);}

前端代码实现

  • \src\module-employees 添加分配角色的组件
<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.datathis.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 引入组件
<!--分配角色组件 -->
<component v-bind:is="addRole" ref="addRole"></component>

分配权限

需求分析

完成对角色权限的分配。

服务端代码实现

  • 角色实体类中添加与权限的多对多关系并进行JPA配置
@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);//角色与模块 多对多
  • 控制器类( com.ihrm.system.controller.RoleController )添加权限分配
    /*** 分配权限*/@RequestMapping(value = "/role/assignPrem", method = RequestMethod.PUT)public Result assignPrem(@RequestBody Map<String,Object> map) {//1.获取被分配的角色的idString 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);}
  • 服务层中添加分配权限方法
   /*** 分配权限*/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);}

骚戴理解:上面的分配权限的逻辑和分配角色的逻辑基本一样的,区别就在于下面这两行代码,如果这个权限是按钮,那么这个按钮一定是绑定了一个api权限的,所以如果前端赋按钮权限,那么api权限也要一起赋予,这里一开始我觉得按钮权限和api权限是一对一的关系,所以不能理解为什么用List来接收,后面发现有的时候一个按钮会发多个请求,例如“分配角色”按钮就会发两个请求

List<Permission> apiList =  permissionDao.findByTypeAndPid(PermissionConstants.PERMISSION_API, permission.getId());
perms.addAll(apiList);//自定赋予API权限
  • 数据层
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);
}

骚戴理解:这里直接在PermissionDao里面写了findByTypeAndPid方法,但是没有写具体的sql,我当时就很纳闷他是怎么实现的,这其实就是根据这个方法的命名来实现的,findByTypeAndPid他会根据这个方法名解析出Type和Pid字段,然后用and拼接成sql语句,实现的效果就是按这两个值去查询

前端代码实现

  • 在\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>
  • 在\\src\api\base\role.js中添加分配权限的API方法

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

  • \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-checkboxnode-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>
  • 完成添加权限

常见的认证机制

HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth

Cookie Auth

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie 对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏 览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效

OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资 源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频 编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样, OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容

这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证 权限管理的企业应用。

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

HRM中的TOKEN签发与验证

什么是JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。在Java世界中通过JJWT实现JWT创建和验证。

JJWT的快速入门

token的创建

  • 创建maven工程,引入依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.6.0</version>
</dependency>
  • 创建类CreateJwtTest,用于生成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() );}
}
  • 测试运行,输出如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTM0NTh9.gq0J- cOM_qCNqU_s-d_IrRytaNenesPmqAIhQpYXHZk

token的解析

我们刚才已经创建了token,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

创建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());}
}

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token

自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims

  • 创建CreateJwtTest3,并存储指定的内容
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() );}
}
  • 修改ParseJwtTest,获取指定信息
public class ParseJwtTest {public static void main(String[] args) {StringcompactJws="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTczMjMsImV4cCI6MTUyMzQxNzM4Mywicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJsb2dvLnBuZyJ9.b11p4g4rE94rqFhcfzdJTPCORikqP_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工具类

在ihrm_common工程中创建JwtUtil工具类

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.创建jwtBuilderJwtBuilder jwtBuilder = Jwts.builder().setId(id).setSubject(name).setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, key);//3.根据map设置claimsfor(Map.Entry<String,Object> entry : map.entrySet()) {jwtBuilder.claim(entry.getKey(),entry.getValue());}jwtBuilder.setExpiration(new Date(exp));//4.创建tokenString token = jwtBuilder.compact();return token;}/*** 解析token字符串获取clamis*/public Claims parseJwt(String token) {Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();return claims;}}
  • 修改ihrm_common工程的application.yml, 添加配置
jwt:config:key: saas-ihrmttl: 360000

登录成功签发token

  • 配置JwtUtil。修改ihrm_system工程的启动类
@Bean    
public JwtUtil jwtUtil(){    return new util.JwtUtil();        
}
  • 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);}}
  • 测试运行结果

使用postman验证登录返回:

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

获取用户信息鉴权

需求:用户登录成功之后,会发送一个新的请求到服务端,获取用户的详细信息。获取用户信息的过程中必须登录才能,否则不能获取。

前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格+token

  • 添加响应值对象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);}
}
  • UserController.java添加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));
}
  • 验证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 ", "");//比较并获取claimsClaims 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));
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/157758.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TiDB 7.4 发版:正式兼容 MySQL 8.0

MySQL 是全球最受欢迎的开源数据库&#xff0c;长期位于 DB-Engines Ranking 排行榜第二名&#xff0c;在世界范围内拥有数量庞大的企业用户和开发者。然而&#xff0c;随着时间的推移&#xff0c;MySQL 用户正面临新挑战。Oracle 官宣将在 2023 年 10 月终止 MySQL 5.7 版本的…

LeetCode【739】每日温度

题目&#xff1a; 思路&#xff1a; https://www.bilibili.com/video/BV1PJ411H7P7/?spm_id_from333.337.search-card.all.click&vd_source2f682a60feabf0f730ad09e0f980ce83 单调栈 思考&#xff1a; 解决栈类问题&#xff0c;思考入栈&#xff0c;出栈条件&#xff1b;…

解决“413 Request Entity Too Large”错误 代表请求包太大,服务器拒绝响应

解决办法&#xff1a; 在nginx的配置文件nginx.conf中&#xff0c;添加这么一句client_max_body_size 1024m; 意思是最大请求是1024m。这个配置可以放到 http段 或者 server段 或者 location段。

链表(3):双链表

引入 我们之前学的单向链表有什么缺点呢&#xff1f; 缺点&#xff1a;后一个节点无法看到前一个节点的内容 那我们就多设置一个格子prev用来存放前面一个节点的地址&#xff0c;第一个节点的prev存最后一个节点的地址&#xff08;一般是null&#xff09; 这样一个无头双向…

关于智能控制领域中模糊控制算法的概述

智能控制领域中的模糊控制算法是一种基于模糊逻辑的控制策略&#xff0c;它通过对模糊集合的刻画来处理模糊信息&#xff0c;从而获得模糊输出并进行控制。模糊控制算法在实际控制工程中具有良好的应用前景&#xff0c;它不但具有较强的鲁棒性和适应性&#xff0c;而且可以为复…

【干货】VS2017简介、编译、启动单项目和启动多项目

1. VS2017简单介绍 解决方案和项目的区别&#xff1a; 一般一个解决方案会有多个项目&#xff0c;一个项目里面一般只有一个main文件&#xff0c;所以需要右键单击某个项目将其设置成启动项目&#xff0c;才可以启动该项目。 2. 编译github的代码仓 一般都会有CMakeLists.t…

Go函数介绍与一等公民

Go函数介绍与一等公民 函数对应的英文单词是 Function&#xff0c;Function 这个单词原本是功能、职责的意思。编程语言使用 Function 这个单词&#xff0c;表示将一个大问题分解后而形成的、若干具有特定功能或职责的小任务&#xff0c;可以说十分贴切。函数代表的小任务可以…

基于nodejs+vue校园失物招领平台设计与实现

科学技术日新月异的如今&#xff0c;计算机在生活各个领域都占有重要的作用&#xff0c;尤其在信息管理方面&#xff0c;在这样的大背景下&#xff0c;学习计算机知识不仅仅是为了掌握一种技能&#xff0c;更重要的是能够让它真正地使用到实目 录 摘 要 I ABSTRACT II 目 录 II…

用servlet实现一个简单的猜数字游戏。

需要两个页面&#xff0c;一个jsp页面&#xff08;guess.jsp&#xff09;和servlet页面&#xff08;servlet&#xff09;。 一.jsp页面 在jsp页面中需要实现&#xff1a; 1.创建随机数并且保存在session中。 2.做个form表单提交猜的数字给servlet页面。 <%page import&…

CakePHP 3.x/4.x反序列化RCE链

最近网上公开了cakephp一些反序列化链的细节&#xff0c;但是没有公开poc&#xff0c;并且网上关于cakephp的反序列化链比较少&#xff0c;于是自己跟一下 &#xff0c;构造pop链。 CakePHP简介 CakePHP是一个运用了诸如ActiveRecord、Association Data Mapping、Front Contr…

手搭手Mybatis-Plus数据迁移至TDSQL

环境介绍 技术栈 springbootmybatis-plusdruidbaomidoumysqloracle 软件 版本 mysql 8 IDEA IntelliJ IDEA 2022.2.1 JDK 1.8 Spring Boot 2.7.13 mybatis 2.3.1 Navicat测试连接TDSQL 开启访问外网 IDEA环境搭建 pom.xml所需依赖 <dependencies><dep…

【Minecraft开服教程】使用 MCSM 面板一键搭建我的世界服务器,并内网穿透公网远程联机

文章目录 前言1.Mcsmanager安装2.创建Minecraft服务器3.本地测试联机4. 内网穿透4.1 安装cpolar内网穿透4.2 创建隧道映射内网端口 5.远程联机测试6. 配置固定远程联机端口地址6.1 保留一个固定TCP地址6.2 配置固定TCP地址 7. 使用固定公网地址远程联机 前言 MCSManager是一个…

【通义千问】大模型Qwen GitHub开源工程学习笔记(4)-- 模型的量化与离线部署

摘要: 量化方案基于AutoGPTQ,提供了Int4量化模型,其中包括Qwen-7B-Chat和Qwen-14B-Chat。更新承诺在模型评估效果几乎没有损失的情况下,降低存储要求并提高推理速度。量化是指将模型权重和激活的精度降低以节省存储空间并提高推理速度的过程。AutoGPTQ是一种专有量化工具。…

通讯网关软件023——利用CommGate X2HTTP实现HTTP访问Modbus TCP

本文介绍利用CommGate X2HTTP实现HTTP访问Modbus TCP。CommGate X2HTTP是宁波科安网信开发的网关软件&#xff0c;软件可以登录到网信智汇(http://wangxinzhihui.com)下载。 【案例】如下图所示&#xff0c;SCADA系统上位机、PLC、设备具备Modbus RTU通讯接口&#xff0c;现在…

Meta开源数字水印Stable Signature,极大增强生成式AI安全

全球社交、科技巨头Meta&#xff08;Facebook、Instagram等母公司&#xff09;在官网宣布&#xff0c;开源数字水印产品Stable Signature&#xff0c;并公开论文。 据悉&#xff0c;Stable Signature是由Meta和INRIA&#xff08;法国国家信息与自动化研究所&#xff09;联合开…

分享一个查询OpenAI Chatgpt key余额查询的工具网站

OpenAI Key 余额查询工具 欢迎使用 OpenAI Key 余额查询工具网站&#xff01;这个工具可以帮助您轻松地验证您的 OpenAI API 密钥&#xff0c;并查看您的余额。 http://tools.lbbit.top/check_key/ 什么是 OpenAI Key 余额查询工具&#xff1f; OpenAI Key 余额查询工具是一…

最详细STM32,cubeMX 按键点亮 led

这篇文章将详细介绍 如何在 stm32103 板子上使用 按键 点亮一个LED. 文章目录 前言一、如何控制按键&#xff1f;为什么按键要接上拉电阻或者下拉电阻呢&#xff1f; 二、cubeMX配置工程自动生成代码解析 三、读取引脚电平函数四、按键为什么要消抖如何消除消抖 五、实现按键控…

八大排序算法(含时间复杂度、空间复杂度、算法稳定性)

文章目录 八大排序算法(含时间复杂度、空间复杂度、算法稳定性)1、&#xff08;直接&#xff09;插入排序1.1、算法思想1.2、排序过程图解1.3、排序代码 2、希尔排序3、冒泡排序3.1、算法思想3.2、排序过程图解3.3、排序代码 4、&#xff08;简单&#xff09;选择排序4.1、算法…

小谈设计模式(27)—享元模式

小谈设计模式&#xff08;27&#xff09;—享元模式 专栏介绍专栏地址专栏介绍 享元模式模式结构分析享元工厂&#xff08;FlyweightFactory&#xff09;享元接口&#xff08;Flyweight&#xff09;具体享元&#xff08;ConcreteFlyweight&#xff09;非共享具体享元&#xff0…

竞赛选题 深度学习LSTM新冠数据预测

文章目录 0 前言1 课题简介2 预测算法2.1 Logistic回归模型2.2 基于动力学SEIR模型改进的SEITR模型2.3 LSTM神经网络模型 3 预测效果3.1 Logistic回归模型3.2 SEITR模型3.3 LSTM神经网络模型 4 结论5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 …