【java功能大全】1.手机短信验证码一键注册登录流程(redis存储证码+redis锁机制限制ip发短信+拦截器限制60秒短信发送+封装优雅异常+Hibernate-Validate校验)

目录

发送验证码

注册登录

用户表设计

​编辑申请腾讯云短信与密钥

找到云短信服务

开通腾讯云短信服务

​编辑​​​​​创建短信签名

​编辑​编辑创建短信正文模版​编辑​编辑

等待审核

测试短信​编辑

SDK密钥创建

SpringBoot集成腾讯云短信

pom中导入腾讯云短信的sdk坐标:

resource创建资源文件,放入腾讯云短信的信息

构建资源类,和秘钥信息做好映射,方便后续获得

发送短信源码(可在腾讯云官网查询源码)

在controller中测试发送

redis相关操作

引入依赖

yml中配置redis

基础信息属性类中注入redis工具类

附:完整的基础信息属性类

附:redis工具类

代码实现(redis存储验证码+redis锁机制限制ip发短信)

附:用户获得用户ip的工具类

使用拦截器限制60秒短信发送

增加拦截器

注册拦截器

封装优雅异常降低代码侵入性

自定义异常类

全局异常处理器

优雅异常处理类

附:自定义响应数据类型枚举升级版

附:响应结果枚举

使用Hibernate-Validate进行参数校验

bean属性参数校验

用户一键注册登录代码实现

controller(添加了redis token)

service


发送验证码


1.点击按钮(获得验证码)
2.设置:60秒内只能获得一次验证码
3.设置:验证码有效时间(5分钟/15分钟/30分钟)
4.发送验证码到手机

注册登录

1.点击按钮 注册登录
2.判断验证码有效
3.查询判断用户是否存在
    a.不存在,则注册
    b. 存在,则登录
4.删除已使用的短信验证码
5.创建用户令牌并且协同用户信息返回给前端

用户表设计


申请腾讯云短信与密钥

找到云短信服务

  • 注册腾讯云
  • 个人实名认证
  • 进入到控制台,找到短信(或搜索即可或云产品中找到短信)

开通腾讯云短信服务

开通云短信服务,开通短信服务后才能发短信。


​​​​​创建短信签名



创建短信正文模版

等待审核

由于目前腾讯云短信只支持他用(公司),自用还在跟运营商沟通,如果后期运营商还不给予通过,腾讯云会修改该功能

测试短信

SDK密钥创建

在云产品找到访问秘钥



新建秘钥

SpringBoot集成腾讯云短信

pom中导入腾讯云短信的sdk坐标:

<!-- 第三方云厂商相关依赖 -->

<dependency>

    <groupId>com.tencentcloudapi</groupId>

    <artifactId>tencentcloud-sdk-java</artifactId>

    <!-- go to https://search.maven.org/search?q=tencentcloud-sdk-java and get the latest version. -->


    <!-- 请到https://search.maven.org/search?q=tencentcloud-sdk-java查询所有版本,最新版本如下 -->


    <version>3.1.598</version>

</dependency

resource创建资源文件,放入腾讯云短信的信息

构建资源类,和秘钥信息做好映射,方便后续获得

@Component
@Data
@PropertySource("classpath:tencentCloud.properties")
@ConfigurationProperties(prefix = "tencent.cloud")
public class TencentCloudProperties {

    private String SecretId;
    private String SecretKey;

}

发送短信源码(可在腾讯云官网查询源码)


修改后的发送短信源码

@Component
public class SMSUtils {
    @Autowired
    private TencentCloudProperties tencentCloudProperties;

    public void sendSMS(String phone, String code) throws Exception {
        try {
            /* 必要步骤:
             * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
             * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
             * 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
             * 以免泄露密钥对危及你的财产安全。
             * CAM密匙查询获取: https://console.cloud.tencent.com/cam/capi*/
            Credential cred = new Credential(tencentCloudProperties.getSecretId(),
                    tencentCloudProperties.getSecretKey());

            // 实例化一个http选项,可选的,没有特殊需求可以跳过
            HttpProfile httpProfile = new HttpProfile();

//            httpProfile.setReqMethod("POST"); // 默认使用POST

            /* SDK会自动指定域名。通常是不需要特地指定域名的,但是如果你访问的是金融区的服务
             * 则必须手动指定域名,例如sms的上海金融区域名: sms.ap-shanghai-fsi.tencentcloudapi.com */
            httpProfile.setEndpoint("sms.tencentcloudapi.com");

            // 实例化一个client选项
            ClientProfile clientProfile = new ClientProfile();
            clientProfile.setHttpProfile(httpProfile);
            // 实例化要请求产品的client对象,clientProfile是可选的
            SmsClient client = new SmsClient(cred, "ap-nanjing", clientProfile);

            // 实例化一个请求对象,每个接口都会对应一个request对象
            SendSmsRequest req = new SendSmsRequest();
            String[] phoneNumberSet1 = {"+86" + phone};//电话号码
            req.setPhoneNumberSet(phoneNumberSet1);
            req.setSmsSdkAppId("1400568450");   // 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId
            req.setSignName("火热男");         // 签名(创建签名中的签名内容)
            req.setTemplateId("1108902");       // 模板id:必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看

            /* 模板参数(自定义占位变量): 若无模板参数,则设置为空 */
            String[] templateParamSet1 = {code};
            req.setTemplateParamSet(templateParamSet1);

            // 返回的resp是一个SendSmsResponse的实例,与请求对象对应
            SendSmsResponse resp = client.SendSms(req);
            // 输出json格式的字符串回包
//            System.out.println(SendSmsResponse.toJsonString(resp));
        } catch (TencentCloudSDKException e) {
            System.out.println(e.toString());
        }
    }

//    public static void main(String[] args) {
//        try {
//            new SMSUtils().sendSMS("18812345612", "7896");
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
//    }
}


在controller中测试发送


    @Autowired
    private SMSUtils smsUtils;

    @GetMapping("sms")
    public Object sms() throws Exception {

        smsUtils.sendSMS(MyInfo.getMobile(), "9875");

        return "Send SMS OK~~~";
    }

redis相关操作

引入依赖

<!-- 引入Redis依赖 -->

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

yml中配置redis

spring:
  redis:
    host: 192.168.138.128
    port: 6379
    password: redis

基础信息属性类中注入redis工具类

//基础信息属性
public class BaseInfoProperties {

    @Autowired
    public RedisOperator redis;


}

附:完整的基础信息属性类
 

package com.imooc.base;

import com.github.pagehelper.PageInfo;
import com.google.gson.Gson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

public class BaseInfoProperties {

    @Autowired
    public RedisOperator redis;

    public static final String SYMBOL_DOT = ".";       // 小圆点,无意义,可用可不用

