提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
API网关引入
可以作为统一的接口请求入口
然后进行身份验证
spring-cloud-gateway
作用
◦ 权限控制: 作为微服务的⼊⼝, 对⽤⼾进⾏权限校验, 如果校验失败则进⾏拦截
◦ 动态路由: ⼀切请求先经过⽹关, 但⽹关不处理业务, ⽽是根据某种规则, 把请求转发到某个微服务
◦ 负载均衡: 当路由的⽬标服务有多个时, 还需要做负载均衡
◦ 限流: 请求流量过⾼时, 按照⽹关中配置微服务能够接受的流量进⾏放⾏, 避免服务压⼒过⼤
引入
我们要先在ck-oj下面单独创建一个项目了,就是网关的项目,这个是独立于oj-modules的
先引入依赖
java
<!-- SpringCloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- SpringCloud Loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
然后是配置文件和启动类
java
spring:
application:
name: oj-gateway
profiles:
active: local
cloud:
nacos:
discovery:
namespace: 6ef31ff7-e5b5-42a7-bc10-2bb286804635
server-addr: http://localhost:8848
config:
namespace: 6ef31ff7-e5b5-42a7-bc10-2bb286804635
server-addr: http://localhost:8848
file-extension: yaml
nacos配置
java
server:
port: 19090
spring:
cloud:
gateway:
routes:
# 管理模块
- id: oj-system
uri: lb://oj-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
- id: oj-system是随便起的,不要重复就可以了
- uri: ⽬标服务地址, ⽀持普通URI 及 lb://应⽤注册服务名称 . lb表⽰负载均衡, 使⽤ lb:// ⽅式表⽰从注册中⼼获取服务地址,oj-system表示服务名
- predicates: 路由条件, 根据匹配结果决定是否执⾏该请求路由, 上述代码中, 我们把符合Path规则的⼀切请求, 都代理到uri参数指定的地
- filters:于定义在请求转发到⽬标地址之前或之后执⾏的过滤器。
◦ StripPrefix:这是⼀个StripPrefix过滤器,它的作⽤是从请求的路径中去除⼀部分前缀。StripPrefix=1 表⽰去除1个路径段。例如,如果原始请求的路径是 /system/test ,那么经过这个过滤器处理后,转发到⽬标服务的路径就变成了 /test 。
这个意思就是以/system开头的请求,用网关19090开头的,以/system开头的请求,就会把这个请求/system路由,转发给oj-system程序,但是oj-system程序是以sysUser开头的,所以StripPrefix的目标就是转发路由给oj-system的时候,去掉第一个路由/system,在转发给路由

