(十三)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的登录。


版权声明:本文为wdyan297原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>