微信扫码登陆验证在Go系统开发时的应用与实践

微信扫码登录实现

登录流程

总体来说,就是三步:

  • 点击微信登录,跳转到微信页面
  • 微信扫码登录,确认登陆
  • 微信跳转回来

这里,我们就得,明确两个问题:

  1. 跳到微信界面,跳过去的 URL 是什么?
  2. 跳转回来的 URL 是什么?
    这些我们就得从微信给我们提供的 API 出发了。

微信登录 API

微信扫码登录其实是一个 OAuth2 授权过程。简单的说,即使你作为用户授权第三方应用获得了对应的access_token, 第三方应用就认为你登录了。

在大多数场景下,第一次登录的时候还会尝试回去用户的信息。


从上面的图可以看出来,我们需要构建一个 URL,里面要携带上一些参数:

  • appid:在微信开放平台注册的 ID
  • redirect_uri:微信扫码登录后跳转回来的地址
  • response_type:固定为 code
  • scope:应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login
  • state:用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验

用户允许授权后,将会重定向到redirect_uri的网址上,并且带上code和state参数。

redirect_uri?code=CODE&state=STATE

如果用户禁止授权,则会阻止重定向。

从微信跳转回来后,会携带上一个 code,我们要用这个 code 去微信里面换取一个access_token。也是一个发起调用的过程,这里就需要传入appidsecret

要点总结

要想实现微信登录,必须经过两次跳转:

  • 点击微信登录,跳转到微信页面。跳转过去的地址是微信的扫码地址,根据要求必须携带上redirect_uri,appidstate三个属性
  • 微信扫码登录,确认登陆,从微信跳转回来。跳转回来的地址就是redirect_uri的地址。微信此时会携带上临时的授权码code
  • 后台处理redirect_uri中带过来的 code,找微信换取真正的长时间有效的授权码

设计过程

接口抽象


经过前面的分析,我们应该明确,需要两个接口:

第一个接口:用于构建跳转到微信的 URL

第二个接口:处理微信跳转回来的请求

func (h *OAuth2WeChatHandler) RegisterRoutes(r *gin.Engine) {
	we := r.Group("/oauth2/wechat")
	we.GET("/authurl", h.AuthURL)
	we.Any("/callback", h.Callback)
}

构造URL

构造URL,实质上是拼接字符串的过程,我们需要定义一个 wechat.Service,并且提供实现。

再实现的过程中,我们使用 shortuuid来代替 state字短,因为我们还不知道state现在有什么用途。

var redirectUrl = url.PathEscape("https://test.com/oauth2/wechat/callback")

func (ws *service) AuthURL(ctx context.Context) (string, error) {
	const urlPattern = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=%s#wechat_redirect"
	state := uuid.New()
	return fmt.Sprintf(urlPattern, ws.appId, redirectUrl, state), nil
}

用户 Handler 的实现。

func (h *OAuth2WeChatHandler) AuthURL(ctx *gin.Context) {
	url, err := h.svc.AuthURL(ctx)
	if err != nil {
		ctx.JSON(http.StatusOK, Result{
			Code: 5,
			Msg:  "get auth url failed",
		})
	}
	ctx.JSON(http.StatusOK, Result{
		Code: 2,
		Msg:  url,
	})
}

现在扫码之后,我们就可以跳转回需要登陆的页面了。接下来,就考虑怎么进行验证 code

从微信回调回来的 code 是一个临时的授权码,所以还需要调用微信的接口,获得真正的授权码。(本质上也是调用一个 API)

func (ws *service) VerifyCode(ctx context.Context, code string, state string) (domain.WechatInfo, error) {
	const targetPattern = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
	target := fmt.Sprintf(targetPattern, ws.appId, ws.appSecret, code)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
	if err != nil {
		return domain.WechatInfo{}, err
	}
	resp, err := ws.client.Do(req)
	if err != nil {
		return domain.WechatInfo{}, err
	}
	decoder := json.NewDecoder(resp.Body)

	var result Result
	err = decoder.Decode(&result)
	if err != nil {
		return domain.WechatInfo{}, err
	}
	if result.ErrCode != 0 {
		return domain.WechatInfo{}, fmt.Errorf("wechat auth error: %s", result.ErrMsg)
	}
	return domain.WechatInfo{
		OpenId:  result.OpenID,
		UnionId: result.UnionID,
	}, nil
}

