SpringBoot+Shiro+JWT+Redis+Mybatis-plus 前后端分离实战项目

在这里插入图片描述

前言

这篇博客是在我上篇发的 SpringBoot+Shiro+Redis+Mybatis-plus 实战项目 之上添加了JWT认证和前后端分离,所以这篇博客重点是贴出 JWT 学习总结的代码,希望可以帮助到大家!
在这里插入图片描述


JWT学习总结

什么是JWT?

JWT 全称就是 JSON WEB TOKEN,可以看作是一个获得请求资格的令牌,我们有了这个令牌,才可以访问到网站的大部分功能(接口)。


JWT的结构?

JWT 分成三段

  1. header
    header 里面主要是放 加密的算法名和类型
  2. payload(负载)
    payload 主要是放一些我们想传递给 token 中保存的用户部分信息字段,比如 用户名 用户ID等
  3. sign(核心安全信息)
    sign 是JWT 的核心安全信息,它其实就是一个字符串,在公司中这个字符串一定不能被暴露出去。

以 . 号连接,有点像 IP 地址的格式
例如: header.payload.sign


在这里插入图片描述


JWT整合SpringBoot的依赖

 <dependency>
     <groupId>com.auth0</groupId>
     <artifactId>java-jwt</artifactId>
     <version>3.15.0</version>
 </dependency>


JWT核心代码配置

JWTUtil

package com.jmu.shiro_demo.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.security.Signature;
import java.util.Calendar;
import java.util.Map;

public class JWTutil {

    private static final String SIGN = "jiachengren"; //JWT 签名
    private static final int DEFAULT_JWT_EXPIRE_DAYS = 7; //默认JWT过期天数

    public static String getToken(Map<String,String> map) {
        JWTCreator.Builder builder = JWT.create();
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,DEFAULT_JWT_EXPIRE_DAYS);
        //1.header 默认
        //2.payload 遍历 map
       map.forEach((k,v) -> {
            builder.withClaim(k,v);
        });
        //3.设置过期时间和签名后生成 token
        String token = builder
                .withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256(SIGN));
        return token;
    }

    public static DecodedJWT verify(String token){
        return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    }

}

JWT拦截器

package com.jmu.shiro_demo.intercepetor;

import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jmu.shiro_demo.utils.JWTutil;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

public class JWTIntercepetor implements HandlerInterceptor {

    private final String TOKEN_NAME = "token";

    //JWT拦截器,所有请求都被这个拦截器拦截,校验header中的token,token校验通过再放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.token一般存放在 header 中,所以从 request.getHeader()中获取 token
        String token = request.getHeader(TOKEN_NAME);
        Map<String,Object> map = new HashMap<String, Object>();
        try {
            JWTutil.verify(token);
            map.put("state",true);
            return true;
        } catch (AlgorithmMismatchException e) {
            map.put("msg","JWT算法不匹配!");
        } catch (SignatureVerificationException e) {
            map.put("msg","JWT签名不匹配!");
        } catch (TokenExpiredException e) {
            map.put("msg","token(用户信息)已经过期,请重新登录!");
        } catch (Exception e) {
            map.put("msg","token无效!");
        }
        map.put("state",false);
        response.setContentType("application/json");
        String msg = new ObjectMapper().writeValueAsString(map);
        response.getWriter().println(msg);
        return false;
    }

}

全局拦截器配置

package com.jmu.shiro_demo.config;

import com.jmu.shiro_demo.intercepetor.JWTIntercepetor;
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 IntercepetorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTIntercepetor())
                .addPathPatterns("/**") //拦截所有请求
                .excludePathPatterns("/logout","/js/**","/index","/getAuthCode","/login","/toLogin","/user/**"); // 放行 登陆请求 下面的所有请求
    }

}

登陆成功的时候生成JWT token 返回给前端

package com.jmu.shiro_demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jmu.shiro_demo.entity.Permission;
import com.jmu.shiro_demo.entity.Role;
import com.jmu.shiro_demo.entity.User;
import com.jmu.shiro_demo.mapper.UserMapper;
import com.jmu.shiro_demo.service.RoleService;
import com.jmu.shiro_demo.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jmu.shiro_demo.utils.JWTutil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author ${author}
 * @since 2021-04-20
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private RoleService roleService;

    @Override
    public String login(String username, String password, String code, HttpSession session, HttpServletResponse response, Model model) {
        //在校验登陆之前先检验验证码的正确性
        String realAuthCode = (String) session.getAttribute("code");
        if(!realAuthCode.equalsIgnoreCase(code)) {
            model.addAttribute("msg","验证码错误!");
            return "login";
        }
        //1.获取 Subject
        Subject subject = SecurityUtils.getSubject();
        //2.封装 token
        UsernamePasswordToken shiroToken = new UsernamePasswordToken(username, password);
        //3.调用 subject.login(token) 方法
        try {
            subject.login(shiroToken);
            //登录成功之后返回 token 给客户端
            Map<String,String> map = new HashMap<String, String>();
            map.put("username",username);
            String token = JWTutil.getToken(map);  //登陆成功,生成JWT token
            response.setHeader("token",token); // 将 token 设置在 header 里面
            model.addAttribute("token",token);
        }catch (UnknownAccountException e1) {
            //用户名不存在
            model.addAttribute("msg","用户名不存在!");
            return "/login";
        }catch (IncorrectCredentialsException e2) {
            model.addAttribute("msg","密码错误!");
            return "/login";
        }
        return "/index";
    }

    @Override
    public User getUserByUserName(String username) {
        QueryWrapper<User> wrapper = new QueryWrapper<User>();
        wrapper.eq("username",username);
        return this.baseMapper.selectOne(wrapper);
    }

    @Override
    public List<Permission> getUserPermissionsByUserId(Integer userId) {
        List<Role> roles = getUserRoleByUserId(userId);
        List<Permission> permissions = new LinkedList<Permission>();
        roles.stream().forEach(role -> {
            permissions.addAll(roleService.getRolePermissionsByRoleId(role.getRoleid()));
        });
        return permissions;
    }

    @Override
    public List<Role> getUserRoleByUserId(Integer userId) {
        return this.baseMapper.getUserRoleByUserId(userId);
    }

    @Override
    public String logout() {
        SecurityUtils.getSubject().logout();
        return "redirect:/index";
    }

}


