登录入口

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;
    }

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