若依实现微信小程序登录&APP登录(新用户表,新Token认证,与原有后台管理登录隔离)

前言

近期公司有项目要做微信小程序和app的项目,为了快速开发采用若依框架,本文使用的若依框架版本为3.8.8,当前时间为2024年8月是若依最新版本。
在原有的后台管理系统登录流程上,新增新的登录接口用于小程序或APP登录,创建新的用户表,新的token认证登,与原有的后台管理系统登录流程分开,流程更清晰,逻辑更严谨。
本文需要对SpringSecurity+jwt的认证流程有一定的了解,如果不了解单纯复制粘贴也可以实现,但是你会看的很难受,建议还是学一学。
使用lombok快速生成get,set方法
在我的项目中修改了项目名,ruoyi改成了app,大家对照目录的时候注意

搭建若依项目

参照官方文档搭建即可 若依分离版官方文档

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) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci 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 '删除标志(0代表存在 2代表删除)',
  `login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP',
  `login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

插入测试数据

insert  into `app_user`(`user_id`,`user_name`,`nick_name`,`email`,`mobile`,`sex`,`avatar`,`password`,`salt`,`status`,`del_flag`,`login_ip`,`login_date`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) values (1,'xing','星哥','123456@163.com','19999999999','1','','bb6d30fe528388b8f08631afb5a22c5c','GoEskKGp','0','0','192.168.3.4','2024-08-13 09:48:46','管理员','2024-08-13 09:24:06','用户注册创建','2024-08-13 09:24:06','测试app账号');

2.创建AppUser实体类

此实体类为上述数据库表AppUser用户表实体类
注意:与 SysUser 同级目录
创建目录:ruoyi-common——>src——>main——>java——>com.ruoyi.common——>core——>domian——>entity

package com.app.common.core.domain.entity;

import com.app.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;

/**
 * 用户对象 app_user
 */
@Data
public class AppUser extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 用户账号
     */
    private String userName;

    /**
     * 用户昵称
     */
    private String nickName;

    /**
     * 用户邮箱
     */
    private String email;

    /**
     * 手机号码
     */
    private String mobile;

    /**
     * 用户性别(0=男,1=女,2=未知)
     */
    private String sex;

    /**
     * 用户头像
     */
    private String avatar;

    /**
     * 密码
     */
    private String password;

    /**
     * 盐
     */
    private String salt;

    /**
     * 帐号状态(0=正常,1=停用)
     */
    private String status;

    /**
     * 删除标志(0代表存在 2代表删除)
     */
    private String delFlag;

    /**
     * 最后登录IP
     */
    private String loginIp;

    /**
     * 最后登录时间
     */
    private Date loginDate;
}

3.创建 AppUserMapper.xml 映射文件

目录:ruoyi-system——>src——>main——>resources——>mapper.system
注意:与 SysUserMapper.xml 同级目录

<?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.app.system.mapper.AppUserMapper">

    <resultMap type="AppUser" id="AppUserResult">
        <id property="userId" column="user_id"/>
        <result property="userName" column="user_name"/>
        <result property="nickName" column="nick_name"/>
        <result property="email" column="email"/>
        <result property="mobile" column="mobile"/>
        <result property="sex" column="sex"/>
        <result property="avatar" column="avatar"/>
        <result property="password" column="password"/>
        <result property="salt" column="salt"/>
        <result property="status" column="status"/>
        <result property="delFlag" column="del_flag"/>
        <result property="loginIp" column="login_ip"/>
        <result property="loginDate" column="login_date"/>
        <result property="createBy" column="create_by"/>
        <result property="createTime" column="create_time"/>
        <result property="updateBy" column="update_by"/>
        <result property="updateTime" column="update_time"/>
        <result property="remark" column="remark"/>
    </resultMap>

    <sql id="selectAppUserVo">
        select user_id, user_name, nick_name, email, avatar, mobile, password, salt, sex, status, del_flag, login_ip, login_date, create_by, create_time, remark from app_user
    </sql>

    <select id="selectAppUserByUserName" parameterType="String" resultMap="AppUserResult">
        <include refid="selectAppUserVo"/>
        where user_name = #{userName} and del_flag = '0'
    </select>

    <update id="updateAppUser" parameterType="AppUser">
        update app_user
        <set>
            <if test="userName != null and userName != ''">user_name = #{userName},</if>
            <if test="nickName != null and nickName != ''">nick_name = #{nickName},</if>
            <if test="email != null ">email = #{email},</if>
            <if test="mobile != null ">mobile = #{mobile},</if>
            <if test="sex != null and sex != ''">sex = #{sex},</if>
            <if test="avatar != null and avatar != ''">avatar = #{avatar},</if>
            <if test="password != null and password != ''">password = #{password},</if>
            <if test="salt != null and salt != ''">salt = #{salt},</if>
            <if test="status != null and status != ''">status = #{status},</if>
            <if test="loginIp != null and loginIp != ''">login_ip = #{loginIp},</if>
            <if test="loginDate != null">login_date = #{loginDate},</if>
            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
            <if test="remark != null">remark = #{remark},</if>
            update_time = sysdate()
        </set>
        where user_id = #{userId}
    </update>
