2022最新微信小程序授权登录(前后端分离)
一、前言首先说一下微信小程序最近两个比较大的变动:1. 获取用户信息接口由原来的wx.getUserInfo更换为wx.getUserProfile2021年4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。新增getUserProfile接口,若开发者需要获取用户的个
一、前言
首先说一下微信小程序最近两个比较大的变动:
1. 获取用户信息接口由原来的wx.getUserInfo更换为wx.getUserProfile
2021年4月28日24时后发布的新版本小程序,开发者调用wx.getUserInfo将不再弹出弹窗,直接返回匿名的用户个人信息,获取加密后的openID、unionID数据的能力不做调整。
新增getUserProfile接口,若开发者需要获取用户的个人信息,可以通过wx.getUserProfile接口进行获取,该接口只返回用户个人信息,不包含用户身份标识符。该接口中desc属性(声明获取用户个人信息后的用途)后续会展示在弹窗中,请开发者谨慎填写。开发者每次通过该接口获取用户个人信息均需用户确认,请开发者妥善保管用户快速填写的头像昵称,避免重复弹窗。
新版本的小程序最直观的感受:进入小程序时不会立刻跳出弹窗,而是当用户进行相关操作,比如点击了某个请求按钮,才会跳出弹窗提示授权。
官方公告:小程序登录、用户信息相关接口调整说明 | 微信开放社区
2. 小程序获取用户信息相关接口,不再返回用户性别及地区信息
根据相关法律法规,进一步规范开发者调用用户信息相关接口,小程序获取用户信息相关接口,不再返回用户性别及地区信息,这也就意味着,现在开放的接口只能获取到用户的头像和昵称两个用户信息,其余信息需要用户自己填写。
对开发者而言,这次的改动降低了获取信息的难度,但相对的,获取到的数据的重要性也下降了。以往获取信息的方式需要小程序端获取encryData、iv到后端进行解密,后端再返回给前端相关信息,而现在可以直接获取头像与用户名,只需调用后端接口将其存储到数据库即可。
官方公告: 微信公众平台用户信息相关接口调整公告 | 微信开放社区
二、前置准备
1. 技术栈
前端:微信小程序开发(不使用云开发)
后端:spring boot + mysql + mybatis + jwt
2. 了解登录流程
大致流程:
1. 前端调用wx.login获取code,再调用后端接口传递code
注意:code是临时的,只有5分钟的使用时间,而且只能使用一次
2. 后端用获取的code与微信接口服务换取openid(用户唯一标识)与session_key(可以用于解密私密信息encrydata,现在只能获取头像和昵称),关联openid和session_key自定义登录态session,利用session生成token
注意:不可以把解析出来的openid和session_key直接返回给前端,会造成信息安全问题
3. 将token返回给前端
4. 前端缓存token
5. 用户登录时,登录接口获取到token,再调用其他接口时,拦截器进行拦截,如果token有效,则放行请求;如果token失效(不存在、过期、格式不正确等原因),则无法访问该接口,需要重新登录。
说明:如果觉得token验证太过复杂,也可以退而求其次,采用微信小程序自带的wx.checkSeesion检查下发的session_key是否过期(固定为两天)。
wx.checkSeesion是前端检查,非常方便,但是缺点也很明显:耗时长,通常需要300+ms ,另外前后端传递私密数据时,需要额外考虑数据安全问题(以openid为例,前端每次需要传递openid时,都需要先获取临时code,再传递给后端,后端再用code换取openid,开销极大),因此正式开发时极不建议使用wx.checkSeesion,token验证方式可以较好解决上述问题。
三、开发代码
1、后端代码
1. config包(主要是一些配置信息)
1. InterceptorConfig类(拦截器配置类)
这里有一点需要注意,拦截器加载的时间点在springcontext之前,会导致拦截器中自动注入为null,因此需要用@Bean提前加载;
另外,addPathPatterns用于添加拦截的路径,理论上除了登入登出接口,其他接口都需要拦截。
为什么要使用拦截器?因为前端获取到token后,如果每次请求都在请求体中加入token,会导致前后端代码非常冗长,因此可以将token放置于请求头header中,每次请求利用拦截器进行拦截,开发者仅需关注业务逻辑信息。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
public JwtInterceptor getJwtInterceptor(){
return new JwtInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(getJwtInterceptor())
.addPathPatterns("/user/**") //拦截用户接口
.excludePathPatterns("/user/index/**");//登录接口不拦截
}
}
2、common(公共包)
与util包有一定区别,util包一般放置静态工具类,当工具类较多时应该使用common包进行细化
1. Result类(用于返回消息,简化版,实际状态码远不止两个)
@Data
@NoArgsConstructor
public class Result {
private int code;
private String msg;
private Object data;
public static Result succ(Object data){
return succ(200,"操作成功",data);
}
public static Result succ(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setData(data);
r.setMsg(msg);
return r;
}
public static Result succ(String msg, Object data) {
Result r = new Result();
r.setCode(200);
r.setData(data);
r.setMsg(msg);
return r;
}
public static Result fail(String msg){
return fail(500,msg,null);
}
public static Result fail(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setData(data);
r.setMsg(msg);
return r;
}
public static Result fail(String msg, Object data) {
Result r = new Result();
r.setCode(500);
r.setData(data);
r.setMsg(msg);
return r;
}
public static Result fail(int code, String msg) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(null);
return r;
}
}
2. TokenException类(自定义异常)
继承RuntimeException异常类,RuntimeException属于非受检异常,仅在运行时捕获,编译时不会检查,因此可以不加try-catch语句直接抛出(使用见下文拦截器类JwtInterceptor)。
public class TokenException extends RuntimeException{
public TokenException() {super();}
public TokenException(String msg) {
super(msg);
}
}
3. GlobalExceptionHandler类(全局异常处理)
token失效状态码可以与前端做约定,一般使用401表示未经授权
@RestControllerAdvice
public class GlobalExceptionHandler {
//token失效异常
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = TokenException.class)
public Result handler(TokenException e){
return Result.fail(401, e.getMessage());
}
}
3、util包(业务工具包)
1. JwtUtil类
先导入依赖(采用jjwt)
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
配置基本信息(写入application.yml,secret为密码,expire为过期时间,header为请求头名称)
markerhub:
jwt:
secret: 2019scaumis25710000de581c0f9eb5
expire: 604800
header: Authorization
编写Jwt工具类
@Data
@Component
@ConfigurationProperties(prefix = "markerhub.jwt")
public class JwtUtil {
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
* @param session
* @return
*/
public String getToken(String session){
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ","JWT")
.setSubject(session)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
}
/**
* 从token中获取自定义登录态session后解密获取openid
* @param token
* @return
*/
public String getOpenidFromToken(String token){
String openid;
String session;
try{
//解析token获取session
Claims cliams = getCliamByToken(token);
session = cliams.getSubject();
//解密session
EncryptUtil encryptUtil = new EncryptUtil();
String jsonString = encryptUtil.decrypt(session);
JSONObject jsonObject = JSONObject.fromObject(jsonString);
openid = jsonObject.getString("openid");
return openid;
}
catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 从token中获取荷载
* @param token
* @return
*/
public Claims getCliamByToken(String token){
try{
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
catch (Exception e){
return null;
}
}
/**
* 校验token
* @param token
* @return
*/
public void verifyToken(String token){
//在拦截器抛出异常
Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
}
2. EncryptUtil类(加解密工具类)
这里采用DES加密策略,但是不推荐,可以考虑更换为AES或RSA
public class EncryptUtil {
// 字符串默认键值
private static String strDefaultKey = "2022@#$%^&";
//加密工具
private Cipher encryptCipher = null;
// 解密工具
private Cipher decryptCipher = null;
/**
* 默认构造方法,使用默认密钥
*/
public EncryptUtil() throws Exception {
this(strDefaultKey);
}
/**
* 指定密钥构造方法
*/
public EncryptUtil(String strKey) throws Exception {
Key key = getKey(strKey.getBytes());
encryptCipher = Cipher.getInstance("DES");
encryptCipher.init(Cipher.ENCRYPT_MODE, key);
decryptCipher = Cipher.getInstance("DES");
decryptCipher.init(Cipher.DECRYPT_MODE, key);
}
/**
* 将byte数组转换为表示16进制值的字符串, 如:byte[]{8,18}转换为:0813,和public static byte[]
*/
public static String byteArr2HexStr(byte[] arrB) throws Exception {
int iLen = arrB.length;
// 每个byte用2个字符才能表示,所以字符串的长度是数组长度的2倍
StringBuffer sb = new StringBuffer(iLen * 2);
for (int i = 0; i < iLen; i++) {
int intTmp = arrB[i];
// 把负数转换为正数
while (intTmp < 0) {
intTmp = intTmp + 256;
}
// 小于0F的数需要在前面补0
if (intTmp < 16) {
sb.append("0");
}
sb.append(Integer.toString(intTmp, 16));
}
return sb.toString();
}
/**
* 将表示16进制值的字符串转换为byte数组,和public static String byteArr2HexStr(byte[] arrB)
*/
public static byte[] hexStr2ByteArr(String strIn) throws Exception {
byte[] arrB = strIn.getBytes();
int iLen = arrB.length;
// 两个字符表示一个字节,所以字节数组长度是字符串长度除以2
byte[] arrOut = new byte[iLen / 2];
for (int i = 0; i < iLen; i = i + 2) {
String strTmp = new String(arrB, i, 2);
arrOut[i / 2] = (byte) Integer.parseInt(strTmp, 16);
}
return arrOut;
}
/**
* 加密字节数组
*/
public byte[] encrypt(byte[] arrB) throws Exception {
return encryptCipher.doFinal(arrB);
}
/**
* 加密字符串
*/
public String encrypt(String strIn) throws Exception {
return byteArr2HexStr(encrypt(strIn.getBytes()));
}
/**
* 解密字节数组
*/
public byte[] decrypt(byte[] arrB) throws Exception {
return decryptCipher.doFinal(arrB);
}
/**
* 解密字符串
*/
public String decrypt(String strIn) throws Exception {
return new String(decrypt(hexStr2ByteArr(strIn)));
}
/**
* 从指定字符串生成密钥,密钥所需的字节数组长度为8位 不足8位时后面补0,超出8位只取前8位
*/
private Key getKey(byte[] arrBTmp) throws Exception {
// 创建一个空的8位字节数组(默认值为0)
byte[] arrB = new byte[8];
// 将原始字节数组转换为8位
for (int i = 0; i < arrBTmp.length && i < arrB.length; i++) {
arrB[i] = arrBTmp[i];
}
// 生成密钥
Key key = new javax.crypto.spec.SecretKeySpec(arrB, "DES");
return key;
}
}
4. HttpClientUtil(Http请求工具类)
这个工具类直接cv即可,主要用于向微信小程序开放接口发送网址请求
public class HttpClientUtil {
public static String doGet(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
String resultString = "";
CloseableHttpResponse response = null;
try {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doGet(String url) {
return doGet(url, null);
}
public static String doPost(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (param != null) {
List<NameValuePair> paramList = new ArrayList<>();
for (String key : param.keySet()) {
paramList.add(new BasicNameValuePair(key, param.get(key)));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doPost(String url) {
return doPost(url, null);
}
public static String doPostJson(String url, String json) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 向指定 URL 发送POST方法的请求
*/
public static String sendPost(String url, String paramUrl) {
PrintWriter out = null;
BufferedReader in = null;
String result = "";
try {
JSONObject param = new JSONObject(paramUrl);
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
// 设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
// 获取URLConnection对象对应的输出流
out = new PrintWriter(conn.getOutputStream());
// 发送请求参数
out.print(param);
// flush输出流的缓冲
out.flush();
// 定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
System.out.println("发送 POST 请求出现异常!" + e);
e.printStackTrace();
}
// 使用finally块来关闭输出流、输入流
finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
return result;
}
}
5、GetUserInfoUtil(获取用户信息工具类)
WX_LOGIN_APPID和WX_LOGIN_SECRET为微信小程序的账号(appid)和密码,前后端需保持一致,否则无法解析code。
public class GetUserInfoUtil {
// 请求的网址
public static final String WX_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session";
// appid
public static final String WX_LOGIN_APPID = ""; //自己的appid
// 密匙
public static final String WX_LOGIN_SECRET = ""; //自己的secret
// 固定参数
public static final String WX_LOGIN_GRANT_TYPE = "authorization_code";
//通过code换取微信小程序官网获取的信息
public static JSONObject getResultJson(String code){
//配置请求参数
Map<String,String> params = new HashMap<>();
params.put("appid", WX_LOGIN_APPID);
params.put("secret",WX_LOGIN_SECRET);
params.put("js_code",code);
params.put("grant_type",WX_LOGIN_GRANT_TYPE);
//向微信服务器发送请求
String wxRequestResult = HttpClientUtil.doGet(WX_LOGIN_URL,params);
JSONObject resultJson = JSONObject.fromObject(wxRequestResult);
return resultJson;
}
//获取openid
public static String getOpenid(String code){
return getResultJson(code).getString("openid");
}
}
4. interceptor包(拦截器包)
1. JwtInterceptor类(Jwt拦截器类)
这里的异常抛出也可以写在JwtUilt工具类中
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
//获取请求头token
String token = request.getHeader("Authorization");
try{
jwtUtil.verifyToken(token); //校验token
return true; //放行请求
}catch (ExpiredJwtException e){
e.printStackTrace();
throw new TokenException("token过期!");
}catch (MalformedJwtException e){
e.printStackTrace();
throw new TokenException("token格式错误!");
}catch (SignatureException e){
e.printStackTrace();
throw new TokenException("无效签名!");
}catch (IllegalArgumentException e){
e.printStackTrace();
throw new TokenException("非法请求!");
}catch (Exception e){
e.printStackTrace();
throw new TokenException("token无效!");
}
}
}
5. entity包(实体包)
注:先在数据库建出相应的表
1. Owner类
应该是User类,因为博主编写的时候考虑的是宠物主所以用的是Owner,可以根据自己业务需求修改用户实体
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("用户实体类")
public class Owner {
@ApiModelProperty("openid")
private String openid;
@ApiModelProperty("用户昵称")
private String nickname;
@ApiModelProperty("头像地址")
private String avatarUrl;
@ApiModelProperty("用户性别")
private String gender;
@ApiModelProperty("省份")
private String province;
@ApiModelProperty("城市")
private String city;
@ApiModelProperty("区")
private String district;
@ApiModelProperty("手机号")
private String phone;
@ApiModelProperty("用户实名")
private String name;
@ApiModelProperty("身份证号")
private String sfznum;
@ApiModelProperty("用户地址")
private String address;
@Override
public String toString(){
return "{" + nickname + "," + avatarUrl + "," + gender + "," + province + "," + city + "," +
phone + "," + name + "," + sfznum + "," + address + "}";
}
}
2. OwnerVo类(用于更新用户填写的信息)
Vo类用于前后端传递所需数据,因为实际应用中并不会用到数据库实体的所有字段
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("用户个人信息Vo类")
public class OwnerVo {
@ApiModelProperty("手机号")
private String phone;
@ApiModelProperty("用户实名")
private String name;
@ApiModelProperty("详细地址")
private String address;
}
6. mapper包(数据库访问包,也有人喜欢用Dao表示)
1. OwnerMapper接口(同样可以根据自己的业务逻辑自行编写)
@Mapper
@Repository
public interface OwnerMapper {
//新建用户
int insertOwner(String openid);
//登录时更新微信小程序获取的信息
int updateOwnerWxInfo(String openid, String nickname, String avatarUrl);
//后续用户写入个人信息后更新信息
int updateOwnerInfo(@Param("openid") String openid,
@Param("ownerVo") OwnerVo ownerVo);
//查询用户个人信息
Owner queryOwnerInfo(String openid);
}
OwnerMapper.xml(namespace应写自己的路径)
<?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.petsafety.mapper.user.OwnerMapper">
<insert id="insertOwner" parameterType="String">
insert into owner (openid)
values (#{openid})
</insert>
<update id="updateOwnerWxInfo">
update owner
set nickname = #{nickname}, avatar_url = #{avatarUrl}
where openid = #{openid}
</update>
<update id="updateOwnerInfo" >
update owner
set phone = #{ownerVo.phone}, name = #{ownerVo.name}, address = #{ownerVo.address}
where openid = #{openid}
</update>
<select id="queryOwnerInfo" parameterType="String" resultType="Owner">
select owner.openid ,owner.nickname, owner.avatar_url avatarUrl, owner.gender,
owner.province, owner.city, owner.phone, owner.name,
owner.sfznum, owner.address
from owner
where openid = #{openid}
</select>
</mapper>
7. service包(业务逻辑包)
1. OwnerService接口
public interface OwnerService {
//新建用户
int insertOwner(String openid);
//登录时插入微信小程序获取的信息
int updateOwnerWxInfo(String openid, String nickname, String avatarUrl);
//后续用户写入个人信息后更新信息
int updateOwnerInfo(String openid, OwnerVo ownerVo);
//查看用户个人信息
Owner queryOwnerInfo(String openid);
}
2. OwnerServiceImpl类
@Service("ownerService")
public class OwnerServiceImpl implements OwnerService{
@Autowired
OwnerMapper ownerMapper;
//新建用户
@Override
public int insertOwner(String openid){return ownerMapper.insertOwner(openid);}
//登录时插入微信小程序获取的信息
@Override
public int updateOwnerWxInfo(String openid, String nickname, String avatarUrl){
return ownerMapper.updateOwnerWxInfo(openid, nickname, avatarUrl);
}
//后续用户写入个人信息时更新信息
@Override
public int updateOwnerInfo(String openid, OwnerVo ownerVo){
return ownerMapper.updateOwnerInfo(openid, ownerVo);
}
//查看用户个人信息
@Override
public Owner queryOwnerInfo(String openid){ return ownerMapper.queryOwnerInfo(openid);}
}
8. controller包(控制器包)
1. WxLoginController类
@Api(tags = "WxLoginController")
@RestController
@RequestMapping("/user")
public class WxLoginController {
@Autowired
private OwnerService ownerService;
@Autowired
JwtUtil jwtUtil;
@ApiOperation("微信授权登录")
@PostMapping("/index/login")
public Result authorizeLogin(@NotBlank @RequestParam("code") String code) {
//通过code换取信息
JSONObject resultJson = GetUserInfoUtil.getResultJson(code);
if (resultJson.has("openid")){
//获取sessionKey和openId
String sessionKey = resultJson.get("session_key").toString();
String openid = resultJson.get("openid").toString();
//生成自定义登录态session
String session = null;
Map<String,String> sessionMap = new HashMap<>();
sessionMap.put("sessionKey",sessionKey);
sessionMap.put("openid",openid);
session = JSONObject.fromObject(sessionMap).toString();
//加密session
try {
EncryptUtil encryptUtil = new EncryptUtil();
session = encryptUtil.encrypt(session);
} catch (Exception e) {
e.printStackTrace();
}
//生成token
String token = jwtUtil.getToken(session);
Map<String,String> result = new HashMap<>();
result.put("token", token);
//查询用户是否存在
Owner owner = ownerService.queryOwnerInfo(openid);
if (owner != null){
return Result.succ("登录成功", result); //用户存在,返回结果
}else { //用户不存在,新建用户信息
int rs = ownerService.insertOwner(openid);
if (rs <= 0){
return Result.fail("登录失败");
}
return Result.succ("登录成功", result);
}
}
return Result.fail("授权失败"+ resultJson.getString("errmsg"));
}
@ApiOperation("存储用户个人信息")
@PostMapping("/index/person-info")
public Result insertPersonInfo(@RequestParam("nickname") String nickname,
@RequestParam("avatarUrl") String avatarUrl,
HttpServletRequest request){
//获取请求头token
String token = request.getHeader("Authorization");
//从token中获取openid
String openid = jwtUtil.getOpenidFromToken(token);
int result = ownerService.updateOwnerWxInfo(openid, nickname, avatarUrl);
if(result <= 0){
return Result.fail("更新失败!");
}
return Result.succ("更新成功!", null);
}
}
2. OwnerController类
@Api(tags = "OwnerController")
@RestController
@RequestMapping("/user/person-info")
public class OwnerController {
@Autowired
OwnerServiceImpl ownerService;
@Autowired
JwtUtil jwtUtil;
@PutMapping("/update-person-info")
@ApiOperation("修改用户个人信息")
public Result updateOwnerInfo(HttpServletRequest request,
@RequestBody OwnerVo ownerVo){
//获取请求头token
String token = request.getHeader("Authorization");
//获取openid
String openid = jwtUtil.getOpenidFromToken(token);
int result = ownerService.updateOwnerInfo(openid, ownerVo);
if (result <= 0){
return Result.fail("修改失败",null);
}
return Result.succ("修改成功",null);
}
}
2、前端代码
注:本文注重登录逻辑,界面设计读者应自行编写
1、app.js(小程序初始化)
App({
/**
* 当小程序初始化完成时,会触发 onLaunch(全局只触发一次)
*/
onLaunch: function () {
// 静默登录
wx.login({
success(res) {
wx.request({ // 调用登录接口,获取用户登录凭证token
url: 'http://localhost:8888/user/index/login',
method: 'POST',
header: {
'Content-Type': "application/x-www-form-urlencoded",
},
data: {
code: res.code,
},
success(res) { // 接口调用成功,获取token并缓存
console.log(res);
wx.setStorageSync('token', res.data.data.token); // 将token进行缓存到'token'字段
},
fail() {
console.log("登录出现错误!");
}
})
}
});
},
})
2、pages/mine(登录界面)
mine.js
const app = getApp()
Page({
/**
* 页面的初始数据
*/
data: {
imgSrc: 'http://scau-pet.oss-cn-guangzhou.aliyuncs.com/picture/2021/09/22/af8d477e21f32347.png', //初始图片
imgSrc2: wx.getStorageSync('avatarUrl'), //授权后显示用户头像
username: '请登录>', //初始文字
username2: wx.getStorageSync('nickName'), //授权后显示用户名
},
login() {
let that = this;
wx.getUserProfile({
desc: '用于完善用户资料', //声明获取用户信息的用途
success(res) {
console.log(res);
that.setData({
imgSrc: res.userInfo.avatarUrl,
username: res.userInfo.nickName,
});
wx.setStorageSync('avatarUrl', res.userInfo.avatarUrl);
wx.setStorageSync('nickname', res.userInfo.nickName);
wx.showLoading({
title: '正在登录...',
})
wx.request({
url: 'http://localhost:8888/user/index/person-info',
method: 'POST',
header: {
'Authorization': wx.getStorageSync('token'),
'Content-Type': "application/x-www-form-urlencoded",
},
data: {
nickname: wx.getStorageSync('nickname'),
avatarUrl: wx.getStorageSync('avatarUrl'),
},
success(res) {
console.log(res);
wx.hideLoading();
},
})
},
fail() {
wx.showModal({
title: '警告通知',
content: '您点击了拒绝授权,将无法正常显示个人信息,点击确定重新获取授权。',
});
}
})
},
})
3. pages/index(界面跳转)
与传统账号密码登录不同,用户静默登陆后一定会携带有token,因此不能用是否存在token来判断用户是否已登录,可以根据缓存是否存在用户头像昵称来判断
index.js
var app = getApp();
Page({
// 跳转到登录界面
gotoMine() {
wx.showModal({
title: '提示',
content: '请您先登录!',
showCancel: false,
success: (res) => {
wx.switchTab({
url: '/pages/mine/mine',
});
},
fail: (res) => {
console.log("弹窗出现错误!");
},
});
},
// 跳转到操作界面
gotoOperate() {
let that = this;
if (wx.getStorageSync('nickname') != "" && wx.getStorageSync('avatarUrl') != "") { // 如果本地缓存存在用户信息,则说明用户已授权登录,如果不存在则弹窗提示登陆并跳转到登录界面
wx.navigateTo({
url: '', //跳转到操作界面
})
} else {
that.gotoMine(); // 跳转到登录页面
}
},
})
4. page/myinfo(填写个人信息)
最好有一个全局捕获的工具类,避免每次请求都需要验证一次,另外token失效后需要清空缓存
myinfo.js
const app = getApp();
Page({
/**
* 页面的初始数据
*/
data: {
name_vc: '', // 用户姓名
phone_vc: '', // 手机号码
address_vc: '', // 用户住址
},
// 报错函数
showModal(error) {
wx.showModal({
content: error.msg,
showCancel: false,
})
},
/**
* 表单提交
*/
saveVcData(e) {
let that = this;
let values = e.detail.value;
console.log("form发生了submit事件,携带的数据为:", e.detail.value);
const params = e.detail.value;
// 按钮禁用
that.setData({
diabled: true,
});
// 提交到后端
wx.request({
method: "PUT",
url: 'http://localhost:8888/user/person-info/update-person-info',
header: {
'content-type': 'application/json',
['Authorization']: wx.getStorageSync('token'),
},
dataType: 'json',
data: JSON.stringify({
name: e.detail.value.name,
phone: e.detail.value.phone,
address: e.detail.value.address,
}),
success: (res) => {
console.log(res);
if(res.data.code === 401){
wx.clearStorageSync(), //清空缓存
wx.showToast({
title: '登录已失效,请重新登录!',
icon: 'none',
duration: 2000,
})
}else{
wx.showModal({
content: "提交成功",
showCancel: false,
success (res) {
if (res.confirm) {
// 清空表单内容
that.setData({
name_vc: '', // 用户姓名
phone_vc: '', // 手机号码
address_vc: '', // 用户住址
})
}
}
})
}
},
fail: (res) => {
wx.showToast({
title: '提交失败,请检查网络!',
icon: 'none',
duration: 2000,
})
}
})
},
})
四、测试
1、静默登录
即用户进入小程序时,调用"http://localhost:8888/user/index/login"接口
2、用户未登录
点击确定跳转至登录页
3、授权登录
点击拒绝
点击允许
授权登录成功!
4、更新信息
token未失效
token失效(一般为token过期)
附:一个常见报错
这个报错就是上文提到的前后端appid和serct不一致造成的,而且前端不可以直接在界面修改,需要新建一个小程序使用正确的appid和serct(好狗的小程序)。
更多推荐
所有评论(0)