一、后端实现

1.问题描述

当我们使用系统之外的用户表,就无法进行一个登录校验,这时候用两种方法实现

2.方法一--自定义用户登录

比较麻烦这里给一个参考文章
若依实现微信小程序登录&APP登录(新用户表,新Token认证,与原有后台管理登录隔离)_若依小程序登录-CSDN博客

3.方法二--去适配若依本身系统

只是想要能够登录,能够获取token信息,能够获取到token对应的用户信息而已,有必要那么复杂吗?直接用若依的那一套流程生成token不就行了,让我们先来看一下若依底层实现登录生成token的源码

原理刨析

看一下pc后端的控制层,这里调了loginService的login方法

    /**
     * 登录方法
     * 
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

让我们看看loginService的login方法写了什么

/**
     * 登录验证
     * 
     * @param username 用户名
     * @param password 密码
     * @param code 验证码
     * @param uuid 唯一标识
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid)
    {
        // 验证码校验
        validateCaptcha(username, code, uuid);
        // 登录前置校验
        loginPreCheck(username, password);
        // 用户验证
        Authentication authentication = null;
        try
        {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            AuthenticationContextHolder.setContext(authenticationToken);
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(authenticationToken);
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        }
        finally
        {
            AuthenticationContextHolder.clearContext();
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        recordLoginInfo(loginUser.getUserId());
        // 生成token
        return tokenService.createToken(loginUser);
    }

ok找到了,tokenService.createToken(loginUser);便是若依生成token的方法

思路:我们参考这一套流程我们关键点是不是就是调用tokenService.createToken(loginUser)去生成token,这里需要一个loginUser,我们直接去构造一个是不是就行了

实现app的登录(用户名+密码)

1.创建AppLoginService服务层去伪造loginUser 来适配若依系统、

package com.ruoyi.service;


import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.entity.user.User;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.service.IUserService;
import com.ruoyi.system.service.ISysConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;

/**
 * APP登录服务
 */
@Service
public class AppLoginService {
    @Autowired
    private TokenService tokenService;

    @Autowired
    private IUserService userService;

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private ISysConfigService configService;

    @Resource
    private AuthenticationManager authenticationManager;

    /**
     * APP登录验证
     */
    public String login(String username, String password, String code, String uuid) {
        // 登录前置校验
        loginPreCheck(username, password);

        // 用户验证
        User user = userService.getByName(username);
        if (user == null) {
            throw new ServiceException("用户不存在");
        }

        if (user.getStatus() != 1) {
            throw new ServiceException("账号已被禁用");
        }

        if (!SecurityUtils.matchesPassword(password, user.getPassword())) {
            throw new ServiceException("密码错误");
        }

        // 创建LoginUser对象
        LoginUser loginUser = createLoginUser(user);

        // 生成token
        return tokenService.createToken(loginUser);
    }
    /**
     * 登录前置校验
     */
    private void loginPreCheck(String username, String password) {
        // 用户名或密码为空 错误
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            throw new ServiceException("用户名或密码不能为空");
        }
        // 密码如果不在指定范围内 错误
        if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
            throw new ServiceException("密码长度必须在5-20个字符之间");
        }
        // 用户名不在指定范围内 错误
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
                || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
            throw new ServiceException("用户名长度必须在2-20个字符之间");
        }
//        // IP黑名单校验
//        String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
//        if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) {
//            throw new ServiceException("很抱歉,您的IP已被列入系统黑名单");
//        }
    }

    /**
     * 创建LoginUser对象
     */
    private LoginUser createLoginUser(User user) {
        LoginUser loginUser = new LoginUser();
        loginUser.setUserId(user.getId());

        // 创建SysUser对象(用于适配系统)
        SysUser sysUser = new SysUser();
        sysUser.setUserId(user.getId());
        sysUser.setUserName(user.getUsername());
        sysUser.setPhonenumber(user.getPhone());
        sysUser.setStatus("0"); // 正常状态

        loginUser.setUser(sysUser);
//        // 设置权限信息
//        Set<String> permissions = new HashSet<>();
//        permissions.add("app:user"); // 设置APP用户基础权限
//        loginUser.setPermissions(permissions);

        return loginUser;
    }
}

