初识若依-框架的功能

1-文件结构

1)后端结构

2)前端结构

2-核心技术

  • 前端技术栈 ES6、vue、vuex、vue-router、vue-cli、axios、element-ui
  • 后端技术栈 SpringBoot、MyBatis、Spring Security、Jwt

前端技术

后端技术

SpringBoot 框架
1 、介绍
Spring Boot 是一款开箱即用框架,提供各种默认配置来简化项目配置。让我们的 Spring 应用变的更
轻量化、更快的入门。 在主程序执行 main 函数就可以运行。你也可以打包你的应用为 jar 并通过使用
java - jar 来运行你的 Web 应用。它遵循 " 约定优先于配置 " 的原则, 使用 SpringBoot 只需很少的配
置,大部分的时候直接使用默认的配置即可。同时可以与 Spring Cloud 的微服务无缝结合。
提示
Spring Boot2.x 版本环境要求必须是 jdk8 或以上版本,服务器 Tomcat8 或以上版本
2 、优点
使编码变得简单: 推荐使用注解。
使配置变得简单: 自动配置、快速集成新技术能力 没有冗余代码生成和 XML 配置的要求
使部署变得简单: 内嵌 Tomcat Jetty Undertow web 容器,无需以 war 包形式部署
使监控变得简单: 提供运行时的应用监控
使集成变得简单: 对主流开发框架的无配置集成。
使开发变得简单: 极大地提高了开发快速构建项目、部署效率。
Spring Security 安全控制
1 、介绍
Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安
全框架。
2 、功能
Authentication 认证,就是用户登录
Authorization 授权,判断用户拥有什么权限,可以访问什么资源
安全防护,跨站脚本攻击, session 攻击等
非常容易结合 Spring 进行使用

3-验证码功能

验证码分为前端验证码和后端验证码
若依里面的验证是由后端生成,当打开登录页面就向后台发送请求获取验证码
流程: 当登录页面加载后,会之间发送一个获取验证码到后端服务器接口,服务器中会调用该接口,在功能方法中生成一个计算式 1+3=?@4 ,后端通过 @ 分割,把 1+3= ?这个式子返回给前端,把 4 的结果值 放在 redis 中,用来是来实现 2 分钟内可以随时访问的结果。
前端发送请求: http://localhost/dev-api/captchaImage 通过反向代理,最终的请求路径是 http://localhost:8080/captchaImage
后端接口处理:
                        根据/captchaImage去ruoyi-admin 模块,进行查找【 ctrl+shift+f
package com.ruoyi.web.controller.common;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.code.kaptcha.Producer;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.sign.Base64;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.system.service.ISysConfigService;

/**
 * 验证码操作处理
 * 
 * @author ruoyi
 */
@RestController
public class CaptchaController
{
    @Resource(name = "captchaProducer")
    private Producer captchaProducer;

    @Resource(name = "captchaProducerMath")
    private Producer captchaProducerMath;

    @Autowired
    private RedisCache redisCache;
    
    @Autowired
    private ISysConfigService configService;
    /**
     * 生成验证码
     */
    @GetMapping("/captchaImage")
    public AjaxResult getCode(HttpServletResponse response) throws IOException
    {
        AjaxResult ajax = AjaxResult.success();
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        ajax.put("captchaEnabled", captchaEnabled);
        if (!captchaEnabled)
        {
            return ajax;
        }

        // 生成验证码随机数
        String uuid = IdUtils.simpleUUID();
        //组成验证码的随机key值
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;

        String capStr = null, code = null;
        BufferedImage image = null;

        // 生成验证码
        String captchaType = RuoYiConfig.getCaptchaType();
        if ("math".equals(captchaType))
        {
            //生成验证码表达式 4+3=?@7
            String capText = captchaProducerMath.createText();
            //截取 4+3=?
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            //截取7
            code = capText.substring(capText.lastIndexOf("@") + 1);
            image = captchaProducerMath.createImage(capStr);
        }

        else if ("char".equals(captchaType))
        {
            capStr = code = captchaProducer.createText();
            image = captchaProducer.createImage(capStr);
        }
        //把生成后的验证码值是7 以key的形式保存到redis中,并定时量分钟,便于后期登录验证
        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try
        {
            ImageIO.write(image, "jpg", os);
        }
        catch (IOException e)
        {
            return AjaxResult.error(e.getMessage());
        }

        ajax.put("uuid", uuid);
        ajax.put("img", Base64.encode(os.toByteArray()));
        return ajax;
    }
}

