【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【17】认证服务01—短信/邮件/异常/MD5


持续学习&持续更新中...

守破离


【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【17】认证服务01

环境搭建

C:\Windows\System32\drivers\etc\hosts

java 复制代码
192.168.56.10 gulimall.com
192.168.56.10 search.gulimall.com
192.168.56.10 item.gulimall.com
192.168.56.10 auth.gulimall.com

Nginx配置:(记得使用Nginx动静分离)

复制代码
# ...

http {
    # ...

    upstream gulimall {
       server 192.168.193.107:88;
    }

    include /etc/nginx/conf.d/*.conf;
}

网关:

yml 复制代码
        - id: gulimall_auth_route
          uri: lb://gulimall-auth
          predicates:
            - Host=auth.gulimall.com

gulimall-auth:

java 复制代码
@Controller
public class LoginController {
    @GetMapping("/login.html")
    public String loginPage() {
        return "login";
    }

    @GetMapping("/reg.html")
    public String regPage() {
        return "reg";
    }
}

或者:

java 复制代码
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
    /**
     * 视图映射
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {

        /**
         *     @GetMapping("/login.html")
         *     public String loginPage(){
         *          //空方法
         *         return "login";
         *     }
         */
        //只是get请求能映射
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

验证码倒计时

前端:

java 复制代码
    $(function () {
        $("#sendCode").click(function () {
            //2、倒计时
            if ($(this).hasClass("disabled")) {
                //正在倒计时。
            } else {
                //1、给指定手机号发送验证码
                // $.get("/sms/sendEmail?email=" + $("#phoneNum").val(), function (data) {
                $.get("/sms/sendcode?phone=" + $("#phoneNum").val(), function (data) {
                    if (data.code != 0) {
                        alert(data.msg);
                    }
                });
                timeoutChangeStyle();
            }
        });
    })

    var num = 60;

    function timeoutChangeStyle() {
        $("#sendCode").attr("class", "disabled");
        if (num == 0) {
            $("#sendCode").text("发送验证码");
            num = 60;
            $("#sendCode").attr("class", "");
        } else {
            var str = num + "s 后再次发送";
            $("#sendCode").text(str);
            setTimeout("timeoutChangeStyle()", 1000);
        }
        num--;
    }

短信服务

购买短信套餐后,扫码激活,然后绑定测试手机号码:

然后点击:调用API发送短信 按钮 (使用【专用】测试签名/模板)

然后 发起调用 ,复制相关信息即可

增加权限授予RAM子账号SMS和MPush的权限。

xml 复制代码
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>alibabacloud-dysmsapi20170525</artifactId>
            <version>3.0.0</version>
        </dependency>
java 复制代码
// This file is auto-generated, don't edit it. Thanks.
package com.atguigu.gulimall.auth.sms;

import com.aliyun.auth.credentials.Credential;
import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
import com.aliyun.sdk.service.dysmsapi20170525.AsyncClient;
import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsResponse;
import com.google.gson.Gson;
import darabonba.core.client.ClientOverrideConfiguration;

import java.util.concurrent.CompletableFuture;

public class SendSms {
    public static void main(String[] args) throws Exception {

        // HttpClient Configuration
        /*HttpClient httpClient = new ApacheAsyncHttpClientBuilder()
                .connectionTimeout(Duration.ofSeconds(10)) // Set the connection timeout time, the default is 10 seconds
                .responseTimeout(Duration.ofSeconds(10)) // Set the response timeout time, the default is 20 seconds
                .maxConnections(128) // Set the connection pool size
                .maxIdleTimeOut(Duration.ofSeconds(50)) // Set the connection pool timeout, the default is 30 seconds
                // Configure the proxy
                .proxy(new ProxyOptions(ProxyOptions.Type.HTTP, new InetSocketAddress("<your-proxy-hostname>", 9001))
                        .setCredentials("<your-proxy-username>", "<your-proxy-password>"))
                // If it is an https connection, you need to configure the certificate, or ignore the certificate(.ignoreSSL(true))
                .x509TrustManagers(new X509TrustManager[]{})
                .keyManagers(new KeyManager[]{})
                .ignoreSSL(false)
                .build();*/

        // Configure Credentials authentication information, including ak, secret, token
        StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
                // Please ensure that the environment variables ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET are set.
                .accessKeyId("xxxx")
                .accessKeySecret("xxxx")
                //.securityToken(System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN")) // use STS token
                .build());

        // Configure the Client
        AsyncClient client = AsyncClient.builder()
                .region("cn-shanghai") // Region ID
                //.httpClient(httpClient) // Use the configured HttpClient, otherwise use the default HttpClient (Apache HttpClient)
                .credentialsProvider(provider)
                //.serviceConfiguration(Configuration.create()) // Service-level configuration
                // Client-level configuration rewrite, can set Endpoint, Http request parameters, etc.
                .overrideConfiguration(
                        ClientOverrideConfiguration.create()
                                  // Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
                                .setEndpointOverride("dysmsapi.aliyuncs.com")
                        //.setConnectTimeout(Duration.ofSeconds(30))
                )
                .build();

        // Parameter settings for API request
        SendSmsRequest sendSmsRequest = SendSmsRequest.builder()
                .signName("阿里云短信测试")
                .templateCode("xxxx")
                .phoneNumbers("xxxx")
                .templateParam("{\"code\":\"1111\"}")
                // Request-level configuration rewrite, can set Http request parameters, etc.
                // .requestConfiguration(RequestConfiguration.create().setHttpHeaders(new HttpHeaders()))
                .build();

        // Asynchronously get the return value of the API request
        CompletableFuture<SendSmsResponse> response = client.sendSms(sendSmsRequest);
        // Synchronously get the return value of the API request
        SendSmsResponse resp = response.get();
        System.out.println(new Gson().toJson(resp));
        // Asynchronous processing of return values
        /*response.thenAccept(resp -> {
            System.out.println(new Gson().toJson(resp));
        }).exceptionally(throwable -> { // Handling exceptions
            System.out.println(throwable.getMessage());
            return null;
        });*/

        // Finally, close the client
        client.close();
    }

}

