一、用户注册(手机验证码)
目前主流采用手机号注册方式,因为收集到手机号对用户推广、业务推广有极其重要的价值。结合上篇采用阿里云短信服务实现手机验证码的发送,这里整合实现用手机号实现用户注册。
思路:前端在输入手机号之后,需要对手机号进行校验,用户需要接收短信验证码并完成验证码校验之后即可成功注册。具体步骤:
1、判断当前手机号是否已注册;
2、调用阿里云短信服务api实现验证码发送;
3、验证码发送成功并存放至redis缓存,利用缓存淘汰机制(设置有效时间)实现验证码过期;
4、验证码校验,通过即注册成功。
1.1 用户实体类
package com.zhmsky.service_ucenter.entity;import com.baomidou.mybatisplus.annotation.*;import java.util.Date;import java.io.Serializable;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Data;import lombok.EqualsAndHashCode;/** * * 会员表 *
* * @author zhmsky * @since 2022-07-16 */@Data@EqualsAndHashCode(callSuper = false)@ApiModel(value="UcenterMember对象", description="会员表")public class UcenterMember implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "会员id") @TableId(value = "id", type = IdType.ASSIGN_ID) private String id; @ApiModelProperty(value = "微信openid") private String openid; @ApiModelProperty(value = "手机号") private String mobile; @ApiModelProperty(value = "密码") private String password; @ApiModelProperty(value = "昵称") private String nickname; @ApiModelProperty(value = "性别 1 女,2 男") private Integer sex; @ApiModelProperty(value = "年龄") private Integer age; @ApiModelProperty(value = "用户头像") private String avatar; @ApiModelProperty(value = "用户签名") private String sign; @ApiModelProperty(value = "是否禁用 1(true)已禁用, 0(false)未禁用") private Boolean isDisabled; @ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除") private Boolean isDeleted; @ApiModelProperty(value = "创建时间") @TableField(fill = FieldFill.INSERT) private Date createTime; @ApiModelProperty(value = "更新时间") @TableField(fill = FieldFill.UPDATE) private Date updateTime;}
1.2 注册用户视图对象
package com.zhmsky.service_ucenter.entity.VO;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Data;/** * @author zhmsky * @date 2022/7/16 17:18 */@Data@ApiModel(value="注册对象", description="注册对象")public class RegisterVO { @ApiModelProperty(value = "昵称") private String nickname; @ApiModelProperty(value = "手机号") private String mobile; @ApiModelProperty(value = "密码") private String password; @ApiModelProperty(value = "验证码") private String code;}
1.3 短信验证码的发送
调用阿里云短信服务实现短信验证码发送
package com.zhmsky.msmService.service.impl;import com.aliyuncs.DefaultAcsClient;import com.aliyuncs.IAcsClient;import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;import com.aliyuncs.exceptions.ClientException;import com.aliyuncs.exceptions.ServerException;import com.aliyuncs.profile.DefaultProfile;import com.zhmsky.msmService.service.MsmService;import org.springframework.stereotype.Service;/** * @author zhmsky * @date 2022/7/6 21:01 * 短信验证码实现类 */@Servicepublic class MsmServiceImpl implements MsmService { /** * 发送短信验证码 * @param phone 手机号 * @param code 被发送的验证码 * @return */ @Override public String sendCodeMsg(String phone, String code) { String checkCode=""; DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "LTAI5tDfHQPQ5WA5dBkrfFxu", "eRID7ZigveAH7fMRKbCDq92jjRl68R"); IAcsClient client = new DefaultAcsClient(profile); SendSmsRequest request = new SendSmsRequest(); request.setSignName("阿里云短信测试"); request.setTemplateCode("SMS_154950909"); request.setPhoneNumbers(phone); request.setTemplateParam("{\"code\":\""+code+"\"}"); try { SendSmsResponse response = client.getAcsResponse(request); checkCode = response.getCode(); } catch (ServerException e) { e.printStackTrace(); } catch (ClientException e) { System.out.println("ErrCode:" + e.getErrCode()); System.out.println("ErrMsg:" + e.getErrMsg()); System.out.println("RequestId:" + e.getRequestId()); } return checkCode; }}
短信验证码发送接口
package com.zhmsky.msmService.controller;import com.zhmsky.msmService.service.MsmService;import com.zhmsky.msmService.utils.RandomUtil;import com.zhmsky.result.Result;import com.zhmsky.result.ResultUtil;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.*;import java.util.HashMap;import java.util.Map;import java.util.concurrent.TimeUnit;/** * @author zhmsky * @date 2022/7/6 20:58 */@RestController@Api("短信注册控制器")@CrossOrigin@RequestMapping("/msmService")public class MsmController { @Autowired private MsmService msmService; @Autowired private RedisTemplate<String,String> redisTemplate; @GetMapping("/send/{phone}") @ApiOperation("发送短信验证码") public Result<String> sendMsg(@PathVariable String phone){ //通过redis设置缓存时间实现验证码过期 String code = redisTemplate.opsForValue().get(phone); if(!StringUtils.isEmpty(code)){ return new ResultUtil<String>().setSuccessMsg("请勿重复发送!"); } /** 如果缓存里面没有那么就重新发送 **/ //生成随机验证码 String fourBitRandom = RandomUtil.getFourBitRandom(); //调用阿里云api实现短信发送 String checkCode = msmService.sendCodeMsg(phone, fourBitRandom); if("OK".equals(checkCode)){ //将验证码保存到redis中并设置有效时间为5分钟 redisTemplate.opsForValue().set(phone,fourBitRandom,5, TimeUnit.MINUTES); return new ResultUtil<String>().setData(fourBitRandom,"验证码发送成功!"); }else{ return new ResultUtil<String>().setErrorMsg("验证码发送给失败!"); } }}
1.4 注册功能实现
1、接口保护,参数非空判断;
2、验证手机号是否已注册;
3、验证码校验;
4、入库
/** * 用户注册 * @param registerVO * @return */ @Override public boolean register(RegisterVO registerVO) { //获取注册数据,接口保护,参数校验 String code = registerVO.getCode(); String mobile = registerVO.getMobile(); String nickname = registerVO.getNickname(); String password = registerVO.getPassword(); if(StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)||StringUtils.isEmpty(code)||StringUtils.isEmpty(nickname)){ throw new MyException(20005,"注册失败!"); } //判断手机号是否已注册 QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>(); wrapper.eq("mobile",mobile); Long count = baseMapper.selectCount(wrapper); if(count>0){ throw new MyException(20005,"注册失败!"); } //验证码校验 //先从redis中获取验证码 String cacheCode = redisTemplate.opsForValue().get(mobile); if(!code.equals(cacheCode)){ throw new MyException(20005,"注册失败!"); } //入库 UcenterMember ucenterMember = new UcenterMember(); ucenterMember.setMobile(mobile); ucenterMember.setNickname(nickname); ucenterMember.setPassword(MD5.encrypt(password)); ucenterMember.setIsDisabled(false); ucenterMember.setAvatar("http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132"); int insert = baseMapper.insert(ucenterMember); if(insert>0){ return true; }else{ return false; } }
二、用户登录
2.1 账号密码登录
登录流程:
1、调用登录接口并返回token字符串
2、将返回的token字符串放到cookie里面
3、创建前端拦截器,判断cookie里面是否有token字符串,如果有则将token放到request的header中
4、调用接口,根据token获取用户信息并再次放到cookie中(为了首页面展示)
5、再从cookie中取出用户信息进行展示
思路:
1、接口保护,参数非空校验;
2、验证账号是否注册;
3、验证账号是否禁用;
4、验证密码;
5、登录并返回token。
为了实现单点登录,采用token令牌方式,引入jwt
jwt工具类:
package com.zhmsky.jwt;import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jws;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import org.springframework.util.StringUtils;import javax.servlet.http.HttpServletRequest;import java.util.Date;/** * @author zhmsky * @date 2022/7/6 17:53 */public class JwtUtils { //设置token过期时间 public static final long EXPIRE = 1000 * 60 * 60 * 24; //一天 //签名加密密钥 public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; /** * 生成token * @param id * @param nickname * @return */ public static String getJwtToken(String id, String nickname){ String JwtToken = Jwts.builder() //设置头信息 .setHeaderParam("typ", "JWT") .setHeaderParam("alg", "HS256") //设置token过期时间 .setSubject("user") //随便写 .setIssuedAt(new Date()) //当前时间加上设置的过期时间 .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) //设置token主体,可存储用户信息 .claim("id", id) .claim("nickname", nickname) //签名哈希 .signWith(SignatureAlgorithm.HS256, APP_SECRET) .compact(); return JwtToken; } /** * 判断token是否存在与有效 * @param jwtToken * @return */ public static boolean checkToken(String jwtToken) { if(StringUtils.isEmpty(jwtToken)) return false; try { Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 判断token是否存在与有效 * @param request * @return */ public static boolean checkToken(HttpServletRequest request) { try { String jwtToken = request.getHeader("token"); if(StringUtils.isEmpty(jwtToken)) return false; Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 根据token获取会员id * @param request * @return */ public static String getMemberIdByJwtToken(HttpServletRequest request) { String jwtToken = request.getHeader("token"); if(StringUtils.isEmpty(jwtToken)) return ""; Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); Claims claims = claimsJws.getBody(); return (String)claims.get("id"); }}
登录业务:
/** * 用户登录 * @param member * @return token */ @Override public String login(UcenterMember member) { //获取账号和密码 String mobile = member.getMobile(); String password = member.getPassword(); if(StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)){ throw new MyException(20010,"账号和密码不能为空!"); } //判断账号和密码是否存在 QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>(); wrapper.eq("mobile",mobile); UcenterMember ucenterMember = baseMapper.selectOne(wrapper); if(ObjectUtils.isEmpty(ucenterMember)){ throw new MyException(20011,"账号不存在,请重新输入!"); } //判断该用户是否被禁用 Boolean isDisabled = ucenterMember.getIsDisabled(); if(isDisabled){ throw new MyException(20013,"该账号已禁用!"); } //判断密码是否正确 //密码存储肯定是加密的,实际开发中数据库不会存储明文密码 //先将输入的密码加密,再和数据库密码比较 //MD5加密 String realPassword = ucenterMember.getPassword(); if(!MD5.encrypt(password).equals(realPassword)){ throw new MyException(20012,"密码错误,请重新输入!"); } //登录成功,返回token(通过查出来的用户数据去生成token) return JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname()); }
登录接口:
@PostMapping ("/login") @ApiOperation("用户登录") public Result<String> login(@RequestBody(required = false) UcenterMember ucenterMember){ //登录生成token并返回 String token = memberService.login(ucenterMember); return new ResultUtil<String>().setData(token); }
登录成功后,前端每次请求都携带token,从request对象中获取token再解析token获取用户信息。
@GetMapping("/getUserInfo") @ApiOperation("根据token获取用户信息") public Result<UcenterMember> getUserInfo(HttpServletRequest httpServletRequest){ //从request对象中获取token,再根据token获取用户信息 String userId = JwtUtils.getMemberIdByJwtToken(httpServletRequest); //根据用户id获取用户信息 UcenterMember ucenterMember = memberService.getById(userId); return new ResultUtil<UcenterMember>().setData(ucenterMember); }
2.2 微信扫码登录
2.2.1 准备工作:
网站应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统。
在进行微信OAuth2.在进行微信OAuth2.0授权登录接入之前,在微信开放平台注册开发者帐号,并拥有一个已审核通过的网站应用,并获得相应的AppID和AppSecret,申请微信登录且通过审核后,可开始接入流程。
2.2.2 授权流程
- 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
- 通过code参数加上AppID和AppSecret等,通过API换取access_token;
- 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
第一步:请求生成微信二维码
根据官方文档,直接请求微信开放平台固定地址https://open.weixin.qq.com/connect/qrconnect” />
获取随机code,请求微信资源服务器固定地址拿到accessToken(访问凭证)和openId(用户唯一标识),再通过accessToken和openId请求微信资源服务器固定地址拿到扫码人的基本信息。获取到用户基本信息后就可以进行校验完成入库等操作。@GetMapping("/callback") @ApiOperation("微信扫码确认后执行回调") public String callback(String code,String state){ //1、接收code值 //用code去请求微信的固定地址,得到accessToken和openId //向认证服务器发送请求换取access_token String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" + "?appid=%s" + "&secret=%s" + "&code=%s" + "&grant_type=authorization_code"; //带参数的真实认证服务器请求地址 String accessTokenUrl = String.format(baseAccessTokenUrl, ConstWxUtil.WX_OPEN_APP_ID, ConstWxUtil.WX_OPEN_APP_SECRET, code ); //2、请求认证服务器获取接口调用凭证access_token和用户唯一标识openId try { String accessTokenInfo = HttpClientUtils.get(accessTokenUrl); //将上述json字符串转换成map对象 /* { "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN", "openid":"OPENID", "scope":"SCOPE", "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" } */ Gson gson = new Gson(); HashMap mapAccessTokenInfo = gson.fromJson(accessTokenInfo, HashMap.class); //取出的access_token String access_token = (String)mapAccessTokenInfo.get("access_token"); //取出的openid String openid = (String)mapAccessTokenInfo.get("openid"); //3、再通过获取出来的access_token和openid去请求微信开放平台服务器获取扫码人信息 //访问微信的资源服务器,获取用户信息 String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" + "?access_token=%s" + "&openid=%s"; String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid); //发送请求获取扫码人基本信息 String userInfo = HttpClientUtils.get(userInfoUrl); /* { "openid":"o3_SC5_eI--mIC9ikI2pvTuZhYnk", "nickname":"Kong", "sex":0, "language":"", "city":"", "province":"", "country":"", "headimgurl":"https:\/\/thirdwx.qlogo.cn\/mmopen\/vi_32\/hAqkcbxzEJzic0WYl9pHDglAvYBI4iagLsSLXb2ialcxa3Au6UmwibSiadGMtbQia0oAzmzq26k2f1ES4q1HbS6aIYuA\/132", "privilege":[], "unionid":"oWgGz1OqVll-tTU4R_DM_zRp7Rjc" } */ //将上面的json字符串转换成map对象 HashMap mapUserInfo = gson.fromJson(userInfo, HashMap.class); //扫码人基础信息 String nickname = (String)mapUserInfo.get("nickname"); String headImgurl=(String)mapUserInfo.get("headimgurl"); String openId=(String)mapUserInfo.get("openid"); //扫码后自动注册(入库) //先判断是否已注册 boolean isExist = memberService.getUserOpenId(openId); if(!isExist){ //入库 UcenterMember member = new UcenterMember(); member.setNickname(nickname); member.setOpenid(openid); member.setAvatar(headImgurl); memberService.save(member); } UcenterMember ucenterMember = memberService.getUserByOpenId(openId); String token = JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname()); //因为端口号不同存在跨域问题,cookie不能跨域,所以这里使用url重写 return "redirect:http://localhost:3000?token="+token; } catch (Exception e) { e.printStackTrace(); throw new MyException(20010,"登录失败!"); } }
总结:
实现微信授权登录就好比开宝箱,一共需要3把钥匙,第一把钥匙是appid(这个需要在微信开放平台完成注册和认证由平台颁发);通过appid请求固定地址可以生成微信二维码;用户扫描二维码授权后拿到第二把钥匙code(随机唯一值);再通过code去请求固定服务器地址拿到第三把钥匙openid(用户唯一标识)和accessToken(访问凭证);最后再通过openid和accessToken请求固定地址拿到微信用户基本信息。