</mapper>

4.创建 AppUserMapper 数据层

目录:ruoyi-system——>src——>main——>java——>com.ruoyi.system——>mapper
注意:与 SysUserMapper 同级目录

package com.app.system.mapper;

import com.app.common.core.domain.entity.AppUser;

/**
 * 用户表 数据层
 *
 * @author ruoyi
 */
public interface AppUserMapper {
    /**
     * 通过用户名查询用户
     *
     * @param userName 用户名
     * @return 用户对象信息
     */
    AppUser selectAppUserByUserName(String userName);

    /**
     * 修改用户信息
     *
     * @param appUser 用户信息
     * @return 结果
     */
    int updateAppUser(AppUser appUser);
}

5.创建 IAppUserService 业务层

目录:ruoyi-system——>src——>main——>java——>com.ruoyi.system——>service
注意:与 ISysUserService 同级目录

package com.app.system.service;

import com.app.common.core.domain.entity.AppUser;

/**
 * 用户 业务层
 *
 * @author ruoyi
 */
public interface IAppUserService {
    /**
     * 通过用户账号查询用户
     *
     * @param userName 用户账号
     * @return 用户对象信息
     */
    AppUser selectAppUserByUserName(String userName);

    /**
     * 修改用户信息
     *
     * @param appUser 用户信息
     * @return 结果
     */
    int updateAppUser(AppUser appUser);

    /**
     * 密码校验
     *
     * @param password 明文
     * @param salt     盐
     * @param hashPwd  密文
     * @return boolean
     */
    boolean checkPassword(String password, String salt, String hashPwd);
}

6.创建 AppUserServiceImpl 业务实现层

目录:ruoyi-system——>src——>main——>java——>com.ruoyi.system——>service——>impl
注意:与 SysUserServiceImpl 同级目录

package com.app.system.service.impl;

import com.app.common.core.domain.entity.AppUser;
import com.app.system.mapper.AppUserMapper;
import com.app.system.service.IAppUserService;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import javax.annotation.Resource;

/**
 * 用户 业务层处理
 *
 * @author ruoyi
 */
@Service
public class AppUserServiceImpl implements IAppUserService {
    @Resource
    private AppUserMapper appUserMapper;

    /**
     * 通过用户名查询用户
     *
     * @param userName 用户名
     * @return 用户对象信息
     */
    @Override
    public AppUser selectAppUserByUserName(String userName) {
        return appUserMapper.selectAppUserByUserName(userName);
    }

    /**
     * 修改保存用户信息
     *
     * @param appUser 用户信息
     * @return 结果
     */
    @Override
    public int updateAppUser(AppUser appUser) {
        return appUserMapper.updateAppUser(appUser);
    }

    /**
     * 密码校验
     *
     * @param password 明文
     * @param salt     盐
     * @param hashPwd  密文
     * @return boolean
     */
    public boolean checkPassword(String password, String salt, String hashPwd) {
        return hashPwd.equals(DigestUtils.md5DigestAsHex(password.concat(salt).getBytes()));
    }

