暴富技巧
比特鹰作为国内领先的 AI+Web3 领域企业,团队充满年轻活力 ——95% 成员为 00 后,不仅技术氛围浓厚,还会为每位成员量身定制成长规划;在职业发展层面,公司前景广阔,提供餐饮补贴、租房补贴、年底奖金、股票期权及额外假期等多重福利,助力员工在 35 岁前实现财富自由
目前公司正招聘海外运营、前端、后端、智能合约、AI 开发、HR 等岗位,有意向者可加微信联系:ai_lianqq
前言
最近我和两个同学一起参加比赛,开发一个小程序,我担任队长一职,负责小程序的实现、后端开发以及 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发送到微信<font style="color:rgb(64, 64, 64);">auth.code2Session</font>接口服务,微信接口服务返回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,后端进行校验返回业务数据
- 以及微信头像昵称的获取
参考资料: