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