theme: channing-cyan
一、前言
在微信小程序的開發(fā)過程中,如果想要保留用戶
的數據
(比如:操作記錄
、購物車信息
等等)就必須要用戶
登陸。為什么呢?比如說,數據庫中有一條數據
你如何知道這條數據屬于誰?屬于那個用戶呢?這就需要用戶登錄來獲取用戶
的唯一標識
從而確定這條數據是屬于哪個用戶的,那么如何做微信小程序的登陸功能呢?讓我們使用Springboot
框架+AOP
一起來學習吧!
二、流程
微信小程序登錄
流程:
開發(fā)者服務器
處理流程:
1.1 獲取用戶Code
通過wx.login
來獲取臨時登錄code
:
javascript wx.login({ success (res) { if (res.code) { //發(fā)起網絡請求 wx.request({ url: 'https://example.com/onLogin', data: { code: res.code } }) } else { console.log('登錄失敗!' + res.errMsg) } } })
1.2 獲取appid
在注冊微信開發(fā)者賬
后,可以在微信小程序管理后臺
獲取appid
:
1.3 獲取appsecret
小程序密鑰同樣是在注冊微信開發(fā)者平臺賬號后,在管理后臺獲取的: 由于微信小程序密鑰不以明文的方式展示,如果忘記了,
重置
下就可以了。
1.4 開發(fā)者服務向微信接口服務發(fā)起請求
拿著微信code
、appid
、appsecret
在開發(fā)者服務器
去請求微信接口服務
換取 openId
和secretKey
(這里我們使用ApiPost工具來進行請求,當然PostMan工具也行):
調用微信接口服務
接口(注意是Get
請求):
javascript https://api.weixin.qq.com/sns/jscode2session?
1.5 返回值
java { "session_key": "xxxxx", "openid": "xxxxx" }
拿到返回值后,應該入庫
,保存一下。 數據庫結構如下: 等下次該用戶登錄時,走完
1.4
流程后,可以根據返回值中的openid
在我們庫中找到該用戶,然后進行后續(xù)的操作。
1.6 自定義token
所謂token
就是用來確認用戶的身份證,拿到下面的返回值后,我們有下面兩種方式生成自定義token
:
(1)使用業(yè)務ID
生成token
(推薦使用,后續(xù)的內容都是以用戶ID作為例子的):

(2)使用session_key
生成token
:
java { "session_key": "xxxxx" }
(3)生成token
的工具:
使用md5
加密工具來生成token
,工具類如下:
```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);
}
} ```
注意:ENCODE_KEY
加密密鑰不是固定的可以自己設置,但是?。。?code>ENCODE_KEY和IV_KEY
偏移量的字符數量
一定要保持一致?。?!否者解密失?。。?!
測試:
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);
結果:
java 加密:UYKwmVTh39qvwHsQ+tkFow== 解密:test123456..
(5)將生成好的token
放入到Redis
(不重要,可以省略)
之所以放入Redis
是因為它可以設置過期時間,可以實現token
過期重新登錄的功能。比如:如果接收到微信小程序
請求所攜帶的token
后先去Redis
查詢是否存在
,如果不存
在則判定過期,直接返回讓再次用戶登錄。
```java @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放到本地
在開發(fā)者服務器
返回給微信小程序結果后,將token
放入到本地存儲。
javascript ... //將token放到本地 wx.setStorageSync('token', result.sessionKey) ...
1.8 請求帶上token
向開發(fā)者服務器
發(fā)起請求時,在header
中帶上token
javascript ... wx.request({ url: 'https://xxxx.com/api/method', header:{"token":wx.getStorageSync('token')}, success:function(res){}, fail:function(res){} }) ...
1.9 開發(fā)者服務器驗證token
開發(fā)者服務器
在接收到微信端發(fā)起的業(yè)務請求時,通過AOP
進行攔截獲取header
中的token
:
(1)AOP
統(tǒng)一攔截:
使用Spring
的AOP
來攔截請求獲取token
。
java //獲取token String token = request.getHeader("token"); log.info("token:{}",token);
(2)解密token
java ... String token = 'xxxx'; log.info("解密前:{}",decryptData); String decryptData = AESUtil.decryptFromString(token, Mode.CBC, Padding.ZeroPadding); log.info("解密結果:{}",decryptData); //拿到用戶ID String userId = decryptData; ...
(3)驗證是否過期(不重要,可以省略的步驟)
java @Autowired private RedisTemplate redisTemplate; ... //用戶ID String userId = decryptData ValueOperations valueOperations = redisTemplate.opsForValue(); String userInfoRedis = (String)valueOperations.get(userId); ...
三、前后端完整代碼
2.1 前端代碼
(1)登陸
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)發(fā)起業(yè)務請求
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 后端代碼
后端使用的Java
語言,框架是Springboot
+ AOP
實現。 目錄結構如下: yml
配置文件:
(1)依賴
```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)切面相關代碼
```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
,這種是比上一種優(yōu)雅一些,也可以完全做到隨用隨取。但是需要注意在會話
結束后一定要移除ThreadLocal
中的用戶信息,否則會導致內存溢出(這很重要),一般使用切面
的后置通知來做這件事情。
execution(* xx.xx.controller.*.*(..))
解釋:在方法執(zhí)行時,xx.xx.controller包下的所有類
下面的所有帶有任何參數的方法
都需要走這個切面。
@PointCut
注解值的規(guī)則:
execution
:方法執(zhí)行時觸發(fā)。- 第一個
*
:返回任意類型。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);
}
}
```
業(yè)務邏輯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
層代碼,業(yè)務的沒有必要。
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 { /* * 小程序AppId */ private String appId; /* * 小程序密鑰 / private String appSecret; /* * 授權類型 / private String grantType; /* * auth.code2Session 的 url */ private String authCodeSessionUrl; } ```
(6)yml
配置信息
xml wx: app-id: xxxx app-secret: xxxx auth-code-session-url: https://api.weixin.qq.com/sns/jscode2session? grant-type: authorization_code
測試結果
都可以拿到
UserId
并返回。文章來源:http://www.zghlxwxcb.cn/news/detail-771401.html
下面就可以開心的處理業(yè)務邏輯啦!?。?span toymoban-style="hidden">文章來源地址http://www.zghlxwxcb.cn/news/detail-771401.html
到了這里,關于微信小程序登錄流程(包含前端、后端代碼)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!