雪花算法
为了高效的生成有序且唯一的ID,可以采用雪花算法来进行实现,为什么不去采用UUID呢?首先,UUID是一个128位的值,相较于雪花算法生成的64位的值,长了很多,在数据库中存储时耗费的时间更长,UUID生成后没有顺序关系,导致它不适合做主键,雪花算法排序具有可读性,在一些状况下更容易地追踪。
雪花算法的原理
IdUtil.getSnowflake有两个参数,第一个时数据中心的编号,第二个时机器的编号,可以包装每台机器生成的id不重复。最高位是0,1-bit,表示正数,之后41bit位的时间戳,后面的10bit位是数据中心和工作机器的id(保证每一台机器生成同一时间的id不同),最后12位是序列号,保证一个机器1ms内生成id不同,大约4096个,在并发的情况下。
登录和注册
采用注册和登录二合一,当手机号不存在,把手机号增加到系统中,然后登录,手机号存在,直接登录。
短信验证码接口的开发
用户输入手机号,对应的接口需要传递参数,不需要返回值,因为是发送到手机上的。为了规范,可能之后的开发设计到传入的参数不同,因此可以封装成一个sendReq专门用来传参,为了保证手机号是11位,在上面加上@Pattern(regexp = "^\\d{10}$",message="手机号格式错误")。
service层调用mapper进行查询手机号,如果不存在,插入一条记录,最后生成验证码,代码如下:
public void sendCode(MemberSendCodeReq req) {
String mobile = req.getMobile();
MemberExample memberExample = new MemberExample();
memberExample.createCriteria().andMobileEqualTo(mobile);
List<Member> list = memberMapper.selectByExample(memberExample);
// 如何手机号不存在,插入一条记录
if(CollUtil.isEmpty(list)) {
LOG.info("手机号不存在,插入一条记录");
Member member = new Member();
member.setId(SnowUtil.getSnowflakeNextId());
member.setMobile(mobile);
memberMapper.insert(member);
}else {
LOG.info("手机号已存在");
}
// 生成验证码
// String code= RandomUtil.randomString(4);
// 为了测试
String code = "8888";
LOG.info("生成的短信验证码:{}",code);
}
为了使前端能够更好的处理业务,将所有的统一封装成CommResp<>返回给前端。
@PostMapping("/send-code")
public CommonResp<Long> sendCode(@Valid MemberSendCodeReq req) {
memberService.sendCode(req);
return new CommonResp<>();
}
登录接口的话,是需要传手机号和短信验证码的。输入验证码,点击登录,后端的接口进行参数校验,需要检验手机号是否存在,校验短信验证码更新状态
由于在设计发送验证码时的需要查询数据库的是否存在某个用户,登录是也要查询,因此可以将其封装成一个方法,减少重复代码。
private Member selectByMobile(String mobile) {
MemberExample memberExample = new MemberExample();
memberExample.createCriteria().andMobileEqualTo(mobile);
List<Member> list = memberMapper.selectByExample(memberExample);
if(CollUtil.isEmpty(list)) {
return null;
}else {
return list.get(0);
}
}
登录时如果手机不存在,即手机号没有在页面显示,就抛出异常,验证码不一致亦需要抛出异常,用户有很多属性,需要登录成功后返给前端,由于有密码这样的敏感字段,因此可以将返回值进行一个封装。
登录方式的设计
单点登录的设计
所谓的单点登录,就是一次登录,到处访问。方式1:redis+token,token是一个随机的字符串,为每个用户登录生成的,每一次登录生成一个新的token不能够使用用户id,因为用户id相同,r容易被破解,将token作为key,将用户信息作为value,存放到redis中去,这样后端将登录的信息缓存起来,再把token返给前端,前端每个请求把token给带上,这就实现了单点登陆的功能。jwt登录生成的用户token是有意义的,是用户信息的加密数据,通过加密数据可以反向解出当前登录的是哪一个数据。
JWT的原理
结构:Header头部信息,主要包含令牌类型和声明了JWT的签名算法等信息默认是使用HS265算法进行加密。。Payload载荷信息,主要承载各种声明并传递明文数据。Signatrue签名,用于校验数据。
流程:
-
认证请求:
用户尝试登录或执行需要认证的操作时,向服务器发送包含用户名和密码的请求。 -
生成JWT:
一旦用户的凭据被验证,服务器会创建一个JWT。此JWT包含了必要的声明,比如用户的ID或者角色,然后用服务器端的密钥对其进行签名。 -
返回JWT给客户端:
服务器将生成的JWT返回给客户端。客户端应该保存这个JWT(通常是在HTTP Only的cookie中或者本地存储中)。 -
访问受保护资源:
当客户端想要访问受保护的资源时,它会在请求头中携带这个JWT(通常是在Authorization头中,格式为Bearer <token>
)。 -
验证JWT:
每次收到带有JWT的请求时,服务器都会检查JWT的有效性,包括验证签名是否正确、检查声明中的过期时间等。如果一切正常,服务器则允许访问请求的资源。
存在问题及解决方案
- token被解密,可以加盐进行解决
- token被拿到第三方去使用,可以使用限流
由于前端需要Token,因此可以生成后根据Resp返回给前端,登录成功后,进行setToken。
MemberLoginResp memberLoginResp = BeanUtil.copyProperties(memberDB, MemberLoginResp.class);
Map<String,Object> map = BeanUtil.beanToMap(memberLoginResp);
String key = "month12306";
String token = JWTUtil.createToken(map,key.getBytes());
memberLoginResp.setToken(token);
return memberLoginResp;
由于不仅仅一个地方使用到token,因此可以把它封装成一个工具类。
package com.month.train.common.util;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class);
private static final String key = "month12306";
public static String createToken(Long id, String mobile) {
// LOG.info("开始生成JWT token,id:{},mobile:{}", id, mobile);
// GlobalBouncyCastleProvider.setUseBouncyCastle(false);
DateTime now = DateTime.now();
DateTime expTime = now.offsetNew(DateField.HOUR, 24);
Map<String, Object> payload = new HashMap<>();
// 签发时间
payload.put(JWTPayload.ISSUED_AT, now);
// 过期时间
payload.put(JWTPayload.EXPIRES_AT, expTime);
// 生效时间
payload.put(JWTPayload.NOT_BEFORE, now);
// 内容
payload.put("id", id);
payload.put("mobile", mobile);
String token = JWTUtil.createToken(payload, key.getBytes());
LOG.info("生成JWT token:{}", token);
return token;
}
public static boolean validate(String token) {
// LOG.info("开始JWT token校验,token:{}", token);
// GlobalBouncyCastleProvider.setUseBouncyCastle(false);
JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
// validate包含了verify
boolean validate = jwt.validate(0);
LOG.info("JWT token校验结果:{}", validate);
return validate;
}
public static JSONObject getJSONObject(String token) {
// GlobalBouncyCastleProvider.setUseBouncyCastle(false);
JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
JSONObject payloads = jwt.getPayloads();
payloads.remove(JWTPayload.ISSUED_AT);
payloads.remove(JWTPayload.EXPIRES_AT);
payloads.remove(JWTPayload.NOT_BEFORE);
LOG.info("根据token获取原始内容:{}", payloads);
return payloads;
}
public static void main(String[] args) {
createToken(1L, "123");
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE3MzU0NjIzMTYsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE3MzU1NDg3MTYsImlhdCI6MTczNTQ2MjMxNn0.Xp6cMnjQnsizwqqxJf52yeDytkGyb5_XA-39j0K2jf4";
validate(token);
getJSONObject(token);
}
}
对于正常的业务请求,需要增加一层登录校验,用来保证登录的人才能够操作,因此可以在gateway模块下增加登录拦截器以及JWT校验