简单把这些代码整改一下:

java 复制代码
@Configuration
public class SMSConfig {

    @Value("${spring.cloud.alicloud.access-key}")
    private String accessId;
    @Value("${spring.cloud.alicloud.secret-key}")
    private String secretKey;

    @Bean
    public StaticCredentialProvider provider() {
       return StaticCredentialProvider.create(Credential.builder().accessKeyId(accessId).accessKeySecret(secretKey).build());
    }

}
java 复制代码
@RestController
public class SendSmsController {

    @Autowired
    private StaticCredentialProvider provider;

    /**
     * 提供接口,供别的服务调用
     *
     * @param phone
     * @param code
     * @return "body": {
     * "bizId": "774515119736291045^0",
     * "code": "OK",
     * "message": "OK",
     * "requestId": "D6BD5A90-8755-5C82-B631-0F40AB7B41B0"
     * }
     */
    @GetMapping("/sms/send")
    public R sendSms(@RequestParam("phone") String phone, @RequestParam("code") String code) throws ExecutionException, InterruptedException {

        AsyncClient client = AsyncClient.builder().region("cn-shanghai") // Region ID
                .credentialsProvider(provider).overrideConfiguration(ClientOverrideConfiguration.create().setEndpointOverride("dysmsapi.aliyuncs.com")).build();

        SendSmsRequest sendSmsRequest = SendSmsRequest.builder().signName("阿里云短信测试").templateCode("SMS_154950909").phoneNumbers(phone).templateParam("{\"code\":\"" + code + "\"}").build();

        CompletableFuture<SendSmsResponse> response = client.sendSms(sendSmsRequest);
        SendSmsResponse resp = response.get();

        /*
            {
                "headers": {
                    "Keep-Alive": "timeout\u003d25" ......
                },
                "statusCode": 200,
                "body": {
                    "bizId": "774515119736291045^0",
                    "code": "OK",
                    "message": "OK",
                    "requestId": "D6BD5A90-8755-5C82-B631-0F40AB7B41B0"
                }
            }
         */
        client.close();

        if (resp.getBody().getMessage().equalsIgnoreCase("OK")) return R.ok();
        return R.error(BizCodeEnume.SMS_SEND_EXCEPTION);
    }

}

邮件服务

xml 复制代码
<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4.1</version>
</dependency>
java 复制代码
@Data
public class EmailVo {
    private String receiveMail;
    private String subject;
    private String content;
}
java 复制代码
@Configuration
public class EmailConfig {

// 我在Nacos配置中心配的user和password
    @Value("${mail.user}")
    private String mailUser;

    @Value("${mail.password}")
    private String mailPassword;