4-前端接口请求处理

前端发送的请求全部都单独封装到了 api 的文件夹中,并且在其中调用了 utils/request.js 中的 axios 进行
异步发送请求,其中 baseURL 的值是调用 .env.development 中的开发环境数据。

        1)验证码请求是在登录页面加载后里面发送:

加完注释后的代码
<template>
  <div class="login">
    <!-- 登录表单组件 -->
    <el-form ref="loginForm" :model="loginForm" :rules="loginFormRules" class="login-form">
      <!-- 系统标题 -->
      <h3 class="title">若依后台管理系统</h3>

      <!-- 用户名输入框 -->
      <el-form-item prop="username">
        <el-input
          v-model="loginForm.username"
          type="text"
          auto-complete="off"
          placeholder="账号"
        >
          <!-- 用户图标插槽 -->
          <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
        </el-input>
      </el-form-item>

      <!-- 密码输入框 -->
      <el-form-item prop="password">
        <el-input
          v-model="loginForm.password"
          type="password"
          auto-complete="off"
          placeholder="密码"
          @keyup.enter.native="handleLogin"
        >
          <!-- 密码图标插槽 -->
          <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
        </el-input>
      </el-form-item>

      <!-- 验证码输入框(根据captchaEnabled开关显示) -->
      <el-form-item prop="code" v-if="captchaEnabled">
        <el-input
          v-model="loginForm.code"
          auto-complete="off"
          placeholder="验证码"
          style="width: 63%"
          @keyup.enter.native="handleLogin"
        >
          <!-- 验证码图标插槽 -->
          <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
        </el-input>

        <!-- 验证码图片,点击刷新 -->
        <div class="login-code">
          <img :src="codeUrl" @click="getCode" class="login-code-img" />
        </div>
      </el-form-item>

      <!-- 记住密码复选框 -->
      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>

      <!-- 登录按钮 -->
      <el-form-item style="width:100%;">
        <el-button
          :loading="loading"
          size="medium"
          type="primary"
          style="width:100%;"
          @click.native.prevent="handleLogin"
        >
          <!-- 根据加载状态显示不同文本 -->
          <span v-if="!loading">登 录</span>
          <span v-else>登 录 中...</span>
        </el-button>

        <!-- 注册链接(根据register开关显示) -->
        <div style="float: right;" v-if="register">
          <router-link class="link-type" :to="'/register'">立即注册</router-link>
        </div>
      </el-form-item>
    </el-form>

    <!-- 底部版权信息 -->
    <div class="el-login-footer">
      <span>Copyright © 2018-2024 ruoyi.vip All Rights Reserved.</span>
    </div>
  </div>
</template>

<script>
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt'

export default {
  name: "Login",
  data() {
    return {
      // 验证码图片URL
      codeUrl: "",

      // 登录表单数据模型
      loginForm: {
        username: "admin",
        password: "admin123",
        rememberMe: false,
        code: "",
        uuid: ""
      },

      // 登录表单验证规则
      loginFormRules: {
        username: [
          { required: true, trigger: "blur", message: "请输入您的账号" }
        ],
        password: [
          { required: true, trigger: "blur", message: "请输入您的密码" }
        ],
        code: [{ required: true, trigger: "change", message: "请输入验证码" }]
      },

      // 登录按钮加载状态
      loading: false,

      // 验证码开关
      captchaEnabled: true,

      // 注册开关
      register: false,

      // 重定向路径
      redirect: undefined
    };
  },

  // 监听路由变化,获取重定向参数
  watch: {
    $route: {
      handler(route) {
        this.redirect = route.query && route.query.redirect;
      },
      immediate: true
    }
  },

  // 组件创建时执行
  created() {
    // 获取验证码信息并初始化表单状态
    this.getCode();
    this.getCookie();
  },

  methods: {
    // 获取验证码
    getCode() {
      getCodeImg().then(res => {
        this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled;
        if (this.captchaEnabled) {
          this.codeUrl = "data:image/gif;base64," + res.img;
          this.loginForm.uuid = res.uuid;
        }
      });
    },

    // 从cookie中恢复登录信息
    getCookie() {
      const username = Cookies.get("username");
      const password = Cookies.get("password");
      const rememberMe = Cookies.get('rememberMe')
      this.loginForm = {
        username: username === undefined ? this.loginForm.username : username,
        password: password === undefined ? this.loginForm.password : decrypt(password),
        rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
      };
    },

    // 处理登录操作
    handleLogin() {
      // 表单校验
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true;

          // 处理记住密码功能
          if (this.loginForm.rememberMe) {
            Cookies.set("username", this.loginForm.username, { expires: 30 });
            Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
            Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
          } else {
            Cookies.remove("username");
            Cookies.remove("password");
            Cookies.remove('rememberMe');
          }

          // 发起登录请求
          this.$store.dispatch("Login", this.loginForm).then(() => {
            // 成功后跳转至重定向路径或首页
            this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
          }).catch(() => {
            // 登录失败,重置加载状态和验证码
            this.loading = false;
            if (this.captchaEnabled) {
              this.getCode();
            }
          });
        }
      });
    }
  }
};
</script>