    public static final String TOKEN_USER_PREFIX = "app";       // app端的用户token前缀
    public static final String TOKEN_SAAS_PREFIX = "saas";      // 企业saas平台的用户token前缀
    public static final String TOKEN_ADMIN_PREFIX = "admin";    // 运营管理平台的用户token前缀

    public static final String APP_USER_JSON = "app-user-json";       // app端的用户
    public static final String SAAS_USER_JSON = "saas-user-json";      // 企业saas平台的用户
    public static final String ADMIN_USER_JSON = "admin-user-json";    // 运营管理平台的用户

    public static final Integer COMMON_START_PAGE = 1;
    public static final Integer COMMON_START_PAGE_ZERO = 0;
    public static final Integer COMMON_PAGE_SIZE = 10;

    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";
    public static final String REDIS_USER_INFO = "redis_user_info";

    public static final String REDIS_ADMIN_TOKEN = "redis_admin_token";
    public static final String REDIS_ADMIN_INFO = "redis_admin_info";

    public static final String SAAS_PLATFORM_LOGIN_TOKEN = "saas_platform_login_token";
    public static final String SAAS_PLATFORM_LOGIN_TOKEN_READ = "saas_platform_login_token_read";

    public static final String REDIS_SAAS_USER_TOKEN = "redis_saas_user_token";
    public static final String REDIS_SAAS_USER_INFO = "redis_saas_user_info";

    // 某个字典code下所对应的所有字典列表
    public static final String REDIS_DATA_DICTIONARY_ITEM_LIST = "redis_data_dictionary_item_list";

    // 企业信息相关
    public static final String REDIS_COMPANY_BASE_INFO = "company_base_info";
    public static final String REDIS_COMPANY_MORE_INFO = "company_more_info";
    public static final String REDIS_COMPANY_PHOTOS = "redis_company_photos";
    public static final String REDIS_COMPANY_IS_VIP = "redis_company_is_vip";

    // 用户简历信息
    public static final String REDIS_RESUME_INFO = "redis_resume_info";
    public static final String REDIS_MAX_RESUME_REFRESH_COUNTS = "max_resume_refresh_counts";
    public static final String ZK_MAX_RESUME_REFRESH_COUNTS = "max_resume_refresh_counts";
    public static final String CACHE_MAX_RESUME_REFRESH_COUNTS = "max_resume_refresh_counts";
    public static final String USER_ALREADY_REFRESHED_COUNTS = "user_already_refreshed_counts";
    public static final String REDIS_RESUME_EXPECT = "redis_resume_expect";

    public static final String DELAY_ERROR_RETRY_COUNTS = "delay_error_retry_counts";

    public static final String HR_COLLECT_RESUME_COUNTS = "hr_collect_resume_counts";
    public static final String HR_READ_RESUME_RECORD_COUNTS = "hr_read_resume_record_counts";
    public static final String WHO_LOOK_ME_COUNTS = "who_look_me_counts";
    public static final String CAND_FOLLOW_HR_COUNTS = "cand_follow_hr_counts";
    public static final String CAND_COLLECT_JOB_COUNTS = "cand_collect_job_counts";

    // HR的面试记录数
    public static final String HR_INTERVIEW_RECORD_COUNTS = "hr_interview_record_counts";
    // 候选人的面试记录数
    public static final String CAND_INTERVIEW_RECORD_COUNTS = "cand_interview_record_counts";

    // 职位信息
    public static final String REDIS_JOB_DETAIL = "redis_job_detail";
    public static final String HR_ALL_JOB_COUNTS = "hr_all_job_counts";

    public static final String CHAT_MSG_LIST = "chat_msg_list";

    // 文章阅读总数
    public static final String REDIS_ARTICLE_READ_COUNTS = "redis_article_read_counts";
    // 标记用户阅读,与article关系
    public static final String REDIS_USER_READ_ARTICLE = "redis_user_read_article";

    // 短视频的评论总数
    public static final String REDIS_VLOG_COMMENT_COUNTS = "redis_vlog_comment_counts";
    // 短视频的评论喜欢数量
    public static final String REDIS_VLOG_COMMENT_LIKED_COUNTS = "redis_vlog_comment_liked_counts";
    // 用户点赞评论
    public static final String REDIS_USER_LIKE_COMMENT = "redis_user_like_comment";

    // 我的关注总数
    public static final String REDIS_MY_FOLLOWS_COUNTS = "redis_my_follows_counts";
    // 我的粉丝总数
    public static final String REDIS_MY_FANS_COUNTS = "redis_my_fans_counts";
    // 博主和粉丝的关联关系,用于判断他们是否互粉
    public static final String REDIS_FANS_AND_VLOGGER_RELATIONSHIP = "redis_fans_and_vlogger_relationship";

    // 视频和发布者获赞数
    public static final String REDIS_VLOG_BE_LIKED_COUNTS = "redis_vlog_be_liked_counts";
    public static final String REDIS_VLOGER_BE_LIKED_COUNTS = "redis_vloger_be_liked_counts";

    // 用户是否喜欢/点赞视频,取代数据库的关联关系,1:喜欢,0:不喜欢(默认) redis_user_like_vlog:{userId}:{vlogId}
    public static final String REDIS_USER_LIKE_VLOG = "redis_user_like_vlog";


    // 支付中心地址 - 创建商户订单
//    public static final String PAYMENT_URL_CREATE_MERCHANT_ORDER = "http://192.168.1.6:9060/payment/createMerchantOrder";		// dev
    public static final String PAYMENT_URL_CREATE_MERCHANT_ORDER = "http://172.17.172.127:9060/payment/createMerchantOrder";		// prod
//    String PAYMENT_URL_CREATE_MERCHANT_ORDER = "http://payment.t.mukewang.com/foodie-payment/payment/createMerchantOrder";		// produce
    // 支付中心地址 - 获得微信支付二维码
//    public static final String PAYMENT_URL_GET_WXPAY_QRCODE = "http://192.168.1.6:9060/payment/getWXPayQRCode";		// dev
    public static final String PAYMENT_URL_GET_WXPAY_QRCODE = "http://172.17.172.127:9060/payment/getWXPayQRCode";		// prod
//    String PAYMENT_URL_GET_WXPAY_QRCODE = "http://payment.t.mukewang.com/foodie-payment/payment/getWXPayQRCode";		// produce

    // 某某网 - 支付后的回调通知api接口地址
//    public static final String PAY_RETURN_URL = "http://192.168.1.6:6001/tradeOrder/notifyMerchantOrderPaid";             // dev
    public static final String PAY_RETURN_URL = "http://172.17.172.127:6001/tradeOrder/notifyMerchantOrderPaid";             // prod
//    public static final String PAY_RETURN_URL = "http://api.t.mukewang.com/foodie-api/tradeOrder/notifyMerchantOrderPaid";        // prod


//    public Map<String, String> getErrors(BindingResult result) {
//        Map<String, String> map = new HashMap<>();
//        List<FieldError> errorList = result.getFieldErrors();
//        for (FieldError ff : errorList) {
//            // 错误所对应的属性字段名
//            String field = ff.getField();
//            // 错误的信息
//            String msg = ff.getDefaultMessage();
//            map.put(field, msg);
//        }
//        return map;
//    }