返回字段

当微信校验通过之后,我们会拿到一个 Result 结构,具体这个结构体都有哪些字段呢?

关键的两部分为:授权码和 ID。

  • 授权码部分:
    • access_token:后面我们可以拿着 access_token 去访问微信,获取用户的数据
    • expires_in:access_token 的有效期
    • refresh_token: 当access_token过期之后,我们可以拿着refresh_token去找微信换一个新的access_token

注意,使用 access_tokenrefresh_token是一个典型的长短token实现。
access_token是短token,refresh_token是长token。

  • ID 部分:
    • open_id: 在这个应用下为一的 ID
    • union_id: 在这个公司下唯一的 ID

假设你的公司在微信公众平台上注册了两个产品:A 和 B。
对于某一个用户张三来说,他在A上有一个open_id,在B上也有一个open_id
但是张三在你们公司的A和B两个产品的union_id都是同一个。

type Result struct {
	ErrCode int64  `json:"errcode"`
	ErrMsg  string `json:"errmsg"`

	AccessToken  string `json:"access_token"`
	ExpiresIn    int64  `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`

	OpenID  string `json:"openid"`
	Scope   string `json:"scope"`
	UnionID string `json:"unionid"`
}

优化登陆

这个类似于我们之前的短信登陆案例。

  • 如果用户第一次登陆,我们就注册一个新的账号
  • 如果用户不是第一次登陆,就直接设置一个 JWT token
    用户 Handler 接口的实现。
func (h *OAuth2WeChatHandler) Callback(ctx *gin.Context) {
	code := ctx.Query("code")
	state := ctx.Query("state")
	info, err := h.svc.VerifyCode(ctx, code, state)
	if err != nil {
		ctx.JSON(http.StatusOK, Result{
			Code: 5,
			Msg:  "verify code failed",
		})
	}
	u, err := h.userSvc.FindOrCreateByWechat(ctx, info)
	if err != nil {
		ctx.JSON(http.StatusOK, Result{
			Code: 5,
			Msg:  "system error",
		})
	}
	err = h.setJWTToken(ctx, u.Id)
	if err != nil {
		ctx.JSON(http.StatusOK, Result{
			Code: 5,
			Msg:  "set token failed",
		})
	}
	ctx.JSON(http.StatusOK, Result{
		Code: 2,
		Msg:  "success",
	})
}

实现 service 层上的函数。

func (svc *userService) FindOrCreateByWechat(ctx *gin.Context, info domain.WechatInfo) (domain.User, error) {
	u, err := svc.repo.FindByWechatOpenId(ctx, info.OpenId)
	if err != repository.ErrUserNotFound {
		return u, err
	}
	u = domain.User{
		WechatInfo: info,
	}
	err = svc.repo.Create(ctx, u)
	if err != nil || err == repository.ErrUserDuplicate {
		return u, err
	}
	return svc.repo.FindByWechatOpenId(ctx, info.OpenId)
}

实现 repository 层上的函数。

func (ur *CacheUserRepository) FindByWechatOpenId(ctx context.Context, OpenId string) (domain.User, error) {
	u, err := ur.dao.FindByWechatOpenId(ctx, OpenId)
	if err != nil {
		return domain.User{}, err
	}
	return ur.toDomainUser(u), nil
}

对于领域对象的设计。

type WechatInfo struct {
	OpenId  string `json:"openid"`
	UnionId string `json:"unionid"`
}

// User 领域对象,可以理解为 DDD 中的 entity
type User struct {
	Id       int64
	Email    string
	Password string
	//Ctime    time.Time

	NickName     string
	Birthday     time.Time
	Introduction string
	WechatInfo   WechatInfo
	Phone        string
}

预防 CROS 攻击

这里我们主要是想让大家理解如何使用 state

理解 state 的核心是抓住攻击者让你使用他的临时授权码来登录账号。

具体步骤为:

  • 攻击者首先会弄出来一个绑定微信的临时授权码。
  • 正常用户登录成功
  • 攻击者伪造一个页面,诱导用户点击,攻击者带着正常用户的Cookie(过或者JWT token)去请求,攻击者的临时授权码去绑定。

