2021.11.29 更新文章
你好,我是博主,一起学习吧!!!
写这篇文章的原因,主要是因为最近在写毕业设计,用到了小程序,这中间曲曲折折,一言难尽啊。毕业设计真的让人麻脑阔??。唉
最近在持续更新,每天推送完代码,遇到的问题都记下来,希望对大家也能有所帮助。
在网上找了很多很多,看了不下几十篇,说实话,有些给出了核心代码,添上一个微信官方的那张流程图就结束了,会的人一下就懂了。但是说实话,真的不适合入门学者,浪费很多时间都不一定能解决问题,将代码复制完不是少这就是少那,或者就是不齐,不然就是跑不起来,不知道看到这篇文章的你有没有遇到过这样的问题。
所以我自己将踩坑的经历写下来了,希望能够帮助到大家,开源进步,交流进步,一起学习!!!
注意
挺多小伙伴遇到过这个问题,如果大家对文章内容存有疑惑或者实现不了这个小demo亦或者文章中有什么错误,可以直接评论、留言或可以直接发问题到 邮箱:nzc_wyh@
希望能够帮助到大家(当然,如果我可以做到的话 ??)
看到都会尽快回复大家,谢谢大家,一起努力
微信官方文档
一、微信小程序官方登录流程图
个人理解
:
-
调用
()
获取code
,这个code的作用是实现微信临时登录的url
中的一个非常重要的参数。- 微信授权的url=“
/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code
” -
js_code
所用到的值就是 获取到的code。
- 微信授权的url=“
-
把获取到的
code
传给我们自己的SpringBoot
后端,由我们后端向微信接口服务发送请求。String url = "/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code"; String replaceUrl = ("{0}", appid).replace("{1}", secret).replace("{2}", code); String res = (replaceUrl);//后面有代码的,莫急
- 1
- 2
- 3
-
appid
:应用ID,secret
:应用密钥,js_code
:前台传给我们的code
-
secret
获取方式:- 进入微信公众平台
- 左侧菜单选择【开发管理】
- 右侧tab选择【开发设置】
-
AppSecret
栏右侧点击重置会弹出一个二维码,需要开发者扫描二维码才可以重置AppSecret。出现AppSecret后点击复制,并保存你的AppSecret。 - 没保存就只能重新生成了。
-
后端发送请求后获取到的返回信息:
{"session_key":"G59Evf/Em54X6WsFsrpA1g==","openid":"o2ttv5L2yufc4-sdf"}
- 1
-
按照官方文档所讲:自定义登录态与
openid和session_key
关联,有很多方式可以实现的,如:- 第一种方式:我们可以将
openid和session_key
存进redis中,前端来访问的时候带上就能够访问了。 - 第二种方式:利用
jwt
方式生成Token
返回给前端,让前端下次请求时能够带上,就能允许他们访问了。
- 第一种方式:我们可以将
-
前端将
token
存入storage
-
前端在
()
发起业务请求携带自定义登录态,后端进行请求头的检查就可以了。 -
后端返回业务数据
上述就是官方的方式,但是在现在的时代,数据是非常重要的,不可能说不将用户数据持久化的,所以这个流程会稍稍多一些操作的。
二、个人实现登录流程图
三、小程序端
先说一下,这里只是测试的Demo,是分开测试的,先在前端把我要测试的数据获取出来。
我本地没有微信的编程环境,我是拿小伙伴的微信环境进行测试的。
2.1、调用()
({
success:function(res){
if(){
();
}
}
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
就是这样的一个字符串:
我们将这个返回的code
,先保存起来,稍后我们在后端测试中会用上的。
2.2、调用getUserInfo()
<button open-type="getUserInfo" bindgetuserinfo="userInfoHandler"> Click me <tton>
// 微信授权
({
success: function(res) {
(res);
}
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
打印出来是这样的一些数据。
我们需要保存的是
-
encrytedData
:包括敏感数据在内的完整用户信息的加密数据(即可以通过反解密,获取出用户数据),详见 用户数据的签名验证和加解密 -
iv
:加密算法的初始向量,详见 用户数据的签名验证和加解密
至此,我们需要在前台获取的数据,已经结束了,接下来就用我们获取到的数据一起来看后端吧!!!
2021.11.29 更新此处代码,并非cv可用,只能说是借鉴一下逻辑,知晓大致流程
??逻辑和我之前图上的有一点点偏差,请大家着重于登录思路,不要纠结于代码,非常不好意思。
import { request } from "../../request/"
Page({
data: {
encryptedData: "",
iv: "",
sessionId:""
},
onLoad: function (options) {
},
getUserProfile(e) {
const that = this;
// 获得 encryptedData & iv
({
desc: '业务需要',
success: res => {
({ encryptedData: , iv: })
// 获得 sessionId
({
success: async (res) =>{
const code = ;
if(){
const res = await request({ url: '/weixin/sessionId/' + code })
if( == 200 ){
({ sessionId: })
const {encryptedData, iv, sessionId} = ;
// 带着 encryptedData, iv, sessionId 去获得 token
const res2 = await request({
url: '/weixin/authLogin',
method: "POST",
data: {
"encryptedData": encryptedData,
"iv": iv,
"sessionId": sessionId
}
})
if( == 200 ){
const userInfo = ;
const token = ;
('userInfo', userInfo);
('token', token);
({
delta: 1,
});
}else{
();
({
title: '登录失败!',
icon: 'error',
})
}
}else{
();
({
title: '登录失败!',
icon: 'error',
})
return;
}
}else{
("获取用户登录状态失败!" + );
}
}
})
}
})
},
handleCancel(){
({
delta: 1,
});
}
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
四、SpringBoot后端
为了将代码精简,我这边只是把获取到的数据输出出来,并未真实的保存到数据中。业务操作用注释在文中展示。
项目结构:
3.1、相关jar
创建一个SpringBoot项目,或者maven项目都可以。
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId></groupId>
<version>2.5.2</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<!--使用hutool中对http封装工具类 调用 HTTP 请求-->
<dependency>
<groupId></groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.5</version>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
3.2、yml配置文件
server:
port: 8081
spring:
application:
name: springboot-weixin
redis:
database: 0
port: 6379
host: localhost
password:
weixin:
appid: 'appid'
secret: '应用密钥'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
3.3、公共类
就是一常量
public class RedisKey {
public static final String WX_SESSION_ID = "wx_session_id";
}
/**
* 统一响应结果集
* @author crush
*/
@Data
public class Result<T> {
//操作代码
Integer code;
//提示信息
String message;
//结果数据
T data;
public Result() {
}
public Result(ResultCode resultCode) {
= ();
= ();
}
public Result(ResultCode resultCode, T data) {
= ();
= ();
= data;
}
public Result(String message) {
= message;
}
public static Result SUCCESS() {
return new Result();
}
public static <T> Result SUCCESS(T data) {
return new Result(, data);
}
public static Result FAIL() {
return new Result();
}
public static Result FAIL(String message) {
return new Result(message);
}
}
/**
* 通用响应状态
*/
public enum ResultCode {
/* 成功状态码 */
SUCCESS(0, "操作成功!"),
/* 错误状态码 */
FAIL(-1, "操作失败!"),
/* 参数错误:10001-19999 */
PARAM_IS_INVALID(10001, "参数无效"),
PARAM_IS_BLANK(10002, "参数为空"),
PARAM_TYPE_BIND_ERROR(10003, "参数格式错误"),
PARAM_NOT_COMPLETE(10004, "参数缺失"),
/* 用户错误:20001-29999*/
USER_NOT_LOGGED_IN(20001, "用户未登录,请先登录"),
USER_LOGIN_ERROR(20002, "账号不存在或密码错误"),
USER_ACCOUNT_FORBIDDEN(20003, "账号已被禁用"),
USER_NOT_EXIST(20004, "用户不存在"),
USER_HAS_EXISTED(20005, "用户已存在"),
/* 系统错误:40001-49999 */
FILE_MAX_SIZE_OVERFLOW(40003, "上传尺寸过大"),
FILE_ACCEPT_NOT_SUPPORT(40004, "上传文件格式不支持"),
/* 数据错误:50001-599999 */
RESULT_DATA_NONE(50001, "数据未找到"),
DATA_IS_WRONG(50002, "数据有误"),
DATA_ALREADY_EXISTED(50003, "数据已存在"),
AUTH_CODE_ERROR(50004, "验证码错误"),
/* 权限错误:70001-79999 */
PERMISSION_UNAUTHENTICATED(70001, "此操作需要登陆系统!"),
PERMISSION_UNAUTHORISE(70002, "权限不足,无权操作!"),
PERMISSION_EXPIRE(70003, "登录状态过期!"),
PERMISSION_TOKEN_EXPIRED(70004, "token已过期"),
PERMISSION_LIMIT(70005, "访问次数受限制"),
PERMISSION_TOKEN_INVALID(70006, "无效token"),
PERMISSION_SIGNATURE_ERROR(70007, "签名失败"),
//操作代码
int code;
//提示信息
String message;
ResultCode(int code, String message) {
= code;
= message;
}
public int code() {
return code;
}
public String message() {
return message;
}
public void setCode(int code) {
= code;
}
public void setMessage(String message) {
= message;
}
}
package ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
/**
* redis 配置类
*
* @author crush
*/
@EnableCaching
@Configuration
@ConditionalOnClass()
@EnableConfigurationProperties()
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key hasKey的序列化
(stringRedisSerializer);
(stringRedisSerializer);
(redisConnectionFactory);
();
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
(redisConnectionFactory);
return stringRedisTemplate;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
3.4、Controller
import ;
import ;
import ;
import .slf4j.Slf4j;
import ;
import .*;
/**
*
* @author crush
* @since 2021-09-14
*/
@Slf4j
@RestController
@RequestMapping("/weixin")
public class WeixinController {
@Autowired
IWeixinService weixinService;
//这个就是那个使用传code进来的接口
@GetMapping("/sessionId/{code}")
public String getSessionId(@PathVariable("code") String code){
return (code);
}
@PostMapping("/authLogin")
public Result authLogin(@RequestBody WXAuth wxAuth) {
Result result = (wxAuth);
("{}",result);
return result;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
3.5、service层
public interface IWeixinService extends IService<Weixin> {
String getSessionId(String code);
Result authLogin(WXAuth wxAuth);
}
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import .slf4j.Slf4j;
import ;
import ;
import ;
import ;
import ;
/**
* @author crush
* @since 2021-09-14
*/
@Slf4j
@Service
public class WeixinServiceImpl extends ServiceImpl<WeixinMapper, Weixin> implements IWeixinService {
@Value("${}")
private String appid;
@Value("${}")
private String secret;
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
WxService wxService;
@Override
public String getSessionId(String code) {
String url = "/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
String replaceUrl = ("{0}", appid).replace("{1}", secret).replace("{2}", code);
String res = (replaceUrl);
String s = ().toString();
().set(RedisKey.WX_SESSION_ID + s, res);
return s;
}
@Override
public Result authLogin(WXAuth wxAuth) {
try {
String wxRes = ((), (), ());
("用户信息:"+wxRes);
//用户信息:{"openId":"o20","nickName":"juana","gender":2,"language":"zh_CN","city":"Changsha","province":"Hunan","country":"China","avatarUrl":"头像链接","watermark":{"timestamp":dsfs,"appid":"应用id"}}
WxUserInfo wxUserInfo = (wxRes,);
// 业务操作:你可以在这里利用数据 对数据库进行查询, 如果数据库中没有这个数据,就添加进去,即实现微信账号注册
// 如果是已经注册过的,就利用数据,生成jwt 返回token,实现登录状态
return (wxUserInfo);
} catch (Exception e) {
();
}
return ();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
2021年11月27号:应该是微信接口更新了,在此处通过解密获取到的信息中,并不包含openId啦,得自己去拿到才可以。特此在此补充,有问题大家可以一起聊
牵扯到用户信息解密的方法,想要了解,可以去微信官方文档中进行了解,我对此没有深入。
import .Base64;
import ;
import ;
import ;
import .slf4j.Slf4j;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
@Slf4j
@Component
public class WxService {
@Autowired
private StringRedisTemplate redisTemplate;
public String wxDecrypt(String encryptedData, String sessionId, String vi) throws Exception {
// 开始解密
String json = ().get(RedisKey.WX_SESSION_ID + sessionId);
("之前存储在redis中的信息:"+json);
//之前存储在redis中的信息:{"session_key":"G59Evf/Em54X6WsFsrpA1g==","openid":"o2ttv5L2yufc4-VoSPhTyUnToY60"}
JSONObject jsonObject = (json);
String sessionKey = (String) ("session_key");
byte[] encData = .(encryptedData);
byte[] iv = .(vi);
byte[] key = (sessionKey);
AlgorithmParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = ("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return new String((encData), "UTF-8");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
最后写个启动类就可以开始测试了。
@SpringBootApplication
public class SpringBootWeixin {
public static void main(String[] args) {
();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
五、测试
写完后端,接下来,可以利用我们之前收集的那些小程序中获取到的数据啦。
1、先发送第一个请求:
code
:就是之前我们获取到的数据。
http://localhost:8081/weixin/sessionId/{code}
- 1
会返回一个sessionId
回来,在第二个请求中需要携带。
2、再发送第二个请求
http://localhost:8081/weixin/authLogin
- 1
请求方式:post
data
:json格式数据
{
"encryptedData":"sYiwcAM73Ci2EB3y9+C6.....",
"iv": "xZGOj6RwaOS==",
"sessionId":"我们上一个请求获取到sessionId"
}
- 1
- 2
- 3
- 4
- 5
请求成功是下面这样的。
兄弟们,这里的省市名称现在也是拿不到的啦,具体原因还不知道,可能是小程序版本更新啦。--2022.4.27号留
我们把我们需要的存储到数据库持久化即可啦。
六、自言自语
这只是一个小demo,在使用中大都会结合security
安全框架和Jwt
一起使用,周末吧,周末比较有空,有空就会更新出来。
你好,我是博主,有问题可以留言评论或者私信我,大家一起交流学习!
不过都看到这里啦,点个赞吧???
源码
:
SpringBoot-weixin-gitee
SpringBoot-weixin-github
今天的文章就到了这里啦,下次再见!!!
非常感谢大家阅读这篇文章,然后从我这篇写的博文获取到解决自己手头上的问题的思路,也有不少小伙伴通过邮箱联系到我,一起针对问题探讨,有成功解决问题的,当然也有失败的,从而去继续baidu、google的,哈哈。
你们在阅读的同时也在帮我测试这篇文章的可读性,代码的正确性,以及帮助我指出文章里的问题,在此都非常感谢大家。
补注
:本篇文章的 demo 更多的是在帮助大家快速的实现登录流程,针对登录流程加深一下理解,但到真正使用时,仍有许多要完善的地方,此处的 demo 的代码也并非全部规范化开发,请大家使用的时候,稍加注意。
下个月抽空一定写一个 2.0 版本出来,希望能够帮助到更多人,这个月仍忙于手上的事情,无法继续写文。希望下月的文章也能获得你们的阅读
文章拖到七月,也仍没完成。有点糟心。
谢谢你读到本文的结尾。他日再见时,已有所成,万事胜意
更新于2022.4.27