dubbo仿猫眼微服务架构—业务基础环境构建和用户模块开发

API网关

考虑一个业务场景:提交订单时,要检查用户是否登陆,检查库存是否足够,再提交订单。而这三个操作属于三个不同的微服务,这样调用请求时,要建立三个连接比较耗时。

API网关的作用就类似于hao123网站,作为一个门户,只需此网站,就可以面向所有的网站。

前端只面向API网关。
Gateway模块

API网关的常见作用

  • 身份验证和安全
  • 审查和监测(当前业务的执行时间,调用了什么服务,用户行为记录)
  • 动态路由
  • 压力测试
  • 负载均衡
  • 静态相应处理

基础环境构建

gateway模块

环境配置

采用guns构建业务基础环境,简化开发。

首先到码云上下载guns的源代码:https://gitee.com/naan1993/guns/

使用IDEA在本地打开下载好的源代码工程。进入到rest模块目录下,在项目中找到配置文件,根据自己的实际情况修改相应的数据库配置和项目端口配置。

datasource:
	url: jdbc:mysql://127.0.0.1:3306/guns_rest?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
	# 自己的用户名和密码
	username: root
	password: 
	filters: log4j,wall,mergeStat
  1. 复制rest模块,并改名为guns-gateway模块,修改此模块的pom文件
    gateway的pom文件
  2. 修改总的pom文件,添加guns-gateway模块的依赖
<modules>
    <module>guns-admin</module>
    <module>guns-core</module>
    <module>guns-rest</module>
    <module>guns-generator</module>
    <module>guns-gateway</module>
    </modules>
  1. 在工程中整合dubbo

在gateway的pom文件中加入dubbo和zookeeper依赖
依赖
修改配置文件
yml配置
最后在启动类上添加注解:
@EnableDubboConfiguration

到这里dubbo的集成就结束了。首先打开zookeeper,然后再启动这个模块。如果zookeeper的日志中打印出注册信息说明注册成功。

抽离公共API

将guns-cores复制一份改为guns-api,将没用的包全部删除,然后按照上面讲过的步骤对其进行修改。api模块中装的是所有模块都会依赖到的公共接口。在API中写好所有的接口之后,要install,放入Maven库中。

在gateway以及其他模块中,直接在pom文件中进行配置,引入API模块就可以了。这样就避免了每个模块都要写API。
API接口层
到这里架构搭建就基本完成了。

用户模块开发

  • 学会API网关权限验证和其他服务交互
  • 学会springboot的自定义配置
  • 学会Dubbo负载均衡策略选择和使用

用户表结构

DROP TABLE IF EXISTS mooc_user_t;
CREATE TABLE mooc_user_t(
   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;

用户模块搭建

首先复制gateway模块,取名为guns-user,修改配置文件,pom文件和项目端口等信息。

建立用户信息类:UserModel(注册所需信息,用户名密码手机号等)和UserInfoModel(查询的用户信息。除了注册所需用户名邮箱之外的信息,如昵称、性别、生日、个性签名等)。记得添加序列化标识。

在API模块中添加UserAPI包,并定义接口中的方法,包括用户注册与登录,用户信息获取与修改等方法。

public interface UserAPI {
	//login有返回值:返回用户的UUID  —可以把当前活跃用户放到Redis缓存中,并设置一个TTL。
	//JWT可能七天以后再失效,但只要缓存过期了,即使用户携带JWT也需要重新登录。
    int login(String username,String password);

    boolean register(UserModel userModel);

    boolean checkUsername(String username);

    UserInfoModel getUserInfo(int uuid);

    UserInfoModel updateUserInfo(UserInfoModel userInfoModel);

}

在user模块中建立userServiceImpl类,实现这些接口中的方法。

在gateway层写与前端交互的UserController方法。

权限验证—采用JWT

修改gateway中的yaml文件,增加忽略列表的配置。

rest:
  auth-open: true #jwt鉴权机制是否开启(true或者false)
  sign-open: true #签名机制是否开启(true或false)

jwt:
  header: Authorization   #http请求头所需要的字段
  secret: mySecret        #jwt秘钥
  expiration: 604800      #7天 单位:秒
  auth-path: auth         #认证请求的路径
  md5-key: randomKey      #md5加密混淆key
  ignore-url: /user/register,/user/check,/film/getIndex,/film/getConditionList,/film/getFilms,/film/films,/cinema/getCinemas,/cinema/getCondition,/cinema/getFields,/cinema/getFieldInfo #忽略列表,有些服务不需要JWT鉴权
server:
  port: 8082 #项目端口

同时,在gateway中的JwtProperties(Jwt配置类)中加入ignoreUrl变量(springBoot会自动根据驼峰原则定位到yml中的配置并读取)。

@Configuration
//自动读取yml配置文件中以JWT_PREFIX为开头的配置
@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 = "";
	
	//相应的get set方法
	//...

JWT的生成—AuthController类,使用jwtTokenUtil生成token并返回。在用户登录时,以后的请求,用户都会携带这个token供我们验证。

@RestController
public class AuthController {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Reference(interfaceClass = UserAPI.class)
    private UserAPI userAPI;

    @RequestMapping(value = "${jwt.auth-path}")
    public ResponseVO createAuthenticationToken(AuthRequest authRequest) {

        boolean validate = true;
        	//判断用户是否存在
            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("用户名或密码错误");
        }
    }
}