发送请求http://127.0.0.1:19090/system/test/list
就成功了
说明没有问题
身份认证机制
方式
• 基于** 的⾝份认证:这是最常⻅的⾝份认证⽅式。当⽤⼾⾸次登录时,服务器会将⽤⼾信息存⼊session并⽣产⼀个唯⼀的Session ID,然后返回给客⼾端。此后的请求中客⼾端会要携带这个Session ID,服务器通过验证Session ID的有效性来判断⽤⼾的⾝份。
• 基于OAuth的⾝份认证:OAuth认证机制是⼀种安全、开放且简易的授权标准,它允许⽤⼾授权 第三⽅应⽤**(微信QQ)访问其账⼾资源,⽽⽆需向这些应⽤提供⽤⼾名和密码。如使⽤微信、QQ等账号登录其他⽹站或应⽤。
• 基于Token的⾝份认证:这种⽅式中,服务器在⽤⼾登录成功后,会返回⼀个Token给客⼾端。客⼾端每次请求资源时,都需要在请求头中携带这个Token。服务器通过验证Token的有效性来判断⽤⼾的⾝份。这种⽅式常⻅于前后端分离的架构中,如使⽤JWT(JSON Web Token)进⾏⾝份认证。
Jwt
JWT组成
它由三部分组成:头部(header)、载荷(payload)和签名(signature)。
• 头部(header):包含令牌的类型和使⽤的算法。使⽤base64编码
• 载荷(payload):包含⽤⼾信息和其他元数据。(使⽤base64编码)
使⽤base64编码
• 签名(signature):⽤于验证令牌的完整性和真实性。
Header中定义的签名算法(base64编码(header) + "." + base64编码(payload),secret
⾝份认证流程
• 客⼾端使⽤⽤⼾名跟密码请求登录 。
• 服务端收到请求,去验证⽤⼾名与密码 。
• 验证成功后,服务端会签发⼀个Token,再把这个Token发送给客⼾端 。(token上述的jwt串)
• 客⼾端收到Token以后可以把它存储起来,⽐如放在Cookie⾥或者Local Storage⾥ 。
• 客⼾端每次向服务端请求资源的时候需要带着服务端签发的Token 。
• 服务端收到请求,然后去验证客⼾端请求⾥⾯带着的Token,如果验证成功,就向客⼾端返回请求的数据 。
为什么选择JWT
• 简单⽅便:JWT认证机制不需要像传统的Session认证那样在服务器端存储任何会话信息,所有的认证和授权信息都包含在JWT中。这种⽅式简化了认证流程,减少了服务器的负担。
• 安全可靠:JWT使⽤数字签名来验证其完整性和真实性,确保数据在传输过程中不被篡改。
• 易于扩展:JWT是⽆状态的,服务器不需要存储⽤⼾的会话信息,这使得应⽤程序更容易进⾏⽔平扩展,这⼀点很适⽤于我们采⽤的微服务架构。当系统需要处理⼤量⽤⼾请求时,⽆状态的认证⽅式更加适合。
• ⽀持跨域:JWT认证机制中在客⼾端与服务器进⾏通信时,客⼾端会将JWT作为请求的⼀部分发送给服务器,不依赖于浏览器的cookie或session,因此不会受到同源策略的限制。这使得它⾮常适合处理跨域请求。如果你的Web项⽬需要与前端应⽤或其他服务进⾏跨域通信,JWT认证机制会是⼀个好选择。
总结:使⽤JWT简单⽅便、安全可靠。可以减少服务器存储带来的开销和复杂性,实现跨域⽀持和⽔平扩展,并且更适应⽆状态和微服务架构。
问题
- jwt中payload中存储⽤⼾相关信息,采⽤base64编码,很简单就解密了,没有加密因此jwt中不能存储敏感数据。
- jwt是⽆状态的,因此如果想要修改⾥⾯的内容就必须重新签发⼀次新的jwt。
⽤⼾修改⾃⼰个⼈信息之后就需要重新登录,才能更新jwt - ⽆法延⻓jwt的过期时间,意思就是过期时间不会改变,确定就不会变了
⽤⼾正在操作突然⾝份认证失败,过期时间突然到了,体验就不好
处理方案


我们将使⽤redis + jwt的结构完成⾝份认证。jwt中仅存储⽤⼾的唯⼀标识信息,使⽤redis作为第三⽅存储机制,存储⽤于⽤⼾⾝份认证的信息,并通过redis控制jwt的过期时间。
redis引入
本地安装
先安装镜像
java
docker pull redis
然后启动容器
java
docker run --name oj-redis -d -p 6379:6379 redis --requirepass "123456"
--requirepass "123456"是redis的密码

代码配置
这也是一个公共的组件,所以也写在oj-common里面
先引入redis依赖
java
<!-- SpringBoot Boot Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Alibaba Fastjson -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version>
</dependency>
Fastjson 是为了给redis进行序列化
我们先弄一个Fastjson 的序列化的工具
java
public class JsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
public JsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t).getBytes(DEFAULT_CHARSET);
}
@Override
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);
}
}
这个类的serialize和deserialize就是进行序列化和反序列化的
其中还包含对中文的操作,因为是UTF-8的
java
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
JsonRedisSerializer serializer = new JsonRedisSerializer(Object.class);
// 使⽤StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采⽤StringRedisSerializer的序列化⽅式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
RedisTemplate其实就是对redis进行操作的类,进行get和set之类的
我们自己创建一个RedisTemplate的bean,然后进行属性的设置
RedisConnectionFactory的作用就是和redis建立连接的,就是配置文件里面的东西
JsonRedisSerializer 是我们自定义的序列化器
java
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采⽤StringRedisSerializer的序列化⽅式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
这几行就是对key和value的序列化器分别进行设置
Key的序列化器就是redis导入依赖中默认提供的序列化器,这个用string类型的序列化器
Value,因为value会存放一个具体的对象,所以要先把对象序列化,然后再存储
template.afterPropertiesSet();就是完善后续redis配置操作
这样我们就完成了对template这个bean的初始化
分装service
为什么封装service:
**抽象与解耦:**封装第三⽅组件可以提供⼀个更⾼级的抽象层,使得你的代码与具体的第三⽅实现解耦。这样,如果将来需要更换第三⽅组件或调整其配置,你只需要修改封装的service层,⽽不需要修改整个应⽤中的⼤量代码。
**统⼀接⼝:**即使多个第三⽅⼯具提供相似的功能,它们的API和⽤法也可能各不相同。通过封装,我们可以提供⼀个统⼀的接⼝,使得其他开发者⽆需关⼼底层⼯具的具体差异。
扩展性 :通过封装,我们可以更容易地为第三⽅⼯具添加额外的功能或逻辑。以满⾜项⽬的特定的需求。
错误处理与异常管理 :第三⽅⼯具可能会抛出特定的异常或错误。通过封装,我们可以统⼀处理这些错误,并将它们转换为更通⽤或更有意义的异常,这样其他开发者就可以更容易地理解和处理这些错误。
**代码可读性与维护性:**使⽤封装的service可以使代码更加清晰和易于理解,因为你可以为service层提供有意义的名称和⽂档,以便其他开发者知道如何使⽤它以及它的功能。同时,如果将来有新⼈加⼊项⽬,他们也可以更容易地理解和使⽤封装的service。
java
@Component
public class RedisService {
@Autowired
public RedisTemplate redisTemplate;
//************************ 操作key ***************************
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 设置有效时间
*
* @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
*/
public boolean deleteObject(final String key) {
return redisTemplate.delete(key);
}
//************************ 操作String类型 ***************************
/**
* 缓存基本的对象,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 Long
timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key, Class<T> clazz) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
T t = operation.get(key);
if (t instanceof String) {
return t;
}
return JSON.parseObject(String.valueOf(t), clazz);
}
//*************** 操作list结构 ****************
/**
* 获取list中存储数据数量
*
* @param key
* @return
*/
public Long getListSize(final String key) {
return redisTemplate.opsForList().size(key);
}
/**
* 获取list中指定范围数据
*
* @param key
* @param start
* @param end
* @param clazz
* @param <T>
* @return
*/
public <T> List<T> getCacheListByRange(final String key, long start, long
end, Class<T> clazz) {
List range = redisTemplate.opsForList().range(key, start, end);
if (CollectionUtils.isEmpty(range)) {
return null;
}
return JSON.parseArray(JSON.toJSONString(range), clazz);
}
/**
* 底层使⽤list结构存储数据(尾插 批量插⼊)
*/
public <T> Long rightPushAll(final String key, Collection<T> list) {
return redisTemplate.opsForList().rightPushAll(key, list);
}
/**
* 底层使⽤list结构存储数据(头插)
*/
public <T> Long leftPushForList(final String key, T value) {
return redisTemplate.opsForList().leftPush(key, value);
}
/**
* 底层使⽤list结构,删除指定数据
*/
public <T> Long removeForList(final String key, T value) {
return redisTemplate.opsForList().remove(key, 1L, value);
}
//************************ 操作Hash类型 ***************************
public <T> T getCacheMapValue(final String key, final String hKey,
Class<T> clazz) {
Object cacheMapValue = redisTemplate.opsForHash().get(key, hKey);
if (cacheMapValue != null) {
return JSON.parseObject(String.valueOf(cacheMapValue), clazz);
}
return null;
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @param clazz 待转换对象类型
* @param <T> 泛型
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final
Collection<String> hKeys, Class<T> clazz) {
List list = redisTemplate.opsForHash().multiGet(key, hKeys);
List<T> result = new ArrayList<>();
for (Object item : list) {
result.add(JSON.parseObject(JSON.toJSONString(item), clazz));
}
return result;
}
/**
* 往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);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <K, T> void setCacheMap(final String key, final Map<K, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
public Long deleteCacheMapValue(final String key, final String hKey) {
return redisTemplate.opsForHash().delete(key, hKey);
}
}
这个其实就可以看做是一个工具类,分装了很多的方法
然后还是要配置org.springframework.boot.autoconfigure.AutoConfiguration.imports
测试
我们在oj-system里面使用redis
所以还是先引入
然后就是对redis进行配置文件的增加
配置文件里面的内容不能写在oj-common-redis里面,因为第一就是不能加载配置文件,引入依赖的时候,第二就是配置文件应该需要能启动吧
在nocos里面引入
java
spring:
data:
redis:
host: localhost
password: 123456
java
@GetMapping("/testRedis")
public String testRedis() {
SysUser sysUser = new SysUser();
sysUser.setUserAccount("admin");
sysUser.setPassword("123456");
redisService.setCacheObject("sysUser", sysUser);
SysUser sysUser1 = redisService.getCacheObject("sysUser", SysUser.class);
return sysUser1.toString();
}

docker-desktop验证
java
redis-cli
//密码验证
auth 123456
keys *
get sysUser

这样就成功了
身份认证机制
JWT引入
我们在oj-common-security里面使用Jwt
java
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.4.0-b180830.0359</version>
</dependency>
其中引入jaxb-api的目的是因为,jwt和springboot的版本问题,有些类,springboot可能没有,所以我们引入了jaxb-api,这些,那些缺失的类就有了
然后分装一个jwt的工具类
java
public class JwtUtils {
/**
* ⽣成令牌
*
* @param claims 数据
* @param secret 密钥
* @return 令牌
*/
public static String createToken(Map<String, Object> claims, String secret)
{
String token =
Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512,
secret).compact();
return token;
}
/**
* 从令牌中获取数据
*
* @param token 令牌
* @param secret 密钥
* @return 数据
*/
public static Claims parseToken(String token, String secret) {
return
Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
}
这里定义了两个方法,一个是生成token,一个是解析token
其中SignatureAlgorithm.HS512是一个加密算法,是一个相对居中的加密算法,性能,安全性都是居中的
java
public static void main(String[] args) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId",123456789L);
createToken(claims,"zxcvbnmasdfghjkl");
String token = createToken(claims,"zxcvbnmasdfghjkl");
System.out.println(token);
Claims claims1 = parseToken(token,"zxcvbnmasdfghjkl");
System.out.println(claims1.get("userId"));
}