    public PagedGridResult setterPagedGrid(List<?> list,
                                           Integer page) {
        PageInfo<?> pageList = new PageInfo<>(list);
        PagedGridResult gridResult = new PagedGridResult();
        gridResult.setRows(list);
        gridResult.setPage(page);
        gridResult.setRecords(pageList.getTotal());
        gridResult.setTotal(pageList.getPages());
        return gridResult;
    }

}

附:redis工具类


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;


@Component
public class RedisOperator {
	
	@Autowired
	private StringRedisTemplate redisTemplate;

	// Key(键),简单的key-value操作

	/**
	 * 判断key是否存在
	 * @param key
	 * @return
	 */
	public boolean keyIsExist(String key) {
		return redisTemplate.hasKey(key);
	}

	/**
	 * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
	 * 
	 * @param key
	 * @return
	 */
	public long ttl(String key) {
		return redisTemplate.getExpire(key);
	}
	
	/**
	 * 实现命令:expire 设置过期时间,单位秒
	 * 
	 * @param key
	 * @return
	 */
	public void expire(String key, long timeout) {
		redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
	}
	
	/**
	 * 实现命令:increment key,增加key一次
	 * 
	 * @param key
	 * @return
	 */
	public long increment(String key, long delta) {
		return redisTemplate.opsForValue().increment(key, delta);
	}

	/**
	 * 累加,使用hash
	 */
	public long incrementHash(String name, String key, long delta) {
		return redisTemplate.opsForHash().increment(name, key, delta);
	}

	/**
	 * 累减,使用hash
	 */
	public long decrementHash(String name, String key, long delta) {
		delta = delta * (-1);
		return redisTemplate.opsForHash().increment(name, key, delta);
	}

	/**
	 * hash 设置value
	 */
	public void setHashValue(String name, String key, String value) {
		redisTemplate.opsForHash().put(name, key, value);
	}

	/**
	 * hash 获得value
	 */
	public String getHashValue(String name, String key) {
		return (String)redisTemplate.opsForHash().get(name, key);
	}

	/**
	 * 实现命令:decrement key,减少key一次
	 *
	 * @param key
	 * @return
	 */
	public long decrement(String key, long delta) {
		return redisTemplate.opsForValue().decrement(key, delta);
	}

	/**
	 * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
	 */
	public Set<String> keys(String pattern) {
		return redisTemplate.keys(pattern);
	}

	/**
	 * 实现命令:DEL key,删除一个key
	 * 
	 * @param key
	 */
	public void del(String key) {
		redisTemplate.delete(key);
	}

	// String(字符串)

	/**
	 * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, String value) {
		redisTemplate.opsForValue().set(key, value);
	}

	/**
	 * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
	 * 
	 * @param key
	 * @param value
	 * @param timeout
	 *            (以秒为单位)
	 */
	public void set(String key, String value, long timeout) {
		redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
	}

	/**
	 * 如果key不存在,则设置,如果存在,则不操作
	 * @param key
	 * @param value
	 */
	public void setnx60s(String key, String value) {
		redisTemplate.opsForValue().setIfAbsent(key, value, 60, TimeUnit.SECONDS);
	}

	/**
	 * 如果key不存在,则设置,如果存在,则报错
	 * @param key
	 * @param value
	 */
	public void setnx(String key, String value) {
		redisTemplate.opsForValue().setIfAbsent(key, value);
	}

	/**
	 * 实现命令:GET key,返回 key所关联的字符串值。
	 * 
	 * @param key
	 * @return value
	 */
	public String get(String key) {
		return (String)redisTemplate.opsForValue().get(key);
	}

	/**
	 * 批量查询,对应mget
	 * @param keys
	 * @return
	 */
	public List<String> mget(List<String> keys) {
		return redisTemplate.opsForValue().multiGet(keys);
	}

	/**
	 * 批量查询,管道pipeline
	 * @param keys
	 * @return
	 */
	public List<Object> batchGet(List<String> keys) {

//		nginx -> keepalive
//		redis -> pipeline

		List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
			@Override
			public String doInRedis(RedisConnection connection) throws DataAccessException {
				StringRedisConnection src = (StringRedisConnection)connection;

				for (String k : keys) {
					src.get(k);
				}
				return null;
			}
		});

		return result;
	}


	// Hash(哈希表)

	/**
	 * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
	 * 
	 * @param key
	 * @param field
	 * @param value
	 */
	public void hset(String key, String field, Object value) {
		redisTemplate.opsForHash().put(key, field, value);
	}

	/**
	 * 实现命令:HGET key field,返回哈希表 key中给定域 field的值
	 * 
	 * @param key
	 * @param field
	 * @return
	 */
	public String hget(String key, String field) {
		return (String) redisTemplate.opsForHash().get(key, field);
	}

	/**
	 * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
	 * 
	 * @param key
	 * @param fields
	 */
	public void hdel(String key, Object... fields) {
		redisTemplate.opsForHash().delete(key, fields);
	}

	/**
	 * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
	 * 
	 * @param key
	 * @return
	 */
	public Map<Object, Object> hgetall(String key) {
		return redisTemplate.opsForHash().entries(key);
	}

	// List(列表)

	/**
	 * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
	 * 
	 * @param key
	 * @param value
	 * @return 执行 LPUSH命令后,列表的长度。
	 */
	public long lpush(String key, String value) {
		return redisTemplate.opsForList().leftPush(key, value);
	}

	/**
	 * 实现命令:LPOP key,移除并返回列表 key的头元素。
	 * 
	 * @param key
	 * @return 列表key的头元素。
	 */
	public String lpop(String key) {
		return (String)redisTemplate.opsForList().leftPop(key);
	}

	/**
	 * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
	 * 
	 * @param key
	 * @param value
	 * @return 执行 LPUSH命令后,列表的长度。
	 */
	public long rpush(String key, String value) {
		return redisTemplate.opsForList().rightPush(key, value);
	}

}

代码实现(redis存储验证码+redis锁机制限制ip发短信)

@RestController
@RequestMapping("passport")
@Slf4j
public class PassportController extends BaseInfoProperties {

    @Autowired
    private SMSUtils smsUtils;


    @PostMapping("getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile,
                                      HttpServletRequest request) throws Exception {

        if (StringUtils.isBlank(mobile)) {
            return GraceJSONResult.error();
        }

        //获得验证码的同时,设置一个ip占位,类似于锁的机制
        // 获得用户ip
        String userIp = IPUtil.getRequestIp(request);
        // 限制用户只能在60s以内获得一次验证码
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, mobile);

