📌 微信小程序第一节 ——自定义顶部、底部导航栏以及获取胶囊体位置信息。
📌 微信小程序第二节 —— 自定义组件
📌 微信小程序第三节 —— 页面跳转的那些事儿
📌 微信小程序第四节—— 网络请求那些事儿
- 😜作 者:是江迪呀
- ✒️本文关键词:
微信小程序
、登陆
、token
、前端
、后端
、验证
、加密
- ☀️每日 一言:趁青春尚存,别为生活沉沦。
前言
在微信小程序的开发过程中,如果想要保留用户
的数据
(比如:操作记录
、购物车信息
等等)就必须要用户
登陆。为什么呢?比如说,数据库中有一条数据
你如何知道这条数据属于谁?属于那个用户呢?这就需要用户登录来获取用户
的唯一标识
从而确定这条数据是属于哪个用户的,这就需要用到用户登陆,那么如何做微信小程序的登陆功能呢?让我们使用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
请求):
https://api.weixin.qq.com/sns/jscode2session?
1.5 返回值
{"session_key": "xxxxx","openid": "xxxxx"
}
拿到返回值后,应该入库
,保存一下。
数据库结构如下:
等下次该用户登录时,走完1.4
流程后,可以根据返回值中的openid
在我们库中找到该用户,然后进行后续的操作。
1.6 自定义token
拿到下面的返回值后,我们有下面两种方式生成自定义token
:
(1)使用业务ID生成token(推荐使用,后续的内容都是以用户ID作为例子的)
:
(2)使用session_key生成token
:
{"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);
结果:
加密: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
放到返回体中返回给微信端。
...
return returnSuccess(token);
1.7 将token
放到本地
在开发者服务器
返回给微信小程序结果后,将token
放入到本地存储。
...
//将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
。
//获取tokenString 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.resultwx.setStorageSync('token', result.token)//页面跳转...},fail:function(res){},})}}})
(2)发起业务请求
wx.request({url: "https://xxxx.com/test/test",method: "GET",dataType:"json",data:{},//在heard中戴上tokenheader:{"token":wx.getStorageSync('token')},success:function(res){...},fail:function(res){}});
2.2 后端代码
后端使用的Java
语言,框架是Springboot
+ AOP
实现。
目录结构如下:
yml
配置文件:
(1)依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.1.2.RELEASE</version>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.3.7.RELEASE</version>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.16.16</version>
</dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.30</version>
</dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.6.3</version>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><version>3.0.4</version>
</dependency>
(2)切面相关代码
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.trueland.config.AopException;
import cn.trueland.model.Base;
import cn.trueland.model.User;
import cn.trueland.model.UserContent;
import cn.trueland.utils.AESUtil;
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 {@Autowiredprivate HttpServletRequest request;@Pointcut("execution(* cn.trueland.controller.*.*(..))")public void pointCut(){}@Around(value = "pointCut()")public Object Around(ProceedingJoinPoint joinPoint) throws Throwable {//获取tokenString token = request.getHeader("token");log.info("token:{}",token);//不存在token直接抛出异常if(StringUtils.isEmpty(token)){throw new AopException();}//解析tokenString 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 com.hjd.task.common.AbstractController;
import com.hjd.task.dto.wx.WxLoginRequestDto;
import com.hjd.task.dto.wx.WxLoginResponseDto;
import com.hjd.task.entity.exception.Response;
import com.hjd.task.service.IWxLoginService;
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 extends AbstractController {@Autowiredprivate 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());//请求微信服务接口获取 openIdString result = HttpClientUtil.doGet(authCode2Session);String openId = JSONObject.parseObject(result).getString("openid");String sessionKey = JSONObject.parseObject(result).getString("session_key");//入库 并返回 userId (逻辑省略)String userId = ...;//将用户信息存入redisredisTemplate.opsForValue().set(userId,userId ,3, TimeUnit.DAYS);String token = AESUtil.encryptFromString(userId, Mode.CBC, Padding.ZeroPadding);return token;}
(4)实体类相关代码
import lombok.Data;
@Data
public class WxLoginRequestDto {/*** code*/private String code;
}
import lombok.Data;@Data
public class Base {private String userId;
}
import lombok.Data;@Data
public class User {private String userId;
}
public class UserContent {private static final ThreadLocal<User> 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: xxxxapp-secret: xxxxauth-code-session-url: https://api.weixin.qq.com/sns/jscode2session?grant-type: authorization_code
测试结果
都可以拿到UserId
并返回。
下面就可以开心的处理业务逻辑啦!!!
三、总结
再多言语都没一张图来的贴切,整个业务流程如下图: