springSecurity + jwt + redis 前后端分离用户认证和授权

springSecurity + jwt + redis 前后端分离用户认证和授权

记录一下使用springSecurity搭建用户认证和授权的代码、、、

技术栈使用springSecurity + redis + JWT + mybatisPlus

部分代码来自:https://blog.csdn.net/I_am_Hutengfei/article/details/100561564

零. 共用工具类和sql

1.工具类

@Data
public class JsonResult<T> implements Serializable {
   private Boolean success;
   private Integer errorCode;
   private String errorMsg;
   private T data;

   public JsonResult() {
  }

   public JsonResult(Boolean success, Integer errorCode, String errorMsg, T data) {
       this.success = success;
       this.errorCode = errorCode;
       this.errorMsg = errorMsg;
       this.data = data;
  }

   // 成功或者失败都能走这个
   public JsonResult(boolean success) {
       this.success = success;
       this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
       this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
  }

   // 成功或者失败都能走这个,并且可以传一个枚举来改变默认枚举的值
   public JsonResult(boolean success, ResultCode resultEnum) {
       this.success = success;
       // 传来的枚举为null就用默认的,不为null就用传来的枚举
       this.errorCode = success ? (resultEnum==null?ResultCode.SUCCESS.getCode():resultEnum.getCode()) : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
       this.errorMsg = success ? (resultEnum==null?ResultCode.SUCCESS.getMessage():resultEnum.getMessage()): (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
  }

   // 成功或者失败都能用
   // 用户可以传一个任意对象过来,用默认的成功或者失败的枚举
   public JsonResult(boolean success, T data) {
       this.success = success;
       this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
       this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
       this.data = data;
  }

   // 成功或者失败都能用
   // 用户可以传一个任意对象和自定义枚举过来
   public JsonResult(boolean success, ResultCode resultEnum, T data) {
       this.success = success;
       this.errorCode = success ? (resultEnum==null ? ResultCode.SUCCESS.getCode() : resultEnum.getCode()): (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
       this.errorMsg = success ? (resultEnum==null ? ResultCode.SUCCESS.getMessage() : resultEnum.getMessage()) : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
       this.data = data;
  }

   public static JsonResult success() {
       return new JsonResult(true);
  }

   public static JsonResult success(ResultCode resultEnum) {
       return new JsonResult(true,resultEnum);
  }

   public static <T> JsonResult<T> success(T data) {
       return new JsonResult(true, data);
  }

   public static <T> JsonResult<T> success(ResultCode resultEnum,T data){
       return new JsonResult<>(true,resultEnum,data);
  }

   public static JsonResult fail() {
       return new JsonResult(false);
  }

   public static <T> JsonResult fail(T data) {
       return new JsonResult(false, data);
  }

   public static JsonResult fail(ResultCode resultEnum) {
       return new JsonResult(false, resultEnum);
  }

   public static <T> JsonResult<T> fail(ResultCode resultEnum, T data) {
       return new JsonResult(false, resultEnum, data);
  }
}
public enum ResultCode {
   /* 成功 */
   SUCCESS(200, "请求成功"),
   SUCCESS_login(200, "用户登录成功"),
   SUCCESS_logout(200, "用户退出成功"),

   /* 默认失败 */
   COMMON_FAIL(999, "失败"),

   /* 参数错误:1000~1999 */
   PARAM_NOT_VALID(1001, "参数无效"),
   PARAM_IS_BLANK(1002, "参数为空"),
   PARAM_TYPE_ERROR(1003, "参数类型错误"),
   PARAM_NOT_COMPLETE(1004, "参数缺失"),

   /* 用户错误 */
   USER_FAIL_LOGIN(2000, "登陆失败"),
   USER_NOT_LOGIN(2001, "用户未登录"),
   USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
   USER_CREDENTIALS_ERROR(2003, "密码错误"),
   USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
   USER_ACCOUNT_DISABLE(2005, "账号不可用"),
   USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
   USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
   USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
   USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),

   /* 业务错误 */
   NO_PERMISSION(3001, "当前账号没有此权限");
   private Integer code;
   private String message;

   ResultCode(Integer code, String message) {
       this.code = code;
       this.message = message;
  }

   public Integer getCode() {
       return code;
  }

   public void setCode(Integer code) {
       this.code = code;
  }

   public String getMessage() {
       return message;
  }

   public void setMessage(String message) {
       this.message = message;
  }

   /**
    * 根据code获取message
    *
    * @param code
    * @return
    */
   public static String getMessageByCode(Integer code) {
       for (ResultCode ele : values()) {
           if (ele.getCode().equals(code)) {
               return ele.getMessage();
          }
      }
       return null;
  }
}

2.mybatisplus和sql

//mybatisPlus自动生成代码使用
public class CreateCode {
   public static void main(String[] args) {
       FastAutoGenerator.create("jdbc:mysql://localhost:3306/security", "root", "123456")
              .globalConfig(builder -> {
                   builder.author("bao") // 设置作者
                          .enableSwagger() // 开启 swagger 模式
                          .fileOverride() // 覆盖已生成文件
                          .outputDir("D://user"); // 指定输出目录
              })
              .packageConfig(builder -> {
                   builder.parent("com.example.alipay.user") // 设置父包名
                          .moduleName("") // 设置父包模块名
                          .pathInfo(Collections.singletonMap(OutputFile.xml, "D://user")); // 设置mapperXml生成路径
              })
              .packageConfig(builder -> {
                   builder.parent("com.example.alipay.user") // 设置父包名
                          .moduleName("") // 设置父包模块名
                          .pathInfo(Collections.singletonMap(OutputFile.controller, "D://user")); // 设置mapperXml生成路径
              })
              .packageConfig(builder -> {
                   builder.parent("com.example.alipay.user") // 设置父包名
                          .moduleName("") // 设置父包模块名
                          .pathInfo(Collections.singletonMap(OutputFile.entity, "D://user")); // 设置mapperXml生成路径
              })
              .packageConfig(builder -> {
                   builder.parent("com.example.alipay.user") // 设置父包名
                          .moduleName("") // 设置父包模块名
                          .pathInfo(Collections.singletonMap(OutputFile.mapper, "D://user")); // 设置mapperXml生成路径
              })
              .packageConfig(builder -> {
                   builder.parent("com.example.alipay.user") // 设置父包名
                          .moduleName("") // 设置父包模块名
                          .pathInfo(Collections.singletonMap(OutputFile.service, "D://")); // 设置mapperXml生成路径
              })
              .packageConfig(builder -> {
                   builder.parent("com.example.alipay.user") // 设置父包名
                          .moduleName("") // 设置父包模块名
                          .pathInfo(Collections.singletonMap(OutputFile.serviceImpl, "D://")); // 设置mapperXml生成路径
              })
              .strategyConfig(builder -> {
                   builder.addInclude("sys_user") // 设置需要生成的表名
                          .addTablePrefix("t_", "c_"); // 设置过滤表前缀
              })
              .strategyConfig(builder -> {
                   builder.addInclude("sys_role") // 设置需要生成的表名
                          .addTablePrefix("t_", "c_"); // 设置过滤表前缀
              })
              .strategyConfig(builder -> {
                   builder.addInclude("sys_permission") // 设置需要生成的表名
                          .addTablePrefix("t_", "c_"); // 设置过滤表前缀
              })
              .strategyConfig(builder -> {
                   builder.addInclude("sys_request_path") // 设置需要生成的表名
                          .addTablePrefix("t_", "c_"); // 设置过滤表前缀
              })
              .strategyConfig(builder -> {
                   builder.addInclude("sys_request_path_permission_relation") // 设置需要生成的表名
                          .addTablePrefix("t_", "c_"); // 设置过滤表前缀
              })
              .strategyConfig(builder -> {
                   builder.addInclude("sys_role_permission_relation") // 设置需要生成的表名
                          .addTablePrefix("t_", "c_"); // 设置过滤表前缀
              })
              .strategyConfig(builder -> {
                   builder.addInclude("sys_user_role_relation") // 设置需要生成的表名
                          .addTablePrefix("t_", "c_"); // 设置过滤表前缀
              })
              .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
              .execute();

  }
}

sql

/*
Navicat Premium Data Transfer

Source Server         : localhost_3306
Source Server Type   : MySQL
Source Server Version : 50719
Source Host           : localhost:3306
Source Schema         : security

Target Server Type   : MySQL
Target Server Version : 50719
File Encoding         : 65001

Date: 27/05/2022 15:05:34
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT "主键id",
 `permission_code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT "权限code",
 `permission_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT "权限名",
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = "权限表" ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, "sys:user:list", "用户查询");
INSERT INTO `sys_permission` VALUES (2, "sys:user:delete", "用户删除");
INSERT INTO `sys_permission` VALUES (3, "sys:user:online", "在线用户查询");

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT "主键id",
 `role_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT "角色值",
 `role_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT "角色名",
 `role_description` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT "角色说明",
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = "用户角色表" ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, "admin", "管理员", "管理员,拥有所有权限");
INSERT INTO `sys_role` VALUES (2, "user", "普通用户", "普通用户,拥有部分权限");

-- ----------------------------
-- Table structure for sys_role_permission_relation
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission_relation`;
CREATE TABLE `sys_role_permission_relation`  (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT "主键id",
 `role_id` int(11) NULL DEFAULT NULL COMMENT "角色id",
 `permission_id` int(11) NULL DEFAULT NULL COMMENT "权限id",
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = "角色-权限关联关系表" ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_role_permission_relation
-- ----------------------------
INSERT INTO `sys_role_permission_relation` VALUES (1, 1, 1);
INSERT INTO `sys_role_permission_relation` VALUES (7, 1, 2);
INSERT INTO `sys_role_permission_relation` VALUES (8, 1, 3);
INSERT INTO `sys_role_permission_relation` VALUES (9, 2, 1);

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
 `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
 `user_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT "账号",
 `nick_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT "用户名",
 `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT "用户密码",
 `last_login_time` datetime NULL DEFAULT NULL COMMENT "最后一次登录时间",
 `enabled` tinyint(1) NULL DEFAULT 1 COMMENT "账号是否可用。默认为1(可用)",
 `not_expired` tinyint(1) NULL DEFAULT 1 COMMENT "是否过期。默认为1(没有过期)",
 `account_not_locked` tinyint(1) NULL DEFAULT 1 COMMENT "账号是否锁定。默认为1(没有锁定)",
 `credentials_not_expired` tinyint(1) NULL DEFAULT 1 COMMENT "证书(密码)是否过期。默认为1(没有过期)",
 `create_time` datetime NULL DEFAULT NULL COMMENT "创建时间",
 `update_time` datetime NULL DEFAULT NULL COMMENT "修改时间",
 `create_user` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT "创建人",
 `update_user` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT "修改人",
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = "用户表" ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ("4044aaa1-dca1-11ec-81da-00ff8ecb7c92", "admin", "admin", "$2a$10$fdaPecPEB3jJQCR4/E2lz.Gg6cnXLlsR1Qa2C4r7yO.hPjTOMr/he", "2022-05-27 14:18:28", 1, 1, 1, 1, "2019-08-29 06:29:24", "2022-05-26 14:13:09", "", "");
INSERT INTO `sys_user` VALUES ("63561932-dca1-11ec-81da-00ff8ecb7c92", "user001", "user001", "$2a$10$fdaPecPEB3jJQCR4/E2lz.Gg6cnXLlsR1Qa2C4r7yO.hPjTOMr/he", "2022-05-27 10:45:30", 1, 1, 1, 1, NULL, "2022-05-25 13:16:30", "admin", "admin");

-- ----------------------------
-- Table structure for sys_user_role_relation
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role_relation`;
CREATE TABLE `sys_user_role_relation`  (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT "主键id",
 `user_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT "用户id",
 `role_id` int(11) NULL DEFAULT NULL COMMENT "角色id",
 PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = "用户角色关联关系表" ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_user_role_relation
-- ----------------------------
INSERT INTO `sys_user_role_relation` VALUES (1, "4044aaa1-dca1-11ec-81da-00ff8ecb7c92", 1);
INSERT INTO `sys_user_role_relation` VALUES (2, "63561932-dca1-11ec-81da-00ff8ecb7c92", 2);

SET FOREIGN_KEY_CHECKS = 1;

一.redis配置

使用redis来保存登陆用户的信息,一来防止验证重复查询数据库,二来后期可以在此基础上做在线用户查询,强退用户等操作。

1. redis工具类(来自若依框架)

/**
* spring redis 工具类
*
* @author ruoyi
**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
   @Autowired
   public RedisTemplate redisTemplate;

   /**
    * 缓存基本的对象,Integer、String、实体类等
    *
    * @param key 缓存的键值
    * @param value 缓存的值
    */
   public <T> void setCacheObject(final String key, final T value)
  {
       redisTemplate.opsForValue().set(key, value);
  }

