目录
数据表
使用token
实体类
权限枚举类
自定义注解
控制层添加注解
添加控制层切面
RedisUtil工具类
测试
登陆
执行权限所需20以上的查询用户操作
执行权限所需100以上的删除用户操作
我展示我项目中实现的效果,仅提供思路,请按需使用代码
要实现权限校验,首先数据表和实体类上需要有权限字段,我的表中permission和gender是通过外键约束permission表和gender表实现枚举的,因为可拓展性更好
/*
Navicat Premium Data Transfer
Source Server : Yan
Source Server Type : MySQL
Source Server Version : 80027
Source Host : localhost:3306
Source Schema : coc
Target Server Type : MySQL
Target Server Version : 80027
File Encoding : 65001
Date: 07/05/2023 20:00:45
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键',
`account` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '账号',
`password` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码',
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名',
`gender` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '未知' COMMENT '性别',
`telephone` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱',
`signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '无签名' COMMENT '签名',
`avatar_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'C:\\Users\\Yan\\Desktop\\user\\avatar\\avatar.jpg' COMMENT '头像地址',
`permission` int NULL DEFAULT 20 COMMENT '权限',
`banned` bit(1) NULL DEFAULT b'0' COMMENT '是否被封禁',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '无备注' COMMENT '备注',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `f_gender`(`gender` ASC) USING BTREE,
INDEX `f_permission`(`permission` ASC) USING BTREE,
CONSTRAINT `f_gender` FOREIGN KEY (`gender`) REFERENCES `gender` (`gender_name`) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT `f_permission` FOREIGN KEY (`permission`) REFERENCES `permission` (`weight`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
项目使用token校验,那么可以通过请求发送过来的token取到redis缓存中token对应的用户id,因此需要一个存放token:id的hash表,由于我做的token验证直接从表中取token校验,同时也需要取对应的id,所以token和id互为key和value
这是我用mybatis-plsu-generator根据数据表自动生成的
package com.greenjiao.coc.bean;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.Data;
/**
*
*
*
*
* @author yan
* @since 2023-05-07
*/
@Data
@TableName("user")
public class User {
/**
* 主键
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;
/**
* 账号
*/
@TableField("account")
private String account;
/**
* 密码
*/
@TableField("password")
private String password;
/**
* 姓名
*/
@TableField("name")
private String name;
/**
* 性别
*/
@TableField("gender")
private String gender;
/**
* 手机号
*/
@TableField("telephone")
private String telephone;
/**
* 邮箱
*/
@TableField("email")
private String email;
/**
* 签名
*/
@TableField("signature")
private String signature;
/**
* 头像地址
*/
@TableField("avatar_address")
private String avatarAddress;
/**
* 权限
*/
@TableField("permission")
private Integer permission;
/**
* 是否被封禁
*/
@TableField("banned")
private Boolean banned;
/**
* 备注
*/
@TableField("remark")
private String remark;
/**
* 创建时间
*/
@TableField("create_time")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}
定义不同权限身份
package com.greenjiao.coc.common.enumconstant;
/**
* 权限信息
*/
public enum PERMISSION_TYPE {
BANNED_USER(0, "封禁用户"),
BLOCKED_SPEECH_USER(10, "禁言用户"),
NORMAL_USER(20, "普通用户"),
PREMIUM_USER(30, "高级用户"),
NORMAL_ADMIN(100, "管理员"),
TOP_ADMIN(999, "最高管理员");
private final Integer weight;
private final String name;
PERMISSION_TYPE(Integer weight, String name) {
this.weight = weight;
this.name = name;
}
public Integer getWeight() {
return weight;
}
public String getName() {
return name;
}
}
我们需要对一些控制层中特定的方法进行权限校验,并且部分方法的权限要求可能是不同的,所以需要给方法添加自定义注解,注解中包含所需的权限
package com.greenjiao.coc.annotation;
import com.greenjiao.coc.common.enumconstant.PERMISSION_TYPE;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 权限校验注解
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {
PERMISSION_TYPE permission() default PERMISSION_TYPE.TOP_ADMIN;
}
我的用户的控制层,除了登陆和注册外,其他的方法都使用自定义注解Role设置对应所需权限
package com.greenjiao.coc.controller;
import com.greenjiao.coc.annotation.Role;
import com.greenjiao.coc.bean.User;
import com.greenjiao.coc.common.DataVO;
import com.greenjiao.coc.common.enumconstant.PERMISSION_TYPE;
import com.greenjiao.coc.service.impl.UserServiceImpl;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/coc/user")
public class UserController {
private final UserServiceImpl userService;
public UserController(UserServiceImpl userService) {
this.userService = userService;
}
@PostMapping("/register")
public DataVO register(@RequestBody User user) {
return userService.register(user);
}
@PostMapping("/login")
public DataVO
添加其中的权限校验@Before注解指定切点
需要权限校验的切点为controller包下所有类的所有方法,并且要拥有Role注解
package com.greenjiao.coc.aspect;
import com.alibaba.fastjson2.JSON;
import com.greenjiao.coc.annotation.Role;
import com.greenjiao.coc.bean.User;
import com.greenjiao.coc.common.DataVO;
import com.greenjiao.coc.common.ServerConstants;
import com.greenjiao.coc.exception.ExceptionEnum;
import com.greenjiao.coc.exception.ExceptionTip;
import com.greenjiao.coc.mapper.UserMapper;
import com.greenjiao.coc.util.MyUtil;
import com.greenjiao.coc.util.RedisUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Component
public class ControllerAspect {
private final RedisUtil redisUtil;
private final UserMapper userMapper;
public ControllerAspect(RedisUtil redisUtil, UserMapper userMapper) {
this.redisUtil = redisUtil;
this.userMapper = userMapper;
}
// 指定切点为controller目录中所有类的selectAll方法并且要求携带的参数是Map param
@Pointcut(value = "execution(* com.greenjiao.coc.controller..selectAll(..)) && args(param)", argNames = "param")
public void controllerPoint(Map param) {
}
@Pointcut(value = "execution(* com.greenjiao.coc.controller..*(..)) && @annotation(com.greenjiao.coc.annotation.Role)")
public void controllerCheckRolePoint() {
}
/**
* 将查询的内容统一转为对应实体类
* @param joinPoint
* @param param
* @return
* @throws Throwable
*/
@Around(value = "controllerPoint(param) && args(..)", argNames = "joinPoint,param")
public Object changeParam(ProceedingJoinPoint joinPoint, @RequestBody Map param) throws Throwable {
Integer page = (Integer) param.get(ServerConstants.DEFAULT_PAGE_NAME); //获得param中用于分页的page和limit后将其移除,剩余在param中的键值对即为需要查询的条件
param.remove(ServerConstants.DEFAULT_PAGE_NAME);
Integer limit = (Integer) param.get(ServerConstants.DEFAULT_LIMIT_NAME);
param.remove(ServerConstants.DEFAULT_LIMIT_NAME);
String className = MyUtil.getClassName(joinPoint.getTarget().getClass().getName()); //工具类获取全限定类名
Class> clazz = Class.forName(className); //反射机制创建类
Object obj = JSON.parseObject(JSON.toJSONString(param), clazz); //将Map中剩余的键值对转为对应类型的json对象
Map params = new HashMap<>(); //重新存放最后需要新返回的参数,procceed方法的参数需要一个Object数组,
params.put(ServerConstants.DEFAULT_BEAN_NAME, obj); //但是controller层中的selectAll方法又只有一个参数,
params.put(ServerConstants.DEFAULT_PAGE_NAME, page); //如果直接将键值对放到Object数组中将会报参数个数异常,
params.put(ServerConstants.DEFAULT_LIMIT_NAME, limit); //所以这里将键值对放到Map中,再将Map放到Object数组中
return joinPoint.proceed(new Object[]{params}); //procceed方法的参数需要一个Object数组,
}
/**
* 权限校验
*/
@Before("controllerCheckRolePoint()")
public void checkRole(JoinPoint joinPoint){
// 获取Role注解中的permission权重值
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Role role = method.getAnnotation(Role.class);
Integer requiredWeight = role.permission().getWeight();
// 从redis表中查找token对应的id,获取对应的用户
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("token");
String id = (String) redisUtil.hget(ServerConstants.REDIS_TOKEN_TABLE_NAME, token);
User user = userMapper.selectById(id);
// 校验
if(user.getPermission() < requiredWeight){
throw new ExceptionTip(ExceptionEnum.LOW_PERMISSION);
}
}
}
这是我权限校验使用的RedisUtil工具类,由于无用的警告太多,看的烦我用@SuppressWarnings注解将所有警告忽略了
并且将其给Spring管理了,所以可以在其他地方自动注入
package com.greenjiao.coc.util;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类
*/
@SuppressWarnings("all")
@Component
public class RedisUtil {
@Resource
private RedisTemplate redisTemplate;
/****************** common start ****************/
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection) CollectionUtils.arrayToList(key));
}
}
}
/****************** common end ****************/
/****************** String start ****************/
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/****************** String end ****************/
/****************** Map start ****************/
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map
使用一个权限值为20的账号登陆
获取到数据
可见提示权限不足
如果有需要请向我提问,教学相长