        String code = (int)((Math.random() * 9 + 1) * 100000) + "";
        smsUtils.sendSMS(mobile, code);
        log.info("验证码为:{}", code);

        // 把验证码存入到redis,用于后续的注册登录进行校验
        redis.set(MOBILE_SMSCODE + ":" + mobile, code, 30 * 60);

        return GraceJSONResult.ok();
    }

}

附:用户获得用户ip的工具类

import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;

/**
 * 用户获得用户ip的工具类
 */
public class IPUtil {

    /**
     * 获取请求IP:
     * 用户的真实IP不能使用request.getRemoteAddr()
     * 这是因为可能会使用一些代理软件,这样ip获取就不准确了
     * 此外我们如果使用了多级(LVS/Nginx)反向代理的话,ip需要从X-Forwarded-For中获得第一个非unknown的IP才是用户的有效ip。
     * @param request
     * @return
     */
    public static String getRequestIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    private static final String IP_UNKNOWN = "unknown";
    private static final String IP_LOCAL = "127.0.0.1";
    private static final int IP_LEN = 15;

    /**
     * 获取客户端真实ip
     * @param request request
     * @return 返回ip
     */
    public static String getIP(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ipAddress = headers.getFirst("x-forwarded-for");
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = Optional.ofNullable(request.getRemoteAddress())
                    .map(address -> address.getAddress().getHostAddress())
                    .orElse("");
            if (IP_LOCAL.equals(ipAddress)) {
                // 根据网卡取本机配置的IP
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    // ignore
                }
            }
        }

        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.length() > IP_LEN) {
            int index = ipAddress.indexOf(",");
            if (index > 0) {
                ipAddress = ipAddress.substring(0, index);
            }
        }
        return ipAddress;
    }

}

使用拦截器限制60秒短信发送

增加拦截器

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class SMSInterceptor extends BaseInfoProperties implements HandlerInterceptor {
    /**
      *拦截请求,访问controller之前
      *
      */ 
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        // 获得用户ip
        String userIp = IPUtil.getRequestIp(request);
        // 获得用于判断的boolean
        boolean ipExist = redis.keyIsExist(MOBILE_SMSCODE + ":" + userIp);

        if (ipExist) {
            log.error("短信发送频率太高了~~!!!");
            GraceException.display(ResponseStatusEnum.SMS_NEED_WAIT_ERROR);
            return false;
        }

        /**
         * false: 请求被拦截
         * true: 放行,请求验证通过
         */
        return true;
    }

    /**
      *请求访问到controller之后,渲染视图之前
      *
      */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }
    
    /**
      *请求访问到controller之后,渲染视图之后
      *
      */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

注册拦截器

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    /**
     * 在springboot容器中放入拦截器
     * @return
     */
    @Bean
    public SMSInterceptor smsInterceptor() {
        return new SMSInterceptor();
    }

    @Bean
    public JWTCurrentUserInterceptor jwtCurrentUserInterceptor() {
        return new JWTCurrentUserInterceptor();
    }

    /**
     * 注册拦截器,并且拦截指定的路由,否则不生效
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(smsInterceptor())
                .addPathPatterns("/passport/getSMSCode");

        registry.addInterceptor(jwtCurrentUserInterceptor())
                .addPathPatterns("/**");
    }
}

封装优雅异常降低代码侵入性

一般我们不会手动抛出RuntimeException异常,会自定义一个异常在抛出

自定义异常类

/**
 * 自定义异常
 * 目的:统一的处理异常信息
 *      便于解耦,拦截器、service、controller等异常信息的解耦
 *      不会被service返回的类型而限制
 */
public class MyCustomException extends RuntimeException {

    private ResponseStatusEnum responseStatusEnum;

    public MyCustomException(ResponseStatusEnum responseStatusEnum) {
        super("异常状态码为:" + responseStatusEnum.status() +
                "异常信息为:" + responseStatusEnum.msg());
        this.responseStatusEnum = responseStatusEnum;
    }

    public ResponseStatusEnum getResponseStatusEnum() {
        return responseStatusEnum;
    }
    public void setResponseStatusEnum(ResponseStatusEnum responseStatusEnum) {
        this.responseStatusEnum = responseStatusEnum;
    }
}

抛出后的异常通过aop机制拦截到,随后通过ExceptionHandler处理,再包装一个json返回前端即可:

全局异常处理器

@ControllerAdvice
public class GraceExceptionHandler {

    @ExceptionHandler(MyCustomException.class)
    @ResponseBody
    public GraceJSONResult returnMyCustomException(MyCustomException e) {
        e.printStackTrace();
        return GraceJSONResult.exception(e.getResponseStatusEnum());
    }

    @ExceptionHandler({
            SignatureException.class,
            ExpiredJwtException.class,
            UnsupportedJwtException.class,
            MalformedJwtException.class,
            io.jsonwebtoken.security.SignatureException.class
    })
    @ResponseBody
    public GraceJSONResult returnSignatureException(SignatureException e) {
        e.printStackTrace();
        return GraceJSONResult.exception(ResponseStatusEnum.JWT_SIGNATURE_ERROR);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public GraceJSONResult returnNotValidException(MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        Map<String, String> errors = getErrors(result);
        return GraceJSONResult.errorMap(errors);
    }

    public Map<String, String> getErrors(BindingResult result) {

        Map<String, String> map = new HashMap<>();

        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError fe : errorList) {
            // 错误所对应的属性字段名
            String field = fe.getField();
            // 错误信息
            String message = fe.getDefaultMessage();

            map.put(field, message);
        }

        return map;
    }

}

优雅的处理异常,降低代码中的异常的侵入性,这也是很多公司的代码规范,业务代码里不可以手动抛出Exception,所以代码如下,更加优雅:

优雅异常处理类

/**
 * 优雅的处理异常,统一进行封装
 */
public class GraceException {

    public static void display(ResponseStatusEnum statusEnum) {
        throw new MyCustomException(statusEnum);
    }

}

附:自定义响应数据类型枚举升级版

/**
 * 自定义响应数据类型枚举升级版本
 *
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 * 				本类可提供给 H5/ios/安卓/公众号/小程序 使用
 * 				前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 *
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V2.0
 */
public class GraceJSONResult {

    // 响应业务状态码
    private Integer status;

    // 响应消息
    private String msg;

    // 是否成功
    private Boolean success;

    // 响应数据,可以是Object,也可以是List或Map等
    private Object data;

    /**
     * 成功返回,带有数据的,直接往OK方法丢data数据即可
     * @param data
     * @return
     */
    public static GraceJSONResult ok(Object data) {
        return new GraceJSONResult(data);
    }
    /**
     * 成功返回,不带有数据的,直接调用ok方法,data无须传入(其实就是null)
     * @return
     */
    public static GraceJSONResult ok() {
        return new GraceJSONResult(ResponseStatusEnum.SUCCESS);
    }
    public GraceJSONResult(Object data) {
        this.status = ResponseStatusEnum.SUCCESS.status();
        this.msg = ResponseStatusEnum.SUCCESS.msg();
        this.success = ResponseStatusEnum.SUCCESS.success();
        this.data = data;
    }


    /**
     * 错误返回,直接调用error方法即可,当然也可以在ResponseStatusEnum中自定义错误后再返回也都可以
     * @return
     */
    public static GraceJSONResult error() {
        return new GraceJSONResult(ResponseStatusEnum.FAILED);
    }

    /**
     * 错误返回,map中包含了多条错误信息,可以用于表单验证,把错误统一的全部返回出去
     * @param map
     * @return
     */
    public static GraceJSONResult errorMap(Map map) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, map);
    }

    /**
     * 错误返回,直接返回错误的消息
     * @param msg
     * @return
     */
    public static GraceJSONResult errorMsg(String msg) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, msg);
    }

    /**
     * 错误返回,token异常,一些通用的可以在这里统一定义
     * @return
     */
    public static GraceJSONResult errorTicket() {
        return new GraceJSONResult(ResponseStatusEnum.TICKET_INVALID);
    }

    /**
     * 自定义错误范围,需要传入一个自定义的枚举,可以到[ResponseStatusEnum.java[中自定义后再传入
     * @param responseStatus
     * @return
     */
    public static GraceJSONResult errorCustom(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }
    public static GraceJSONResult exception(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }

    public GraceJSONResult(ResponseStatusEnum responseStatus) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, Object data) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
        this.data = data;
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, String msg) {
        this.status = responseStatus.status();
        this.msg = msg;
        this.success = responseStatus.success();
    }

    public GraceJSONResult() {
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }
}

附:响应结果枚举

/**
 * 响应结果枚举,用于提供给GraceJSONResult返回给前端的
 * 本枚举类中包含了很多的不同的状态码供使用,可以自定义
 * 便于更优雅的对状态码进行管理,一目了然
 */
public enum ResponseStatusEnum {

    SUCCESS(200, true, "操作成功!"),
    FAILED(500, false, "操作失败!"),

    // 50x
    UN_LOGIN(501,false,"请登录后再继续操作!"),
    TICKET_INVALID(502,false,"会话失效,请重新登录!"),
    HR_TICKET_INVALID(5021,false,"手机端会话失效,请重新登录!"),
    NO_AUTH(503,false,"您的权限不足,无法继续操作!"),
    MOBILE_ERROR(504,false,"短信发送失败,请稍后重试!"),
    SMS_NEED_WAIT_ERROR(505,false,"短信发送太快啦~请稍后再试!"),
    SMS_CODE_ERROR(506,false,"验证码过期或不匹配,请稍后再试!"),
    USER_FROZEN(507,false,"用户已被冻结,请联系管理员!"),
    USER_UPDATE_ERROR(508,false,"用户信息更新失败,请联系管理员!"),
    USER_INACTIVE_ERROR(509,false,"请前往[账号设置]修改信息激活后再进行后续操作!"),
    USER_INFO_UPDATED_ERROR(5091,false,"用户信息修改失败!"),
    USER_INFO_UPDATED_NICKNAME_EXIST_ERROR(5092,false,"昵称已经存在!"),
    USER_INFO_UPDATED_IMOOCNUM_EXIST_ERROR(5092,false,"慕课号已经存在!"),
    USER_INFO_CANT_UPDATED_IMOOCNUM_ERROR(5092,false,"慕课号无法修改!"),
    FILE_UPLOAD_NULL_ERROR(510,false,"文件不能为空,请选择一个文件再上传!"),
    FILE_UPLOAD_FAILD(511,false,"文件上传失败!"),
    FILE_FORMATTER_FAILD(512,false,"文件图片格式不支持!"),
    FILE_MAX_SIZE_500KB_ERROR(5131,false,"仅支持500kb大小以下的文件上传!"),
    FILE_MAX_SIZE_2MB_ERROR(5132,false,"仅支持2MB大小以下的文件上传!"),
    FILE_MAX_SIZE_8MB_ERROR(5132,false,"体验版仅支持8MB以下的文件上传!"),
    FILE_MAX_SIZE_100MB_ERROR(5132,false,"仅支持100MB大小以下的文件上传!"),
    FILE_NOT_EXIST_ERROR(514,false,"你所查看的文件不存在!"),
    USER_STATUS_ERROR(515,false,"用户状态参数出错!"),
    USER_NOT_EXIST_ERROR(516,false,"用户不存在!"),
    USER_PARAMS_ERROR(517,false,"用户请求参数出错!"),

    // 自定义系统级别异常 54x
    SYSTEM_INDEX_OUT_OF_BOUNDS(541, false, "系统错误,数组越界!"),
    SYSTEM_ARITHMETIC_BY_ZERO(542, false, "系统错误,无法除零!"),
    SYSTEM_NULL_POINTER(543, false, "系统错误,空指针!"),
    SYSTEM_NUMBER_FORMAT(544, false, "系统错误,数字转换异常!"),
    SYSTEM_PARSE(545, false, "系统错误,解析异常!"),
    SYSTEM_IO(546, false, "系统错误,IO输入输出异常!"),
    SYSTEM_FILE_NOT_FOUND(547, false, "系统错误,文件未找到!"),
    SYSTEM_CLASS_CAST(548, false, "系统错误,类型强制转换错误!"),
    SYSTEM_PARSER_ERROR(549, false, "系统错误,解析出错!"),
    SYSTEM_DATE_PARSER_ERROR(550, false, "系统错误,日期解析出错!"),
    SYSTEM_NO_EXPIRE_ERROR(552, false, "系统错误,缺少过期时间!"),

    HTTP_URL_CONNECT_ERROR(551, false, "目标地址无法请求!"),

    // admin 管理系统 56x
    ADMIN_USERNAME_NULL_ERROR(561, false, "管理员登录名不能为空!"),
    ADMIN_USERNAME_EXIST_ERROR(562, false, "管理员账户名已存在!"),
    ADMIN_NAME_NULL_ERROR(563, false, "管理员负责人不能为空!"),
    ADMIN_PASSWORD_ERROR(564, false, "密码不能为空或者两次输入不一致!"),
    ADMIN_CREATE_ERROR(565, false, "添加管理员失败!"),
    ADMIN_PASSWORD_NULL_ERROR(566, false, "密码不能为空!"),
    ADMIN_LOGIN_ERROR(567, false, "管理员不存在或密码不正确!"),
    ADMIN_FACE_NULL_ERROR(568, false, "人脸信息不能为空!"),
    ADMIN_FACE_LOGIN_ERROR(569, false, "人脸识别失败,请重试!"),
    ADMIN_DELETE_ERROR(5691, false, "删除管理员失败!"),
    CATEGORY_EXIST_ERROR(570, false, "文章分类已存在,请换一个分类名!"),

    // 媒体中心 相关错误 58x
    ARTICLE_COVER_NOT_EXIST_ERROR(580, false, "文章封面不存在,请选择一个!"),
    ARTICLE_CATEGORY_NOT_EXIST_ERROR(581, false, "请选择正确的文章领域!"),
    ARTICLE_CREATE_ERROR(582, false, "创建文章失败,请重试或联系管理员!"),
    ARTICLE_QUERY_PARAMS_ERROR(583, false, "文章列表查询参数错误!"),
    ARTICLE_DELETE_ERROR(584, false, "文章删除失败!"),
    ARTICLE_WITHDRAW_ERROR(585, false, "文章撤回失败!"),
    ARTICLE_REVIEW_ERROR(585, false, "文章审核出错!"),
    ARTICLE_ALREADY_READ_ERROR(586, false, "文章重复阅读!"),

    COMPANY_INFO_UPDATED_ERROR(5151,false,"企业信息修改失败!"),
    COMPANY_INFO_UPDATED_NO_AUTH_ERROR(5151,false,"当前用户无权修改企业信息!"),
    COMPANY_IS_NOT_VIP_ERROR(5152,false,"企业非VIP或VIP特权已过期,请至企业后台充值续费!"),


    // 人脸识别错误代码
    FACE_VERIFY_TYPE_ERROR(600, false, "人脸比对验证类型不正确!"),
    FACE_VERIFY_LOGIN_ERROR(601, false, "人脸登录失败!"),

    // 系统错误,未预期的错误 555
    SYSTEM_ERROR(555, false, "系统繁忙,请稍后再试!"),
    SYSTEM_OPERATION_ERROR(556, false, "操作失败,请重试或联系管理员"),
    SYSTEM_RESPONSE_NO_INFO(557, false, ""),
    SYSTEM_ERROR_GLOBAL(558, false, "全局降级:系统繁忙,请稍后再试!"),
    SYSTEM_ERROR_FEIGN(559, false, "客户端Feign降级:系统繁忙,请稍后再试!"),
    SYSTEM_ERROR_ZUUL(560, false, "请求系统过于繁忙,请稍后再试!"),
    SYSTEM_PARAMS_SETTINGS_ERROR(5611, false, "参数设置不规范!"),
    ZOOKEEPER_BAD_VERSION_ERROR(5612, false, "数据过时,请刷新页面重试!"),
    SYSTEM_ERROR_BLACK_IP(5621, false, "请求过于频繁,请稍后重试!"),

    DATA_DICT_EXIST_ERROR(5631, false, "数据字典已存在,不可重复添加或修改!"),
    DATA_DICT_DELETE_ERROR(5632, false, "删除数据字典失败!"),

    REPORT_RECORD_EXIST_ERROR(5721, false, "请不要重复举报噢~!"),

    RESUME_MAX_LIMIT_ERROR(5711, false, "本日简历刷新次数已达上限!"),

    JWT_SIGNATURE_ERROR(5555, false, "用户校验失败,请重新登录!"),
    JWT_EXPIRE_ERROR(5556, false, "登录有效期已过,请重新登录!"),

    // 支付错误相关代码
    PAYMENT_USER_INFO_ERROR(5901, false, "用户id或密码不正确!"),
    PAYMENT_ACCOUT_EXPIRE_ERROR(5902, false, "该账户授权访问日期已失效!"),
    PAYMENT_HEADERS_ERROR(5903, false, "请在header中携带支付中心所需的用户id以及密码!"),
    PAYMENT_ORDER_CREATE_ERROR(5904, false, "支付中心订单创建失败,请联系管理员!"),



    // admin 相关错误代码
    ADMIN_NOT_EXIST(5101, false, "管理员不存在!");

    // 响应业务状态
    private Integer status;
    // 调用是否成功
    private Boolean success;
    // 响应消息,可以为成功或者失败的消息
    private String msg;

    ResponseStatusEnum(Integer status, Boolean success, String msg) {
        this.status = status;
        this.success = success;
        this.msg = msg;
    }

    public Integer status() {
        return status;
    }
    public Boolean success() {
        return success;
    }
    public String msg() {
        return msg;
    }
}

使用Hibernate-Validate进行参数校验

bean属性参数校验

我们可以通过controller的接口来接受一个对象来保存到数据库,但是这么做可能会有一些非法的参数,所以可以借助hibernate验证来做,我们可以整合一下。如果使用了springboot,则不需要引用任何依赖,因为spring-boot-starter-web包中已经包含了Hibernate-Validator 依赖。

注册登录接口api

 @PostMapping("login")
    public GraceJSONResult login(@Valid @RequestBody RegistLoginBO registLoginBO,
                                      HttpServletRequest request) throws Exception {

        String mobile = registLoginBO.getMobile();
        String code = registLoginBO.getSmsCode();

      
        return GraceJSONResult.ok();
    }

注册登录用户字段校检BO

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class RegistLoginBO {

    @NotBlank(message = "手机号不能为空")
    @Length(min = 11, max = 11, message = "手机号长度不正确")
    private String mobile;
    @NotBlank(message = "验证码不能为空")
    private String smsCode;

}

在全局异常捕获器中添加Hibernate-Validate异常处理,显示友好提示(GraceExceptionHandler类已写完)

 @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public GraceJSONResult returnNotValidException(MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        Map<String, String> errors = getErrors(result);
        return GraceJSONResult.errorMap(errors);
    }

    public Map<String, String> getErrors(BindingResult result) {

        Map<String, String> map = new HashMap<>();

        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError fe : errorList) {
            // 错误所对应的属性字段名
            String field = fe.getField();
            // 错误信息
            String message = fe.getDefaultMessage();

            map.put(field, message);
        }

        return map;
    }

用户一键注册登录代码实现

controller(添加了redis token)



import com.google.gson.Gson;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.UUID;

@RestController
@RequestMapping("passport")
@Slf4j
public class PassportController extends BaseInfoProperties {

    @Autowired
    private SMSUtils smsUtils;

    @Autowired
    private JWTUtils jwtUtils;

    @Autowired
    private UsersService usersService;

    @PostMapping("getSMSCode")
    public GraceJSONResult getSMSCode(String mobile,
                                      HttpServletRequest request) throws Exception {

        if (StringUtils.isBlank(mobile)) {
            return GraceJSONResult.error();
        }

        // 获得用户ip
        String userIp = IPUtil.getRequestIp(request);
        // 限制用户只能在60s以内获得一次验证码
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, mobile);