整合Token机制

  • 使用 TokenService 生成标准的token

  • 关键适配:将自定义User对象适配到LoginUser

业务处理 -> 创建LoginUser -> 调用TokenService -> 返回Token

2.创建一个AppLoginController类

package com.ruoyi.controller;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.entity.user.User;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.service.AppLoginService;
import com.ruoyi.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * app的登录
 *
 * @author adi
 * #Date: 2025/3/5 16:53
 */
@RestController
@RequestMapping("/app")
public class AppLoginController extends BaseController {
    @Autowired
    private AppLoginService appLoginService;

    @Autowired
    private IUserService userService;
    @PostMapping("/login")
    public AjaxResult appLogin(@RequestBody LoginBody loginBody) {
        // 参数校验
        if (StringUtils.isEmpty(loginBody.getUsername()) || StringUtils.isEmpty(loginBody.getPassword())) {
            return AjaxResult.error("用户名或密码不能为空");
        }

        try {
            // 生成令牌
            String token = appLoginService.login(loginBody.getUsername(),
                    loginBody.getPassword(),
                    loginBody.getCode(),
                    loginBody.getUuid());

            // 获取用户信息
            User user = userService.getByName(loginBody.getUsername());

            // 构建返回数据
            Map<String, Object> data = new HashMap<>();
            data.put(Constants.TOKEN, token);
            data.put("userId", user.getId());
            data.put("username", user.getUsername());
            data.put("phone", user.getPhone());
            // 可以根据需要添加更多用户信息

            return AjaxResult.success("登录成功", data);
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }
//    /**
//     * 退出登录
//     *
//     * @return 结果
//     */
//    @PostMapping("/logout")
//    public AjaxResult logout() {
//        try {
//            // 获取当前登录用户token
//            String token = getToken();
//            if (StringUtils.isNotEmpty(token)) {
//                // 删除用户缓存记录
//                appLoginService.logout(token);
//            }
//            return AjaxResult.success("退出成功");
//        } catch (Exception e) {
//            return AjaxResult.error(e.getMessage());
//        }
//    }

}

这样我们就完成了app后端的登录验证,这样拿到的token和若依系统一致,后续解析验证就不会出错

实现小程序的登录(code+phoneCode)

1.服务层

package com.ruoyi.uniapp.service.impl;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.entity.user.User;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.service.IUserService;
import com.ruoyi.uniapp.service.AuthService;
import lombok.Data;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Security;
import java.time.LocalDateTime;
import java.util.*;

/**
 * 微信小程序登录服务实现类
 */
@Service
public class AuthServiceImpl implements AuthService {
    @Autowired
    private TokenService tokenService;

    @Autowired
    private IUserService userService;

    private static final String APP_ID = "换成自己的";
    private static final String APP_SECRET = "换成自己的";
    private static final String WX_URL = "https://api.weixin.qq.com/sns/jscode2session";

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 处理微信小程序登录
     * @return Map 包含token和手机号等用户信息
     */
    @Override
    public Map<String, Object> login(String code, String encryptedData, String iv) throws Exception {
        RestTemplate restTemplate = createRestTemplate();
        ObjectMapper objectMapper = new ObjectMapper();

        // 1. 获取session_key和openid
        String sessionUrl = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
                WX_URL, APP_ID, APP_SECRET, code);

        JsonNode sessionResponse = restTemplate.getForObject(sessionUrl, JsonNode.class);

        if (sessionResponse == null || !sessionResponse.has("openid")) {
            throw new Exception("获取openid失败");
        }

        String openid = sessionResponse.get("openid").asText();
        String sessionKey = sessionResponse.get("session_key").asText();

        // 2. 解密手机号
        String phoneInfo = decryptPhoneNumber(sessionKey, encryptedData, iv);
        JsonNode phoneData = objectMapper.readTree(phoneInfo);
        String phoneNumber = phoneData.get("phoneNumber").asText();

