# SpringSecurityInfo **Repository Path**: supvegetable/spring-security-info ## Basic Information - **Project Name**: SpringSecurityInfo - **Description**: 这里是本人自学SpringSecurity的资料,希望能够填满 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2023-11-09 - **Last Updated**: 2023-12-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: SpringSecurity ## README 这是我的自学笔记,当然也可以是你的参考笔记,不适合直接照着我这个学。我是已经看过视频,了解过Spring Security的过滤器链以及基本流程。 所以本自学笔记只保留了实现的具体步骤。适合初学并且喜欢先看视频在动手做的朋友看这个笔记。(因为我也是小白啊,初学也不精,写下这篇希望也能帮助其他人) # 1.Spring Security ##1.1创建项目,添加依赖 ```xml org.springframework.boot spring-boot-starter-parent 2.5.4 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 添加过以上依赖,先运行确保项目没有问题 **注意**:此时还没有添加 Spring Security依赖 我们在做Web项目时,常常会涉及到用户登录,不同用户,可以使用的功能不一样。此时想要实现这种功能,常常是**在servlet请求之前**添加各种**拦截器**/**过滤器**,从而实现对各种请求的处理。对于开发人员来说,**自己编写完整可靠的拦截器难度过大,且浪费时间。**因此Spring提供了一款安全框架Spring Security供开发人员使用。 Spring Security依靠**过滤器链**实现了更完善的安全解决方案,包括身份认证、授权管理、攻击防护等功能。 ## 1.2添加Spring Security依赖 ```xml org.springframework.boot spring-boot-starter-security ``` Spring Security本身就是Spring家族中的一员,因此添加上Spring Security依赖,你的项目就已经被安全框架所保护。 运行项目,打开浏览器,访问项目会自动跳转到由Spring Security自带的登录页面**用户名默认**为:user,**密码:**控制台打印的一长串字符。 项目已经被保护起来,登录验证也有,此时走的是**默认过滤器中的方法**,那我们只需要重写Spring Security过滤器中查询用户的方法即可。 ## 1.3重写查询用户的方法 ### 3.1准备数据库表 ```sql SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sys_menu 菜单权限表 -- ---------------------------- DROP TABLE IF EXISTS `sys_menu`; CREATE TABLE `sys_menu` ( `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', `menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称', `parent_id` bigint(20) NULL DEFAULT 0 COMMENT '父类ID', `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '路由地址', `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件路径', `menu_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '菜单类型( M目录 C菜单 F按钮)', `visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)', `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '锁定状态(0正常 1停用)', `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限标识', `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '菜单图标', `created_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`menu_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for sys_role 角色信息表 -- ---------------------------- DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID', `role_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名称', `role_key` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '角色权限字符串', `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '角色状态(0正常 1停用)', `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', `created_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`role_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for sys_role_menu 角色权限关联表 -- ---------------------------- DROP TABLE IF EXISTS `sys_role_menu`; CREATE TABLE `sys_role_menu` ( `role_id` bigint(20) NOT NULL COMMENT '角色ID', `menu_id` bigint(20) NOT NULL COMMENT '菜单ID', PRIMARY KEY (`role_id`, `menu_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色权限关联表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for sys_user 用户信息表 -- ---------------------------- DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户账号', `nick_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户昵称', `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码', `sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)', `phonenumber` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '手机号码', `avatar` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户头像', `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)', `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', `created_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`user_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for sys_user_role 用户角色关联表 -- ---------------------------- DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `user_id` bigint(20) NOT NULL COMMENT '用户ID', `role_id` bigint(20) NOT NULL COMMENT '角色ID', PRIMARY KEY (`user_id`, `role_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户角色关联表' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1; ``` 想省事此时可以**只创建用户信息表**,其他和权限相关的表现在还用不到 ### 3.2用户的实体类 ```java //1.这里用了lombok自动生成get/set方法和有参与无参的构造方法 //2.@TableName等是Mybatis-Plus的功能,如果不知道可以改动实体类名为sysUser,对照数据库表修改一下即可。 @Data @AllArgsConstructor @NoArgsConstructor @TableName("sys_user") public class User { @TableId("user_id") private String id; //账号 private String userName; //昵称 private String nickName; //密码 private String password; //0:男 1:女 2:未知 private int sex; //电话 @TableField("phonenumber") private String phoneNumber; // 存储头像的字符串类型变 private String avatar; //0:正常 1:禁用 private int status; //是否删除 private int delFlag; //使用Mybatis-Plus自动填充 @TableField("create_time") private String createTime; @TableField("update_time") private String updateTime; @TableField("created_by") private String createBy; @TableField("updated_by") private String updateBy; } ``` ### 3.3添加依赖 我们希望的是可以**查询自己数据库里面的用户信息**,所以肯定要**引入数据库相关的依赖** ```xml com.baomidou mybatis-plus-boot-starter 3.4.2 com.alibaba druid-spring-boot-starter 1.2.16 mysql mysql-connector-java 8.0.13 ``` ### 3.4yml配置文件编写 ```yaml spring: datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/vegetable_frame?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 ``` url,username,password**记得改成自己的**,mysql是**低版本**的话driver-class-name: com.mysql.jdbc.Driver用这个 ### 3.5Mapper 编写UserMapper,数据层,查询数据库 ```java import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.htu.springsecurityfast.domain.User; @Mapper public interface UserMapper extends BaseMapper { } //自己写个单元测试,一步一步来,看看数据库这步有没有调通 ``` ###3.6Service重写方法就是在这里 重写UserDetailsService中的loadUserByUsername方法。 **如何重写?**UserDetailsService本身是一个接口,我们只需要编写一个实现类,就能重写该接口中的loadUserByUsername方法 ```java //编写UserDetailsService的实现类 @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired//userMapper数据层查询 private UserMapper userMapper; @Override //注意返回值是UserDetails public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //Mybatis-Plus中的语法,可以根据自己的喜好自行修改 User user = userMapper.selectOne(new LambdaQueryWrapper().eq(User::getUserName, username)); if (ObjectUtils.isEmpty(user)){ throw new UsernameNotFoundException("用户名不存在"); } //返回需要是UserDetails类型,Ctrl+单击,发现UserDetails是Spring Security提供的一个不可更改的接口,那我们只需要自己写一个实体对象去实现UserDetails即可 //接口没办法直接new生成,老老实实写实体类 //TODO 没有权限信息,后面在这里进行封装 return new LoginUser(user); } } ``` 自己写一个实体类实现UserDetails ```java @Data @AllArgsConstructor @NoArgsConstructor public class LoginUser implements UserDetails { private User user; @Override //权限列表,一个用户可以拥有许多权限,此时我们只做登录验证,这里先不考虑,返回null即可 public Collection getAuthorities() { return null; } @Override //用户的密码 public String getPassword() { return user.getPassword(); } @Override //用户名 public String getUsername() { return user.getUserName(); } @Override //账号是否正常 public boolean isAccountNonExpired() { return true; } @Override //是否被锁定 public boolean isAccountNonLocked() { return true; } @Override //验证是否没有过期 public boolean isCredentialsNonExpired() { return true; } @Override //是否启用 public boolean isEnabled() { return true; } } ``` 此时登录查询自己的数据库功能已经完成。 **注意**:数据库的密码此时明文存储需要在前面加上{noop} ### 3.7添加BCryptPasswordEncoder Spring Security**自带**的密码加密,相对BCryptPasswordEncoder来说不够方便,存储在数据库中的密码需要根据**加密规则添加{}表示**,不方便操作。因此使用BCryptPasswordEncoder。 在IOC容器中注入BCryptPasswordEncoder,Spring Security会自动使用。配置大于约定思想。有了就用你的,没了就用我自己的 **新增一个SecurityConfig对Spring Security进行配置** ```java @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` ## 1.4重写登录/退出 Spring Security默认是有登录和退出页面的,不嫌弃也可以用,哈哈~ 接口**三要素**:Controller,Service,Mapper。Mapper的数据层上一步已经改写过Spring Security,查询我们自己的数据库。在**重写UserDetailsService中loadUserByUsername(String username)方法时**,拥**有一个username**,这个username其实就是**过滤器链一步步传递下来**的。那接下来就只需要聚焦**如何把我们提交的数据塞给Spring Security**。 Spring Security中的**用户信息都被封装进Authentication**,然后**塞入方法中进行传递**。那么此时**目标明确**,我们只需要把**浏览器传递过来的用户信息塞进Authentication**,并**传入认证方法**中即可。 **-------------------------------------------------------------------------------------------------------------------------------------------------------** **我的疑惑**:在认证之后用户信息会被放进SecurityContextHolder上下文中,为什么还要**Redis**缓存用户信息。 **自己查的资料**:这个上下文对象中的用户信息会在**过滤器链走完只会自动清除**,所以**每次都要验证**,因此需要Redis缓存用户信息,然后配合上JWT过滤器,如果用户有效,则**用Redis中取出的数据放入SecurityContextHolder上下文中**,这样不仅方便**后续过滤器验证**,而且是前后端分离项目认证的一中解决方案。 为尽量考虑系统的可用性,因此我们**增加Redis和JWT**。做着玩的话,可用忽略这些,只去弄Controller,Service,Mapper即可。 ### 4.1准备Redis 1. 引入Redis依赖 ```xml org.springframework.boot spring-boot-starter-data-redis ``` 2. yml配置文件编写 ```yaml spring: redis: port: 6379 host: 127.0.0.1 #有账号密码的自己改一下 ``` 3. 配置Redis序列化方式 直接往Redis里面存会出现乱码,不方便操作,需要配置一下序列化方式。 ```java @Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置序列化key的序列化方式 //String redisTemplate.setKeySerializer(new StringRedisSerializer()); //Hash redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 设置序列化算法为Java的默认序列化 //String redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); //Hash redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); redisTemplate.afterPropertiesSet(); return redisTemplate; } } ``` 4. Redis工具类 自己使用redisTemplate手搓也可以,我这个也是copy若依的,里面加了点Stream的方法 ```java /** * 功能描述:SpringData Redis 的工具类 **/ @RequiredArgsConstructor @Component @SuppressWarnings({"unchecked", "all"}) @Slf4j public class RedisUtil { private final RedisTemplate redisTemplate; // =============================common============================ /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 根据 key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(Object key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 查找匹配key * * @param pattern key * @return / */ public List scanKey(String pattern) { ScanOptions options = ScanOptions.scanOptions().match(pattern).build(); RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); RedisConnection rc = Objects.requireNonNull(factory).getConnection(); Cursor cursor = rc.scan(options); List result = new ArrayList<>(); while (cursor.hasNext()) { result.add(new String(cursor.next())); } try { RedisConnectionUtils.releaseConnection(rc, factory); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 分页查询 key * * @param patternKey key * @param page 页码 * @param size 每页数目 * @return / */ public List findKeysForPage(String patternKey, int page, int size) { ScanOptions options = ScanOptions.scanOptions().match(patternKey).build(); RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); RedisConnection rc = Objects.requireNonNull(factory).getConnection(); Cursor cursor = rc.scan(options); List result = new ArrayList<>(size); int tmpIndex = 0; int fromIndex = page * size; int toIndex = page * size + size; while (cursor.hasNext()) { if (tmpIndex >= fromIndex && tmpIndex < toIndex) { result.add(new String(cursor.next())); tmpIndex++; continue; } // 获取到满足条件的数据后,就可以退出了 if (tmpIndex >= toIndex) { break; } tmpIndex++; cursor.next(); } try { RedisConnectionUtils.releaseConnection(rc, factory); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 判断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 可以传一个值 或多个 */ public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } // ============================String============================= /** * 普通缓存获取 * * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 批量获取 * * @param keys * @return */ public List multiGet(List keys) { return redisTemplate.opsForValue().multiGet(Collections.unmodifiableCollection(keys)); } /** * 普通缓存放入 * * @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 value 值 * @param time 时间 * @param timeUnit 类型 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time, TimeUnit timeUnit) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, timeUnit); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } // ================================Map================================= /** * 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 hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map 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 map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } public boolean DeviceSet(String key, Map 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, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, - by); } // ============================set============================= /** * 根据key获取Set中的所有值 * * @param key 键 * @return */ public Set 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; } } // ===============================增 list @RenShiWei 2020/2/6================================= /** * 获取list缓存的内容 * * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return */ public List 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 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 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 { return redisTemplate.opsForList().remove(key, count, value); } catch (Exception e) { e.printStackTrace(); return 0; } } //-----------------------自定义工具扩展 @RenShiWei 2020/2/6---------------------- /** * 功能描述:在list的右边添加元素 * 如果键不存在,则在执行推送操作之前将其创建为空列表 * * @param key 键 * @return value 值 * @author RenShiWei * Date: 2020/2/6 23:22 */ public Long rightPushValue(String key, Object value) { return redisTemplate.opsForList().rightPush(key, value); } /** * 功能描述:在list的右边添加集合元素 * 如果键不存在,则在执行推送操作之前将其创建为空列表 * * @param key 键 * @return value 值 * @author RenShiWei */ public Long rightPushList(String key, List values) { return redisTemplate.opsForList().rightPushAll(key, values); } /** * 指定缓存失效时间,携带失效时间的类型 * * @param key 键 * @param time 时间(秒) * @param unit 时间的类型 TimeUnit枚举 */ public boolean expire(String key, long time, TimeUnit unit) { try { if (time > 0) { redisTemplate.expire(key, time, unit); } } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * @param prefix 前缀 * @param ids id */ public void delByKeys(String prefix, Set ids) { Set keys = new HashSet<>(); for (Long id : ids) { keys.addAll(redisTemplate.keys(new StringBuffer(prefix).append(id).toString())); } long count = redisTemplate.delete(keys); // 此处提示可自行删除 log.debug("--------------------------------------------"); log.debug("成功删除缓存:" + keys.toString()); log.debug("缓存删除数量:" + count + "个"); log.debug("--------------------------------------------"); } /** * XADD 往队列中添加信息 * @param record 添加的记录 */ public RecordId add(String stream,Map record){ RecordId add = redisTemplate.opsForStream().add(stream, record); return add; } /** * 读消费者组中的消息 * @param group 消费者组 * @param stream 队列名称 * @param consumer 消费者 * @param number 读取数量 * @param duration 阻塞时间 * @return */ public List> xReadGroup(String group , String stream , String consumer , Integer number,Duration duration){ //XREADGROUP GROUP group consumer COUNT 1 BLOCK 2000 STREAM stream > List> log = redisTemplate.opsForStream().read( //指定消费者组 和消费者名称(GROUP group consumer) Consumer.from(group, consumer), //一次读一条 阻塞2秒钟 (COUNT 1 BLOCK 2000) StreamReadOptions.empty().count(number).block(duration), //指定队列 读最新的 (STREAM stream >) StreamOffset.create(stream, ReadOffset.lastConsumed()) ); return log; } /** * 做ACK消息处理确认 * @param id 消息id * @param stream 队列 * @param group 消费者组 * @return 确认消息条数 */ public Long sACK(RecordId id ,String stream ,String group){ Long acknowledge = redisTemplate.opsForStream().acknowledge(stream, group, id); return acknowledge; } /** * 读Pending-list中的消息 异步不可用 * @param group 消费者组 * @param stream 队列 * @param consumer 消费者名称 * @param number 读取数量 * @return */ public List> xPending(String group , String stream , String consumer , Integer number){ //XREADGROUP GROUP group consumer COUNT 1 STREAM stream 0 List> log = redisTemplate.opsForStream().read( //指定消费者组 和消费者名称(GROUP group consumer) Consumer.from(group, consumer), //一次读一条 (COUNT number ) StreamReadOptions.empty().count(number), //指定队列 读最新的 (STREAM stream 0) StreamOffset.create(stream, ReadOffset.from("0")) ); return log; } /** * 创建消费者组 * @param stream 消息队列 * @param gruop 消费者组 * @return */ public String xGroup(String stream , String gruop){ //XGRUOP CREATE stream gruop 0 MKSTREAM String msg = redisTemplate.opsForStream().createGroup(stream, ReadOffset.from("0"), gruop); //设置队列最大长度10000L redisTemplate.opsForStream().trim(stream,10000L,false); return msg; } /** * Pending - list 队列消息详情 * @param stream 队列名称 * @param gruop 消费者组名称 * @return PendingMessagesSummary 对象 */ public PendingMessagesSummary Pending(String stream , String gruop){ PendingMessagesSummary pending = redisTemplate.opsForStream().pending(stream, gruop); return pending; } /** * 根据id范围读取指定消息 * @param stream 队列名称 * @param indexId 起始id * @param endId 结束id * @return List> */ public List> readDesignateMessage(String stream , String indexId , String endId ){ List> recordList = redisTemplate.opsForStream().range(stream, Range.closed(indexId, endId)); return recordList; } public Object claim(String stream , String group , String consumer , RecordId id){ Object execute = redisTemplate.execute(new RedisCallback() { @Override public List doInRedis(RedisConnection connection) throws DataAccessException { List claim = connection.xClaim(stream.getBytes(), group, consumer, Duration.ofDays(0), id); return claim; } }); return execute; } } ``` ### 4.2准备JWT 1.引入JWT依赖 ```xml io.jsonwebtoken jjwt 0.9.1 ``` JDK8以上需要额外移入以下依赖 ```xml javax.xml.bind jaxb-api 2.4.0-b180830.0359 com.sun.xml.bind jaxb-impl 3.0.0-M4 com.sun.xml.bind jaxb-core 3.0.0-M4 javax.activation activation 1.1.1 ``` 2. yml配置文件编写 ```yaml #我这边是用了@Value属性注入的方式,可用不配置,然后直接在下面的步骤直接给值 # token配置 token: #token签名(密钥)自己搞个炫酷一点的 Signature: 123456 #令牌有效期 expireTime: 3600000 ``` 3. JWT工具类 ```java @Component public class JwtUtil { //令牌签名 @Value("${token.Signature}") private String Signature; //令牌过期时间 @Value("${token.expireTime}") private int expireTime; /** * 生成Token * @param subject 设置主题 * @param claims 设置Payload中信息 * @return Token */ public String creatToken(String subject , Map claims){ Date now = new Date(); Date expiryDate = new Date(now.getTime() + expireTime); SecretKey secretKey = generalKey(); String token = Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(now) //设置签发时间 .setExpiration(expiryDate) //设置过期时间 .signWith(SignatureAlgorithm.HS256, secretKey) //设置HS256加密算法 设置签名 .compact(); return token; } public String creatTokenWithoutClaim(String subject){ Date now = new Date(); Date expiryDate = new Date(now.getTime() + expireTime); SecretKey secretKey = generalKey(); String token = Jwts.builder() .setSubject(subject) .setIssuedAt(now) //设置签发时间 .setExpiration(expiryDate) //设置过期时间 .signWith(SignatureAlgorithm.HS256, secretKey) //设置HS256加密算法 设置签名 .compact(); return token; } /** * 验证token是否有效 * @param token Token * @return true / false */ public boolean verifyToken(String token){ try { Jwts.parser().setSigningKey(Signature).parseClaimsJws(token); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 获取Token中的所有信息 * @param token Token * @return Claims * @throws Exception e */ public Claims parseToken(String token) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); } /** * 获取Token中的用户名 * @param token token * @return 用户名 * @throws Exception e */ public String getUserName(String token) throws Exception { return parseToken(token).getSubject(); } /** * 生成加密secretKey * @return */ public SecretKey generalKey() { byte[] encodedKey = Base64.getEncoder().encode(Signature.getBytes()); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } } ``` ### 4.3统一返回数据类R 不要感觉熟悉,因为这就是瑞吉外卖中的R,还是老朋友。 ```java @Data public class R { //1.成功 0.失败 private Integer code; //错误信息 private String msg; //数据 private T data; //动态数据 private Map map = new HashMap(); public static R success(){ R r = new R(); r.code=1; return r; } public static R success(T object){ R r = new R(); r.data=object; r.code=1; return r; } public static R success(String msg,T object){ R r = new R(); r.data=object; r.code=1; r.msg=msg; return r; } public static R error(String msg) { R r = new R(); r.msg = msg; r.code = 0; return r; } public R add(String key, Object value) { this.map.put(key, value); return this; } } ``` ### 4.4LoginController 名字自己看看**想起什么就起什么吧**,但是也要见名知意嗷。 Post请求**数据在请求体中**,所以用@RequestBody,**接收数据的User类要用咱们自己的**,因为Spring Security也有一个User ```java @RestController @RequestMapping("/user") public class LoginController { //直接copy肯定会有错,因为你没有LoginService,创建一个吧。 @Autowired private LoginService loginService; /** * 用户登录接口 */ @PostMapping("/login") public R login(@RequestBody User user) { if (ObjectUtils.isEmpty(user)){ return R.error("用户名或密码不能为空"); } String token = loginService.login(user); return R.success("登录成功","token:"+token); } } ``` ### 4.5LoginService 1. LoginService接口 ```java public interface LoginService { String login(User user); } ``` 2. LoginService实现类 ```java @Service public class LoginServiceImpl implements LoginService { @Override public String login(User user) { } } ``` 这会应该就**迷了**,前面**搞那么多东西**,到这会了**该弄什么**,**正事是啥来着?** **点一下**左边的**1.4重写登录/退出回去**看看 ​ 2.1把传过来的**user信息封装进Authentication:**Authentication本身是一个**接口**,有许**多实现类**,我们的查询方法是使用用户名查询,所以**使用UsernamePasswordAuthenticationToken()这个实现类**,往代码里面填吧。 ```java @Service public class LoginServiceImpl implements LoginService { @Override public String login(User user) { //2.1 Authentication authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword()); } } ``` ​ 2.2把Authentication传入认证方法中:**怎么往里面传递?**我们此时写的Service接口等于**取代**了Spring Security中**UsernamePasswordFilter**,那UsernamePasswordFilter中**调用什么方法我们就可用调用什么方法**。 UsernamePasswordFilter调用**AuthenticationManager中的authenticate方法** AuthenticationManager需要我们**手动暴露**一下,**点击**左侧的**3.7添加BCryptPasswordEncoder**,然后在**SecurityConfig**中**添加如下代码**对AuthenticationManager进行暴露 ```java @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } ``` 接下来实现2.2的步骤,**UsernamePasswordFilter调用什么我们就调用什么** ```java @Service public class LoginServiceImpl implements LoginService { //2.2 @Autowired private AuthenticationManager authenticationManager; @Override public String login(User user) { //2.1 Authentication authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword()); //2.2 OK 到此重写查询用户的方法,与重写登录方法正式接头!!! Authentication authenticate = authenticationManager.authenticate(authenticationToken); } } ``` ### 4.6搞Redis和JWT 到这里终于就是熟悉或者相对于好理解的操作了 ```java @Service public class LoginServiceImpl implements LoginService { //2.2 @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtUtil jwtUtil; @Autowired private RedisUtil redisUtil; @Override public String login(User user) { //2.1 Authentication authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword()); //2.2 OK 到此重写查询用户的方法,与重写登录方法正式接头!!! Authentication authenticate = authenticationManager.authenticate(authenticationToken); //以下是4.6的内容------------------------------------------------------------------------------ if (ObjectUtils.isEmpty(authenticate)){ throw new RuntimeException("用户名或密码错误"); } LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); //根据用户id和用户名生成token Map loginUserJwtInfo = new HashMap<>(); loginUserJwtInfo.put("id",loginUser.getUser().getId()); loginUserJwtInfo.put("userName",loginUser.getUser().getUserName()); String token = jwtUtil.creatToken(loginUser.getUser().getUserName(), loginUserJwtInfo); loginUser.getUser().setPassword(null); //将用户信息存入redis redisUtil.set("login:user"+loginUser.getUser().getId(),loginUser); return token; } } ``` ### 4.7SecurityConfig进一步配置 我们**重写了登录/退出的方法**,因此需要告诉Spring Security**我们自己的接口不要拦截**,这样才可以正常登录与退出**继续添加如下代码**: ```java @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login").anonymous() .anyRequest().authenticated(); http.cors(); } ``` ### 4.8JWT过滤器 上面我们使用JWT生成了Token返回给前端,接下来的**每个请求**,都可以要求**前端在请求头中添加Token**。我们可用对Token进行验证,这样就可用进一步**分流**,**有效的Token可用直接放行**,**无效的Token让他们去登录** 在**第三步**从Redis获取用户的时候我的**没法正常的强转**,因此使用的Hutool工具包转换了一下: ```xml cn.hutool hutool-all 5.8.18 ``` ```java @Component public class JwtTokenFilter extends OncePerRequestFilter { @Autowired private RedisUtil redisUtil; @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //1.如果请求头中没有解析出token,则直接放行 String token = request.getHeader("token"); String userId; if (!StringUtils.hasText(token)){ filterChain.doFilter(request, response); return; } //2.获取到token之后,解析token try { Claims claims = jwtUtil.parseToken(token); userId = (String) claims.get("id"); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token解析失败"); } //3.判断token是否失效 就是这里!!!!!!!!!!!! Object o = redisUtil.get("login:user" + userId); LoginUser loginUser = BeanUtil.copyProperties(o, LoginUser.class); if (ObjectUtils.isEmpty(loginUser)){ throw new RuntimeException("token失效"); } //4.将用户信息存入SecurityContextHolder,供后续过滤器使用 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); } } ``` 咱们**自己定义**好了过滤器,要把他**放在哪里**,需要**告诉Spring**,既然在进入Spring Security**用户校验之前**,我们就确定下来了位置。**那就是UsernamePasswordFilter之前**。 在SecurityConfig中添加如下代码: ```java @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login").anonymous() .anyRequest().authenticated(); //------------------------这里是新添加的------------------------------------------- http.addFilterBefore(jwtTokenFilter,UsernamePasswordAuthenticationFilter.class); //--------------------------------------------------------------------------------- http.cors(); } ``` ### 4.9退出登录 上面登录功能已经实现,现在回顾一下,用户**验证账号密码之后**,把用户**存到Spring Security的上下文中**,供后续过滤器使用,但是这个上下文是**一次性的**,每次执行完之后会自动删除,所以就要每次都验证,影响系统效率,所以引入 Redis + JWT。验证完账号密码之后,将**用户的信息存入到Redis**中,然后再**根据用户信息使用JWT生成token返回给前端**,前端每次请求只需要在**请求头中添加token**即可。后端定义**JWT过滤器来判定用户是否登录**,**登录**直接把用户信息**存入到Spring Security的上下文**中,**没有**登录的话则**跳转登录**。 自己定义的JWT过滤器,**验证是否登录的方式**是到**Redis**中查询**是否有用户信息**,没有则判定未登录,有则登录。 一个用户调用退出登录的接口,肯定就是已经登录过了,我们**可以根据用户上下文来获取到用户**,因此此接口不需要传参数,且因为JWT过滤器的逻辑,我们只需要把Redis中的用户信息删除即可。 ```java @DeleteMapping("/logout") public R logout(){ //在SecurityContextHolder上下文中获取用户信息 LoginUser LoginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (ObjectUtils.isEmpty(LoginUser)){ return R.error("退出失败"); } String id = LoginUser.getUser().getId(); //根据key在Redis中删除用户 redisUtil.del("login:user"+id); return R.success("退出成功"); } ``` ## 1.5权限 **权限**就是看本系统的**用户是否有使用某个功能的身份**,看他身份够不够,没身份就不能赛脸,咱就别看了,Spring Security提供便捷的鉴权形式,**配置**+**注解**都可以使用。 一个**用户**可以有**很多身份**,超级管理员,VIP,SVIP等等,那么我们就可以在**数据库的用户表**中添加一个**权限字段**来**存储权限信息**。这是自己的简单想法,可以实现权限的需求,但是一个方案的施行有很多方式,我们大致知道是怎么回事就行。接下来就要用别人更好的想法或者是方案,那就是**RBAC模型**。 ### 5.1RBAC模型 RBAC(Role-Based Access Control)基于**角色**的访问控制,在此模型中访问的权限是通过角色来进行管理和控制的。 为什么要使用这个,最直观的例子,一个**用户**可以**拥有巨多权限**,系统**每注册**一个用户,**都要分配权限**,系统功能比较少,就四五个权限到无所谓,有个二三十个,分配起来就汗流浃背了兄弟们。所以分层,给**用户分配角色**(一个用户可以拥有多个角色),**角色拥有权限**,就可以解决频繁分配的问题。 | 用户 | 小明:普通+VIP | 小刚:普通 | 小美:普通+VIP+SVIP | | :--: | :------------: | :----------: | :-----------------: | | 角色 | 普通 | VIP | SVIP | | 权限 | 只能聊天 | 可以发表情包 | 可以发语音 | 以上的情况: 小明可以聊天+发表情包;小刚只能聊天;小美可以聊天+表情包+发语音。通过这样的分层控制就可以为用户添加角色来分配限权,**既方便分配权限,又更细致化**。 都是自己的理解,难绷!!! ### 5.2准备数据库表 **3.1**中**只弄了用户信息表**的同学**需要加下面这几张表**,**直接全都弄**了的**不需要**这一步。 ```sql SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sys_menu 菜单权限表 -- ---------------------------- DROP TABLE IF EXISTS `sys_menu`; CREATE TABLE `sys_menu` ( `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', `menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称', `parent_id` bigint(20) NULL DEFAULT 0 COMMENT '父类ID', `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '路由地址', `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件路径', `menu_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '菜单类型( M目录 C菜单 F按钮)', `visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)', `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '锁定状态(0正常 1停用)', `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限标识', `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '菜单图标', `created_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`menu_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for sys_role 角色信息表 -- ---------------------------- DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID', `role_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名称', `role_key` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_german2_ci NOT NULL COMMENT '角色权限字符串', `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '角色状态(0正常 1停用)', `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', `created_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_by` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`role_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for sys_role_menu 角色权限关联表 -- ---------------------------- DROP TABLE IF EXISTS `sys_role_menu`; CREATE TABLE `sys_role_menu` ( `role_id` bigint(20) NOT NULL COMMENT '角色ID', `menu_id` bigint(20) NOT NULL COMMENT '菜单ID', PRIMARY KEY (`role_id`, `menu_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色权限关联表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for sys_user_role 用户角色关联表 -- ---------------------------- DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `user_id` bigint(20) NOT NULL COMMENT '用户ID', `role_id` bigint(20) NOT NULL COMMENT '角色ID', PRIMARY KEY (`user_id`, `role_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户角色关联表' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1; ``` ### 5.3Menu实体类 ```java import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @AllArgsConstructor @NoArgsConstructor @TableName("sys_menu") public class Menu implements Serializable { private static final long serialVersionUID = 1L; @TableId("menu_id") private String id; private String menuName; private String parentId; private String path; private String component; private String menuType; private String visible; private String status; private String perms; private String icon; @TableField("create_by") private String createBy; @TableField("create_time") private String createTime; @TableField("update_by") private String updateBy; @TableField("update_time") private String updateTime; } ``` ### 5.4Service ```java import java.util.List; public interface MenuService { /** * 根据用户id查询限权 */ List selectMenuById(String id); } ``` 实现类 ```java import com.htu.springsecurityfast.mapper.MenuMapper; import com.htu.springsecurityfast.service.MenuService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class MenuServiceImpl implements MenuService { @Autowired private MenuMapper menuMapper; /** * 根据用户id查询限权 * @param id 用户id * @return 限权集合 */ @Override public List selectMenuById(String id) { return menuMapper.selectMenuById(id); } } ``` ### 5.5Mapper 1. 编写MenuMapper,数据层,查询数据库中的权限。 ```java import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.htu.springsecurityfast.domain.Menu; import java.util.List; public interface MenuMapper extends BaseMapper { List selectMenuById(String id); } ``` 2. Mapper映射文件 ~~~xml -- 这里改成自己的包 ~~~ ### 5.6LoginUser新增字段 ~~~java import com.alibaba.fastjson.annotation.JSONField; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Data @NoArgsConstructor //我们自己写了含参构造要去掉@AllArgsConstructor public class LoginUser implements UserDetails, Serializable { private static final long serialVersionUID = 1L; private User user; //--------------------------这里是新加的--------------------------------- private List permissions; //---------------------------------------------------------------------- //--------------------------这里是新加的--------------------------------- @JSONField(serialize = false) @JsonIgnore private List authorities; //---------------------------------------------------------------------- public LoginUser(User user) { this.user = user; } //--------------------------这里是新加的--------------------------------- //自己定义含参构造 public LoginUser(User user, List permissions) { this.user = user; this.permissions = permissions; } //---------------------------------------------------------------------- @Override public Collection getAuthorities() { //--------------------------这里是新加的--------------------------------- //由return null改造成这样 if (authorities!= null) { return authorities; } authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); return authorities; //---------------------------------------------------------------------- } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override @JSONField(serialize = false) @JsonIgnore public boolean isAccountNonExpired() { return true; } @Override @JSONField(serialize = false) @JsonIgnore public boolean isAccountNonLocked() { return true; } @Override @JSONField(serialize = false) @JsonIgnore public boolean isCredentialsNonExpired() { return true; } @Override @JSONField(serialize = false) @JsonIgnore public boolean isEnabled() { return true; } } ~~~ ### 5.7查询权限信息封装进登录用户 Spring Security和数据库有交互的是那一部分?还记得么? 我们在对比用户名的时候重写UserDetailsService接口中的loadUserByUsername方法去数据库中对比。因此还在我们自己的实现类中去添加查询权限的逻辑。 ```java import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; import com.htu.springsecurityfast.domain.LoginUser; import com.htu.springsecurityfast.domain.User; import com.htu.springsecurityfast.mapper.UserMapper; import com.htu.springsecurityfast.service.MenuService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; //--------------------------这里是新加的--------------------------------- @Autowired private MenuService menuService; //---------------------------------------------------------------------- @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1.根据用户名查询用户信息 User user = userMapper.selectOne(new LambdaQueryWrapper().eq(User::getUserName, username).eq(User::getStatus, 0)); if (ObjectUtils.isEmpty(user)){ throw new UsernameNotFoundException("用户名不存在"); } //--------------------------这里是新加的--------------------------------- // 2.根据用户id查询权限信息 List menus = menuService.selectMenuById(user.getId()); //---------------------------------------------------------------------- //3.封装用户信息进UserDetails中,传递给SpringSecurity进行使用 //这里一点小改动,new LoginUser(user);这样只封装进去了用户信息,没有封装进去权限信息,换一下构造方法即可,5.6已经改造过可以接收权限参数 return new LoginUser(user,menus); } } ``` ## 1.6鉴权 在上面我们已经查询用户所拥有的权限并封装到Login User传递给Spring Security上下文中。现在只需要给**接口加上要鉴定的权限**即可。这里采用**注解方式**加。 ### 6.1开启注解鉴权 ```java @SpringBootApplication @MapperScan("com.htu.springsecurityfast.mapper") //在启动类上加上这个,开启注解鉴权 @EnableGlobalMethodSecurity(prePostEnabled = true) public class SpringSecurityFastApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityFastApplication.class, args); } } ``` ### 6.2接口添加鉴权字段 ```java //@PreAuthorize访问之前先鉴权。()中其实是true和false。hasAuthority()返回的就是true或false。 //整个流程下来是这样的,访问接口前(PreAuthorize)先看看你有没有所要求的权限(hasAuthority),有可以访问,没有就权限不足 @PreAuthorize("hasAuthority('sys:data:run111')") @GetMapping("/hello") public String hello() { return "Hello World!"; } ```