若依分离版实战:5步构建小程序专属登录体系
在移动互联网时代,小程序已成为连接用户与服务的重要桥梁。作为开发者,如何快速为小程序项目搭建稳定可靠的后端登录系统?本文将带你深入若依(RuoYi)框架,从零构建一套完整的APP/小程序登录体系。
1. 数据库设计与用户表创建
任何登录系统的核心都是用户数据存储。在若依分离版中,我们需要为小程序用户创建独立的数据表结构,与后台管理系统用户隔离。
CREATE TABLE `app_user` ( `user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `user_name` varchar(30) NOT NULL COMMENT '用户账号', `nick_name` varchar(30) NOT NULL COMMENT '用户昵称', `email` varchar(50) DEFAULT '' COMMENT '用户邮箱', `mobile` varchar(11) DEFAULT '' COMMENT '手机号码', `sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)', `avatar` varchar(100) DEFAULT '' COMMENT '头像地址', `password` varchar(100) DEFAULT '' COMMENT '密码', `salt` varchar(50) DEFAULT '' COMMENT '盐', `status` char(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)', `del_flag` char(1) DEFAULT '0' COMMENT '删除标志', `login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP', `login_date` datetime DEFAULT NULL COMMENT '最后登录时间', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小程序用户信息表';关键设计考虑:
- 保留基础用户信息字段
- 添加密码加密所需的salt字段
- 设置合理的字段长度和默认值
- 与后台sys_user表保持结构相似但独立
提示:建议在开发环境初始化测试数据,方便后续接口调试
2. 核心实体与Mapper层实现
基于MyBatis的ORM映射是若依框架的数据访问基础。我们需要创建对应的Java实体类和Mapper接口。
2.1 实体类定义
public class AppUser extends BaseEntity { private Long userId; private String userName; private String nickName; private String email; private String mobile; private String sex; private String avatar; private String password; private String salt; private String status; private String delFlag; private String loginIp; private Date loginDate; // getter/setter省略 }注意事项:
- 继承BaseEntity获取基础字段
- 字段命名与数据库表保持一致
- 实现Serializable接口
2.2 Mapper接口与XML配置
<!-- AppUserMapper.xml --> <select id="selectAppUserByUserName" resultMap="AppUserResult"> select * from app_user where user_name = #{userName} and del_flag = '0' </select>常见问题排查:
- XML文件位置必须在mapper.system包下
- namespace属性必须指向完整接口路径
- 参数名与接口方法保持一致
3. 业务逻辑层实现
业务层负责处理核心登录逻辑,包括密码校验、用户状态验证等。
3.1 密码加密与验证
public boolean checkPassword(String password, String salt, String hashPwd) { String encrypted = DigestUtils.md5DigestAsHex( (password + salt).getBytes() ); return encrypted.equals(hashPwd); }安全建议:
- 使用强哈希算法(如MD5+盐)
- 盐值长度建议8位以上
- 避免在日志中输出敏感信息
3.2 登录状态维护
public String login(String username, String password) { AppUser user = appUserMapper.selectByUserName(username); if(user == null || !checkPassword(password, user.getSalt(), user.getPassword())) { throw new ServiceException("用户名或密码错误"); } LoginAppUser loginUser = new LoginAppUser(user); return tokenService.createToken(loginUser); }4. Token认证体系集成
若依采用JWT作为认证机制,我们需要扩展支持小程序用户的token处理。
4.1 TokenService扩展
@Component public class AppTokenService { @Value("${token.secret}") private String secret; public String createToken(LoginAppUser loginUser) { String token = IdUtils.fastUUID(); loginUser.setToken(token); refreshToken(loginUser); Map<String, Object> claims = new HashMap<>(); claims.put(Constants.LOGIN_APP_USER_KEY, token); return Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } }关键配置项:
- token.secret:签名密钥
- token.expireTime:过期时间(分钟)
- token.header:请求头字段名
4.2 安全过滤器适配
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { if(request.getRequestURI().startsWith("/app/")) { // 处理小程序请求 LoginAppUser user = appTokenService.getLoginUser(request); if(user != null) { UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, null); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response); } }5. 控制器与接口暴露
最后一步是将登录能力通过REST API暴露给小程序端。
5.1 登录接口实现
@RestController @RequestMapping("/app") public class AppLoginController extends BaseController { @PostMapping("/login") public AjaxResult login(@RequestBody LoginBody loginBody) { String token = loginService.login( loginBody.getUsername(), loginBody.getPassword() ); return success().put(Constants.TOKEN, token); } @GetMapping("/userInfo") public AjaxResult userInfo() { LoginAppUser user = getLoginUser(); return success().put("user", user); } }5.2 安全配置放行
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/app/login").permitAll() .antMatchers("/app/**").authenticated(); }接口测试要点:
- 验证/login接口返回的token
- 使用token访问/userInfo获取用户信息
- 测试错误密码场景
- 验证token过期行为
实战中的经验分享
在实际项目中,我们遇到了几个典型问题值得分享:
- 跨域问题:小程序端需要配置服务器地址白名单
- Token刷新:实现静默刷新机制提升用户体验
- 密码策略:强制要求用户设置复杂密码
- 登录限制:防止暴力破解需要添加尝试次数限制
// 示例:登录次数限制实现 public String login(String username, String password) { String cacheKey = "login:limit:" + username; Integer attempts = redisCache.getCacheObject(cacheKey); if(attempts != null && attempts >= 5) { throw new ServiceException("尝试次数过多,请稍后再试"); } try { // 正常登录逻辑 return token; } catch(AuthenticationException e) { redisCache.increment(cacheKey); redisCache.expire(cacheKey, 1, TimeUnit.HOURS); throw e; } }