这样就行了
其中zxcvbnmasdfghjkl是秘钥,秘钥不应该是硬编码的形式,应该是可以变化的,所以我们要设置到nacos中,对于秘钥的设置
生成token
身份验证机制,有很多内容
第一个就是在用户登录成功的时候生成token,然后返回token
第二就是在请求过来的时候,要检验token
第三就是用户在使用系统的时候,要延长token的过期时间
java
@Value("${jwt.secret}")
private String secret;
java
@Override
public R<String> login(String userAccount, String password) {
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.select(SysUser::getPassword).eq(SysUser::getUserAccount, userAccount);
SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
if(sysUser == null){
return R.fail(ResultCode.FAILED_USER_NOT_EXISTS);
}
if(!BCryptUtils.matchesPassword(password, sysUser.getPassword())){
return R.fail(ResultCode.FAILED_LOGIN);
}
Map<String, Object> claims = new HashMap<>();
claims.put("userId", sysUser.getUserId());
String token = JwtUtils.createToken(claims, secret);
return R.ok(token);
}
这样就OK了
然后就还要把用户数据存入redis中,jwt就存储用户标识就可以了,因为要防止中途拿到jwt,然后拿到用户数据,所以jwt不能存储敏感信息,我们用redis存储敏感信息
redis缓存用户数据
java
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
我们引入hutool来生成UUID
在oj-common-core中增加常量和枚举类

