登录入口
1.app 正常登录入口
2.app 网页登录,比如分享直播卡片时,进入直播间需要先进行登录
3.pc 登录
一,app常见的登录方式
1.手机号验证码登录
2.用户名密码登录
3.一键登录
二,手机验证码登录示意图
三,流程
0.登录or注册
需要手机号,获取验证码除了登录,还可能是注册的场景,不同的类型需要加以区分
1.获取验证码:输入手机号,调用阿里云短信服务,获取验证码
详细:
1.base64编码,手机号是不能明文传输的,需要前端base64编码,后台解码
2.验证码如何生成的,是在代码中生成的随机六位数,作为阿里云发送短信的参数
3.注册or登录,不同的场景,阿里云短信发送,文案不同;且注册时,判断用户是否已经注册,查看用户表是否存在,根据手机号码查询,且存下的手机号码也是md5加密的
4.发送限制,一个手机号一天发送短信的次数要限制,redis处理
5.落地,发送记录落表,后续验证正确性,状态包含未使用 已使用,输入正确及标记为已使用
6.缓存验证码,过期时间为60s(过期后验证码输入依然可用),后续验证正确性时,先取缓存,再根据手机号查询表
2.登录:参数:手机号码+验证码,调用登录接口,返回用户信息
1.验证码正确性,先取缓存验证码,再根据手机号查询验证码记录表
2.输入次数限制,注意区分输入验证码次数限制与验证码发送限制 redis
3.输入剩余次数提示,redis记录失败次数
4.异步更新验证码记录,线程异步更新,更新状态已使用+验证时间
5.验证成功后,删除验证码缓存
6.更新用户信息,如最新一次登录时间,登录ip
7.返回用户及权限信息,包括token
四,token生成策略
1.公钥加密生成token
参数:用户名+过期日期(当前时间毫秒数+过期时间,30天)
2.私钥解密解析token
五,代码
1.token生成
@Override
public Map<String, Object> getToken(String uid, Integer exp) {
if (StringUtils.isBlank(uid)) {
throw ExceptionUtils.throwException(PARAM_ERROR);
}
String encryptToken = RSADecryptUntil.encryptByTokenPublicKey(
uid + "#" + (((int) Math.floor((System.currentTimeMillis() / 1000))) + exp));
if (StringUtils.isBlank(encryptToken)) {
throw ExceptionUtils.throwException(TOKEN_ENCRYPTION_FAIL);
}
Map<String, Object> map = new HashMap<>();
map.put("token", RSADecryptUntil.Base64Replace(encryptToken));
return map;
}
package com.zgzt.platform.authentication.utils;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
public class RSADecryptUntil {
private static final Logger log = LoggerFactory.getLogger(RSADecryptUntil.class);
private static final String ALGORITHM = "RSA";
private static final String PUBLICK_EY = "PUBLICK_EY";
private static final String PRIVATE_KEY = "PRIVATE_KEY";
/**
* 加密算法
*/
private static final String CIPHER_DE = "RSA";
/**
* 解密算法
*/
private static final String CIPHER_EN = "RSA";
/**
* 密钥长度
*/
private static final Integer KEY_LENGTH = 1024;
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;
/**
* token 公钥和私钥
*/
private static String tokenPublicKey = "AoQkavJtaZNzqUV0VByMU3n7KLdR/Sf1/kpTzLwqW9mB1GoFsuvrQ1/DVWFcuW54lNQl3/3ptK+megiyX3aq3O8nB92B69xUGXAyF" + "J2XTeX5WneLWVznR/zQPo5mxwILj7eJ6hVdWwIDAQAB";
private static String tokenPrivateKey = "F6ZHkw+2OtesChCRq8m1pk3OpRXRUHIxTefsot1H9J/X+SlPMvCpb2YHUagWy6+tDX8NVYVy5bniU1CXf/em0r6Z6CLJfdqrc7ycH" + "3YHr3FQZcDIUnZdN5flad4tZXOdH/NA+jmbHAguPt4nqFV1bAgMBAAECgYEAwUzuGQ5X36U9Qy4AxG299L4KiZscav4Lr3NOhWUfn" + "I1Di+1UBb/6PzHZYfhQl4tGVzedH2SItqOKegTRI7G9Tlt2SLroYwXBhWzS785XKPygREN7sDXvWBoxD4Squ/0iV883fPXVLdUjSQn" + "OniWI9DnD8/m1SWnzAkLpt4JjvxECQQD4yKJJyAjvv9uQv0HED3nHXEEDR5IxfqEW+51LmxFPGBXXKRvqyS3hjzUQb8ixNAMRFFReA" + "drYTGS4PQ/xVSHHAkEA1Y315wyXRj670oi9JsOjRNQ8ToCPCFXWWbQevlJj9t2R61nQxEVyBxHnPGsniOLJ0MMrEl/2IcWc0ZtuCRw" + "nzQJBAMZ4cRfBUHfLrGNGYTYDTpif3XG7WELKDcNjCfJ2DBH4WfwjXJUq18J5V9D8DLRplQS8Hi489pTWJQfiFuTlkKMCQCeF02nEccb" + "FW3t2ZRNkh7X4VYTt1Arl3/rQFBSDKQ8KKLRW9gUtGRJn5NTQvAtgdZtWU4VeDy5m5UQBsRasiE0CQD6opMGepDgYkVRDcOfyvc/Yiyy" + "lCpMWkQk3ZjlRW2i9+d2zuQNUt22R3/N6JfBbnSDp0brauQpxIJvuG0D6TZQ=";
/**
* 极光秘钥
*/
public static final String jPushPrivateKey = "kixR/SHVACkPXynvf5ZFD3UZxNGS4fVp78DFIOQY3UgnVdRqkktkjkgDDTUiYRaC\n" + "FDBcgYn4atEJk/lXeTQaw8Z6hrBL4vgMB1nzhUeR0FSknFNxZj7vjLt1TIjkG32Q\n" + "16QRJpTAdz/gi0+iiHd3HVqnj6EDAgMBAAECgYAfQI6FABH914+bxMm6zvAosr6t\n" + "i8o1Ew7PqwGcpG+7Wt5+ikoFK7u0ZOnd5wYpiqbhdkCBbvFIbwtYSM6266YggufO\n" + "FQ75uaVVjgN8yNB0Dfw/+5ymdoTfN4+Al+Rn7uDYuUyVdYKO6081RusQwqkhrU7K\n" + "w9jJd2BXpvD/+Ig6EQJBAOhaKdhe1HdtV8Hcgkk/ZT3wJAfy7Q8TQQk+pYeXK/i4\n" + "tG6ZBwZ5NwNXufEj2gp83bmw8Lhl7vMekvXs72OHUesCQQDB6xn2g3bqwikMp5WU\n" + "1v4BSuPbrRLIFONvlamVPlratZnxlDxAXRa/hY+HJORdVzCl7PWhMXhaMcuHntU4\n" + "5Y9JAkAUjITO4fQga8crGflbyQOHKsnE+jME9kr2KlgxWalF4e/zKA17ARVgck27\n" + "idQqwUhKt99SL5GmZrnQjhfN0ZXpAkAYYmjcX8GnWYzx42ziz3oXTYSDjirrb/z9\n" + "fhNaCgJAuE9IWnyNF2eR48idlN0Gg71BUB+/Ckp5BQPz5NwpEGzJAkA908Loukwm\n" + "+qaHOVOTtRtzwjYQ9c6ReWyALMCdQZ64O7OOGozcyBgWQ/CbqKiYew/h7Pz1OJXI\n" + "yzCUz9DONIo3";
/**
* 生成秘钥对,公钥和私钥
*
* @return
* @throws NoSuchAlgorithmException
*/
public static Map<String, Object> genKeyPair() throws NoSuchAlgorithmException {
Map<String, Object> keyMap = new HashMap<String, Object>();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
keyPairGenerator.initialize(KEY_LENGTH); // 秘钥字节数
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
keyMap.put(PUBLICK_EY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
/**
* 公钥加密
*
* @param data
* @param publicKey
* @return
* @throws InvalidKeySpecException
*/
public static byte[] encryptByPublicKey(byte[] data, String publicKey) throws Exception {
// 得到公钥
byte[] keyBytes = Base64.decodeBase64(publicKey.getBytes());
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
Key key = keyFactory.generatePublic(x509EncodedKeySpec);
// 加密数据,分段加密
Cipher cipher = Cipher.getInstance(CIPHER_EN);
cipher.init(Cipher.ENCRYPT_MODE, key);
int inputLength = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
while (inputLength - offset > 0) {
if (inputLength - offset > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offset, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offset, inputLength - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
/**
* 私钥解密
*
* @param data
* @param privateKey
* @return
* @throws Exception
*/
public static byte[] decryptByPrivateKey(byte[] data, String privateKey) throws Exception {
// 得到私钥
byte[] keyBytes = Base64.decodeBase64(privateKey.getBytes());
PKCS8EncodedKeySpec pKCS8EncodedKeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
Key key = keyFactory.generatePrivate(pKCS8EncodedKeySpec);
// 解密数据,分段解密
Cipher cipher = Cipher.getInstance(CIPHER_DE);
cipher.init(Cipher.DECRYPT_MODE, key);
int inputLength = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
byte[] tmp;
while (inputLength - offset > 0) {
if (inputLength - offset > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(data, offset, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offset, inputLength - offset);
}
out.write(cache);
i++;
offset = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
/**
* 获取公钥
*
* @param keyMap
* @return
*/
public static String getPublicKey(Map<String, Object> keyMap) {
Key key = (Key) keyMap.get(PUBLICK_EY);
String str = new String(Base64.encodeBase64(key.getEncoded()));
return str;
}
/**
* 获取私钥
*
* @param keyMap
* @return
*/
public static String getPrivateKey(Map<String, Object> keyMap) {
Key key = (Key) keyMap.get(PRIVATE_KEY);
String str = new String(Base64.encodeBase64(key.getEncoded()));
return str;
}
//私钥解密
public static String decryptByPrivateKey(String content, String privateKey) {
try {
byte[] decryptStrByte = RSADecryptUntil.decryptByPrivateKey(Base64.decodeBase64(content), privateKey);
return new String(decryptStrByte);
} catch (Exception e) {
log.error("decryptByPrivateKey" + e.getMessage());
}
return null;
}
//极光钥解密
public static String decryptByJPushPrivateKey(String content) {
try {
byte[] decryptStrByte = RSADecryptUntil.decryptByPrivateKey(Base64.decodeBase64(content), jPushPrivateKey);
return new String(decryptStrByte);
} catch (Exception e) {
log.error("decryptByPrivateKey" + e.getMessage());
}
return null;
}
//私钥解密
public static String decryptByTokenPrivateKey(String content) {
try {
byte[] decryptStrByte = RSADecryptUntil.decryptByPrivateKey(Base64.decodeBase64(content), tokenPrivateKey);
return new String(decryptStrByte);
} catch (Exception e) {
log.error("decryptByPrivateKey:content:" + content + ";preContent:" + RSADecryptUntil.Base64Replace(
content) + ";err:" + e.getMessage());
}
return null;
}
//公钥加密
public static String encryptByTokenPublicKey(String content) {
try {
//log.info("公钥加密");
byte[] encryptStrByte = RSADecryptUntil.encryptByPublicKey(content.getBytes(), tokenPublicKey);
byte[] btt = Base64.encodeBase64(encryptStrByte);
return new String(btt);
} catch (Exception e) {
log.error("encryptByPublicKey" + e.getMessage());
}
return null;
}
/**
* 从普通字符串转换为适用于URL的Base64编码字符串
*
* @param normalString
* @return
*/
public static String Base64Replace(String normalString) {
return normalString.replace('+', '*').replace('/', '-').replace('=', '.');
}
/**
* 从替换过得字符串转成正确的编码字符串
*
* @param base64String
* @return
*/
public static String Base64Restore(String base64String) {
return base64String.replace('.', '=').replace('*', '+').replace('-', '/');
}
}
2.手机验证码登录
依赖包
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.0.6</version>
</dependency>
2.1发送验证码
/**
* * @param type 发送验证码类型 10 注册 11验证码登录,12修改手机号
**/
@PostMapping("/sendCode")
@ApiOperation("发送短信验证码")
public BaseResult<Boolean> sendCode(HttpServletRequest request, @RequestBody ReqPhoneSmsVO reqDTO)
throws UnsupportedEncodingException {
String phoneBase64 = reqDTO.getPhone();
if (StringUtils.isBlank(phoneBase64)) {
return BaseResult.success(false);
}
Base64.Decoder decoder = Base64.getDecoder();
String phone = new String(decoder.decode(phoneBase64), "UTF-8");
return BaseResult.success(registerService.sendCode(phone, reqDTO.getType(), getIp(request)));
}
@Override
public Boolean sendCode(String phone, Integer type, String ip) {
if (StringUtils.isEmpty(phone) || type == null || !CheckMobilePhoneNum(phone)) {
log.error("phone format error ");
throw ExceptionUtils.throwException(PARAM_ERROR);
}
//判断发送短信类型,控制发送短信次数
String hexStr = HexUntil.str2HexStr(phone);
String key = "jysvcn:" + type + ":" + hexStr;
Object valObj = redisMgr.get(key);
Integer value = 0;
if (!Objects.isNull(valObj)) {
value = Integer.valueOf(String.valueOf(valObj));
}
if (value != null && value >= maxCodeNum) {
log.error("captcha transmission limit reached");
throw ExceptionUtils.throwException(CAPTCHA_LIMIT);
}
String code = "111111";
if (message) {
code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000));
}
String result = savePhoneMsg(phone, type, ip, code);
if (Objects.isNull(result)) {
log.error("captcha transmission failed");
throw ExceptionUtils.throwException(CAPTCHA_ERROR);
}
//添加redis 缓存中
redisMgr.put(key, DateTimeUtils.getSecond(), value == null ? "1" : (++value) + "");
redisMgr.put("jysvc:" + type + ":" + hexStr, 3600, result);
return true;
}
@Override
public String savePhoneMsg(String phone, Integer type, String ip, String code) {
String phoneMD5 = DigestUtils.md5DigestAsHex(phone.getBytes());
// 临时注释掉发送验证码
if (type == 10) {//注册验证码,检查是否存在
//判断用户是否存在
Register info = getUserInfoByPhoneMd5(phoneMD5);
if (info != null) {//如果存在,报错
log.info("register -Already registered:{}", phoneMD5);
throw ExceptionUtils.throwException(REGISTER_REPEAT);
}
}
//判断是否开启真实的发送验证码
if (message) {
String sendJson = aliYunSmsUtils.sendSms(phone, "{\"code\":\"" + code + "\"}", SmsTypeEnum.valueOf(type),
type);
if (StringUtils.isEmpty(sendJson)) {
throw ExceptionUtils.throwException(CODE_SEND_FAIL);
}
}
executor.execute(() -> {
PhoneMsg phoneMsg = new PhoneMsg();
phoneMsg.setId(0);
phoneMsg.setIp(ip);
phoneMsg.setPhone(phone.substring(0, 3) + "****" + phone.substring(7));
phoneMsg.setVerify(code);
phoneMsg.setCreatetime((int) Math.floor((System.currentTimeMillis() / 1000)));
phoneMsg.setType(type);
phoneMsg.setPhoneMd5(phoneMD5);
String sendJson1 = SmsTypeEnum.getDesc(type);
phoneMsg.setContent(sendJson1.replace("${code}", code));
phoneMsg.setStatus(0);
phoneMsgMapper.insertPhoneCode(phoneMsg);
});
return code;
}
2.2手机号验证码登录
@PostMapping("/codeLogin")
@ApiOperation("验证码登录")
public BaseResult<Map<String, Object>> codeLogin(HttpServletRequest request, @RequestBody ReqPhoneLoginVO reqDTO)
throws UnsupportedEncodingException {
String ua = request.getHeader("User-Agent");
String phoneBase64 = reqDTO.getPhone();
if (!StringUtils.isBlank(phoneBase64)) {
Base64.Decoder decoder = Base64.getDecoder();
reqDTO.setPhone(new String(decoder.decode(phoneBase64), "UTF-8"));
}
//暂定除了手机和pc客户端的登录都是web
return BaseResult.success(registerService.codeLogin(reqDTO, getIp(request), "web"));
}
@Data
public class ReqPhoneLoginVO {
private String phone;
private String code;
}
public Map<String, Object> codeLogin(ReqPhoneLoginVO reqDTO, String ip, String clientType) {
String phone = reqDTO.getPhone();
String code = reqDTO.getCode();
if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
throw ExceptionUtils.throwException(PARAM_EMPTY);
}
String base = phone;//统一改为不加盐的MD5数据-20210410
String phoneMD5 = DigestUtils.md5DigestAsHex(base.getBytes());
//验证手机号的登录验证码是否正确
Boolean register = updateRegisterCode(phone, code, 11, phoneMD5);
if (!register) {
throw ExceptionUtils.throwException(CODE_ERROR);
}
//判断用户是否存在
String uuid = LogbackUtil.generateTraceId();
String regId = "";
String clientPlatForm = "web";
Register info = getUserInfoByPhoneMd5(phoneMD5);
if (info != null) {
return getLoginInfo(info, uuid, regId, ip, clientPlatForm, phone);
} else {//如果不存在,直接进行注册
//密码随机生成
//String pwd= ""+rand.nextInt(899999)+ 100000;//6位随机数字
//String pwd = "966812";//6位随机数字
String pwd = (phone.length() > 6) ? phone.substring(phone.length() - 6) : "966812";
return registerByPhone(phone, phoneMD5, pwd, uuid, regId, ip, clientPlatForm);
}
}
@Override
public Boolean updateRegisterCode(String phone, String code, Integer type, String phoneMd5) {
//首先去库中查询是否有验证码 并且判断验证码验证次数是否超过上限 5次
String phoneKey = HexUntil.str2HexStr(phone);
String redisCountKey = "jysvcen:" + type + ":" + phoneKey;
Object num = redisMgr.get(redisCountKey);
if (num != null && (Integer) num >= maxCodeNum) {
log.error("Verification code error limit");
throw ExceptionUtils.throwException(CODE_VERIFY_LIMIT);
}
Object redisCode = redisMgr.get("jysvc:" + type + ":" + phoneKey);
if (Objects.isNull(redisCode)) {
redisCode = getVerifyCodeDb(type, phoneMd5);
//log.info("getVerifyCodeDb:code:{}",redisCode);
}
//查询不到验证码 或者验证码有误
if (Objects.isNull(redisCode)) {
log.error("Verification code has expired");
throw ExceptionUtils.throwException(CODE_EXPIRED);
}
if (!((String) redisCode).equals(code)) {
Integer errNum = (Integer) num;
//记录失败次数 一小时刷新
errNum = errNum == null ? 1 : errNum + 1;
redisMgr.put(redisCountKey, 3600, errNum);
log.error("Verification code error" + errNum);
Integer errSurplus = maxCodeNum - errNum;
if (errSurplus == 0) {
throw ExceptionUtils.throwException(SEND_TOO_MANY_TIMES);
} else {
throw ExceptionUtils.throwException(CODE_ERROR_SURPLUS, errSurplus);
}
}
executor.execute(() -> {
//异步修改验证码
Integer verifytime = (int) Math.floor((System.currentTimeMillis() / 1000));
phoneMsgMapper.updateByVerifyPhone(verifytime, phoneMd5);
});
//验证过后清除Redis中的code
redisMgr.remove("jysvc:" + type + ":" + phoneKey);
return true;
}