        // 3. 查询或创建用户
        User user = userService.getByPhone(phoneNumber);
        if (user == null) {
            // 创建新用户
            user = new User();
            user.setPhone(phoneNumber);
            user.setUsername(phoneNumber); // 使用手机号作为用户名
            user.setPassword(SecurityUtils.encryptPassword(openid)); // 使用openid作为初始密码
            user.setStatus(1); // 正常状态
            user.setCreateTime(new Date());
            userService.add(user);
        }

        // 4. 创建LoginUser对象(需要适配您的User对象)
        LoginUser loginUser = createLoginUser(user);

        // 5. 生成token
        String token = tokenService.createToken(loginUser);

        // 6. 构建返回数据
        Map<String, Object> userData = new HashMap<>();
        userData.put("token", token);
        userData.put("phoneNumber", phoneNumber);
        userData.put("openid", openid);
        userData.put("userId", user.getId());

        return userData;
    }
    /**
     * 创建LoginUser对象
     * 将User对象转换为系统需要的LoginUser对象
     */
    private LoginUser createLoginUser(User user) {
        LoginUser loginUser = new LoginUser();
        loginUser.setUserId(user.getId());

        // 创建SysUser对象(用于适配系统)
        SysUser sysUser = new SysUser();
        sysUser.setUserId(user.getId());
        sysUser.setUserName(user.getUsername());
        sysUser.setPhonenumber(user.getPhone());
        // 设置其他必要的用户信息

        loginUser.setUser(sysUser);
        // 如果需要设置权限信息
        // loginUser.setPermissions(Arrays.asList("common:user:query"));

        return loginUser;
    }
    /**
     * 解密手机号数据
     * @param sessionKey 会话密钥
     * @param encryptedData 加密数据
     * @param iv 初始向量
     * @return 解密后的手机号JSON字符串
     */
    private String decryptPhoneNumber(String sessionKey, String encryptedData, String iv) throws Exception {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(sessionKey);
            byte[] ivBytes = Base64.getDecoder().decode(iv);
            byte[] dataBytes = Base64.getDecoder().decode(encryptedData);

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            byte[] decrypted = cipher.doFinal(dataBytes);

            return new String(decrypted, "UTF-8");
        } catch (Exception e) {
            throw new Exception("手机号解密失败: " + e.getMessage());
        }
    }

    /**
     * 创建配置好的RestTemplate实例
     * 用于处理微信API的HTTP请求
     * @return RestTemplate实例
     */
    private RestTemplate createRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        // 添加消息转换器,支持多种响应格式
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setSupportedMediaTypes(Arrays.asList(
                MediaType.APPLICATION_JSON,
                MediaType.APPLICATION_OCTET_STREAM,
                MediaType.TEXT_PLAIN,
                MediaType.ALL
        ));
        restTemplate.getMessageConverters().add(converter);
        return restTemplate;
    }

} 

2.控制层

package com.ruoyi.uniapp.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.uniapp.service.AuthService;

import java.util.HashMap;
import java.util.Map;

/**
 * 微信小程序登录控制器
 *
 */
@RestController
@RequestMapping("/weixin/auth")
public class AuthController {

    @Autowired
    private AuthService authService;

    /**
     * 处理微信小程序登录请求
     * @param requestBody 请求体,包含code和phoneCode
     *        code: 微信登录临时凭证
     *        phoneCode: 手机号获取凭证
     * @return ResponseEntity 返回登录结果
     *         成功返回格式:{"code":200, "message":"登录成功", "data":{phoneNumber:"xxx",openid:"xxx","token":"xxx"}}
     *         失败返回格式:{"code":400, "message":"错误信息", "data":{}}
     */
    @PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> requestBody) {
        String code = requestBody.get("code");
        String encryptedData = requestBody.get("encryptedData");
        String iv = requestBody.get("iv");
        Map<String, Object> response = new HashMap<>();

        try {
            if (code == null || encryptedData == null || iv == null) {
                throw new Exception("缺少必要的参数");
            }

            // 调用服务层处理登录逻辑
            Map<String, Object> userData = authService.login(code, encryptedData, iv);

            // 构建成功响应
            response.put("code", 200);
            response.put("message", "登录成功");
            response.put("data", userData);

            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(response);
        } catch (Exception e) {
            response.put("code", 400);
            response.put("message", e.getMessage() != null ? e.getMessage() : "登录失败");
            response.put("data", new HashMap<>());

            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(response);
        }
    }
}