    @Bean
    public Properties props() {
        // 创建Properties 类用于记录邮箱的一些属性
        Properties props = new Properties();
        // 表示SMTP发送邮件,必须进行身份验证
        props.put("mail.smtp.auth", "true");
        //此处填写SMTP服务器
        props.put("mail.smtp.host", "smtp.qq.com");
        //端口号,QQ邮箱端口587
        props.put("mail.smtp.port", "587");
        // 此处填写,写信人的账号
        props.put("mail.user", mailUser);
        // 此处填写16位STMP口令
        props.put("mail.password", mailPassword);
        return props;
    }

    @Bean
    public Authenticator authenticator(Properties props) {
        // 构建授权信息,用于进行SMTP进行身份验证
        return new Authenticator() {
            protected PasswordAuthentication getPasswordAuthentication() {
                // 用户名、密码
                String userName = props.getProperty("mail.user");
                String password = props.getProperty("mail.password");
                return new PasswordAuthentication(userName, password);
            }
        };
    }
}
java 复制代码
@RestController
public class SendEmailController {

    @Autowired
    private Properties props;

    @Autowired
    private Authenticator authenticator;

    @PostMapping("/email/send")
    public R sendEmail(@RequestBody EmailTo emailTo) throws MessagingException {
        // 使用环境属性和授权信息,创建邮件会话
        Session mailSession = Session.getInstance(props, authenticator);
        // 创建邮件消息
        MimeMessage message = new MimeMessage(mailSession);
        // 设置发件人
        InternetAddress form = new InternetAddress(props.getProperty("mail.user"));
        message.setFrom(form);
        // 设置收件人的邮箱
        InternetAddress to = new InternetAddress(emailTo.getReceiveMail());
        message.setRecipient(Message.RecipientType.TO, to);
        // 设置邮件标题
        message.setSubject(emailTo.getSubject());
        // 设置邮件的内容体
        message.setContent(emailTo.getContent(), "text/html;charset=UTF-8");
        // 最后当然就是发送邮件啦
        Transport.send(message);

        return R.ok();
    }

}

验证码

短信形式:

java 复制代码
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone) {
//        Redis缓存验证码:存起来方便下次校验 以及 可以给验证码设置有效期

        String code = getRandomCode().toString();

//        防止同一个手机号在60s内再次发送验证码
        String key = AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone;

        String oldCode = stringRedisTemplate.opsForValue().get(key);
        if (!StringUtils.isEmpty(oldCode)) {
            long l = Long.parseLong(oldCode.split("_")[1]);
            if (System.currentTimeMillis() - l < 60000) { // 如果时间间隔小于60s
                return R.error(BizCodeEnume.SMS_MULTI_EXCEPTION);
            }
        }

//        R r = thirdPartyFeignService.sendSms(phone, code);
//        if (r.getCode() == BizCodeEnume.SUCCESS.getCode()) {
//            code = code + "_" + System.currentTimeMillis();
//            stringRedisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES); //过期时间5分钟
//        }
//        return r;

        CompletableFuture.runAsync(() -> thirdPartyFeignService.sendSms(phone, code), threadPool);
        CompletableFuture.runAsync(() -> {
            stringRedisTemplate.opsForValue().set(key, codeResolve(code), 5, TimeUnit.MINUTES); //过期时间5分钟
        }, threadPool);
        return R.ok();
    }

生成验证码(随机四位数):

java 复制代码
    private Integer getRandomCode() {
        //4位数字验证码:想要[1000,9999],也就是[1000,10000)

        // Math.random() -> [0, 1)  // (int) Math.random()永远为0
        // Math.random() * (end - begin) -> [0, end - begin)
        // begin + Math.random() * (end - begin) -> [begin, end)
        int code = (int) (1000 + Math.random() * (10000 - 1000));
        return code;
    }

邮件形式:

java 复制代码
    @GetMapping("/sms/sendEmail")
    public R sendEmailCode(@RequestParam("email") String email) throws MessagingException {
        String code = UUID.randomUUID().toString().substring(0, 5);
        String key = AuthServerConstant.EMAIL_CODE_CACHE_PREFIX + email;

        String oldCode = stringRedisTemplate.opsForValue().get(key);
        if (!StringUtils.isEmpty(oldCode)) { // 说明5分钟内已经给该邮箱发送过验证码了
            long l = Long.parseLong(oldCode.split("_")[1]);
            if (System.currentTimeMillis() - l < 60000) { // 如果时间间隔小于60s
                return R.error(BizCodeEnume.SMS_MULTI_EXCEPTION);
            }
        }

        CompletableFuture.runAsync(() -> {
            // 给Redis放置验证码
            String realSaveCode = code + "_" + System.currentTimeMillis();
            stringRedisTemplate.opsForValue().set(key, realSaveCode, 5, TimeUnit.MINUTES); //过期时间5分钟
        }, threadPool);

        CompletableFuture.runAsync(() -> {
            // 发送邮件
            try {
                EmailTo emailTo = new EmailTo();
                emailTo.setReceiveMail(email);
                emailTo.setContent("验证码:" + code + "------有效期5分钟!");
                emailTo.setSubject("欢迎注册!");
                thirdPartyFeignService.sendEmail(emailTo);
            } catch (MessagingException e) {
                e.printStackTrace();
            }
        }, threadPool);

        return R.ok();
    }