        String code = (int)((Math.random() * 9 + 1) * 100000) + "";
//        smsUtils.sendSMS(mobile, code);
        log.info("验证码为:{}", code);

        // 把验证码存入到redis,用于后续的注册登录进行校验
        redis.set(MOBILE_SMSCODE + ":" + mobile, code, 30 * 60);

        return GraceJSONResult.ok();
    }

    @PostMapping("login")
    public GraceJSONResult login(@Valid @RequestBody RegistLoginBO registLoginBO,
                                      HttpServletRequest request) throws Exception {

        String mobile = registLoginBO.getMobile();
        String code = registLoginBO.getSmsCode();

        // 1. 从redis中获得验证码进行校验判断是否匹配
        String redisCode = redis.get(MOBILE_SMSCODE + ":" + mobile);
        if (StringUtils.isBlank(redisCode) || !redisCode.equalsIgnoreCase(code)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }

        // 2. 根据mobile查询数据库,判断用户是否存在
        Users user = usersService.queryMobileIsExist(mobile);
        if (user == null) {
            // 2.1 如果查询的用户为空,则表示没有注册过,则需要注册信息入库
            user = usersService.createUsers(mobile);
        }

        // 3. 保存用户token,分布式会话到redis中
       String uToken = TOKEN_USER_PREFIX + SYMBOL_DOT + UUID.randomUUID().toString();
       redis.set(REDIS_USER_TOKEN + ":" + user.getId(), uToken);
   
        // 4. 用户登录注册以后,删除redis中的短信验证码
        redis.del(MOBILE_SMSCODE + ":" + mobile);

        // 5. 返回用户的信息给前端
        UsersVO usersVO = new UsersVO();
        BeanUtils.copyProperties(user, usersVO);
        usersVO.setUserToken(uToken);

        return GraceJSONResult.ok(usersVO);
    }

    @PostMapping("logout")
    public GraceJSONResult logout(@RequestParam String userId,
                                      HttpServletRequest request) throws Exception {

        // 后端只需要清除用户的token信息即可,前端也需要清除相关的用户信息
//        redis.del(REDIS_USER_TOKEN + ":" + userId);

        return GraceJSONResult.ok();
    }
}

service



import com.baomidou.mybatisplus.extension.service.IService;


/**
 * <p>
 * 用户表 服务类
 * </p>
 */
public interface UsersService extends IService<Users> {

    /**
     * 判断用户是否存在,如果存在则返回用户信息,否则null
     * @param mobile
     * @return
     */
    public Users queryMobileIsExist(String mobile);

    /**
     * 创建用户信息,并且返回用户对象
     * @param mobile
     * @return
     */
    public Users createUsers(String mobile);
}
package com.imooc.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 */
@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements UsersService {

    @Autowired
    private UsersMapper usersMapper;

    private static final String USER_FACE1 = "http://122.152.205.72:88/group1/M00/00/05/CpoxxF6ZUySASMbOAABBAXhjY0Y649.png";

    @Override
    public Users queryMobileIsExist(String mobile) {

        Users user = usersMapper.selectOne(new QueryWrapper<Users>()
                .eq("mobile", mobile));

        return user;
    }

    @Transactional
    @Override
    public Users createUsers(String mobile) {

        Users user = new Users();

        user.setMobile(mobile);
        user.setNickname("用户" + DesensitizationUtil.commonDisplay(mobile));
        user.setRealName("用户" + DesensitizationUtil.commonDisplay(mobile));
        user.setShowWhichName(ShowWhichName.nickname.type);

        user.setSex(Sex.secret.type);
        user.setFace(USER_FACE1);
        user.setEmail("");

        LocalDate birthday = LocalDateUtils
                .parseLocalDate("1980-01-01",
                        LocalDateUtils.DATE_PATTERN);
        user.setBirthday(birthday);

        user.setCountry("中国");
        user.setProvince("");
        user.setCity("");
        user.setDistrict("");
        user.setDescription("这家伙很懒,什么都没留下~");

        // 我参加工作的日期,默认使用注册当天的日期
        user.setStartWorkDate(LocalDate.now());
        user.setPosition("底层码农");
        user.setRole(UserRole.CANDIDATE.type);
        user.setHrInWhichCompanyId("");

        user.setCreatedTime(LocalDateTime.now());
        user.setUpdatedTime(LocalDateTime.now());

        usersMapper.insert(user);

        return user;
    }
}

用户一键注册登录代码实现(JWT+Gateway改进版)

1.用户登录,经过网关,再到授权中心,授权中心颁发令牌token(加密)
2.客户端接收到token之后,保存在本地,用户无感知
3.用户发起第二次请求,每次请求都会携带这个令牌token,通过header或者cookie携带
4.后端接受到token,转发至授权中心进行认证,认证通过则继续后续业务,认证失败则返回错误信息
5.改进版:后端接受token,在网关中直接对其进行校验,减少二次分发,减少链路。

引入依赖

 <!-- 引入JJWT的依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

附:JWT工具类

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import sun.misc.BASE64Encoder;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
@Slf4j
@RefreshScope
public class JWTUtils {

    public static final String at = "@";

    @Autowired
    private JWTProperties jwtProperties;

    @Value("${jwt.key}")
    public String JWT_KEY;

    public String createJWTWithPrefix(String body, Long expireTimes, String prefix) {
        if (expireTimes == null)
            GraceException.display(ResponseStatusEnum.SYSTEM_NO_EXPIRE_ERROR);

        return prefix + at + createJWT(body, expireTimes);
    }

    public String createJWTWithPrefix(String body, String prefix) {
        return prefix + at + createJWT(body);
    }

    public String createJWT(String body) {
        return dealJWT(body, null);
    }

    public String createJWT(String body, Long expireTimes) {
        if (expireTimes == null)
            GraceException.display(ResponseStatusEnum.SYSTEM_NO_EXPIRE_ERROR);

        return dealJWT(body, expireTimes);
    }

    public String dealJWT(String body, Long expireTimes) {

//        String userKey = jwtProperties.getKey();
        String userKey = JWT_KEY;
        log.info("Nacos jwt key = " + JWT_KEY);

        // 1. 对秘钥进行base64编码
        String base64 = new BASE64Encoder().encode(userKey.getBytes());

        // 2. 对base64生成一个秘钥的对象
        SecretKey secretKey = Keys.hmacShaKeyFor(base64.getBytes());

        String jwt = "";
        if (expireTimes != null) {
            jwt = generatorJWT(body, expireTimes, secretKey);
        } else {
            jwt = generatorJWT(body, secretKey);
        }
        log.info("JWTUtils - dealJWT: generatorJWT = " + jwt);

        return jwt;
    }

    public String generatorJWT(String body, SecretKey secretKey) {
        String jwtToken = Jwts.builder()
                .setSubject(body)
                .signWith(secretKey)
                .compact();
        return jwtToken;
    }