和app登录的思路是一样的,都是将自己的用户表适配成LoginUser,然后再调用若依的生成token方法生成token,只是多了一些获取openid这种小程序特有的流程

放行白名单SecurityConfig

二、前端实现

1.状态管理vuex

(一开始用的pinia,后面和vue版本兼容有点问题就换vuex了),创建stores/user.js存储用户信息

"use strict";
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        token: "",
        userInfo: {
            nickName: "",
            avatarUrl: "",
            phoneNumber: ""
        }
    },
    
    mutations: {
        INIT_STATE(state) {
            // 从本地存储初始化状态
            state.token = uni.getStorageSync("token") || ""
            state.userInfo = uni.getStorageSync("userInfo") || {
                nickName: "",
                avatarUrl: "",
                phoneNumber: ""
            }
        },

        SET_TOKEN(state, token) {
            state.token = token
            uni.setStorageSync("token", token)
        },
        
        SET_USER_INFO(state, userInfo) {
            state.userInfo = {
                ...userInfo,
                nickName: userInfo.nickName || "",
                avatarUrl: userInfo.avatarUrl || "/static/images/locate.png",
                phoneNumber: userInfo.phoneNumber || ""
            }
            uni.setStorageSync("userInfo", state.userInfo)
        },
        
        LOGOUT(state) {
            state.token = ""
            state.userInfo = {
                nickName: "",
                avatarUrl: "",
                phoneNumber: ""
            }
            uni.clearStorageSync()
        }
    },
    
    actions: {
        // 初始化状态
        initState({ commit }) {
            commit('INIT_STATE')
        },

        setToken({ commit }, token) {
            commit('SET_TOKEN', token)
        },
        
        setUserInfo({ commit }, userInfo) {
            commit('SET_USER_INFO', userInfo)
        },
        
        logout({ commit }) {
            commit('LOGOUT')
            // 确保清除所有相关的存储
            uni.clearStorageSync()
        }
    },

    getters: {
        isAuthenticated: state => !!state.token,
        userInfo: state => state.userInfo
    }
})

2.封装auth工具管理token


const TokenKey = 'Admin-Token'

export function getToken() {
    return uni.getStorageSync(TokenKey)
}

export function setToken(token) {
    return uni.setStorageSync(TokenKey, token)
}

export function removeToken() {
    return uni.removeStorageSync(TokenKey)
}

3..登录页面的实现

小程序登录页面 (pages/login/mp-login.vue):

<template>
    <view class="login-container">
      <view class="login-header">
        <image class="logo" src="/static/images/bubble.png" mode="aspectFit"></image>
        <text class="title">欢迎使用</text>
      </view>
  
      <view class="login-body">
        <button
            class="wx-login-btn"
            open-type="getPhoneNumber"
            @getphonenumber="handleGetPhoneNumber"
            :loading="loading"
        >
          微信一键登录
        </button>
  
        <view class="privacy-policy">
          登录即代表您已同意
          <text class="link" @tap="showPrivacyPolicy">《用户协议和隐私政策》</text>
        </view>
      </view>
    </view>
  </template>
  
  <script>
import { wxLogin } from '@/api/login'
import { mapState, mapActions } from 'vuex'

