springboot 整合 JWT 和请求拦截,实现利用 token 做请求安全拦截校验,且实现阻止并发登录

Posted Johnson_9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot 整合 JWT 和请求拦截,实现利用 token 做请求安全拦截校验,且实现阻止并发登录相关的知识,希望对你有一定的参考价值。

目录

一、导入依赖

二、编写 jwt 工具类,实现生成 token 和解析 token

三、在登录请求中向redis中添加token信息

1、先注入redis的接口类

2、在登录方法中生成token并插入redis,有效期一天

四、实现请求拦截器

1、编写自定义的请求拦截器

2、实现WebMvcConfigurer接口,重写实现其添加拦截器方法

五、测试总结

1、请求拦截

①正确 token

②错误的token

③空token

④从redis中删掉token

2、阻止并发登录

3、总结


一、导入依赖

导入 jwt 的依赖

<!--    jjwt-->
<dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
</dependency>

二、编写 jwt 工具类,实现生成 token 和解析 token

jwt 工作流程

 可以传入具体的用户信息,方便解析校验

// Jwt工具类
public class JwtUtil 

    //private static long time = 1000*10;        // token 有效期为10秒
    private static long time = 1000*60*60*24;   // token 有效期为一天
    private static String signature = "admin";

    // 生成token ,三个参数是我实体类的字段,可根据自身需求来传,一般只需要用户id即可
    public static String createJwtToken(String operNo,String operName ,String organNo)
        JwtBuilder builder = Jwts.builder();
        String jwtToken = builder
                // header
                .setHeaderParam("typ","JWT")
                .setHeaderParam("alg","HS256")
                // payload 载荷
                .claim("operNo",operNo)
                .claim("operName",operName)
                .claim("organNo",organNo)
                .claim("date",new Date())
                .setSubject(operNo)
                .setExpiration(new Date(System.currentTimeMillis()+time))
                .setId(UUID.randomUUID().toString())
                // signature 签名信息
                .signWith(SignatureAlgorithm.HS256,signature)
                // 用.拼接
                .compact();
        return jwtToken;
    

    // 验证token是否还有效,返回具体内容
    public static Claims checkToken(String token)
        if(token == null)
            return null;
        
        JwtParser parser = Jwts.parser();
        try 
            Jws<Claims> claimsJws = parser.setSigningKey(signature).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            System.out.println(claims.get("operNo"));
            System.out.println(claims.get("operName"));
            System.out.println(claims.get("organNo"));
            System.out.println(claims.getId());
            System.out.println(claims.getSubject()); // 签名
            System.out.println(claims.getExpiration()); // 有效期
            // 如果解析token正常,返回claims
            return claims;
        catch (Exception e) 
            // 如果解析token抛出异常,返回null
            return null;
        

    

三、在登录请求中向redis中添加token信息

1、先注入redis的接口类

如果不知道怎么配置redis的可以去看这篇文章 => springboot整合redis并实现mybatis二级缓存

@Autowired
StringRedisTemplate redisTemplate;

2、在登录方法中生成token并插入redis,有效期一天

redis中key值使用字符串 "operToken" 加上用户 id 拼接而成,value 就是 token 的具体内容

也可以插入一个map,redis的键依旧为字符串 "operToken" 加上用户 id 拼接而成,map中的键为token版本号(可以更好的验证并发登录替换了token,不同的随机数即可),值为token的具体内容

// 插入JWT的token
String token = JwtUtil.createJwtToken(loginOper.getOperNo(),loginOper.getOperName(),loginOper.getOrganNo());
loginOper.setToken(token);
// 将JWT的token存入redis,有效期一天
redisTemplate.opsForValue().set("operToken"+loginOper.getOperNo(),token,1, TimeUnit.DAYS);

四、实现请求拦截器

三个请求拦截器