   /**
    * 缓存基本的对象,Integer、String、实体类等
    *
    * @param key 缓存的键值
    * @param value 缓存的值
    * @param timeout 时间
    * @param timeUnit 时间颗粒度
    */
   public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
  {
       redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
  }

   /**
    * 设置有效时间
    *
    * @param key Redis键
    * @param timeout 超时时间
    * @return true=设置成功;false=设置失败
    */
   public boolean expire(final String key, final long timeout)
  {
       return expire(key, timeout, TimeUnit.SECONDS);
  }

   /**
    * 设置有效时间
    *
    * @param key Redis键
    * @param timeout 超时时间
    * @param unit 时间单位
    * @return true=设置成功;false=设置失败
    */
   public boolean expire(final String key, final long timeout, final TimeUnit unit)
  {
       return redisTemplate.expire(key, timeout, unit);
  }

   /**
    * 获得缓存的基本对象。
    *
    * @param key 缓存键值
    * @return 缓存键值对应的数据
    */
   public <T> T getCacheObject(final String key)
  {
       ValueOperations<String, T> operation = redisTemplate.opsForValue();
       return operation.get(key);
  }

   /**
    * 删除单个对象
    *
    * @param key
    */
   public boolean deleteObject(final String key)
  {
       return redisTemplate.delete(key);
  }

