对于一个技术而言,我们不能为了用它而用它,而且应该从业务出发,我为了解决什么问题才用哪种技术。
首先,jwt全称是Json Web Token,在讨论基于Token的身份认证是如何工作的以及它的好处之前,我们先来看一下以前我们是怎么做的:
由于HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证。
传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。
这种基于服务器的身份认证方式存在一些问题:
Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。
Scalability : 由于Session是在内存中的,这就带来一些扩展性的问题。
CORS : 当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用AJAX调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。
CSRF : 用户很容易受到CSRF攻击。
相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。
Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
Session的状态是存储在服务器端,客户端只有session id;而jwt的token的状态是存储在客户端。
同时jwt也很好的解决了跨域的问题。
如果你正在构建从服务器到服务器或客户端到服务器(如:移动应用 APP 或单页面应用)的 API 服务,那么使用 JWT 是非常明智的。比如:
你的客户端需要通过 API 进行身份验证,并返回 JWT
然后,客户端使用返回的 JWT 经过身份验证去请求其它的 API 服务
这些其它 API 服务通过客户端的 JWT 验证客户端是可信的,并且可以执行某些操作无需再次验证
对于这类 API 服务,JWT 非常适合,因为客户端需要频繁进行请求,并且权限是可控的,通常认证数据以无状态方式持久存在,不需要过多依赖用户信息。
如果你正在构建的应用类似单点登录或 OpenID Connect 认证,JWT 同样十分适合,就是实现一种通过第三方验证用户的方法。
研究原理当然是推荐先看一下官网文档:https://jwt.io/introduction/
文档上写的也很清楚,我就简单讲讲,不细说了
jwt分为3部分:
Header
Payload
Signature
完整的token样式如:
xxxxx.yyyyy.zzzzz
示例:
{
“alg”: “HS256”,
“typ”: “JWT”
}
然后使用Base64Url 编码
{
“sub”: “1234567890”,
“name”: “John Doe”,
“admin”: true
}
然后使用Base64Url 编码
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
然后使用Base64Url 编码
最后的token就是 前3部分使用 . 连接起来
官网的libraries https://jwt.io/#libraries 有介绍实现这个的规范的库,我们使用的就是图中的jedis
访问作者的github:https://github.com/jwtk/jjwt ,发现文档写的很详细,大家可以多阅读阅读
但是光使用jwt没有解决注销、修改密码、token自动续签这3个场景。使用redis可以解决,关于实现方式,我们后面再讲。
读完jjwt的文档后就可以上手整合springboot了
<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>
JwtUtil.java
/**
* @author hy
* @date 2019-07-24
*/
@Component
public class JwtUtil {
Logger logger = LoggerFactory.getLogger(JwtUtil.class);
private static JwtUtil jwtUtil;
@Autowired
AppConfig appConfig;
private static SecretKey key;
@PostConstruct
public void init(){
jwtUtil = this;
logger.info("----------------初始化 JWT 成功");
}
public static SecretKey getSecretKey(){
if (key == null){
key = Keys.hmacShaKeyFor(jwtUtil.appConfig.getSecretKey().getBytes());
}
return key;
}
/**
* 获得token
* @param subject 需要携带的json信息
* @return token
*/
public synchronized static String getToken(String subject){
SecretKey key = getSecretKey();
String token = Jwts.builder()
.setIssuer(jwtUtil.appConfig.getAppName())
.setAudience("client")
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
.signWith(key)
.setId(String.valueOf(UUID.randomUUID()))
.compact();
return token;
}
/**
* 获得token
* @param iss 发起方
* @param aud 接收方
* @param exp 过期时间,单位分钟
* @param subject 需要携带的json信息
* @return token
*/
public static String getToken(String iss,String aud,Integer exp,String subject){
SecretKey key = getSecretKey();
String token = Jwts.builder()
.setIssuer(iss)
.setAudience(aud)
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + exp * 60 * 1000))
.signWith(key)
.setId(String.valueOf(UUID.randomUUID()))
.compact();
return token;
}
public static Object parseToken(String token){
String keySecret = jwtUtil.appConfig.getSecretKey();
SecretKey key = Keys.hmacShaKeyFor(keySecret.getBytes());
try {
Claims body = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
return body;
}catch (ExpiredJwtException expiredJwtException){
return ResultCode.TIME_OUT;
}
}
}
这里面我使用了AppConfig 这个配置类,也可以不用,我这么做只是方便在配置文件中修改 secretKey
上面只是实现了一个工具类,我们还需要一个filter才能让jwt更好 的发挥作用。
JwtFilter.java
@Configuration
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String servletPath = request.getServletPath();
if (StringUtils.isEmpty(servletPath)){
return;
}
if (servletPath.startsWith("/test") || servletPath.startsWith("/druid") || servletPath.startsWith("/user/hello") || servletPath.startsWith("/user/login") ){
//放行
filterChain.doFilter(request, response);
return;
}
String authorization = request.getHeader("Authorization");
if (StringUtils.isEmpty(authorization)){
fail(response,ResultCode.ERROR_TOKEN);
return;
}
String prefix = "Bearer ";
if (!authorization.startsWith(prefix)){
fail(response,ResultCode.ERROR_TOKEN);
return;
}
String token = authorization.substring(prefix.length());
if (StringUtils.isEmpty(token)){
fail(response,ResultCode.ERROR_TOKEN);
return;
}
Object r = JwtUtil.parseToken(token);
if (r instanceof Integer){
fail(response, (Integer) r);
}else if (r instanceof Claims){
//放行
filterChain.doFilter(request, response);
}
}
private void fail(HttpServletResponse response,Integer resultCode) throws IOException {
// 失败
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
JsonResult result = new JsonResult(resultCode,"token验证失败");
JSONObject jsonObject = new JSONObject();
jsonObject.put("result", result);
writer.print(jsonObject);
writer.close();
}
}
JwtFilter能够统一验证处理token
使用示例:
@PostMapping(value = "/login")
public String login(
@RequestBody JSONObject in
){
String username = in.getString("username");
String password = in.getString("password");
//验证账户
String token = JwtUtil.getToken(in.toJSONString());
redisUtil.set("token", token);
return redisUtil.get("token");
}
验证成功返回了token后,前端将token存进header的Authorization里,并且每次请求携带这个Authorization就行了