Day15用户中心
15.1阿里短信服务
采用异步发送方式
- 短信服务监听MQ消息
- 收到消息后发送短信,根据消息
routing_key
不同,发送不同类型的短信 - 其它服务要发送短信时,通过MQ通知短信微服务。
依赖
spring-boot-starter-amqp
aliyun-java-sdk-core
配置
server:
port: 8085
spring:
application:
name: sms-service
rabbitmq:
host: ly-mq
username: leyou
password: 123321
virtual-host: /leyou
ly:
sms:
accessKeyID: LTAI4G2f8Hbgw8qiK8uunGzE # 你自己的accessKeyId
accessKeySecret: TO90pNZJ4HB2MjomtL76qbYxdCR8QW # 你自己的AccessKeySecret
signName: 乐优商城 # 签名名称
verifyCodeTemplate: SMS_204127315 # 模板名称
domain: dysmsapi.aliyuncs.com # 域名
action: SendSMS # API类型,发送短信
version: 2017-05-25 # API版本,固定值
regionID: cn-hangzhou # 区域id
属性配置类
@Data
@ConfigurationProperties(prefix = "ly.sms")
public class SmsProperties {
String accessKeyID;
....
}
阿里客户端配置类
@Configuration
@EnableConfigurationProperties(SmsProperties.class)
public class SmsConfiguration {
@Bean
public IAcsClient acsClient(SmsProperties prop){
DefaultProfile profile = DefaultProfile.getProfile(
prop.getRegionID(), prop.getAccessKeyID(), prop.getAccessKeySecret());
return new DefaultAcsClient(profile);
}
}
工具类
public void sendVerifyCode(String phone, String code) {
// 参数
String param = String.format(VERIFY_CODE_PARAM_TEMPLATE, code);
// 发送短信
sendMessage(phone, prop.getSignName(), prop.getVerifyCodeTemplate(), param);
}
/**
* 通用的发送短信的方法
*
* @param phone 手机号
* @param signName 签名
* @param template 模板
* @param param 模板参数,json风格
*/
private void sendMessage(String phone, String signName, String template, String param) {
CommonRequest request = new CommonRequest();
request.setProtocol(ProtocolType.HTTPS);
request.setMethod(MethodType.POST);
request.setDomain(prop.getDomain());
request.setVersion(prop.getVersion());
request.setAction(prop.getAction());
request.putQueryParameter(SMS_PARAM_KEY_PHONE, phone);
request.putQueryParameter(SMS_PARAM_KEY_SIGN_NAME, signName);
request.putQueryParameter(SMS_PARAM_KEY_TEMPLATE_CODE, template);
request.putQueryParameter(SMS_PARAM_KEY_TEMPLATE_PARAM, param);
try {
CommonResponse response = client.getCommonResponse(request);
if (response.getHttpStatus() >= 300) {
log.error("【SMS服务】发送短信失败。响应信息:{}", response.getData());
}
// 获取响应体
Map<String, String> resp = JsonUtils.toMap(response.getData(), String.class, String.class);
// 判断是否是成功
if (!StringUtils.equals(OK, resp.get(SMS_RESPONSE_KEY_CODE))) {
// 不成功,
log.error("【SMS服务】发送短信失败,原因{}", resp.get(SMS_RESPONSE_KEY_MESSAGE));
}
log.info("【SMS服务】发送短信成功,手机号:{}, 响应:{}", phone, response.getData());
} catch (ServerException e) {
log.error("【SMS服务】发送短信失败,服务端异常。", e);
} catch (ClientException e) {
log.error("【SMS服务】发送短信失败,客户端异常。", e);
}
}
监听器
@Component
public class MessageListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = QueueConstants.SMS_VERIFY_CODE_QUEUE, durable = "true"),
exchange = @Exchange(name = ExchangeConstants.SMS_EXCHANGE_NAME, type = ExchangeTypes.TOPIC),
key = RoutingKeyConstants.VERIFY_CODE_KEY
))
public void listenVerifyCodeMessage(Map<String,String> msg){
// 获取参数
if(CollectionUtils.isEmpty(msg)){
// 如果消息为空,不处理
return;
}
// 手机号
String phone = msg.get("phone");
if (!RegexUtils.isPhone(phone)) {
// 手机号有误,不处理
return;
}
// 验证码
String code = msg.get("code");
if (!RegexUtils.isCodeValid(code)) {
// 验证码有误,不处理
return;
}
// 发送短信
try {
smsUtils.sendVerifyCode(phone, code);
} catch (Exception e) {
// 短信发送失败,我不想重试,异常捕获
log.error("【SMS服务】短信验证码发送失败", e);
}
}
15.2用户中心
ly-user
- ly-user-api:接口
- ly-user-pojo:实体
- ly-user-service:业务和服务
添加网关路由
业务代码
- 根据用户名和密码查询
passwordEncoder.matches()
方法进行密码比对
- 注册
- 校验验证码
- 对密码加密
- 写入数据库
- 发送短信验证码
- 我们接收页面发送来的手机号码
- 使用Apache的工具类生成一个随机验证码
- 将验证码保存在服务端(要用redis代替session)
- 发送短信,将验证码发送到用户手机(向MQ发送消息)
- 判断信息是否存在
加密算法
-
Spring提供的BCryptPasswordEncoder
- 加密:算法会对明文密码随机生成一个salt,使用salt结合密码来加密,得到最终的密文
- 验证:需要先拿到加密后的密码和要验证的密码,根据已加密的密码来推测出salt,然后利用相同的算法和salt对要验证码的密码加密,与已加密的密码对比即可。
-
配置类
@Data @Configuration @ConfigurationProperties(prefix = "ly.encoder.crypt") public class PasswordConfig { private int strength; private String secret; @Bean public BCryptPasswordEncoder passwordEncoder(){ // 利用密钥生成随机安全码 SecureRandom secureRandom = new SecureRandom(secret.getBytes()); // 初始化BCryptPasswordEncoder return new BCryptPasswordEncoder(strength, secureRandom); } }
-
配置属性
ly: encoder: crypt: secret: ${random.uuid} # 随机的密钥,使用uuid strength: 6 # 加密强度4~31,决定盐加密时的运算强度,超过10以后加密耗时会显著增加
15.3Hibernate Validator
-
Hibernate提供的一个开源框架,使用注解方式非常方便的实现服务端的数据校验。
-
SpringBoot的web启动器中已经集成了相关依赖
Bean校验的注解
Constraint | 详细信息 |
---|---|
@Valid | 被注释的元素是一个对象,需要检查此对象的所有字段值 |
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
@NotBlank | 被注释的字符串的必须非空 |
@URL(protocol=,host=, port=,regexp=, flags=) | 被注释的字符串必须是一个有效的url |
@CreditCardNumber | 被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性 |
使用步骤
- 给User添加校验
@Pattern(regexp = "...", message = "...")
- 在controller中只需要给User添加 @Valid注解即可
自定义返回结果
-
额外添加一个参数
BindingResult result
/** * 注册功能 * @return 无 */ @PostMapping("register") public ResponseEntity<Void> register(@Valid User user, BindingResult result, @RequestParam("code") String code){ if (result.hasErrors()) { String msg = result.getFieldErrors().stream().map(FieldError::getDefaultMessage) .collect(Collectors.joining("|")); throw new LyException(400, msg); } userService.register(user, code); return ResponseEntity.status(HttpStatus.CREATED).build(); }
Day16JWT
16.1无状态登录
- 有状态
- 服务端需要记录每次会话的客户端信息,从而识别客户端身份
- 如tomcat中的session
- 缺点
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,无法进行水平扩展
- 客户端请求依赖服务端,多次请求必须访问同一台服务器
- 无状态
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备
自描述信息
,通过这些信息识别客户端身份 - 流程
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的token
- 服务端对token进行解密,判断是否有效。
16.2JWT简介
JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权
数据格式:
-
Header:头部,通常头部有两部分信息:
- token类型,这里是JWT
- 签名算法,自定义
我们会对头部进行base64加密(可解密),得到第一部分数据
-
Payload:载荷,就是有效数据,一般包含下面信息:
- 标准载荷:JWT规定的信息,jwt的元数据:
- JTI: JWT的id,当前jwt的唯一标识(像身份证号)
- IAT: issue at 签发时间
- EXP:过期时间
- SUB:签发人
- …
- 自定义载荷:
- 用户身份信息,(注意,这里因为采用base64加密,可解密,因此不要存放敏感信息)
这部分也会采用base64加密,得到第二部分数据
- 标准载荷:JWT规定的信息,jwt的元数据:
-
Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
JWT登录流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x5sXteQd-1605669098933)(assets/image-20200614172855833.png)]
16.3秘钥管理
微服务获取保存在auth中的密钥流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uiiKtYkU-1605669098935)(assets/image-20200614210546978.png?lastModify=1602564312)]
16.4登录验证
16.5页面获取用户信息
16.6登录超时控制
16.7刷新登录时间
信息)
这部分也会采用base64加密,得到第二部分数据
- Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
JWT登录流程
[外链图片转存中…(img-x5sXteQd-1605669098933)]
16.3秘钥管理
微服务获取保存在auth中的密钥流程
[外链图片转存中…(img-uiiKtYkU-1605669098935)]