结果是什么呢?攻击者可以通过微信扫码登录成功,看到正常用户的数据信息。

那么我们如何解决这个问题呢?

整体思路就是:

  • 当生成 AuthURL 的时候,我们标识一下这一次的会话,将 state 和这一次的请求绑定在一起。
  • 等到回调回来的时候,我们看看回调中的 state 是不是我们生成时候用的 state


因为在整个系统中,我们使用的JWT来做身份验证的,所以这里依旧使用JWT。

使用JWT的好处是,直接用JWT里面的state和回调过来的state比较就可以了,不需要存储到 redis 中。

我们这里放在Cookie中,因为微信回来的时候直接经过后端,并未经过前端。

设置 State 到Cookie中。

func (h *OAuth2WeChatHandler) SetStateCookie(ctx *gin.Context, state string) error {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, StateClaims{
		State: state,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 10)),
		},
	})
	tokenStr, err := token.SignedString(h.stateKey)
	if err != nil {
		return err
	}
	ctx.SetCookie("jwt-state", tokenStr, 600, "/oauth2/wechat/callback", "",
		h.cfg.Secure, true)
	return nil
}

校验 State 字短。

整个校验的过程就是我们从 JWT 中拿到我们存储的 state,然后进行比较。

func (h *OAuth2WeChatHandler) VerifyState(ctx *gin.Context) error {
	state := ctx.Query("state")
	ck, err := ctx.Cookie("jwt-state")
	if err != nil {
		return fmt.Errorf("get cookie failed: %w", err)
	}

	var sc StateClaims
	token, err := jwt.ParseWithClaims(ck, &sc, func(token *jwt.Token) (interface{}, error) {
		return h.stateKey, nil
	})
	if err != nil || !token.Valid {
		return fmt.Errorf("verify state failed: %w", err)
	}
	if state != sc.State {
		return errors.New("state not match")
	}
	return nil
}

那么就有人会问了,你这还是会有 CORS 的问题呀。但是 Cookie泄漏是一件比较困难的事情,一般是你电脑本身中病毒了,单纯的跨域攻击是没办法拿到的。

这里,我们向大家讲述了如何进行微信扫码登录,并且介绍如何使用state字段。

绝大部分的公司都没有处理 state,所以你要解释清楚,什么情况下不处理state会造成跨域问题,以及如何解决的问题。

  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
开发微信扫码登录程序是一项非常有挑战性的任务。首先,我们需要了解微信扫码登录的工作原理。 微信扫码登录是一种基于OAuth 2.0协议的认证方式。下面是开发微信扫码登录程序的基本步骤: 1. 注册开发者账号:在微信开放平台注册一个开发者账号,并创建一个应用。 2. 配置开发环境:将微信提供的开发工具包集成到开发环境中。可以选择使用Java、Python等编程语言开发。 3. 获取授权地址:在后台配置应用的授权回调地址。用户扫码登录后,微信会将授权码返回到该地址。 4. 生成二维码:调用微信提供的API生成用户扫码登录所需的二维码。 5. 监听回调:在后台实现一个回调接口,监听微信回调的授权码。 6. 获取用户信息:通过授权码,调用微信提供的API获取用户的基本信息,如昵称、头像等。 7. 实现登录逻辑:将获取到的用户信息与本地系统用户进行关联,实现用户的登录逻辑。 开发微信扫码登录程序需要对微信开放平台的文档进行深入研究,并掌握相关的API调用所需的参数和格式。此外,需要具备网络编程、接口开发等相关的技能。 开发微信扫码登录程序对于企业来说有很多好处。首先,可以提供一种方便快捷的登录方式,避免用户需要记忆过多的账号和密码。其次,可以增加用户粘性,提高用户的黏性和活跃度。最后,可以帮助企业获取用户的基本信息,便于个性化推送和精准营销。 总结来说,开发微信扫码登录程序是一项复杂的任务,需要充分了解微信扫码登录的工作原理和开发流程。通过合理的开发和配置,可以提供一种便捷的登录方式,增加用户黏性,并为企业实现个性化推送和精准营销提供基础数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值