export default {
    data() {
        return {
            loading: false
        }
    },

    computed: {
        ...mapState(['token', 'userInfo'])
    },

    created() {
        if (this.token) {
            uni.switchTab({
                url: '/pages/startIndex/index'
            })
        }
    },

    methods: {
        ...mapActions(['setToken', 'setUserInfo']),

        /**
         * 处理获取手机号回调
         * @param {Object} e - 回调事件对象
         */
        async handleGetPhoneNumber(e) {
            // 首先检查是否获取到手机号码
            if (e.detail.errMsg !== "getPhoneNumber:ok") {
                uni.showToast({
                    title: '获取手机号失败,请重试',
                    icon: 'none'
                })
                return
            }

            try {
                this.loading = true
                console.log('开始登录流程...')

                // 1. 调用微信官方login获取登录code
                const loginResult = await uni.login({
                    provider: 'weixin'
                })

                const code = loginResult.code
                console.log('获取到登录code:', code)

                // 2. 调用登录接口
                const loginRes = await wxLogin({
                    code,
                    phoneCode: e.detail.code,
                    encryptedData: e.detail.encryptedData,
                    iv: e.detail.iv
                })

                console.log('登录返回数据:', loginRes)

                if (loginRes.code === 200 && loginRes.data) {
                    const phoneNumber = loginRes.data.phoneNumber || loginRes.data.phone
                    const userInfo = {
                        nickName: phoneNumber ? `用户${phoneNumber.slice(-4)}` : '微信用户',
                        avatarUrl: '/static/images/locate.png',
                        phoneNumber: phoneNumber
                    }

                    await this.setToken(loginRes.data.token)
                    await this.setUserInfo(userInfo)

                    uni.showToast({ 
                        title: '登录成功', 
                        icon: 'success',
                        duration: 1500
                    })

                    setTimeout(() => {
                        uni.reLaunch({
                            url: '/pages/startIndex/index'
                        })
                    }, 1500)
                } else {
                    throw new Error(loginRes.message || '登录失败')
                }
            } catch (error) {
                console.error('登录失败:', error)
                uni.showToast({
                    title: error.message || '登录失败,请重试',
                    icon: 'none',
                    duration: 2000
                })
            } finally {
                this.loading = false
            }
        },

        // 显示隐私政策
        showPrivacyPolicy() {
            uni.navigateTo({
                url: '/pages/privacy/privacy',
                fail: () => {
                    uni.showToast({
                        title: '隐私政策页面不存在',
                        icon: 'none'
                    })
                }
            })
        }
    }
}
</script>
  
<style lang="scss" scoped>
.login-container {
  min-height: 100vh;
  padding: 60rpx 40rpx;
  background-color: #ffffff;

  .login-header {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 100rpx;

    .logo {
      width: 160rpx;
      height: 160rpx;
      margin-bottom: 30rpx;
    }

    .title {
      font-size: 36rpx;
      font-weight: bold;
      color: #333333;
    }
  }

  .login-body {
    margin-top: 100rpx;

    .wx-login-btn {
      width: 100%;
      height: 88rpx;
      line-height: 88rpx;
      background: #07c160;
      color: #ffffff;
      font-size: 32rpx;
      border-radius: 44rpx;

      &::after {
        border: none;
      }
    }

    .privacy-policy {
      margin-top: 30rpx;
      text-align: center;
      font-size: 24rpx;
      color: #999999;

      .link {
        color: #07c160;
      }
    }
  }
}
</style>
  

app登录页面app-login.vue

<template>
    <view class="login-container">
        <u-form :model="loginForm" ref="loginForm">
            <u-form-item label="账号">
                <u-input
                    v-model="loginForm.username"
                    placeholder="请输入账号"
                    :border="true"
                />
            </u-form-item>
            <u-form-item label="密码">
                <u-input
                    v-model="loginForm.password"
                    type="password"
                    placeholder="请输入密码"
                    :border="true"
                />
            </u-form-item>
        </u-form>
        
        <view class="btn-group">
            <u-button type="primary" @click="handleLogin" :loading="loading">
                登录
            </u-button>
        </view>
    </view>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { appLogin } from '@/api/login'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const loading = ref(false)

const loginForm = reactive({
    username: '',
    password: '',
    code: '',
    uuid: ''
})

