1. 项目概述
在基于若依框架开发的企业级应用中,经常会遇到需要扩展会员系统的场景。不同于后台管理员的权限体系,会员系统通常需要独立的登录入口、用户数据表和权限控制机制。本文将详细介绍如何在若依框架中实现一套完整的会员登录系统,同时保持与原有管理员系统的隔离性。
作为一款流行的前后端分离权限管理系统,若依(RuoYi)本身提供了完善的用户认证和权限控制功能。但当我们需要为普通用户提供会员服务时,直接使用原有的sys_user表和管理员登录接口显然不合适。这会导致权限混淆、数据混杂等问题。
我在多个实际项目中实施会员系统扩展时,总结出了一套既保持若依原有架构优势,又能实现会员系统独立运行的方案。核心思路是"隔离用户体系,复用认证机制"——将会员数据与管理员数据完全分离,同时充分利用若依已有的token生成、密码加密等基础设施。
2. 核心设计思路
2.1 模块化设计原则
若依框架本身采用模块化设计,这为我们扩展会员系统提供了良好的基础。按照若依的最佳实践,新增功能应该以独立模块的形式存在,而不是直接修改原有系统模块。
对于会员系统,我建议创建专门的ruoyi-member模块,包含以下核心组件:
- domain:会员实体类
- mapper:数据库操作接口
- service:业务逻辑实现
- controller:对外暴露的API接口
这种设计有三大优势:
- 代码隔离:会员相关代码集中管理,不与后台管理系统代码混杂
- 易于维护:功能变更或问题排查时,只需关注特定模块
- 可扩展性:未来可以方便地添加会员积分、等级等功能
2.2 用户体系隔离方案
会员系统与管理员系统的彻底隔离是关键。这包括几个层面:
数据层隔离:
- 创建独立的member_user表存储会员信息
- 与sys_user表完全分离,避免ID冲突或数据混淆
- 会员表设计应考虑业务需求,如手机号、会员等级等字段
认证层隔离:
- 提供独立的/member/login接口,区别于管理员的/login
- 会员token生成使用相同机制,但可考虑添加前缀区分
- 密码加密方式保持一致(MD5),确保安全性统一
权限层隔离:
- 会员角色与管理员角色分开配置
- 前端路由权限也要做相应隔离
- API访问权限通过注解或配置进行控制
实际项目中常见的错误是将会员和管理员数据混在同一张表中,通过user_type字段区分。这种做法短期内看似节省开发量,但随着业务复杂化会带来诸多维护问题,不建议采用。
3. 具体实现步骤
3.1 环境准备与模块创建
首先需要在现有若依项目中新增会员模块。以下是具体操作步骤:
- 在父pom.xml中添加模块声明:
xml复制<modules>
<!-- 原有模块 -->
<module>ruoyi-admin</module>
<module>ruoyi-system</module>
<!-- 新增会员模块 -->
<module>ruoyi-member</module>
</modules>
- 创建ruoyi-member模块,配置pom.xml依赖:
xml复制<dependencies>
<!-- 必须依赖 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<!-- 可选但推荐依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
</dependency>
</dependencies>
关键依赖说明:
- ruoyi-common:必须引入,包含token、加密等核心工具类
- mybatis-plus:推荐使用,简化数据库操作
- ruoyi-framework:可选,如需复用若依的权限注解需要引入
3.2 数据库设计与实体类实现
会员表设计应考虑以下基本字段:
sql复制CREATE TABLE `member_user` (
`member_id` bigint NOT NULL AUTO_INCREMENT COMMENT '会员ID',
`member_name` varchar(30) NOT NULL COMMENT '会员账号',
`password` varchar(100) NOT NULL COMMENT '密码',
`phone` varchar(11) DEFAULT NULL COMMENT '手机号',
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1禁用)',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`member_id`),
UNIQUE KEY `idx_member_name` (`member_name`),
UNIQUE KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员用户表';
对应Java实体类:
java复制@Data
@TableName("member_user")
public class MemberUser implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long memberId;
private String memberName;
private String password;
private String phone;
private String status;
private LocalDateTime createTime;
// 业务扩展字段
private Integer level;
private Integer points;
}
Mapper接口示例:
java复制@Mapper
public interface MemberUserMapper extends BaseMapper<MemberUser> {
@Select("SELECT * FROM member_user WHERE member_name = #{memberName}")
MemberUser selectMemberByUserName(String memberName);
@Select("SELECT * FROM member_user WHERE phone = #{phone}")
MemberUser selectMemberByPhone(String phone);
}
3.3 登录服务核心实现
会员登录服务的核心是验证用户身份并生成访问令牌。以下是关键实现点:
java复制@Service
public class MemberLoginServiceImpl implements IMemberLoginService {
@Autowired
private MemberUserMapper memberUserMapper;
@Autowired
private TokenService tokenService;
@Override
public AjaxResult login(String username, String password) {
// 1. 参数基础校验
if (StringUtils.isEmpty(username)) {
throw new ServiceException("会员账号不能为空");
}
if (StringUtils.isEmpty(password)) {
throw new ServiceException("密码不能为空");
}
// 2. 查询会员信息(支持账号/手机号登录)
MemberUser member = memberUserMapper.selectMemberByUserName(username);
if (member == null) {
member = memberUserMapper.selectMemberByPhone(username);
if (member == null) {
throw new ServiceException("会员账号不存在");
}
}
// 3. 账号状态检查
if (UserStatus.DISABLE.getCode().equals(member.getStatus())) {
throw new ServiceException("账号已被禁用");
}
// 4. 密码验证
if (!Md5Utils.matches(password, member.getPassword())) {
throw new ServiceException("密码不正确");
}
// 5. 构建登录用户信息
LoginUser loginUser = new LoginUser();
loginUser.setUserId(member.getMemberId());
loginUser.setUsername(member.getMemberName());
loginUser.setMemberUser(member); // 扩展字段
// 6. 记录登录日志(可选)
recordLoginLog(member.getMemberId(), member.getMemberName());
// 7. 生成并返回token
String token = tokenService.createToken(loginUser);
return AjaxResult.success()
.put("token", token)
.put("memberInfo", buildMemberInfo(member));
}
private void recordLoginLog(Long memberId, String username) {
// 实现登录日志记录逻辑
}
private Map<String, Object> buildMemberInfo(MemberUser member) {
// 构建返回给前端的会员信息
}
}
3.4 控制器与接口暴露
将会员登录接口暴露给前端:
java复制@RestController
@RequestMapping("/member")
public class MemberLoginController {
@Autowired
private IMemberLoginService memberLoginService;
@PostMapping("/login")
public AjaxResult login(@RequestParam String username,
@RequestParam String password) {
return memberLoginService.login(username, password);
}
@PostMapping("/logout")
public AjaxResult logout() {
// 获取当前登录会员
LoginUser loginUser = tokenService.getLoginUser();
if (loginUser != null) {
// 删除token记录
tokenService.delLoginUser(loginUser.getToken());
}
return AjaxResult.success("退出成功");
}
@GetMapping("/info")
public AjaxResult getMemberInfo() {
LoginUser loginUser = tokenService.getLoginUser();
// 返回会员详细信息
}
}
3.5 安全配置调整
在SecurityConfig中配置会员接口的访问规则:
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁用CSRF
.csrf().disable()
// 认证配置
.authorizeRequests()
// 会员登录相关接口放行
.antMatchers(
"/member/login",
"/member/register",
"/member/sendSmsCode"
).permitAll()
// 会员信息接口需要认证
.antMatchers("/member/**").authenticated()
// 管理后台接口保持原有配置
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
// 添加JWT过滤器
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
4. 进阶优化与扩展
4.1 多端登录支持
实际业务中,会员可能需要在多个设备上登录。可以通过以下方式优化token机制:
- 在token中记录设备信息:
java复制String token = tokenService.createToken(loginUser, deviceType);
- 在会员表中记录登录设备:
sql复制ALTER TABLE member_user ADD COLUMN last_login_device VARCHAR(20);
- 实现并发登录控制:
java复制// 在登录服务中添加设备校验逻辑
if (isLoggedInOnOtherDevice(member.getMemberId(), deviceType)) {
// 强制下线或提示用户
}
4.2 多种登录方式集成
除了账号密码登录,还可以集成以下登录方式:
短信验证码登录:
java复制@PostMapping("/loginBySms")
public AjaxResult loginBySms(@RequestParam String phone,
@RequestParam String smsCode) {
// 验证短信码
verifySmsCode(phone, smsCode);
// 查询或自动注册会员
MemberUser member = getOrCreateMemberByPhone(phone);
// 生成token
return buildLoginResult(member);
}
第三方登录(微信/支付宝):
java复制@PostMapping("/loginByWechat")
public AjaxResult loginByWechat(@RequestParam String code) {
// 通过code获取微信用户信息
WechatUserInfo wechatInfo = wechatService.getUserInfo(code);
// 绑定或创建会员账号
MemberUser member = bindWechatUser(wechatInfo);
// 生成token
return buildLoginResult(member);
}
4.3 权限控制增强
会员系统的权限控制可以分层实现:
- 接口级别权限:
java复制@PreAuthorize("@ss.hasPermi('member:order:create')")
@PostMapping("/order/create")
public AjaxResult createOrder() {
// 创建订单逻辑
}
- 数据权限:
java复制// 在SQL中自动添加数据权限过滤
@DataScope(deptAlias = "m", userAlias = "m")
@Select("SELECT * FROM member_order m WHERE m.status = #{status}")
List<MemberOrder> selectOrderByStatus(String status);
- 前端路由权限:
javascript复制// 前端根据会员权限动态生成路由
const routes = filterAsyncRoutes(asyncRoutes, member.roles);
5. 常见问题与解决方案
5.1 密码安全问题
问题场景:会员密码加密方式不一致导致登录失败
解决方案:
- 确保注册和登录使用相同的加密算法:
java复制// 注册时加密密码
String encryptedPwd = Md5Utils.encrypt(password);
member.setPassword(encryptedPwd);
// 登录时验证
Md5Utils.matches(inputPwd, storedPwd);
- 定期提醒会员修改密码
- 重要操作增加二次验证
5.2 Token失效问题
问题场景:会员token无故失效或过期时间不一致
排查步骤:
- 检查token生成配置:
yaml复制# application.yml
token:
# 管理员token配置
admin:
expireTime: 7200 # 2小时
secret: admin-secret
# 会员token配置
member:
expireTime: 2592000 # 30天
secret: member-secret
- 验证token服务实现:
java复制// 自定义token服务
public class CustomTokenService extends TokenService {
@Override
protected String buildTokenKey(String token) {
// 区分会员和管理员token
return isMemberToken(token) ? "member:" + token : "admin:" + token;
}
}
5.3 并发登录冲突
问题场景:同一账号多设备登录导致业务异常
解决方案:
- 实现登录互斥策略:
java复制// 登录时记录设备信息
String token = tokenService.createToken(loginUser, deviceId);
// 校验当前设备是否允许登录
if (!checkDeviceAllow(memberId, deviceId)) {
throw new ServiceException("该账号已在其他设备登录");
}
- 提供主动下线功能:
java复制@PostMapping("/kickout/{deviceId}")
public AjaxResult kickoutDevice(@PathVariable String deviceId) {
// 强制下线指定设备
tokenService.kickoutByDevice(getCurrentMemberId(), deviceId);
return AjaxResult.success();
}
6. 性能优化建议
- 缓存会员信息:频繁访问的会员数据可以缓存到Redis
java复制// 登录成功后缓存会员信息
redisCache.setCacheObject("member:info:"+memberId, memberInfo);
- 分库分表考虑:会员量大的系统提前规划分库分表
sql复制-- 按会员ID哈希分表
CREATE TABLE member_user_0 (
-- 表结构同member_user
) ENGINE=InnoDB;
CREATE TABLE member_user_1 (
-- 表结构同member_user
) ENGINE=InnoDB;
- 登录限流保护:防止暴力破解
java复制// 使用Guava RateLimiter限制登录频率
private final RateLimiter loginLimiter = RateLimiter.create(5.0); // 每秒5次
public AjaxResult login(String username, String password) {
if (!loginLimiter.tryAcquire()) {
throw new ServiceException("操作过于频繁,请稍后再试");
}
// 正常登录逻辑
}
在实际项目中实施会员系统时,我强烈建议先做好详细的设计评审,特别是要明确会员与管理员系统的边界。曾经在一个电商项目中,因为没有彻底分离两套用户体系,导致后期出现了数据混乱的问题,不得不进行痛苦的数据迁移。采用本文介绍的模块化设计方案后,后续的扩展和维护都变得轻松许多。