<style rel="stylesheet/scss" lang="scss">
/* 登录页面样式 */
.login {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/login-background.jpg");
  background-size: cover;
}

/* 系统标题样式 */
.title {
  margin: 0px auto 30px auto;
  text-align: center;
  color: #707070;
}

/* 登录表单样式 */
.login-form {
  border-radius: 6px;
  background: #ffffff;
  width: 400px;
  padding: 25px 25px 5px 25px;

  /* 输入框样式 */
  .el-input {
    height: 38px;
    input {
      height: 38px;
    }
  }

  /* 输入框图标样式 */
  .input-icon {
    height: 39px;
    width: 14px;
    margin-left: 2px;
  }
}

/* 提示文字样式 */
.login-tip {
  font-size: 13px;
  text-align: center;
  color: #bfbfbf;
}

/* 验证码区域样式 */
.login-code {
  width: 33%;
  height: 38px;
  float: right;

  /* 验证码图片样式 */
  img {
    cursor: pointer;
    vertical-align: middle;
  }
}

/* 底部版权信息样式 */
.el-login-footer {
  height: 40px;
  line-height: 40px;
  position: fixed;
  bottom: 0;
  width: 100%;
  text-align: center;
  color: #fff;
  font-family: Arial;
  font-size: 12px;
  letter-spacing: 1px;
}

/* 验证码图片样式 */
.login-code-img {
  height: 38px;
}
</style>

2)getCodeImg()是在API中封装好的请求信息

import request from '@/utils/request'

// 登录方法
export function login(username, password, code, uuid) {
  const data = {
    username,
    password,
    code,
    uuid
  }
  return request({
    url: '/login',
    headers: {
      isToken: false,
      repeatSubmit: false
    },
    method: 'post',
    data: data
  })
}

// 注册方法
export function register(data) {
  return request({
    url: '/register',
    headers: {
      isToken: false
    },
    method: 'post',
    data: data
  })
}

// 获取用户详细信息
export function getInfo() {
  return request({
    url: '/getInfo',
    method: 'get'
  })
}

// 退出方法
export function logout() {
  return request({
    url: '/logout',
    method: 'post'
  })
}

// 获取验证码
export function getCodeImg() {
  return request({
    url: '/captchaImage',
    headers: {
      isToken: false
    },
    method: 'get',
    timeout: 20000
  })
}

3)其上的reqeustaxios的封装,在login.js中引入的request.js文件,并在其中设置动态的 baseURL

通过以上解析,就会发现我们就可以在前端发送请求了,以下是以验证码来作为案例解析如下:
验证码请求最终组合后的结果是: http://localhost/dev-api/captchaImage
但是会发现该请求不是请求后端 8080 接口,而是调用的是前端的 80 端口服务。
问题: 难道验证码是从前端获取的数据吗?
答案: 不是的,前端是没有接口的,调用的还是后端接口,只是发送前端请求并在前端中配置了代理服务来转发给后端接口了
        优点:可以解决跨域问题
        代码:在vue.config.js 这里实现了代理的替换工作,流程如下图:
前端发送的网址: http://localhost/dev-api/captchaImage
替换规则:
1. 先把 dev-api 之前的使用 http://localhost:8080 替换 --> http://localhost:8080/dev-api/captchaIma
ge
2. /dev-api 使用空格替换 --> http://localhost:8080/captchaImage
3. 最后把替换的网址进行后端的访问 -> http://localhost:8080/captchaImage
其中vue 项目环境的快速切换可以查看我的这篇文章

5-登录功能

1)登录流程

前端 login.vue 中使用 vuex 中的 Login 事件来调用 API 中的 login 方法来进行发送登录请求,后 端接口处理 login 请求,并接受登录的用户名、密码、验证码值、 uuid 等传入后端,在后端先进行登录前验证,验证通过后再使用 springboot security 方式进行登录认证。认证成功后,后端会使用 uuid 来生成 token 并使用 jwt token 进行加密后,放入 redis 数据库后再返回给前端,让前端把 token 保存到本地的 cookie 中,实现 token 的会话跟踪技术。

2)前端发送请求信息:

                         http://localhost/dev-api/log

3)后端接口接受请求:

package com.ruoyi.web.controller.monitor;

import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.service.SysPasswordService;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.service.ISysLogininforService;

/**
 * 系统访问记录控制器
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/logininfor")
public class SysLogininforController extends BaseController {

    /**
     * 注入系统访问记录服务接口
     */
    @Autowired
    private ISysLogininforService logininforService;

    /**
     * 注入密码服务类,用于处理账户解锁等操作
     */
    @Autowired
    private SysPasswordService passwordService;

    /**
     * 判断当前用户是否有权限查看系统访问记录列表
     * 如果有权限,则允许访问
     */
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysLogininfor logininfor) {
        // 初始化分页参数
        startPage();
        // 调用服务接口查询系统访问记录列表
        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
        // 返回封装好的分页数据
        return getDataTable(list);
    }

    /**
     * 判断当前用户是否有权限导出系统访问记录
     * 如果有权限,则允许导出
     * 记录日志:登录日志(BusinessType.EXPORT)
     */
    @Log(title = "登录日志", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysLogininfor logininfor) {
        // 调用服务接口查询系统访问记录列表
        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
        // 使用ExcelUtil工具类导出Excel文件
        ExcelUtil<SysLogininfor> util = new ExcelUtil<>(SysLogininfor.class);
        util.exportExcel(response, list, "登录日志");
    }

    /**
     * 判断当前用户是否有权限删除系统访问记录
     * 如果有权限,则允许删除
     * 记录日志:登录日志(BusinessType.DELETE)
     */
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
    @Log(title = "登录日志", businessType = BusinessType.DELETE)
    @DeleteMapping("/{infoIds}")
    public AjaxResult remove(@PathVariable Long[] infoIds) {
        // 调用服务接口批量删除指定ID的系统访问记录
        return toAjax(logininforService.deleteLogininforByIds(infoIds));
    }

    /**
     * 判断当前用户是否有权限清理所有系统访问记录
     * 如果有权限,则允许清理
     * 记录日志:登录日志(BusinessType.CLEAN)
     */
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
    @Log(title = "登录日志", businessType = BusinessType.CLEAN)
    @DeleteMapping("/clean")
    public AjaxResult clean() {
        // 调用服务接口清理所有系统访问记录
        logininforService.cleanLogininfor();
        // 返回成功结果
        return success();
    }

    /**
     * 判断当前用户是否有权限解锁账户
     * 如果有权限,则允许解锁
     * 记录日志:账户解锁(BusinessType.OTHER)
     */
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
    @Log(title = "账户解锁", businessType = BusinessType.OTHER)
    @GetMapping("/unlock/{userName}")
    public AjaxResult unlock(@PathVariable("userName") String userName) {
        // 调用密码服务类清除指定用户名的登录失败记录缓存
        passwordService.clearLoginRecordCache(userName);
        // 返回成功结果
        return success();
    }
}

4)在该login中进行登录前验证和springboot security验证