   /**
    * 删除集合对象
    *
    * @param collection 多个对象
    * @return
    */
   public long deleteObject(final Collection collection)
  {
       return redisTemplate.delete(collection);
  }

   /**
    * 缓存List数据
    *
    * @param key 缓存的键值
    * @param dataList 待缓存的List数据
    * @return 缓存的对象
    */
   public <T> long setCacheList(final String key, final List<T> dataList)
  {
       Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
       return count == null ? 0 : count;
  }

   /**
    * 获得缓存的list对象
    *
    * @param key 缓存的键值
    * @return 缓存键值对应的数据
    */
   public <T> List<T> getCacheList(final String key)
  {
       return redisTemplate.opsForList().range(key, 0, -1);
  }

   /**
    * 缓存Set
    *
    * @param key 缓存键值
    * @param dataSet 缓存的数据
    * @return 缓存数据的对象
    */
   public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
  {
       BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
       Iterator<T> it = dataSet.iterator();
       while (it.hasNext())
      {
           setOperation.add(it.next());
      }
       return setOperation;
  }

   /**
    * 获得缓存的set
    *
    * @param key
    * @return
    */
   public <T> Set<T> getCacheSet(final String key)
  {
       return redisTemplate.opsForSet().members(key);
  }

   /**
    * 缓存Map
    *
    * @param key
    * @param dataMap
    */
   public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
  {
       if (dataMap != null) {
           redisTemplate.opsForHash().putAll(key, dataMap);
      }
  }

