微服务的编程测评系统-网关-身份认证-redis-jwt
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- API网关引入
- 作用
- 引入
- 身份认证机制
- 方式
- Jwt
- JWT组成
- ⾝份认证流程
- 为什么选择JWT
- 问题
- 处理方案
- redis引入
- 本地安装
- 代码配置
- 分装service
- 测试
- docker-desktop验证
- 身份认证机制
- JWT引入
- 生成token
- redis缓存用户数据
- 请求验证
- 总结
前言
API网关引入
可以作为统一的接口请求入口
然后进行身份验证
spring-cloud-gateway
作用
◦ 权限控制: 作为微服务的⼊⼝, 对⽤⼾进⾏权限校验, 如果校验失败则进⾏拦截
◦ 动态路由: ⼀切请求先经过⽹关, 但⽹关不处理业务, ⽽是根据某种规则, 把请求转发到某个微服务
◦ 负载均衡: 当路由的⽬标服务有多个时, 还需要做负载均衡
◦ 限流: 请求流量过⾼时, 按照⽹关中配置微服务能够接受的流量进⾏放⾏, 避免服务压⼒过⼤
引入
我们要先在ck-oj下面单独创建一个项目了,就是网关的项目,这个是独立于oj-modules的
先引入依赖
<!-- 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>
然后是配置文件和启动类
spring:application:name: oj-gatewayprofiles:active: localcloud:nacos:discovery:namespace: 6ef31ff7-e5b5-42a7-bc10-2bb286804635server-addr: http://localhost:8848config:namespace: 6ef31ff7-e5b5-42a7-bc10-2bb286804635server-addr: http://localhost:8848file-extension: yaml
nacos配置
server:port: 19090
spring:cloud:gateway:routes:# 管理模块- id: oj-systemuri: lb://oj-systempredicates:- 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引入
本地安装
先安装镜像
docker pull redis
然后启动容器
docker run --name oj-redis -d -p 6379:6379 redis --requirepass "123456"
–requirepass "123456"是redis的密码
代码配置
这也是一个公共的组件,所以也写在oj-common里面
先引入redis依赖
<!-- 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 的序列化的工具
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;}@Overridepublic byte[] serialize(T t) throws SerializationException {if (t == null) {return new byte[0];}return JSON.toJSONString(t).getBytes(DEFAULT_CHARSET);}@Overridepublic 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的
@Configuration
public class RedisConfig extends CachingConfigurerSupport {@Beanpublic 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 是我们自定义的序列化器
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。
@Component
public class RedisService {@Autowiredpublic 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 TimeUnitunit) {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 Longtimeout, 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, longend, 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, finalCollection<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, finalT 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里面引入
spring:data:redis:host: localhostpassword: 123456
@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验证
redis-cli
//密码验证
auth 123456
keys *
get sysUser
这样就成功了
身份认证机制
JWT引入
我们在oj-common-security里面使用Jwt
<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的工具类
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) {returnJwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}
}
这里定义了两个方法,一个是生成token,一个是解析token
其中SignatureAlgorithm.HS512是一个加密算法,是一个相对居中的加密算法,性能,安全性都是居中的
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的过期时间
@Value("${jwt.secret}")private String secret;
@Overridepublic 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缓存用户数据
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.22</version>
</dependency>
我们引入hutool来生成UUID
在oj-common-core中增加常量和枚举类
public class JwtConstants {public static final String LOGIN_USER_ID = "userId";public static final String LOGIN_USER_KEY = "userKey";
}
public class CacheConstants {public static final String LOGIN_USER_KEY = "loginUserKey:";public static final long EXPIRED = 720;
}
@AllArgsConstructor
@Getter
public enum UserIdentity {ADMIN(2, "管理员"),ORDINARY(1, "普通用户");private final Integer identity;private final String des;
}
然后把生成token和缓存的步骤放在oj-common-security中,因为普通用户的登录也要进行这个步骤
@Data
public class LoginUser {private Integer identity;
}
@Service
public class TokenService {@Autowiredprivate 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;}
}
@Overridepublic 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了
这样就成功了
请求验证
接下来完成第二步,对接口进行统一的验证
就需要在网关进行处理了
先在网关引入依赖
<dependency><groupId>com.ck</groupId><artifactId>oj-common-security</artifactId><version>1.0-SNAPSHOT</version></dependency>
@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;}
}
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里面
/*** ⽹关鉴权**/
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {// 排除过滤的 uri ⽩名单地址,在nacos⾃⾏添加@Autowiredprivate IgnoreWhiteProperties ignoreWhite;@Value("${jwt.secret}")private String secret;@Autowiredprivate RedisService redisService;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChainchain) {ServerHttpRequest request = exchange.getRequest();String url = request.getURI().getPath();// 跳过不需要验证的路径if (matches(url, ignoreWhite.getWhites())) {return chain.filter(exchange);}//从http请求头中获取tokenString 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中的keyboolean 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, Stringmsg) {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));}@Overridepublic 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();是获取请求的接口地址
因为登录的时候不需要进行身份认证
所以要把登录过滤出去
我们定义了一个接口白名单,在接口白名单里面的,都不需要进行身份的认证
if (matches(url, ignoreWhite.getWhites())) {return chain.filter(exchange);}
判断当前接口在白名单中,就不需要进行身份认证了
unauthorizedRespons是一个异常,我们用不了原来的全局异常处理了,因为这个是在网关中
然后再jwtUtils里面完善方法
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所有的配置
然后就会把读取到的配置,自动加载到这个类的成员变量上面
security:ignore:whites: - /syatem/sysUser/login- /friend/user/login
nacos网关这样配置的话,对应的成员变量whites数组就有值了
private boolean isMatch(String pattern, String url) {AntPathMatcher matcher = new AntPathMatcher();return matcher.match(pattern, url);}
pattern是白名单中的
pattern可以看成一个匹配规则,就可以检测是否匹配了
可以写一些特殊字符
/*** 判断url是否与规则匹配* 匹配规则中:* ? 表⽰单个字符;* * 表⽰⼀层路径内的任意字符串,不可跨层级;* ** 表⽰任意层路径;** @param pattern 匹配规则* @param url 需要匹配的url* @return 是否匹配*/
现在开始测试一下
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
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///"));
如果没有特殊字符的话,那么就必须一模一样才可以匹配了
所以修改naocs配置
security:ignore:whites: - /**/login
在getToken方法里面
token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);
这个是去掉请求头的前缀,Bearer
使用unauthorizedResponse原因是gateway是基于webflux的
但是全局异常处理器,是基于注解RestControllerAdvice,是专门给springmvc下的请求专门设计的,并不适用于springcloudgateway
所以我们用webflux的方法来分装result,就是方法webFluxResponseWriter