登录入口
1.app 正常登录入口
2.app 网页登录,比如分享直播卡片时,进入直播间需要先进行登录
3.pc 登录
一,app常见的登录方式
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生成
@Overridepublic 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)));}
@Overridepublic 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;}
@Overridepublic 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客户端的登录都是webreturn 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数据-20210410String 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);}}
@Overridepublic 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中的coderedisMgr.remove("jysvc:" + type + ":" + phoneKey);return true;}