   /**
    * 获得缓存的Map
    *
    * @param key
    * @return
    */
   public <T> Map<String, T> getCacheMap(final String key)
  {
       return redisTemplate.opsForHash().entries(key);
  }

   /**
    * 往Hash中存入数据
    *
    * @param key Redis键
    * @param hKey Hash键
    * @param value 值
    */
   public <T> void setCacheMapValue(final String key, final String hKey, final T value)
  {
       redisTemplate.opsForHash().put(key, hKey, value);
  }

   /**
    * 获取Hash中的数据
    *
    * @param key Redis键
    * @param hKey Hash键
    * @return Hash中的对象
    */
   public <T> T getCacheMapValue(final String key, final String hKey)
  {
       HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
       return opsForHash.get(key, hKey);
  }

   /**
    * 删除Hash中的数据
    *
    * @param key
    * @param mapkey
    */
   public void delCacheMapValue(final String key, final String hkey)
  {
       HashOperations hashOperations = redisTemplate.opsForHash();
       hashOperations.delete(key, hkey);
  }

   /**
    * 获取多个Hash中的数据
    *
    * @param key Redis键
    * @param hKeys Hash键集合
    * @return Hash对象集合
    */
   public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
  {
       return redisTemplate.opsForHash().multiGet(key, hKeys);
  }

   /**
    * 获得缓存的基本对象列表
    *
    * @param pattern 字符串前缀
    * @return 对象列表
    */
   public Collection<String> keys(final String pattern)
  {
       return redisTemplate.keys(pattern);
  }
}

2. redis序列化与反序列化配置

这里不做序列化,保存的redis信息在可视化工具中将是一堆乱码,后期在通过redisTemplate.keys(pattern)获取key值时会获取不到。