package com.ruoyi.framework.web.service;
/**
     * 登录验证
     * 
     * @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);
    }

5)在调用UserDetailsServiceImpl.loadUserByUsername进行认证

package com.ruoyi.framework.web.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;

/**
 * 用户验证处理服务实现类
 *
 * <p>本类实现了 {@link UserDetailsService} 接口,负责根据用户名加载用户信息,
 * 并进行一系列的身份验证和权限赋予操作,以确保用户能够安全地登录系统。
 *
 * @author ruoyi
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
    /**
     * 日志记录器,用于记录与用户验证相关的日志信息
     */
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    /**
     * 用户服务接口注入,用于查询、验证用户信息
     */
    @Autowired
    private ISysUserService userService;

    /**
     * 密码服务接口注入,用于对用户密码进行验证
     */
    @Autowired
    private SysPasswordService passwordService;

    /**
     * 权限服务接口注入,用于获取用户的菜单权限信息
     */
    @Autowired
    private SysPermissionService permissionService;

    /**
     * {@inheritDoc}
     * <p>
     * 根据用户名加载用户详细信息,并将其封装为Spring Security框架所需的{@link UserDetails}对象。
     * 在此过程中,会执行以下步骤:
     * <ol>
     *   <li>通过用户服务查询数据库,获取对应用户名的用户信息。</li>
     *   <li>检查用户是否存在,已删除,或已停用,如遇上述情况,抛出异常并记录日志。</li>
     *   <li>使用密码服务对用户密码进行验证,确保其有效性。</li>
     *   <li>将验证后的用户信息与用户所拥有的菜单权限相结合,创建一个包含完整身份信息的{@link LoginUser}对象。</li>
     * </ol>
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        // 查询用户信息
        SysUser user = userService.selectUserByUserName(username);

        // 用户不存在或已被删除
        if (StringUtils.isNull(user) || UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 不存在或已被删除.", username);
            throw new ServiceException(MessageUtils.message("user.not.exists" /* 或 "user.password.delete" */));
        }

        // 用户已停用
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException(MessageUtils.message("user.blocked"));
        }

        // 验证用户密码
        passwordService.validate(user);

        // 创建包含权限信息的登录用户对象
        return createLoginUser(user);
    }

    /**
     * 将系统用户信息转化为登录用户对象(包含权限信息)
     *
     * @param user 系统用户信息
     * @return 登录用户对象,包含用户ID、部门ID、用户基本信息以及菜单权限列表
     */
    public LoginUser createLoginUser(SysUser user)
    {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}

6)在上面的方法最后调用createLoginUser来查询该登录用户的所有的菜单权限

public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(user.getUserId(), user.getDeptId(), user,
permissionService.getMenuPermission(user));
}
 /**
     * 获取菜单数据权限
     * 
     * @param user 用户信息
     * @return 菜单权限信息
     */
    public Set<String> getMenuPermission(SysUser user)
    {
        Set<String> perms = new HashSet<String>();
        // 管理员拥有所有权限
        if (user.isAdmin())
        {
            perms.add("*:*:*");
        }
        else
        {
            List<SysRole> roles = user.getRoles();
            if (!CollectionUtils.isEmpty(roles))
            {
                // 多角色设置permissions属性,以便数据权限匹配权限
                for (SysRole role : roles)
                {
                    Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());
                    role.setPermissions(rolePerms);
                    perms.addAll(rolePerms);
                }
            }
            else
            {
                perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
            }
        }
        return perms;
    }

7)在第4步的最后面调用tokenService.createToken(loginUser)来生成tokenjwt加密

package com.ruoyi.framework.web.service;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.AddressUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import eu.bitwalker.useragentutils.UserAgent;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

/**
 * Token管理服务类,负责Token的生成、解析、刷新、验证及用户信息的缓存与清除。
 *
 * @author ruoyi
 */
@Component
public class TokenService
{
    private static final Logger log = LoggerFactory.getLogger(TokenService.class);

    // Token相关配置项
    @Value("${token.header}")
    private String header; // Token请求头标识

    @Value("${token.secret}")
    private String secret; // Token加密密钥

    @Value("${token.expireTime}")
    private int expireTime; // Token过期时间(分钟)

    // 时间单位常量
    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; // 20分钟(毫秒)

    @Autowired
    private RedisCache redisCache; // Redis缓存工具类

    /**
     * 从请求中获取当前登录用户信息
     *
     * @param request HTTP请求对象
     * @return 登录用户信息
     */
    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求中的Token
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            try
            {
                // 解析Token,获取用户唯一标识(UUID)
                Claims claims = parseToken(token);
                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);

                // 根据UUID从Redis中获取缓存的登录用户信息
                String userKey = getTokenKey(uuid);
                LoginUser user = redisCache.getCacheObject(userKey);
                return user;
            }
            catch (Exception e)
            {
                log.error("获取用户信息异常: {}", e.getMessage());
            }
        }
        return null;
    }

    /**
     * 将登录用户信息存入缓存
     *
     * @param loginUser 登录用户信息
     */
    public void setLoginUser(LoginUser loginUser)
    {
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken()))
        {
            // 自动刷新Token过期时间并缓存用户信息
            refreshToken(loginUser);
        }
    }

    /**
     * 从缓存中移除指定Token对应的登录用户信息
     *
     * @param token Token值
     */
    public void delLoginUser(String token)
    {
        if (StringUtils.isNotEmpty(token))
        {
            String userKey = getTokenKey(token);
            redisCache.deleteObject(userKey);
        }
    }

    /**
     * 为登录用户生成新的Token
     *
     * @param loginUser 登录用户信息
     * @return 新生成的Token值
     */
    public String createToken(LoginUser loginUser)
    {
        String token = IdUtils.fastUUID(); // 生成新的Token值
        loginUser.setToken(token); // 更新用户信息中的Token值
        setUserAgent(loginUser); // 设置用户代理信息

        // 封装Token中的声明信息
        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);

        // 生成并返回Token字符串
        return createToken(claims);
    }

    /**
     * 验证Token的有效性并自动刷新即将过期的Token
     *
     * @param loginUser 登录用户信息
     */
    public void verifyToken(LoginUser loginUser)
    {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();

        // 如果Token剩余有效期不足20分钟,则自动刷新
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
        {
            refreshToken(loginUser);
        }
    }

    /**
     * 刷新Token的有效期,并更新用户信息在缓存中的过期时间
     *
     * @param loginUser 登录用户信息
     */
    public void refreshToken(LoginUser loginUser)
    {
        // 更新用户登录时间和Token过期时间
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);

        // 根据Token值将登录用户信息缓存到Redis中
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

    /**
     * 设置登录用户的浏览器、操作系统等客户端信息
     *
     * @param loginUser 登录用户信息
     */
    public void setUserAgent(LoginUser loginUser)
    {
        // 解析请求中的User-Agent头信息
        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));

        // 获取用户IP地址
        String ip = IpUtils.getIpAddr();

        // 设置用户信息
        loginUser.setIpaddr(ip);
        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        loginUser.setBrowser(userAgent.getBrowser().getName());
        loginUser.setOs(userAgent.getOperatingSystem().getName());
    }

    /**
     * 从数据声明生成Token字符串
     *
     * @param claims 数据声明
     * @return 生成的Token字符串
     */
    private String createToken(Map<String, Object> claims)
    {
        // 使用HS512算法和密钥构建并签名Token
        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 解析Token字符串,获取其中的数据声明
     *
     * @param token 待解析的Token字符串
     * @return 数据声明对象
     */
    private Claims parseToken(String token)
    {
        // 使用密钥解析并验证Token
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 从Token中提取用户名
     *
     * @param token Token字符串
     * @return 用户名
     */
    public String getUsernameFromToken(String token)
    {
        Claims claims = parseToken(token);
        return claims.getSubject();
    }

    /**
     * 从请求中获取Token值
     *
     * @param request HTTP请求对象
     * @return Token值
     */
    private String getToken(HttpServletRequest request)
    {
        // 从请求头中获取Token,去除前缀
        String token = request.getHeader(header);
        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
        {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

    /**
     * 根据用户唯一标识(UUID)生成Redis中存储登录用户信息的键名
     *
     * @param uuid 用户唯一标识(UUID)
     * @return 缓存键名
     */
    private String getTokenKey(String uuid)
    {
        return CacheConstants.LOGIN_TOKEN_KEY + uuid;
    }
}

8)最后在第3步中,把生成的token返回给前端,并在前端进行cookie保存

6-Token

Token 是服务端生成的一串字符串,以作客户端进行请求的一个令牌, 当第一次登录后,服务器生成一个 Token 便将此 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次带 上用户名和密码
大概的流程:
        1)客户端使用用户名和密码请求登录( 服务器 -- 后端 )
        2)服务端收到请求,验证用户名和密码
        3)验证成功后,服务端( 后台 ) 会生成一个 token ,然后把这个 token 发送给客户端 ( 前端 )
        4)客户端收到token 后把它存储起来,可以放在 cookie 或者 Local Storage (本地存储)里 【 Vuex
        5)客户端每次向服务端发送请求的时候都需要带上服务端发给的token( 客户端 -- 前端 )
        6)服务端收到请求,然后去验证客户端请求里面带着token ,如果验证成功,就向客户端返回请求的 数据
        7)这个token 必须在每次请求时传递给服务器,应该保存在请求头里,另外服务器要支持 CORS( 跨域 资源共享 ) 的策略。

7-注册功能

若依管理系统也实现了注册功能,只是隐藏起来了而已;

        1)在登录页面中开启注册跳转的变量值改为true

        2)在配置文件中开启注册功能

        3)注册成功

注册后的用户即可实现登录功能了(但是没有啥权限)。

8-首页动态菜单的加载

流程:登录成功后,前端页面会直接跳转到 / 的根路径下,并开始加载首页效果。在首页中会同时发送 /getInfo /getRoutes 方式从后端接口中获取当前的登录对象信息和该用户下的动态菜单,来丰 富首页。

        1)在登录成功后进行路由页面跳转

        2)在该页面中进行首页的多个组件的组合

3) 动态实现左侧动态菜单

        在首页加载时,发送后端请求动态获取菜单信息

9-欢迎页

修改首页为图表统计,在路由下的 index.js 中修改跳转的页面名称即可

10-公共接口

如果有些接口是不需要验证权限可以公开访问的,这个时候就需要我们给接口放行。
使用注解方式,只需要在 Controller 的类或方法上加入 @Anonymous 该注解即可。
// @PreAuthorize("@ss.xxxx('....')") 注释或删除掉原有的权限注解
@Anonymous
@GetMapping("/list")
public List<SysXxxx> list(SysXxxx xxxx)
{
return xxxxList;
}

【示例】

@RestController
public class TController {
// 全局访问的接口 -- 不同登录直接访问
@Anonymous // 开放接口
@GetMapping("/tt")
public AjaxResult get(){
AjaxResult result = new AjaxResult();
result.put("mess","我的请求不用登录,即可直接访问");
return result;
}
}

11-定制板块

Maven 多模块下新建子模块流程案例。

1)新建业务模块目录,例如: ruoyi-test

2)在 ruoyi-test 业务模块下新建 pom.xml 文件引入公共模块ruoyi-common,后期根据项目需要引入其他jar包即可。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <groupId>com.ruoyi</groupId>
        <artifactId>ruoyi</artifactId>
        <version>x.x.x</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>ruoyi-test</artifactId>

    <description>
        test系统模块
    </description>

    <dependencies>
        <!-- 通用工具-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-common</artifactId>
        </dependency>
    </dependencies>

</project>

3)在父项目的ruoyi的根目录 pom.xml 加入ruoyi-test子模块

<!-- 测试模块-->
<dependency>
    <groupId>com.ruoyi</groupId>
    <artifactId>ruoyi-test</artifactId>
    <version>${ruoyi.version}</version>
</dependency>

4)在根目录 pom.xml 模块节点 modules 添加ruoyi-test子模块

<module>ruoyi-test</module>

5)ruoyi-admin 目录的 pom.xml 添加模块ruoyi-test子模块

<!-- 测试模块-->
<dependency>
    <groupId>com.ruoyi</groupId>
    <artifactId>ruoyi-test</artifactId>
</dependency>

6)测试模块

ruoyi - test 业务模块添加 com.ruoyi.test.controller 包,新建 TController.java
package com.ruoyi.controller;

import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.AjaxResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

// 测试子模块开发
@RestController
public class TmController {

    @Anonymous
    @GetMapping("/tm")
    public AjaxResult get() {
        AjaxResult result = new AjaxResult();
        result.put("mess", "子模块测试开发!!!");
        return result;
    }
}

大总结:

如果我有哪里写的不对,或者不好的地方,欢迎各位大佬指正和批评。

谢谢了

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值