前端如何利用 JWT token

这是 前后端分离的登陆界面, 登陆成功后在 success 里面进行保存 token 和跳转页面

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <script th:src="@{/js/jquery-3.6.0.slim.min.js}"></script>
</head>
<body>
<h3 style="color: red" th:text="${msg}"/>
<div>
    账号: <input type="text" name="username" id="username"> <br/>
    密码: <input type="password" name="password" id="password"> <br/>
    验证码: <input type="text" name="code" id="code"><img id="changeImg" src="/getAuthCode" alt=""><br/>
    <button id="loginBtn">登陆</button>
</div>
<script>

    $(function () {
        //点击更换验证码   拼接随机数
        $("#changeImg").click(function(){
            $("#changeImg").attr('src',"/getAuthCode?d="+Math.random());
        });
        $('#loginBtn').click(function () {
            let username = $('#username').val()
            let password = $('#password').val()
            let code = $('#code').val()
            $.ajax({
                url:"/user/login",
                data:{"username":username,"password":password,"code":code},
                method:"POST",
                success: function (data) {
                    window.localStorage.setItem("token",data.token) //将token设置在页面的 localStorage 中
                    window.location = data.url //跳转页面
                    console.log(data)
                },
                error: function () {
                    alert("error!")
                }
            })
        })
    })
</script>
</body>
</html>


项目源码(CodeChina平台)

shiro_demo


踩过的坑

  1. 前端报错 $.ajax is not function
    解决:将压缩版本的 jquery 更换成 非压缩的
  2. jquery获取不到 文本框中的值
    解决:使用 $(’#id’).val() 函数
  3. js被拦截
    解决:所有的拦截器的时候放行 /js/**

项目运行

这里主要演示前后端分离下JWT的效果,shiro 和 redis 的效果在上篇实战博客中有发
演示说明,登陆的时候会保存一个 jwt token 到 客户端(浏览器)的localstorage 中,然后每次发起一个后台请求的时候,在 请求头(header)中就会携带这个 token, 后台如果校验这个 token 通过,就响应请求,在这里我的响应请求的方式,就是简单的成功跳转页面。

在这里插入图片描述

小彩蛋: 写这篇博客文章的时候,博主是一边听赵雷(一个低调优秀的民谣歌手)的《鼓楼》这首民谣,真的好听!推荐大家也去听(不是广告🤭)哈哈哈哈😄😄!


总结

JWT 是一种用于减缓认证处理业务过程中的繁琐步骤,提高效率的技术,使用起来也不会很难,但是需要好好理解为什么要用 JWT,为什么会出现 token 令牌机制,最后留个问题给大家思考:JWTtoken需要保存在数据库中吗?有啥想法,都可以在下面评论!希望学到的童鞋可以给博主个三连!👍👍


坚持分享,坚持原创,喜欢博主的靓仔靓女们可以看看博主的首页博客!
您的点赞与收藏是我分享博客的最大赞赏!
博主博客地址: https://blog.csdn.net/weixin_43967679

  • 62
    点赞
  • 109
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 114
    评论
springboot:是一个基于Java开发的框架,简化了Spring应用的初始化配置和部署过程。它提供了一套开发规范和约定,帮助开发人员快速搭建高效稳定的应用程序。 mybatis-plus:是基于MyBatis的增强工具,提供了一些便捷的CRUD操作方法和代码生成功能,简化了数据库操作的开发工作。它能够轻松集成SpringBoot应用中,提高开发效率。 springmvc:是一种基于MVC设计模式的Web框架,用于构建Web应用程序。它能够从URL中解析请求参数,并将请求分发给对应的Controller进行处理。SpringMVC提供了一套灵活的配置和注解方式,支持RESTful风格的API开发。 shiro:是一种用于身份验证和授权的框架,可以集成SpringBoot应用中。它提供了一套简单易用的API,可以处理用户认证、角色授权、会话管理等安全相关的功能。Shiro还支持集成其他认证方式,如LDAP、OAuth等。 redis:是一种开源的内存数据库,采用键值对存储数据。Redis具有高性能、高并发和持久化等特点,常用于缓存、消息队列和分布式锁等场景。在企业级报表后台管理系统中,可以使用Redis来进行缓存数据,提高系统的响应速度和性能。 企业级报表后台管理系统:是一种用于统一管理和生成报表的系统。它通常包括用户权限管理、报表设计、报表生成、数据分析等功能。使用SpringBootMyBatis-Plus、SpringMVC、ShiroRedis等技术,可以快速搭建一个可靠、高效的报表管理系统,满足企业对数据分析和决策的需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jiachengren

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值