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)其上的reqeust是axios的封装,在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)来生成token并jwt加密
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;
}
}
大总结:
如果我有哪里写的不对,或者不好的地方,欢迎各位大佬指正和批评。
谢谢了