Spring boot + Vue 微信第三方登录实践

1. 流程图

第三方登录流程图

微信登录流程如下:

  1. 点击微信登录,新开窗口,跳转到微信扫码页面
  2. 用户扫码登录,新开窗口跳转到后台微信登录接口
  3. 后台判断用户是否成功登录,将新开窗口重定向到前台微信登录处理页面
  4. 微信登录处理页面会关闭新开窗口,判断用户是否成功登录,如果用户成功登录,则写入cookie并且跳转首页,如果用户没有注册用户,则携带后端的token跳转注册页面

2. 前端代码核心逻辑实现

对于前端来说,主要

第一步 编写前端第三方登录处理类

import URL from 'url-parse'
export default class OauthHandler {
  constructor(name, thirdBaseUrl, appId) {
    // 当前第三方登录名称
    this.name = name
    // 第三方登录跳转URL
    this.thirdBaseUrl = thirdBaseUrl
    // 第三方登录APPID
    this.appId = appId
    this.newPage = null
  }
  login() {
    const referrer = (window.globalPortalVue.$route.query && window.globalPortalVue.$route.query.referrer) || '/'
    localStorage.setItem(constant['COMMON|OAUTH'].referrerLocalName, referrer)
    localStorage.setItem(constant['COMMON|OAUTH'].handlerLocalName, constant['COMMON|OAUTH_HANDLER'].login)
    this.openThirdPage(constant['COMMON|OAUTH_HANDLER'].login)
  }
  bind() {
    localStorage.setItem(constant['COMMON|OAUTH'].handlerLocalName, constant['COMMON|OAUTH_HANDLER'].bind)
    this.openThirdPage(constant['COMMON|OAUTH_HANDLER'].bind)
  }
  unBind() {
    localStorage.setItem(constant['COMMON|OAUTH'].handlerLocalName, constant['COMMON|OAUTH_HANDLER'].unbind)
    this.openThirdPage(constant['COMMON|OAUTH_HANDLER'].unbind)
  }
  async genThirdUrl(type) {
    const thirdUrl = new URL(this.thirdBaseUrl)
    return thirdUrl
  }
  getThirdPageSize() {
    return {
      width: 500,
      height: 500
    }
  }

  getType() {
    throw new Error('请覆写此方法')
  }
  async openThirdPage(type) {
    const thirdUrl = await this.genThirdUrl(type)
    const { width, height } = this.getThirdPageSize()
    this.unMountWindowOauthFunction()
    this.mountWindowOauthFunction()
    const windowHeight = window.screen.availHeight
    const windowWidth = window.screen.availWidth
    console.log(this.newPage)
    if (this.newPage != null && !this.newPage.closed) {
      return
    }
    this.newPage = window.open(
      thirdUrl.toString(),
      '_blank',
      `toolbar=no,directories=no,status=no,menubar=no,height=${height},width=${width},top=${(windowHeight - height) / 2},left=${(windowWidth - width) / 2}`
    )
    if (this.newPage) {
      return
    }
    window.open(
      thirdUrl.toString(),
      '_self'
    )
  }
  unMountWindowOauthFunction() {
    window.globalPortalVue.$off('oauthCallBack')
  }
  mountWindowOauthFunction() {
    console.log('监听事件')
    window.globalPortalVue.$on('oauthCallBack', (result) => {
      console.log('监听到事件')
      if (result.code === constant['COMMON|CODE'].success) {
        const name = localStorage.getItem(constant['COMMON|OAUTH'].handlerLocalName)
        this.successHandler(name, result)
      } else {
        this.errorHandler(result)
      }
    })
  }
  successHandler(name) {
    console.log('成功')
    // 登录
    if (name === constant['COMMON|OAUTH_HANDLER'].login) {
      new Promise((resolve, reject) => {
        event.emit('user/oauthLogin', resolve, reject)
      }).then(() => {
        new Promise((resolve, reject) => {
          event.emit('user/setUserEnterpriseList', resolve, reject)
        }).then(enterpriseList => {
          loginSuccessRedirect(enterpriseList)
        }).catch(err => {
          console.error(err.message)
          window.globalPortalVue.$Message.error(this.name + '登录失败!')
        })
      }).catch((err) => {
        console.error(err.message)
        window.globalPortalVue.$Message.error(this.name + '微信登录失败!')
      })
      return
    }
    // 绑定
    if (name === constant['COMMON|OAUTH_HANDLER'].bind) {
      new Promise((resolve, reject) => {
        event.emit('user/updateUserInfo', resolve, reject)
      }).then(re => {
        window.globalPortalVue.$Message.success('绑定' + this.name + '成功!')
      }).catch(err => {
        console.error(err)
        window.globalPortalVue.$Message.error('绑定' + this.name + '失败,请重试!')
      })
    }
    // 解除绑定
    if (name === constant['COMMON|OAUTH_HANDLER'].unbind) {
      new Promise((resolve, reject) => {
        event.emit('user/updateUserInfo', resolve, reject)
      }).then(re => {
        window.globalPortalVue.$Message.success('解绑' + this.name + '成功!')
      }).catch(err => {
        console.error(err)
        window.globalPortalVue.$Message.error('解绑' + this.name + '失败,请重试!')
      })
    }
  }
  errorHandler(result) {
    console.log('授权失败', result)
    if (result.code === constant['COMMON|CODE'].error) {
      window.globalPortalVue.$Message.error(result.message)
      return
    }
    if (result.code === constant['COMMON|CODE'].userNotFound) {
      window.globalPortalVue.$router.push({
        path: '/oauthLogin',
        query: {
          token: result.token,
          type: this.getType()
        }
      })
    }
  }
}

