云商城-网关鉴权
第12章 网关鉴权
课程目标
1、网关鉴权设计
1)网关鉴权实现分析
2)网关鉴权表结构设计
3)JWT令牌讲解
2、身份认证
1)令牌颁发
2)令牌安全处理
3、鉴权实现
1)鉴权服务搭建
2)鉴权校验
1 网关鉴权设计
网关鉴权涉及的内容比较多,我们来分析一下:
1:身份识别:这里的身份识别是指用户登录信息识别,很久以前用Session,而微服务用JWT令牌。
2:身份鉴权:用户是否有权限访问当前指定方法。
3:鉴权位置:微服务网关中鉴权,还是在每个微服务鉴权。
4:鉴权类型:
a)功能权限
b)数据权限,复杂系数非常高,适合金融系统或者大型ERP系统
1.1 网关鉴权设计
用户鉴权方式有多种,可以在每个服务之间进行鉴权,也可以在微服务网关进行鉴权,如果在各个服务之间进行鉴权,效率会非常低,如果在微服务网关鉴权,就需要把暴露的端口控制好,只暴露微服务网关端口,其他端口一律不允许直接访问。很明显微服务网关鉴权效率最高、最实用,如下图:

基于微服务网关鉴权流程:
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令牌认证流程:

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部分,分别为Header、Payload、Signature,将三部分组合就是标准的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令牌的,这时候就存在盗用风险。
令牌盗用该如何解决?

如果令牌被盗,我们可以通过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封装,代码如下:

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-cart和mall-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上用该令牌,效果如下:

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 鉴权执行流程分析

鉴权流程如上图:
1:程序启动,初始化加载角色对应的权限,并存入到Redis缓存
2:用户登录的时候,如果登录成功,将用户信息封装到JWT令牌,令牌中包含用户角色ID
3:用户访问后端服务,在微服务网关处对权限进行校验,如果权限校验通过,则允许访问,否则不允许访问
流程看起来很简单,但是要注意的细节很多,比如有些地址是不需要权限访问的,比如登录、注册,令牌有可能会被别人修改,修改后把角色修改成权限很多的角色,对系统是会造成数据安全隐患,这些问题我们都需要一个一个解决。
3.3 权限校验
3.3.1 权限初始化
按照上面分析流程,我们在程序启动的时候,就需要初始化权限数据,也就是需要程序启动就执行相关代码,SpringBoot可以实现ApplicationRunner 或 CommandLineRunner接口,实现程序初始化启动。
初始化执行操作我们要执行2个操作,分别是角色权限初始化和所有权限初始化,所有权限初始化主要用来校验哪些地址需要被校验,而所有权限地址中不存在的地址那一定是不需要校验的,用户没登录也是能访问的。
1)Dao
我们在mall-permission-service的com.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数据如下:

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

此时我们把com.gupaoedu.vip.mall.api.filter.ApiFilter的执行顺序调整一下:
@Override
public int getOrder() {
//在RouteToRequestFilter之后执行
return 10001;
}
调整后,过滤器执行顺序如下图:

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中。
运行时一些参数如下:

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

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

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

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

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中进行调用;

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中进行调用,代码如下:

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