java
public class JwtConstants {
public static final String LOGIN_USER_ID = "userId";
public static final String LOGIN_USER_KEY = "userKey";
}
java
public class CacheConstants {
public static final String LOGIN_USER_KEY = "loginUserKey:";
public static final long EXPIRED = 720;
}
java
@AllArgsConstructor
@Getter
public enum UserIdentity {
ADMIN(2, "管理员"),
ORDINARY(1, "普通用户");
private final Integer identity;
private final String des;
}
然后把生成token和缓存的步骤放在oj-common-security中,因为普通用户的登录也要进行这个步骤
java
@Data
public class LoginUser {
private Integer identity;
}

java
@Service
public class TokenService {
@Autowired
private RedisService redisService;
public String createToken(Long userId, String secret,Integer identity){
Map<String, Object> claims = new HashMap<>();
String userKey = UUID.fastUUID().toString();
claims.put(JwtConstants.LOGIN_USER_ID, userId);
claims.put(JwtConstants.LOGIN_USER_KEY, userKey);
String token = JwtUtils.createToken(claims, secret);
LoginUser loginUser = new LoginUser();
loginUser.setIdentity(identity);//2表示管理员,1表示普通用户
redisService.setCacheObject(CacheConstants.LOGIN_USER_KEY+userKey, loginUser, CacheConstants.EXPIRED, TimeUnit.MINUTES);
return token;
}
}
java
@Override
public R<String> login(String userAccount, String password) {
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.select(SysUser::getPassword, SysUser::getUserId).eq(SysUser::getUserAccount, userAccount);
SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
if(sysUser == null){
return R.fail(ResultCode.FAILED_USER_NOT_EXISTS);
}
if(!BCryptUtils.matchesPassword(password, sysUser.getPassword())){
return R.fail(ResultCode.FAILED_LOGIN);
}
return R.ok(tokenService.createToken(sysUser.getUserId(),secret, UserIdentity.ADMIN.getIdentity()));
}
这样就OK了