/**
* @descriptions: fastjson自定义序列化配置
* @author: bao
* @date: 2022/5/27 11:18
* @version: 1.0
*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
   private ObjectMapper objectMapper = new ObjectMapper();
   public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

   private Class<T> clazz;

   static {
       ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
       //如果遇到反序列化autoType is not support错误,请添加并修改一下包名到bean文件路径
//         ParserConfig.getGlobalInstance().addAccept("org.springframework.security.core.authority");
  }
   public FastJson2JsonRedisSerializer(Class<T> clazz) {
       super();
       this.clazz = clazz;
  }

   public byte[] serialize(T t) throws SerializationException {
       if (t == null) {
           return new byte[0];
      }
       return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
  }

   public T deserialize(byte[] bytes) throws SerializationException {
       if (bytes == null || bytes.length <= 0) {
           return null;
      }
       String str = new String(bytes, DEFAULT_CHARSET);

       return JSON.parseObject(str, clazz);
  }
   public void setObjectMapper(ObjectMapper objectMapper) {
       Assert.notNull(objectMapper, ""objectMapper" must not be null");
       this.objectMapper = objectMapper;
  }

   protected JavaType getJavaType(Class<?> clazz) {
       return TypeFactory.defaultInstance().constructType(clazz);
  }

}
/**
* @descriptions: redis序列化,redisTemplate配置
* @author: bao
* @date: 2022/5/26 13:26
* @version: 1.0
*/
@Configuration
public class RedisConfig {
   @Bean
   public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
       RedisTemplate<Object, Object> template = new RedisTemplate<>();
       template.setConnectionFactory(redisConnectionFactory);
       //使用Fastjson2JsonRedisSerializer来序列化和反序列化redis的value值 by zhengkai
       FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

       ObjectMapper mapper = new ObjectMapper();
       mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
       mapper.activateDefaultTyping(new DefaultBaseTypeLimitingValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
       serializer.setObjectMapper(mapper);

       template.setValueSerializer(serializer);
       //使用StringRedisSerializer来序列化和反序列化redis的key值
       template.setKeySerializer(new StringRedisSerializer());
       template.afterPropertiesSet();
       return template;
  }
}

3.redis缓存常量

由于没写别的功能,目前只有一个常量
public class CacheConst {
  public static String LOGIN_USER_KEY = "loginUser:";
}

二 . JWT配置

1.工具类

public class JwtUtils {
   //token过期时间设置24小时
   public static final long EXPIRE = 1000 * 60 * 60 * 24;
   //密钥
   public static final String APP_SECRET = "sdfkdjAYANMENGod33ksfdkds";


   /**
    * @descriptions 保存用户所有信息
    * @author bao
    * @date 2022/5/25 15:49
    * @return java.lang.String
    */
   public static String getJwtToken(String loginId){

       String JwtToken = Jwts.builder()
              .setHeaderParam("typ", "JWT")
              .setHeaderParam("alg", "HS256")
              .setSubject("jacob-user")
              .setIssuedAt(new Date())
              .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
              .claim("loginId", loginId)
              .signWith(SignatureAlgorithm.HS256, APP_SECRET)
              .compact();

       return JwtToken;
  }



   /**
    * 根据token,判断token是否存在与有效
    * @param jwtToken
    * @return
    */
   public static boolean checkToken(String jwtToken) {
       if(StringUtils.isEmpty(jwtToken)) return false;
       try {
           Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
      } catch (Exception e) {
           e.printStackTrace();
           return false;
      }
       return true;
  }

   /**
    * 根据request判断token是否存在与有效(也就是把token取出来罢了)
    * @param request
    * @return
    */
   public static boolean checkToken(HttpServletRequest request) {
       try {
           String jwtToken = request.getHeader("UserToken");
           if(StringUtils.isEmpty(jwtToken)) return false;
           Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
      } catch (Exception e) {
           e.printStackTrace();
           return false;
      }
       return true;
  }

   /**
    * 根据token获取用户的id
    * @param request
    * @return
    */
   public static String getUseridByJwtToken(HttpServletRequest request) {
       String jwtToken = request.getHeader("UserToken");
       if(StringUtils.isEmpty(jwtToken)) return null;
       try{
           Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
           Claims claims = claimsJws.getBody();
           return (String) claims.get("loginId");
      }catch (Exception e){
           e.printStackTrace();
           return null;
      }
  }
}

2. jwt过滤器

