持续学习&持续更新中…
守破离
【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【17】认证服务01
- 环境搭建
- 验证码倒计时
- 短信服务
- 邮件服务
- 验证码
- 短信形式:
- 邮件形式:
- 异常机制
- MD5
- 参考
环境搭建
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
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架构师 | 微服务 | 大型电商项目.
本文完,感谢您的关注支持!