    /**
     * 随即生成盐
     * 8位随机数
     *
     * @return salt 盐
     */
    private String generateSalt() {
        return RandomStringUtils.randomAlphanumeric(8);
    }

    public static void main(String[] args) {
        AppUserServiceImpl appUserService = new AppUserServiceImpl();
        String password = "123456";
        String salt = appUserService.generateSalt();
        System.out.printf("明文密码:%s%n", password);
        System.out.printf("盐:%s%n", salt);
        System.out.printf("密文密码:%s%n", DigestUtils.md5DigestAsHex(password.concat(salt).getBytes()));
    }
}

7.创建 LoginAppUser 登录用户身份权限类

目录:ruoyi-common——>src——>main——>java——>com.ruoyi.common——>core——>domian——>model
注意:与 LoginUser 同级目录

package com.app.common.core.domain.model;

import com.alibaba.fastjson2.annotation.JSONField;
import com.app.common.core.domain.entity.AppUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * 登录用户身份权限
 *
 * @author ruoyi
 */
public class LoginAppUser implements UserDetails {
    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 用户唯一标识
     */
    private String token;

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 过期时间
     */
    private Long expireTime;

    /**
     * 登录IP地址
     */
    private String ipaddr;

    /**
     * 登录地点
     */
    private String loginLocation;

    /**
     * 浏览器类型
     */
    private String browser;

    /**
     * 操作系统
     */
    private String os;

    /**
     * 用户信息
     */
    private AppUser appUser;

    public LoginAppUser() {
    }

    public LoginAppUser(AppUser appUser) {
        this.appUser = appUser;
    }

