session认证的缺陷实在很明显,由于session是保存在做事器里,以是如果分布式支配运用的话,会涌现session不能共享的问题,很难扩展。于是乎为理解决session共享的问题,又引入了redis,接着往下看。
token认证这种办法跟session的办法流程差不多,不同的地方在于保存的是一个token值到redis,token一样平常是一串随机的字符(比如UUID),value一样平常是用户ID,并且设置一个过期韶光。每次要求做事的时候带上token在要求头,后端吸收到token则根据token查一下redis是否存在,如果存在则表示用户已认证,如果token不存在则跳到登录界面让用户重新登录,登录成功后返回一个token值给客户端。
优点是多台做事器都是利用redis来存取token,不存在不共享的问题,以是随意马虎扩展。缺陷是每次要求都须要查一下redis,会造成redis的压力,还有增加了要求的耗时,每个已登录的用户都要保存一个token在redis,也会花费redis的存储空间。

有没有更好的办法呢?接着往下看。
什么是JWTJWT(全称:Json Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的办法,用于作为JSON工具在各方之间安全地传输信息。该信息可以被验证和信赖,由于它是数字署名的。
上面说法比较文绉绉,大略点说便是一种认证机制,让后台知道该要求是来自于受信的客户端。
首先我们先看一个流程图:
流程描述一下:
用户利用账号、密码登录运用,登录的要求发送到Authentication Server。Authentication Server进行用户验证,然后创建JWT字符串返回给客户端。客户端要求接口时,在要求头带上JWT。Application Server验证JWT合法性,如果合法则连续调用运用接口返回结果。可以看出与token办法有一些不同的地方,便是不须要依赖redis,用户信息存储在客户端。以是关键在于天生JWT,和解析JWT这两个地方。
JWT的数据构造JWT一样平常是这样一个字符串,分为三个部分,以"."隔开:
xxxxx.yyyyy.zzzzz
JWT第一部分是头部分,它是一个描述JWT元数据的Json工具,常日如下所示。
{ "alg": "HS256", "typ": "JWT"}
alg属性表示署名利用的算法,默认为HMAC SHA256(写为HS256),typ属性表示令牌的类型,JWT令牌统一写为JWT。
末了,利用Base64 URL算法将上述JSON工具转换为字符串保存。
PayloadJWT第二部分是Payload,也是一个Json工具,除了包含须要通报的数据,还有七个默认的字段供选择。
分别是,iss:发行人、exp:到期韶光、sub:主题、aud:用户、nbf:在此之前不可用、iat:发布韶光、jti:JWT ID用于标识该JWT。
如果自定义字段,可以这样定义:
{ //默认字段 "sub":"主题123", //自定义字段 "name":"java技能爱好者", "isAdmin":"true", "loginTime":"2021-12-05 12:00:03"}
须要把稳的是,默认情形下JWT是未加密的,任何人都可以解读其内容,因此如果一些敏感信息不要存放在此,以防信息透露。
JSON工具也利用Base64 URL算法转换为字符串保存。
SignatureJWT第三部分是署名。是这样天生的,首先须要指定一个secret,该secret仅仅保存在做事器中,担保不能让其他用户知道。然后利用Header指定的算法对Header和Payload进行打算,然后就得出一个署名哈希。也便是Signature。
那么Application Server如何进行验证呢?可以利用JWT前两段,用同一套哈希算法和同一个secret打算一个署名值,然后把打算出来的署名值和收到的JWT第三段比较,如果相同则认证通过。
JWT的优点json格式的通用性,以是JWT可以跨措辞支持,比如Java、JavaScript、PHP、Node等等。可以利用Payload存储一些非敏感的信息。便于传输,JWT构造大略,字节占用小。不须要在做事端保存会话信息,易于运用的扩展。怎么利用JWT首先引入Maven依赖。
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version></dependency>
创建工具类,用于创建jwt字符串和解析jwt。
@Componentpublic class JwtUtil { @Value("${jwt.secretKey}") private String secretKey; public String createJWT(String id, String subject, long ttlMillis, Map<String, Object> map) throws Exception { JwtBuilder builder = Jwts.builder() .setSubject(null) // 发行者 .setId(id) .setSubject(subject) .setIssuedAt(new Date()) // 发行韶光 .signWith(SignatureAlgorithm.HS256, secretKey) // 署名类型 与 密钥 .compressWith(CompressionCodecs.DEFLATE);// 对载荷进行压缩 if (!CollectionUtils.isEmpty(map)) { builder.setClaims(map); } if (ttlMillis > 0) { builder.setExpiration(new Date(System.currentTimeMillis() + ttlMillis)); } return builder.compact(); } public Claims parseJWT(String jwtString) { return Jwts.parser().setSigningKey(secretKey) .parseClaimsJws(jwtString) .getBody(); }}
接着在application.yml配置文件配置jwt.secretKey。
## 用户天生jwt字符串的secretKeyjwt: secretKey: ak47
接着创建一个相应体。
public class BaseResponse { private String code; private String msg; public static BaseResponse success() { return new BaseResponse("0", "成功"); } public static BaseResponse fail() { return new BaseResponse("1", "失落败"); } //布局器、getter、setter方法}public class JwtResponse extends BaseResponse { private String jwtData; public static JwtResponse success(String jwtData) { BaseResponse success = BaseResponse.success(); return new JwtResponse(success.getCode(), success.getMsg(), jwtData); } public static JwtResponse fail(String jwtData) { BaseResponse fail = BaseResponse.fail(); return new JwtResponse(fail.getCode(), fail.getMsg(), jwtData); } //布局器、getter、setter方法}
接着创建一个UserController:
@RestController@RequestMapping("/user")public class UserController { @Resource private UserService userService; @RequestMapping(value = "/login", method = RequestMethod.POST) public JwtResponse login(@RequestParam(name = "userName") String userName, @RequestParam(name = "passWord") String passWord){ String jwt = ""; try { jwt = userService.login(userName, passWord); return JwtResponse.success(jwt); } catch (Exception e) { e.printStackTrace(); return JwtResponse.fail(jwt); } }}
还有UserService:
@Servicepublic class UserServiceImpl implements UserService { @Resource private JwtUtil jwtUtil; @Resource private UserMapper userMapper; @Override public String login(String userName, String passWord) throws Exception { //登录验证 User user = userMapper.findByUserNameAndPassword(userName, passWord); if (user == null) { return null; } //如果能查出,则表示账号密码精确,天生jwt返回 String uuid = UUID.randomUUID().toString().replace("-", ""); HashMap<String, Object> map = new HashMap<>(); map.put("name", user.getName()); map.put("age", user.getAge()); return jwtUtil.createJWT(uuid, "login subject", 0L, map); }}
还有UserMapper.xml:
@Mapperpublic interface UserMapper { User findByUserNameAndPassword(@Param("userName") String userName, @Param("passWord") String passWord);}
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="io.github.yehongzhi.jwtdemo.mapper.UserMapper"> <select id="findByUserNameAndPassword" resultType="io.github.yehongzhi.jwtdemo.model.User"> select from user where user_name = #{userName} and pass_word = #{passWord} </select></mapper>
user表构造如下:
启动项目,然后用POSTMAN要求login接口。
返回的jwt字符串如下:
eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLzE1VslJ6OnHFsxnzX67coKSjlJgOFDEzqAUAAAD__w.qib2DrjRKcFnY77Cuh_b1zSzXfISOpCA-g8PlAZCWoU
接着我们写一个接口吸收这个jwt,并做验证。
@RestController@RequestMapping("/jwt")public class TestController { @Resource private JwtUtil jwtUtil; @RequestMapping("/test") public Map<String, Object> test(@RequestParam("jwt") String jwt) { //这个步骤可以利用自定义表明+AOP编程做解析jwt的逻辑,这里为了简便就直接写在controller里 Claims claims = jwtUtil.parseJWT(jwt); String name = claims.get("name", String.class); String age = claims.get("age", String.class); HashMap<String, Object> map = new HashMap<>(); map.put("name", name); map.put("age", age); map.put("code", "0"); map.put("msg", "要求成功"); return map; }}
像这样能正常解析成功的话,就表示该用户登录未过期,并且已认证成功,以是可以正常调用做事。那么有人会问了,这个jwt字符串能不能被假造呢?
除非你知道secretKey,否则是不能假造的。比如客户端随便猜一个secretKey的值,然后假造一个jwt:
eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLzE1VslJ6OnHFsxnzX67coKSjlJgOFDEzqAUAAAD__w.bHr9p3-t2qR4R50vifRVyaYYImm2viZqiTlDdZHmF5Y
然后传进去解析,会报以下缺点:
还记得事理吧,是根据前面两部分(Header、Payload)加上secretKey利用Header指定的哈希算法打算出第三部分(Signature),以是可以看出最关键便是secretKey。secretKey只有做事端自己知道,以是客户端不知道secretKey的值是假造不了jwt字符串的。
总结末了讲讲JWT的缺陷,任何技能都不是完美的,以是我们得用辩证思维去看待任何一项技能。
安全性没法担保,以是jwt里不能存储敏感数据。由于jwt的payload并没有加密,只是用Base64编码而已。无法中途废弃。由于一旦签发了一个jwt,在到期之前始终都是有效的,如果用户信息发生更新了,只能等旧的jwt过期后重新签发新的jwt。续签问题。当签发的jwt保存在客户端,客户端一贯在操作页面,按道理该当一贯为客户端续长有效韶光,否则当jwt有效期到了就会导致用户须要重新登录。那么怎么为jwt续签呢?最大略粗暴便是每次签发新的jwt,但是由于过于暴力,会影响性能。如果要优雅一点,又要引入Redis办理,但是这又把无状态的jwt硬生生变成了有状态的,违背了初衷。以是印证了那句话,没有最好的技能,只有适宜的技能。感谢大家的阅读,希望看完之后能对你有所收成。
以为有用就点个赞吧,你的点赞是我创作的最大动力~
我是一个努力让大家记住的程序员。我们下期再见!
!
!
能力有限,如果有什么缺点或者不当之处,请大家批评示正,一起学习互换!