Skip to content

Commit e1499b6

Browse files
committed
提交手撕-项目文档
1 parent cd6c45c commit e1499b6

10 files changed

Lines changed: 1763 additions & 0 deletions
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# 订单自动取消
2+
3+
> 如果系统的并发量不是特别高的,或者日访问量不是特别高的话,可以使用Redis的key过期事件,如果访问量过高,还是采用延迟队列吧。
4+
5+
## Redis的配置
6+
```
7+
#
8+
# notify-keyspace-events Ex
9+
#
10+
# By default all notifications are disabled because most users don't need
11+
# this feature and the feature has some overhead. Note that if you don't
12+
# specify at least one of K or E, no events will be delivered.
13+
notify-keyspace-events "Ex"
14+
```
15+
16+
## RedisListenerConfig配置
17+
```java
18+
@Configuration
19+
public class RedisListenerConfig {
20+
@Bean
21+
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
22+
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
23+
container.setConnectionFactory(connectionFactory);
24+
return container;
25+
}
26+
}
27+
```
28+
29+
## RedisKeyExpirationListener监听事件
30+
31+
```java
32+
@Component
33+
@Slf4j
34+
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
35+
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
36+
super(listenerContainer);
37+
}
38+
39+
@Autowired
40+
private IOrderService orderService;
41+
42+
/**
43+
* 监听key过期事件
44+
* @param message
45+
* @param pattern
46+
*/
47+
@Override
48+
public void onMessage(Message message, byte[] pattern) {
49+
super.onMessage(message, pattern);
50+
String expiredKey = message.toString();
51+
log.info("redis key过期:{}",expiredKey);
52+
String orderCancleKey = RedisConstants.ORDER_CANCLE_EXPIRE.getKey();
53+
if (expiredKey.startsWith(orderCancleKey)) {
54+
// 获取订单id
55+
String[] strings = expiredKey.split(orderCancleKey);
56+
String orderId = strings[1];
57+
log.warn("过期订单ID:" + orderId);
58+
// 得到过期订单。
59+
// 1。 更改订单状态就完事了
60+
OrderRequest request = new OrderRequest();
61+
request.setOrderStatus("2"); // 关闭订单
62+
request.setUuid(Convert.toLong(orderId));
63+
orderService.updateOrderStatus(request);
64+
log.warn("过期订单已处理:" + orderId);
65+
}
66+
}
67+
}
68+
```
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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

Comments
 (0)