/**前置处理:在业务处理器处理请求之前被调用*/
	boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception;

	/**中置处理:在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView ,现在这个很少使用了*/
	void postHandle(
			HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
			throws Exception;

    /**后置处理:在DispatcherServlet完全处理完请求后被调用,可用于清理资源等*/
	void afterCompletion(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception;

在请求处理之前,切面的给每个请求做一个校验,校验请求头中的token信息是否有效且信息正确

1、编写自定义的请求拦截器

先取出请求头中token的信息,然后判断这个token是否存在,如果存在再去校验这个token是否真实有效,如果有效再取出token中的用户信息,根据用户id来找出redis中的token,再判断redis中的token是否存在,不存在则说明过期了,如果存在则继续对比请求头中的token是否一致,不一致的话则说明token错误或者被别人并发登录,这里就没有办法判断具体是哪种情况,所以用map集合来存入redis就可以更大程度的判断出(先判断最新版本号的token是否和请求头中的相同,再判断过往版本号的token是否有相同,如果有则说明并发登录,如果没有则token错误。加入map之前需要判断map的大小是否超过一定数值,比如5,超过5则删除之前的数据;对map里键值对单独设置过期时也可以)当然,这还是不够精准

@Component // @Component注解一定要加上
public class Interceptor implements HandlerInterceptor 
    // 注入redis
    @Autowired
    StringRedisTemplate redisTemplate;

    // 处理请求之前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        // 取出请求头中Authorization的信息,就是token内容,接下来就是各种判断
        String requestToken = request.getHeader("Authorization");
        if(!StringUtils.isEmpty(requestToken))
            Claims claims = JwtUtil.checkToken(request.getHeader("Authorization"));
            if (claims != null) 
                String token = redisTemplate.opsForValue().get("operToken"+claims.get("operNo"));
                if(Boolean.TRUE.equals(redisTemplate.hasKey("operToken" + claims.get("operNo"))))
                    if(requestToken.equals(token))
                        // token正确
                        return true;
                    else 
                        // token错误,判为并发登录,挤下线
                        // 对应的修改响应头的状态,用于前端判断做出相应的策略
                        response.setStatus(411);
                        return false;
                    
                else 
                    // token不存在于redis中,已过期
                    response.setStatus(410);
                    return false;
                
            
            // 解析token中的用户信息claims为null
            response.setStatus(409);
            return false;
        
        // requestToken为空
        response.setStatus(409);
        return false;
    

    // 处理请求之后执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception 
        System.out.println("处理请求之后执行");
    

2、实现WebMvcConfigurer接口,重写实现其添加拦截器方法

@Component
public class InterceptorConfig  implements WebMvcConfigurer 
    // 注入自定义拦截器
    @Autowired
    private Interceptor interceptor;

    // 重写添加拦截器方法
    @Override
    public void addInterceptors(InterceptorRegistry registry)  // InterceptorRegistry 为拦截器注册对象
        registry.addInterceptor(interceptor)  // 注册自定义拦截器
        .addPathPatterns("/sys/basic-api/**")// 拦截的路径
        .excludePathPatterns(); // 不拦截的路径
    

五、测试总结

1、请求拦截

给刚刚的自定义拦截器加上输出

然后进行测试

①正确 token

 可以发现是先打印输出结果,再执行请求,创建SqlSession

②错误的token

因为是错误的token,token校验是通过不了的,因此返回的用户信息也为空,打印完直接结束,没有执行请求

③空token

 打印完直接结束,没有执行请求

④从redis中删掉token

 redis中查不到token,判定为token过期

2、阻止并发登录

因为每次生成的token中都有随机id,所以每次登录时生成的token肯定都不一样

所以并发登陆以后,之前登录的用户再一次发送请求就会被验证拦截,根据返回的411状态码来判断账号已在别处登录

 

3、总结

利用token来实现请求拦截校验是完全没有问题的,但是现实中可以尽可能的多加几层校验,确保足够的安全

token的存储方式还是有很大的改进空间,虽然也能勉强实现阻止多并发登录,但是不够完善,不能精准判断出具体的错误情况,所以由此引出 spring security

传送门  ==> 前后端分离项目整合spring security,自定义登录验证接口,并精准有效阻止并发登录

前后端分离学习笔记 ---[跨域问题,JWT,路由守卫,Axios设置请求拦截和响应拦截]

跨域问题

跨域是指从一个域名的网页去请求另一个域名的资源。
浏览器对 JavaScript 具有默认的安全限制,同源处理策略;

同源 : 协议,域名,端口都相同;
只要 协议,域名,端口中任何一个是不同的,就是跨域;

如果一个网页可以随意地访问另外一个网站的资源,那么就有可能在客户完全不知情的情况下出现安全问题。
但是在某些情况下时需要跨域的,这次学习前后端项目分离时,前后端服务器的端口不一样,这就需要跨域进行请求和响应数据信息;或者说在同一项目中需要访问子域系统模块,就需要跨域访问;
那么当然也可以不跨域完成,即在一个打开的窗口中内嵌窗口进行链接请求访问.

后端配置解决跨域

跨域资源共享(Cross-originResource Sharing)

  • 实现跨站访问控制,可安全地进行跨站数据传输;
  • 服务器端对于 CORS 的支持,需要设置 Access-Control-Allow-Origin;要在后台中加上响应头来允许域请求.
  • 当浏览器检测到了配置,才允许 Ajax 进行跨域的访问;

(1) 在类或方法上使用注解@CrossOrigin("...配置Ip和端口");仅达到局部配置跨域的效果[仅在使用了注解的地方生效];

比如在上次搭建的案例中就用了这个注解;前后端分离学习笔记(1) —[Vue基础]

(2)手工设置响应头-----使用 HttpServletResponse 对象来添加响应头
(Access-Control-Allow-Origin)来授权原始域
Origin 的值也可以设置为"*" ,表示全部放行

例如

@RequestMapping("/test")
    @ResponseBody
    public String index(HttpServletResponse response)
        response.addHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5277/");
        return "Hello World";
    

(3)使用配置类完成全局跨域设置—>返回一个新的 CorsFilter Bean,并添加映射路径和具体的 CORS
配置信息

配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Collections;


@Configuration
public class CorsConfig 
    @Bean
    public CorsFilter corsFilter() 
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //1,允许任何来源
        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
        //2,允许任何请求头
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        //3,允许任何方法
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        //4,允许凭证
        corsConfiguration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source
                = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    

Json web token

传统的基于session访问鉴权机制

在之前的学习中,后端标识信息的处理一般都放在了后端的session中;

根据 http 协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时发送给我们的应用.

那么当出现不同的客户端,不同的用户时,独立的服务器已无法承载更多的用户,而这时候基于 session 认证就出现问题了,服务器可能会受到恶意的信息交互.

  • 扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,限制了负载均衡器的能力 , 限制了应用的扩展能力。
  • CSRF (跨站请求伪造):因为是基于 cookie 来进行用户识别的, cookie 如果被截获,用户就会很容易受到跨站请求伪造的攻击。

基于 token 的访问鉴权机制

它是无状态的,不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于 token 认证机制的应用不需要去考虑用户在哪一台服务器登录了,便于应用扩展.

  • 用户使用账号和密码发出 post 请求;
  • 服务器使用私钥创建一个 jwt;
  • 服务器返回此 jwt给浏览器;
  • 浏览器将该 jwt 串在请求头中像服务器发送请求;
  • 服务器验证该 jwt;
  • 返回响应的资源到浏览器。


Json web token

JWT 是为在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准((RFC 7519);定义的一种简洁的,自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。
由于是经过签名的,所以通信信息是安全的;JWT 可使用 HMAC 算法/ RSA 的公私秘钥对进行签名.

  • 只要一次登录后;在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证,访问不同的域明也没有问题.
  • 信息交换在通信的双方之间使用 JWT 对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

特点

  • 简洁(Compact): 可以通过 URL,POST 参数或者在 HTTP header 发送,数据量小,当然传输速度也很快.
  • 它具有自包含(Self-contained)性:负载中包含了所有用户所需要的信息,避免了多次查询数据库(但是建议不要将重要信息存入其中[比如密码,私密信息等]);
  • 由于 Token 在传递时会以 JSON 加密的形式发到倒客户端,所以 JWT 是跨语言的,原则上任何 web 形式都支持。
  • 无需在服务端保存会话信息,因为只要系统登录后,它每次接受请求时都会去验证请求头中的token令牌是否符合设定的签名规则,较适用于分布式微服务。

JWT结构
由三部分构成
头部(header)

完整的头部例如:

 'typ': 'JWT',
 'alg': 'HS256'

然后将头部进行 base64 转码,构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
10010101 01010101
100101 010101 01010

载荷(payload, 保存用户的信息[例如用户Id,账号])
签证(signature).

jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:
• header (base64 后的)
• payload (base64 后的)
• secret

具体的搭建引入
在项目的pom.xml中引入依赖

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

配置类设置

package com.xiaozhi.backserver.startspringboot.util;

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

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author by @CSDN 小智RE0
 * @date 2021-12-28 
 */

public class JWTUtil 
    /**
     * jwt生成token
     * @param id 管理员Id
     * @param account 管理员账号
     * @param type    管理员类型
     * @return
     */
    public static String token (Integer id, String account,Integer type)
        String token = "";
        try 
            //过期时间 为1970.1.1 0:0:0 至 过期时间  当前的毫秒值 + 有效时间
            Date expireDate = new Date(new Date().getTime() + 1800*1000);
            //秘钥及加密算法
            Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
            //设置头部信息
            Map<String,Object> header = new HashMap<>();
            header.put("typ","JWT");
            header.put("alg","HS256");
            //携带id,账号信息,生成签名
            token = JWT.create()
                    .withHeader(header)
                    .withClaim("id",id)
                    .withClaim("account",account)
                    .withClaim("type",type)
                    .withExpiresAt(expireDate)
                    .sign(algorithm);
        catch (Exception e)
            e.printStackTrace();
            return  null;
        
        return token;
    

    //验证Token是否还有效; true有效,false失效;
    public static boolean verify(String token)
        try 
            //验签
            Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
         catch (Exception e) //当传过来的token如果有问题,抛出异常
            return false;
        
    

    /**
     * 获得token 中playload部分数据,按需使用
     * @param token
     * @return
     */
    public static DecodedJWT getTokenInfo(String token)
        return JWT.require(Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE")).build().verify(token);
    

后端配置拦截

自定义登录拦截器

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 登录拦截器
 *
 * */
public class LoginInterceptor implements HandlerInterceptor 
    //预处理-->进入到控制器之前
    //返回false 不进入处理器,返回true,就进入拦截器中执行
    //在控制器之前走这个,决定是否走这个控制器
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        //在请求头中取到token令牌;
        String token = request.getHeader("token");
        //验证token;
        boolean b = JWTUtil.verify(token);
        //若令牌不符合则响应信息为 401;
        if(!b)
            response.getWriter().print(401);
        
        return b;
    


配置

import com.xiaozhi.backserver.startspringboot.util.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer

	public void addInterceptors(InterceptorRegistry registry) 
		InterceptorRegistration inter =  registry.addInterceptor(new LoginInterceptor());
				inter.addPathPatterns("/**");
				//放行地址;登录请求访问路径
				inter.excludePathPatterns("/api/login/login");
	

路由守卫

全局配置
在路由配置的index.js中配置路由守卫,若在当前浏览器的session中没有找到token令牌,则强制将回退到登录页面;

//to-将要访问的页面地址,from-从哪个页面访问的,next-放行函数
rout.beforeEach((to, from, next) => 
	//如果用户访问的登录页,直接放行;
	if (to.path == '/login') 
		return next();
	 else 
		//若没有令牌,则推到登录页面;
		var token = window.sessionStorage.getItem("token");
		if (token == null) 
			return next("/login");
		 else 
			next();
		
	
)

Axios设置前端请求拦截和响应拦截

注意这里的401响应拦截,和后端设置的401登录请求拦截正好形成一个前后闭环;

//导入axios;
import axios from 'axios';
//设置访问后台服务器地址
axios.defaults.baseURL = "http://127.0.0.1:5277/api/";
//将 axios 挂载;
Vue.prototype.$http = axios;

//全局请求拦截和响应拦截配置;
//axios 请求拦截
axios.interceptors.request.use(config => 
	//为请求头对象,添加 Token 验证的 token 字段;
	config.headers.token = window.sessionStorage.getItem('token');
	return config;
)
// 添加响应拦截器
axios.interceptors.response.use((res) => 
	//后端拦截器拦截状态码;
	if (res.data == 401) 
		//清除session信息;
		window.sessionStorage.clear();
		router.replace("/login");
	
	//服务器异常状态码拦截;
	if(res.data.code == 500)
		this.$message(message:resp.data.msg,type: 'warning');
		return;
	
	return res;
);

基础整合一下登录案例

后端这里的话,和之前的差不多,服务层,数据访问层这里就不放了

/**
 * @author by @CSDN 小智RE0
 */
@RestController
@RequestMapping(value = "/api/login")
public class LoginController 


    CommonResult commonResult;

    @Autowired
    LoginService loginService;


    //跨域接收;
    //@CrossOrigin("http://localhost:8080/")
    @RequestMapping(value = "/login")
    public CommonResult login(@RequestBody Admin admin)
        try
            System.out.println(admin);

            Admin admin1 = loginService.loginUser(admin);
            if(admin1!=null)
                String token = JWTUtil.token(admin1.getId(), admin1.getAccount(),admin1.getType());
                admin1.setToken(token);

                commonResult = new CommonResult(200,"正确",admin1);
            else
                commonResult = new CommonResult(201,"账号或密码错误",null);
            
        catch (Exception e)
            e.printStackTrace();
            commonResult = new CommonResult(500,"服务器错误",null);
        
        return commonResult;
    

需要注意的是,数据响应时,注意分级
响应时同样还是用一个公共类进行封装,数据存储在属性data中;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author by @CSDN 小智RE0
 * @date 2021-12-28 16:02
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult 
    private Integer code;
    private String msg;
    private Object data;

在取得响应的数据时需要写两级data,第一级的data是axios数据请求响应时的数据域,第二级才是公共类中的属性data数据;

前端登录组件Login.vue

<template>
  <div class="login_container">
     <!-- 登录盒子-->
     <div class="login_box">
          <!-- 头像盒子-->
          <div class="img_box">
                <img src="../assets/logo.png" />
          </div>
        <div style="margin-top: 100px; padding-right: 30px;">
			<!-- 登录表单-->
			 <el-form ref="form" :model="form" label-width="80px">
			   <el-form-item label="账号">
			     <el-input v-model="form.account"></el-input>
			   </el-form-item>
			   <el-form-item label="密码">
			     <el-input type="password" v-model="form.password"></el-input>
			   </el-form-item>
			   
			   <el-form-item>
			     <el-button type="primary" plain @click="login()">登录</el-button>
			     <el-button>取消</el-button>
			   </el-form-item>
			 </el-form>
		</div>
     </div>
  </div>
</template>

<script>
	export default
		data:function()
			return
				form:
					account:"",
					password:""
				
			
		,
		methods:
		  login()
			  //注意这里定义 _this, this是vue对象;
			  var _this = this;
			  //console.log(this.form);
			  this.$http.post("login/login",this.form).then(function(resp)
				  //回调函数;
				 console.log(resp);
				 //警告消息弹框;
				  if(resp.data.code == 201)
					  _this.$message(message:resp.data.msg,type: 'warning');
					  Java安全框架-Springboot整合JWT

JWT整合Springboot

前后端分离学习笔记 ---[跨域问题,JWT,路由守卫,Axios设置请求拦截和响应拦截]

前后端分离学习笔记 ---[跨域问题,JWT,路由守卫,Axios设置请求拦截和响应拦截]

深入总结SpringBoot整合JWT,这应该是全网讲的最通俗易懂的了

总结关于spring security 使用 JWT 和 账户密码登录 整合在一起的新感悟