这样就成功了
请求验证
接下来完成第二步,对接口进行统一的验证
就需要在网关进行处理了
先在网关引入依赖
java
<dependency>
<groupId>com.ck</groupId>
<artifactId>oj-common-security</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
java
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties
{
/**
* 放⾏⽩名单配置,⽹关不校验此处的⽩名单
*/
private List<String> whites = new ArrayList<>();
public List<String> getWhites()
{
return whites;
}
public void setWhites(List<String> whites)
{
this.whites = whites;
}
}
java
public class HttpConstants {
/**
* 服务端url标识
*/
public static final String SYSTEM_URL_PREFIX = "system";
/**
* ⽤⼾端url标识
*/
public static final String FRIEND_URL_PREFIX = "friend";
/**
* 令牌⾃定义标识
*/
public static final String AUTHENTICATION = "Authorization";
/**
* 令牌前缀
*/
public static final String PREFIX = "Bearer ";
}
这个是常量,放在core里面
java
/**
* ⽹关鉴权
*
*/
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {
// 排除过滤的 uri ⽩名单地址,在nacos⾃⾏添加
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Value("${jwt.secret}")
private String secret;
@Autowired
private RedisService redisService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
// 跳过不需要验证的路径
if (matches(url, ignoreWhite.getWhites())) {
return chain.filter(exchange);
}
//从http请求头中获取token
String token = getToken(request);
if (StrUtil.isEmpty(token)) {
return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims;
try {
claims = JwtUtils.parseToken(token, secret); //获取令牌中信息 解析payload中信息
if (claims == null) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
} catch (Exception e) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
String userKey = JwtUtils.getUserKey(claims); //获取jwt中的key
boolean isLogin = redisService.hasKey(getTokenKey(userKey));
if (!isLogin) {
return unauthorizedResponse(exchange, "登录状态已过期");
}
String userid = JwtUtils.getUserId(claims); //判断jwt中的信息是否完整
if (StrUtil.isEmpty(userid)) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
LoginUser user = redisService.getCacheObject(getTokenKey(userKey),
LoginUser.class);
if (url.contains(HttpConstants.SYSTEM_URL_PREFIX) &&
!UserIdentity.ADMIN.getValue().equals(user.getIdentity())) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
if (url.contains(HttpConstants.FRIEND_URL_PREFIX) &&
!UserIdentity.ORDINARY.getValue().equals(user.getIdentity())) {
return unauthorizedResponse(exchange, "令牌验证失败");
}
return chain.filter(exchange);
}
/**
* 查找指定url是否匹配指定匹配规则链表中的任意⼀个字符串
*
* @param url 指定url
* @param patternList 需要检查的匹配规则链表
* @return 是否匹配
*/
private boolean matches(String url, List<String> patternList) {
if (StrUtil.isEmpty(url) || CollectionUtils.isEmpty(patternList)) {
return false;
}
for (String pattern : patternList) {
if (isMatch(pattern, url)) {
return true;
}
}
return false;
}
/**
* 判断url是否与规则匹配
* 匹配规则中:
* ? 表⽰单个字符;
* * 表⽰⼀层路径内的任意字符串,不可跨层级;
* ** 表⽰任意层路径;
*
* @param pattern 匹配规则
* @param url 需要匹配的url
* @return 是否匹配
*/
private boolean isMatch(String pattern, String url) {
AntPathMatcher matcher = new AntPathMatcher();
return matcher.match(pattern, url);
}
/**
* 获取缓存key
*/
private String getTokenKey(String token) {
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
/**
* 从请求头中获取请求token
*/
private String getToken(ServerHttpRequest request) {
String token =
request.getHeaders().getFirst(HttpConstants.AUTHENTICATION);
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StrUtil.isNotEmpty(token) &&
token.startsWith(HttpConstants.PREFIX)) {
token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);
}
return token;
}
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String
msg) {
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
return webFluxResponseWriter(exchange.getResponse(), msg,
ResultCode.FAILED_UNAUTHORIZED.getCode());
}
//拼装webflux模型响应
private Mono<Void> webFluxResponseWriter(ServerHttpResponse response,
String msg, int code) {
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE);
R<?> result = R.fail(code, msg);
DataBuffer dataBuffer =
response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
@Override
public int getOrder() {
return -200;
}
public static void main(String[] args) {
AuthFilter authFilter = new AuthFilter();
// 测试 ?
String pattern = "/sys/?bc";
System.out.println(authFilter.isMatch(pattern,"/sys/abc"));
System.out.println(authFilter.isMatch(pattern,"/sys/cbc"));
System.out.println(authFilter.isMatch(pattern,"/sys/acbc"));
System.out.println(authFilter.isMatch(pattern,"/sdsa/abc"));
System.out.println(authFilter.isMatch(pattern,"/sys/abcw"));
// 测试*
// String pattern = "/sys/*/bc";
// System.out.println(authFilter.isMatch(pattern,"/sys/a/bc"));
//
System.out.println(authFilter.isMatch(pattern,"/sys/sdasdsadsad/bc"));
// System.out.println(authFilter.isMatch(pattern,"/sys/a/b/bc"));
// System.out.println(authFilter.isMatch(pattern,"/a/b/bc"));
// System.out.println(authFilter.isMatch(pattern,"/sys/a/b/"));
// 测试**
// String pattern = "/sys/**/bc";
// System.out.println(authFilter.isMatch(pattern, "/sys/a/bc"));
// System.out.println(authFilter.isMatch(pattern,
// "/sys/sdasdsadsad/bc"));
// System.out.println(authFilter.isMatch(pattern, "/sys/a/b/bc"));
// System.out.println(authFilter.isMatch(pattern,
// "/sys/a/b/s/23/432/fdsf///bc"));
// System.out.println(authFilter.isMatch(pattern,
// "/a/b/s/23/432/fdsf///bc"));
// System.out.println(authFilter.isMatch(pattern,
// "/sys/a/b/s/23/432/fdsf///"));
}
}
这个AuthFilter 就是主要进行身份认证的类
GlobalFilter是一个全局的过滤器:主要处理的是,网关转发请求给服务这个过程
拦截到请求之后,就会进入filter方法
这个里面就可以进行身份认证了
filter是GlobalFilter要实现的方法
getOrder是Ordered要实现的方法
这个表示全局过滤器要执行的顺序,因为可能有多个过滤器,所以过滤器的执行有先后顺序
返回的值越小,过滤器就越先被执行
request.getURI().getPath();是获取请求的接口地址
因为登录的时候不需要进行身份认证
所以要把登录过滤出去
我们定义了一个接口白名单,在接口白名单里面的,都不需要进行身份的认证
java
if (matches(url, ignoreWhite.getWhites())) {
return chain.filter(exchange);
}
判断当前接口在白名单中,就不需要进行身份认证了
unauthorizedRespons是一个异常,我们用不了原来的全局异常处理了,因为这个是在网关中
然后再jwtUtils里面完善方法
java
public static String getUserKey(Claims claims) {
Object value = claims.get(JwtConstants.LOGIN_USER_KEY);
return value == null ? "" : value.toString();
}
public static String getUserId(Claims claims) {
Object value = claims.get(JwtConstants.LOGIN_USER_ID);
return value == null ? "" : value.toString();
}
因为先判断的jwt,jwt正确,但是查不到redis数据,说明过期了
jwt都没有,说明从来没有登录
ignoreWhite.getWhites()就是白名单
在IgnoreWhiteProperties类中
@ConfigurationProperties(prefix = "security.ignore")配置了前缀security.ignore,用这个前缀把配置从外部拉过来,其实就是从nacos里面拉取的配置
就是从nacos上面读取前缀为security.ignore所有的配置
然后就会把读取到的配置,自动加载到这个类的成员变量上面
java
security:
ignore:
whites:
- /syatem/sysUser/login
- /friend/user/login
nacos网关这样配置的话,对应的成员变量whites数组就有值了
java
private boolean isMatch(String pattern, String url) {
AntPathMatcher matcher = new AntPathMatcher();
return matcher.match(pattern, url);
}
pattern是白名单中的
pattern可以看成一个匹配规则,就可以检测是否匹配了
可以写一些特殊字符
java
/**
* 判断url是否与规则匹配
* 匹配规则中:
* ? 表⽰单个字符;
* * 表⽰⼀层路径内的任意字符串,不可跨层级;
* ** 表⽰任意层路径;
*
* @param pattern 匹配规则
* @param url 需要匹配的url
* @return 是否匹配
*/
现在开始测试一下
java
AuthFilter authFilter = new AuthFilter();
// 测试 ?
String pattern = "/sys/?bc";
System.out.println(authFilter.isMatch(pattern,"/sys/abc"));
System.out.println(authFilter.isMatch(pattern,"/sys/cbc"));
System.out.println(authFilter.isMatch(pattern,"/sys/acbc"));
System.out.println(authFilter.isMatch(pattern,"/sdsa/abc"));
System.out.println(authFilter.isMatch(pattern,"/sys/abcw"));

