章节概要
- 学会 API 网关权限验证和其他服务交互
- 学会开发 SpringBoot 的自定义配置
- 学会 Dubbo 负载均衡策略选择和使用
修改 Guns 中的 JWT 模块
- 增加忽略验证 URL 配置
- 修改返回内容匹配业务
- 增加 Threadlocal 的用户信息保存
业务功能开发
- 增加用户服务并提供接口
- 初步了解 API 网关与服务之间交互的过程
- 根据接口文档开发用户接口
创建用户表
DROP TABLE IF EXISTS user;
CREATE TABLE user(
UUID INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键编号',
user_name VARCHAR(50) COMMENT '用户账号',
user_pwd VARCHAR(50) COMMENT '用户密码',
nick_name VARCHAR(50) COMMENT '用户昵称',
user_sex INT COMMENT '用户性别 0-男,1-女',
birthday VARCHAR(50) COMMENT '出生日期',
email VARCHAR(50) COMMENT '用户邮箱',
user_phone VARCHAR(50) COMMENT '用户手机号',
address VARCHAR(50) COMMENT '用户住址',
head_url VARCHAR(50) COMMENT '头像URL',
biography VARCHAR(200) COMMENT '个人介绍',
life_state INT COMMENT '生活状态 0-单身,1-热恋中,2-已婚,3-为人父母',
begin_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间'
) COMMENT '用户表' ENGINE = INNODB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
insert into user(user_name,user_pwd,nick_name,user_sex,birthday,email,user_phone,address,head_url,life_state,biography) values('admin','0192023a7bbd73250516f069df18b500','管理员',0,'2018-07-31','admin@gmail.com','13888888888','浙江省杭州市西湖区某路某号','cinema/img/head-img.jpg',0,'我是苦逼的管理员');
insert into user(user_name,user_pwd,nick_name,user_sex,birthday,email,user_phone,address,head_url,life_state,biography) values('test','5e2de6bd1c9b50f6e27d4e55da43b917','测试用户',0,'2018-08-20','test@gmail.com','13866666666','测试地址','cinema/img/head-img.jpg',1,'我是测试用户');
用户服务与网关交互
在 guns 项目中复制一份 guns-gateway 模块并重命名为 guns-user(修改相应子模块和主模块的 pom.xml 等),并在 application.yml 中将其鉴权机制等关闭(网关才需要)
rest:
auth-open: false #jwt鉴权机制是否开启(true或者false)
sign-open: false #签名机制是否开启(true或false)
端口设为8083
server:
port: 8083 #项目端口
在 spring:dubbo: 下添加 Dubbo 协议和端口(UserApplication 需要向外部暴露接口,先向注册中心注册,然后通过 Dubbo 协议被远程调用)
spring:
application:
name: cinema-user
dubbo:
server: true
registry: zookeeper://localhost:2181
protocol:
name: dubbo
port: 20881
在 guns-user 中编写 UserAPI 的实现类,其中 UserAPI 接口是来自于 guns-api 的(在前一章已经做了业务 API 抽离),并通过 @Service 向外部暴露
@Component
@Service(interfaceClass = UserAPI.class)
public class UserImpl implements UserAPI {
@Override
public boolean login(String username, String password) {
System.out.println("This is user service! " + username + ", " + password);
return false;
}
}
然后,在 guns-gateway 中就可以远程调用 UserAPI 接口了
@Reference(interfaceClass = UserAPI.class)
private UserAPI userAPI;
userAPI.login(username, password);
基于 SpringBoot 配置忽略列表
配置 guns-gateway 网关模块中的 application.yml,在 jwt 下添加 ignore-url 配置,用逗号分隔 url(注意不能有空格!),这些接口将不通过 jwt 验证过滤器
jwt:
header: Authorization #http请求头所需要的字段
secret: mySecret #jwt秘钥
expiration: 604800 #7天 单位:秒
auth-path: auth #认证请求的路径
md5-key: randomKey #md5加密混淆key
ignore-url: /user/,/film/ #忽略列表
在 JwtProperties 中添加一项成员变量,并添加 get、set 函数,这个类是配置类,@ConfigurationProperties(prefix = JwtProperties.JWT_PREFIX)
表示用于从配置文件 application.yml 中获取 jwt 开头的配置变量
@Configuration
@ConfigurationProperties(prefix = JwtProperties.JWT_PREFIX)
public class JwtProperties {
public static final String JWT_PREFIX = "jwt";
private String header = "Authorization";
private String secret = "defaultSecret";
private Long expiration = 604800L;
private String authPath = "auth";
private String md5Key = "randomKey";
private String ignoreUrl = "";
在 AuthFilter 的 doFilterInternal 方法中添加以下代码,用于忽略配置列表
// 配置忽略列表
String ignoreUrl = jwtProperties.getIgnoreUrl();
String[] ignoreUrls = ignoreUrl.split(",");
for (int i = 0; i < ignoreUrls.length; i++) {
if (request.getServletPath().equals(ignoreUrls[i])) {
chain.doFilter(request, response);
return;
}
}
修改 JWT 申请的返回报文
在 guns-gateway 模块中的 modular.vo 下,创建 ResponseVO 类作为响应VO类,其中包含了三种状态 Status :0-成功,1-业务失败,999-系统异常
public class ResponseVO<M> {
/**
* 返回状态 [0-成功,1-业务失败,999-系统异常]
*/
private Integer status;
/**
* 返回信息
*/
private String msg;
/**
* 返回数据实体
*/
private M data;
private ResponseVO() {}
public static<M> ResponseVO success(M m) {
ResponseVO responseVO = new ResponseVO();
responseVO.setStatus(0);
responseVO.setData(m);
return responseVO;
}
public static ResponseVO serviceFail(String msg) {
ResponseVO responseVO = new ResponseVO();
responseVO.setStatus(1);
responseVO.setMsg(msg);
return responseVO;
}
public static ResponseVO appFail(String msg) {
ResponseVO responseVO = new ResponseVO();
responseVO.setStatus(999);
responseVO.setMsg(msg);
return responseVO;
}
之后在 AuthController 中就可以用该类作为返回对象了
@RequestMapping(value = "${jwt.auth-path}")
public ResponseVO createAuthenticationToken(AuthRequest authRequest) {
boolean validate = true;
// 去掉guns自身携带的用户名密码验证机制,使用自己的
int userId = userAPI.login(authRequest.getUserName(), authRequest.getPassword());
if (userId == 0) {
validate = false;
}
if (validate) {
// randomKey和token已经生成完毕
final String randomKey = jwtTokenUtil.getRandomKey();
final String token = jwtTokenUtil.generateToken("" + userId, randomKey);
// 返回值
return ResponseVO.success(new AuthResponse(token, randomKey));
} else {
return ResponseVO.serviceFail("用户名或密码错误");
}
}
增加 Threadlocal 的用户信息保存
在 guns-gateway 中的 common 下创建 CurrentUser 类用于保存 userId(不直接保存 userInfoModel 是因为更省空间,避免用户量大时出现内存溢出),该 userId 存在 ThreadLocal 中和线程绑定,同一线程内共享
public class CurrentUser {
/**
* 线程绑定的存储空间
*/
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void saveUserId(String userId) {
threadLocal.set(userId);
}
public static String getCurrentUser() {
return threadLocal.get();
}
}
之后,在 AuthFilter 验证 token 时,就可以通过 token 获取 userID,并且将之存入 ThreadLocal,以便后续业务调用了
String userId = jwtTokenUtil.getUsernameFromToken(authToken);
if (userId == null) {
return;
} else {
CurrentUser.saveUserId(userId);
}
在用户后续访问时,就可以取出 userId,并从数据库或 redis 中获取用户信息了
@Controller
@RequestMapping("/hello")
public class ExampleController {
@RequestMapping("")
public ResponseEntity hello() {
System.out.println(CurrentUser.getCurrentUser());
// 这里将以userId为key,userInfo为value存在redis中,TTL为30分钟
// 或以userId为条件去数据库取出(和redis只有快慢的区别)
return ResponseEntity.ok("请求成功!");
}
}
用户模块开发
业务功能开发流程
使用代码生成器生成数据项
在每个 guns 模块的 test 下,都有 enerator.EntityGenerator 这个用于实体生成的类,只需根据需要稍微修改一下方法的参数即可。在 guns-user 模块中生成即可得到:UserT, UserTMapper, UserTMapper.xml
...
// 生成代码的绝对目录
gc.setOutputDir("/Users/zjxjwxk/Documents/GitHub/Cinema-Dubbo/guns-user/src/main/java");
...
// 作者信息
gc.setAuthor("zjxjwxk");
...
// 数据库连接配置
dsc.setUsername("root");
dsc.setPassword("19981018");
dsc.setUrl("jdbc:mysql://127.0.0.1:3306/guns_rest?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8");
...
// 数据库表前缀
strategy.setInclude(new String[]{"user_t"});
...
// 生成 Entity、Mapper、XML 的包的相对目录
pc.setEntity("com.stylefeng.guns.rest.common.persistence.model");
pc.setMapper("com.stylefeng.guns.rest.common.persistence.dao");
pc.setXml("com.stylefeng.guns.rest.common.persistence.dao.mapping");
...
此处生成的实体类属于 guns-user 模块,用于和数据库打交道。以下是 guns-gateway 模块和 guns-user 模块不同的三种 user 类,UserModel 和 UserInfoModel 属于 BO,而 MoocUserT (即UserT) 属于 DO。
实现相应的接口功能
在 guns-user 模块的 UserServiceImpl 中实现 UserAPI (在 guns-api 中)的接口,如:
@Component
@Service(interfaceClass = UserAPI.class)
public class UserServiceImpl implements UserAPI {
@Autowired
private UserTMapper userTMapper;
@Override
public boolean register(UserModel userModel) {
...
}
}
在 guns-gateway 模块的 UserController 中实现编写相应 url 和控制层代码,如:
@RequestMapping("/user/")
@RestController
public class UserController {
@Reference(interfaceClass = UserAPI.class)
private UserAPI userAPI;
@RequestMapping(name = "register", method = RequestMethod.POST)
public ResponseVO register(UserModel userModel) {
...
}
}
Dubbo 特性
存在的问题
- 必须先启动服务提供者,否则会报错(如下)。如果有两个服务互相依赖,那么两个服务都无法启动成功。(Dubbo 启动检查)
Caused by: java.lang.IllegalStateException: Failed to check the status of the service com.stylefeng.guns.api.user.UserAPI. No provider available for the service com.stylefeng.guns.api.user.UserAPI from the url zookeeper://localhost:2181/com.alibaba.dubbo.registry.RegistryService?application=cinema-gateway&dubbo=2.6.0&interface=com.stylefeng.guns.api.user.UserAPI&methods=updateUserInfo,checkUserName,getUserInfo,login,register&pid=10451®ister.ip=192.168.6.102&side=consumer×tamp=1584633672918 to the consumer 192.168.6.102 use dubbo version 2.6.0
- 如果我们将用户模块部署多台,消费者会如何访问?(Dubbo 负载均衡)
- spring: dubbo: 中的 protocol 属性是做什么的?(Dubbo 多协议支持)
Dubbo 启动检查
官方文档:https://dubbo.apache.org/zh-cn/docs/user/demos/preflight-check.html
- 服务启动过程中验证服务提供者的可用性
- 验证过程出现问题,则阻止整个 Spring 容器初始化
- 服务启动检查可以尽可能早的发现服务问题
Dubbo 默认打开了启动检查,每个接口的 @Reference 注解都默认 check = true,如果确实想取消启动检查,只需在每个消费者所引用的接口的 @Reference 注解后手动设置 check = false 即可(但是并不推荐)。关闭启动检查后,无论所调用接口的服务有没有启动,都能够正常启动。但如果其调用没有启动的服务,将会报以下错误:
com.alibaba.dubbo.rpc.RpcException: No provider available from registry localhost:2181 for service com.stylefeng.guns.api.user.UserAPI on consumer 192.168.6.102 use dubbo version 2.6.0, may be providers disabled or not registered ?
Dubbo 负载均衡
官方文档:https://dubbo.apache.org/zh-cn/docs/user/demos/loadbalance.html
在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random
随机调用。
负载均衡策略
策略名称 | 策略描述 |
---|---|
Random | 随机,按权重设置随机概率 |
RoundRobin | 轮询,按公约后的权重设置轮询比率 |
LeastActive | 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差 |
ConsistentHash | 一致性 Hash,相同参数的请求总是发到同一提供者 |
配置
服务端服务级别
<dubbo:service interface="..." loadbalance="roundrobin"/>
客户端服务级别
<dubbo:reference interface="..." loadbalance="roundrobin" />
服务端方法级别
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
客户端方法级别
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>
Dubbo 多协议支持
官方文档:https://dubbo.apache.org/zh-cn/docs/user/demos/multi-protocols.html
多协议支撑
- Dubbo 支持多种协议
- 最常见的协议是 dubbo
- RMI、Hessian、HTTP、Redis、Memcached 等多种协议
协议对比
特性对比 | dubbo | RMI | Hessian |
---|---|---|---|
连接数 | 单连接 | 多连接 | 多连接 |
连接方式 | 长连接 | 短连接 | 短连接 |
传输协议 | TCP 传输 | 同步传输 | 同步传输 |
传输方式 | NIO 异步传输 | 同步传输 | 同步传输 |
适用场景 | 1. 数据包较小2. 消费者个数多3. 常规方式 | 1. 数据包大小不一2. 消费者和提供者数量相差不大 | 1. 数据包大小不一2. 消费者和提供者数量相差不大 |
总结
- API 网关与服务模块调用方式(API 网关相当于服务消费者,服务模块相当于服务提供者)
- Dubbo 特性:负载均衡、启动检查、Dubbo 协议
- JWT 的业务应用