功能28:JWT
功能27:实现用户登录
功能26:新增一个新员工培训页面
功能25:角色管理
功能24:菜单管理
功能23:从后端获取路由/菜单数据
功能22:用户管理
功能21:使用axios发送请求
功能20:使用分页插件
功能19:集成MyBatis-Plus
功能18:创建后端工程
功能17:菜单管理
功能16:角色管理
功能15:用户管理
功能14:使用本地SVG图标库
功能13:侧边栏加入Logo
功能12:折叠/展开侧边栏
功能11:实现面包屑功能
功能10:添加首页菜单项
功能9:退出登录功能
功能8:页面权限控制
功能7:路由全局前置守卫
功能6:动态添加路由记录
功能5:侧边栏菜单动态显示
功能4:首页使用Layout布局
功能3:点击登录按钮实现页面跳转
功能2:静态登录界面
功能1:创建前端项目
前言
JWT(JSON Web Token)是一种基于 Token 的轻量级认证协议,由 RFC 7519 规范定义。其核心思想是:
• 无状态:服务端无需存储 Token,认证信息通过加密签名直接嵌入 Token 中。
• 自包含性:Token 本身包含用户身份、权限等关键数据,服务端只需验证签名即可信任内容。
• 跨域友好:通过 HTTP Header(如 Authorization
)传递,天然支持跨域场景。
Token 结构
JWT 由三部分组成,以 .
分隔:
Header.Payload.Signature
• Header:声明 Token 类型(typ: "JWT"
)和签名算法(如 alg: HS256
)。
• Payload:存放用户身份、权限、过期时间等数据(称为 Claims)。
• Signature:对前两部分的签名,防止数据篡改。
实现JWT的包很多,选一个用的人多的Java JWT。如何选三方包也是开发中的难点。
一.操作步骤
1.引入依赖
<!-- Auth0 JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
2.Controller
src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
- 用户登录成功后,根据用户ID,生成token,返回给前端。
- getRouters接口,解析请求头里的token,获取用户ID
@RestController
public class SysLoginController {
@Autowired
private TokenService tokenService;
@Autowired
private ISysMenuService menuService;
@Autowired
private ISysUserService sysUserService;
@PostMapping("/login")
public AjaxResult login(@RequestBody SysUserDTO userDTO) {
LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
if (userDTO.getUserName() != null) {
lambdaQueryWrapper.eq(SysUser::getUserName, userDTO.getUserName());
}
SysUser sysUser = sysUserService.getOne(lambdaQueryWrapper);
if (sysUser != null && sysUser.getPassword().equals(userDTO.getPassword())) {
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = tokenService.createToken(String.valueOf(sysUser.getUserId()));
ajax.put(Constants.TOKEN, token);
return ajax;
} else {
throw new RuntimeException();
}
}
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters(HttpServletRequest request) {
String userId = tokenService.getToken(request);
return AjaxResult.success(menuService.buildMenus(Long.valueOf(userId)));
}
}
3.配置文件
增加token相关的配置
src/main/resources/application.yml
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyzaaaaaa
# 令牌有效期(默认30分钟)
expireTime: 30
4.配置类
src/main/java/com/ruoyi/framework/web/service/TokenService.java
@Component
public class TokenService {
// 令牌自定义标识
@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;
/**
* 从数据声明生成令牌
*
* @return 令牌
*/
public String createToken(String userId) {
Algorithm algorithm = Algorithm.HMAC256(secret);
// 生成Access Token
return JWT.create()
.withIssuer("com-ruoyi")
.withSubject(userId)
.withExpiresAt(new Date(System.currentTimeMillis() + expireTime * MILLIS_MINUTE))
.withClaim("role", "test")
.sign(algorithm);
}
/**
* 从令牌中获取数据声明
*/
public String parseToken(String token) {
DecodedJWT jwt = JWT.require(Algorithm.HMAC256(secret))
.build()
.verify(token);
return jwt.getSubject();
}
public 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 parseToken(token);
}
}
5.新增常量
src/main/java/com/ruoyi/common/constant/Constants.java
/**
* 令牌前缀
*/
public static final String TOKEN_PREFIX = "Bearer ";
6.前端axios新增请求拦截器
src\utils\request.js
import axios from 'axios'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: import.meta.env.VITE_APP_BASE_API,
// 超时
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
return Promise.resolve(res.data)
},
error => {
return Promise.reject(error)
}
)
export default service
7.修改全局守卫Bug
在获取路由接口失败后,注销后跳转到根目录。
src\permission.js
import router from './router'
import { getToken } from '@/utils/auth'
import usePermissionStore from '@/stores/permission'
import useUserStore from '@/stores/user'
import { ElMessage } from 'element-plus'
const whiteList = ['/login']
const whiteListDict = whiteList.reduce((acc, cur) => {
acc[cur] = true;
return acc;
}, {});
router.beforeEach(async (to, from, next) => {
if (getToken()) {
if (whiteListDict[to.path]) {
next({ path: '/' })
} else {
if (router.getRoutes().length <= 3) {
try {
const newRouteRecord = await usePermissionStore().generateRoutes()
newRouteRecord.forEach(route => {
router.addRoute(route) // 动态添加可访问路由表
})
next({ ...to, replace: true })
} catch (error) {
useUserStore().logout().then(() => {
ElMessage.error(error)
next({ path: '/' })
})
}
} else {
next()
}
}
} else {
// 没有token
if (whiteListDict[to.path]) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
}
}
})
二.功能验证
运行项目,浏览器访问http://localhost:5173/index
输入用户名和密码登录
浏览器使用开发者工具,查看getRouters请求是否带上token
debug后端程序,检查在getRouters里获取的userId是否是登录用户的userId
三.思考
目前的用户管理,角色管理,菜单管理的新增,都只做了单表的新增,如何才能把用户,角色,菜单三张表关联起来?