异常机制

java 复制代码
    @PostMapping("/regist")
    public R regist(@RequestBody MemberRegistVo vo){
        try{
            memberService.regist(vo);
        }catch (PhoneExistException e){
            return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION);
        }catch (UsernameExistException e){
            return R.error(BizCodeEnume.USER_EXIST_EXCEPTION);
        }
        return R.ok();
    }
java 复制代码
    @Override
    public void regist(MemberRegistVo vo) {
        //检查用户名和手机号是否唯一。为了让controller能感知异常:异常机制
        String phone = vo.getPhone(); checkPhoneUnique(phone);
        String userName = vo.getUserName(); checkUsernameUnique(userName);

        MemberEntity entity = new MemberEntity();
        entity.setMobile(phone);
        entity.setUsername(userName);
        entity.setNickname(userName);

        //设置默认等级
        MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
        entity.setLevelId(levelEntity.getId());

        //密码要进行加密存储。//当然,也可以在前端就加密发过来
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        entity.setPassword(encode);

        //其他的默认信息
        //保存
        this.baseMapper.insert(entity);
    }
java 复制代码
    @Override
    public void checkPhoneUnique(String phone) throws PhoneExistException {
        Integer mobile = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (mobile > 0) {
            throw new PhoneExistException();
        }
    }

    @Override
    public void checkUsernameUnique(String username) throws UsernameExistException {
        Integer count = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));
        if (count > 0) {
            throw new UsernameExistException();
        }
    }
java 复制代码
public class UsernameExistException extends RuntimeException {
    public UsernameExistException() {
        super("用户名存在");
    }
}

R:

java 复制代码
public class R extends HashMap<String, Object> {
    public static final String CODE = "code";
    public static final String MSG = "msg";
    public static final String DATA = "data";

    //利用fastjson进行逆转
    public <T> T getData(String key, TypeReference<T> typeReference) {
        Object data = get(key);// 默认是map
        String s = JSON.toJSONString(data); // 得转为JSON字符串
        T t = JSON.parseObject(s, typeReference);
        return t;
    }

    //利用fastjson进行逆转
    public <T> T getData(TypeReference<T> typeReference) {
        return getData(DATA, typeReference);
    }

    public R setData(Object data) {
        put(DATA, data);
        return this;
    }

    public R() {
        put(CODE, BizCodeEnume.SUCCESS.getCode());
        put(MSG, BizCodeEnume.SUCCESS.getMsg());
    }

    public static R error() {
        return error("服务器未知异常,请联系管理员");
    }

    public static R error(String msg) {
//        500
        return error(org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    }

    public static R error(int code, String msg) {
        R r = new R();
        r.put(CODE, code);
        r.put(MSG, msg);
        return r;
    }

    public static R error(BizCodeEnume bizCodeEnume) {
        R r = new R();
        r.put(CODE, bizCodeEnume.getCode());
        r.put(MSG, bizCodeEnume.getMsg());
        return r;
    }

    public static R ok(String msg) {
        R r = new R();
        r.put(MSG, msg);
        return r;
    }

    public static R ok(Map<String, Object> map) {
        R r = new R();
        r.putAll(map);
        return r;
    }

    public static R ok() {
        return new R();
    }

    public R put(String key, Object value) {
        super.put(key, value);
        return this;
    }

    public Integer getCode() {
        return (Integer) this.get(CODE);
    }

    public String getMsg() {
        return (String) this.get(MSG);
    }
}
java 复制代码
/***
 * TODO 写博客
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5位数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。
 *      10:通用             000:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *  11: 商品
 *  12: 订单
 *  13: 购物车
 *  14: 物流
 */
public enum BizCodeEnume {
    SUCCESS(0, "OK"),
    HTTP_SUCCESS(200, "OK"),

    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    TOO_MANY_REQUEST(10002,"请求流量过大"),
    SMS_MULTI_EXCEPTION(10003,"验证码获取频率太高,请1分钟后再试"),
    SMS_SEND_EXCEPTION(10004,"验证码发送失败"),
    SMS_CODE_EXCEPTION(10005,"验证码错误"),
    REG_ERROR_EXCEPTION(10006,"用户名或手机已存在,注册失败"),

    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST_EXCEPTION(15001,"用户存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号存在"),
    NO_STOCK_EXCEPTION(21000,"商品库存不足"),
    LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号密码错误");

