微信小程序登录流程包含前端后端代码
微信小程序登录流程(包含前端、后端代码)
theme: channing-cyan
一、前言
在微信小程序的开发过程中,如果想要保留
用户
的
数据
(比如:
操作记录
、
购物车信息
等等)就必须要
用户
登陆。为什么呢?比如说,数据库中有一条
数据
你如何知道这条数据属于谁?属于那个用户呢?这就需要用户登录来获取
用户
的
唯一标识
从而确定这条数据是属于哪个用户的,那么如何做微信小程序的登陆功能呢?让我们使用
Springboot
框架+
AOP
一起来学习吧!
二、流程
微信小程序登录
流程:
开发者服务器
处理流程:
1.1 获取用户 Code
通过
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) } } })
1.2 获取 appid
在注册
微信开发者账
后,可以在
微信小程序管理后台
获取
appid
:
1.3 获取 appsecret
小程序密钥同样是在注册微信开发者平台账号后,在管理后台获取的:
由于微信小程序密钥不以明文的方式展示,如果忘记了,
重置
下就可以了。
1.4 开发者服务向微信接口服务发起请求
拿着
微信code
、
appid
、
appsecret
在
开发者服务器
去请求
微信接口服务
换取
openId
和
secretKey
(这里我们使用 ApiPost 工具来进行请求,当然 PostMan 工具也行):
调用
微信接口服务
接口(注意是
Get
请求):
javascript https://api.weixin.qq.com/sns/jscode2session?
1.5 返回值
java { "session_key": "xxxxx", "openid": "xxxxx" }
拿到返回值后,应该
入库
,保存一下。 数据库结构如下:
等下次该用户登录时,走完
1.4
流程后,可以根据返回值中的
openid
在我们库中找到该用户,然后进行后续的操作。
1.6 自定义 token
所谓
token
就是用来确认用户的身份证,拿到下面的返回值后,我们有下面两种方式生成
自定义token
:
(1)使用 业务ID
生成 token
(推荐使用,后续的内容都是以用户 ID 作为例子的): 在这里插入图片描述
(2)使用 session_key
生成 token
:
java { "session_key": "xxxxx" }
(3)生成 token
的工具:
使用
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_KEY
和
IV_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..
(5)将生成好的 token
放入到 Redis
(不重要,可以省略)
之所以放入
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);
(6)返回 token
给微信小程序
将
token
放到返回体中返回给微信端。
java ... return returnSuccess(token);
1.7 将 token
放到本地
在
开发者服务器
返回给微信小程序结果后,将
token
放入到本地存储。
javascript ... //将token放到本地 wx.setStorageSync('token', result.sessionKey) ...
1.8 请求带上 token
向
开发者服务器
发起请求时,在
header
中带上
token
...
wx.request({ url: 'https://xxxx.com/api/method', header:{"token":wx.getStorageSync('token')}, success:function(res){}, fail:function(res){} })
...
1.9 开发者服务器验证 token
开发者服务器
在接收到微信端发起的业务请求时,通过
AOP
进行拦截获取
header
中的
token
:
(1) AOP
统一拦截:
使用
Spring
的
AOP
来拦截请求获取
token
。
java //获取token String token = request.getHeader("token"); log.info("token:{}",token);
(2)解密 token
...
String token = 'xxxx'; log.info("解密前:{}",decryptData); String decryptData = AESUtil.decryptFromString(token, Mode.CBC, Padding.ZeroPadding); log.info("解密结果:{}",decryptData); //拿到用户ID String userId = decryptData;
...
(3)验证是否过期(不重要,可以省略的步骤)
@Autowired private RedisTemplate redisTemplate;
...
//用户ID String userId = decryptData ValueOperations valueOperations = redisTemplate.opsForValue(); String userInfoRedis = (String)valueOperations.get(userId);
...
三、前后端完整代码
2.1 前端代码
(1)登陆
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){}, }) } } })
(2)发起业务请求
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){} });
2.2 后端代码
后端使用的
Java
语言,框架是
Springboot
+
AOP
实现。 目录结构如下:
yml
配置文件:
(1)依赖
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
(2)切面相关代码
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("登录超时,请重新登录"); } }
(3)控制层代码
登陆
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();
}
}
(4) Service
层代码:
这里我只帖登陆的
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;
}
(4)实体类相关代码
登录请求
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();
}
}
(5)配置类
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; }
(6) yml
配置信息
wx:
app-id: xxxx
app-secret: xxxx
auth-code-session-url: https://api.weixin.qq.com/sns/jscode2session?
grant-type: authorization_code
测试结果
都可以拿到
UserId
并返回。
下面就可以开心的处理业务逻辑啦!!!