const handleLogin = async () => {
    // 表单验证
    if (!loginForm.username || !loginForm.password) {
        uni.showToast({
            title: '账号和密码不能为空',
            icon: 'none'
        })
        return
    }
    
    loading.value = true
    try {
        const res = await appLogin(loginForm)
        
        // 保存token到store
        userStore.setToken(res.data.token)
        
        // 保存用户信息到store
        userStore.setUserInfo({
            nickName: res.data.username,
            phoneNumber: res.data.phone,
            // 如果后端返回了头像地址,也可以保存
            avatarUrl: res.data.avatar || '/static/images/locate.png',
            // 其他用户信息
            userId: res.data.userId,
            ...res.data
        })
        
        // 显示登录成功提示
        uni.showToast({
            title: '登录成功',
            icon: 'success',
            duration: 1500
        })
        
        // 延迟跳转,让用户看到成功提示
        setTimeout(() => {
            // 跳转到首页
            uni.switchTab({
                url: '/pages/startIndex/index'
            })
        }, 1500)
        
    } catch (error) {
        console.error('登录失败:', error)
        uni.showToast({
            title: error.message || '登录失败,请重试',
            icon: 'none'
        })
    } finally {
        loading.value = false
    }
}
</script>

<style lang="scss" scoped>
.login-container {
    padding: 40rpx;
    
    .btn-group {
        margin-top: 50rpx;
    }
    
    :deep(.u-form-item) {
        margin-bottom: 20rpx;
    }
    
    :deep(.u-input) {
        height: 80rpx;
        
        &__input {
            height: 80rpx;
            line-height: 80rpx;
            font-size: 28rpx;
        }
    }
    
    .u-button {
        height: 80rpx;
        line-height: 80rpx;
        font-size: 32rpx;
    }
}
</style>

4.app.vue

1.通过注解控制不同平台的登陆页面

设置// #ifdef APP-PLUS // #endif // #ifdef MP-WEIXIN// #endif来控制

2.在onLaunch初始化user的store

<script>
	import { getToken } from '@/utils/auth.js'

	export default {
		onLaunch: function() {
			// 初始化 store 状态
			this.$store.dispatch('initState')
			console.log('App Launch')
		},
		onShow: function() {
			console.log('App Show')
		},
		onHide: function() {
			console.log('App Hide')
		},
		methods: {
			checkLogin: function() {
				var token = getToken()
				if (!token) {
					// #ifdef APP-PLUS
					uni.redirectTo({
						url: '/pages/login/app-login'
					})
					// #endif
					
					// #ifdef MP-WEIXIN
					uni.redirectTo({
						url: '/pages/login/mp-login'
					})
					// #endif
				}
			}
		}
	}
</script>

<style>
	/*每个页面公共css */
	@import url("css/common.css");
	@import url("node_modules/@dcloudio/uni-ui/lib/uni-icons/uniicons.css");
</style>

5.封装请求工具utils/request.js

这块可以直接去看pc端的request.js看请求头格式,也可以取浏览器网络查看

1.统一封装请求头之类的

2.加了拦截器来控制是否需要登录

// utils/request.js
import { getToken } from '@/utils/auth'
import store from '../stores/user'

// // 根据环境获取基础URL
// const getBaseUrl = () => {
//     return process.env.NODE_ENV === 'development' 
//         ? 'http://localhost:8080' 
//         : 'https://your-production-domain.com'
// }
const getBaseUrl = () => {
    return 'http://localhost:9996'
}

// 创建请求拦截器
const requestInterceptor = (config) => {
    // 获取 token,优先从 store 获取,如果没有则从本地存储获取
    const token = store.state.token || getToken()
    if (token) {
        // 将 token 添加到请求头
        config.header = {
            ...config.header,
            'Authorization': `Bearer ${token}`
        }
    }
    return config
}

