springboot 整合 JWT 和请求拦截,实现利用 token 做请求安全拦截校验,且实现阻止并发登录
Posted Johnson_9
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot 整合 JWT 和请求拦截,实现利用 token 做请求安全拦截校验,且实现阻止并发登录相关的知识,希望对你有一定的参考价值。
目录
二、编写 jwt 工具类,实现生成 token 和解析 token
2、实现WebMvcConfigurer接口,重写实现其添加拦截器方法
一、导入依赖
导入 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,路由守卫,Axios设置请求拦截和响应拦截]
前后端分离学习笔记 ---[跨域问题,JWT,路由守卫,Axios设置请求拦截和响应拦截]