JWT的使用—AuthFilter类。客户端所有请求首先走这里进行验证,并在这里保存用户信息。

public class AuthFilter extends OncePerRequestFilter {
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private JwtProperties jwtProperties;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request.getServletPath().equals("/" + jwtProperties.getAuthPath())) {
            chain.doFilter(request, response);
            return;
        }

        //配置忽略列表
        String ignoreUrl = jwtProperties.getIgnoreUrl();
        String[] ignoreUrls = ignoreUrl.split(",");
        for (int i=0;i<ignoreUrls.length;i++){
            if(request.getServletPath().startsWith(ignoreUrls[i])){
                chain.doFilter(request,response);
                return;
            }
        }

        final String requestHeader = request.getHeader(jwtProperties.getHeader());
        String authToken = null;
        if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
            authToken = requestHeader.substring(7);
            //通过token获取UserID,并将之存入Threadlocal,以便后续业务调用
            String userId = jwtTokenUtil.getUsernameFromToken(authToken);
            if(userId==null){
                return;
            }else {
            	//保存当前用户
                CurrentUser.saveUserId(userId);
            }
            //验证token是否过期,包含了验证jwt是否正确
            try {
                boolean flag = jwtTokenUtil.isTokenExpired(authToken);
                if (flag) {
                    RenderUtil.renderJson(response, new ErrorTip(BizExceptionEnum.TOKEN_EXPIRED.getCode(), BizExceptionEnum.TOKEN_EXPIRED.getMessage()));
                    return;
                }
            } catch (JwtException e) {
                //有异常就是token解析失败
                RenderUtil.renderJson(response, new ErrorTip(BizExceptionEnum.TOKEN_ERROR.getCode(), BizExceptionEnum.TOKEN_ERROR.getMessage()));
                return;
            }
        } else {
            //header没有带Bearer字段
            RenderUtil.renderJson(response, new ErrorTip(BizExceptionEnum.TOKEN_ERROR.getCode(), BizExceptionEnum.TOKEN_ERROR.getMessage()));
            return;
        }
        chain.doFilter(request, response);
    }
}

JWT存在的问题:在退出登录/修改密码时如何让JWT失效?

  • 将 token 存入 DB(如 Redis)中,失效则删除;但增加了一个每次校验时候都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则(这不就和 session 一样了么?)。
  • 在 JWT 中增加一个版本号字段,失效则改变该版本号。服务器端可以设计用户id和一个版本号的对应关系,版本号设置成自动增长的整型数字,或者就是一个时间戳,这样用之前的token去登录时,判断版本号不一致就让重新登录,分配新的版本号,服务器端每次登录以后就更新一下对应数据的版本号。
  • 在服务端设置加密的 key 时,为每个用户生成唯一的 key,失效则改变该 key。

在本项目中,我们在设计login方法时,增加了一个返回值。返回值即是用户的uuid。在用户登录成功后,我们可以把这个uuid存在Redis中,并设置过期时间。后续在AuthController进行验证时,如果在Redis中找不到uuid,即使携带JWT,也要重新登录。

JWT在某些方面,比Session要优越。比如,如何允许用户只能在最近五个设备登录?

session: 使用数据库,创建 token 数据库表,有 id, token, user_id 三个字段,user 与 token 表为 1:m 关系。每次登录添加一行记录。根据 token 获取 user_id,再根据 user_id 获取该用户有多少设备登录,超过 5 个,则删除最小 id 一行。

jwt: 使用计数器,在用户表中添加字段 count,默认值为 0,每次登录 count 字段自增1,每次登录创建的 jwt 的 Payload 中携带数据 current_count 为用户的 count 值。每次请求权限接口时,根据 JWT 获取 count 以及 current_count,根据 user_id 查用户表获取 count,判断与 current_count 差值是否小于 5。

对于这个需求,JWT 略简单些,而使用 session 还需要多维护一张 token 表。

业务功能开发与dubbo负载均衡

使用代码生成器生成数据项

实现方法

验证忽略列表

申请JWT

使用JWT访问其他权限功能

报错

java.lang.IllegalStateException:Ambiguous mapping. Cannot map 'UserController' method

报错原因修改
"name"属性的作用是为该映射起一个名字,而并不表示该映射的具体路径;"value"属性表示该映射的具体路径。

java.lang.IllegalStateException:Serialized class com.stylefeng.guns.api.user.UserInfoModel must implements java.io.Serializable

必须实现序列化接口:
错误解决2
必须先启动服务提供者,否则会报错—Dubbo中默认启动检查。关闭启动检查:check = false

com.alibaba.dubbo.rpc.RpcException:No provider available from register localhost:2181 for service com.stylefeng.guns.api.user.UserAPI...

关闭启动检查
负载均衡

Dubbo中的负载均衡策略

策略名称 策略描述
Random 随机,按权重设置随机概率
RoundRobin 轮询,按公约后的权重设置轮询比率
LeastActive 最少活跃调用数 使慢的提供者收到更少请求
ConsistentHash 一致性Hash 相同参数的请求总是发到同一提供者。当某台提供者挂了以后,基于虚拟节点,不会引发剧烈变动
发布了4 篇原创文章 · 获赞 0 · 访问量 235

猜你喜欢

转载自blog.csdn.net/weixin_43867524/article/details/105590896
今日推荐