1. 应用场景
前后端分离项目保持登录状态。
问题:ajax请求如何跨域,将无法携带jsessionid,这样会导致服务器端的session不可用。如何解决?
后端:
登录接口在验证过用户密码后,将用户的身份信息转换成一个特殊格式的字符串(token),放入响应体(或响应头)。
前端:
浏览器收到登录成功响应后,保存该token在本地,当下次发送请求时,取出该token并携带在请求头(或请求体中)。
后端:
使用filter或者拦截器校验请求中的token是否有效,如果有效就代表用户已登录,放行。
2. 对于token的需求
(1) 可以自定义信息(用户身份、权限信息)
(2) 需要使得服务器端可以进行有效性校验(即token只能由服务器端颁发,不能由其他第三方伪造)
(3) 需要能设置有效期
3. JWT的数据结构
https://jwt.io/ 官网地址
(1)HEADER //头
(2)PAYLOAD //体
(3)SIGNATURE //签名
4. 生成JWT
HMACSHA256(
base64UrlEncode(header) + "." +base64UrlEncode(payload),
your-256-bit-secret
)
(1)计算header的base64编码
(2)计算payload的base64编码
(3)计算签名
5. 校验token是否合法
原理:重新计算token的sign,与token中的sign进行比对,一致则合法。
6. 校验token有效期
原理,在jwt的payload中加入过期日期,然后在校验token时检查是否过期
这是一个测试类需要导入Hutool工具类
java
package com.qf.fmall.jwt;
import cn.hutool.jwt.JWT;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
public class TestJwt {
public static final String secretkey="qfsyjava2302";
public static void main(String[] args) throws UnsupportedEncodingException, InterruptedException {
String token1 = createToken();
//验证token的合法性
// String token="eyJoZWFkZXIwMSI6InRlc3QxMjMiLCJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsdWZmeSIsImlhdCI6MTY5MzIwNjc2NiwicGhvbmUiOiIxMzg5OTk5ODg4OCIsInNleCI6MSwiYWdlIjoxOX0.UY3v3twt30GAN6iCOlTTZwA0YpwMBFDg3JyWgxp1_7U";
Thread.sleep(3000);
//使用hutool工具类校验Jwt
boolean validate = JWT.of(token1)//把token字符串传入,用来构建jwt对象
.setKey(secretkey.getBytes("utf-8"))//传入计算签名要使用的密钥
// .verify() 只验证token值,不验证时间
.validate(3l);//校验jwt是否合法,本质上就是重算签名,并且比较签名是否一致。
//这里的leeway是秒,就是在原本的设定的失效时间,容许容忍几秒
System.out.println(validate);
}
public static String createToken() throws UnsupportedEncodingException {
//利用hutool工具类生成Jwt
HashMap<String, Object> payloadMap = new HashMap<>();
payloadMap.put("age",19);
payloadMap.put("sex",1);
payloadMap.put("phone","13899998888");
HashMap<String, Object> headMap = new HashMap<>();
headMap.put("header01","test123");
String token = JWT.create()
.addHeaders(headMap) //放入自定义Jwt 头
.setSubject("luffy")//在 jwt 的payload中放入 sub(代表用户身份信息)
.setExpiresAt(new Date(System.currentTimeMillis()+1000*3))// 在jwt的payload中放入 jwt 失效时间
.setIssuedAt(new Date()) //在 jwt 的payload 中 放入 jwt的签发时间
.addPayloads(payloadMap) // 在jwt 的payload 中放入其他自定义内容
.setKey(secretkey.getBytes("utf-8")) //放入计算jwt 需要的密钥字符串
.sign();
System.out.println(token);
return token;
}
}
7. 开启shiro登录校验
1.用拦截器的方式实现
实现拦截器接口
java
package com.qf.fmall.interceptor;
import cn.hutool.jwt.JWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//0.判断如果是浏览器发送的Option 请求,直接放行
String method = request.getMethod();
if (method.equals("OPTIONS")){
return true;
}
//1.获取 请求头中的 token
String token= request.getHeader("token");
//校验jwttoken
if (token==null){
log.info("没带token,拒绝访问");
return false;
}
boolean validate = JWT.of(token)
.setKey("qfsyjava2302".getBytes("utf-8"))
.validate(0);
return validate;
}
}
注册拦截器
java
package com.qf.fmall.config;
import com.qf.fmall.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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 IntercepterConfig implements WebMvcConfigurer {
@Autowired
JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/index/indeximg");
}
}
2.整合shiro过滤器
1.前端登录方法
java
@GetMapping("/login")
@CrossOrigin
public ResultVo login(@RequestParam("username") String username,@RequestParam("password") String password) throws JsonProcessingException {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token =new UsernamePasswordToken(username,password);
try {
subject.login(token);
Users principal = (Users) subject.getPrincipal();
//当用户登录成功之后,创建jwt
//把principal---> json
ObjectMapper mapper = new ObjectMapper();//获取springboot内置的json转换对象
String userjson = mapper.writeValueAsString(principal);
String jwt = JWT.create()
.setSubject(userjson)
.setExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 30))
.setIssuedAt(new Date())
.setKey("qfsyjava2302".getBytes())
.sign();
return ResultVo.vo(10000,jwt,principal);
} catch (AuthenticationException e) {
e.printStackTrace();
return ResultVo.vo(112300,"failed",null);
}
}
2.后端验证过滤器
java
package com.qf.fmall.filter;
import cn.hutool.jwt.JWT;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.springframework.stereotype.Component;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
@Component("jwt")
@Slf4j
public class JwtFilter extends AccessControlFilter {
/**
* 用于验证访问受保护的方法
* @param servletRequest
* @param servletResponse
* @param o
* @return
* @throws Exception
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
//0.判断如果是浏览器发送的Option 请求,直接放行
String method = request.getMethod();
if (method.equals("OPTIONS")){
return true;
}
//1.获取 请求头中的 token
String token= request.getHeader("token");
//校验jwttoken
if (token==null){
log.info("没带token,拒绝访问");
return false;
}
boolean validate = JWT.of(token)
.setKey("qfsyjava2302".getBytes("utf-8"))
.validate(0);
return validate;
}
/**
* onAccessDenied 方法是Shiro框架中的一个回调方法,当主体(用户)被拒绝访问特定资源时会被调用。该方法不返回布尔值。
* 当主体尝试访问受保护的资源但权限不足或未认证时,Shiro会触发 onAccessDenied 方法。通常,它用于处理被拒绝访问的情况,例如将用户重定向到错误页面或返回错误消息。
* 因此,说 onAccessDenied 方法返回 true 是没有意义的。该方法的目的是处理访问被拒绝的情况,而不是返回布尔值。
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
return false;
}
}
3.让请求走shiro过滤器
在ShiroConfig类下写
java
//配置Shiro过滤器链
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/user/login**","anon");
chainDefinition.addPathDefinition("/user/regist**","anon");
chainDefinition.addPathDefinition("/error","anon");
//让/index/indeximg 由自定义的shiro filter 进行处理, "jwt" 这个值是IOC容器中filter的名字
chainDefinition.addPathDefinition("/index/indeximg","jwt");
// chainDefinition.addPathDefinition("/**","authc");
return chainDefinition;
}
但是shiro不仅会在shiro过滤器链下注册此过滤器,还会在springmvc过滤器链上注册
所以我们,还要让springmvc上的过滤器链上的此过滤器失效
4.让全局过滤器链上的JwtFilter失效
java
package com.qf.fmall.config;
import com.qf.fmall.filter.JwtFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 注意,为了不让 自定义filter也被中注册到全局FitlterChain中,
* 需要添加如下配置类
*/
@Configuration
public class FilterConfig {
@Autowired
JwtFilter jwtFilter;
@Bean
public FilterRegistrationBean<JwtFilter> jwtFilterFilterRegistrationBean(){
FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
//要注册的filter的对象
registrationBean.setFilter(jwtFilter);
//让当前filter不用注册到,全局过滤器链上
registrationBean.setEnabled(false);
return registrationBean;
}
}