    public LoginAppUser(Long userId, AppUser appUser) {
        this.userId = userId;
        this.appUser = appUser;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    @JSONField(serialize = false)
    @Override
    public String getPassword() {
        return appUser.getPassword();
    }

    @Override
    public String getUsername() {
        return appUser.getUserName();
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isEnabled() {
        return true;
    }

    public Long getLoginTime() {
        return loginTime;
    }

    public void setLoginTime(Long loginTime) {
        this.loginTime = loginTime;
    }

    public String getIpaddr() {
        return ipaddr;
    }

    public void setIpaddr(String ipaddr) {
        this.ipaddr = ipaddr;
    }

    public String getLoginLocation() {
        return loginLocation;
    }

    public void setLoginLocation(String loginLocation) {
        this.loginLocation = loginLocation;
    }

    public String getBrowser() {
        return browser;
    }

    public void setBrowser(String browser) {
        this.browser = browser;
    }

    public String getOs() {
        return os;
    }

    public void setOs(String os) {
        this.os = os;
    }

    public Long getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(Long expireTime) {
        this.expireTime = expireTime;
    }

    public AppUser getAppUser() {
        return appUser;
    }

    public void setAppUser(AppUser appUser) {
        this.appUser = appUser;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
}

8.创建 LoginAppBody 用户登录对象类

目录:ruoyi-common——>src——>main——>java——>com.ruoyi.common——>core——>domian——>model
注意:与 LoginBody 同级目录

package com.app.common.core.domain.model;

/**
 * 用户登录对象
 *
 * @author ruoyi
 */
public class LoginAppBody
{
    /**
     * 用户名
     */
    private String username;
    /**
     * 用户密码
     */
    private String password;

    public String getUsername()
    {
        return username;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }

    public String getPassword()
    {
        return password;
    }

    public void setPassword(String password)
    {
        this.password = password;
    }
}

9.添加通用常量信息 Constants & CacheConstants 常量信息

Constants

目录:ruoyi-common——>src——>main——>java——>com.ruoyi.common——>constant

    /**
     * APP令牌前缀
     */
    public static final String LOGIN_APP_USER_KEY = "login_app_user_key";

    /**
     * APP接口前缀
     */
    public static final String APP_URI = "/app/";

图示:
在这里插入图片描述

CacheConstants

目录:ruoyi-common——>src——>main——>java——>com.ruoyi.common——>constant

public static final String LOGIN_APP_TOKEN_KEY = "login_app_tokens:";

图示:
在这里插入图片描述

10.创建 AppTokenService TOKEN处理类

目录:ruoyi-framework——>src——>main——>java——>com.ruoyi.framework——>web——>service
注意:与 TokenService 同级目录

package com.app.framework.web.service;

import com.app.common.constant.CacheConstants;
import com.app.common.constant.Constants;
import com.app.common.core.domain.model.LoginAppUser;
import com.app.common.core.redis.RedisCache;
import com.app.common.utils.ServletUtils;
import com.app.common.utils.StringUtils;
import com.app.common.utils.ip.AddressUtils;
import com.app.common.utils.ip.IpUtils;
import com.app.common.utils.uuid.IdUtils;
import eu.bitwalker.useragentutils.UserAgent;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * token验证处理
 *
 * @author ruoyi
 */
@Component
public class AppTokenService {
    // 令牌自定义标识
    @Value("${token.header}")
    private String header;

    // 令牌秘钥
    @Value("${token.secret}")
    private String secret;

    // 令牌有效期(默认30分钟)
    @Value("${token.expireTime}")
    private int expireTime;

    protected static final long MILLIS_SECOND = 1000;

    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

    @Resource
    private RedisCache redisCache;

    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginAppUser getLoginAppUser(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token)) {
            try {
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(Constants.LOGIN_APP_USER_KEY);
                String userKey = getTokenKey(uuid);
                LoginAppUser loginAppUser = redisCache.getCacheObject(userKey);
                return loginAppUser;
            } catch (Exception e) {
            }
        }
        return null;
    }

    /**
     * 设置用户身份信息
     */
    public void setLoginAppUser(LoginAppUser loginAppUser) {
        if (StringUtils.isNotNull(loginAppUser) && StringUtils.isNotEmpty(loginAppUser.getToken())) {
            refreshToken(loginAppUser);
        }
    }

    /**
     * 删除用户身份信息
     */
    public void delLoginAppUser(String token) {
        if (StringUtils.isNotEmpty(token)) {
            String userKey = getTokenKey(token);
            redisCache.deleteObject(userKey);
        }
    }

    /**
     * 创建令牌
     *
     * @param loginAppUser 用户信息
     * @return 令牌
     */
    public String createAppToken(LoginAppUser loginAppUser) {
        String token = IdUtils.fastUUID();
        loginAppUser.setToken(token);
        setUserAgent(loginAppUser);
        refreshToken(loginAppUser);

        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_APP_USER_KEY, token);
        return createToken(claims);
    }

    /**
     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
     *
     * @param loginAppUser
     * @return 令牌
     */
    public void verifyToken(LoginAppUser loginAppUser) {
        long expireTime = loginAppUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
            refreshToken(loginAppUser);
        }
    }

    /**
     * 刷新令牌有效期
     *
     * @param loginAppUser 登录信息
     */
    public void refreshToken(LoginAppUser loginAppUser) {
        loginAppUser.setLoginTime(System.currentTimeMillis());
        loginAppUser.setExpireTime(loginAppUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginAppUser.getToken());
        redisCache.setCacheObject(userKey, loginAppUser, expireTime, TimeUnit.MINUTES);
    }

    /**
     * 设置用户代理信息
     *
     * @param loginAppUser 登录信息
     */
    public void setUserAgent(LoginAppUser loginAppUser) {
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
        String ip = IpUtils.getIpAddr();
        loginAppUser.setIpaddr(ip);
        loginAppUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        loginAppUser.setBrowser(userAgent.getBrowser().getName());
        loginAppUser.setOs(userAgent.getOperatingSystem().getName());
    }

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.getSubject();
    }

    /**
     * 获取请求token
     *
     * @param request
     * @return token
     */
    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(header);
        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

    private String getTokenKey(String uuid) {
        return CacheConstants.LOGIN_APP_TOKEN_KEY + uuid;
    }
}

11.创建 AppLoginService 登录校验方法类

目录:ruoyi-framework——>src——>main——>java——>com.ruoyi.framework——>web——>service
注意:与 SysLoginService 同级目录

