jwt(json web token)
用户发送按照约定,向服务端发送 Header、Payload 和 Signature,并包含认证信息(密码),验证通过后服务端返回一个token,之后用户使用该token作为登录凭证,适合于移动端和api
jwt使用流程
代码实现
1. 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.johnfnash.learn.springboot</groupId>
<artifactId>jwt-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwt-demo</name>
<description>Demo project for JWT</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. JWT token生成及解析工具类
package com.johnfnash.learn.jwt.util;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.util.encoders.Base64;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
public class JwtUtils {
/**
* 签发 JWT
*
* @param id
* @param subject 可以是 JSON 数据,尽可能少
* @param ttlMillis
* @return
*/
public static String createJWT(String userId, String subject, long ttlMillis) {
SignatureAlgorithm algorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// header Map
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
SecretKey secretKey = generalKey();
Claims claims = Jwts.claims();
claims.setId(UUID.randomUUID().toString()) // jti (JWT ID),防止jwt被重新发送
.setSubject(subject) // 主题
.setIssuedAt(now) // 签发时间
.setIssuer("user"); // 签发者
// payload 中 放入自定义信息
Map<String, Object> selfMap = new HashMap<>();
selfMap.put("userId", userId);
claims.putAll(selfMap);
JwtBuilder builder = Jwts.builder().setHeader(map) // header
.setClaims(claims) // 使用 JSON 实例设置 payload
.signWith(algorithm, secretKey); // 签名算法以及密钥
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate); // 过期时间
}
return builder.compact();
}
private static SecretKey generalKey() {
byte[] encodedKey = Base64.decode(SystemConstant.JWT_SECRET);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
*
* 解析JWT字符串
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) {
Claims claims = null;
try {
SecretKey secretKey = generalKey();
claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
} catch (Exception e) {
System.err.println("token 校验失败");
}
return claims;
}
public static void main(String[] args) {
String token = createJWT("aaaa", "同步", 60000L);
System.out.println("token: " + token);
System.out.println(parseJWT(token));
}
}
3. JWT 校验 filter
package com.johnfnash.learn.jwt.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import org.apache.catalina.servlet4preview.http.HttpServletRequest;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import com.johnfnash.learn.jwt.exception.ServiceException;
import com.johnfnash.learn.jwt.util.JwtUtils;
import io.jsonwebtoken.Claims;
/**
* JWT 校验 filter
*/
@WebFilter(filterName = "jwtFilter", urlPatterns = "/hello/*")
public class HTTPBearerAuthorizeAttribute implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
if("options".equals(httpRequest.getMethod())) {
chain.doFilter(request, response);
return;
}
String auth = httpRequest.getHeader("Authorization");
if(auth != null && auth.length() > 7) {
String headStr = auth.substring(0, 6).toLowerCase();
if (headStr.compareTo("bearer") == 0)
{
auth = auth.substring(7, auth.length());
Claims claims = JwtUtils.parseJWT(auth);
if(claims != null) {
chain.doFilter(request, response);
return;
}
}
}
// token 校验失败,抛出异常
throw new ServiceException("token校验失败");
}
@Override
public void destroy() {
}
}
注:
- 从 请求 的header 从获取传入的 token 并进行校验,校验通过则请求走下去;否则,抛出异常,在异常统一处理类中处理。
- 需要在启动类中添加 @ServletComponentScan 注解扫描定义的 filter
@SpringBootApplication
@ServletComponentScan
public class JwtDemoApplication {
public static void main(String[] args) {
SpringApplication.run(JwtDemoApplication.class, args);
}
}
其中,异常处理类代码如下:
package com.johnfnash.learn.jwt.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.johnfnash.learn.jwt.dto.JsonResult;
import com.johnfnash.learn.jwt.enums.StatusCodeEnum;
import com.johnfnash.learn.jwt.exception.ServiceException;
/**
* 统一异常处理
*
*/
@ControllerAdvice
@ResponseBody
public class UnifiedExceptionHandler {
@ExceptionHandler(value = ServiceException.class)
public JsonResult<?> handleAuthorizationException(HttpServletRequest request,
HttpServletResponse response, ServiceException ex) {
JsonResult<?> jr = JsonResult.of(StatusCodeEnum.AUTH_ERR.getCode(), null);
if (ex instanceof ServiceException) {
jr.setMsg(ex.getMessage() != null ? ex.getMessage() : "授权校验失败");
} else {
jr.setMsg("授权校验失败");
}
return jr;
}
}
注:其中 ServiceException 为自定义的业务异常类,JsonResult 为对响应结果进行的一个封装。
4. token 的获取
登录成功之后获取JWT,大致流程如下:
package com.johnfnash.learn.jwt.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.johnfnash.learn.jwt.util.JwtUtils;
@RestController
@RequestMapping("/auth")
public class AuthController {
@RequestMapping(value="/login",method = RequestMethod.POST)
public String login(String loginName, String password) {
// 1. 进行账号、密码校验
// 2. 校验通过之后
String userId = "adadsad";
String jwt = JwtUtils.createJWT(userId, loginName, 1800000);
return jwt;
}
}
5. API 的 token 校验
package com.johnfnash.learn.jwt.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.johnfnash.learn.jwt.dto.JsonResult;
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("detail")
public JsonResult<String> hello() {
return JsonResult.of("hello world!");
}
}
filter 中定义了需要进行过滤的请求的 url pattern,这个 controller 下的接口就是要被过滤,进行token校验的。
6. 测试
1) 登录,获取token
2) 使用上面获取的token进行接口调用
未使用token,获取token错误,或者token过期时
使用正确的token时
注意:token 放在 请求的 Authorization 头中。