第一个true,true,false,false
java
String pattern = "/sys/*/bc";
System.out.println(authFilter.isMatch(pattern,"/sys/a/bc"));
System.out.println(authFilter.isMatch(pattern,"/sys/sdasdsadsad/bc"));
System.out.println(authFilter.isMatch(pattern,"/sys/a/b/bc"));
System.out.println(authFilter.isMatch(pattern,"/a/b/bc"));
System.out.println(authFilter.isMatch(pattern,"/sys/a/b/"));

java
String pattern = "/sys/**/bc";
System.out.println(authFilter.isMatch(pattern, "/sys/a/bc"));
System.out.println(authFilter.isMatch(pattern,"/sys/sdasdsadsad/bc"));
System.out.println(authFilter.isMatch(pattern, "/sys/a/b/bc"));
System.out.println(authFilter.isMatch(pattern,"/sys/a/b/s/23/432/fdsf///bc"));
System.out.println(authFilter.isMatch(pattern,"/a/b/s/23/432/fdsf///bc"));
System.out.println(authFilter.isMatch(pattern,"/sys/a/b/s/23/432/fdsf///"));

如果没有特殊字符的话,那么就必须一模一样才可以匹配了
所以修改naocs配置
java
security:
ignore:
whites:
- /**/login
在getToken方法里面
java
token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);
这个是去掉请求头的前缀,Bearer
使用unauthorizedResponse原因是gateway是基于webflux的
但是全局异常处理器,是基于注解RestControllerAdvice,是专门给springmvc下的请求专门设计的,并不适用于springcloudgateway
所以我们用webflux的方法来分装result,就是方法webFluxResponseWriter