赞
踩
什么是jwt
全名: Json Web Token
是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。
跨域认证的问题
互联网服务离不开用户认证。一般流程是下面这样。
1、用户向服务器发送用户名和密码。
2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
3、服务器向用户返回一个 session_id,写入用户的 Cookie。
4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
jwt实现无状态认证,在服务器端不用保存任何sessionid
每次用户请求服务器都将携带登录时生成的jwt token
在服务器端保存着解密token的密钥
jwt分成3部分
Header(头部)
Payload(负载)
Signature(签名)
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
JWT 的几个特点
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
jwt实战
jwt工具类 JwtUtil
package com.xfxerj.xfxwecat.utils.jwt; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.apache.commons.lang3.StringUtils; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.UUID; public class JwtUtil { //主体发送人 private static final String subjectPrefix="zidingyi"; /** * 过期时间 单位毫秒 */ private static final long ttlMillis=30*24*60*60*1000L; /** * 用户登录成功后生成Jwt * 使用Hs256算法 私匙使用用户密码 * * @param * @param jwtTokenDto 存起来的对象 * @return */ public static String createJWT(JWTTokenDto jwtTokenDto) { //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //生成JWT的时间 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的) Map<String, Object> claims = new HashMap<String, Object>(); if(jwtTokenDto.getUserId()>0){ claims.put("userId", jwtTokenDto.getUserId()); } if(!StringUtils.isEmpty(jwtTokenDto.getUserName())){ claims.put("userName", jwtTokenDto.getUserName()); } if(!StringUtils.isEmpty(jwtTokenDto.getOpenid())){ claims.put("openid", jwtTokenDto.getOpenid()); } //生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取, // 切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。 // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。 String key = jwtTokenDto.getSecretKey(); //生成签发人 String subject = subjectPrefix; //下面就是在为payload添加各种标准声明和私有声明了 //这里其实就是new一个JwtBuilder,设置jwt的body JwtBuilder builder = Jwts.builder() //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值, // 一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setClaims(claims) //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值, // 主要用来作为一次性token,从而回避重放攻击。 .setId(UUID.randomUUID().toString()) //iat: jwt的签发时间 .setIssuedAt(now) //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的, // 作为什么用户的唯一标志。 .setSubject(subject) //设置签名使用的签名算法和签名使用的秘钥 .signWith(signatureAlgorithm, key); if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); //设置过期时间 builder.setExpiration(exp); } return builder.compact(); } /** * Token的解密 * @param token 加密后的token * @param jwtTokenDto 参与加密的对象 * @return */ public static Claims parseJWT(String token, JWTTokenDto jwtTokenDto) { //签名秘钥,和生成的签名的秘钥一模一样 String key = jwtTokenDto.getSecretKey(); //得到DefaultJwtParser Claims claims = Jwts.parser() //设置签名的秘钥 .setSigningKey(key) //设置需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } /** * 校验token * 在这里可以使用官方的校验,我这里校验的是token中携带的密码于数据库一致的话就校验通过 * @param token * @param jwtTokenDto * @return */ public static Boolean isVerify(String token, JWTTokenDto jwtTokenDto) { //签名秘钥,和生成的签名的秘钥一模一样 String key = jwtTokenDto.getSecretKey(); String subject =subjectPrefix; //得到DefaultJwtParser Claims claims = Jwts.parser() //设置签名的秘钥 .setSigningKey(key) //设置需要解析的jwt .parseClaimsJws(token).getBody(); if (claims.getSubject().equals(subject)) { return true; } return false; } public static void main(String[] args) { /*JWTTokenDto jwtTokenDto = new JWTTokenDto(); jwtTokenDto.setUserId("1"); jwtTokenDto.setUserName("黄超"); //加密token String jwt = createJWT(100000, jwtTokenDto); System.out.println(jwt); //解密token Claims claims = parseJWT(jwt, jwtTokenDto); System.out.println(claims); System.out.println(claims.get("userName"));*/ } }
jwtdto
package com.xfxerj.xfxwecat.utils.jwt; public class JWTTokenDto { /** * 秘钥 * 自己随便生成一个密钥 保证数据没有被篡改 */ private static String secretKey = "dfc9df5c52a747a21d79b08f5ebd356c"; /** * 用户名 * 需要传输的字段 */ private String userName; /** * 用户id * 需要传输的字段 */ private Integer userId; /** * 用户唯一标识 * 需要传输的字段 */ private String openid; public String getSecretKey() { return secretKey; } public void setSecretKey(String secretKey) { this.secretKey = secretKey; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } }
什么service 实现类 神略了 直接到apiController
我这里写的是微信小程序的授权登录
@PostMapping("login") public JsonData login(@RequestParam(value = "code",required = true) String code, String encryptedData,String iv, Integer parentId, HttpServletResponse response){ System.out.println("encryptedData: " + encryptedData); User user = userService.save(code,encryptedData,iv,parentId); if(user == null){ return JsonData.buildError("授权失败"); } JWTTokenDto jwtTokenDto = new JWTTokenDto(); jwtTokenDto.setUserId(user.getId()); jwtTokenDto.setUserName(user.getUsername()); jwtTokenDto.setOpenid(user.getOpenid()); //生成jwt String token = JwtUtil.createJWT(jwtTokenDto); //存入redis 过期时间一天 redisUtil.setEx(token, user.getOpenid(), 1, TimeUnit.DAYS); Map<String,String> tokenMap = new HashMap<>(); tokenMap.put("loginToken",token); //jsonData是我封装的一个返回类 return JsonData.buildSuccess(tokenMap); }
pom.xml文件
<!-- JWT相关 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <!-- gson工具,alibaba json 选择随便用一个--> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.0</version> </dependency> <!--alibaba json--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency>
参考链接
添加链接描述
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。