目录
代码实现(redis存储验证码+redis锁机制限制ip发短信)
发送验证码
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)-路径匹配规则器(稍后更新)