package com.app.framework.web.service;

import com.app.common.core.domain.entity.AppUser;
import com.app.common.core.domain.model.LoginAppUser;
import com.app.common.exception.ServiceException;
import com.app.common.utils.DateUtils;
import com.app.common.utils.ip.IpUtils;
import com.app.system.service.IAppUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 登录校验方法
 *
 * @author ruoyi
 */
@Component
public class AppLoginService {
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Resource
    private AppTokenService appTokenService;
    @Resource
    private IAppUserService appUserSevice;


    /**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @return 结果
     */
    public String login(String username, String password) {
        AppUser appUser = appUserSevice.selectAppUserByUserName(username);
        if (appUser == null) {
            throw new ServiceException("用户不存在");
        }
        if (!appUserSevice.checkPassword(password, appUser.getSalt(), appUser.getPassword())) {
            throw new ServiceException("密码不正确");
        }
        LoginAppUser loginAppUser = new LoginAppUser(appUser.getUserId(), appUser);
        recordLoginInfo(loginAppUser.getUserId());
        // 生成token
        return appTokenService.createAppToken(loginAppUser);
    }

    /**
     * 记录登录信息
     *
     * @param userId 用户ID
     */
    private void recordLoginInfo(Long userId) {
        AppUser appUser = new AppUser();
        appUser.setUserId(userId);
        appUser.setLoginIp(IpUtils.getIpAddr());
        appUser.setLoginDate(DateUtils.getNowDate());
        appUserSevice.updateAppUser(appUser);
    }
}

12.修改JwtAuthenticationTokenFilter token过滤器 验证token有效性

目录:ruoyi-framework——>src——>main——>java——>com.ruoyi.framework——>security——>filter

package com.app.framework.security.filter;

import java.io.IOException;
import java.util.Collection;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.app.common.constant.Constants;
import com.app.common.core.domain.model.LoginAppUser;
import com.app.framework.web.service.AppTokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.app.common.core.domain.model.LoginUser;
import com.app.common.utils.SecurityUtils;
import com.app.common.utils.StringUtils;
import com.app.framework.web.service.TokenService;

/**
 * token过滤器 验证token有效性
 *
 * @author ruoyi
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Resource
    private TokenService tokenService;

    @Resource
    private AppTokenService appTokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        if (request.getRequestURI().contains(Constants.APP_URI)) {
            logger.info("前台接口请求:{}", request.getRequestURI());
            LoginAppUser loginAppUser = appTokenService.getLoginAppUser(request);
            if (StringUtils.isNotNull(loginAppUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
                appTokenService.verifyToken(loginAppUser);
                auth(loginAppUser, loginAppUser.getAuthorities(), request);
            }

        } else {
            logger.info("后台接口请求:{}", request.getRequestURI());
            LoginUser loginUser = tokenService.getLoginUser(request);
            if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
                tokenService.verifyToken(loginUser);
                auth(loginUser, loginUser.getAuthorities(), request);
            }
        }
        chain.doFilter(request, response);
    }

    /**
     * 验证token
     *
     * @param obj         登录信息
     * @param authorities 已授予的权限集合
     * @param request     网络请求
     */
    private void auth(Object obj, Collection<? extends GrantedAuthority> authorities, HttpServletRequest request) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(obj, null, authorities);
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}

13.安全服务工具类 SecurityUtils 添加信息

目录:ruoyi-common——>src——>main——>java——>com.ruoyi.common——>utils

/**
     * 获取APP用户
     **/
    public static LoginAppUser getLoginAppUser()
    {
        try
        {
            return (LoginAppUser) getAuthentication().getPrincipal();
        }
        catch (Exception e)
        {
            throw new ServiceException("获取APP用户信息异常", HttpStatus.UNAUTHORIZED);
        }
    }

图示:
在这里插入图片描述

14.web层通用数据类 BaseController 添加信息

目录:ruoyi-common——>src——>main——>java——>com.ruoyi.common——>core——>controller

/**
     * 获取APP用户缓存信息
     */
    public LoginAppUser getLoginAppUser()
    {
        return SecurityUtils.getLoginAppUser();
    }

