|
| 1 | +# Redis绑定Token |
| 2 | +> 先介绍一下概念 |
| 3 | +
|
| 4 | +## cookie |
| 5 | +HTTP 协议是**无状态的**,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 |
| 6 | + |
| 7 | +Cookie 是服务器发送到用户浏览器并保存在**本地的一小块数据**,它会在浏览器之后向同一服务器**再次发起请求时被携带上**,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 |
| 8 | + |
| 9 | +### 用途 |
| 10 | +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) |
| 11 | +- 个性化设置(如用户自定义设置、主题等) |
| 12 | +- 浏览器行为跟踪(如跟踪分析用户行为等) |
| 13 | + |
| 14 | +## session |
| 15 | +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 |
| 16 | + |
| 17 | +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 |
| 18 | + |
| 19 | +使用 Session 维护用户登录状态的过程如下: |
| 20 | +1. 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; |
| 21 | +2. 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; |
| 22 | +3. 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; |
| 23 | +4. 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 |
| 24 | + |
| 25 | +> 注意:Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 |
| 26 | +
|
| 27 | +### session和cookie选择 |
| 28 | +- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session; |
| 29 | +- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; |
| 30 | +- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 |
| 31 | + |
| 32 | +## Token |
| 33 | +实际上,Token是在服务端将用户信息经过Base64Url编码过后传给在客户端。每次用户请求的时候都会带上这一段信息,因此服务端拿到此信息进行解密后就知道此用户是谁了,这个方法叫做JWT(Json Web Token)。 |
| 34 | + |
| 35 | +## JWT |
| 36 | +> JWT(json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。 |
| 37 | +
|
| 38 | +cookie+session这种模式通常是保存在内存中,而且服务从**单服务到多服务会面临的session共享问题**,随着用户量的增多,开销就会越大。而JWT不是这样的,只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。 |
| 39 | + |
| 40 | +### 构成 |
| 41 | +第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。详情请见[官网](https://jwt.io/introduction/) |
| 42 | + |
| 43 | +简单说一下 |
| 44 | + |
| 45 | +#### header |
| 46 | +是一个Json对象,描述JWT的元数据,通常是下面这样子的: |
| 47 | +```json |
| 48 | +{ |
| 49 | + "alg": "HS256", |
| 50 | + "typ": "JWT" |
| 51 | +} |
| 52 | +``` |
| 53 | +上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。 |
| 54 | + |
| 55 | + |
| 56 | +#### payload |
| 57 | +Payload部分也是一个Json对象,用来存放实际需要传输的数据,JWT官方规定了下面几个官方的字段供选用。 |
| 58 | + |
| 59 | +- iss (issuer):签发人 |
| 60 | +- exp (expiration time):过期时间 |
| 61 | +- sub (subject):主题 |
| 62 | +- aud (audience):受众 |
| 63 | +- nbf (Not Before):生效时间 |
| 64 | +- iat (Issued At):签发时间 |
| 65 | +- jti (JWT ID):编号 |
| 66 | + |
| 67 | +也可以定义私有属性 |
| 68 | + |
| 69 | +#### signature |
| 70 | +Signature部分是对前面的两部分的数据进行签名,防止数据篡改。 |
| 71 | + |
| 72 | +首先需要定义一个秘钥,这个秘钥只有服务器才知道,不能泄露给用户,然后使用Header中指定的签名算法(默认情况是HMAC SHA256),算出签名以后将Header、Payload、Signature三部分拼成一个字符串,每个部分用.分割开来,就可以返给用户了。 |
| 73 | + |
| 74 | +## 在项目中如何写 |
| 75 | + |
| 76 | +### JwtTokenUtil |
| 77 | +这里是使用guns搭建的项目,自带的工具类,我就不详细赘述了,主要是一下我们的业务逻辑,以及如何使用。 |
| 78 | + |
| 79 | + |
| 80 | +### AuthController |
| 81 | +> sb-gateway-auth-AuthController |
| 82 | +
|
| 83 | +能请求到AuthContoroller,说明两种情况: |
| 84 | +1. 你没有携带token |
| 85 | +2. 你token过期了 |
| 86 | + |
| 87 | +而这个业务处理,是为了生成token |
| 88 | + |
| 89 | +```java |
| 90 | + @ApiOperation(value = "获取token接口", notes = "每调用一次,就会随机生成一串token", response = ResponseData.class) |
| 91 | + @RequestMapping(value = "${jwt.auth-path}") |
| 92 | + public ResponseData createAuthenticationToken(@Validated AuthRequest authRequest, BindingResult bindingResult) { |
| 93 | + //Validated 的作用是抵消了使用很多if判断 |
| 94 | + if (bindingResult.hasErrors()) { |
| 95 | + for (FieldError fieldError : bindingResult.getFieldErrors()) { |
| 96 | + log.error("参数:{}校验失败,原因:{}", fieldError.getField(), fieldError.getDefaultMessage()); |
| 97 | + } |
| 98 | + return new ResponseUtil<>().setErrorMsg("用户参数设置错误:" + CommonBindingResult.getErrors(bindingResult)); |
| 99 | + } |
| 100 | + // 获取用户的账号和密码 |
| 101 | + UserLoginRequst req = new UserLoginRequst(); |
| 102 | + req.setUsername(authRequest.getUserName()); |
| 103 | + req.setPassword(authRequest.getPassword()); |
| 104 | + // 调用userAPI.login业务获取用户id |
| 105 | + UserLoginResponse res = userAPI.login(req); |
| 106 | + String userId = "" + res.getUserId(); |
| 107 | + if (res.getUserId() != 0) { |
| 108 | + // 如果id不等于0,说明账户存 |
| 109 | + // 从redis看一下userId在不在 |
| 110 | + if (redisUtils.hasKey(userId)) { |
| 111 | + // 如果存在,两种情况:1.你在其他登陆,2.别人登陆 |
| 112 | + // 删除redis中的键为userId的 |
| 113 | + redisUtils.del(userId); |
| 114 | + } |
| 115 | + // 以下两步,针对于userId生成token |
| 116 | + res.setRandomKey(jwtTokenUtil.getRandomKey()); |
| 117 | + String token = jwtTokenUtil.generateToken(userId, res.getRandomKey()); |
| 118 | + res.setToken(token); |
| 119 | + // 写进redis |
| 120 | + redisUtils.set(userId, token, RedisConstants.TOKEN_EXPIRE.getTime()); |
| 121 | + // 返回给用户 |
| 122 | + return new ResponseUtil<>().setData(res); |
| 123 | + } else { |
| 124 | + // 如果id等于0,那么说明两种情况:1.账号密码错误,2.账户不存在 |
| 125 | + return new ResponseUtil<>().setErrorMsg("账号密码错误"); |
| 126 | + } |
| 127 | + } |
| 128 | +``` |
| 129 | + |
| 130 | +### AuthFilter |
| 131 | +> sb-gateway->auth->filter->AuthFileter |
| 132 | +
|
| 133 | +#### 忽略列表 |
| 134 | +```java |
| 135 | +// 配置忽略列表 |
| 136 | +String ignoreUrl = jwtProperties.getIgnoreUrl(); // 获取忽略的url |
| 137 | +String[] ignoreUrls = ignoreUrl.split(","); |
| 138 | +for(int i=0;i<ignoreUrls.length;i++){ |
| 139 | + if(request.getServletPath().startsWith(ignoreUrls[i])){ |
| 140 | + chain.doFilter(request, response); |
| 141 | + return;// 不需要认证token |
| 142 | + } |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +#### 认证token |
| 147 | +```java |
| 148 | + // 获取携带的header中的authentication字段 |
| 149 | + final String requestHeader = request.getHeader(jwtProperties.getHeader()); |
| 150 | + String authToken = null; |
| 151 | + if (requestHeader != null && requestHeader.startsWith("Bearer ")) { |
| 152 | + // 解析token |
| 153 | + authToken = requestHeader.substring(7) |
| 154 | + |
| 155 | + // 先获取id |
| 156 | + try { |
| 157 | + // 获取userId,上一块不是针对于userId生成的token嘛? |
| 158 | + String userId = jwtTokenUtil.getUsernameFromToken(authToken); |
| 159 | + // 判断userId是否在redis存在 |
| 160 | + if (!redisUtils.hasKey(userId)) { |
| 161 | + // 如果userId不存在,说明token在Redis过期了,需要重新获取 |
| 162 | + CommonResponse response1 = new CommonResponse(); |
| 163 | + response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); |
| 164 | + response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); |
| 165 | + RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); |
| 166 | + return; |
| 167 | + } else { |
| 168 | + // 如果存在,去取token |
| 169 | + String token = (String) redisUtils.get(userId); |
| 170 | + // 判断redis的token和当前的浏览器的token是否一致 |
| 171 | + if(!token.equals(authToken)) { |
| 172 | + // 如果不相等,说明别人已经登录了,你当前的token无效,需要重新登录,将它替换掉。 |
| 173 | + // 比如,两个手机不能同时登录同一个账户的QQ |
| 174 | + CommonResponse response1 = new CommonResponse(); |
| 175 | + response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); |
| 176 | + response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); |
| 177 | + RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); |
| 178 | + return; |
| 179 | + } |
| 180 | + } |
| 181 | + } catch (Exception e) { |
| 182 | + // 如果发生异常,所携带的token有问题,防止爬虫造token |
| 183 | + CommonResponse response1 = new CommonResponse(); |
| 184 | + response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); |
| 185 | + response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); |
| 186 | + RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); |
| 187 | + return; |
| 188 | + } |
| 189 | + } else { |
| 190 | + //header没有带Bearer字段 |
| 191 | + CommonResponse response1 = new CommonResponse(); |
| 192 | + response1.setCode(SbCode.TOKEN_VALID_FAILED.getCode()); |
| 193 | + response1.setMsg(SbCode.TOKEN_VALID_FAILED.getMessage()); |
| 194 | + RenderUtil.renderJson(response, new ResponseUtil<>().setData(response1)); |
| 195 | + return; |
| 196 | + } |
| 197 | + chain.doFilter(request, response); |
| 198 | + } |
| 199 | +``` |
| 200 | + |
| 201 | +**以上流程图,日后补充** |
0 commit comments