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

news/2024/10/6 0:51:06/

持续学习&持续更新中…

守破离


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

环境搭建

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

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;
}

网关:

        - id: gulimall_auth_routeuri: lb://gulimall-authpredicates:- Host=auth.gulimall.com

gulimall-auth:

@Controller
public class LoginController {@GetMapping("/login.html")public String loginPage() {return "login";}@GetMapping("/reg.html")public String regPage() {return "reg";}
}

或者:

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {/*** 视图映射*/@Overridepublic 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");}
}

验证码倒计时

前端:

在这里插入图片描述

    $(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的权限。

在这里插入图片描述

        <dependency><groupId>com.aliyun</groupId><artifactId>alibabacloud-dysmsapi20170525</artifactId><version>3.0.0</version></dependency>
// 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, tokenStaticCredentialProvider 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 ClientAsyncClient 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 requestSendSmsRequest 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 requestCompletableFuture<SendSmsResponse> response = client.sendSms(sendSmsRequest);// Synchronously get the return value of the API requestSendSmsResponse 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 exceptionsSystem.out.println(throwable.getMessage());return null;});*/// Finally, close the clientclient.close();}}

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

@Configuration
public class SMSConfig {@Value("${spring.cloud.alicloud.access-key}")private String accessId;@Value("${spring.cloud.alicloud.secret-key}")private String secretKey;@Beanpublic StaticCredentialProvider provider() {return StaticCredentialProvider.create(Credential.builder().accessKeyId(accessId).accessKeySecret(secretKey).build());}}
@RestController
public class SendSmsController {@Autowiredprivate 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);}}

邮件服务

<dependency><groupId>javax.mail</groupId><artifactId>mail</artifactId><version>1.4.1</version>
</dependency>
@Data
public class EmailVo {private String receiveMail;private String subject;private String content;
}
@Configuration
public class EmailConfig {// 我在Nacos配置中心配的user和password@Value("${mail.user}")private String mailUser;@Value("${mail.password}")private String mailPassword;@Beanpublic Properties props() {// 创建Properties 类用于记录邮箱的一些属性Properties props = new Properties();// 表示SMTP发送邮件,必须进行身份验证props.put("mail.smtp.auth", "true");//此处填写SMTP服务器props.put("mail.smtp.host", "smtp.qq.com");//端口号,QQ邮箱端口587props.put("mail.smtp.port", "587");// 此处填写,写信人的账号props.put("mail.user", mailUser);// 此处填写16位STMP口令props.put("mail.password", mailPassword);return props;}@Beanpublic 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);}};}
}
@RestController
public class SendEmailController {@Autowiredprivate Properties props;@Autowiredprivate 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();}}

在这里插入图片描述

验证码

短信形式:

    @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) { // 如果时间间隔小于60sreturn 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();}

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

    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;}

邮件形式:

    @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) { // 如果时间间隔小于60sreturn 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();}

异常机制

    @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();}
    @Overridepublic 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);}
    @Overridepublic void checkPhoneUnique(String phone) throws PhoneExistException {Integer mobile = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));if (mobile > 0) {throw new PhoneExistException();}}@Overridepublic void checkUsernameUnique(String username) throws UsernameExistException {Integer count = this.baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", username));if (count > 0) {throw new UsernameExistException();}}
public class UsernameExistException extends RuntimeException {public UsernameExistException() {super("用户名存在");}
}

R:

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);// 默认是mapString 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) {
//        500return 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);}
}
/**** 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_860">MD5

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

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

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

加盐:

  • 通过生成随机数与MD5生成字符串进行组合
  • 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可
百度网盘的秒传:在上传文件之前,计算出该文件的MD5值,看有没有人之前上传过,也就是去匹配百度网盘的数据库中有没有相同的 MD5 值, 如果有一样的就不用传了 
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallAuthApplicationTests {@Testpublic 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架构师 | 微服务 | 大型电商项目.


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



http://www.ppmy.cn/news/1473799.html

相关文章

Android 换肤之主题换肤

文章目录 Android 换肤之主题换肤概述效果实现代码结构定义属性定义主题在Activity中使用在Fragment中使用工具类 源码下载 Android 换肤之主题换肤 概述 Android 实现应用内换肤的常用方式&#xff08;两种&#xff09;&#xff1a; 通过Theme切换主题&#xff0c;即静态方…

产品经理-的职业发展(9)

找一份好工作&#xff0c;就是为了获得更好的职业发展&#xff0c;下面分别给大家介绍下大、中、小型公司的职业发展路径 中小型公司 中小型公司的规模往往相对不大&#xff0c;又处于飞速发展过程中&#xff0c;培养体系和晋升标准都不够成熟&#xff0c;所以实际的职业发展路…

项目2:API Hunter 细节回顾 -1

一. 接口调用 对于开发者来说&#xff0c;接口的调用应当是方便快捷的&#xff0c;而且出于安全考虑&#xff0c;通常会选择在后端调用第三方 API&#xff0c;避免在前端暴露诸如密码的敏感信息。 若采用 HTTP 调用方式&#xff1a; HttpClientRestTemplate第三方库&#xf…

Python入门 2024/7/6

目录 元组的定义和操作 字符串的定义和操作 字符串 字符串的替换 字符串的分割 字符串的规整操作&#xff08;去除前后空格&#xff09; 字符串的规整操作&#xff08;去掉前后指定字符串&#xff09; 操作 字符串的替换 字符串的分割 字符串的规整操作 统计字符串的…

前端面试题21(js排序方法)

JavaScript 中有多种内置和自定义的排序方法。内置的 .sort() 方法是最直接的排序方式&#xff0c;而自定义排序算法如冒泡排序、选择排序、插入排序、希尔排序、快速排序等则提供了更深层次的学习和应用价值。下面我将详细介绍这些排序方法&#xff0c;并给出相应的示例代码。…

Linux 常用指令详解

Linux 是一个强大而灵活的操作系统&#xff0c;掌握常用的 Linux 指令是使用和管理 Linux 系统的基础。本文将介绍一些常用的 Linux 指令&#xff0c;并附上 Vim 和 g 的常用指令说明&#xff0c;帮助你更好地进行开发和操作。 1. 基本文件操作指令 1.1 显示目录内容 ls常用…

存储结构与管理磁盘

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 目录 一、一切从“/”开始 二、物理设备的命名规则 三、文件系统与数据资料 四、挂载硬件设备 五、添加硬盘设备 六、添加交换分区 七、磁盘容…

如何从相机的存储卡中恢复原始照片

“不好了。” 当您意识到自己不小心从存储卡中删除了照片&#xff0c;或者错误地格式化了相机的记忆棒时&#xff0c;您首先会喊出这两个词。这是一种常见的情况&#xff0c;每个人一生中都会遇到这种情况。幸运的是&#xff0c;有办法从相机的 RAW 记忆棒中恢复已删除的照片。…