第三步 实现后台微信登录处理接口

实现微信登录Controller

@RequestMapping("/wechatOpenPlatform")
public class WechatOpenPlatformController{
    @Autowired
    WechatOauthService wechatOauthService;

    @GetMapping("login")
    @ApiOperation(value = "微信网页登录", notes = "微信网页登录")
    public void login(@PathVariable String region, @NotNull(message = "微信登录参数错误") String code, HttpServletResponse response) {
        wechatOauthService.wechatLogin(code, response);
    }
}

实现微信登录service实现

public interface WechatOauthService {

    /**
     * 微信网页登录
     * 
     * @param oauth2Handler
     * @param callbackUrl
     * @param authTokenForm
     * @param response
     */
    void wechatLogin(String code,
        HttpServletResponse response);

}
@Service
public class WechatOauthServiceImpl implements WechatOauthService {
  private static String callBackUrl = "http://www.xxxx.com/#/wechat/callback";
  private static String openAppId= "xxxxx";
  private static String openSecret= "xxxx";

  @Autowired
  UserService userService;

  @Autowired
  AutowireCapableBeanFactory autowireCapableBeanFactory;

  @Override
  public void wechatLogin (String code, HttpServletResponse response) {
    WechatHandler wechatHandler= new WechatHandler(openAppId, openSecret);
    // 实现容器外bean注入
    autowireCapableBeanFactory.autowireBean(wechatHandler);
    // 获取用户token
    AuthTokenVo authTokenVo = wechatHandler.fetchToken(code);
    // 根据token获取用户信息
    WechatUserVo wechatUserVo = wechatHandler.fetchUserInfo(authTokenVo);
    // 根据unionId判断用户是否存在
    UserMo userMo = userService.selectUserByUnionId(wechatUserVo.getUnionId());
    if (userMo == null) {
    // 登录失败
    response.sendRedirect(callBackUrl + "?code=500&token=" + wechatUserVo.getUnionId());
    }
    // 调用登录方法,写入cookie
    userService.login(userMo);
    // 登录成功
    response.sendRedirect(callBackUrl + "?code=200");
  }
}

// 调用微信接口获取微信信息处理类

public class WechatHandler {

   /**
     * 应用唯一标识
     */
    private String appId;

    /**
     * 应用密钥
     */
    private String secret;

    /**
     * 请求方法
     */
    @Resource
    RestTemplate restTemplate;