在这里插入图片描述

15.创建 AppLoginController 控制器

目录:ruoyi-admin——>src——>main——>java——>com.ruoyi——>web——>controller——>system
注意:与 SysUserController 同级目录

package com.app.web.controller.system;

import com.app.common.annotation.Anonymous;
import com.app.common.constant.Constants;
import com.app.common.core.controller.BaseController;
import com.app.common.core.domain.AjaxResult;
import com.app.common.core.domain.entity.AppUser;
import com.app.common.core.domain.model.LoginAppBody;
import com.app.framework.web.service.AppLoginService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;

/**
 * 登录验证
 *
 * @author ruoyi
 */
@Api("APP登录管理")
@RestController
@RequestMapping("/app")
public class AppLoginController extends BaseController {
    @Resource
    private AppLoginService appLoginService;

    @ApiOperation("登录")
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginAppBody loginAppBody) {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = appLoginService.login(loginAppBody.getUsername(), loginAppBody.getPassword());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

    @ApiOperation("根据token获取用户信息")
    @GetMapping("/userInfo")
    @Anonymous
    public AjaxResult userInfo() {
        AppUser appUser = getLoginAppUser().getAppUser();
        AjaxResult ajax = AjaxResult.success();
        ajax.put("appUser", appUser);
        return ajax;
    }
}

16.SecurityConfig添加匿名访问地址

目录:ruoyi-framework——>src——>main——>java——>com.ruoyi.framework——>config

                // 对于APP登录login 允许匿名访问
                .antMatchers("/app/login").permitAll() 

图示:
在这里插入图片描述

17.测试接口

使用Apifox测试接口

登录接口

登录成功后返回token
在这里插入图片描述

根据token获取用户信息

携带token访问,JwtAuthenticationTokenFilter token过滤器会拦截所有请求,验证token的有效性,从而获取用户信息
在这里插入图片描述

小程序登录

1.小程序登录和APP登录区别:

注意:如果您直接跳到此处观看小程序登录,一定要将上面APP登录时的代码补充完整

小程序登录和APP登录认证方式是一样的,可以共用上述的认证流程。
区别就是APP使用的是用户名,密码,手机号,验证码等参数进行登录。
小程序也可以使用上述参数的形式进行登录,但是现在大部分都是一键登录,下面我们分析一下小程序一键登录流程。

2.小程序登录流程

微信小程序的登录接口

根据微信官方文档,微信有提供自己的登录接口,可用于现有的微信用户,并且授予基本信息

整体流程

  1. 前端通过wx.login 获取 js_code
  2. 将获取的js_code发送给后端
  3. 后端拿到js_code,小程序 appId,小程序 appSecret,grant_type(参数默认写死为authorization_code)
  4. 将上述四个参数通过GET请求发送给微信小程序登录接口,请求地址:https://api.weixin.qq.com/sns/jscode2session
  5. 请求成功后拿到响应参数:session_key(会话密钥),unionid(用户在开放平台的唯一标识符),errmsg(错误信息),openid(用户唯一标识),errcode(错误代码)
  6. 我们可以拿openid(用户唯一标识)当作数据库表中的唯一字段,如上述APP登录时候的user_name(用户账号)
  7. 后续登陆时拿openid进行认证即可

3.在配置文件中添加小程序参数

目录:ruoyi-admin——>src——>resource——>application.yml
注意:请改写为自己的小程序参数 appId & appSecret

#微信小程序配置
wx:
  minApp:
    appId: wxedxxxxxxxx7cxx1x
    appSecret: 0ccxxxx54d1xxxx88xa9xxxxde07xxx8

图示:
在这里插入图片描述

4.修改AppUserMapper.xml,新增insert语句

目录:ruoyi-system——>src——>main——>resources——>mapper.system

