一、使用场景
单体应用的登录验证与接口保护:有时我们并不想希望我们的所有接口都是完全开放的,比如用户登陆后获取个人信息等接口,因此需要对用户进行验证,而验证过程需要在访问接口之前进行,因此可通过拦截器来方便的实现。(更复杂的认证授权可使用Shiro或SpringSecurity)
二、拦截器
拦截器会在访问接口之前对请求进行拦截,因此可以在拦截器中对接口请求进行验证,只有通过认证才能对资源进行访问,不然直接返回。
三、JWT令牌认证
拦截器对请求进行拦截了以后,就需要对用户进行认证,而JWT是当前使用最多的方式,相对于session来说减小了服务器的压力。JWT基于Json,令牌(token)中内容易扩展、数字签名防篡改、安全性高。地址:JWT官网
实现流程: 用户通过用户名和密码登录以后,进行用户名和密码的比对,比对无误则根据用户名等(如id,TEL)信息生成token(生成协议为WT),返回给前端,前端将该token存储在Cookie或LocalStorage中,每次调用接口时都会将该token放到Header(请求头)中,用于后端的接口进行认证。
简单理解:通过二维码(用户名和密码)去售票机(登录接口)取景区门票(token),然后拿着门票(token)去玩各个项目(资源接口),玩之前检票员(拦截器)会对票进行检验(令牌合法性的验证)。
四、JWT工具类
既然需要这个令牌(token),那么就应有一套系统来生成、验证令牌(token)。而JWT这个工具类的目的就是用来生成令牌、验证令牌的合法性以及从令牌中获取令牌包含的信息。代码如下:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
public class JwtUtils {
public static final long EXPIRE = 1000 * 60 * 60 * 12; // token过期时间 12小时
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; // 加密的密钥
// 生成token字符串
public static String getJwtToken(String id, String nickname){
String JwtToken = Jwts.builder()
// 头信息
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256") // 加密方式
// 设置过期时间
.setSubject("xxx") // 项目名
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
// 设置用户信息 可以加多个
.claim("id", id)
.claim("nickname", nickname)
// 签名方式
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
return JwtToken;
}
// 判断token的合法性、有效期等进行判断,直接对token进行判断
// 注意这里的异常直接交给拦截器中去处理
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
throw e; // 抛出异常交给拦截器处理
}
return true;
}
// 判断token是否存在与有效,从请求头中获取token
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
// 根据token获取用户信息
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody(); // 得到用户数据的主体
return (String)claims.get("id");
}
// 根据token获取用户信息 从请求头中获取token
public static String getMemberNickNameByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody(); // 得到用户数据的主体
return (String)claims.get("nickname");
}
}
五、颁发令牌
这里需要做的就是用户登陆并验证通过后对用户颁发一个令牌(token),这样用户再来访问接口的时候就带着这个令牌来访问。
@PostMapping("login")
public DataReturn login(@RequestBody User user) {
// 1. 验证用户名和密码
// 2. 验证通过则通过工具类中的getJwtToken方法生成token并返回
}
六、请求拦截
使用拦截器也非常的简单,只需要实现在拦截器类与拦截器的配置类中实现HandlerInterceptor和WebMvcConfigurer接口,并进行相应的配置。(这两个类名可随意取,只要实现这两个接口即可)
① 拦截器类:JwtInterception.class
注意1:这里通过了所有的OPTION请求,因为浏览器在发送请求之前会预发送一个OPTION,不过滤掉虽然可以访问到结果,但是会生成大量错误日志。同时这里对验证token产生的异常时对不同的异常进行了处理,如无效、过期等,并返回对应的信息到前端。
注意2:这里直接通过request.getHeader("token")
获取token,因此前端在请求头中放token时不需要使用Authorization Bearer +token的方式,直接token:token值 即可,当然可根据自己实际情况修改。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.xxxxutils.JwtUtils;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
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 JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(request.getMethod().toUpperCase().equals("OPTIONS")){
return true; // 通过所有OPTION请求
} else {
String token = request.getHeader("token"); // 获取请求头中的token
Map<String, Object> map = new HashMap<>();
try {
boolean verify = JwtUtils.checkToken(token);
if (verify) {
return true; // 通过验证
} else {
return false; // 未通过验证
}
} catch (SignatureException e) {
e.printStackTrace();
map.put("msg", "无效签名");
} catch (UnsupportedJwtException e) {
e.printStackTrace();
map.put("msg", "不支持的签名");
} catch (ExpiredJwtException e) {
e.printStackTrace();
map.put("msg", "token过期");
} catch (MalformedJwtException e) { // IllegalArgumentException
e.printStackTrace();
map.put("msg", "不支持的签名格式");
} catch (Exception e) {
e.printStackTrace();
map.put("msg", "token无效");
}
map.put("state", false);
// 将map转为json
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
}
② 拦截器配置类:InterceptionConfig.class
只需要实现WebMvcConfigurer接口并进行拦截器的配置,包括对什么样的请求进行拦截,对什么样的请求不拦截。
import com.ziyue.courseservice.interceptors.JwtInterceptor;
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
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor())
.addPathPatterns("/service/**") // 拦截的请求 /service/** 表示拦截service下所有
.excludePathPatterns("/user/login"); // 不拦截的请求 如登录接口
}
}
Email : beyonderwei@Gmail.com
Website : http://beyonderwei.com
WeChat: