微信小程序登录流程包含前端后端代码

目录

微信小程序登录流程(包含前端、后端代码)


https://i-blog.csdnimg.cn/blog_migrate/1de458c1f416ee1d3c84f4af62cd9409.png

在微信小程序的开发过程中,如果想要保留 用户数据 (比如: 操作记录购物车信息 等等)就必须要 用户 登陆。为什么呢?比如说,数据库中有一条 数据 你如何知道这条数据属于谁?属于那个用户呢?这就需要用户登录来获取 用户唯一标识 从而确定这条数据是属于哪个用户的,那么如何做微信小程序的登陆功能呢?让我们使用 Springboot 框架+ AOP 一起来学习吧!


微信小程序登录 流程:

https://i-blog.csdnimg.cn/blog_migrate/108ef52978663e4a6d8d14e15d3d680a.png

开发者服务器 处理流程:

https://i-blog.csdnimg.cn/blog_migrate/c2a170ea841b431f419d240703ee0438.png

通过 wx.login 来获取临时登录 code

wx.login({ success (res) { if (res.code) { //发起网络请求 wx.request({ url: 'https://example.com/onLogin', data: { code: res.code } }) } else { console.log('登录失败!' + res.errMsg) } } })

在注册 微信开发者账 后,可以在 微信小程序管理后台 获取 appidhttps://i-blog.csdnimg.cn/blog_migrate/f4c76ac080af1c19e6eb7f8bad2867ec.png

小程序密钥同样是在注册微信开发者平台账号后,在管理后台获取的: https://i-blog.csdnimg.cn/blog_migrate/5cc1b7c6db52bcca0677ff71fa4af6a1.png 由于微信小程序密钥不以明文的方式展示,如果忘记了, 重置 下就可以了。

拿着 微信codeappidappsecret开发者服务器 去请求 微信接口服务 换取 openIdsecretKey (这里我们使用 ApiPost 工具来进行请求,当然 PostMan 工具也行): https://i-blog.csdnimg.cn/blog_migrate/1ca97e6a3045fa533ac0ad059121a60e.png

调用 微信接口服务 接口(注意是 Get 请求):

javascript https://api.weixin.qq.com/sns/jscode2session?

java { "session_key": "xxxxx", "openid": "xxxxx" }

拿到返回值后,应该 入库 ,保存一下。 数据库结构如下: https://i-blog.csdnimg.cn/blog_migrate/14685e78ed6d5be19254abcb4d2414e0.png 等下次该用户登录时,走完 1.4 流程后,可以根据返回值中的 openid 在我们库中找到该用户,然后进行后续的操作。

所谓 token 就是用来确认用户的身份证,拿到下面的返回值后,我们有下面两种方式生成 自定义token

java { "session_key": "xxxxx" }

使用 md5 加密工具来生成 token ,工具类如下:

import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.symmetric.AES;

import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets;

public class AESUtil {

/**

- 加密密钥
  */
  private static final String ENCODE*KEY = "test_key_secret*";
  /**
- 偏移量
  */
  private static final String IV_KEY = "0000000000000000";

public static String encryptFromString(String data, Mode mode, Padding padding) {
AES aes;
if (Mode.CBC == mode) {
aes = new AES(mode, padding,
new SecretKeySpec(ENCODE_KEY.getBytes(), "AES"),
new IvParameterSpec(IV_KEY.getBytes()));
} else {
aes = new AES(mode, padding,
new SecretKeySpec(ENCODE_KEY.getBytes(), "AES"));
}
return aes.encryptBase64(data, StandardCharsets.UTF_8);
}

public static String decryptFromString(String data, Mode mode, Padding padding) {
AES aes;
if (Mode.CBC == mode) {
aes = new AES(mode, padding,
new SecretKeySpec(ENCODE_KEY.getBytes(), "AES"),
new IvParameterSpec(IV_KEY.getBytes()));
} else {
aes = new AES(mode, padding,
new SecretKeySpec(ENCODE_KEY.getBytes(), "AES"));
}
byte[] decryptDataBase64 = aes.decrypt(data);
return new String(decryptDataBase64, StandardCharsets.UTF_8);
}
}

注意: ENCODE_KEY 加密密钥不是固定的可以自己设置,但是!!! ENCODE_KEYIV_KEY 偏移量的字符 数量 一定要保持一致!!!否者解密失败!!!

测试:

String encryptData = AESUtil.encryptFromString("test123456..", Mode.CBC, Padding.ZeroPadding); System.out.println("加密:" + encryptData); String decryptData = AESUtil.decryptFromString(encryptData, Mode.CBC, Padding.ZeroPadding); System.out.println("解密:" + decryptData);

结果:

java 加密:UYKwmVTh39qvwHsQ+tkFow== 解密:test123456..

之所以放入 Redis 是因为它可以设置过期时间,可以实现 token 过期重新登录的功能。比如:如果接收到 微信小程序 请求所携带的 token 后先去 Redis 查询是否 存在 ,如果 不存 在则判定过期,直接返回让再次用户登录。

@Autowired private RedisTemplate redisTemplate; .... //微信用户的唯一标识 private String userId= 'xxxxx' // token 放入 redis 并设置 3 天过期 redisTemplate.opsForValue().set(userId,JSONObject.toJSONString(userInfo),3, TimeUnit.DAYS);

token 放到返回体中返回给微信端。

java ... return returnSuccess(token);

开发者服务器 返回给微信小程序结果后,将 token 放入到本地存储。

javascript ... //将token放到本地 wx.setStorageSync('token', result.sessionKey) ...

开发者服务器 发起请求时,在 header 中带上 token

... 
wx.request({ url: 'https://xxxx.com/api/method', header:{"token":wx.getStorageSync('token')}, success:function(res){}, fail:function(res){} })
...

开发者服务器 在接收到微信端发起的业务请求时,通过 AOP 进行拦截获取 header 中的 token

使用 SpringAOP 来拦截请求获取 token

java //获取token String token = request.getHeader("token"); log.info("token:{}",token);

...
String token = 'xxxx'; log.info("解密前:{}",decryptData); String decryptData = AESUtil.decryptFromString(token, Mode.CBC, Padding.ZeroPadding); log.info("解密结果:{}",decryptData); //拿到用户ID String userId = decryptData; 
...
@Autowired private RedisTemplate redisTemplate;
 ... 
//用户ID String userId = decryptData ValueOperations valueOperations = redisTemplate.opsForValue(); String userInfoRedis = (String)valueOperations.get(userId); 
...
wx.login({ success(res){ if(res.code){ wx.request({ url:'https://xxxx.com/login/wxLogin', method:"POST", data:{"code":res.code} , dataType:"json", success:function(res){ result = res.data.result wx.setStorageSync('token', result.token) //页面跳转 ... }, fail:function(res){}, }) } } })
wx.request({ url: "https://xxxx.com/test/test", method: "GET", dataType:"json", data:{}, //在heard中戴上token header:{"token":wx.getStorageSync('token')}, success:function(res){ ... }, fail:function(res){} });

后端使用的 Java 语言,框架是 Springboot + AOP 实现。 目录结构如下: https://i-blog.csdnimg.cn/blog_migrate/db5945102f9d7f43be62d35c2b67359f.png yml 配置文件: https://i-blog.csdnimg.cn/blog_migrate/ce46788f7898b34ec540f68ef62420c6.png

org.springframework.boot
spring-boot-starter-web
2.1.2.RELEASE

org.springframework.boot
spring-boot-starter
2.3.7.RELEASE

org.projectlombok
lombok
1.16.16

org.slf4j
slf4j-api
1.7.30

cn.hutool
hutool-all
5.6.3

org.springframework.boot
spring-boot-starter-aop
3.0.4
import lombok.extern.slf4j.Slf4j; 
import org.aspectj.lang.ProceedingJoinPoint; 
import org.aspectj.lang.annotation.After; 
import org.aspectj.lang.annotation.Around; 
import org.aspectj.lang.annotation.Aspect; 
import org.aspectj.lang.annotation.Pointcut; 
import org.springframework.beans.BeanUtils; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.stereotype.Component; 
import org.springframework.util.StringUtils; 
import javax.servlet.http.HttpServletRequest;

@Aspect @Component @Slf4j 
public class TestAspect { 
@Autowired 
private HttpServletRequest request;
@Pointcut("execution(_ xx.xxx.controller._._(..)) && !execution(_ xx.xxx.controller.WxLogin.*(..)")
public void pointCut(){}
@Around(value = "pointCut()")
public Object Around(ProceedingJoinPoint joinPoint) throws Throwable {
    //获取 token
    String token = request.getHeader("token");
    log.info("token:{}",token);
    //不存在 token 直接抛出异常
    if(StringUtils.isEmpty(token)){
        throw new AopException();
    }
    //解析 token
    String userId = AESUtil.decryptFromString(token, Mode.CBC, Padding.ZeroPadding);
    log.info("解析 token:{}",userId);
    //将 token 放入到 Base 基础类
    Base base = new Base();
    base.setUserId(userId);
    //放到 Base 中
    final Object[] args = joinPoint.getArgs();
    for (Object arg : args) {
        if(arg instanceof Base){
            BeanUtils.copyProperties(base, arg);
            }
        }
        //放到 ThreadLocal 中   
        User user = new User();
        user.setUserId(userId);
        UserContent.setUserContext(user);
        return joinPoint.proceed();
    }

    @After(value = "pointCut()")
    public void controllerAfter() throws Throwable {
        log.info("后置通知");
        log.info("移除 ThreadLocal 中的用户信息:{}",UserContent.getUserContext());
        UserContent.removeUserContext();
    }

}

从上面代码中我们可以看到。我们通过解密可以拿到 UserId ,这个值我们是频繁使用的,那么如何做到 随用随取 呢?

  • 第一种方式:使用 Base 基础类,然后让 Controller 需要传递参数的 DTO 都继承 Base 然后就可以随时使用 UserId 了。
  • 第二种方式:使用 ThreadLocal ,这种是比上一种优雅一些,也可以完全做到随用随取。但是需要注意在 会话 结束后一定要移除 ThreadLocal 中的用户信息,否则会导致内存溢出(这很重要),一般使用 切面 的后置通知来做这件事情。

execution(* xx.xx.controller.*.*(..)) 解释:在方法执行时,xx.xx.controller 包下的所有 下面的所有带有任何参数的 方法 都需要走这个切面。

@PointCut 注解值的规则:

  • execution :方法执行时触发。
  • 第一个 * :返回任意类型。
  • xx.xx.controller :具体的报路径。
  • 第二个 * :任意类。
  • 第三个 * :任意方法。
  • (..) :任意参数。

如果想要排除 xxController 类可以这样写: @Pointcut(“execution(* xx.xxx.xxxx.controller. . (..)) " + “&& !execution(* xx.xxx.xxxx.controller.xxController.*(..))”) 比如 登陆的时候就需要 放行 登陆的接口。

public class AopException extends Exception { public AopException() { super("登录超时,请重新登录"); } }

登陆 Controller 代码:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/login") public class WxLogin {

    @Autowired
    private IWxLoginService iWxLoginService;

    @PostMapping("/wxLogin")
    public Response wxLogin(@RequestBody WxLoginRequestDto requestDto){
        WxLoginResponseDto wxLoginResponseDto = iWxLoginService.wxLogin(requestDto);
        return returnSuccess(wxLoginResponseDto);
    }
}

业务逻辑 Controller 代码:

import cn.trueland.model.Base;
import cn.trueland.model.UserContent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController @RequestMapping("/test")
 public class TestController {
    @GetMapping("/test")
    public String test(Base base){
     return base.getUserId();
      }
    @GetMapping("/test2")
    public String test2(){
    return UserContent.getUserContext().getUserId();
    }

}

这里我只帖登陆的 Service 层代码,业务的没有必要。

public String wxLogin(WxLoginRequestDto requestDto) { 
    if(StringUtils.isBlank(requestDto.getCode())){
     throw new BusinessException("code为空!"); 
     } //获取微信服务接口地址 
    String authCode2Session = wxConfig.getAuthCode2Session(requestDto.getCode()); //请求微信服务接口获取 openId 
    String result = HttpClientUtil.doGet(authCode2Session); 
    String openId = JSONObject.parseObject(result).getString("openid");
    String sessionKey = JSONObject.parseObject(result).getString("session_key"); 
 //入库 并返回 userId (逻辑省略) String userId = ...; 
 //将用户信息存入redis 
 redisTemplate.opsForValue().set(userId,userId ,3, TimeUnit.DAYS);
  String token = AESUtil.encryptFromString(userId, Mode.CBC, Padding.ZeroPadding); 
  return token; 
  }

登录请求 DTO :

java import lombok.Data; @Data public class WxLoginRequestDto { /** * code */ private String code; }

基础类 Base :

import lombok.Data;

@Data public class Base { private String userId; }

用户实体类 User`:

import lombok.Data;

@Data public class User { private String userId; }

用户信息实体 UserContent`:

public class UserContent { private static final ThreadLocal
userInfo = new ThreadLocal();

public static User getUserContext(){
return userInfo.get();
}

public static void setUserContext(User userContext){
userInfo.set(userContext);
}

public static void removeUserContext(){
userInfo.remove();
}
}
import lombok.Data; 
import org.springframework.boot.context.properties.ConfigurationProperties; 
import org.springframework.stereotype.Component;

@Data @Component @ConfigurationProperties(prefix = "wx") public class WxConfig { /
** * 小程序AppId */ private String appId; /*
* * 小程序密钥
*/ private String appSecret; /*
* * 授权类型
*/ private String grantType; /*
* * auth.code2Session 的 url */ private String authCodeSessionUrl; } 
wx: 
    app-id: xxxx 
    app-secret: xxxx 
    auth-code-session-url: https://api.weixin.qq.com/sns/jscode2session? 
    grant-type: authorization_code

https://i-blog.csdnimg.cn/blog_migrate/d7b4aaae356e0d6e3b50c06bc53f8b71.png

https://i-blog.csdnimg.cn/blog_migrate/c39bc28d7d03e532bf08e54f52955671.png 都可以拿到 UserId 并返回。

下面就可以开心的处理业务逻辑啦!!!