持續學習&持續更新中…
守破離
【雷豐陽-谷粒商城 】【分布式高級篇-微服務架構篇】【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架構師 | 微服務 | 大型電商項目.
本文完,感謝您的關注支持!