theme: channing-cyan
I. Introduction
During the development process of WeChat applet, if you want to retain 用户
( 数据
such as: 操作记录
, 购物车信息
etc.), you must 用户
log in. why? For example, if there is a piece of data in the database, 数据
how do you know who this piece of data belongs to? Which user does it belong to? This requires the user to log in to obtain it 用户
to 唯一标识
determine which user this data belongs to. So how to implement the login function of the WeChat applet? Let’s use Springboot
the framework + AOP
learn together!
2. Process
微信小程序登录
process:
开发者服务器
Processing flow:
1.1 Obtain usersCode
wx.login
Get a temporary login via code
:
javascript 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 Getappid
After registering 微信开发者账
, you can 微信小程序管理后台
obtain appid
:
1.3 Getappsecret
The mini program key is also obtained in the management backend after registering a WeChat developer platform account: Since the WeChat mini program key is not displayed in clear text, if you forget it, 重置
just download it.
1.4 The developer service initiates a request to the WeChat interface service
Take 微信code
, appid
, and request appsecret
in exchange for 开发者服务器
sum (here we use the ApiPost tool to make the request, of course the PostMan tool will also work):微信接口服务
openId
secretKey
Call 微信接口服务
the interface (note that it is Get
a request):
javascript https://api.weixin.qq.com/sns/jscode2session?
1.5 Return value
java { "session_key": "xxxxx", "openid": "xxxxx" }
After getting the return value, you should 入库
save it. The database structure is as follows: the next time the user logs in, 1.4
after completing the process, the user can be found in our library based on the return value openid
, and then subsequent operations can be performed.
1.6 Customizationtoken
The so-called token
is used to confirm the user's ID card. After getting the following return value, we have the following two ways to generate it 自定义token
:
(1) Use 业务ID
generation token
(recommended, the following content uses user ID as an example):
(2)Use session_key
to generate token
:
java { "session_key": "xxxxx" }
(3) Generated token
tools:
Use md5
encryption tools to generate token
, the tool classes are as follows:
```java 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);
}
} ```
Note: ENCODE_KEY
The encryption key is not fixed and can be set by yourself, however! ! ! The characters of the offset ENCODE_KEY
must be consistent! ! ! Otherwise, decryption fails! ! !IV_KEY
数量
test:
java 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);
result:
java 加密:UYKwmVTh39qvwHsQ+tkFow== 解密:test123456..
(5) Put the generated ones token
into Redis
(not important, can be omitted)
The reason why it is put in Redis
is that it can set the expiration time and realize token
the function of re-login after expiration. For example: after receiving 微信小程序
the request, token
first Redis
check whether it is there 存在
. If 不存
it is, it will be determined to be expired and return directly to allow the user to log in again.
```java @Autowired private RedisTemplate redisTemplate; .... //The unique identifier of the WeChat user private String userId= 'xxxxx' //Put the token into redis and set it to expire in 3 days redisTemplate.opsForValue().set(userId, JSONObject.toJSONString(userInfo),3, TimeUnit.DAYS);
```
(6) Return token
to WeChat applet
Will token
be placed in the return body and returned to WeChat.
java ... return returnSuccess(token);
1.7 will token
be placed locally
After 开发者服务器
the results are returned to the WeChat applet, they will token
be placed in local storage.
javascript ... //将token放到本地 wx.setStorageSync('token', result.sessionKey) ...
1.8 Request to bringtoken
开发者服务器
When making a request to header
thetoken
javascript ... wx.request({ url: 'https://xxxx.com/api/method', header:{"token":wx.getStorageSync('token')}, success:function(res){}, fail:function(res){} }) ...
1.9 Developer Server Verificationtoken
开发者服务器
When receiving a business request initiated by WeChat, AOP
intercept and header
obtain token
:
(1) AOP
Unified interception:
Used Spring
to AOP
intercept request acquisition token
.
java //获取token String token = request.getHeader("token"); log.info("token:{}",token);
(2) Decryptiontoken
java ... String token = 'xxxx'; log.info("解密前:{}",decryptData); String decryptData = AESUtil.decryptFromString(token, Mode.CBC, Padding.ZeroPadding); log.info("解密结果:{}",decryptData); //拿到用户ID String userId = decryptData; ...
(3) Verify whether it has expired (not important, a step that can be omitted)
java @Autowired private RedisTemplate redisTemplate; ... //用户ID String userId = decryptData ValueOperations valueOperations = redisTemplate.opsForValue(); String userInfoRedis = (String)valueOperations.get(userId); ...
3. Complete front-end and back-end codes
2.1 Front-end code
(1)Login
javascript 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) Initiate a business request
javascript 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 Backend code
Java
The language and framework used by the backend are Springboot
+ AOP
implemented. The directory structure is as follows: yml
Configuration file:
(1)Dependence
```xml 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)Aspect related code
```java 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.*(..))") 比如 登陆的时候就需要放行
登陆的接口。
```java public class AopException extends Exception { public AopException() { super("登录超时,请重新登录"); } }
```
(3)控制层代码
登陆Controller
代码:
```java 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
代码:
```java 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
层代码,业务的没有必要。
java 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
:
```java import lombok.Data;
@Data public class Base { private String userId; }
`` 用户实体类
User`:
```java import lombok.Data;
@Data public class User { private String userId; }
`` 用户信息实体
UserContent`:
```java 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)配置类
```java import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;
@Data @Component @ConfigurationProperties(prefix = "wx") public class WxConfig { / * * Mini program AppId */ private String appId; / * * Mini program key / private String appSecret; / * * Authorization type / private String grantType ; / * * url of auth.code2Session */ private String authCodeSessionUrl; } ```
(6) yml
Configuration information
xml wx: app-id: xxxx app-secret: xxxx auth-code-session-url: https://api.weixin.qq.com/sns/jscode2session? grant-type: authorization_code
Test Results
All can be obtained UserId
and returned.
Now you can happily handle the business logic! ! !