最近在使用JWT做一个单点登录与接口鉴权的功能,正好可以对JWT有深一步的了解。
一、JWT使用场景:
1. 授权:用户登录后,每个请求都包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT地一个特性,因为它开销小,并且可以轻松地跨域使用。
2. 信息交换: 对于安全的在各方之间传输信息而言,JWT无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,还可以验证内容有没有被篡改。
使用依赖:
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>7.1</version>
</dependency>
核心代码:
/** * 采用HS256算法生成token * * @param payLoadMap 荷载 * @return * @throws JOSEException */ public static String createTokenHS256(Map<String, Object> payLoadMap) throws JOSEException { JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256); Payload payload = new Payload(new JSONObject(payLoadMap)); JWSObject jwsObject = new JWSObject(jwsHeader, payload); log.info("header : " + jwsHeader); log.info("payload : " + payload); JWSSigner jwsSigner = new MACSigner(TokenConstants.SECRET); jwsObject.sign(jwsSigner); return jwsObject.serialize(); } /** * 解析token * * @param token * @return * @throws ParseException * @throws JOSEException */ public static Map<String, Object> parseTokenHS256(String token) throws ParseException, JOSEException { JWSObject jwsObject = JWSObject.parse(token); JWSVerifier jwsVerifier = new MACVerifier(TokenConstants.SECRET); return verify(jwsObject, jwsVerifier); } /** * 验证token * * @param jwsObject * @param jwsVerifier * @return * @throws JOSEException */ private static Map<String, Object> verify(JWSObject jwsObject, JWSVerifier jwsVerifier) throws JOSEException { Map<String, Object> resultMap = new HashMap<>(); Payload payload = jwsObject.getPayload(); if (jwsObject.verify(jwsVerifier)) { resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_PARSE_SUCCESS); JSONObject jsonObject = payload.toJSONObject(); resultMap.put(TokenConstants.DATA, jsonObject); if (jsonObject.containsKey(TokenConstants.EXPIRE_TIME)) { Long expireTime = Long.valueOf(jsonObject.get(TokenConstants.EXPIRE_TIME).toString()); Long nowTime = System.currentTimeMillis(); log.info("nowTime : " + nowTime); if (nowTime > expireTime) { resultMap.clear(); resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_EXPIRED); } } } else { resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_PARSE_FAILED); } return resultMap; }
单元测试:
@Test public void createTokenTest() { String appId = "appId"; String appSecret = "appSecret"; Map<String, Object> map = new HashMap<>(); map.put("appId", appId); map.put("appSecret", appSecret); Long createTime = System.currentTimeMillis(); map.put("createTime", createTime); map.put("expireTime", createTime + 60 * 60 * 2); try { String token = TokenHS256Util.createTokenHS256(map); System.out.println(token); log.info("token : " + token); } catch (JOSEException e) { log.error("create token failed. "); e.printStackTrace(); } } @Test public void ValidTokenTest() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJhcHBTZWNyZXQiOiJhcHBTZWNyZXQiLCJleHBpcmVUaW1lIjoxNTU3NDczMDI2OTg3LCJjcmVhdGVUaW1lIjoxNTU3NDczMDE5Nzg3LCJhcHBJZCI6ImFwcElkIn0.mNwTFBLOtc3hD90SI7gKV1YlahulOOartZFaLFbqK0Q"; if (token != null) { try { Map<String, Object> validMap = TokenHS256Util.parseTokenHS256(token); Integer result = (Integer) validMap.getOrDefault(TokenConstants.RESULT, TokenConstants.TOKEN_PARSE_FAILED); switch (result) { case TokenConstants.TOKEN_PARSE_SUCCESS: log.info("token parse success."); JSONObject jsonObject = (JSONObject) validMap.getOrDefault(TokenConstants.DATA, StringUtils.EMPTY); log.info("appId = " + jsonObject.getOrDefault("appId", StringUtils.EMPTY)); log.info("appSecret = " + jsonObject.getOrDefault("appSecret", StringUtils.EMPTY)); log.info("sta = " + jsonObject.getOrDefault("sta", StringUtils.EMPTY)); log.info("expire = " + jsonObject.getOrDefault("expire", StringUtils.EMPTY)); break; case TokenConstants.TOKEN_EXPIRED: log.error("token has expired. "); break; default: log.error("token parse failed. "); break; } } catch (ParseException | JOSEException e) { e.printStackTrace(); } } }
二、JWT组成
JWT由三部分组成,它们之间用圆点(.)连接
我们项目里的header内容固定是:
{"alg":"HS256"}
表示使用的是HS256算法,然后用base64多对这个json编码就得到JWT的第一部分,即Header。
payload包含声明(要求),声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型:registered,public,private
registered claims: 这里有一组预定义的声明,它们不是强制的,但是推荐。比如iss(issure), exp(expiration time), sub(subject), aud(audience)等
public claims: 可以随意定义
private claims: 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
例如:
{"appSecret":"appSecret","expireTime":1558059574173,"createTime":1558059566973,"appId":"appId"}
对payload进行base64编码就得到JWT的第二部分
注意:不要在JWT的payload或header中放置敏感信息,除非它们是加密的。
Signature:为了得到签名部分,你必须有编码过的header、编码过的payload、一个密钥,签名算法是header中指定的那个,然后对他们签名即可。
签名是用于验证消息在传递过程中有没有被更改,并且对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
三、官网的debugger (https://jwt.io/)
(每次修改verify signature里面的内容,生成的token是会改动的,所以可以验证消息在传递过程中是否有被修改)
四、JWT的工作原理
在认证时,当用户用他们的凭证成功登陆以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,需要非常小心以防出现安全问题。一般而言,保存令牌时不应该超过你所需要它的时间。
无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema。
header 应该看起来是这样的:
Authorization:Bearer <token>
服务器上的受保护的路由将会检查Authorization
header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。
如果JWT包含足够多的必须的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。
如果token是在授权头(Authorization header)中发送的,那么跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。
第一步:请求授权接口,获取授权码token
第二步:使用授权码token访问受保护的资源(比如:API)
五、基于服务器端session的身份认证
HTTP协议是无状态的,即如果我们已经认证了一个用户,那么下一次请求时,服务器不知道来者何人,需要再次进行认证。
传统的做法是将已经认证过的用户信息存储在服务器上,比如session,用户下次请求时带着session ID,然后服务器以此检查用户是否认证过。
缺陷:
1. session: 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器在此处的开销就越大。 2. scalability: 由于session是在内存中,扩展不灵活。 3. CORS: 当想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用Ajax调用从另一个域名下获取资源时,可能会遇到禁止请求的问题。 4. CSRF: 用户很容易受到CSRF攻击。
六、基于token的身份认证
基于token的身份认证是无状态的,服务器或者session中不会存储任何用户信息,没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。
主要流程:
1. 用户携带用户名、密码请求访问 2. 服务器校验用户凭证 3. 应用提供一个token给客户端 4. 客户端存储token,并且在随后的每一次请求中都带着它 5. 服务器校验token并返回数据
注意:每一次请求都要token;token应该放在请求header中,我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin:*
使用token的优势: 1. 无状态和可扩展性 2. 安全,token不是cookie,每次请求的时候token都会被发送,而且由于没有cookie被发送,还有助于防止CSRF攻击。 即使在你的实现中将token存储在客户端的cookie中,这个cookie也只是一种存储机制,而非身份认证机制,没有基于会话的操作。 token在一段时间以后会过期,这个时候用户需要重新登录,这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。