跳至主要內容

云商城-网关鉴权

soulballad实践项目SpringCloudAlibaba 云商城SpringCloudAlibaba约 6120 字大约 20 分钟

第12章 网关鉴权

课程目标

1、网关鉴权设计

​ 1)网关鉴权实现分析

​ 2)网关鉴权表结构设计

​ 3)JWT令牌讲解

2、身份认证

​ 1)令牌颁发

​ 2)令牌安全处理

3、鉴权实现

​ 1)鉴权服务搭建

​ 2)鉴权校验

1 网关鉴权设计

网关鉴权涉及的内容比较多,我们来分析一下:

1:身份识别:这里的身份识别是指用户登录信息识别,很久以前用Session,而微服务用JWT令牌。
2:身份鉴权:用户是否有权限访问当前指定方法。
3:鉴权位置:微服务网关中鉴权,还是在每个微服务鉴权。
4:鉴权类型:
	a)功能权限
	b)数据权限,复杂系数非常高,适合金融系统或者大型ERP系统

1.1 网关鉴权设计

用户鉴权方式有多种,可以在每个服务之间进行鉴权,也可以在微服务网关进行鉴权,如果在各个服务之间进行鉴权,效率会非常低,如果在微服务网关鉴权,就需要把暴露的端口控制好,只暴露微服务网关端口,其他端口一律不允许直接访问。很明显微服务网关鉴权效率最高、最实用,如下图:

1612108997212

基于微服务网关鉴权流程:

1:Service_Permission实现权限更新操作
2:Canal监听MySQL中权限变更日志,并将变更数据同步到Redis
3:Gateway拦截用户所有请求,在Gateway中加载Redis里用户的权限
4:在Gateway中加载权限后,对当前用户请求操作进行权限判断
5:如果有权限,则放行,没有权限,则拦截

1.2 网关鉴权表结构设计

如果要做一套实用的鉴权系统,我们需要设计一套权限表,权限表涉及权限资源表、角色表、角色授权表。

权限资源表:permission

CREATE TABLE `permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `source_name` varchar(20) NOT NULL COMMENT '权限名字',
  `url` varchar(200) NOT NULL COMMENT '权限访问地址,支持通配符',
  `url_match` int(1) NOT NULL COMMENT '匹配方式:0 完全匹配   1 通配符匹配',
  `service_name` varchar(100) NOT NULL COMMENT '服务名字',
  `method` varchar(10) NOT NULL COMMENT 'GET/POST/PUT/OPTIONS/DELETE/*',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

角色信息表:role_info

CREATE TABLE `role_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(20) NOT NULL,
  `description` varchar(500) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

角色授权表:role_permission