<insert id="insertAppUser" parameterType="AppUser" useGeneratedKeys="true" keyProperty="userId">
        insert into app_user
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="userName != null and userName != ''">user_name,</if>
            <if test="nickName != null and nickName != ''">nick_name,</if>
            <if test="email != null">email,</if>
            <if test="mobile != null">mobile,</if>
            <if test="sex != null">sex,</if>
            <if test="avatar != null">avatar,</if>
            <if test="password != null">password,</if>
            <if test="salt != null">salt,</if>
            <if test="status != null">status,</if>
            <if test="delFlag != null">del_flag,</if>
            <if test="loginIp != null">login_ip,</if>
            <if test="loginDate != null">login_date,</if>
            <if test="createBy != null">create_by,</if>
            <if test="createTime != null">create_time,</if>
            <if test="updateBy != null">update_by,</if>
            <if test="updateTime != null">update_time,</if>
            <if test="remark != null">remark,</if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="userName != null and userName != ''">#{userName},</if>
            <if test="nickName != null and nickName != ''">#{nickName},</if>
            <if test="email != null">#{email},</if>
            <if test="mobile != null">#{mobile},</if>
            <if test="sex != null">#{sex},</if>
            <if test="avatar != null">#{avatar},</if>
            <if test="password != null">#{password},</if>
            <if test="salt != null">#{salt},</if>
            <if test="status != null">#{status},</if>
            <if test="delFlag != null">#{delFlag},</if>
            <if test="loginIp != null">#{loginIp},</if>
            <if test="loginDate != null">#{loginDate},</if>
            <if test="createBy != null">#{createBy},</if>
            <if test="createTime != null">#{createTime},</if>
            <if test="updateBy != null">#{updateBy},</if>
            <if test="updateTime != null">#{updateTime},</if>
            <if test="remark != null">#{remark},</if>
        </trim>
    </insert>

5.修改AppUserMapper数据层增加新增用户接口

目录:ruoyi-system——>src——>main——>java——>com.ruoyi.system——>mapper

    /**
     * 新增用户信息
     *
     * @param appUser 用户信息
     * @return 结果
     */
    public int insertAppUser(AppUser appUser);

6.修改IAppUserService增加新增用户接口

目录:ruoyi-system——>src——>main——>java——>com.ruoyi.system——>service

    /**
     * 新增用户
     * @param appUser
     * @return
     */
    int insertAppUser(AppUser appUser);

7.修改AppUserServiceImpl 业务实现层

目录:ruoyi-system——>src——>main——>java——>com.ruoyi.system——>service——>impl

    /**
     * 新增用户
     * @param appUser
     * @return
     */
    @Override
    public int insertAppUser(AppUser appUser) {
        return appUserMapper.insertAppUser(appUser);
    }

8.修改AppLoginService

目录:ruoyi-framework——>src——>main——>java——>com.ruoyi.framework——>web——>service

修改内容:
1.新增成员变量 appId&appSecret,使用@Value注解读取配置文件参数并赋值给对应得变量
2.新增minLogin方法实现小程序登录

package com.app.framework.web.service;

import com.alibaba.fastjson2.JSONObject;
import com.app.common.core.domain.entity.AppUser;
import com.app.common.core.domain.model.LoginAppUser;
import com.app.common.exception.ServiceException;
import com.app.common.utils.DateUtils;
import com.app.common.utils.StringUtils;
import com.app.common.utils.http.HttpUtils;
import com.app.common.utils.ip.IpUtils;
import com.app.system.service.IAppUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 登录校验方法
 *
 * @author ruoyi
 */
@Component
public class AppLoginService {
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Resource
    private AppTokenService appTokenService;
    @Resource
    private IAppUserService appUserSevice;

    @Value("${wx.minApp.appId}")
    private String appId;
    @Value("${wx.minApp.appSecret}")
    private String appSecret;


    /**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @return 结果
     */
    public String login(String username, String password) {
        AppUser appUser = appUserSevice.selectAppUserByUserName(username);
        if (appUser == null) {
            throw new ServiceException("用户不存在");
        }
        if (!appUserSevice.checkPassword(password, appUser.getSalt(), appUser.getPassword())) {
            throw new ServiceException("密码不正确");
        }
        LoginAppUser loginAppUser = new LoginAppUser(appUser.getUserId(), appUser);
        recordLoginInfo(loginAppUser.getUserId());
        // 生成token
        return appTokenService.createAppToken(loginAppUser);
    }


    /**
     * 小程序登录
     * @param code
     * @return
     */
    public String minLogin(String code) {
        //1根据code执行小程序登录获取 openId
        String url = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
        String replaceUrl = url.replace("{0}",appId).replace("{1}",appSecret).replace("{2}",code);
        System.out.println(replaceUrl);
        JSONObject jsonObject = JSONObject.parseObject(HttpUtils.sendGet(replaceUrl));
        if(jsonObject.get("errcode")!=null){
            throw new ServiceException(String.format("获取微信授权信息失败,错误编码:%s,错误信息:%s",String.valueOf(jsonObject.get("errcode")),(String)jsonObject.get("errmsg")));
        }
        //2根据openId判断用户是否存在 不存在自动注册 openId对应数据库表user_name
        AppUser appUser = appUserSevice.selectAppUserByUserName(String.valueOf(jsonObject.get("openid")));
        if (appUser == null) {
            appUser = new AppUser();
            appUser.setUserName(String.valueOf(jsonObject.get("openid")));
            appUserSevice.insertAppUser(appUser);
        }
        //3生成token
        LoginAppUser loginAppUser = new LoginAppUser(appUser.getUserId(), appUser);
        recordLoginInfo(loginAppUser.getUserId());
        return appTokenService.createAppToken(loginAppUser);
    }

    /**
     * 记录登录信息
     *
     * @param userId 用户ID
     */
    private void recordLoginInfo(Long userId) {
        AppUser appUser = new AppUser();
        appUser.setUserId(userId);
        appUser.setLoginIp(IpUtils.getIpAddr());
        appUser.setLoginDate(DateUtils.getNowDate());
        appUserSevice.updateAppUser(appUser);
    }

}

9.修改AppLoginController新增小程序登录

目录:ruoyi-admin——>src——>main——>java——>com.ruoyi——>web——>controller——>system
重要:方法接收的参数code是前端通过 wx.login 获取得 js_code

    @ApiOperation("微信小程序登录")
    @GetMapping("/minLogin")
    public AjaxResult minLogin(String code){
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = appLoginService.minLogin(code);
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

10.测试接口

使用Apifox测试

登录接口:

在这里插入图片描述

根据token获取用户信息

注意:Headers中要携带token访问
在这里插入图片描述

实现效果

第一次登录没有查询到用户,自动注册,将用户信息封装成token返回
再次登录,根据openid查询数据库,将查询到的用户信息封装成token返回

谢谢您的耐心观看,点赞 收藏 转发 是对我最大的支持

  • 37
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
要统一多个微信小程序和一个网页端管理系统的用户登录,你可以使用微信开放平台提供的授权登录功能,将微信小程序和网页端管理系统的用户登录都绑定在同一个微信开放平台账号下。这样,用户只需要在微信开放平台登录一次,就可以在不同的应用中共享登录状态。 在MySQL中,你可以创建一个包含以下列的来存储用户信息: ``` CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(50) NOT NULL, unionid VARCHAR(50) NOT NULL, nickname VARCHAR(50) NOT NULL, avatar VARCHAR(255) NOT NULL, app_id VARCHAR(50) NOT NULL ); ``` 这个包含以下列: - `id`:自增长的用户ID,作为主键。 - `openid`:用户微信小程序中的唯一标识。 - `unionid`:用户在微信开放平台中的唯一标识。 - `nickname`:用户昵称。 - `avatar`:用户头像地址。 - `app_id`:存储用户所属的应用的ID,可以是微信小程序或网页端管理系统的ID。 在这个中,你可以将所有应用的用户信息存储在同一个中,通过`app_id`字段来区分不同的应用。通过微信开放平台提供的授权登录功能,你可以在用户登录时获取到用户的`openid`和`unionid`,并将其存储到这个中。 在用户登录时,你可以根据用户的`openid`或`unionid`查询该用户是否存在,并且该用户所属的应用是否正确。如果存在,则可以在MySQL中存储登录状态,以便用户在不同的应用中共享登录状态。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值