持续学习&持续更新中…
守破离
【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【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_route
uri: lb://gulimall-auth
predicates:
- 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 {
/**
* 视图映射
*/
@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");
}
}
验证码倒计时
前端:
$(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, 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();
}
}
简单把这些代码整改一下:
@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());
}
}
@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);
}
}
邮件服务
<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;
@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);
}
};
}
}
@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();
}
}
验证码
短信形式:
@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();
}
生成验证码(随机四位数):
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) { // 如果时间间隔小于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();
}
异常机制
@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();
}
@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);
}
@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();
}
}
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);// 默认是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);
}
}
/***
* 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 {
@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架构师 | 微服务 | 大型电商项目.
本文完,感谢您的关注支持!