一、前言

首先说一下微信小程序最近两个比较大的变动:

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(好狗的小程序)。

 

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