* 继承了OncePerRequestFilter抽象类,该抽象类在每次请求时只执行一次过滤,即它的作用就是保证一次请求只通过一次filter,
* 重写其doFilterInternal方法来自定义处理逻辑
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
   /**
    * 直接将我们前面写好的service注入进来,通过service获取到当前用户的权限
    * */
   @Autowired
   private RedisCache redisCache;

   @Override
   protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
       // 验证当前token是否有效
       if (JwtUtils.checkToken(httpServletRequest)) {
           // 获取到当前用户的account
           String id = JwtUtils.getUseridByJwtToken(httpServletRequest);
           //根据id从redis中获取登录用户信息
           ParserConfig.getGlobalInstance().addAccept("org.springframework.security.core.authority.SimpleGrantedAuthority.");
           LoginUser userDetails = redisCache.getCacheObject(CacheConst.LOGIN_USER_KEY + id);

           // token中username不为空,并且Context中的认证为空,进行token验证
           if (!Objects.isNull(userDetails) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
               System.out.println("自定义JWT过滤器获得用户名为-" + userDetails.getUsername());
               UsernamePasswordAuthenticationToken authentication =
                       new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
               authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
               //将authentication放入SecurityContextHolder中
               SecurityContextHolder.getContext().setAuthentication(authentication);
          }
      }
       // 放行给下个过滤器
       filterChain.doFilter(httpServletRequest, httpServletResponse);
  }
}

三. springSecurity配置

1.自定义UserDetails实现类

这个类里面的 Collection<? extends GrantedAuthority> authorities,在反序列化时会失败,上面的fastjson自定义序列化方式,主要是为了解决这个问题。

public class LoginUser implements Serializable, UserDetails{

   private static final long serialVersionUID = 1L;
   private SysUser sysUser;
   private Collection<? extends GrantedAuthority> authorities;
   public LoginUser() {
  }

   public LoginUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
       Assert.isTrue(sysUser.getUserName() != null && !"".equals(sysUser.getUserName()) && sysUser.getPassword() != null,
               "Cannot pass null or empty values to constructor");
       this.sysUser = sysUser;
       this.authorities = Collections.unmodifiableSet((Set<? extends GrantedAuthority>) authorities);
  }

   @Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
       return this.authorities;
  }

   @Override
   public String getPassword() {
       return this.sysUser.getPassword();
  }

   @Override
   public String getUsername() {
       return this.sysUser.getUserName();
  }

   @Override
   public boolean isAccountNonExpired() {
       return this.sysUser.getCredentialsNotExpired();
  }

   @Override
   public boolean isAccountNonLocked() {
       return this.sysUser.getAccountNotLocked();
  }

   @Override
   public boolean isCredentialsNonExpired() {
       return this.sysUser.getCredentialsNotExpired();
  }

   @Override
   public boolean isEnabled() {
       return this.sysUser.getEnabled();
  }

   public SysUser getSysUser() {
       return sysUser;
  }

   public void setSysUser(SysUser sysUser) {
       this.sysUser = sysUser;
  }

   public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
       this.authorities = authorities;
  }
}

2.自定义UserDetailsService实现类

在调用登录接口(这里用的security默认登录接口/login),会自动执行UserDetailsService的loadUserByUsername,返回一个userdetails,也就是上面的自定义实现类。

在这个方法中要配置用户信息,和用户权限。

@Service
public class SecurityUserService implements UserDetailsService {

   @Autowired
   private SysUserMapper sysUserMapper;
   @Autowired
   private SysPermissionMapper sysPermissionMapper;

   /**
    * 根据用户名查找数据库,判断是否存在这个用户
    *
    * @return*/
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

       // 用户名必须是唯一的,不允许重复
       SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper<SysUser>().eq("user_name",username));
//获取用户权限
       List<SysPermission> sysPermissions = sysPermissionMapper.getUserRolesByUserId(sysUser.getId());
       //将用户权限存入Userdetails的authorities中,以便security做权限判断
       Set<GrantedAuthority> grantedAuthorities = new LinkedHashSet<>();
       sysPermissions.stream().forEach(sysPermission -> {
           GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(sysPermission.getPermissionCode());
           grantedAuthorities.add(grantedAuthority);
      });

       LoginUser loginUser = new LoginUser(sysUser, grantedAuthorities);
       return loginUser;
  }
}

这里要用到的一个sql

sysPermissionMapper.getUserRolesByUserId(sysUser.getId());


   <select id="getUserRolesByUserId" resultType="com.bao.xinghe.user.entity.SysPermission">
      SELECT
          p.*
      FROM
          sys_user AS u
              LEFT JOIN sys_user_role_relation AS ur
                        ON u.id = ur.user_id
              LEFT JOIN sys_role AS r
                        ON r.id = ur.role_id
              LEFT JOIN sys_role_permission_relation AS rp
                        ON r.id = rp.role_id
              LEFT JOIN sys_permission AS p
                        ON p.id = rp.permission_id
      WHERE u.id = #{userId}
   </select>

