SpringBoot整合SpringSecurity
该博文不做原理的讲解,直接提供代码,实现SpringBoot整合SpringSecurity+JWT+Redis前后分离。
一、搭建项目
1、构建springboot项目
如果不会搭建springboot项目,可以参考我下面写的博文,如果会搭建springboot项目,直接跳过这步。
第一节:Idea父子项目创建
第二节:springboot整合Mybatis(入门)
第三节:springboot整合Mybatis(mapper的@Select)
第四节:springboot整合Mybatis(controller+service+mapper)完整过程
第五节:springboot整合Mybatis(声明式事务@Transactional)
2、导入依赖
核心依赖
<!--security 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--redis 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--引入jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
其他依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--简化get set等-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.25</version>
</dependency>
<!-- mybatis 支持 SprigBoot -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- mysql 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<!--阿里巴巴数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<!--MyBatis Plus 的依赖包 ,比如可以直接使用他封装好的sql语句,selectOne等-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>
<!--常用工具类 比如数字处理NumberUtils、字符串处理类StringUtils、日期类DateUtils-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
</dependencies>
3、配置yml
security没有啥配置,主要是配置数据库、redis。mysql和redis就不做安装的教程了,自己去安装下就好,很简单的。
server:
port: 8081
spring:
profiles:
active: dev
datasource:
url: jdbc:mysql://localhost:3306/study-demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# redis配置
redis:
host: localhost
port: 6379
#我没有给redis设置账号和密码
#username: guest
#password: guest
# 日志输出配置
logging:
level:
root: info
#jwt 自定义配置
Jwt:
#jwt签名私钥
secretKey: sdsdsd23232323
4、启动项目
看到输出Using generated security password:xxxx 就是Springboot整合SpringSecurity成功了
访问服务http://127.0.0.1:8081/ 这是我自己的ip和端口。你会看到跳转到SpringSecurity自带的页面了
- 默认账号 user
- 密码就是控制台输出的一串MD5 487eb5c9-0d7e-4a06-b9d5-1c0235b4ca2a
二、建立相关用户角色权限表
我为了方便,就只建了用户表、权限表和用户权限关联表
1、创建数据库的字符集和排序规则
2、用户表结构
CREATE TABLE `sys_user` (
`user_id` int(8) NOT NULL AUTO_INCREMENT,
`account` varchar(32) DEFAULT NULL COMMENT '账号',
`user_name` varchar(32) DEFAULT NULL COMMENT '用户名',
`password` varchar(64) DEFAULT NULL COMMENT '用户密码',
`last_login_time` datetime DEFAULT NULL COMMENT '上一次登录时间',
`enabled` tinyint(1) DEFAULT '1' COMMENT '账号是否可用。默认为1(可用)',
`account_not_expired` tinyint(1) DEFAULT '1' COMMENT '是否过期。默认为1(没有过期)',
`account_not_locked` tinyint(1) DEFAULT '1' COMMENT '账号是否锁定。默认为1(没有锁定)',
`credentials_not_expired` tinyint(1) DEFAULT NULL COMMENT '证书(密码)是否过期。默认为1(没有过期)',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表';
3、权限表结构
CREATE TABLE `sys_permission` (
`permission_id` int(8) NOT NULL,
`permission_code` varchar(32) DEFAULT NULL,
`permission_name` varchar(32) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`permission_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';
4、用户与权限关联表结构
CREATE TABLE `sys_user_permission_relation` (
`user_permission_relation_id` int(8) NOT NULL,
`user_id` int(8) DEFAULT NULL,
`permission_id` int(8) DEFAULT NULL,
PRIMARY KEY (`user_permission_relation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户权限关联表';
5、插入测试数据
INSERT INTO `sys_user` VALUES (1, '888', '小张', '$2a$10$2mO7/KcswzO3SQU7TX3fiOfkypjdOn3tLBezV/tf2IJXdQu1BpxK2', '2023-08-16 09:45:53', 1, 1, 1, 1, '2023-08-09 17:49:20', '2023-08-09 17:49:22');
INSERT INTO `sys_permission` VALUES (1, 'sys:queryUser', '查询用户', '/getUser');
INSERT INTO `sys_user_permission_relation` VALUES (1, 1, 1);
三、创建实体类和Mapper
1、SysUser
用户实体类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)//链式; 存取器。通过该注解可以控制getter和setter方法的形式。
@TableName("sys_user")
public class SysUser implements Serializable {
private static final long serialVersionUID = 915478504870211231L;
@TableId(value = "user_id", type = IdType.ID_WORKER)
private Integer userId;
//账号
private String account;
//用户名
private String userName;
//用户密码
private String password;
//上一次登录时间
private Date lastLoginTime;
//账号是否可用。默认为1(可用)
private Boolean enabled;
//是否过期。默认为1(没有过期)
private Boolean accountNotExpired;
//账号是否锁定。默认为1(没有锁定)
private Boolean accountNotLocked;
//证书(密码)是否过期。默认为1(没有过期)
private Boolean credentialsNotExpired;
//创建时间
private Date createTime;
//修改时间
private Date updateTime;
}
用户mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.study.test.web.entity.SysUser;//这是你自己实体类放的路径,记得修改下
public interface SysUserMapper extends BaseMapper<SysUser> {
}
2、SysPermission
权限实体类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)//链式; 存取器。通过该注解可以控制getter和setter方法的形式。
@TableName("sys_permission")
public class SysPermission implements Serializable {
@TableId(value = "permission_id", type = IdType.ID_WORKER)
private Integer permissionId;
private String permissionCode;
private String permissionName;
private String url;
}
权限mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.study.test.web.entity.SysPermission;//这是你自己实体类放的路径,记得修改下
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface SysPermissionMapper extends BaseMapper<SysPermission> {
/**
* 通过用户id查询用户的权限数据
* @param userId
* @return
*/
@Select({"<script>"+
" SELECT p.* FROM"+
" sys_user u"+
" LEFT JOIN sys_user_permission_relation r ON u.user_id = r.user_id"+
" LEFT JOIN sys_permission p on r.permission_id = p.permission_id"+
" WHERE u.user_id = #{userId}"+
"</script>"
})
List<SysPermission> selectPermissionList(@Param("userId") Integer userId);
}
3、SysUserPermissionRelation
用户与权限关联实体类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)//链式; 存取器。通过该注解可以控制getter和setter方法的形式。
@TableName("sys_user_permission_relation")
public class SysUserPermissionRelation {
@TableId(value = "user_permission_relation_id", type = IdType.ID_WORKER)
private Integer userPermissionRelationId;
private Integer userId;
private Integer permissionId;
}
用户与权限关联mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.study.test.web.entity.SysUserPermissionRelation;//这是你自己实体类放的路径,记得修改下
public interface SysUserPermissionRelationMapper extends BaseMapper<SysUserPermissionRelation> {
}
4、配置@MapperScan
千万别忘记配置@MapperScan,不然找不到mapper,StudyApplication 是我启动类的名称。
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.study.test.web.mapper")//这要修改为你自己mapper放的路径
public class StudyApplication {
public static void main(String[] args) {
SpringApplication.run(StudyApplication.class, args);
}
}
这些工作完成后,记得重启下项目看下有没有成功,不成功就需要自己调试下了
四、Redis配置
这里的redis主要是为了实现SpringSecurity整合JWT,实现redis的token登录。
1、RedisConfig
package com.study.test.common.config;//这是我存放redisconfig的路径
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
//RedisConfig 配置类
@Configuration
public class RedisConfig {
//解决redis可视化乱码问题,方便调试查找问题
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer); // key的序列化类型
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2、RedisUtil
redis工具,解决使用redis代码重复问题
package com.study.test.common.utils;//这是我存放redis工具的路径
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
@Order(-1)
public final class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return 0
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================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<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* 0
*
* @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<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return 0
*/
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 表头, 第二个元素,依次类推;index<0时,-,表尾,-倒数第二个元素,依次类推
* @return 0
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return 0
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return 0
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
新增完redis的配置后记得重启项目看看有没有问题
五、统一全局返回格式与处理系统异常
1、统一全局返回格式
ApiCode
package com.study.test.common.api;//这是我存放ApiCode 的目录
public enum ApiCode {
SUCCESS(200, "成功"),
SYSTEM_ERROR(500, "操作失败"),
NOT_FOUND(404,"未找到该资源");
private final int code;
private final String msg;
ApiCode(final int code, final String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
ApiResult
package com.study.test.common.api;//这是我存放ApiResult的路径
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
public class ApiResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
//状态码
private int code;
//返回数据
private T data;
//结果信息
private String message;
//时间字符串
private String time;
private ApiResult(){
}
//定义成功的构造器
private ApiResult(T data){
this.code = ApiCode.SUCCESS.getCode();
this.message = ApiCode.SUCCESS.getMsg();
this.data = data;
this.time = LocalDateTime.now().toString();
}
private ApiResult(ApiCode apiCode){
this.code = apiCode.getCode();
this.message = apiCode.getMsg();
this.time = LocalDateTime.now().toString();
}
private ApiResult(int code,String msg){
this.code = code;
this.message = msg;
this.time = LocalDateTime.now().toString();
}
private ApiResult(ApiCode apiCode,T data){
this.code = apiCode.getCode();
this.message = apiCode.getMsg();
this.data = data;
this.time = LocalDateTime.now().toString();
}
/**
* 成功的时候调用
* @param data
* @return
* @param <T>
*/
public static <T> ApiResult<T> success(T data){
return new ApiResult(data);
}
/**
* 根据状态返回结果
* @param apiCode
* @return
* @param <T>
*/
public static <T> ApiResult<T> build(ApiCode apiCode){
return new ApiResult(apiCode);
}
/**
* 根据code和msg返回结果
* @param code
* @param msg
* @return
* @param <T>
*/
public static <T> ApiResult<T> build(int code,String msg){
return new ApiResult(code,msg);
}
/**
* 根据状态和数据返回结果
* @param apiCode
* @param data
* @return
* @param <T>
*/
public static <T> ApiResult<T> build(ApiCode apiCode,T data){
return new ApiResult(apiCode,data);
}
/**
* 返回异常结果
* @param code
* @param msg
* @return
* @param <T>
*/
public static <T> ApiResult<T> error(int code,String msg){
return new ApiResult(code,msg);
}
}
2、全局系统异常处理
1、自定义异常类
package com.study.test.common.exception;//这是我异常类存放的目录
import com.study.test.common.api.ApiCode;
import lombok.Data;
/**
* 自定义异常类
*/
@Data
public class BusinessException extends RuntimeException{
private int code;
private String msg;
public BusinessException(ApiCode apiCode) {
super(apiCode.getMsg());
this.code = apiCode.getCode();
this.msg = apiCode.getMsg();
}
}
2、捕获全局异常
package com.study.test.common.exception;//这是我存放全局异常的目录
import com.study.test.common.api.ApiCode;
import com.study.test.common.api.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//自定义异常
@ExceptionHandler(BusinessException.class)
public ApiResult systemExceptionHandler(BusinessException e) {
log.error("BusinessException全局异常:{}",e);
return ApiResult.error(e.getCode(), e.getMsg());
}
//系统异常
@ExceptionHandler(Exception.class)
public ApiResult exceptionHandler(Exception e) {
log.error("Exception全局异常:{}",e);
return ApiResult.error(ApiCode.SYSTEM_ERROR.getCode(), e.getMessage());
}
}
记得重启看看有没有问题
六、工具类
1、JwtUtils工具类
package com.study.test.common.utils;//这是我存放jwt工具类的目录
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtils {
private static String secretKey;
private static Integer amount = 1800;//jwt的过期周期/秒 默认30分钟
@Value("${Jwt.secretKey}")
public void secretKey(String secretKey) {
JwtUtils.secretKey = secretKey;
}
/**
* 创建token
* @param payloadMap 存储的内容,自定义,一般是用户id
* @return
*/
public static String generateToken(Map<String, String> payloadMap) {
HashMap headers = new HashMap();
JWTCreator.Builder builder = JWT.create();
//定义jwt过期时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, amount);
//payload
payloadMap.forEach((k, v) ->{
builder.withClaim(k, v);
});
// 生成token
String token = builder.withHeader(headers)//header
//.withClaim("second",amount)//jwt的过期周期/秒,可以用于jwt快过期的时候自动刷新
.withExpiresAt(instance.getTime())//指定令牌的过期时间
.sign(Algorithm.HMAC256(secretKey));//签名
return token;
}
/**
* 校验token是否合法
* @param token
* @return
*/
public static DecodedJWT verifyToken(String token) {
/*
如果有任何验证异常,此处都会抛出异常
SignatureVerificationException 签名不一致异常
TokenExpiredException 令牌过期异常
AlgorithmMismatchException 算法不匹配异常
InvalidClaimException 失效的payload异常
*/
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token);
return decodedJWT;
}
/**
* 获取token信息
* @param token
* @return
*/
public static DecodedJWT getTokenInfo(String token) {
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token);
return decodedJWT;
}
/**
* 获取token信息方法
*/
/*public static Map<String, Claim> getTokenInfo(String token) {
return JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token).getClaims();
}*/
}
2、WebUtils工具类
package com.study.test.common.utils;//这是我存放web工具类的目录
import javax.servlet.http.HttpServletResponse;
public class WebUtils {
public static String rednerString(HttpServletResponse response, String content) {
try{
response.setStatus(200);
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().print(content);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
七、跨域配置
package com.study.test.common.config;//这是我存放跨域的目录
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
//跨越请求配置类
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 你需要跨域的地址 注意这里的 127.0.0.1 != localhost
// * 表示对所有的地址都可以访问
corsConfiguration.addAllowedOrigin("*"); // 1
// 跨域的请求头
corsConfiguration.addAllowedHeader("*"); // 2
// 跨域的请求方法
corsConfiguration.addAllowedMethod("*"); // 3
//加上了这一句,大致意思是可以携带 cookie
//最终的结果是可以 在跨域请求的时候获取同一个 session
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//配置 可以访问的地址
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
八、弄完上这些后,前期项目的准备工作就完成了
参考下即可
九、整合SpringSecurity
1、SpringSecurity认证异常工具类
AuthExceptionUtil
package com.study.test.security.utils;//我存放AuthExceptionUtil的目录
import com.study.test.common.api.ApiResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.AuthorizationServiceException;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.csrf.CsrfException;
//认证异常工具类
public class AuthExceptionUtil {
public static ApiResult getErrMsgByExceptionType(AuthenticationException e) {
if (e instanceof LockedException) {
return ApiResult.error(1100,"账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
return ApiResult.error(1105,"用户名或者密码输入错误!");
}else if (e instanceof InsufficientAuthenticationException) {
return ApiResult.error(403,"请登录!");
} else if (e instanceof AccountExpiredException) {
return ApiResult.error(1101, "账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
return ApiResult.error(1102, ("账户被禁用,请联系管理员!"));
} else if (e instanceof BadCredentialsException) {
return ApiResult.error(1105, "用户名或者密码输入错误!");
}else if (e instanceof AuthenticationServiceException) {
return ApiResult.error(1106, "认证失败,请重试!");
}
return ApiResult.error(1200, e.getMessage());
}
public static ApiResult getErrMsgByExceptionType(AccessDeniedException e) {
if (e instanceof CsrfException) {
return ApiResult.error(-1001, "非法访问跨域请求异常!");
} else if (e instanceof CsrfException) {
return ApiResult.error(-1002,"非法访问跨域请求异常!");
} else if (e instanceof AuthorizationServiceException) {
return ApiResult.error(1101, "认证服务异常请重试!");
}else if (e instanceof AccessDeniedException) {
return ApiResult.error(4003, "权限不足不允许访问!");
}
return ApiResult.error(1200, e.getMessage());
}
}
自定义授权失败异常处理类
package com.study.test.security.service;//我存放的目录
import com.study.test.common.utils.WebUtils;
import com.study.test.security.utils.AuthExceptionUtil;
import com.alibaba.fastjson.JSON;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//自定义授权失败异常处理类
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
System.out.println("AccessDeniedHandler:暂无权限");
WebUtils.rednerString(httpServletResponse, JSON.toJSONString(AuthExceptionUtil.getErrMsgByExceptionType(accessDeniedException)));
}
}
自定义认证失败异常处理类
package com.study.test.security.service;//我存放的目录
import com.study.test.common.utils.WebUtils;
import com.study.test.security.utils.AuthExceptionUtil;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//自定义认证失败异常处理类
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException authenticationException) throws IOException, ServletException {
System.out.println("AuthenticationEntryPoint:用户未登录");
WebUtils.rednerString(httpServletResponse, JSON.toJSONString(AuthExceptionUtil.getErrMsgByExceptionType(authenticationException)));
}
}
重启下看看有没有问题,security的自定义异常就处理完了,接下来就是如何实现自定义登录了
2、前后分离自定义认证
- Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
- AuthenticationManager接口:定义了认证Authentication的方法。
- UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
- UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
UserDetails(用户信息)
我们自定义一个用户信息,要实现security的UserDetails
package com.gzgs.security.web.security.entity;//我存放的路径
import com.alibaba.fastjson.annotation.JSONField;
import com.gzgs.security.web.entity.SysUser;
import lombok.AllArgsConstructor;
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.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LogUser implements UserDetails {
//用户信息
private SysUser user;
//用户权限
private List<String> permissions;
//存储SpringSecurity所需要的权限信息的集合
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
public LogUser(SysUser user,List<String> permissions){
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 将权限信息封装成 SimpleGrantedAuthority
if (authorities != null) {
return authorities;
}
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
authorities = this.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
public boolean isAccountNonExpired() {
return user.getAccountNotExpired();
}
@Override
public boolean isAccountNonLocked() {
return user.getAccountNotLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return user.getCredentialsNotExpired();
}
@Override
public boolean isEnabled() {
return user.getEnabled();
}
}
UserDetailsService(重写loadUserByUsername方法)
在loadUserByUsername方法写自己的登录逻辑,这里面涉及到查数据库
package com.study.test.security.service;//这是我的目录
import com.study.test.security.entity.LogUser;
import com.study.test.web.entity.SysPermission;
import com.study.test.web.entity.SysUser;
import com.study.test.web.mapper.SysPermissionMapper;
import com.study.test.web.mapper.SysUserMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysPermissionMapper sysPermissionMapper;
@Autowired
private SysUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//需要构造出 org.springframework.security.core.userdetails.User 对象并返回
System.out.println("用户名:"+username);
if (username == null || "".equals(username)) {
throw new RuntimeException("用户不能为空");
}
//根据用户名查询用户
SysUser user = userMapper.selectOne(new QueryWrapper<SysUser>().eq("account", username));
if (user == null) {
throw new RuntimeException("用户不存在");
}
List<String> permissionsList = new ArrayList<>();
if (user != null) {
//获取该用户所拥有的权限
List<SysPermission> sysPermissions = sysPermissionMapper.selectPermissionList(user.getUserId());
// 声明用户授权
sysPermissions.forEach(sysPermission -> {
permissionsList.add(sysPermission.getPermissionCode());
});
}
//返回用户信息
return new LogUser(user,permissionsList);
}
//这是加密的算法,把加密后的密码update你用户表的数据库用户的密码上
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123456");
System.out.println(encode);
}
}
JwtAuthenticationFilter(登录校验)
package com.study.test.security.filter;
import com.study.test.common.exception.BusinessException;
import com.study.test.common.utils.JwtUtils;
import com.study.test.common.utils.RedisUtil;
import com.study.test.security.entity.LogUser;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private RedisUtil redisUtil;
//每次请求都会执行这个方法
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws BusinessException, ServletException, IOException {
// 获取Headers上的token,我命名为token
String token = request.getHeader("token");
System.out.println("doFilterInternal:"+token);
if (StringUtils.isEmpty(token)) {
// token不存在 放行 并且直接return 返回
filterChain.doFilter(request, response);
return;
}
// 解析token
String userId = null;
try {
DecodedJWT tokenInfo = JwtUtils.verifyToken(token);
//token过期时间
Date expiresAt = tokenInfo.getExpiresAt();
SimpleDateFormat ymdhms = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("token过期时间:"+ymdhms.format(expiresAt));
//其实这里后端可以做token是否快过期的处理,然后返回新的token给前端
//或者新写一个刷新tokena接口给前端,让前端自己刷新
userId = tokenInfo.getClaim("userId").asString();
} catch (Exception e) {
if(e instanceof TokenExpiredException){
throw new RuntimeException("登录已过期!");
}else {
throw new RuntimeException("token非法");
}
}
// 获取userid 从redis中获取用户信息
String redisKey = "login:" + userId;
LogUser loginUser = (LogUser)redisUtil.get(redisKey);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
//将用户信息存入到SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
SecurityConfig(核心)
package com.study.test.security.config;//我的目录
import com.study.test.security.filter.JwtAuthenticationFilter;
import com.study.test.security.service.AccessDeniedHandlerImpl;
import com.study.test.security.service.AuthenticationEntryPointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
//@EnableWebSecurity //因为我引入了spring-boot-starter-security,所以不用@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//将authenticationManager注入容器中,再自定义登录接口中获取进行认证
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
//注入加密方式--后面就会使用这种方式进行对密码的对比(明文与密码的对比是否匹配)
// 而不使用默认的密码验证
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//配置放行的规则
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 关闭csrf验证(防止跨站请求伪造攻击)由于我们的资源都会收到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问
// 不通过session 获取SecurityContext(基于Token不需要session)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//开启权限拦截
.authorizeRequests()
// 允许登录接口匿名访问
.antMatchers("/sysUser/login", "/sysUser/test","/test/**").anonymous()
.antMatchers("/**.html","/js/**","/css/**","/img/**").permitAll()//放行静态资源
// 其他请求都需要认证
.anyRequest().authenticated();
//将jwtAuthenticationTokenFilter过滤器注入到UsernamePasswordAuthenticationFilter过滤器之前
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// 认证授权异常自定义处理
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)//自定义认证失败异常处理类
.accessDeniedHandler(accessDeniedHandler);//自定义授权失败异常处理类
// 禁用缓存
http.headers().cacheControl();
// 跨域请求配置
http.cors();
}
}
你再重新启动的时候,就看不到控制台输出密码了
我的项目结构
自定义登录,登出
自定义登录的传参loginUserParam
package com.study.test.web.param;//我的目录
import lombok.Data;
@Data
public class LoginUserParam {
//用户名
private String userName;
//用户密码
private String password;
}
LogService
package com.study.test.web.service;//我的目录
import com.study.test.common.api.ApiResult;
import com.study.test.web.param.LoginUserParam;
public interface LogService {
ApiResult login(LoginUserParam param);
ApiResult logOut();
}
LogServiceImpl
package com.study.test.web.service.impl;//我的目录
import com.study.test.common.api.ApiResult;
import com.study.test.common.utils.JwtUtils;
import com.study.test.common.utils.RedisUtil;
import com.study.test.security.entity.LogUser;
import com.study.test.web.param.LoginUserParam;
import com.study.test.web.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Service
public class LogServiceImpl implements LogService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public ApiResult login(LoginUserParam param) {
// 1 获取AuthenticationManager 对象 然后调用 authenticate() 方法
// UsernamePasswordAuthenticationToken 实现了Authentication 接口
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(param.getUserName(), param.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//2 认证没通过 提示认证失败
if (Objects.isNull(authenticate)) {
throw new RuntimeException("认证失败用户信息不存在");
}
//认证通过 使用userid 生成jwt token令牌
LogUser loginUser = (LogUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getUserId().toString();
Map<String, String> payloadMap = new HashMap<>();
payloadMap.put("userId", userId);
payloadMap.put("userName", loginUser.getUser().getUserName());
payloadMap.put("token", JwtUtils.generateToken(payloadMap));
boolean resultRedis = redisUtil.set("login:" + userId, loginUser);
if(!resultRedis){
throw new RuntimeException("redis连接不上,登录失败");
}
return ApiResult.success(payloadMap);
}
@Override
public ApiResult logOut() {
// 1 获取 SecurityContextHolder 中的用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LogUser loginUser = (LogUser)authentication.getPrincipal();
//2 删除redis 中的缓存信
String key = "login:"+loginUser.getUser().getUserId().toString();
redisUtil.del(key);
return ApiResult.success("退出成功!");
}
}
SysUserController
package com.study.test.web.controller;
import com.study.test.common.api.ApiResult;
import com.study.test.web.param.LoginUserParam;
import com.study.test.web.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/sysUser")
public class SysUserController {
@Autowired
private LogService logService;
/**
* 自定义登录
* @param param 登录传参
* @return
*/
@PostMapping("/login")
public ApiResult login(@RequestBody LoginUserParam param) {
return logService.login(param);
}
/**
* 自定义登出
* @return
*/
@PostMapping("/logOut")
public ApiResult logOut() {
return logService.logOut();
}
}
3、登出登入Postman测试
我自定义登录的请求为/sysUser/login 所以你得看看有没有把这个请求放行了,不然会提示无权限访问
登入
登出失败
登出成功
4、自定义security权限校验方法
配置自定义权限校验方法
package com.study.test.security.handler;//我的目录
import com.study.test.security.entity.LogUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.List;
//自定义security权限校验方法
@Component("syex")
public class SecurityPermissionsExpression {
public boolean hasAuthority(String authority){
//获取当前用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LogUser loginUser = (LogUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
//判断用户权限集合中是否存在authority
return permissions.contains(authority);
}
}
到这里,security的配置基本结束了,下面是我的目录结构
我的权限【sys:queryUser】
在控制层配置权限
package com.study.test.web.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/testPreAuthorize")
public class TestPreAuthorizeController {
@PostMapping("/hello")
// 只有sys:queryUser 权限才能访问
//@PreAuthorize("hasAuthority('sys:queryUser')") //这是没有自定义权限校验方法的默认写法
@PreAuthorize("@syex.hasAuthority('sys:queryUser')")
public String hello(){
return "hello";
}
@PostMapping("/hello2")
// 只有sys:queryUser2 权限才能访问
@PreAuthorize("@syex.hasAuthority('sys:queryUser2')")
public String hello2(){
return "hello2";
}
}
Postman测试权限