查看原文
其他

三周学会小程序第五讲:登录的原理和实现

码匠笔记 码匠笔记 2020-08-21

登录原理

前面我们耗费在环境搭建上面已经很多时间,这一讲开始真正的和小程序功能对接。
登录便是小程序的开始,小程序可以方便的使用微信登录,获取用户的个人信息,这样我们就能保留用户的信息和记录用户的操作。我们直接通过一张图进入正题。如图是小程序官方给出的登录过程:

1,调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。 调用 code2Session 接口,换取 用户唯一标识 OpenID 和 会话密钥(sessionkey),为了安全所以需要使用小程序先获取 code 然后再传递到服务器端获取登录信息进行登录,所以到这里你就理解了为什么上一讲《三周学会小程序第三讲:服务端搭建和免费部署》要搭建服务器了。
2,session
key。微信为登录用户设置的登录session,用户校验登录态和下文中我们用于校验用户信息的正确性。
文档地址
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

因为我们不仅仅需要有登录态,还需要获取用户信息保存到服务器端 所以我们需要获取用户信息,目前我们获取用户信息需要在小程序端使用 button 组件,并将 open-type 指定为 getUserInfo 类型,点击 button 的时候通过 bindgetuserinfo属性 绑定的 callback 接收用户信息。通过上述方式我们可以获取到 rawDatasignaturerawData 是用户信息

  1. {

  2.  "nickName": "小匠",

  3.  "gender": 1,

  4.  "language": "zh_CN",

  5.  "city": "北京",

  6.  "province": "北京",

  7.  "country": "CN",

  8.  "avatarUrl": "http://wx.qlogo.cn/mmopen/vi_32/1vZvI39NWFQ9XM4LtQpFrQJ1xlgZxx3w7bQxKARol6503Iuswjjn6nIGBiaycAjAtpujxyzYsrztuuICqIM5ibXQ/0"

  9. }

因为   

signature=sha1(rawData+session_key)

所以我们把这两个属性直接传递到服务器端,不仅获取到了用户信息,用于存储到服务器端,还能校验请求数据的真实性。
文档地址
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html

所以这样以后,小编对流程图进行了优化,这样可以减少一次服务端的请求,提高响应速度。

1, 简单解释一下,首先在小程序端通过调用 wx.logingetUserInfo 获取code 和 用户信息,一起通过 wx.request 发送给开发者服务器端,这样便减少一些请求。
2,调用 jscode2session 接口到微信接口服务获取 session_keyopenId,直接对用户信息进行 SHA1 校验,校验成功以后创建自定义登录态,返回自定义登录态到小程序。
3,自定义登录态是什么呢?做过web的朋友都知道 session 和 cookies 可以组合做登录态,我们这里也是模拟这种设计,只是我们会把session存到数据库中,自定义生成 cookies(token) 传递给小程序端,存储到 storage 里面。因为本讲内容较多,所以把数据库存储放到了下一讲。
4, storage 类似于浏览器的 localStorage,用户小程序端方便的存储一些基础数据。


小程序端逻辑实现

现在我们已经理解了怎么做登录验证,那接下来我们开始编写客户端代码。 首先我们需要在 onLoad 方法里面调用 wx.login() 方法, onLoad 方法在页面加载的是就会调用,当页面加载的时候我们就把 code 存入 data 里面,以便后面调用服务端接口的时候使用。因为每次调用 wx.login 对应的 session_key 就会变,所以必须保证 wx.login 的调用 在获取用户信息之前。这时候我们每次刷新页面都会在控制台看到如下输出(我这里使用的是 Command + S 每次会重新编译小程序)

然后我们需要在 question/index.wxml 文件中的按钮上面添加 open-typebindgetuserinfo,如下

  1. <button class="btn" open-type="getUserInfo"

  2. bindgetuserinfo="getUserInfo" hover-class="btn-click">

  3. 登录

  4. </button>

接下来在 question/index.js 里面添加 getUserInfo 方法用于接收点击按钮获取用户信息的回调,回调方法里面的 userInfo 就是我们想要的用户信息。我们再模拟器里面点击按钮,在控制台里面查看具体的信息,具体模拟器和控制台怎么用,我们在《客户端代码准备和基础功能讲解》中已经有讲解。

  1. // 绑定wxml的button,用户获取用户信息

  2. getUserInfo: function (userInfo) {

  3.    console.log(userInfo)

  4. }

大家应该都知道 JSON 这种数据传输格式,我们下面就使用 wx.request 发送用户信息和 code 到服务器端, wx.request 对于初学者你们可以理解为 ajax。按照小编的接口定义,我只要发送 signature, rawDatacode 到服务器端,然后接收数据。所以我的实现如下。

  1. wx.request({

  2.  url: config.serverHost + '/api/login',

  3.  method: "post",

  4.  data: JSON.stringify({

  5.    code: this.data.code,

  6.    rawData: userInfo.detail.rawData,

  7.    signature: userInfo.detail.signature

  8.  }),

  9.  dataType: "json",

  10.  success: response => {

  11.    wx.hideLoading();

  12.    console.log(response);

  13.    if (response.data.status == 200) {

  14.      // 展示 登录成功 提示框

  15.      wx.showToast({

  16.        title: '登录成功',

  17.        icon: "none",

  18.        duration: 1000

  19.      });

  20.      // 把自定义登录状态 token 缓存到小程序端

  21.      wx.setStorage({

  22.        key: "token",

  23.        data: response.data.data.token

  24.      });

  25.    } else {

  26.      // 展示 错误信息

  27.      wx.showToast({

  28.        title: response.data.message,

  29.        icon: "none",

  30.        duration: 1000

  31.      });

  32.    }

  33.  },

  34.  fail: response => {

  35.    console.log(response)

  36.    wx.showToast({

  37.      title: '登录失败,请重试'

  38.    });

  39.  }

  40. })

切记这段代码块要放在 wx.login 成功以后
url 就是请求的地址, config.serverHost是我封装的常量,以便后面修改地址。
method 是请求类型。
data 是请求的 JSON 请求体。
dataType 是定义的请求类型。
successfail 分别是成功和失败的回调,我们会根据返回的 response 做相应的处理。wx.request 会把我返回的内容再包裹一个 data,所以如上内容登录成功和失败我实际返回的 JSON 如下。

  1. {

  2.    "status": 200,

  3.    "message": "登录成功",

  4.    "data": {

  5.        "token": "86247a92-38d9-4908-a024-07be468c2c76"

  6.    }

  7. }


  1. {

  2.    "status": 400,

  3.    "message": "无效登录"

  4. }


wx.showToast 是提示框,所以当成功以后提示成功,失败以后把message获取到提示给用户。

最后代码中你会看到

  1. wx.showLoading({

  2.  title: "登录中",

  3.  mask: true

  4. });

  1. wx.hideLoading();

是为了优化一下体验,当点击登录的时候提示登录中,等待服务端返回数据的时候消失,到此本节小程序部分已经结束。

小程序源码地址
https://github.com/codedrinker/jiuask
本讲 Tag V5


服务端逻辑实现

上面客户端已经完成,开始开发服务端。 首先我们需要定义一个 API 入口 Controller,用于定义 api/login 接口接收小程序发过来的请求。如下:

  1. @RestController

  2. @Slf4j

  3. public class LoginController {

  4.    // Spring 自动注入 wechatAdapter,因 WechatAdapter 类上面有 @Service 注解

  5.    @Autowired

  6.    private WechatAdapter wechatAdapter;

  7.    // 定义 domain/api/login 访问接口,用于实现登录

  8.    // 使用 LoginDTO 自动解析传递过来的 JSON 数据

  9.    @RequestMapping("/api/login")

  10.    public ResultDTO login(@RequestBody LoginDTO loginDTO) {

  11.        try {

  12.            log.info("login request : {}", loginDTO);

  13.            // 使用 code 调用微信 API 获取 session_key 和 openid

  14.            SessionDTO sessionDTO = wechatAdapter.jscode2session(loginDTO.getCode());

  15.            log.info("login get session : {}", sessionDTO);

  16.            // 检验传递过来的使用户信息是否合法

  17.            DigestUtil.checkDigest(loginDTO.getRawData(), sessionDTO.getSessionKey(), loginDTO.getSignature());

  18.            //TODO: 储存 token

  19.            //生成token,用于自定义登录态,这里的存储逻辑比较复杂,放到下一讲

  20.            TokenDTO data = new TokenDTO();

  21.            data.setToken(UUID.randomUUID().toString());

  22.            return ResultDTO.ok(data);

  23.        } catch (ErrorCodeException e) {

  24.            log.error("login error, info : {}", loginDTO, e.getMessage());

  25.            return ResultDTO.fail(e);

  26.        } catch (Exception e) {

  27.            log.error("login error, info : {}", loginDTO, e);

  28.            return ResultDTO.fail(CommonErrorCode.UNKOWN_ERROR);

  29.        }

  30.    }

  31. }

根据如上代码,我们找一些关键点讲解一下
1,RestController,定义当前 Controller 为 Restful,最简单的理解就是 @RestController = @ResponseBody + @Controller
2,@Slf4j,Lombok注解,需要在 Idea 插件中安装 Lombok,这样在需要使用日志的时候,直接使用 log.*()就可以了。
3,@RequestBody LoginDTO loginDTO,会把传递过来的 JSON 对象自动序列化成对象。
4,ErrorCodeException 自定义 RuntimeException,根据业务的异常友好的提示到小程序端,具体使用interface和enum实现,源码可以参考
com.codedrinker.error 包下面的类。

对于返回对象的统一封装,便于小程序端接收和处理。

  1. @Data

  2. public class ResultDTO {

  3.    private Integer status;

  4.    private Object data;

  5.    private String message;

  6. }

