若依实现自建用户表下小程序、app端的登录认证
一、后端实现
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 状态和本地存储保持同步
遵循正确的初始化顺序
这些修改确保了:
应用启动时正确加载登录状态
页面切换时正确检查登录状态
避免重复的登录提示
提供更好的用户体验