// 创建响应拦截器
const responseInterceptor = (response) => {
    // 如果是登录接口,直接返回响应
    if (response.config && response.config.url.includes('/login')) {
        return response.data
    }

    // 处理其他接口的响应
    if (response.data.code === 401) {
        store.dispatch('logout')
        uni.reLaunch({
            url: '/pages/login/mp-login'
        })
        return Promise.reject(new Error('无效的会话,或者会话已过期,请重新登录。'))
    }
    return response.data
}

const request = (options = {}) => {
    // 处理请求URL
    options.url = `${getBaseUrl()}${options.url}`
    
    // 处理请求头
    options.header = {
        'Content-Type': 'application/json',
        ...options.header
    }
    
    // 应用请求拦截器
    options = requestInterceptor(options)

    return new Promise((resolve, reject) => {
        uni.request({
            ...options,
            success: (res) => {
                try {
                    // 保存当前请求的配置到响应对象中
                    res.config = options
                    // 应用响应拦截器
                    const result = responseInterceptor(res)
                    resolve(result)
                } catch (error) {
                    reject(error)
                }
            },
            fail: (error) => {
                console.log(error)
                let { message } = error
                if (message === 'Network Error') {
                    message = '后端接口连接异常'
                } else if (message.includes('timeout')) {
                    message = '系统接口请求超时'
                } else if (message.includes('Request failed with status code')) {
                    message = '系统接口' + message.substr(message.length - 3) + '异常'
                }
                uni.showToast({
                    title: message,
                    icon: 'none',
                    duration: 2000
                })
                reject(error)
            }
        })
    })
}

export default request

6.登录的一些bug解决总结

例如我storage里面有token也反复登录

解决:reques.js// 获取 token,优先从 store 获取,如果没有则从本地存储获取

const token = store.state.token || getToken())

例如我清除了storage的token不再显示登陆页面

解决:确保登出的时候user状态容器清除了token

logout({ commit }) {
            commit('LOGOUT')
            // 确保清除所有相关的存储
            uni.clearStorageSync()
        }

状态初始化时序问题

  • 原因:store 中的状态没有在应用启动时立即从本地存储初始化

  • 影响:即使本地存储中有 token,store 中的状态也是空的,导致判断为未登录

解决:添加 INIT_STATE mutation 和 initState action,在 App.vue 的 onLaunch 中初始化
生命周期选择问题

  • 原因:使用 onLoad 只在页面加载时检查一次登录状态

  • 影响:如果 store 状态在页面加载后才初始化完成,登录状态检查就会失效

  • 解决:改用 onShow,确保每次页面显示时都检查最新的登录状态

页面检查登录生命周期问题

  • 在页面中使用 onShow 而不是 onLoad 来检查登录状态,因为 onShow 会在每次页面显示时触发

错误提示问题

// 优化前
catch (error) {
    console.error('加载失败:', error);
    uni.showToast({
        title: error.message,
        icon: 'none'
    });
}

// 优化后
catch (error) {
    console.error('加载失败:', error);
    if (error.message.includes('无效的会话')) {
        // 登录失效不显示提示,避免重复
        return;
    }
    uni.showToast({
        title: error.message,
        icon: 'none'
    });
}

状态同步问题

// store/user.js
mutations: {
    INIT_STATE(state) {
        // 确保 store 和本地存储同步
        state.token = uni.getStorageSync("token") || ""
        state.userInfo = uni.getStorageSync("userInfo") || {
            nickName: "",
            avatarUrl: "",
            phoneNumber: ""
        }
    }
}

正确的初始化顺序

// App.vue
onLaunch: function() {
    // 1. 先初始化 store 状态
    this.$store.dispatch('initState')
    // 2. 再执行其他初始化逻辑
    console.log('App Launch')
}

总结:

1. 状态管理需要在应用启动时就完成初始化

  • 使用合适的生命周期钩子检查登录状态

  • 避免重复的错误提示

  • 确保 store 状态和本地存储保持同步

  • 遵循正确的初始化顺序

这些修改确保了:

  • 应用启动时正确加载登录状态

  • 页面切换时正确检查登录状态

  • 避免重复的登录提示

  • 提供更好的用户体验