status 是状态码,如果不是200,都是异常。
data 是数据,可以是多种类型。
message 返回的错误信息。
@Data 是lombok 的注解,可以自动生成 set get 方法,省去了自己编写 set get 的麻烦。

下面是对调用微信的 API 进行封装

  1. @Service

  2. public class WechatAdapter {

  3.    private final Logger logger = LoggerFactory.getLogger(WechatAdapter.class);

  4.    @Value("${wechat.appid}")

  5.    private String appid;

  6.    @Value("${wechat.secret}")

  7.    private String secret;

  8.    public SessionDTO jscode2session(String code) {

  9.        String url = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";

  10.        OkHttpClient okHttpClient = new OkHttpClient();

  11.        Request request = new Request.Builder()

  12.                .addHeader("content-type", "application/json")

  13.                .url(String.format(url, appid, secret, code))

  14.                .build();

  15.        try {

  16.            Response execute = okHttpClient.newCall(request).execute();

  17.            if (execute.isSuccessful()) {

  18.                SessionDTO sessionDTO = JSON.parseObject(execute.body().string(), SessionDTO.class);

  19.                logger.info("jscode2session get url -> {}, info -> {}", String.format(url, appid, secret, code), JSON.toJSONString(sessionDTO));

  20.                return sessionDTO;

  21.            } else {

  22.                logger.error("jscode2session authorize error -> {}", code);

  23.                throw new ErrorCodeException(CommonErrorCode.OBTAIN_OPENID_ERROR);

  24.            }

  25.        } catch (IOException e) {

  26.            logger.error("jscode2session authorize error -> {}", code, e);

  27.            throw new ErrorCodeException(CommonErrorCode.OBTAIN_OPENID_ERROR);

  28.        }

  29.    }

  30. }

简单粗暴的讲解一下 Adapter 里面的关键点。
1,@Value("${wechat.appid}") 是 Spring 的自动注解,直接读取 application-*.yml 里面的配置赋值给appid变量。这时候我们的
application-production.yml 里面的配置如下。

  1. wechat:

  2.  appid: ${WECHAT_APPID}

  3.  secret: ${WECHAT_SECRET}

appid和secret是在小程序控制台->开发设置->开发者ID里面获取,为了安全,我们不能把这些信息直接提交到代码里面,所以我选择了把它们配置到 Heroku 的环境变量里面,在部署的时候会自动的替换掉 application-production.yml 里面的 ${WECHAT_APPID} 占位符,然后通过 @Value 赋值到 @Service里面的变量 appid。具体配置方式如下,还需要先进入 Heroku 的控制台,然后点击Settings 进行配置,记住左边是 application-production.yml 里面的占位符,右边是你小程序的 id 和 secret。

2,OkHttpClient,是使用的 OKHttp,小编觉得这个用起来比 Apache 的 HttpClient 要简单,代码就不需要讲解了,继续要因为一个 pom.xml 文件。

  1. <!--调用 HTTP 请求-->

  2. <dependency>

  3.    <groupId>com.squareup.okhttp3</groupId>

  4.    <artifactId>okhttp</artifactId>

  5.    <version>3.8.1</version>

  6. </dependency>

3,JSON.parseObject 直接解析String到对象。
4,throw new ErrorCodeException(CommonErrorCode.OBTAINOPENIDERROR);
直接返回业务的ErrorCode,

对了,忘记说最重要的一个问题,这次改动需要添加4个pom.xml

  1. <!--自动生成 set get 方法-->

  2. <dependency>

  3.    <groupId>org.projectlombok</groupId>

  4.    <artifactId>lombok</artifactId>

  5.    <version>1.12.4</version>

  6. </dependency>

  7. <!--调用 HTTP 请求-->

  8. <dependency>

  9.    <groupId>com.squareup.okhttp3</groupId>

  10.    <artifactId>okhttp</artifactId>

  11.    <version>3.8.1</version>

  12. </dependency>

  13. <!--解析 JSON 工具-->

  14. <dependency>

  15.    <groupId>com.alibaba</groupId>

  16.    <artifactId>fastjson</artifactId>

  17.    <version>1.2.33</version>

  18. </dependency>

  19. <!--SHA1 加密工具包-->

  20. <dependency>

  21.    <groupId>commons-codec</groupId>

  22.    <artifactId>commons-codec</artifactId>

  23.    <version>1.11</version>

  24. </dependency>

已经在原文中添加注释。
服务端源码地址
https://github.com/codedrinker/jiuask-server
本讲 Tag V5


我是浪漫的分割线


问答

如果您对本系列文章有兴趣,欢迎置顶本订阅号,第一时间获取更新。

如果有任何问题,欢迎留言,小编很热衷和大家一起讨论技术问题。

另外小编创建了一个技术交流群,请添加小编微信,切记备注“小程序”,小编拉你进去。【只讨论技术,非诚勿扰】

最好的赞赏是关注和分享

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存