springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)

一、业务描述

以注册、登录为主线,串联起验证码生成及校验、邮件发送、IP防暴刷、用户统一认证等功能。
实现需基于Spring Cloud 微服务架构,技术涉及Nginx、Eureka、Feign(Ribbon、Hystrix)、Gateway、Config+Bus等。

1.1 注册

1)用户访问到登录页面,在登录页面中有注册新账号功能
2)点击“注册新账号“,跳转到注册页面
3)在注册页面,需要用户输入邮箱地址、密码、确认密码,然后点击”获取验证码“,系统会生成验证码并向所输入的邮箱地址发送该验证码,用户拿到邮箱中的验证码输入后完成注册

规则如下
A:一分钟内只允许获取一次验证码(前端Js控制即可),验证码为随机生成的6位数字,10分钟内有效,验证码存储到mysql数据库中(也可以选择存入到Redis中);

B:存储到mysql数据库之后,使用发邮件功能,将该验证码发送到所输入的邮箱地址中

C:用户从邮箱中拿到验证码,点击注册时,需要进行行校验,因为验证码已经存入数据库,此时只需要查询数据库中该邮箱地址对应的最近一次的验证码记录,校验验证码是否正确,是否超时,若有问题,准确提示给用户

4)注册成功后,根据 <用户邮箱+密码> 生成签发token令牌(此处生成一个UUID模拟即可),该token令牌存入数据库(也可以选择存入到Redis中),并写入cookie中(以后的每次请求都会在cookie中携带该token,网关过滤器通过验证token的合法性来确定用户请求是否合法,如果token合法,根据token取出用户信息---->邮箱),最后重定向到欢迎⻚页面(显示邮箱地址)

1.2 登录

1)用户访问登录页面,在登录页面输入邮箱地址+密码
2)点击登录,后台对用户名和密码进行验证,然后根据<用户邮箱+密码> 生成签发token令牌(此处生成一个UUID模拟即可),该token令牌存入数据库(因为大家未系统学习Redis,所以此处令牌存入数据库即可),并写入cookie中(以后的每次请求都会在cookie中携带该token,网关过滤器通过验证token的合法性来确定用户请求是否合法,如果token合法,根据token取出用户信息---->邮箱),最后重定向到欢迎页面(显示邮箱地址)

1.3 架构描述

springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第1张图片

  • Nginx
    占⽤用端⼝口:80
    实现动静分离。将静态资源 html 页面存放至本地磁盘,数据请求统一经过 GateWay 网关路由到下游微服务。

  • 静态资源(html页面)
    访问前缀:/static/xxx.html
    包括登录页面 login.html、注册页面 register.html、以及成功登录之后的欢迎页面welcome.html,各个页面细节元素后面有描述

  • GateWay 网关
    占用端口:9002端口 数据请求前缀:/api/xxx
    完成统一路由、IP防暴刷(限制单个客户端IP在最近X分钟内请求注册接口不能超过Y次)、统一认证(登录时验证用户名密码是否合法,合法调用用户微服务生成token,写入cookie,并且携带邮箱地址重定向到欢迎页面;后续请求再到来时,验证客户端请求cookie中携带的token是否合法,合法则放行,此处不考虑token更新问题)等功能

    路路径路路由规则:
    /api/user/** 路由到用户微服务
    /api/code/** 路由到验证码微服务
    /api/email/** 路由到邮件微服务

  • dabing-service-user 用户微服务
    占用端口:8080 数据请求前缀:/api/user/**
    提供注册接口、用户是否已注册接口、登录接口(⽣生成token并入库,token写入cookie中)、查
    询用户登录邮箱接口等

  • dabing-service-code 验证码微服务
    占用端口:8081 数据请求前缀:/api/code/**
    用于提供验证码生成、验证码校验等接口,同时调用邮件微服务发送验证码

  • dabing-service-email 邮件微服务
    占用端口:8082 数据请求前缀:/api/email/**
    提供邮件发送功能,用于将生成的验证码发送到注册邮箱

  • Spring Cloud Config+Bus
    占用端口: 9006
    共享的配置:数据库连接信息、邮件发送相关配置、IP防暴暴刷指标参数(X分钟的X,Y上限的Y)
    注意:除去Eureka是2个实例的集群模式,其他保持单实例

1.4 接口描述

涉及到的微服务名称定义、接口定义、参数名称,可以和下文提到的保持一致。

微服务名称 API 接口 返回值 接口描述
dabing-service-use /user/register/{email}/{password}/{code} true/false 注册接⼝口,true成功,false失败
/user/isRegistered/{email} true/false 是否已注册,根据邮箱判断,true代表已经注册过,false代表尚未注册
/user/login/{email}/{password} 邮箱地址 登录接⼝口,验证⽤用户名密码合法性,根据⽤用户名和密码⽣生成token,token存⼊入数据库,并写⼊入cookie中,登录成功返回邮箱地址,重定向到欢迎⻚页
/user/info/{token} 根据token查询⽤用户登录邮箱接⼝口
dabing-service-code /code/create/{email} true/false ⽣生成验证码并发送到对应邮箱,成功true,失败false
/code/validate/{email}/{code} 0/1/2 校验验证码是否正确,0正确1错误2超时
dabing-service-email /email/{email}/{code} true/false 发送验证码到邮箱,true成功,false失败

1.5 页面描述

涉及到界面的,界面样式不做要求,界面元素完备即可。

  • 登录界面 login.html
    springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第2张图片
    登录界面包含输入项:
    邮箱:email
    密码:password
    点击“登录”调用登录接口,成功后重定向到欢迎页面welcome.html
    ajax调用http://localhost:9002/api/user/login/{email}/{password}
    点击“注册新账号”可跳转到注册页面register.html

  • 注册界面 register.html
    springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第3张图片
    注册界面包含输入项:
    邮箱:email
    密码: password
    确认密码:ConfirmPassword
    验证码:code
    点击“获取验证码”:调用获取验证码接口 http://localhost:9002/code/create/{email}
    点击”注册“:调用注册接口 http://localhost:9002/user/register/{email}/{password}/{code}

  • 欢迎页面 welcome.html
    如下显示登录账户信息即可
    springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第4张图片

1.6 关于网关和配置中心的说明

  • GateWay网关
    • 统一路由,数据请求的统一出入口
    • 添加全局过滤器,进行IP注册接口的防暴刷控制,超过阈值直接返回错误码及错误信息(错
      误码:303,错误信息:您频繁进行注册,请求已被拒绝)
    • 添加全局过滤,进行token的验证,用户微服务和验证码微服务的请求不过滤(网关调用下游
      用户微服务的token验证接口)
  • Config配置中心
    将数据库配置、邮件发送相关配置、防暴刷参数配置,使用Config进行管理,对应微服务可以在自己的配置文件中直接使用${xxx.yyy}的方式取出使用

1.7 关于数据表的说明

涉及到两个数据表:验证码存储表+令牌存储表,数据表参考如下

验证码存储表

-- ----------------------------
-- Table structure for dabing_auth_code
-- ----------------------------
DROP TABLE IF EXISTS `dabing_auth_code`;
CREATE TABLE `dabing_auth_code` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '⾃自增主键',
`email` varchar(64) DEFAULT NULL COMMENT '邮箱地址',
`code` varchar(6) DEFAULT NULL COMMENT '验证码',
`createtime` datetime DEFAULT NULL COMMENT '创建时间',
`expiretime` datetime DEFAULT NULL COMMENT '过期时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;

令牌存储表

-- ----------------------------
-- Table structure for dabing_token
-- ----------------------------
DROP TABLE IF EXISTS `dabing_token`;
CREATE TABLE `dabing_token` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '⾃自增主键',
`email` varchar(64) NOT NULL COMMENT '邮箱地址',
`token` varchar(255) NOT NULL COMMENT '令牌',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;

1.8 关于测试

  • 注册测试
  • 登录测试
  • 未登录状态下,清空cookie,直接访问后台的邮件服务,http://www.dabing.com/api/email/{email}/{code},验证无token情况下是否被网关拦截

1.9 关于跨域

前文在 Html 静态页面中有ajax 请求数据API统一接口9002的地方,会涉及跨域问题,可以考虑将静态
资源和数据请求接口放在同一个域名下,根据url前缀在nginx层进行区分。

比如所有部署,包括nginx都在一台机器上
可以给机器设置一个域名 www.dabing.com
静态资源访问 www.dabing.com/static/xxx.html
数据API接口请求 www.dabing.com/api/xxx/yyy
通过/static和/api在nginx层进行区分


1.10 效果视频验证:

注册新账号
一分钟内只允许获取一次验证码
发邮件功能
校验验证码
验证码超时展示
保存令牌数据库
令牌保存cookie中
跳转到欢迎页面
登录
生成Token保存到令牌表和Cookies中最后转到欢迎页面
未登录状态网关拦截
IP防暴刷过滤器:在1分钟内注册超过100次时返回错误信息

二、功能实现:

2.1 一分钟内只允许获取一次验证码

springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第5张图片

2.2 IP防暴刷过滤器
@Component
@RefreshScope // 刷新配置信息
public class IpGlobalFilter implements GlobalFilter, Ordered {
    @Value("${filter.limit.ip.uri}")
    private String limitUri;
    @Value("${filter.limit.ip.maxtimes}")
    private int maxTimes;
    @Value("${filter.limit.ip.limitminutes}")
    private int limitMinutes;

    private static ConcurrentMap<String, List<Long>> ipCache = new ConcurrentHashMap<>();

    public IpGlobalFilter() {
        System.out.println("IpGlobalFilter初始化");
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println(maxTimes);
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        // 客户端IP
        String ip = request.getRemoteAddress().getHostString();

        String path = request.getURI().getPath();
        // 如果请求的服务未设限,直接放行
        if (!path.startsWith(limitUri)) {
            return chain.filter(exchange);
        }
        // 设限服务把本次请求加入缓存
        List<Long> currentIpCache = ipCache.get(ip);
        // 初始化当前ip请求记录
        if (currentIpCache == null) {
            currentIpCache = new ArrayList<>();
            ipCache.put(ip, currentIpCache);
        }
        currentIpCache.add(System.currentTimeMillis());
        // 计算limitMinutes内访问次数是否超过maxTimes
        int count = 0;
        long startTime = System.currentTimeMillis() - (limitMinutes * 60 * 1000);
        for (Long reqTime : currentIpCache) {
            if (reqTime > startTime) {
                count++;
            }
        }
        if (count > maxTimes) {
            response.setStatusCode(HttpStatus.FORBIDDEN);
            String data = "您频繁进⾏注册,请求已被拒绝!";
            DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());
            return response.writeWith(Mono.just(wrap));
        }
        return chain.filter(exchange);
    }

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

2.3 未登录状态网关拦截
@Component
public class TokenGlobalFilter implements GlobalFilter, Ordered {
    @Autowired
    private UserFeignClient userFeignClient;

    /**
     * 进⾏token的验证,⽤户微服务和验证码微服务的请求不过滤(⽹关调⽤下游⽤户微服务的token验证接⼝)
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String path = request.getURI().getPath();
        // 用户微服务和验证码微服务的请求不过滤
        if (path.startsWith("/api/user") || path.startsWith("/api/code")) {
            return chain.filter(exchange);
        }
        // 获取Cookie,token不存在或者用户微服务查询不到重定向到登录页面
        List<HttpCookie> cookies = request.getCookies().get("token");
        if (!CollectionUtils.isEmpty(cookies)) {
            HttpCookie cookie = cookies.get(0);
            String token = cookie.getValue();
            if (!"".equals(userFeignClient.info(token))) {
                return chain.filter(exchange);
            }
        }
        // 返回状态码 303,重定向到登录页面
        response.getHeaders().set(HttpHeaders.LOCATION, "/static/login.html");
        response.setStatusCode(HttpStatus.SEE_OTHER);
        return response.setComplete();
    }

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

2.4 网关配置
server:
  port: 9090
eureka:
  client:
    serviceUrl:
      defaultZone: http://dabingcloudeurekaservera:8761/eureka/,http://dabingcloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
  application:
    name: dabing-cloud-gateway
  cloud:
    gateway:
      routes:
        - id: service-code-router
          uri: lb://dabing-service-code
          predicates:
            - Path=/api/code/**
          filters:
            - StripPrefix=1
        - id: service-user-router
          uri: lb://dabing-service-user
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1
      httpclient:
        connect-timeout: 5000
        response-timeout: 20000
2.5 nginx配置
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    upstream myServer {
        server 127.0.0.1:9090;
   	}

    server {
        listen       80;
        server_name  localhost;

        location /static/ {
            root staticDatas;
        }

        location / {
            root   html;
            index  index.html index.htm;
        }

        location /api {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

  			proxy_pass http://myServer;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
2.6 静态资源路径

springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第6张图片

注意:请求静态资源的时候,url地址栏需要使用配置的本地域名进行访问,否则报跨域问题

springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第7张图片

springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第8张图片

补充:不使用域名实现 跨域

1. 效果

springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第9张图片

2. 实现,在网关配置全局跨域

springcloud注册demo(使⽤第⼀代Spring Cloud核⼼组件完成项⽬构建、编码及测试)_第10张图片

server:
  port: 9090
eureka:
  client:
    serviceUrl:
      defaultZone: http://dabingcloudeurekaservera:8761/eureka/,http://dabingcloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
  application:
    name: dabing-cloud-gateway
  cloud:
    gateway:
      #开启网关的跨域功能,具体微服务上的跨域需要进行关闭,否则无效
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*"   #跨域处理 允许所有的域
            allowedMethods:     # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
        - id: service-code-router
          uri: lb://dabing-service-code
          predicates:
            - Path=/api/code/**
          filters:
            - StripPrefix=1
        - id: service-user-router
          uri: lb://dabing-service-user
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1
      httpclient:
        connect-timeout: 5000
        response-timeout: 20000

仓库地址:

demo仓库地址:https://gitee.com/lg_zk/dabing-user-parent-hw.git

你可能感兴趣的:(分布式架构设计和微服务,spring,cloud,gateway,eureka,feign,email)