基于Spring+SpringMVC+MyBatis博客系统的开发教程(十三)

第13课:第三方 QQ 登录及账号绑定与解除

使用 QQ 第三方登录时要调用第三方接口,需要 AppID 和 AppKey 等信息,所以首先要申请注册一下。

申请注册

首先在百度搜索 QQ 互联或者点击下面链接进入 QQ 互联官网:

https://connect.qq.com/index.html

往下滚动鼠标找到网站应用,然后点击开始创建,如下图:

之后会进入个人资料填写页面,选择“个人接入”,填写好个人信息后点击下一步,注意身份证照片是手持身份证正面拍摄的照片。如图:

接着会让你去邮箱激活,点击邮件内的激活链接,会提示恭喜注册成功,然后点击前往管理中心,可看到开发者提交审核的状态,如下图:

上面提示说审核要7个工作日,我直接找的在线客服,结果很快就审核通过了。

审核通过之后点击“创建应用”,选择“创建网站应用”,填写资料后,点击“创建应用”,如下图:

进入到完善资料页面,需要填写网站域名、回调域等,填写完成之后点击创建应用。其中回调域多写一个用于本地测试,以 http://localhost 开头,以后还可以修改。没有提供方和备案号的同学可随便填,能创建应用就行,主要是为了获取 AppID 和 AppKey,如下图:

到此,创建应用就成功了,前往管理中心查看,如图:

状态处于审核中,找在线客户,审核会很快。

点击查看可以看到你的 AppID 和 AppKey,这两个信息是我们程序中要使用的,记得要保密,不要透露给他人。

QQ 快捷登录

用户在没有账号的情况下允许第三方 QQ 登录。我们接下来看下具体实现步骤。

在注册页面、登录页面添加 QQ 登录图标

QQ 图标可从 QQ 互联官网下载,我会将 QQ 图标放在百度网盘,大家可以在 webapp/images 目录下找到。

这里以注册页面为例,将注册页面的右侧 div 修改如下:

 <div id="regist-right" class="bj_right" style="height: 468px">
        <p>使用以下账号直接登录</p>
        <div>
          <a href="to_login"><img src="${ctx}/images/Connect_logo_3.png" /></a>
        </div>
        <p>已有账号?<a href="login.html">立即登录</a></p>

     </div>

修改后效果如图:

下载 SDK

SDK 的下载地址请见下:

http://wiki.connect.qq.com/sdk%E4%B8%8B%E8%BD%BD

找到 Java 的 SDK 进行下载,下载完成后进行解压,将解压目录的 Sdk4J.jar 打包到 Maven 仓库,和之前方法一样,打开命令窗口输入:

mvn install:install-file -Dfile=D:\sdk\Sdk4J.jar -DgroupId=wang.dreamland.www -DartifactId=Sdk4J -Dversion=1.0.0 -Dpackaging=jar -DgeneratePom=true -DcreateChecksum=true

引入刚刚打包的依赖:

<!--qq-->
<dependency>
    <groupId>wang.dreamland.www</groupId>
    <artifactId>Sdk4J</artifactId>
    <version>1.0.0</version>
</dependency>

然后将解压目录的 qqconnectconfig.properties 配置文件复制到项目的 resources 资源目录下,修改其中前三项,将 app_IDapp_KEY 和回调地址 redirect_URI 替换成自己刚才注册申请的。

QQ 授权登录流程

QQ 授权登录流程如下图所示:

主要包括四大步:

  1. 用户点击 QQ 登录按钮(/to_login");

  2. 后台 LoginController 收到请求后,根据配置文件获取 AppID、Scope 和回调地址等参数信息,然后重定向到 QQ 授权登录页面并携带上述参数;

  3. 进入授权登录页面之后,用户扫码或者点击头像授权后,会跳向我们配置的回调地址 http://localhost:8080/qq_login,并携带上述参数;

  4. 后台 LoginController 收到登录请求(/qq_login)后,通过上述参数调用第三方 API 获取 AccessToken 对象,该对象就包含了用户唯一标识 OpenID、AccessToken 和 QQ 昵称、QQ 头像等。将我们需要的信息存入数据库中之后返回到个人主页,完成 QQ 授权登录。

实现步骤

首先设计一张 open_user 表,用于储存第三方登录用户信息,包括以下字段:

    id              //主键        
    u_id            //用户id
    open_id         //用户唯一标识
    access_token    //授权令牌
    nick_name       //qq昵称
    avatar          //qq头像
    open_type       //第三方类型,比如QQ、WEIXIN、WEIBO
    expired_time    //授权令牌过期时间,默认为三个月

open_user.sql 文件已放入文末的百度网盘中。

接着,生成对应实体类 OpenUser,然后书写 OpenUserService 接口、OpenUserServiceImpl 实现类和 OpenUserMapper 接口。

这里就不再介绍生成过程了,我会直接将相关文件放在文末的百度网盘中。

然后,在 common 包下的 Constants 类下新增第三方类型常量:

    public static final String OPEN_TYPE_QQ = "QQ";
    public static final String OPEN_TYPE_WEIXIN = "WEIXIN";
    public static final String OPEN_TYPE_WEIBO = "WEIBO";

紧接着,在 LoginController.java 中创建映射 URL 为 /to_login 的方法:

    @RequestMapping("/to_login")
    public String toLogin(Model model) {
        HttpServletRequest request = getRequest();
        String url = "";
        try {
            url = new Oauth().getAuthorizeURL(request);
        } catch (QQConnectException e) {
            e.printStackTrace();
        }
        return "redirect:"+url;
    }

调用第三方接口 API 获取 URL,URL 中携带了 client_id 即 AppID、Scope 和回调地址等,然后重定向到该 URL。

关于各个参数的含义 QQ 互联官方文档上有详细说明,这里就不做介绍了。

用户扫码或者点击 QQ 头像登录后,会跳至我们配置的回调地址,在 LoginController 中创建映射 URL 为 qq_login 的方法:

 @Autowired
    private OpenUserService openUserService;
    @RequestMapping("/qq_login")
    public String qqLogin(Model model) {
        User user = null;
        try {
            AccessToken accessTokenObj = (new Oauth()).getAccessTokenByRequest(getRequest());
            String accessToken   = null, openID   = null;
            long tokenExpireIn = 0L;
            if (accessTokenObj.getAccessToken().equals("")) {
                System.out.print("没有获取到响应参数");
            } else {
                accessToken = accessTokenObj.getAccessToken();//授权令牌token
                tokenExpireIn = accessTokenObj.getExpireIn();//过期时间

                // 利用获取到的accessToken 去获取当前用的openid -------- start
                OpenID openIDObj =  new OpenID(accessToken);
                openID = openIDObj.getUserOpenID();//用户唯一标识
                // 利用获取到的accessToken 去获取当前用户的openid --------- end

                UserInfo qzoneUserInfo = new UserInfo(accessToken, openID);
                UserInfoBean userInfoBean = qzoneUserInfo.getUserInfo();
                if (userInfoBean.getRet() == 0) {
                    OpenUser openUser =  openUserService.findByOpenId( openID );
                    if(openUser == null){
                        redisTemplate.opsForValue().set(openID, accessToken, 90, TimeUnit.DAYS);// 有效期三个月
                        openUser = new OpenUser();
                        user = new User();
                        user.setEmail( openID );
                        user.setPassword(MD5Util.encodeToHex(Constants.SALT+accessToken) );
                        user.setEnable( "0" );
                        user.setState("1");
                        user.setNickName( userInfoBean.getNickname() );//设置qq昵称
                        user.setImgUrl( userInfoBean.getAvatar().getAvatarURL50() );//设置qq头像
                        userService.regist( user );
                        openUser.setOpenId( openID );
                        openUser.setAccessToken( accessToken );
                        openUser.setAvatar( userInfoBean.getAvatar().getAvatarURL50() );
                        openUser.setExpiredTime( tokenExpireIn);
                        openUser.setNickName( userInfoBean.getNickname() );
                        openUser.setOpenType( Constants.OPEN_TYPE_QQ );
                        openUser.setuId( user.getId());
                        openUserService.add( openUser );
                    }else {
                         String token = redisTemplate.opsForValue().get( openID );//从redis获取accessToken
                        if(token==null){
                            //已过期
                            openUser.setAccessToken( accessToken );
                            openUser.setAvatar( userInfoBean.getAvatar().getAvatarURL50() );
                            openUser.setExpiredTime( tokenExpireIn);
                            openUser.setNickName( userInfoBean.getNickname() );
                            openUserService.update(openUser);
                        }
                        user = userService.findById( openUser.getuId() );
                    }

                } else {
                    log.info("很抱歉,我们没能正确获取到您的信息,原因是: " + userInfoBean.getMsg());
                }

            }
        } catch (QQConnectException e) {
            e.printStackTrace();
        }
        getSession().setAttribute("user",user);
        return "redirect:/list";
    }

代码解读如下:

(1)Autowired 通过 @Autowired 注入 OpenUserService 对象。

(2)调用第三方 API 获取 AccessToken 对象,根据 AccessToken 对象获取授权令牌和令牌过期时间。

(3)调用第三方 API 根据授权令牌获取 OpenID 对象,根据 OpenID 获取用户唯一标识 OpenID。

(4)根据授权令牌 AccessToken 和用户唯一标识 OpenID 获取 UserInfo 对象,注意这里的 UserInfo 是第三方的实体类,注意导包:

import com.qq.connect.api.qzone.UserInfo;

(5)根据 UserInfo 对象调用 getUserInfo 获取 QQ 用户信息对象 UserInfoBean,该对象包含了 QQ 用户的基本信息:昵称、头像、性别等。

(6)根据用户唯一标识 OpenID 查询 open_user 表,判断该 QQ 用户是否存在,如果不存在则将 AccessToken 保存到 Redis 中,有效期为三个月,key 为 OpenID,然后创建 OpenUser 对象。因为是 QQ 第三方临时登录,此时还没有 User 信息,所以先创建 User 对象,给它分配一个账号和密码,账号就用 OpenID,密码就用经过 MD5 加密后的 AccessToken,当然这里的 Email 账号是不正确的,这里只是临时让用户使用,以后引导用户修改相关信息,然后再设置 state、nickName 等信息,设置完以后插入 user 表中,然后将 OpenID、AccessToken 等其他信息保存到 open_user 表中。

(7)如果查询结果不为空,说明该 QQ 用户已经登录过,根据 OpenID 从 Redis 中查询 AccessToken,判断是否为 null,为 null 则代表授权令牌已过期,则更新 OpenUser 相关信息,然后根据用户 id 查询出用户 User。

(8)最后将用户 user 保存到 Session 中,返回用户个人主页。

重新启动项目,测试第三方 QQ 登录成功!

但是进入修改资料查看,发现账号显示的是 OpenID:

这样肯定是不行的。

所以在进入 profile.jsp 之前进行判断,如果用户 Email 不包含 @ 则认为是第三方登录,引导其进行账号设置。

QQ 账号绑定

引导用户设置信息

我们先梳理下思路。其过程是这样:点击修改个人资料时,判断用户是否是第三方登录且没有绑定账号,如果是则弹出设置账号信息的窗口,引导其进行账号设置。如果已经绑定账号则直接跳转到 profile.jsp 页面。

整个过程主要包括以下几个步骤。

1. 点击修改个人资料,触发点击事件 updateProfile,方法如下:

function updateProfile(email) {
  if(email.indexOf("@")!=-1){
  window.location.href = "${ctx}/profile";
  }else{
   $('#myModal').modal('toggle', 'center');
  }

}

判断是否包含 @,如果有则认为已经是注册用户或者是已绑定账号用户,直接跳转到个人资料修改页面。

如果不包含 @,则认为是第三方登录且未绑定账号的用户,弹出信息设置页面,引导其进行设置。

2. 在 personal.jsp 中引入弹窗 div,弹窗来自 ZUI,经过改造而成,如下:

 <!-- 弹出设置信息对话框 -->
    <div class="modal fade" id="myModal">
    <div class="modal-dialog" style="width: 0px;margin-left: 550px;">
        <div class="modal-content">
            <div class="tab-pane fade in active" id="account-login">
                <div class="content" >
                    <div class="col-sm-6 col-sm-offset-3 col-md-4 col-sm-offset-4 login-box" style="width: 450px;height: 260px;">
                        <span id="update-span" style="color: red"></span>
                        <form id="normal_form" name="form" role="form" class="login-form" action="${ctx}/profile" method="post">
                            <div class="form-group">
                                <label for="email" class="sr-only">用户名</label>
                                <input type="text" id="email" name="email" onblur="checkEmail();" class="form-control" placeholder="邮箱账号">
                            </div>
                            <div class="form-group">
                                <label for="password" class="sr-only">密码</label>
                                <input type="password" id="password" onblur="checkPassword();" name="password" class="form-control" placeholder="密码">
                            </div>
                            <div class="form-group">
                                <label for="phone" class="sr-only">手机号</label>
                                <input type="text" id="phone" name="phone" onblur="checkPhone();" class="form-control" placeholder="手机号">
                            </div>
                        </form>
                        <div class="form-group" style="margin-top: 30px ">
                            <div style="width: 80px;float: left">
                                <button type="button" id="btn" onclick="sure();"  class="btn btn-primary btn-block">确定</button>
                            </div>
                            <div style="width: 80px;float: left;margin-left: 30px">
                                <button type="button" id="cancle" onclick="cancle();" class="btn btn-primary btn-block">取消</button>
                            </div>


                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    </div>

3. 设置 CSS 样式:

    .login-box {
            background: white;
            box-shadow: 0 0 0 15px rgba(255, 255, 255, .1);
            border-radius: 5px;
            padding: 40px;
        }

4. 启动访问效果如下:

5. 对表单分别进行离焦校验,判断其正确性,这里的校验和之前一样,就不再赘述了。

6. 如果用户点击取消,则隐藏弹窗,取消按钮点击事件如下:

//取消
function cancle() {
    $('#myModal').modal('hide');
}

7. 以上校验都正确之后便提交表单,确定按钮点击事件如下:

//确定
function sure() {
    if(checkEmail() && checkPassword() && checkPhone()){
        $("#normal_form").submit();
        alert("激活邮件已发送,请前往邮箱查看并激活账号!");
    }else{
        $("#update-span").text("请将信息填写完整!");
    }
}

8. 修改后台 PersonalController 的 profile 方法,如下:

@Autowired
private OpenUserService openUserService;
@RequestMapping("/profile")
public String profile(Model model, @RequestParam(value = "email",required = false) String email,
                      @RequestParam(value = "password",required = false) String password,
                      @RequestParam(value = "phone",required = false) String phone) {
    User user = (User)getSession().getAttribute("user");
    if(user==null){
        return "../login";
    }

    if(StringUtils.isNotBlank(email)){
        user.setEmail(email);
        user.setPassword(MD5Util.encodeToHex(Constants.SALT+password));
        user.setPhone(phone);
        userService.update(user);
    }
    List<OpenUser> openUsers = openUserService.findByUid(user.getId());
    if(openUsers!=null && openUsers.size()>0){
        for(OpenUser openUser:openUsers){
            if(Constants.OPEN_TYPE_QQ.equals(openUser.getOpenType())){
                    model.addAttribute("qq",openUser.getOpenType());
                }else if(Constants.OPEN_TYPE_WEIBO.equals(openUser.getOpenType())){
                    model.addAttribute("weibo",openUser.getOpenType());
                }else if(Constants.OPEN_TYPE_WEIXIN.equals(openUser.getOpenType())){
                    model.addAttribute("weixin",openUser.getOpenType());
                }
            }
        }     
        UserInfo userInfo =   userInfoService.findByUid(user.getId());
        model.addAttribute("user",user);
        model.addAttribute("userInfo",userInfo);

        return "personal/profile";
    }

代码解读如下:

(1)通过 @Autowired 注解注入 OpenUserService 对象。

(2)判断用户是否为 null,如果是则跳转到登录页面。

(3)判断 Email 是否为空,如果不为空,说明用户进行了信息设置,则将相关信息添加到 user 对象中,最后更新用户表信息。

(4)根据用户 id 查询 open_user 表,会得到一个 List 集合,如果 List 不为空并且长度大于0,则至少是某一种第三方登录,遍历集合,如果是 QQ 第三方登录,则将 QQ 添加到 model 中,其他同理,主要用于后续的账号绑定判断。

(5)查询用户详细信息并添加到 model 中,将 user 也添加到 model 中,最后返回个人资料修改页面。

9. 修改 profile.jsp 页面,判断是否已绑定第三方账号,以 QQ 为例:

     <c:if test="${empty qq}">
          <div style="float: left;">
              Q Q:未绑定
           </div>
           <div style="float: left;margin-left: 150px">
                   <span id="qq_span" style="color: grey" onmouseover="changeColor(this);" onmouseout="backColor(this);">立即绑定</span>
           </div>
      </c:if>
      <c:if test="${not empty qq}">
          <div style="float: left;">
              Q Q:已绑定
           </div>
           <div style="float: left;margin-left: 150px">
                            <span id="qq_span_remove" style="color: grey" onmouseover="changeColor(this);" onmouseout="backColor(this);">解除绑定</span>
            </div>
       </c:if>

上面代码,根据 EL 表达式判断 QQ 是否为空,为空,则显示“未绑定——立即绑定”,不为空则显示“已绑定——解除绑定”。

效果如图:

绑定与解除绑定
QQ 绑定

假如现在是一个已有账号的用户,点击立即绑定 QQ,会触发和刚开始点击 QQ 快捷登录一样的事件:

//qq绑定
function binding_qq() {
    window.location.href = "${ctx}/to_login";
}

返回到授权登录页面后,用户点击授权,进入 LoginController 中映射 URL 为 qq_login 的方法,将其方法修改如下:

@RequestMapping("/qq_login")
public String qqLogin(Model model) {
    User user = (User)getSession().getAttribute("user");
    boolean flag =false;
    try {
        AccessToken accessTokenObj = (new Oauth()).getAccessTokenByRequest(getRequest());
        String accessToken   = null, openID   = null;
        long tokenExpireIn = 0L;
        if (accessTokenObj.getAccessToken().equals("")) {
            System.out.print("没有获取到响应参数");
        } else {
            accessToken = accessTokenObj.getAccessToken();//授权令牌token
            tokenExpireIn = accessTokenObj.getExpireIn();//过期时间

            // 利用获取到的accessToken 去获取当前用的openid -------- start
            OpenID openIDObj =  new OpenID(accessToken);
            openID = openIDObj.getUserOpenID();//用户唯一标识
            // 利用获取到的accessToken 去获取当前用户的openid --------- end

            UserInfo qzoneUserInfo = new UserInfo(accessToken, openID);
            UserInfoBean userInfoBean = qzoneUserInfo.getUserInfo();
            if (userInfoBean.getRet() == 0) {
                OpenUser openUser =  openUserService.findByOpenId( openID );
                if(openUser == null){
                    redisTemplate.opsForValue().set(openID, accessToken, 90, TimeUnit.DAYS);// 有效期三个月
                    openUser = new OpenUser();
                    if(user==null){
                        flag = true;
                        user = new User();
                        user.setEmail( openID );
                            user.setPassword(MD5Util.encodeToHex(Constants.SALT+accessToken) );
                        user.setEnable( "1" );
                        user.setState("0");
                        user.setNickName( userInfoBean.getNickname() );//设置qq昵称
                        user.setImgUrl( userInfoBean.getAvatar().getAvatarURL50() );//设置qq头像
                        userService.regist( user );
                    }
                    openUser.setOpenId( openID );
                    openUser.setAccessToken( accessToken );
                    openUser.setAvatar( userInfoBean.getAvatar().getAvatarURL50() );
                    openUser.setExpiredTime( tokenExpireIn);
                    openUser.setNickName( userInfoBean.getNickname() );
                    openUser.setOpenType( Constants.OPEN_TYPE_QQ );
                    openUser.setuId( user.getId());
                    openUserService.add( openUser );
                }else {
                    String token = redisTemplate.opsForValue().get( openID );//从redis获取accessToken
                    if(token==null){
                        //已过期
                        openUser.setAccessToken( accessToken );
                        openUser.setAvatar( userInfoBean.getAvatar().getAvatarURL50() );
                        openUser.setExpiredTime( tokenExpireIn);
                        openUser.setNickName( userInfoBean.getNickname() );
                        openUserService.update(openUser);
                    }
                    user = userService.findById( openUser.getuId() );
                }

            } else {
                    log.info("很抱歉,我们没能正确获取到您的信息,原因是: " + userInfoBean.getMsg());
            }

        }
    } catch (QQConnectException e) {
        e.printStackTrace();
    }
    getSession().setAttribute("user",user);
    if(flag){
        return "redirect:/list";
    }else {
        model.addAttribute("qq",Constants.OPEN_TYPE_QQ );
        wang.dreamland.www.entity.UserInfo userInfo =   userInfoService.findByUid(user.getId());
        model.addAttribute("user",user);
        model.addAttribute("userInfo",userInfo);
        return "personal/profile";
    }
}

主要修改如下:

(1)从 Session 中获取用户 User。

(2)设置标志位 flag。

(3)判断如果用户为 null 则将 flag 置为 true 并创建 user,然后设置临时账号、密码等,如果已存在则不进行操作,直接将用户 id 赋值给 OpenUser 对象。

(4)根据标志位 flag 判断跳转页面,如果为 false 则说明用户进行 QQ 绑定设置,将相关信息添加到 model 中,然后返回到个人资料修改页面,如果为 true 则跳转到个人主页。其他的代码和之前一样。

最后启动测试,QQ 账号绑定设置成功!

解除绑定

QQ 解除绑定 div 的点击事件如下:

//qq解除绑定
function qq_span_remove() {
    if(confirm("确定解除qq绑定吗?")){
           window.location.href = "${ctx}/remove_qq";
    }
}

在后台 PersonalController 中创建映射 URL 为 /remove_qq 的方法:

@RequestMapping("/remove_qq")
    public String removeQQ(Model model){
        User user = (User) getSession().getAttribute("user");
        if(user == null){
            return "../login";
        }
        openUserService.deleteByUidAndType(user.getId(),Constants.OPEN_TYPE_QQ);
        List<OpenUser> openUsers = openUserService.findByUid(user.getId());
        setAttribute(openUsers,model);
        UserInfo userInfo =   userInfoService.findByUid(user.getId());
        model.addAttribute("user",user);
        model.addAttribute("userInfo",userInfo);
        return "personal/profile";
    }

    public void setAttribute(List<OpenUser> openUsers,Model model){
        if(openUsers!=null && openUsers.size()>0){
            for(OpenUser openUser:openUsers){
                if(Constants.OPEN_TYPE_QQ.equals(openUser.getOpenType())){
                    model.addAttribute("qq",openUser.getOpenType());
                }else if(Constants.OPEN_TYPE_WEIBO.equals(openUser.getOpenType())){
                    model.addAttribute("weibo",openUser.getOpenType());
                }else if(Constants.OPEN_TYPE_WEIXIN.equals(openUser.getOpenType())){
                    model.addAttribute("weixin",openUser.getOpenType());
                }
            }
        }
    }

代码解读如下:

(1)从 Session 中取出 User,判断如果为空直接跳转到登录页面。

(2)根据用户 id 和第三方登录类型删除 OpenUser 对象。

(3)根据用户 id 查询 open_user 表,把其他第三方登录信息查询出来,添加到 model 中,这里将相同方法封装了一下(setAttribute 方法)。

(4)根据用户 id 查询出用户详细信息并添加到 model 中,将 user 页添加到 model 中,然后返回到个人资料修改页面。

最后重启项目测试,解除 QQ 绑定成功!

第13课百度网盘地址:

链接:https://pan.baidu.com/s/1TaqfnWfmj9boeobo7fF9Og 密码:oqap

本课程所开发的项目已经完成,完整代码已托管 Github,大家可通过下面地址下载:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

exodus3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值