JWT 全名 JSON WEB Token 主要作用为用户身份验证, 广泛应用与前后端分离项目当中.
JWT 的优缺点 : https://www.jianshu.com/p/af8360b83a9f
一、pom.xml 引入jar文件
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
二、添加jwt自定义字段 application.yml
# jwt 配置
custom:
jwt:
# header:凭证(校验的变量名)
header: token
# 有效期1天(单位:s)
expire: 5184000
# secret: 秘钥(普通字符串)
secret: aHR0cHM6Ly9teS5vc2NoaW5hLm5ldC91LzM2ODE4Njg=
# 签发者
issuer: test-kou
三、添加加密解密方法
import io.jsonwebtoken.*;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT 工具类
* <p>
* jwt含有三部分:头部(header)、载荷(payload)、签证(signature)
* (1)头部一般有两部分信息:声明类型、声明加密的算法(通常使用HMAC SHA256)
* (2)载荷该部分一般存放一些有效的信息。jwt的标准定义包含五个字段:
* - iss:该JWT的签发者
* - sub: 该JWT所面向的用户
* - aud: 接收该JWT的一方
* - exp(expires): 什么时候过期,这里是一个Unix时间戳
* - iat(issued at): 在什么时候签发的
* (3)签证(signature) JWT最后一个部分。该部分是使用了HS256加密后的数据;包含三个部分
*
* @author kou
*/
@ConfigurationProperties(prefix = "custom.jwt")
@Data
@Component
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
// 秘钥
private String secret;
// 有效时间
private Long expire;
// 用户凭证
private String header;
// 签发者
private String issuer;
/**
* 生成token签名
*
* @param subject
* @return
*/
public String generateToken(String subject) {
final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
Date now = new Date();
// 过期时间
Date expireDate = new Date(now.getTime() + expire * 1000);
//Create the Signature SecretKey
final byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(Base64.getEncoder().encodeToString(getSecret().getBytes()));
final Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
final Map<String, Object> headerMap = new HashMap<>();
headerMap.put("alg", "HS256");
headerMap.put("typ", "JWT");
//add JWT Parameters
final JwtBuilder builder = Jwts.builder()
.setHeaderParams(headerMap)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expireDate)
.setIssuer(getIssuer())
.signWith(signatureAlgorithm, signingKey);
logger.info("JWT[" + builder.compact() + "]");
return builder.compact();
}
/**
* 解析token
*
* @param token token
* @return
*/
public Claims parseToken(String token) {
Claims claims = null;
try {
final byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(Base64.getEncoder().encodeToString(getSecret().getBytes()));
claims = Jwts.parser().setSigningKey(apiKeySecretBytes).parseClaimsJws(token).getBody();
logger.info("Parse JWT token by: ID: {}, Subject: {}, Issuer: {}, Expiration: {}", claims.getId(), claims.getSubject(), claims.getIssuer(), claims.getExpiration());
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException
| IllegalArgumentException e) {
logger.info("Parse JWT errror " + e.getMessage());
return null;
}
return claims;
}
/**
* 判断token是否过期
*
* @param expiration
* @return
*/
public boolean isExpired(Date expiration) {
return expiration.before(new Date());
}
}
四、添加拦截方法,拦截token处理,我这边使用的是filter拦截处理,根据自己需求自定
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* 如果请求中(请求头或者Cookie)中存在JWT,则:
* 1、解析JWT并查找对应的用户信息,然后加入request attribute中
* 2、更新Cookie时间、更新JWT失效时间放入Header
* <p>
* OncePerRequestFilter 一次请求只进入一次filter
*/
@Component
@Slf4j
public class JWTFilter extends OncePerRequestFilter {
public static final String SECURITY_USER = "SECURITY_USER";
// 设置不需要校验的路径
private static final String[] NOT_CHECK_URL = {"/login", "/registered"};
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 判断是否需要对token处理
if (!isNotCheck(request.getRequestURI())) {
// 获取token
String token = getToken(request);
if (StringUtils.isBlank(token)) {
log.info("请求无效,原因:{} 为空!", jwtUtil.getHeader());
request.setAttribute("exceptionCode", SystemParameters.TOKEN_IS_NULL.getIndex());
request.setAttribute("exceptionMessage", "请求无效,原因:" + jwtUtil.getHeader() + " 为空!");
request.getRequestDispatcher("/exception/authentication").forward(request, response);
// throw new AuthenticationException(SystemParameters.TOKEN_IS_NULL.getIndex(), "请求无效,原因:" + jwtUtil.getHeader() + " 为空!");
return;
}
Claims claims = jwtUtil.parseToken(token);
// 判断签名信息
if (null != claims && !claims.isEmpty() && !jwtUtil.isExpired(claims.getExpiration())) {
// 获取签名用户信息
String userId = claims.getSubject();
//获取相应的用户信息,可以在过滤器中先行获取,也可以先保存用户ID,在需要时进行获取
// User user = usersService.findById(Long.valueOf(userId));
request.setAttribute(SECURITY_USER, userId);
Cookie jwtCookie = new Cookie(jwtUtil.getHeader(), URLEncoder.encode(token, "UTF-8"));
jwtCookie.setHttpOnly(true);
jwtCookie.setPath("/");
response.addCookie(jwtCookie);
response.addHeader(jwtUtil.getHeader(), token);
} else {
log.info("{} 无效,请重新登录!", jwtUtil.getHeader());
request.setAttribute("exceptionCode", SystemParameters.AUTHENTICATION_FAILED.getIndex());
request.setAttribute("exceptionMessage", jwtUtil.getHeader() + " 无效,请重新登录!");
request.getRequestDispatcher("/exception/authentication").forward(request, response);
// throw new AuthenticationException(SystemParameters.AUTHENTICATION_FAILED.getIndex(), jwtUtil.getHeader() + " 无效,请重新登录!");
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 获取token
*
* @param request
* @return 返回token
*/
private String getToken(HttpServletRequest request) {
//先从header中获取token
String token = request.getHeader(jwtUtil.getHeader());
//再从cookie中获取
if (StringUtils.isBlank(token)) {
try {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (jwtUtil.getHeader().equals(cookie.getName())) {
token = URLDecoder.decode(cookie.getValue(), "UTF-8");
}
}
}
} catch (Exception e) {
log.error("Can NOT get jwt from cookie -> Message: {}", e);
}
}
return token;
}
/**
* 根据url判断是否需要校验,false需要校验
*
* @param url
* @return 是否需要校验
*/
private boolean isNotCheck(String url) {
// 处理路径以"/" 结尾的"/"
url = url.endsWith("/") ? url.substring(0, url.lastIndexOf("/")) : url;
for (String path : NOT_CHECK_URL) {
// 判断是否以 "/**" 结尾
if (path.endsWith("/**")) {
return url.startsWith(path.substring(0, path.lastIndexOf("/") + 1))
|| url.equals(path.substring(0, path.lastIndexOf("/")));
}
// 判断url == path
if (url.equals(path)) {
return true;
}
}
return false;
}
}
五、配置过滤器
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;
/**
* 过滤器配置
*
* @author kou
*/
@Configuration
public class FilterConfig {
@Autowired
private JWTFilter jwtFilter;
@Bean
public FilterRegistrationBean registrationBean() {
FilterRegistrationBean registration = new FilterRegistrationBean();
// 设置过滤器
registration.setFilter(jwtFilter);
// 设置拦截路径
registration.addUrlPatterns("/rest/*");
// 设置过滤器名称
registration.setName("JWTFilter");
// 设置过滤器执行顺序
registration.setOrder(1);
return registration;
}
}
六、添加异常处理类
/**
* 认证异常类
*
* @author kou
*/
public class AuthenticationException extends RuntimeException {
private static final long serialVersionUID = 1L;
//自定义错误码
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
/**
* Creates a new AuthenticationException.
*/
public AuthenticationException() {
super();
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
*/
public AuthenticationException(String message) {
super(message);
}
/**
* Constructs a new AuthenticationException.
*
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(Throwable cause) {
super(cause);
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
*/
public AuthenticationException(int code, String message) {
super(message);
this.code = code;
}
/**
* Constructs a new AuthenticationException.
*
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(int code, Throwable cause) {
super(cause);
this.code = code;
}
/**
* Constructs a new AuthenticationException.
*
* @param message the reason for the exception
* @param cause the underlying Throwable that caused this exception to be thrown.
*/
public AuthenticationException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}
七、添加全局异常处理类
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 全局异常处理
*
* @author kou
*/
@ControllerAdvice
public class GlobalException {
/**
* 授权登录异常
*/
@ResponseBody
@ExceptionHandler(AuthenticationException.class)
public BaseResult authenticationException(AuthenticationException e) {
return new BaseResult(SystemParameters.FAIL.getIndex(), e.getMessage(), e.getCode());
}
@ExceptionHandler(Exception.class)
@ResponseBody
BaseResult exception(Exception e) {
return new BaseResult(SystemParameters.FAIL.getIndex(), e.getMessage(), SystemParameters.ERROR.getIndex());
}
}
注意:
由于使用filter过滤器,springmvc 不能进行filter拦截异常,故通过请求转发来实现统一异常拦截
八、错误异常处理,用来处理filter转发出来的请求拦截异常,进而让springmvc统一处理异常
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 错误控制器
*
* @author kou
*/
@RestController
public class ErrorController {
@RequestMapping("/exception/authentication")
public BaseResult authenticationException(HttpServletRequest request) {
throw new AuthenticationException(Integer.valueOf(request.getAttribute("exceptionCode").toString()), request.getAttribute("exceptionMessage").toString());
}
}
九、返回统一格式
public class BaseResult {
// 结果状态码
private int ret;
// 结果说明
private String msg;
private int err;
public BaseResult() {
}
public BaseResult(int ret, String msg, int err) {
this.ret = ret;
this.msg = msg;
this.err = err;
}
public int getRet() {
return ret;
}
public void setRet(int ret) {
this.ret = ret;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getErr() {
return err;
}
public void setErr(int err) {
this.err = err;
}
}