(十三)JWT+SpringSecurity实现基于Token的登录–基于SpringBoot+MySQL+Vue+ElementUI+Mybatis前后端分离面向小白管理系统搭建
新手练习-后台管理系统
任务十二 定义统一的返回接口Rusult和异常处理接口
任务十三 JWT+SpringSecurity实现基于Token的登录
前一个任务我们根据商业通用规范,定义了统一返回接口,这样前后端分离开发项目的时候,大家使用统一的返回接口,不但极大提高工作效率,还统一了规范。本次任务,我们完成大家期待已久的登录页面,让系统的逻辑更加规范。通过本次任务,大家能够:
(1)熟练自主设计一个登录或者其他类型页面;
(2)了解Spring Security的工作原理;
(3)理解登录、认证、拦截、放行等基本概念;
(4)实现基于Token的登录。
一、Login页面
1.新建Login.vue页面
新建Login.vue页面,找一个ElementUI中的Container 布局容器,在里面放一个div盒子,设置大小及定位即可,盒子中添加登录页面必须的用户名、密码框及按钮等。登录区域可以在Element UI的表单中找一个类似的,然后改造即可。
初始前端页面搭建代码如下:
<template>
<div class="login_container">
<div class="login_box">
<div style="margin:20px 0; text-align:center; font-size:24px"><b>登录</b></div>
<!-- 用户名-->
<el-form ref="LoginFormRef" :model="loginForm" :rules="LoginFormRules" >
<el-form-item prop="username">
<el-input size="medium" style="margin:10px 0px;width: 300px;margin-left:25px" v-model="loginForm.username" prefix-icon="el-icon-user"></el-input>
</el-form-item>
<!-- 密码-->
<el-form-item prop="password">
<el-input size="medium" style="margin:10px 0px;width: 300px;margin-left:25px" show-password v-model="loginForm.password" prefix-icon="el-icon-lock" type="password"></el-input>
</el-form-item>
<div style="margin:10px 0; text-align:center">
<el-button type="primary" size="small" @click="login" >登录</el-button>
<el-button type="warning" size="small" @click="resetLoginForm">重置</el-button>
</div>
</el-form>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
loginForm: {
username:'',
password:''
},
LoginFormRules:{
username:[
{ required: true, message: '请输入用户名', trigger: 'blur' },
],
password:[
{ required: true, message: '请输入密码', trigger: 'blur' },
]
}
}
},
}
</script>
<style scoped>
.login_container{
background-color: #2b4b6b;
height: 100%;
}
.login_box{
width: 350px;
height: 300px;
background-color: #fff;
border-radius: 3px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%)
}
</style>
2.修改路由文件index.js,添加Login路由
修改路由index.js文件,添加Login路由,并且,在仅输入地址的时候就直接打开登录登录页面,我们对重定向也进行修改,目前的路由还不是动态路由,动态路由将在角色、菜单等维护完成后进行改造。index.js代码如下:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Manage from '../views/Manage.vue'
import User from '../views/User.vue'
import Login from '../views/Login.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Manage',
redirect: '/login',
component: Manage,
children:[
{
path: 'user',
name: 'User',
component: User
},
{
path: 'home',
name: 'Home',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/Home.vue')
}
]
},
{
path: '/login',
name: 'Login',
component: Login
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
注意: 为了保证登录成功后“主页”那个菜单点击正确,原来用/重定向到home页,因为这里的重定向进行了改变,所以Aside组件中的菜单超链接也记得改为/home啊:
<el-menu-item index="/home">
<template slot="title">
<i class="el-icon-house"></i>
<span slot="title">主页</span>
</template>
</el-menu-item>
Aside.vue 的完整代码:
<template>
<el-menu :default-openeds="[]" style="min-height:100%; overflow-x:hidden"
background-color=rgb(48,65,86)
text-color=#ccc
active-text-color=red
router=""
>
<div style="height:60px; line-height:60px; text-align:center">
<img src="../assets/logo.png" style="width:20px;position:relative;top:5px;margin-right:5px"/>
<b style="color:white">后台管理系统</b>
</div>
<el-menu-item index="/home">
<template slot="title">
<i class="el-icon-house"></i>
<span slot="title">主页</span>
</template>
</el-menu-item>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-menu"></i>
<span slot="title">系统管理</span>
</template>
<el-menu-item index="/user">
<i class="el-icon-s-custom"></i>
<span slot="title">用户管理</span>
</el-menu-item>
</el-submenu>
</el-menu>
</template>
<script>
export default {
//输出组件
name: "Aside"
}
</script>
<style scoped>
</style>
3. 运行项目
运行前端项目。输入地址,打开登录页面。
二、后端登录接口
1.新建登录信息类userDTO
一般登录完成的事情比较多,将登录的数据信息单独做一个类,方便后期做各种登录验证、授权等,与前面的实体类User区别一下。在controller包中新建一个包dto并新建一个类userDTO。
userDTO的完整代码如下:
package com.example.demo.controller.dto;
import lombok.Data;
import java.util.List;
//UserDTO用来接受前端登录时传递的用户名和密码
@Data
public class UserDTO {
private String username;
private String password;
private String nickname;
private String token;
//把当前登录用户的角色以及他的菜单项带出来
private String role;
private List<Menu> menus;
}
2. userService类中添加login方法
在userService类中添加login方法,实现登录。
public UserDTO login(UserDTO userDTO) {
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("username",userDTO.getUsername());
queryWrapper.eq("password",userDTO.getPassword());
User one;
try{
one=getOne(queryWrapper);
}catch (Exception e){
throw new ServiceException(Constants.CODE_500,"系统错误");//这里假设查询了多于1条记录,就让他报系统错误
}
if(one!=null){ //以下是登录判断业务
BeanUtil.copyProperties(one,userDTO,true);
return userDTO;//返回登录类userDTO
}else {
throw new ServiceException(Constants.CODE_600,"用户名或密码错误");
}
}
3.userController类添加接口login
在userController类中设置路由,添加接口login并调用userService的login方法。
@PostMapping("/login")
public Result login(@RequestBody UserDTO userDTO){
String username=userDTO.getUsername();//先对userDTO进行是否为空的校验
String password=userDTO.getPassword();
//调用hutool工具中的StrUtil函数实现用户名和密码是否为空的判断
if(StrUtil.isBlank(username) || StrUtil.isBlank(password)){
return Result.error(Constants.CODE_400,"参数错误");
}
UserDTO dto=userService.login(userDTO);
return Result.success(dto);
}
三、 前后端通信实现登录
1. 登录按钮添加login方法
给登录按钮添加login方法。
<el-button type="primary" size="small" @click="login" >登录</el-button>
2. 添加methods,实现login方法
在Login.vue中添加methods,实现login方法。
methods:{
login(){
this.$refs['LoginFormRef'].validate(async (valid) => {
if (valid) {
this.request.post("http://localhost:8084/user/login",this.loginForm).then(res=>{
if(res.code=='200'){
localStorage.setItem("user",JSON.stringify(res.data));//存储用户信息到浏览器
this.$router.push("/home");
this.$message.success("登录成功");
}else{
this.$message.error(res.msg);
}
})
}
})
},
3. 运行项目
实现登录成功。
4.优化Header
修改前端header组件,实现页面显示用户名的 <nickname>
。
(1)将原来的{{name}}替换为{{user.nickname}}
<span>{{user.nickname}}</span><i class="el-icon-arrow-down" style="margin-left:5px"></i>
(2) 添加data,获取login页面存储的user
data() {
return{
user:localStorage.getItem("user")?JSON.parse(localStorage.getItem("user")):{}
}
},
(3) 退出按钮调用logout方法。
<el-dropdown-item>
<span style="text-decoration: none" @click="logout">退出</span>
</el-dropdown-item>
(4)添加logout登出方法。
methods:{
logout(){
this.$router.push("/login");
localStorage.removeItem("user");
this.$message.success("退出成功");
}
}
注意: 如果是一直延续这些任务的同学,这里记着因为直接从数据表中取“昵称”了,所以使用插值方法将原来的{{name}}替换为{{user.nickname}}。
Header.vue的完整代码:
<template>
<el-dropdown style="width:100px; cursor:pointer">
<span>{{user.nickname}}</span><i class="el-icon-arrow-down" style="margin-left:5px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>
<span style="text-decoration: none" @click="logout">退出</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
name: "Header",
props: {
name: String
},
data() {
return{
user:localStorage.getItem("user")?JSON.parse(localStorage.getItem("user")):{}
}
},
methods:{
logout(){
this.$router.push("/login");
localStorage.removeItem("user");
this.$message.success("退出成功");
}
}
}
</script>
<style>
</style>
5. 运行项目
现在运行项目,能够正常登录,而且可以获取到用户个人信息,并且可以实现登出。
四、 集成JWT,实现Token认证
JWT是JSON Web Token的缩写,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。
JWT具体的概念以及定义,优缺点之类的大家可以到网上搜索,讲的很多,简单来说,JWT是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token的生成过程,特别适用于分布式站点的单点登录(SSO) 场景。微服务架构特别流行使用这种登录认证方式。我们本着会用的原则,主要集中在使用上。详细可以参考这篇博客:
https://blog.csdn.net/gjtao1130/article/details/111658060
1. 添加maven依赖
<!--JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
2. 新建包utils,添加TokenUtils类
新建一个包utils,添加TokenUtils类,用来定义生成Token的规则。
packagecom.example.demo.utils;
import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
/**
* 生成token
* @return
*/
public class TokenUtils {
public static String genToken(String userId,String sign){
return JWT.create().withAudience(userId) // 将 user id 保存到 token 里面,作为载荷
.withExpiresAt(DateUtil.offsetHour(new Date(),2)) //2小时后token过期
.sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥
}
}
3. 修改UserDTO
因为返回的时候一定要带上token,所以我们在UserDTO中再加上token(如果已经按照前面的写了,就不用新增了),这时候大家会发现我们单独定义一个返回类的好处了吧,不要和用户类混到一起,做起来非常清楚。
package com.example.demo.controller.dto;
import lombok.Data;
import java.util.List;
//UserDTO用来接受前端登录时传递的用户名和密码
@Data
public class UserDTO {
private String username;
private String password;
private String nickname;
private String token;
}
4. 修改UserService类中的login方法
修改UserService类中的login方法,首先生成token,然后再将token添加到我们的返回类userDTO中。
public UserDTO login(UserDTO userDTO) {
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("username",userDTO.getUsername());
queryWrapper.eq("password",userDTO.getPassword());
User one;
try{
one=getOne(queryWrapper);
}catch (Exception e){
throw new ServiceException(Constants.CODE_500,"系统错误");//这里假设查询了多于1条记录,就让他报系统错误
}
if(one!=null){ //以下是登录判断业务
BeanUtil.copyProperties(one,userDTO,true);
//设置token
String token=TokenUtils.genToken(one.getId().toString(),one.getPassword().toString());
userDTO.setToken(token);
return userDTO;
}else {
throw new ServiceException(Constants.CODE_600,"用户名或密码错误");
}
}
5. 前端运行“登录”
前端运行“登录”之后,打开“开发者工具”,可以在“网络”或者“应用”中查看登录情况,我们可以观察到返回结果user已经有token值。
6.设置请求头
此时大家会发现,只是有了token,但还没有进行登录验证,也就是说,目前在地址栏输入任何一个页面谁都可以访问,这与我们的初衷不一致。接下来就做登录拦截和验证。在utils的request.js添加一个请求头。
request.js完整代码如下。
import axios from 'axios'
const request = axios.create({
baseURL: 'http://localhost:8084',
timeout: 5000
})
// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8';
let user=localStorage.getItem("user")?JSON.parse(localStorage.getItem("user")):{}//获取登录时存放的user对象信息
config.headers['token'] = user.token; // 设置请求头
}, error => {
return Promise.reject(error)
});
// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
response => {
let res = response.data;
// 如果是返回的文件
if (response.config.responseType === 'blob') {
return res
}
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
}
return res;
},
error => {
console.log('err' + error) // for debug
return Promise.reject(error)
}
)
export default request
7. 运行项目
此时运行项目,重新观察,登录时user带的token,和进入之后点击“用户管理”带的token,它们是同一个,也就是说,不同的user登录进行验证,它可以做的操作中都会带一个相同的token,始终进行验证。
8. 新建JwtInterceptor类,设置拦截器
在config包下,添加interceptor包,新建JwtInterceptor类,设置拦截器拦截token。
特别注意:这里的包比较多,导包不要导错,如果运行不成功,逐个参考我这里的进行检查。
package com.example.demo.interceptor;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.sushe.lesson.common.Constants;
import com.sushe.lesson.entity.User;
import com.sushe.lesson.exception.ServiceException;
import com.sushe.lesson.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if(!(handler instanceof HandlerMethod)){
return true;
}
// 执行认证
if (StrUtil.isBlank(token)) {
throw new ServiceException(Constants.CODE_401, "无token验证失败");
}
// 获取 token 中的 userId
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
String errMsg = "token验证失败,请重新登录";
throw new ServiceException(Constants.CODE_401, errMsg);
}
// 根据token中的userid查询数据库
User user = userService.getById(userId);
if (user == null) {
throw new ServiceException(Constants.CODE_401, "用户不存在,请重新登录");
}
// 用户密码加签验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token); // 验证token
} catch (JWTVerificationException e) {
throw new ServiceException(Constants.CODE_401, "token验证失败,请重新登录");
}
return true;
}
}
9. 注册拦截器 在config包下新建类InterceptorConfig
在这个类中我们使用前面已经定义好的JwtInterceptor,然后进行拦截和放行设置。
packagecom.example.demo.config;
import com.example.demo.config.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
// 加自定义拦截器JwtInterceptor,设置拦截规则
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/**") //拦截所有请求,通过判断token是否合法来决定是否登录
.excludePathPatterns("/user/loginVue","/role","/role/page","/**/export","/**/import");//排除这些接口,也就是说,这些接口可以放行
}
@Bean
public JwtInterceptor jwtInterceptor(){
return new JwtInterceptor();
}
}
10.运行项目
现在已经完成登录验证和拦截。这时候如果浏览其他页面,就会提示:
任务总结
请大家注意,这个登录任务还没有涉及到用户权限和菜单,主要实现基于Token的登录,后续任务中我们继续完善角色管理、角色菜单分配、菜单管理等内容后,就可以真正实现登录、认证、授权等内容。通过本次任务,大家能够:
(1)熟练应用Element UI;
(2)基本了解登录认证、拦截、放行等概念;
(3)实现基于Token的登录。