微服务的编程测评系统-网关-身份认证-redis-jwt

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

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简单⽅便、安全可靠。可以减少服务器存储带来的开销和复杂性,实现跨域⽀持和⽔平扩展,并且更适应⽆状态和微服务架构。

问题

  1. jwt中payload中存储⽤⼾相关信息,采⽤base64编码,很简单就解密了,没有加密因此jwt中不能存储敏感数据。
  2. jwt是⽆状态的,因此如果想要修改⾥⾯的内容就必须重新签发⼀次新的jwt。
    ⽤⼾修改⾃⼰个⼈信息之后就需要重新登录,才能更新jwt
  3. ⽆法延⻓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

总结

相关推荐
俞凡9 分钟前
Netflix 数据网关实践
架构
_花卷14 分钟前
🌟ELPIS-如何基于vue3完成领域模型架构
前端·vue.js·架构
yzx9910131 小时前
零基础入门:用按键精灵实现视频自动操作(附完整脚本)
spring boot·微服务·云原生
cui_hao_nan1 小时前
Redis总结
redis·缓存
司铭鸿2 小时前
Java无服务架构新范式:Spring Native与AWS Lambda冷启动深度优化
数据结构·算法·架构·排序算法·代理模式
小哈里2 小时前
【管理】持续交付2.0:业务引领的DevOps-精要增订本,读书笔记(理论模型,技术架构,业务价值)
运维·架构·devops·管理·交付
懂得节能嘛.2 小时前
Netty集群方案详解与实战(Zookeeper + Redis + RabbitMQ)
redis·zookeeper·rabbitmq
~央千澈~2 小时前
laravel RedisException: Connection refused优雅草PMS项目管理系统报错解决-以及Redis 详细指南-优雅草卓伊凡
前端·redis·html·php
天若有情6733 小时前
Redis性能测试全攻略:工具实操与性能优化指南
redis·性能优化·bootstrap
程序员水自流4 小时前
Redis主从复制数据同步实现原理详细介绍
服务器·redis·缓存