    public String generatorJWT(String body, Long expireTimes, SecretKey secretKey) {
        // 定义过期时间
        Date expireDate = new Date(System.currentTimeMillis() + expireTimes);
        String jwtToken = Jwts.builder()
                .setSubject(body)
                .signWith(secretKey)
                .setExpiration(expireDate)
                .compact();
        return jwtToken;
    }

    public String checkJWT(String pendingJWT) {

//        String userKey = jwtProperties.getKey();
        String userKey = JWT_KEY;
        log.info("Nacos jwt key = " + JWT_KEY);

        // 1. 对秘钥进行base64编码
        String base64 = new BASE64Encoder().encode(userKey.getBytes());

        // 2. 对base64生成一个秘钥的对象
        SecretKey secretKey = Keys.hmacShaKeyFor(base64.getBytes());

        // 3. 校验jwt
        JwtParser jwtParser = Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build();       // 构造解析器
        // 解析成功,可以获得Claims,从而去get相关的数据,如果此处抛出异常,则说明解析不通过,也就是token失效或者被篡改
        Jws<Claims> jws = jwtParser.parseClaimsJws(pendingJWT);      // 解析jwt

        String body = jws.getBody().getSubject();

        return body;
    }

}

附:JWT资源配件文件类(JWT工具类中用@Value替代)

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@Data
@PropertySource("classpath:jwt.properties")
@ConfigurationProperties(prefix = "auth")
public class JWTProperties {
    private String key;
}

controller

最后,在注册登录的地方修改原来token的生成方式即可

@PostMapping("login")
    public GraceJSONResult login(@Valid @RequestBody RegistLoginBO registLoginBO,
                                      HttpServletRequest request) throws Exception {

        String mobile = registLoginBO.getMobile();
        String code = registLoginBO.getSmsCode();

        // 1. 从redis中获得验证码进行校验判断是否匹配
        String redisCode = redis.get(MOBILE_SMSCODE + ":" + mobile);
        if (StringUtils.isBlank(redisCode) || !redisCode.equalsIgnoreCase(code)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }

        // 2. 根据mobile查询数据库,判断用户是否存在
        Users user = usersService.queryMobileIsExist(mobile);
        if (user == null) {
            // 2.1 如果查询的用户为空,则表示没有注册过,则需要注册信息入库
            user = usersService.createUsers(mobile);
        }

        // 3. 保存用户token,分布式会话到redis中
//        String uToken = TOKEN_USER_PREFIX + SYMBOL_DOT + UUID.randomUUID().toString();
//        redis.set(REDIS_USER_TOKEN + ":" + user.getId(), uToken);
        String jwt = jwtUtils.createJWTWithPrefix(new Gson().toJson(user),
//                                                    Long.valueOf(60 * 1000),
                                                    TOKEN_USER_PREFIX);

        // 4. 用户登录注册以后,删除redis中的短信验证码
        redis.del(MOBILE_SMSCODE + ":" + mobile);

        // 5. 返回用户的信息给前端
        UsersVO usersVO = new UsersVO();
        BeanUtils.copyProperties(user, usersVO);
        usersVO.setUserToken(jwt);

        return GraceJSONResult.ok(usersVO);
    }

Gateway过滤器校检JWT(1)-路径匹配规则器(稍后更新)

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现用户手机号验证码登录可以分为以下几个步骤: 1. 用户输入手机号和验证码,点击登录按钮。 2. 后端接收到手机号和验证码后,先验证验证码是否正确。 3. 如果验证码正确,后端生成JWT token并将token存储Redis中,同时将token返回给前端。 4. 前端将token存储到本地,以便后续请求时使用。 5. 后续请求时,前端需要在请求头中加入token,后端通过解析token来判断用户是否已登录。 下面是具体实现过程: 1. 在阿里云短信控制台创建短信模板,获取accessKeyId和accessKeySecret。 2. 在Spring Boot项目中添加依赖: ``` <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.0.3</version> </dependency> ``` 3. 实现发送短信验证码的接口: ``` @PostMapping("/sendSms") public Result sendSms(@RequestParam("phone") String phone) { // 生成随机验证码 String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000)); // 发送短信验证码 DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret); IAcsClient client = new DefaultAcsClient(profile); CommonRequest request = new CommonRequest(); request.setSysMethod(MethodType.POST); request.setSysDomain("dysmsapi.aliyuncs.com"); request.setSysVersion("2017-05-25"); request.setSysAction("SendSms"); request.putQueryParameter("RegionId", "cn-hangzhou"); request.putQueryParameter("PhoneNumbers", phone); request.putQueryParameter("SignName", "短信签名"); request.putQueryParameter("TemplateCode", "短信模板编号"); request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}"); try { CommonResponse response = client.getCommonResponse(request); // 将验证码存储Redis中,有效期为5分钟 redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES); return Result.success("短信验证码发送成功"); } catch (Exception e) { return Result.error("短信验证码发送失败"); } } ``` 4. 实现用户手机号验证码登录的接口: ``` @PostMapping("/login") public Result login(@RequestParam("phone") String phone, @RequestParam("code") String code) { // 验证验证码是否正确 String redisCode = redisTemplate.opsForValue().get(phone); if (StringUtils.isBlank(redisCode)) { return Result.error("验证码已过期,请重新发送"); } if (!redisCode.equals(code)) { return Result.error("验证码不正确"); } // 生成JWT token,并存储Redis中 String token = JwtUtils.generateToken(phone); redisTemplate.opsForValue().set(phone, token, 1, TimeUnit.DAYS); // 将token返回给前端 return Result.success(token); } ``` 5. 实现JWT token的生成和解析: ``` public class JwtUtils { private static final String SECRET_KEY = "jwt_secret_key"; // JWT密钥 private static final long EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000; // JWT过期时间(7天) public static String generateToken(String phone) { Date now = new Date(); Date expiration = new Date(now.getTime() + EXPIRATION_TIME); return Jwts.builder() .setSubject(phone) .setIssuedAt(now) .setExpiration(expiration) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static String getPhoneFromToken(String token) { try { Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); return claims.getSubject(); } catch (Exception e) { return null; } } } ``` 6. 在拦截器中验证token并获取用户信息: ``` public class JwtInterceptor implements HandlerInterceptor { private static final String AUTH_HEADER = "Authorization"; // token在请求头中的名称 @Autowired private StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader(AUTH_HEADER); if (StringUtils.isBlank(token)) { throw new BusinessException("未登录登录已过期"); } String phone = JwtUtils.getPhoneFromToken(token); if (StringUtils.isBlank(phone)) { throw new BusinessException("无效的token"); } String redisToken = redisTemplate.opsForValue().get(phone); if (StringUtils.isBlank(redisToken) || !redisToken.equals(token)) { throw new BusinessException("未登录登录已过期"); } return true; } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值