    private final int code;
    private final String msg;

    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

MD5

MD5:Message Digest algorithm 5,信息摘要算法

  • 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
  • 容易计算:从原数据计算出MD5值很容易。
  • 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
  • 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
  • 不可逆(即使知道加密算法,也不能反推出明文密码): MD5是一种信息摘要算法,会损 失元数据,所以不可逆出原数据是什么

但是,由于MD5的抗修改性和强抗碰撞(一个字符串的MD5值永远是那个值),发明了彩虹表(暴力 破解)。所以,MD5不能直接进行密码的加密存储

加盐:

  • 通过生成随机数与MD5生成字符串进行组合

  • 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可

    百度网盘的秒传:在上传文件之前,计算出该文件的MD5值,看有没有人之前上传过,也就是去匹配百度网盘的数据库中有没有相同的 MD5 值, 如果有一样的就不用传了

java 复制代码
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallAuthApplicationTests {
    @Test
    public void contextLoads() {
        //MD5是不可逆的,但是利用它的抗修改性(一个字符串的MD5值永远是那个值),发明了彩虹表(暴力破解)。
        //所以,MD5不能直接进行密码的加密存储;
//        String s = DigestUtils.md5Hex("123456");

        //盐值加密;随机值 加盐 :$1$ + 8位字符
//        只要是同一个材料,做出来的饭是一样的,如果给饭里随机撒点"盐",那么,饭的口味就不一样了
        //"123456"+System.currentTimeMillis();

        //想要再次验证密码咋办?: 将密码再进行盐值(去数据库查当时保存的随机盐)加密一次,然后再去匹配密码是否正确
//        String s1 = Md5Crypt.md5Crypt("123456".getBytes()); //随机盐
//        String s1 = Md5Crypt.md5Crypt("123456".getBytes(),"$1$qqqqqqqq"); //指定盐
//        System.out.println(s1);

//        给数据库加字段有点麻烦,Spring有好用的工具:
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//        String encode = passwordEncoder.encode("123456");
//        $2a$10$coLmFyeppkTPTfD0RJgqL.nx33s0wvUmj.shqEM/6hvwOO4TWiGmy
//        $2a$10$4IP4F/2iFO2gbSvQKyJzGuI3RhU5Qdtr519KsyoXGAy.b7WT4P1RW
//        $2a$10$0hEI3vMkTbTqK76990MGu.s9QKrkjDSpgyhfzR4zsy07oKB9Jw.PS

//        System.out.println(encode);
//        boolean matches = passwordEncoder.matches("123456", "$2a$10$0hEI3vMkTbTqK76990MGu.s9QKrkjDSpgyhfzR4zsy07oKB9Jw.PS");
        boolean matches = passwordEncoder.matches("lpruoyu123", "$2a$10$m7TmOQAin5Tj6QzV1TT0ceW6iLypdN8LHkYP16DUEngJUfYNgWVEm");
        System.out.println(matches);
    }
}

参考

雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目.


本文完,感谢您的关注支持!


相关推荐
jakeswang27 分钟前
细说分布式ID
分布式
LQ深蹲不写BUG1 小时前
微服务的保护方式以及Sentinel详解
微服务·云原生·架构
失散132 小时前
分布式专题——1.2 Redis7核心数据结构
java·数据结构·redis·分布式·架构
王中阳Go2 小时前
头一次见问这么多kafka的问题
分布式·kafka
鼠鼠我捏,要死了捏3 小时前
基于Apache Flink Stateful Functions的事件驱动微服务架构设计与实践指南
微服务·apache flink·实时处理
boonya4 小时前
Kafka核心原理与常见面试问题解析
分布式·面试·kafka
KIDAKN5 小时前
RabbitMQ 重试机制 和 TTL
分布式·rabbitmq
JAVA学习通5 小时前
【RabbitMQ】----初识 RabbitMQ
分布式·rabbitmq
项目題供诗10 小时前
Hadoop(八)
大数据·hadoop·分布式
勇往直前plus10 小时前
Sentinel微服务保护
java·spring boot·微服务·sentinel