springboot实现几种常见登录(注册)方式

1年前 (2022) 程序员胖胖胖虎阿
206 0 0

一、用户注册(手机验证码)

目前主流采用手机号注册方式,因为收集到手机号对用户推广、业务推广有极其重要的价值。结合上篇采用阿里云短信服务实现手机验证码的发送,这里整合实现用手机号实现用户注册。

思路:前端在输入手机号之后,需要对手机号进行校验,用户需要接收短信验证码并完成验证码校验之后即可成功注册。具体步骤:
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;

/**
 * <p>
 * 会员表
 * </p>
 *
 * @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
 * 短信验证码实现类
 */
@Service
public 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 授权流程

  1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
  2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
  3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
    springboot实现几种常见登录(注册)方式
    第一步:请求生成微信二维码
    springboot实现几种常见登录(注册)方式
    根据官方文档,直接请求微信开放平台固定地址https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect,其中有4个必备的参数;

appid:微信开放平台申请通过后颁发的唯一标识
redirect_uri:微信扫码授权后的回调地址
response_type:code
scope:授权作用域,web应用直接填snsapi_login即可

准备好以上参数,在properties文件中完成初始值设置

wx.open.app_id=wxed9954c01bb89b47
wx.open.app_secret=a7482517235173ddb4083788de60b90e
wx.open.redirect_url=http://localhost:8160/api/ucenter/wx/callback

编写参数初始化工具类:

package com.zhmsky.service_ucenter.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @author zhmsky
 * @date 2022/7/17 17:06
 */
@Component
public class ConstWxUtil implements InitializingBean {

    @Value("${wx.open.app_id}")
    private String appId;

    @Value("${wx.open.app_secret}")
    private String appSecret;

    @Value("${wx.open.redirect_url}")
    private String redirectUrl;

    public static String WX_OPEN_APP_ID;
    public static String WX_OPEN_APP_SECRET;
    public static String WX_OPEN_REDIRECT_URL;

    @Override
    public void afterPropertiesSet() throws Exception {
        WX_OPEN_APP_ID = appId;
        WX_OPEN_APP_SECRET = appSecret;
        WX_OPEN_REDIRECT_URL = redirectUrl;
    }
}

二维码生成接口:

    @GetMapping("/getWxCode")
    @ApiOperation("生成微信二维码")
    public String getWxCode() {
        // 微信开放平台授权baseUrl
        String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=%s" +
                "&redirect_uri=%s" +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=%s" +
                "#wechat_redirect";

        // 回调地址
        String redirectUrl = ConstWxUtil.WX_OPEN_REDIRECT_URL;
        try {
            redirectUrl = URLEncoder.encode(redirectUrl, "UTF-8"); //url编码
        } catch (UnsupportedEncodingException e) {
            throw new MyException(20001, e.getMessage());
        }
        String state = "imhelen";
        //生成qrcodeUrl
        String qrcodeUrl = String.format(
                baseUrl,
                ConstWxUtil.WX_OPEN_APP_ID,
                redirectUrl,
                state);

        return "redirect:" + qrcodeUrl;
    }

第二步:微信扫码后执行回调

    @GetMapping("/callback")
    @ApiOperation("微信扫码确认后执行回调")
    public String callback(String code,String state){
        //TODO 
        return "redirect:http://localhost:3000";
    }

再接着,
springboot实现几种常见登录(注册)方式
获取随机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请求固定地址拿到微信用户基本信息。

版权声明:程序员胖胖胖虎阿 发表于 2022年11月12日 上午10:40。
转载请注明:springboot实现几种常见登录(注册)方式 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...