前言
最近我和两个同学一起参加比赛,开发一个小程序,我担任队长一职,负责小程序的实现、后端开发以及 UI 设计,目前项目仍在开发中,感兴趣的话可以点击 Github 链接查看(给个Star),接下来我讲一下我如何使用Uniapp
、Java
实现微信授权登录的😀
一、小程序中微信授权登录的优势
微信小程序极大地简化了登录注册流程。对于用户而言,仅仅需要点击授权按钮,便能够完成登录操作,无需经历繁琐的注册步骤以及输入账号密码等一系列复杂操作,这种便捷的登录方式极大地提升了用户的使用体验。
二、小程序登录流程
我来说一下这个流程图
- 小程序端通过调用wx.login()方法,获取零时登录凭证code,这个code是后面向 auth.code2Session微信服务器接口发送请求的重要参数
- 小程序使用wx.request()方法将获取到的code 发送到开发者服务器也就是后端
- 开发者服务器接收code,将开发者的appid、appsecret以及接收到的code发送到微信的 auth.code2Session接口服务,通过这个接口,开发者服务器可以获取session_key、openid等信息
- 开发者服务器可以将获取的的session_key、openid与自定义登录态进行关联,生成一个唯一的自定义登录态也就是token
- 开发者服务器将生成的自定义登录态返回给小程序
- 小程序接收到自定义登录态之后,将其存储入本地存储storage中,以便后续的业务请求使用
- 当小程序需要进行业务请求时,使用wx.request()方法发起请求,并携带存储在storage中的自定义登录态
- 开发者服务器接收到业务请求之后,通过自定义登录态查询对应的openid和session_key
- 返回业务数据
也就是说小程序调用wx.login()方法获取code,随后发送给后端服务器,后端接收到code之后将开发者的appid、appsecret以及接收到的code发送到微信auth.code2Session接口服务,微信接口服务返回openid、session_key关联生成token后返回给小程序,小程序存储本地storage,进行业务请求时携带token,后端接收后通过token查询对应的openid和session_key,最后返回业务数据
二、前端实现步骤
1.创建项目
使用npx degit dcloudio/uni-preset-vue#vite-ts wx-login-test
命令
创建一个Vite
+Vue3
+Typescript
的Uniapp
的项目
2.运行项目
使用 VSCode
打开项目然后安装依赖
运行pnpm i
安装依赖或者使用npm i
先将page/index/index.vue
文件内容修改一下,内容如下:
<script setup lang="ts"> </script> <template> <div> <button class="button">微信授权登录</button> </div> </template> <style scoped> .button { color: white; padding: 6rpx; background-color: #ED4556; border-radius: none; } </style>
添加appid
,修改manifest.json文件下的mp-weixin
的appid
字段
appid
从微信小程序后台获取
{ // ... "mp-weixin": { // 这里为你的appid "appid": "your_appid", "setting": { "urlCheck": false }, "usingComponents": true }, // ... }
运行pnpm dev:mp-weixin
命令 启动项目
使用微信开发者工具运行根目录下的dist\dev\mp-weixin
效果如下
我们创建了一个登录按钮
3.获取Code
修改一下index.vue
文件,内容如下:
<script setup lang="ts"> const onWxLogin = () => { uni.login({ provider: 'weixin', success: async (res) => { const data = await uni.request({ method: "POST", url: "http://localhost:8080/users/auth/wechat", data: { code: res.code } }) console.log(data); }, }) } </script> <template> <div> <button class="button" @click="onWxLogin">微信授权登录</button> </div> </template> <style scoped> .button { color: white; padding: 6rpx; background-color: #ED4556; border-radius: none; } </style>
点击登录按钮会触发onWxLogin然后使用 uni.login()获取code,provider参数为登录服务提供商,在这里指定登录服务提供商为微信,在success成功回调中获取code,调用uni.request()发送给后端,后端调用微信服务接口获取openid、session_key,生成token
uni.login是一个客户端API,统一封装了各个平台的各种常见的登录方式,包括App手机号一键登陆、三方登录(微信、微博、QQ、Apple、google、facebook)、各家小程序内置登录
三、后端实现步骤
1.创建一个SpringBoot3项目
使用Idea
创建或者在start.spring.io这个地址中创建
选择Spring Web
、Mybatis Framework
、Lombok
、MySQL Driver
依赖
创建之后将application.properties
配置文件后缀修改为application.properties
2.修改Maven配置文件pom.xml
pom.xml
文件内容如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>login-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>login-server</name> <description>login-server</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter-test</artifactId> <version>3.0.3</version> <scope>test</scope> </dependency> <!--httpclient的坐标用于在java中发起请求--> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency> <!--使用fastjson解析json数据 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency> <!--java-jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
3.新建数据库
新建一个数据库,然后新建一个users
表用于存储用户信息
CREATE TABLE `users` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '用户主键ID', `open_id` varchar(255) NOT NULL COMMENT 'openId 微信用户唯一标识', `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '微信用户' COMMENT '用户名', `avatar_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132' COMMENT '用户头像', `create_time` datetime NOT NULL COMMENT '用户创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
给username
、password
指定默认的值
3.修改配置文件
然后再修改配置文件内容如下:
server: port: 8080 # 服务端口 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_database_name # 你的数据库名称 username: your_database_username # 数据库用户名 password: your_database_password # 数据库密码
上面的数据库名
、用户名
、密码
要修改为你的
4.新建UserController
类
创建controller
包,在下面创建UserController
5.添加微信授权登录接口
@RestController @RequiredArgsConstructor @Slf4j @RequestMapping("/users") public class UserController { @PostMapping("/auth/wechat") public String authWechat() { return "登录成功"; } }
测试一下运行吧
点击按钮之后成功获取数据
6.封装Result统一返回类
先在创建一个utils
软件包然后在下面创建一个Result类
用于统一响应结果
package com.example.loginserver.utils; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class Result<T> implements Serializable { private Integer code; private String message; private T data; /** * 成功的结果响应 不带参数 * @return * @param <T> */ public static <T> Result<T> success() { return new Result<>(0, "success", null); } public static <T> Result<T> success(String message,T data) { return new Result<>(0, message, data); } /** * 成功的结果响应 * @param data 响应数据 * @return * @param <T> */ public static <T> Result<T> success(T data) { return new Result<>(0, "success", data); } public static <T> Result<T> failure( String message) { return new Result<>(1, message, null); } /** * 错误的结果响应 * @param code 状态码 * @param message 错误消息 * @return * @param <T> */ public static <T> Result<T> failure(Integer code, String message) { return new Result<>(code, message, null); } /** * 错误的结果响应 * @param code 状态码 * @param message 错误消息 * @param data 错误数据 * @return * @param <T> */ public static <T> Result<T> failure(Integer code, String message, T data) { return new Result<>(code, message, data); } }
7.创建PO
、DTO
类
创建PO
、VO
类,如果有同学分不清 DTO/VO/PO
,我可以简单介绍一下
DTO(Data Transfer Object)
:数据传输对象,前端传递给后端的数据PO(Persistant Object)
:持久对象,属性与数据库表字段一一对应,用于插入数据库数据VO(Value Object)
:值对象,也是用于数据传输的对象,后端返回给前端的,可以指定返回的字段
除了上面说的DTO
、PO
、VO
之外,后面还有DO
、BO
等概念可以自行上网了解这里就不说这么多了
Users 用于插入数据 与数据库表字段一一对应
@Data @Builder public class Users { /** 用户id */ private Long id; /** 微信用户唯一标识 */ private String openId; /** 用户名称 */ private String username; /** 头像路径 */ private String avatarUrl; /** 创建时间 */ private LocalDateTime createTime; }
WeChatDTO 用于接收前端传递的DTO
@Data public class WeChatCodeDTO { /**微信token*/ private String code; }
8.创建Service
、Mapper
controller
- 控制层service
- 业务逻辑层mapper
- 数据访问层
UsersService
package com.example.loginserver.service; public interface UsersService {}
UsersServiceImpl
@Service @Slf4j @RequiredArgsConstructor public class UsersServiceImpl implements UsersService {}
UsersMapper
package com.example.loginserver.mapper; import org.apache.ibatis.annotations.Mapper; @Mapper public class UsersMapper {}
9.实现接口
1.一切准备完毕,那就开始实现吧~😉
UsersController
@RestController @RequiredArgsConstructor @Slf4j @RequestMapping("/users") public class UserController { private final UserService userService; /** * 小程序微信授权登录 * @return */ @PostMapping("/login/wechat") public Result<String> loginWithWeChat(@RequestBody WeChatCodeDTO weChatCodeDTO) { return userService.loginWithWeChat(weChatCodeDTO.getCode()); } }
UsersService
public interface UsersService { UserLoginVO loginWithWeChat(String code); }
然后在UsersServiceImpl
中实现
2.UsersServiceImpl实现
UsersServiceImpl
@Service @RequiredArgsConstructor @Slf4j public class UserServiceImpl implements UserService { private final WeChatProperties weChatProperties; private final UsersMapper usersMapper; private final String WECHAT_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session"; @Override public Result<String> loginWithWeChat(String code) { String openId = getOpenId(code); // 查询用户是否已存在 Long userId = usersMapper.getUserByOpenId(openId); if (userId == null) { Users user = Users.builder().openId(openId).build(); usersMapper.insertUsers(user); String token = JwtTokenUtil.generateTokenWithUserId(user.getId()); return Result.success("登录成功", token); } return Result.success("登录成功", JwtTokenUtil.generateTokenWithUserId(userId)); } public String getOpenId(String code) { // 封装请求参数 HashMap<String, String> map = new HashMap<>(); map.put("appid", weChatProperties.getAppId()); map.put("secret", weChatProperties.getSecret()); map.put("js_code", code); map.put("grant_type", "authorization_code"); // 使用封装好的 HttpClientUtil 从微信后台请求 String json = HttpClientUtil.doGet(WECHAT_LOGIN_URL, map); // 将获取过来的数据解析出来 JSONObject jsonObject = JSON.parseObject(json); // 获取openId log.info("jsonObject:{}", jsonObject); return jsonObject.getString("openid"); } }
getOpenId这个方法是调用封装好的HttpClientUtil向微信auth.code2Session接口服务发起请求获取用户openid、session_key,这里我们返回openid
注意:appid需要跟小程序端的manifest.json文件中的appid一致,否者会报错
在authWechat这个方法中使用getOpenId获取openId,再使用openId查询用户是否已注册,调用封装好的jwt生成token,如果用户不存在则创建用户,并构建成UsersLoginVO对象返回给前端,如果已存在则直接构建成UsersLoginVO对象返回给前端
接下来我们在utils文件夹下新建JwtUtil和HttpClientUtil这两个工具类
JwtUtils
public class JwtTokenUtil { // 秘钥,实际应用中应该妥善保管,比如从配置文件读取等 private static final String SECRET_KEY = "your_secret_key"; // Token过期时间,这里设置为1小时,单位是毫秒,可以按需调整 private static final long EXPIRATION_TIME = 60 * 60 * 1000; /** * 生成携带用户id的token * @param userId * @return */ public static String generateTokenWithUserId(Long userId) { return JWT.create().withClaim("userId", userId).withExpiresAt(new Date(EXPIRATION_TIME)).sign(Algorithm.HMAC256(SECRET_KEY)); } /** * 解析token并返回用户id * @param token * @return */ public static String parseTokenGetUserId(String token) { DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build().verify(token); return String.valueOf(decodedJWT.getClaim("userId")); } }
HttpClientUtil
public class HttpClientUtil { private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class); private static final int TIMEOUT_MSEC = 5 * 1000; public static String doGet(String url, Map<String, String> paramMap) { String result = ""; CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; try { URIBuilder builder = new URIBuilder(url); if (paramMap != null) { for (Map.Entry<String, String> entry : paramMap.entrySet()) { builder.addParameter(entry.getKey(), entry.getValue()); } } URI uri = builder.build(); HttpGet httpGet = new HttpGet(uri); httpGet.setConfig(buildRequestConfig()); response = httpClient.execute(httpGet); logger.info("GET Response status: {}", response.getStatusLine().getStatusCode()); if (response.getStatusLine().getStatusCode() == 200) { result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); logger.debug("GET Response body: {}", result); } } catch (Exception e) { logger.error("Error occurred while sending GET request", e); } finally { closeResources(response, httpClient); } return result; } public static String doPost(String url, Map<String, String> paramMap) { String resultString = ""; CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; try { HttpPost httpPost = new HttpPost(url); if (paramMap != null) { List<NameValuePair> paramList = new ArrayList<>(); for (Map.Entry<String, String> param : paramMap.entrySet()) { paramList.add(new BasicNameValuePair(param.getKey(), param.getValue())); } UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList, StandardCharsets.UTF_8); httpPost.setEntity(entity); } httpPost.setConfig(buildRequestConfig()); response = httpClient.execute(httpPost); logger.info("POST Response status: {}", response.getStatusLine().getStatusCode()); resultString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); logger.debug("POST Response body: {}", resultString); } catch (Exception e) { logger.error("Error occurred while sending POST request", e); } finally { closeResources(response, httpClient); } return resultString; } public static RequestConfig buildRequestConfig() { return RequestConfig.custom() .setConnectTimeout(TIMEOUT_MSEC) .setConnectionRequestTimeout(TIMEOUT_MSEC) .setSocketTimeout(TIMEOUT_MSEC) .build(); } private static void closeResources(CloseableHttpResponse response, CloseableHttpClient httpClient) { try { if (response != null) { response.close(); } if (httpClient != null) { httpClient.close(); } } catch (IOException e) { logger.error("Error occurred while closing resources", e); } } }
3.UsersMapper
UsersMapper
package com.example.loginserver.mapper; import com.example.loginserver.pojo.po.Users; import com.example.loginserver.pojo.vo.UsersLoginVO; import com.example.loginserver.pojo.vo.UsersLoginVO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; @Mapper public interface UsersMapper { // 根据 openid 查询用户是否存在 @Select("select id from users where open_id = #{openId}") Long getUserByOpenId(String openId); // 新增用户 并返回id void insertUsers(Users users); }
在resource
文件夹下新建mapper\UsersMapper.xml
文件,代码如下
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.example.loginserver.mapper.UsersMapper"> // 使用数据库自动生成的键 // 指定将自动生成的键赋值给参数对象(Users)的id属性 // 这样就能从Users对象中获取返回的id了 <insert id="insertUsers" useGeneratedKeys="true" keyProperty="id"> insert into users (open_id, create_time) values ( #{openId}, #{createTime}) </insert> </mapper>
4.添加拦截器
我们可以添加拦截器对token进行校验
@Component @RequiredArgsConstructor @Slf4j public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求的 url String url = request.getRequestURL().toString(); // 2.判断请求的 url 中是否包含 login,如果包含,说明是登录操作,放行 if (url.contains("login") ) { return true; } if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) { System.out.println("OPTIONS 请求,放行"); return true; } // 3.获取请求头中的 Authorization String token = request.getHeader("Authorization"); log.info("请求头:{}", request.getHeader("Authorization")); // 4.判断令牌是否存在,如果不存在,返回错误结果(未登录) if (!StringUtils.hasLength(token)) { response.setStatus(401); return false; } // 5.解析 token,如果解析失败,返回错误结果 try { String userId = JwtTokenUtil.parseTokenGetUserId(token); request.setAttribute("userId", userId); return true; } catch (Exception e) { log.error("解析令牌失败", e); response.setStatus(401); return false; } } }
request.setAttribut
方便后续获取用户id
5.测试
接下来我们重启一下SpringBoot
服务
再打开微信开发者工具
可以看到成功的返回了我们想要的结果用户id
、token
我们来看一下数据库有没有插入用户数据
呐🤓,新增了一条用户数据,username
、password
我们自定了默认值,后面前端可以后端发送请求更新
扩展🔧
- 前端获取到后端返回的用户
id
,token
可以使用uni.setStorage进行本地存储
uni.setStorageSync('token', data.data.token)
- 前端发送请求数据时从
storage
中获取token
,添加到header
请求头中发送给后端服务使用jwt
进行校验
uni.request({ // ... header: { Authorization:uni.getStorageSync('token') }, // ... })
- 关于更新用户信息,由于微信官方收回了
wx.getUserProfile
和wx.getUserInfo
接口,统一返回灰色图像,昵称统一返回"微信用户" - 不过可以使用头像昵称填写能力让用户进行填写获取,更多信息可以点击进入微信开发文档进行阅读
总结📚
好啦!就写到这里吧!😃,相信你应该学会了如何实现微信小程序授权登录吧?前端小程序端实现很简单😏获取code发送给后端,获取到后端返回的自定义登录态之后使用uni.setStorageSync()进行存储,代码主要是后端处理,需要使用HttpClient向微信auth.codeSession接口服务发送请求获取openid,然后判断用户是否存在,如果不存在则创建新用户,存在则返回,生成自定义登录态token,后面前端发送请求获取数据是,校验一下请求头中的token即可
- 微信小程序授权登录优势:微信小程序简化了登录注册流程,用户只需点击授权按钮即可完成登录,无需繁琐操作,提升了用户体验
- 登录流程:小程序端获取code,后端接收code后,将appid、appsecret和code发送到微信auth.codeSession接口服务,获取openid、session_key,生成自定义登录态返回给小程序端,小程序端进行存储,后续发送请求时携带自定义登录态token,后端进行校验返回业务数据
- 以及微信头像昵称的获取
参考资料:
好的谢谢同学们看到这里❤️