    public WechatHandler(String appId, String secret) {
        this.appId = appId;
        this.secret = secret;
    }

    private final static String BASE_URL = "https://api.weixin.qq.com";

    private final static String FETCH_TOKEN_GRANT_TYPE = "authorization_code";

    private final static String TOKEN_URL =
        BASE_URL + "/sns/oauth2/access_token?appid={appId}&secret={secret}&code={code}&grant_type={grantType}";

    private final static String USER_INFO_URL = BASE_URL + "/sns/userinfo?access_token={accessToken}&openid={openId}";

    @Override
    public AuthTokenVo fetchToken(AuthTokenForm authTokenForm) {
        Map<String, String> query = new HashMap<>();
        query.put("appId", getAppId());
        query.put("secret", getSecret());
        query.put("code", authTokenForm.getCode());
        query.put("grantType", FETCH_TOKEN_GRANT_TYPE);
        String result = restTemplate.getForObject(TOKEN_URL, String.class, query);
        JSONObject jsonObject = JSON.parseObject(result);
        if (jsonObject.getInteger("errcode") != null) {
            throw new BusinessException("获取TOKEN失败");
        }
        AuthTokenVo authTokenVo = new AuthTokenVo(jsonObject);
        return authTokenVo;
    }

    @Override
    public AuthTokenVo refreshToken(AuthRefreshTokenForm authRefreshTokenForm) {
        return null;
    }

    @Override
    public Object fetchUserInfo(AuthTokenVo authTokenVo) {
        Map<String, String> query = new HashMap<>();
        query.put("accessToken", authTokenVo.getAccessToken());
        query.put("openId", authTokenVo.getOpenId());
        String result = restTemplate.getForObject(USER_INFO_URL, String.class, query);
        JSONObject jsonObject = JSON.parseObject(result);
        if (jsonObject.getInteger("errcode") != null) {
            throw new BusinessException("获取用户信息失败");
        }
        WechatUserVo wechatUserVo = new WechatUserVo(jsonObject);
        return wechatUserVo;
    }

}

微信用户实体类

@Data
@NoArgsConstructor
public class WechatUserVo {

    /**
     * OPEN_ID
     */
    private String openId;

    /**
     * 唯一标识
     */
    private String unionId;

    /**
     * 昵称
     */
    private String nickName;

    /**
     * 性别
     */
    private Integer sex;

    /**
     * 省份
     */
    private String province;

    /**
     * 城市
     */
    private String city;

    /**
     * 国家
     */
    private String country;

    /**
     * 头像
     */
    private String headImgUrl;

    /**
     * 特权信息
     */
    private String privilege;

    public WechatUserVo(JSONObject jsonObject) {
        this.unionId = jsonObject.getString("unionid");
        this.openId = jsonObject.getString("openid");
        this.nickName = jsonObject.getString("nickname");
        this.sex = jsonObject.getInteger("sex");
        this.province = jsonObject.getString("province");
        this.city = jsonObject.getString("city");
        this.country = jsonObject.getString("country");
        this.headImgUrl = jsonObject.getString("headimgurl");
        this.privilege = jsonObject.getString("privilege");
    }
}

认证token实体类

@Data
@NoArgsConstructor
public class AuthTokenVo {

    /**
     * 用户接口调用凭证
     */
    private String accessToken;

    /**
     * token过期时间
     */
    private String expiresIn;

    /**
     * 刷新token凭证
     */
    private String refreshToken;

    /**
     * 用户标识
     */
    private String openId;

    /**
     * 授权作用域
     */
    private String scope;

    public AuthTokenVo(JSONObject jsonObject) {
        this.accessToken = jsonObject.getString("access_token");
        this.expiresIn = jsonObject.getString("expires_in");
        this.refreshToken = jsonObject.getString("refresh_token");
        this.openId = jsonObject.getString("openid");
        this.scope = jsonObject.getString("scope");
    }
}

微信token请求表单类

@Data
public class AuthTokenForm {

    /**
     * 用户CODE
     */
    private String code;

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354