3.一些要用到的异常过滤器

登陆成功过滤器

在这个过滤器中实现一些登陆成功后的业务处理逻辑,我这里包括修改登录用户信息,保存登录缓存。

@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
   @Autowired
   SysUserMapper mapper;
   @Autowired
   RedisCache redisCache;

   @Override
   public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
       //更新用户表上次登录时间、更新人、更新时间等字段
       LoginUser userDetails = (LoginUser) authentication.getPrincipal();
       SysUser sysUser = userDetails.getSysUser();
       sysUser.setLastLoginTime(new Date());
       mapper.update(sysUser,new QueryWrapper<SysUser>().eq("id",sysUser.getId()));

       //登陆成功用户信息保存redis、登陆有效时长1小时
       String id = sysUser.getId();
       redisCache.setCacheObject(CacheConst.LOGIN_USER_KEY + id, userDetails, 60, TimeUnit.MINUTES);
       // 根据用户的id生成token并返回
       String jwtToken = JwtUtils.getJwtToken(id);
       Map<String,String> results = new HashMap<>();
       results.put("token",jwtToken);

       //返回json数据
       JsonResult result = JsonResult.success(ResultCode.SUCCESS_login,results);
       //处理编码方式,防止中文乱码的情况
       httpServletResponse.setContentType("text/json;charset=utf-8");
       // 把Json数据放入HttpServletResponse中返回给前台
       httpServletResponse.getWriter().write(JSON.toJSONString(result));
  }
}

登陆失败过滤器


@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {

   @Override
   public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
       //返回json数据
       JsonResult result = null;
       if (e instanceof AccountExpiredException) {
           //账号过期
           result = JsonResult.fail(ResultCode.USER_ACCOUNT_EXPIRED);
      } else if (e instanceof BadCredentialsException) {
           //密码错误
           result = JsonResult.fail(ResultCode.USER_CREDENTIALS_ERROR);
      } else if (e instanceof CredentialsExpiredException) {
           //密码过期
           result = JsonResult.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
      } else if (e instanceof DisabledException) {
           //账号不可用
           result = JsonResult.fail(ResultCode.USER_ACCOUNT_DISABLE);
      } else if (e instanceof LockedException) {
           //账号锁定
           result = JsonResult.fail(ResultCode.USER_ACCOUNT_LOCKED);
      } else if (e instanceof InternalAuthenticationServiceException) {
           //用户不存在
           result = JsonResult.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
      }else{
           //其他错误
           result = JsonResult.fail(ResultCode.COMMON_FAIL);
      }
       //处理编码方式,防止中文乱码的情况
       httpServletResponse.setContentType("text/json;charset=utf-8");
       // 把Json数据放入到HttpServletResponse中返回给前台
       httpServletResponse.getWriter().write(JSON.toJSONString(result));
  }
}

未登录处理

@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {

   // 返回的是Json数据
   @Override
   public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
       JsonResult result = JsonResult.fail(ResultCode.USER_NOT_LOGIN);
       httpServletResponse.setContentType("text/json;charset=utf-8");
       httpServletResponse.getWriter().write(JSON.toJSONString(result));
  }
}

无权限处理

@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
   @Override
   public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
       JsonResult noPermission = JsonResult.fail(ResultCode.NO_PERMISSION);
       //处理编码方式,防止中文乱码的情况
       httpServletResponse.setContentType("text/json;charset=utf-8");
       // 把Json数据放到HttpServletResponse中返回给前台
       httpServletResponse.getWriter().write(JSON.toJSONString(noPermission));
  }
}

登出处理

登出时做删除redis缓存操作

@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
   @Autowired
   RedisCache redisCache;
   @Override
   public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
       //退出成功删除redis缓存
       LoginUser userDetails = (LoginUser) authentication.getPrincipal();
       redisCache.deleteObject(CacheConst.LOGIN_USER_KEY + userDetails.getSysUser().getId());
       JsonResult result = JsonResult.success(ResultCode.SUCCESS_logout);
       httpServletResponse.setContentType("text/json;charset=utf-8");
       httpServletResponse.getWriter().write(JSON.toJSONString(result));
  }
}

4.核心配置类

自定义SecurityConfig继承WebSecurityConfigurerAdapter

重写configure方法对security进行核心配置

