SpringBoot切面实现token权限校验详解
作者:吃青椒的秋草鹦鹉
SpringBoot实现token权限
数据表
要实现权限校验,首先数据表和实体类上需要有权限字段,我的表中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校验,那么可以通过请求发送过来的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; /** * <p> * * </p> * * @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<User> register(@RequestBody User user) { return userService.register(user); } @PostMapping("/login") public DataVO<Map<String, Object>> login(@RequestBody User user) { return userService.login(user); } @Role(permission = PERMISSION_TYPE.NORMAL_USER) @PutMapping public DataVO<User> update(@RequestBody User user) { return userService.update(user); } @Role(permission = PERMISSION_TYPE.NORMAL_ADMIN) @DeleteMapping("/{id}") public DataVO<User> delete(@PathVariable String id) { return userService.delete(id); } @Role(permission = PERMISSION_TYPE.NORMAL_USER) @GetMapping public DataVO<User> selectAll(@RequestBody User user) { return userService.selectAll(user); } }
添加控制层切面
添加其中的权限校验@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<String,Object> param @Pointcut(value = "execution(* com.greenjiao.coc.controller..selectAll(..)) && args(param)", argNames = "param") public void controllerPoint(Map<String, Object> 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<String, Object> 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<String, Object> 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工具类
这是我权限校验使用的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<String, Object> 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<String>) 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<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并设置时间 * * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) * @return true成功 false失败 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除hash表中的值 * * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return */ public double hincr(String key, String item, long by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return */ public double hdecr(String key, String item, long by) { return redisTemplate.opsForHash().increment(key, item, -by); } /****************** Map end ****************/ /****************** Set start ****************/ /** * 根据key获取Set中的所有值 * * @param key 键 * @return */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据value从一个set中查询,是否存在 * * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将数据放入set缓存 * * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 将set数据放入缓存 * * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) expire(key, time); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取set缓存的长度 * * @param key 键 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值为value的 * * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /****************** Set end ****************/ /****************** List start ****************/ /** * 获取list缓存的内容 * * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取list缓存的长度 * * @param key 键 * @return */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通过索引 获取list中的值 * * @param key 键 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 * @return */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据索引修改list中的某条数据 * * @param key 键 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N个值为value * * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } /****************** List end ****************/ }
测试
登陆
使用一个权限值为20的账号登陆
执行权限所需20以上的查询用户操作
获取到数据
执行权限所需100以上的删除用户操作
可见提示权限不足
到此这篇关于SpringBoot切面实现token权限校验详解的文章就介绍到这了,更多相关SpringBoot实现token权限内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!