10_项目二Ⅱ

博客围绕用户中心和JWT无状态登录展开。用户中心涉及阿里短信服务、用户注册登录等功能,采用异步发送短信,使用Redis保存验证码,用Spring的加密算法加密密码。JWT部分介绍了无状态登录流程、JWT的结构和登录流程,还提及秘钥管理、登录验证等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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()方法进行密码比对
  • 注册
    • 校验验证码
    • 对密码加密
    • 写入数据库
  • 发送短信验证码
    1. 我们接收页面发送来的手机号码
    2. 使用Apache的工具类生成一个随机验证码
    3. 将验证码保存在服务端(要用redis代替session)
    4. 发送短信,将验证码发送到用户手机(向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)被注释的元素必须符合指定的正则表达式
@Email被注释的元素必须是电子邮箱地址
@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
    • 缺点
      • 服务端保存大量数据,增加服务端压力
      • 服务端保存用户状态,无法进行水平扩展
      • 客户端请求依赖服务端,多次请求必须访问同一台服务器
  • 无状态
    • 服务端不保存任何客户端请求者信息
    • 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
    • 流程
      1. 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
      2. 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
      3. 以后每次请求,客户端都携带认证的token
      4. 服务端对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加密,得到第二部分数据

  • 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)]

16.4登录验证

16.5页面获取用户信息

16.6登录超时控制

16.7刷新登录时间

16.8退出登录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值