之前写的所有过滤器都要在这里配置进去。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //此注解用于开启controller接口注解权限认证
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private SecurityUserService securityUserService;

  @Autowired
  private AuthenticationEntryPoint authenticationEntryPoint;

  @Autowired
  private AuthenticationFailureHandler authenticationFailureHandler;

  @Autowired
  private LogoutSuccessHandler logoutSuccessHandler;

  @Autowired
  private AuthenticationSuccessHandler authenticationSuccessHandler;

  @Autowired
  private CustomizeAccessDeniedHandler customizeAccessDeniedHandler;

  @Autowired
  private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

  /**
    * 对请求进行鉴权的配置
    *
    * @param http
    * @throws Exception
    */
  @Override
  protected void configure(HttpSecurity http) throws Exception {
//关闭cors
      http.cors()
              .and().csrf().disable();

      /**
        * anyRequest         |   匹配所有请求路径
        * access             |   SpringEl表达式结果为true时可以访问
        * anonymous           |   匿名可以访问
        * denyAll             |   用户不能访问
        * fullyAuthenticated |   用户完全认证可以访问(非remember-me下自动登录)
        * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
        * hasAnyRole         |   如果有参数,参数表示角色,则其中任何一个角色可以访问
        * hasAuthority       |   如果有参数,参数表示权限,则其权限可以访问
        * hasIpAddress       |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
        * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
        * permitAll           |   用户可以任意访问
        * rememberMe         |   允许通过remember-me登录的用户访问
        * authenticated       |   用户登录后可访问
        */
      http.authorizeRequests()
              .antMatchers("/sysUser/signUp").permitAll() //这是测试代码。。注册用户
              .anyRequest().authenticated()
          .and()
              .exceptionHandling()
              .authenticationEntryPoint(authenticationEntryPoint)
              .accessDeniedHandler(customizeAccessDeniedHandler)
          .and()
              .formLogin() // 登录
              .permitAll() //允许所有用户
              .successHandler(authenticationSuccessHandler) //登录成功处理逻辑
              .failureHandler(authenticationFailureHandler) //登录失败处理逻辑
          .and()
              .logout()     // 退出
              .permitAll()   //允许所有用户
              .logoutSuccessHandler(logoutSuccessHandler) //退出成功处理逻辑
              .deleteCookies("JSESSIONID")   //登出之后删除cookie
          .and()
              // 不通过session保存securityContext
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

      http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
      http.addFilterBefore(jwtAuthenticationTokenFilter, LogoutFilter.class);
      http.headers().cacheControl();
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.userDetailsService(securityUserService);
  }

  /**
    * 默认开启密码加密,前端传入的密码Security会在加密后和数据库中的密文进行比对,一致的话就登录成功
    * 所以必须提供一个加密对象,供security加密前端明文密码使用
    * @return
    */
  @Bean
  PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
      return super.authenticationManagerBean();
  }
}

四.测试接口

@RestController
@RequestMapping("/sysUser")
public class SysUserController {

   @Autowired
   SysUserServiceImpl sysUserService;

   @Autowired
   PasswordEncoder passwordEncoder;

   @RequestMapping("getUser")
   @PreAuthorize("hasAuthority("sys:user:list")")
   public JsonResult list(){
       List<SysUser> list = sysUserService.list();
       return new JsonResult(true, list);
  }


   @RequestMapping("deleteUser")
   @PreAuthorize("hasAuthority("sys:user:delete")")
   public JsonResult deleteUser(){
       List<SysUser> list = sysUserService.list();
       return new JsonResult(true, list);
  }


   @RequestMapping("signUp")
   public JsonResult signUp(SysUser sysUser){
       String password = passwordEncoder.encode(sysUser.getPassword());
       sysUser.setPassword(password);
       sysUserService.save(sysUser);
       return new JsonResult(true, sysUser);
  }

   @GetMapping("onlineUser")
   @PreAuthorize("hasAuthority("sys:user:online")")
   public JsonResult onlineUser(){
       List<SysUser> userList = sysUserService.onlineUser();
       if (userList == null){
           JsonResult.success("当前无登录用户");
      }
       return JsonResult.success(userList);
  }

}
    @Override
   public List<SysUser> onlineUser() {
       //获取redis所有在线用户key值、
       Set<String> keys = (Set<String>) redisCache.keys("*" + CacheConst.LOGIN_USER_KEY + "*");
       if (keys == null){
           return null;
      }
       List<SysUser> userList = new ArrayList<>();
       keys.stream().forEach( key -> {
           LoginUser loginUser = redisCache.getCacheObject(key);
           userList.add(loginUser.getSysUser());
      });
       return userList;
  }

 

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » springSecurity + jwt + redis 前后端分离用户认证和授权