CREATE TABLE `role_permission` (
  `pid` int(11) NOT NULL,
  `rid` int(11) NOT NULL,
  PRIMARY KEY (`pid`,`rid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

1.3 JWT令牌

用户每次访问后台的时候,如果是一些需要认证的链接,都需要识别用户身份,比如用户抢单、用户中心等,在传统项目中用的是Session,但在微服务中不建议使用Session,使用JWT令牌。

1.3.1 初识JWT令牌

JWT简称JSON Web Token ,也就是通过json形式作为web应用的令牌,用在各方之间安全的将信息作为json对象传输,在数据传输过程中还可以完成数据加密,签名相关处理 。

JWT令牌作用:

身份授权:这是使用jwt的最常见方案,一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该领牌允许的路由,服务和资源,单点登录是的当今广泛使用JWT的一项功能,因为他的开销很小,并且可以在不同的域中使用。

信息交换:JWT令牌是在各方面之间安全地传输信息的好方法,因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人,此外由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。

JWT令牌认证流程:

1612146984727

JWT令牌鉴权流程如上图:

1:用户携带账号密码登录
2:登录通过后,后端服务会封装账号信息,并且采用指定算法进行加密,并将加密后的密文(令牌)返回客户端
3:客户端拿到令牌后,将令牌保存到本地,可以使Cookie,也可以是localStoreage
4:客户端每次发起请求的时候,会将本地令牌写到到请求头中,一起传到后台
5:后台在微服务网关中校验令牌是否正确,如果正确再执行权限校验
6:令牌校验通过、权限校验通过,则执行用户要操作的业务流程
7:令牌校验失败或者权限校验失败,则提示错误信息

JWT令牌校验优势:

1:简洁(Compact):可以通过URL,POST参数或者在HTTP header中发送,因为数据量小,所以传输的速度也快

2:自包含(Self-contained): 负载中包含了所有用户所需的信息,避免了多次查询数据库

3:因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何Web形式都支持

4:不要再服务端保存信息,特别适用于分布式微服务

1.3.2 JWT令牌结构

JWT令牌组成有3部分,分别为HeaderPayloadSignature,将三部分组合就是标准的JWT令牌了,三部分组合通常以"."链接,如下:

HJLDISNDSSDYIREWREWRDFDSDSFBH.DSFTIHGNDSFSDREWREWRDFDSFDSFRDNSJDJ.DSVDSFEWEREREWREWRDFDS

Header:

1:通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC,SHA265或RSA,它会使用Base64编码组成JWT结构的一部分

2:注意: Base64是一种编码,也就是说,他是可以被翻译回原来的样子的,他并不是一种加密的过程

3:{"alg":"HS365", "typ":"JWT"}

Payload:

1:令牌的第二部分是有效负载,其中包含声明,声明有关实体(通常是指用户)和其他数据的声明。同样的他会使用Base64编码组成JWT结构的第二部分

2:{"sub":"12334798", "name":"John Doe", "admin":true}

Signature:

1:前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息,Signature需要使用编码后的Header和Payload以及我们提供的一个密钥,然后使用Header中指定的签名算法(HS256)进行签名,没有被篡改过:

2:如:HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payLoad).secret)

为什么要签名?

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合成新的JWT的话,那么服务器会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的,如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

JWT令牌存在数据安全隐患?

JWT令牌采用了Base64加密,Base64加密数据可以直接解密,因此我们在JWT令牌中尽量不要传输敏感信息,如果非要传输敏感信息,可以把敏感信息进行加密操作,比如AES加密。

1.3.3 JWT令牌实现

JWT令牌使用参考地址:https://github.com/auth0/java-jwt

在创建JWT令牌的时候,会有很多属性需要填写,关于JWT令牌中一些属性,我们说明一下:

iss: jwt签发者
sub: 主题
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

mall-common中引入依赖

<!--JWT令牌-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.12.1</version>
</dependency>

创建com.gupaoedu.mall.util.JwtToken代码如下:

public class JwtToken {

    //默认秘钥
    private static final String DEFAULT_SECRET="springcloudalibaba";

    /***
     * 生成令牌
     * @param dataMap
     * @return
     */
    public static String createToken(Map<String,Object> dataMap){
        return createToken(dataMap,null);
    }

    /***
     * 生成令牌
     * @return
     */
    public static String createToken(Map<String,Object> dataMap,String secret){
        //秘钥为空就采用默认秘钥
        if(StringUtils.isEmpty(secret)){
            secret = DEFAULT_SECRET;
        }

        //创建令牌操作算法
        Algorithm algorithm = Algorithm.HMAC256(secret);
        //创建令牌
        return JWT.create()
                .withClaim("body",dataMap)
                .withIssuer("GP")            //JWT签发者
                .withSubject("JWT令牌")       //主题
                .withAudience("member")      //接收JWT的一方
                .withExpiresAt(new Date(System.currentTimeMillis()+3600000))    //过期时间
                .withNotBefore(new Date(System.currentTimeMillis()))      //指定时间之前JWT令牌是不可用的
                .withIssuedAt(new Date())    //JWT签发时间
                .withJWTId(UUID.randomUUID().toString().replace("-","")) // jwt唯一标识
                .sign(algorithm);
    }

    /***
     * 解析令牌
     * @param token
     * @return
     */
    public static Map<String,Object> parseToken(String token){
        return parseToken(token,null);
    }

    /***
     * 令牌校验并解析
     * @param token
     * @return
     */
    public static Map<String,Object> parseToken(String token,String secret){
        //秘钥为空就采用默认秘钥
        if(StringUtils.isEmpty(secret)){
            secret = DEFAULT_SECRET;
        }
        Algorithm algorithm = Algorithm.HMAC256(secret);
        JWTVerifier verifier = JWT.require(algorithm).build(); //Reusable verifier instance
        DecodedJWT jwt = verifier.verify(token);
        return jwt.getClaim("body").as(Map.class);
    }

    /***
     * 令牌校验解析
     * @param args
     */
    public static void main(String[] args) {
        Map<String,Object> dataMap = new HashMap<String,Object>();
        dataMap.put("name","zhangsan");
        dataMap.put("age","26");
        dataMap.put("address","深圳市");

        Map<String,Object> headerMap = new HashMap<String,Object>();
        headerMap.put("version","v1.0");
        headerMap.put("mysql","5.7");

        //创建令牌
        String token = createToken(dataMap);
        System.out.println(token);

        //解析令牌
        Map<String,Object> resultMap =parseToken(token);
        System.out.println(resultMap);
    }
}

测试结果如下:

eyJteXNxbCI6IjUuNyIsInR5cCI6IkpXVCIsInZlcnNpb24iOiJ2MS4wIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJKV1Tku6TniYwiLCJhdWQiOiJtZW1iZXIiLCJuYmYiOjE2MTIxNTg5MjIsImlzcyI6IkdQIiwiYm9keSI6eyJhZGRyZXNzIjoi5rex5Zyz5biCIiwibmFtZSI6InpoYW5nc2FuIiwiYWdlIjoiMjYifSwiZXhwIjoxNjEyMTYyNTEyLCJpYXQiOjE2MTIxNTg5MTIsImp0aSI6IjE0NTJmNzExYzY1MzQ5ZjI5Y2FhNDUwOTg3MmIzYTc5In0.wMZ9fbuUFtStblckO71FJAbnmNbfJRQEPylsXDr9ewM
eyJteXNxbCI6IjUuNyIsInR5cCI6IkpXVCIsInZlcnNpb24iOiJ2MS4wIiwiYWxnIjoiSFMyNTYifQ
{address=深圳市, name=zhangsan, age=26}

2 身份认证

我们接下来实现用户登录身份鉴权,但此时我们只做鉴权功能,关于权限的基础增删改查作为大家的作业。

2.1 令牌颁发

用户登录获取令牌,我们在mall-user-service中实现令牌颁发。我们需要将Dao、Service、Controller全部创建完成,然后执行登录方法实现。

1)Dao

mall-user-service中创建Dao:

public interface UserInfoMapper extends BaseMapper<UserInfo> {
}

2)Service

接口:在mall-user-service中创建接口com.gupaoedu.vip.mall.user.service.UserInfoService代码如下:

public interface UserInfoService extends IService<UserInfo> {
}

实现类:在mall-user-service创建com.gupaoedu.vip.mall.user.service.impl.UserInfoServiceImpl代码如下:

@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
}

3)Controller

mall-user-service中创建com.gupaoedu.vip.mall.user.controller.UserInfoController代码如下:

@RestController
@RequestMapping(value = "/user/info")
public class UserInfoController {

    @Autowired
    private UserInfoService userInfoService;

    /****
     * 登录
     */
    @PostMapping(value = "/login")
    public RespResult<String> login(@RequestParam String username,@RequestParam String pwd){
        //登录
        UserInfo userInfo = userInfoService.getById(username);
        if(userInfo!=null){
            //匹配密码是否一致
            if(userInfo.getPassword().equals(pwd)){
                //封装用户信息实现加密
                Map<String,Object> dataMap = new HashMap<String,Object>();
                dataMap.put("username",userInfo.getUsername());
                dataMap.put("name",userInfo.getName());
                dataMap.put("roles",userInfo.getRoles());

                //创建令牌
                String token = JwtToken.createToken(dataMap);
                return RespResult.ok(token);
            }
            //账号密码不匹配
            return RespResult.error("账号或者密码错误");
        }
        return RespResult.error("账号不存在");
    }
}

2.2 令牌安全

我们前面说过,令牌数据不安全,其实除了令牌数据不安全之外,令牌还存在被盗用的风险,例如:

1:张三登录获得令牌 zzz
2:李四盗取了张三的令牌 zzz,并用令牌zzz直接访问后台

上面例子执行,后台只要能识别令牌,是不会拒绝zzz令牌的,这时候就存在盗用风险。

令牌盗用该如何解决?

1612161750008

如果令牌被盗,我们可以通过IP识别令牌是否安全,如上图:

1:每次生成令牌的时候,把用户的IP作为令牌的一部分进行MD5加密,并将密文存入到令牌中
2:用户每次访问API接口的时候,都先获取客户端IP,再将IP进行MD5加密,并和令牌中的IP密文比对
3:如果密文一致,则证明IP没有发生变化,如果密文不一致,则证明IP发生变化,提示重新登录

这种操作在很多大厂中都有应用,你们平时登录QQ、微信的时候他们都会提示设备终端发生变化,其实和上图操作是一个道理。

2.2.1 令牌封装

mall-user-service中修改com.gupaoedu.vip.mall.user.controller.UserInfoController添加IP封装,代码如下:

1612165355424

2.2.2 令牌安全校验

我们在mall-api-gateway中创建com.gupaoedu.vip.mall.api.permission.AuthorizationInterceptor用来实现鉴权,代码如下:

public class AuthorizationInterceptor {

    /***
     * 令牌解析
     */
    public static Map<String, Object> jwtVerify(String token,String clientIp){
        try {
            //token解析
            Map<String, Object> resultMap = JwtToken.parseToken(token);
            //令牌中的IP
            String jwtip = resultMap.get("ip").toString();

            //IP校验
            clientIp = MD5.md5(clientIp);
            if(clientIp.equals(jwtip)){
                return resultMap;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

在微服务网关过滤器com.gupaoedu.vip.mall.api.filter.ApiFilter中调用上面方法,同时进行优化:

@Configuration
public class ApiFilter implements GlobalFilter, Ordered {

    @Autowired
    private HotQueue hotQueue;

    /***
     * 执行拦截处理      http://localhost:9001/mall/seckill/order?id&num
     *                 JWT
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        //获取uri
        String uri = request.getURI().getPath();

        if(uri.equals("/mall/user/info/login")){
            //放行
            return chain.filter(exchange);
        }

        //客户端IP
        String ip = IPUtil.getIp(request);
        //用户令牌
        String token = request.getHeaders().getFirst("authorization");
        //令牌校验
        Map<String, Object> resultMap = AuthorizationInterceptor.jwtVerify(token, ip);
        if(resultMap==null){
            endProcess(exchange,401,"no token");
        }

        if(uri.equals("/seckill/order")){
            //秒杀过滤
            seckillFilter(exchange, request, resultMap.get("username").toString());
        }

        //NOT_HOT 直接由后端服务处理
        return chain.filter(exchange);
    }

    /***
     * 秒杀过滤
     * @param exchange
     * @param request
     * @param username
     */
    private void seckillFilter(ServerWebExchange exchange, ServerHttpRequest request, String username) {
        //商品ID
        String id = request.getQueryParams().getFirst("id");
        //数量
        Integer num =Integer.valueOf( request.getQueryParams().getFirst("num") );

        //排队结果
        int result = hotQueue.hotToQueue(username, id, num);

        //QUEUE_ING、HAS_QUEUE
        if(result==HotQueue.QUEUE_ING || result==HotQueue.HAS_QUEUE){
            endProcess(exchange,result,"hot");
        }
    }

    /***
     * 结束程序
     * @param exchange
     * @param code
     * @param message
     */
    public void endProcess(ServerWebExchange exchange,Integer code,String message){
        Map<String,Object> resultMap = new HashMap<String,Object>();
        resultMap.put("code",code);
        resultMap.put("message",message);
        exchange.getResponse().setStatusCode(HttpStatus.OK);
        exchange.getResponse().setComplete();
        exchange.getResponse().getHeaders().add("message",JSON.toJSONString(resultMap));
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

我们把mall-cartmall-user服务配置到gateway中:

spring:
  cloud:
	gateway:
      routes:
        #秒杀
        - id: seckill_route
          uri: lb://mall-seckill
          predicates:
            - Path=/mall/seckill/order/**
          filters:
            - StripPrefix=1
        #购物车
        - id: cart_route
          uri: lb://mall-cart
          predicates:
            - Path=/mall/cart/**
          filters:
            - StripPrefix=1
        #用户服务
        - id: user_route
          uri: lb://mall-user
          predicates:
            - Path=/mall/address/**,/mall/user/info/**
          filters:
            - StripPrefix=1

我们可以在不同IP上登录生成令牌,在其他IP上用该令牌,效果如下:

1612168991412

3 鉴权实现

上面虽然实现了身份认证功能,但还存在权限问题,并不是所有链接地址都需要用户登录,而且用户登录后并不是所有地址都能访问,因此涉及到权限校验的问题。

我们前面分析过permission表,里面所有的url其实就是需要权限校验的地址,用户请求一个地址,如果在该表中不存在,则表示该地址不需要权限校验。

如果需要权限校验,就需要判断当前用户的角色是否包含该权限路径了。

3.1 鉴权服务搭建

1)Api

创建mall-permission-api并将提前生成好的JavaBean拷贝到工程中:

com.gupaoedu.vip.mall.permission.model.Permission:

@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "permission")
public class Permission implements Serializable {

   @TableId(type = IdType.AUTO)
    private Integer id;
    private String sourceName;
    private String url;
    private Integer urlMatch;
    private String serviceName;
    private String method;
}

com.gupaoedu.vip.mall.permission.model.RoleInfo:

@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "role_info")
public class RoleInfo implements Serializable {

    @TableId(type = IdType.AUTO)
    private Integer id;
    private String roleName;
    private String description;
    //权限列表
    @TableField(exist = false)
    private List<Permission> permissions;
}

2)Service

创建mall-permission-service工程

pom.xml依赖如下:

<dependencies>
    <dependency>
        <groupId>com.gupaoedu</groupId>
        <artifactId>mall-permission-api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

创建核心配置文件bootstrap.yml

server:
  port: 8094
spring:
  application:
    name: mall-permission
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.100.130:3306/shop_permission?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: 123456
  cloud:
    nacos:
      config:
        file-extension: yaml
        server-addr: 192.168.100.130:8848
      discovery:
        #Nacos的注册地址
        server-addr: 192.168.100.130:8848
  redis:
    host: 192.168.100.130
    port: 6379
# ====================MybatisPlus====================
mybatis-plus:
  mapper-locations: mapper/*.xml
  type-aliases-package: com.gupaoedu.vip.mall.*.model
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

#日志配置
logging:
  pattern:
    console: "%msg%n"

创建启动类com.gupaoedu.vip.mall.PermissionApplication

@SpringBootApplication
public class PermissionApplication {

    public static void main(String[] args) {
        SpringApplication.run(PermissionApplication.class,args);
    }
}

3)对象创建

Dao、Service、Controller可以直接使用MyBatisPlus代码生成器生成,在参考资料/鉴权工程中有个文件夹permission里面是创建好的Dao、Service、Controller,可以直接拷贝到工程中。

3.2 鉴权执行流程分析

1612678105327

鉴权流程如上图:

1:程序启动,初始化加载角色对应的权限,并存入到Redis缓存
2:用户登录的时候,如果登录成功,将用户信息封装到JWT令牌,令牌中包含用户角色ID
3:用户访问后端服务,在微服务网关处对权限进行校验,如果权限校验通过,则允许访问,否则不允许访问

流程看起来很简单,但是要注意的细节很多,比如有些地址是不需要权限访问的,比如登录、注册,令牌有可能会被别人修改,修改后把角色修改成权限很多的角色,对系统是会造成数据安全隐患,这些问题我们都需要一个一个解决。

3.3 权限校验

3.3.1 权限初始化

按照上面分析流程,我们在程序启动的时候,就需要初始化权限数据,也就是需要程序启动就执行相关代码,SpringBoot可以实现ApplicationRunner 或 CommandLineRunner接口,实现程序初始化启动。

初始化执行操作我们要执行2个操作,分别是角色权限初始化和所有权限初始化,所有权限初始化主要用来校验哪些地址需要被校验,而所有权限地址中不存在的地址那一定是不需要校验的,用户没登录也是能访问的。

1)Dao

我们在mall-permission-servicecom.gupaoedu.vip.mall.permission.mapper.PermissionMapper中创建一个方法,用于查询所有角色的权限

@Select("SELECT * FROM role_permission")
List<Map<Integer, Integer>> allRolePermissions();

2)Service

接口:在com.gupaoedu.vip.mall.permission.service.PermissionService创建2个方法,分别查询不同匹配方式的权限和所有角色权限,代码如下:

//不同匹配方式的权限
List<Permission> findByMatch(int i);

//所有角色的权限映射
List<Map<Integer, Integer>> allRolePermissions();

实现类:修改com.gupaoedu.vip.mall.permission.service.impl.PermissionServiceImpl添加实现方法,代码如下:

/***
 * 根据匹配方式查找
 * @param i
 * @return
 */
@Override
public List<Permission> findByMatch(int i) {
    QueryWrapper<Permission> queryWrapper = new QueryWrapper<Permission>();
    queryWrapper.eq("url_match",i);
    return permissionMapper.selectList(queryWrapper);
}

/**
 * 所有角色的权限
 * @return
 */
@Override
public List<Map<Integer, Integer>> allRolePermissions() {
    return permissionMapper.allRolePermissions();
}

3)初始化加载

创建com.gupaoedu.vip.mall.permission.init.InitPermission实现ApplicationRunner,从而达到SpringBoot工程启动加载的目的,代码如下:

@Component
public class InitPermission implements ApplicationRunner {

    @Autowired
    private PermissionService permissionService;

    @Autowired
    private RedisTemplate redisTemplate;

    /***
     * 初始化
     * @param args
     * @throws Exception
     */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        //根据匹配方式查找
        List<Permission> permissionsMatch0 = permissionService.findByMatch(0);
        List<Permission> permissionsMatch1 = permissionService.findByMatch(1);

        //查询所有角色权限
        List<Map<Integer,Integer>> rolePermissions = permissionService.allRolePermissions();
        //角色权限处理
        Map<String, Set<Permission>> roleMap = rolePermissionFilter(rolePermissions, permissionsMatch0, permissionsMatch1);

        //将所有权限和角色权限存入到Redis缓存
        redisTemplate.boundHashOps("RolePermissionAll").put("PermissionListMatch0",permissionsMatch0);
        redisTemplate.boundHashOps("RolePermissionAll").put("PermissionListMatch1",permissionsMatch1);
        redisTemplate.boundHashOps("RolePermissionMap").putAll(roleMap);
    }


    /***
     * 角色权限过滤
     * @param rolePermissions   : 角色权限映射关系
     * @param permissionsMatch0 :所有完全匹配路径
     * @param permissionsMatch1 :所有通配符匹配路径
     * @return
     */
    public Map<String,Set<Permission>> rolePermissionFilter(List<Map<Integer,Integer>> rolePermissions,
                                                             List<Permission> permissionsMatch0,
                                                             List<Permission> permissionsMatch1){
        //角色权限关系  key=roleid,value=List<Permission>
        Map<String, Set<Permission>> rolePermissionMapping = new HashMap<String,Set<Permission>>();

        //关系循环处理
        for (Map<Integer, Integer> rolePermissionMap : rolePermissions) {
            Integer rid = rolePermissionMap.get("rid");  //角色ID
            Integer pid = rolePermissionMap.get("pid");  //权限ID
            String key0 = "Role_0_"+rid.intValue();
            String key1 = "Role_1_"+rid.intValue();

            //获取当前角色拥有的权限
            Set<Permission> permissionSet0 = rolePermissionMapping.get(key0);
            Set<Permission> permissionSet1 = rolePermissionMapping.get(key1);

            //防止空指针
            permissionSet0=permissionSet0==null? new HashSet<Permission>(): permissionSet0;
            permissionSet1=permissionSet1==null? new HashSet<Permission>(): permissionSet1;

            //循环完全匹配路径
            for (Permission permission : permissionsMatch0) {
                if(permission.getId().intValue()==pid.intValue()){
                    permissionSet0.add(permission);
                    break;
                }
            }
            //循环通配符匹配路径
            for (Permission permission : permissionsMatch1) {
                if(permission.getId().intValue()==pid.intValue()){
                    permissionSet1.add(permission);
                    break;
                }
            }
            //将数据添加到rolePermissionMapping中
            if(permissionSet0.size()>0){
                rolePermissionMapping.put(key0,permissionSet0);
            }
            if(permissionSet1.size()>0){
                rolePermissionMapping.put(key1,permissionSet1);
            }
        }
        return rolePermissionMapping;
    }
}

如上代码,加载了所有权限以及不同角色的权限,并且所有权限都区分了匹配方式:

1:所有权限完全匹配 key=PermissionListMatch0
2:所有权限通配符匹配 key=PermissionListMatch1
3:角色权限完全匹配 key=Role_0_*,这里的*就是角色ID
4:角色权限通配符匹配 key=Role_1_*,这里的*就是角色ID

我们启动mall-permission-service服务,测试后Redis数据如下:

1612687458341

3.3.2 拦截校验

我们接下来实现对用户请求的地址进行校验,先校验当前请求地址是否需要执行权限校验,如果执行拦截才进入校验过程,如果不需要进行拦截是不需要校验的。

1)过滤器顺序调整

用户每次请求,地址栏会先经过处理,比如每次提交都会多携带一个/mall路径,此时对权限校验会造成干扰,需要将该路径处理掉,处理该路径SpringCloud Gateway提供了一个过滤器叫RouteToRequestUrlFilter,该过滤器处理完成后会找到处理当前请求的服务,接下来会调用LoadBalancerClientFilter选择一个处理请求的真实服务,我们要想判断权限,应该在这两个过滤器中间执行权限判断,因为此时知道要调用的服务名字也知道用户请求的真实服务地址,拦截器调用如下图:

1612699933885

此时我们把com.gupaoedu.vip.mall.api.filter.ApiFilter的执行顺序调整一下:

@Override
public int getOrder() {
    //在RouteToRequestFilter之后执行
    return 10001;
}

调整后,过滤器执行顺序如下图:

1612700163615

2)是否校验判断

我们在com.gupaoedu.vip.mall.api.permission.AuthorizationInterceptor中编写一个方法用于判断当前请求是否需要进行权限拦截,代码如下:

@Autowired
private RedisTemplate redisTemplate;

/****
 * 校验是否需要拦截指定请求
 */
public Boolean isIntercept(ServerWebExchange exchange){
    //request
    ServerHttpRequest request = exchange.getRequest();
    //uri
    String uri = request.getURI().getPath();
    //提交方法
    String method = request.getMethodValue();
    //路由URI信息
    URI routerUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
    //获取所有权限
    List<Permission> permissionMatch0 = (List<Permission>) redisTemplate.boundHashOps("RolePermissionAll").get("PermissionListMatch0");
    //完全匹配
    Permission permission = match0(permissionMatch0, uri, method,routerUri.getHost());
    //完全匹配如果没有,则匹配通配符
    if(permission==null){
        //匹配通配符
        List<Permission> permissionMatch1 = (List<Permission>) redisTemplate.boundHashOps("RolePermissionAll").get("PermissionListMatch1");
        //进行匹配,这里作为作业
    }
    //如果此时permission则表示不需要进行权限校验
    if(permission==null){
        //不需要权限校验
        return false;
    }
    return true;
}

/***
 * 完全匹配
 * @param permissionMatch0
 * @param uri
 * @param method
 * @return
 */
public Permission match0(List<Permission> permissionMatch0,String uri,String method,String serviceName){
    //循环匹配
    for (Permission permission : permissionMatch0) {
        String matchUrl = permission.getUrl();
        String matchMethod = permission.getMethod();
        if(matchUrl.equals(uri)){
            //匹配提交方式
            if(!matchMethod.equals("*") && matchMethod.equalsIgnoreCase(method) && serviceName.equals(permission.getServiceName())){
                return permission;
            }
        }
    }
    return null;
}

这里我们会用到RedisTemplate,在mall-service项目中用的都是String进行序列化,我们需要把mall-common中的Redis工具类RedisConfig拷贝到mall-api-gateway中。

运行时一些参数如下:

1612700248755

此时可以通过URI获取要调用的服务名字、用户请求的地址等,如下图:

1612700397573

我们接下来在com.gupaoedu.vip.mall.api.filter.ApiFilter中进行调用:

1612704651735

3)测试

我们请求http://192.168.1.104:9001/mall/brand/category/11159可以发现不需要令牌,如下图:

1612707990631

我们请求http://192.168.1.104:9001/mall/cart/list必须有令牌才能访问,如下图:

1612707944226

3.3.3 角色权限校验

1)令牌校验

我们先对之前代码进行优化,把令牌校验单独抽出到com.gupaoedu.vip.mall.api.permission.AuthorizationInterceptor中,代码如下:

/***
 * 令牌校验
 */
public Map<String, Object> tokenIntercept(ServerWebExchange exchange){
    //request
    ServerHttpRequest request = exchange.getRequest();
    //客户端IP
    String ip = IPUtil.getIp(request);
    //用户令牌
    String token = request.getHeaders().getFirst("authorization");
    //令牌校验
    Map<String, Object> resultMap = AuthorizationInterceptor.jwtVerify(token, ip);
    return resultMap;
}

com.gupaoedu.vip.mall.api.filter.ApiFilter中进行调用;

1612709023765

2)权限校验

每个用户存在多种角色,角色权限校验应该对种种角色权限进行校验,我们在com.gupaoedu.vip.mall.api.permission.AuthorizationInterceptor中编写方法实现校验,代码如下:

/***
 * 角色权限校验
 */
public Boolean rolePermission(ServerWebExchange exchange,Map<String, Object> token){
    //request
    ServerHttpRequest request = exchange.getRequest();
    //uri
    String uri = request.getURI().getPath();
    //提交方法
    String method = request.getMethodValue();
    //路由URI信息
    URI routerUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);

    //获取角色
    String[] roles = token.get("roles").toString().split(",");
    //当前角色权限
    Permission permission = null;

    //循环角色,获取角色权限
    for (String role : roles) {
        //===========完全匹配数据key0==============
        String key0 = "Role_0_"+role;
        //获取角色权限数据
        List<Permission> rolePermissionList0 = (List<Permission>) redisTemplate.boundHashOps("RolePermissionMap").get(key0);
        if(rolePermissionList0!=null){
            //匹配权限
            permission = match0((JSON.parseArray(JSON.toJSONString(rolePermissionList0),Permission.class)), uri, method, routerUri.getHost());
        }

        if(permission==null){
            //===========通配符匹配数据key1,作业==============
        }

        //如果找不到权限,说明无权访问
        if(permission!=null){
            break;
        }
    }
    return permission!=null;
}

我们接下来在com.gupaoedu.vip.mall.api.filter.ApiFilter中进行调用,代码如下:

1612762831229

测试的时候,有权限则顺利通过调用,没有权限的角色会显示如下提示:

1612758554098

上次编辑于:
贡献者: soulballad