parent
676a1e8004
commit
5f96e63086
Binary file not shown.
After Width: | Height: | Size: 279 KiB |
@ -0,0 +1,19 @@
|
||||
package org.example;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* <h1>授权中心启动入口</h1>
|
||||
* */
|
||||
@EnableJpaAuditing // 允许 Jpa 自动审计
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication
|
||||
public class AuthorityCenterApplication {
|
||||
public static void main(String[] args) {
|
||||
|
||||
SpringApplication.run(AuthorityCenterApplication.class, args);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.example.constant;
|
||||
|
||||
/**
|
||||
* <h1>授权需要使用的一些常量信息</h1>
|
||||
* */
|
||||
public final class AuthorityConstant {
|
||||
|
||||
/** RSA 私钥, 除了授权中心以外, 不暴露给任何客户端 */
|
||||
public static final String PRIVATE_KEY = "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAKzKEUVNF+VVgyar2SAcm9xGDCL4yjMLEXXsy0BNM6rZTh2metCIqA1vbKvFvuruTLcTHYMtO0+urup2ScRhiZtkHMI9Vy/MGbiG0o5T2YrDUYZD4jPPRgvZ2L95uI+nFkEm1WZ7nuO37HvIE3+WDVvJnI84omFsLwN3bLCnStw5AgMBAAECgYACCZePWdAVL+HQlProXIJO1XXFwPN2MtCnzB0cIkk5Kc6zjxLIZf0M2dCGrGONG8BJVEj4Zn6BkXlEqTv+LXVCZfOLNJuXTOEdBTWQj1EFk2wuXIqrcZgFqT56ChSscMQTiEe/O7ydyQ2qD/ZbNDOcJMf6nto883ZDLtVtOTdzxQJBANwaMS8O0/X0/gkdzrY3dKjOJFmXOIybdURAR6Mum/PGkX6j9xAUO1clErKTEY1jkuqohLmnXw+pKTQTW/Gt290CQQDI+HFel+S64xZ9SGyISK+gXl1gK1mpMT2YaQjmzwotNljn7U3g0nChbltNANYsRcE5X8/kVoX7AihO+8RZp1oNAkEAgS66SVVZoJVfeHhPN/GKff0nppGz9grUI+/aW/NiQwz7nimcO4q0XWx78eWRuruDokiwRcrvZ2Cwt0jZgRq63QJBAIVli3LbZcK7K1lbclb/0Dulh1tnSutoONdqmLMDqGCcW2UO+guKA6LTqpyxOnhGkNwxgb+xwtr68qCCszFDSR0CQQDC52dm0bei9PCi0pebOhtQVYdPx+zfZE4p+aRCV7pYjm8HgMMJslKX8sgLEOg91gO/925QRb0uN5H5oDjOHFVh";
|
||||
|
||||
/** 默认的 Token 超时时间, 一天 */
|
||||
public static final Integer DEFAULT_EXPIRE_DAY = 1;
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package org.example.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.annotation.IgnoreResponseAdvice;
|
||||
import org.example.service.IJWTService;
|
||||
import org.example.vo.JwtToken;
|
||||
import org.example.vo.UsernameAndPassword;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* <h1>对外暴露的授权服务接口</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/authority")
|
||||
public class AuthorityController {
|
||||
|
||||
private final IJWTService ijwtService;
|
||||
|
||||
public AuthorityController(IJWTService ijwtService) {
|
||||
this.ijwtService = ijwtService;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>从授权中心获取 Token (其实就是登录功能), 且返回信息中没有统一响应的包装</h2>
|
||||
* */
|
||||
@IgnoreResponseAdvice
|
||||
@PostMapping("/token")
|
||||
public JwtToken token(@RequestBody UsernameAndPassword usernameAndPassword)
|
||||
throws Exception {
|
||||
|
||||
log.info("request to get token with param: [{}]",
|
||||
JSON.toJSONString(usernameAndPassword));
|
||||
return new JwtToken(ijwtService.generateToken(
|
||||
usernameAndPassword.getUsername(),
|
||||
usernameAndPassword.getPassword()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>注册用户并返回当前注册用户的 Token, 即通过授权中心创建用户</h2>
|
||||
* */
|
||||
@IgnoreResponseAdvice
|
||||
@PostMapping("/register")
|
||||
public JwtToken register(@RequestBody UsernameAndPassword usernameAndPassword)
|
||||
throws Exception {
|
||||
|
||||
log.info("register user with param: [{}]", JSON.toJSONString(
|
||||
usernameAndPassword
|
||||
));
|
||||
return new JwtToken(ijwtService.registerUserAndGenerateToken(
|
||||
usernameAndPassword
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.example.dao;
|
||||
|
||||
import org.example.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/**
|
||||
* <h1>User Dao 接口定义</h1>
|
||||
* */
|
||||
public interface UserDao extends JpaRepository<User, Long> {
|
||||
|
||||
/**
|
||||
* <h2>根据用户名查询 User 对象</h2>
|
||||
* select * from t_dev_protocol_cloud_user where username = ?
|
||||
* */
|
||||
User findByUsername(String username);
|
||||
|
||||
/**
|
||||
* <h2>根据用户名和密码查询实体对象</h2>
|
||||
* select * from t_dev_protocol_cloud_user where username = ? and password = ?
|
||||
* */
|
||||
User findByUsernameAndPassword(String username, String password);
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package org.example.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EntityListeners;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <h1>用户表实体类定义</h1>
|
||||
* */
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Entity
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
@Table(name = "t_dev_protocol_cloud_user")
|
||||
public class User implements Serializable {
|
||||
|
||||
/** 自增主键 */
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id", nullable = false)
|
||||
private Long id;
|
||||
|
||||
/** 用户名 */
|
||||
@Column(name = "username", nullable = false)
|
||||
private String username;
|
||||
|
||||
/** MD5 密码 */
|
||||
@Column(name = "password", nullable = false)
|
||||
private String password;
|
||||
|
||||
/** 额外的信息, json 字符串存储 */
|
||||
@Column(name = "extra_info", nullable = false)
|
||||
private String extraInfo;
|
||||
|
||||
/** 创建时间 */
|
||||
@CreatedDate
|
||||
@Column(name = "create_time", nullable = false)
|
||||
private Date createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@LastModifiedDate
|
||||
@Column(name = "update_time", nullable = false)
|
||||
private Date updateTime;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package org.example.service;
|
||||
|
||||
|
||||
import org.example.vo.UsernameAndPassword;
|
||||
|
||||
/**
|
||||
* <h1>JWT 相关服务接口定义</h1>
|
||||
* */
|
||||
public interface IJWTService {
|
||||
|
||||
/**
|
||||
* <h2>生成 JWT Token, 使用默认的超时时间</h2>
|
||||
* */
|
||||
String generateToken(String username, String password) throws Exception;
|
||||
|
||||
/**
|
||||
* <h2>生成指定超时时间的 Token, 单位是天</h2>
|
||||
* */
|
||||
String generateToken(String username, String password, int expire) throws Exception;
|
||||
|
||||
/**
|
||||
* <h2>注册用户并生成 Token 返回</h2>
|
||||
* */
|
||||
String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword)
|
||||
throws Exception;
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package org.example.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.constant.AuthorityConstant;
|
||||
import org.example.constant.CommonConstant;
|
||||
import org.example.dao.UserDao;
|
||||
import org.example.entity.User;
|
||||
import org.example.service.IJWTService;
|
||||
import org.example.vo.LoginUserInfo;
|
||||
import org.example.vo.UsernameAndPassword;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import sun.misc.BASE64Decoder;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* <h1>JWT 相关服务接口实现</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public class JWTServiceImpl implements IJWTService {
|
||||
|
||||
private final UserDao ecommerceUserDao;
|
||||
|
||||
public JWTServiceImpl(UserDao ecommerceUserDao) {
|
||||
this.ecommerceUserDao = ecommerceUserDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateToken(String username, String password) throws Exception {
|
||||
|
||||
return generateToken(username, password, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateToken(String username, String password, int expire)
|
||||
throws Exception {
|
||||
|
||||
// 首先需要验证用户是否能够通过授权校验, 即输入的用户名和密码能否匹配数据表记录
|
||||
User ecommerceUser = ecommerceUserDao.findByUsernameAndPassword(
|
||||
username, password
|
||||
);
|
||||
if (null == ecommerceUser) {
|
||||
log.error("can not find user: [{}], [{}]", username, password);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Token 中塞入对象, 即 JWT 中存储的信息, 后端拿到这些信息就可以知道是哪个用户在操作
|
||||
LoginUserInfo loginUserInfo = new LoginUserInfo(
|
||||
ecommerceUser.getId(), ecommerceUser.getUsername()
|
||||
);
|
||||
|
||||
if (expire <= 0) {
|
||||
expire = AuthorityConstant.DEFAULT_EXPIRE_DAY;
|
||||
}
|
||||
|
||||
// 计算超时时间
|
||||
ZonedDateTime zdt = LocalDate.now().plus(expire, ChronoUnit.DAYS)
|
||||
.atStartOfDay(ZoneId.systemDefault());
|
||||
Date expireDate = Date.from(zdt.toInstant());
|
||||
|
||||
return Jwts.builder()
|
||||
// jwt payload --> KV
|
||||
.claim(CommonConstant.JWT_USER_INFO_KEY, JSON.toJSONString(loginUserInfo))
|
||||
// jwt id
|
||||
.setId(UUID.randomUUID().toString())
|
||||
// jwt 过期时间
|
||||
.setExpiration(expireDate)
|
||||
// jwt 签名 --> 加密
|
||||
// .signWith(getPrivateKey(), SignatureAlgorithm.RS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword)
|
||||
throws Exception {
|
||||
|
||||
// 先去校验用户名是否存在, 如果存在, 不能重复注册
|
||||
User oldUser = ecommerceUserDao.findByUsername(
|
||||
usernameAndPassword.getUsername());
|
||||
if (null != oldUser) {
|
||||
log.error("username is registered: [{}]", oldUser.getUsername());
|
||||
return null;
|
||||
}
|
||||
|
||||
User ecommerceUser = new User();
|
||||
ecommerceUser.setUsername(usernameAndPassword.getUsername());
|
||||
ecommerceUser.setPassword(usernameAndPassword.getPassword()); // MD5 编码以后
|
||||
ecommerceUser.setExtraInfo("{}");
|
||||
|
||||
// 注册一个新用户, 写一条记录到数据表中
|
||||
ecommerceUser = ecommerceUserDao.save(ecommerceUser);
|
||||
log.info("register user success: [{}], [{}]", ecommerceUser.getUsername(),
|
||||
ecommerceUser.getId());
|
||||
|
||||
// 生成 token 并返回
|
||||
return generateToken(ecommerceUser.getUsername(), ecommerceUser.getPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>根据本地存储的私钥获取到 PrivateKey 对象</h2>
|
||||
* */
|
||||
private PrivateKey getPrivateKey() throws Exception {
|
||||
|
||||
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(
|
||||
new BASE64Decoder().decodeBuffer(AuthorityConstant.PRIVATE_KEY));
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return keyFactory.generatePrivate(priPKCS8);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
server:
|
||||
port: 7000
|
||||
servlet:
|
||||
context-path: /dev-protocol-springcloud-project-authority-center
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: dev-protocol-springcloud-project-authority-center
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
|
||||
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
|
||||
# server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
|
||||
namespace: 1ccc74ae-9398-4dbe-b9d7-4f9addf9f40c
|
||||
metadata:
|
||||
management:
|
||||
context-path: ${server.servlet.context-path}/actuator
|
||||
jpa:
|
||||
show-sql: true
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
properties:
|
||||
hibernate.show_sql: true
|
||||
hibernate.format_sql: true
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQLDialect
|
||||
open-in-view: false
|
||||
datasource:
|
||||
# 数据源
|
||||
url: jdbc:mysql://127.0.0.1:3306/dev_protocol_springcloud_project?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
password: root
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# 连接池
|
||||
hikari:
|
||||
maximum-pool-size: 8
|
||||
minimum-idle: 4
|
||||
idle-timeout: 30000
|
||||
connection-timeout: 30000
|
||||
max-lifetime: 45000
|
||||
auto-commit: true
|
||||
pool-name: devProtocolSpringcloudHikariCP
|
||||
# kafka:
|
||||
# bootstrap-servers: 127.0.0.1:9092
|
||||
# producer:
|
||||
# retries: 3
|
||||
# consumer:
|
||||
# auto-offset-reset: latest
|
||||
# zipkin:
|
||||
# sender:
|
||||
# type: kafka # 默认是 web
|
||||
# base-url: http://127.0.0.1:9411/
|
||||
|
||||
# 暴露端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
@ -0,0 +1,18 @@
|
||||
### 获取 Token -- 登录功能实现
|
||||
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "Qinyi02@imooc.com",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad"
|
||||
}
|
||||
|
||||
|
||||
### 注册用户并返回 Token -- 注册功能实现
|
||||
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "ImoocQinyiImooc@imooc.com",
|
||||
"password": "25d55ad283aa400af464c76d713c07ad"
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
-- 创建 t_ecommerce_user 数据表
|
||||
CREATE TABLE IF NOT EXISTS `dev_protocol_springcloud_project`.`t_dev_protocol_cloud_user` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
|
||||
`username` varchar(64) NOT NULL DEFAULT '' COMMENT '用户名',
|
||||
`password` varchar(256) NOT NULL DEFAULT '' COMMENT 'MD5 加密之后的密码',
|
||||
`extra_info` varchar(1024) NOT NULL DEFAULT '' COMMENT '额外的信息',
|
||||
`create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
|
@ -0,0 +1,21 @@
|
||||
package org.example;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
/**
|
||||
* <h1>授权中心测试入口</h1>
|
||||
* 验证授权中心环境可用性
|
||||
* */
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class AuthorityCenterApplicationTests {
|
||||
|
||||
@Test
|
||||
public void contextLoad() {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.example.dao;
|
||||
|
||||
import cn.hutool.crypto.digest.MD5;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.entity.User;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
|
||||
/**
|
||||
* <h1>User 相关的测试</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class UserDaoTest {
|
||||
|
||||
@Autowired
|
||||
private UserDao ecommerceUserDao;
|
||||
|
||||
@Test
|
||||
public void createUserRecord() {
|
||||
|
||||
User ecommerceUser = new User();
|
||||
ecommerceUser.setUsername("q@bbbbbbyyyyyy.com");
|
||||
ecommerceUser.setPassword(MD5.create().digestHex("12345678"));
|
||||
ecommerceUser.setExtraInfo("{}");
|
||||
log.info("save user: [{}]",
|
||||
JSON.toJSON(ecommerceUserDao.save(ecommerceUser)));
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
## 授权、鉴权中心微服务功能设计
|
||||
- 什么是 JWT
|
||||
- JSON Web Token (WT)是一个开放标准,它定义了一种紧凑的、自包含的方式,用于作为 JSON对象在各方之间安全地传输信息
|
||||
- 哪些场景下可以考虑使用 JWT?
|
||||
- 用户授权 信息交换
|
||||
---
|
||||
- JWT 的结构(组成部分)
|
||||
- JWT由三个部分组成:Header、Payload、Signature,且用圆点连接 xxxxx.yyyyy.ZZZZZ
|
||||
- Header:由两部分(Token 类型、加密算法名称)组成,并使用 Base64 编码 {'alg':"HS256",'type':"JWT"}
|
||||
- Payload:KV 形式的数据,即你想传递的数据(授权的话就是 Token 信息 )
|
||||
- Signature:为了得到签名部分,你必须有编码过的 Header、编码过的 payload、一个秘钥, 签名算法是 Header 中指定的那个,然对它们签名即可
|
||||
- HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
|
||||
---
|
||||
- 授权、鉴权中心微服务功能逻辑架构
|
||||
- ![授权鉴权中心微服务功能逻辑架构.png](pic/授权鉴权中心微服务功能逻辑架构.png)
|
||||
- 鉴权功能不走HTTP可以更快来进行完成开发, 因为微服务很多模块都要依赖鉴权服务
|
||||
---
|
||||
## 搭建授权、鉴权中心微服务
|
||||
- [FIX] Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set
|
||||
- 加入配置:
|
||||
- spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
|
||||
- [FIX] Caused by: com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time
|
||||
- 在配置文件中 spring.datasource.url 加 serverTimezone=GMT%2B8
|
||||
## 数据表及 ORM 过程
|
||||
-
|
||||
## 生成 RSA256 公钥和私钥对
|
||||
- [RSATest.java]
|
||||
- 一般是放在服务器上, 用的时候拉取下来使用
|
||||
|
||||
## 基于 JWT + RSA256 的授权
|
||||
|
||||
## 验证服务可用性
|
||||
|
||||
## 授权、鉴权中心微服务总结
|
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>dev-protocol</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>dev-protocol-springcloud-project-common</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
<!-- 模块名及描述信息 -->
|
||||
<name>dev-protocol-springcloud-project-common</name>
|
||||
<description>通用模块</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<jjwt.version>0.9.1</jjwt.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.7.22</version>
|
||||
</dependency>
|
||||
<!-- 引入fastjson2-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.18</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,19 @@
|
||||
package org.example.constant;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:18:06
|
||||
* @Description 通用模块常量定义
|
||||
*/
|
||||
public final class CommonConstant {
|
||||
|
||||
/** RSA 公钥 */
|
||||
public static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsyhFFTRflVYMmq9kgHJvcRgwi+MozCxF17MtATTOq2U4dpnrQiKgNb2yrxb7q7ky3Ex2DLTtPrq7qdknEYYmbZBzCPVcvzBm4htKOU9mKw1GGQ+Izz0YL2di/ebiPpxZBJtVme57jt+x7yBN/lg1byZyPOKJhbC8Dd2ywp0rcOQIDAQAB";
|
||||
|
||||
/** JWT 中存储用户信息的 key */
|
||||
public static final String JWT_USER_INFO_KEY = "dev-protocol-user";
|
||||
|
||||
/** 授权中心的 service-id */
|
||||
public static final String AUTHORITY_CENTER_SERVICE_ID = "dev-protocol-springcloud-project-authority-center";
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package org.example.utils;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.example.constant.CommonConstant;
|
||||
import org.example.vo.LoginUserInfo;
|
||||
import sun.misc.BASE64Decoder;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:42
|
||||
* @Description JWT Token 解析工具类
|
||||
*/
|
||||
public class TokenParseUtils {
|
||||
|
||||
/**
|
||||
* <h2>从 JWT Token 中解析 LoginUserInfo 对象</h2>
|
||||
*/
|
||||
public static LoginUserInfo parseUserInfoFromToken(String token) throws Exception {
|
||||
if (null == token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Jws<Claims> claimsJws = parseToken(token, getPublicKey());
|
||||
Claims body = claimsJws.getBody();
|
||||
|
||||
// 如果 Token 已经过期了, 返回 null
|
||||
if (body.getExpiration().before(Calendar.getInstance().getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回 Token 中保存的用户信息
|
||||
return JSON.parseObject(
|
||||
body.get(CommonConstant.JWT_USER_INFO_KEY).toString(),
|
||||
LoginUserInfo.class
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过公钥去解析 JWT Token
|
||||
*/
|
||||
private static Jws<Claims> parseToken(String token, PublicKey publicKey) {
|
||||
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本地存储的公钥获取到 PublicKey 对象
|
||||
*/
|
||||
private static PublicKey getPublicKey() throws Exception {
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
|
||||
new BASE64Decoder().decodeBuffer(CommonConstant.PUBLIC_KEY)
|
||||
);
|
||||
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package org.example.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:02
|
||||
* @Description 通用响应对象定义
|
||||
* {
|
||||
* "code": 0,
|
||||
* "message": "",
|
||||
* "data": {}
|
||||
* }
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CommonResponse<T> implements Serializable {
|
||||
|
||||
/** 错误码 */
|
||||
private Integer code;
|
||||
|
||||
/** 错误消息 */
|
||||
private String message;
|
||||
|
||||
/** 泛型响应数据 */
|
||||
private T Data;
|
||||
|
||||
public CommonResponse(Integer code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.example.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:16
|
||||
* @Description 授权中心鉴权之后给客户端的 Token
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class JwtToken {
|
||||
|
||||
/** JWT */
|
||||
private String token;
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.example.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:24
|
||||
* @Description 登录用户信息
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginUserInfo {
|
||||
|
||||
/** 用户 id */
|
||||
private Long id;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.example.vo;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:17:33
|
||||
* @Description 用户名和密码
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class UsernameAndPassword {
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** 密码 */
|
||||
private String password;
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>dev-protocol</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>dev-protocol-springcloud-project-mvc-config</artifactId>
|
||||
|
||||
<!-- 模块名及描述信息 -->
|
||||
<name>dev-protocol-springcloud-project-mvc-config</name>
|
||||
<description>通用配置模块</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- 引入 Web 功能 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 引入项目通用依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>dev-protocol-springcloud-project-common</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,7 @@
|
||||
package org.example;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello world!");
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package org.example.advice;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.vo.CommonResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:13:14
|
||||
* @Description 全局异常捕获处理
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionAdvice {
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public CommonResponse<String> handlerCommerceException(Exception ex) {
|
||||
CommonResponse<String> response = new CommonResponse<>(-1, "business error");
|
||||
response.setData(ex.getMessage());
|
||||
log.error("commerce service has error: [{}]", ex.getMessage(), ex);
|
||||
return response;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.example.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* @author q
|
||||
* @createTime 2024-08-19 18:21:14
|
||||
* @Description 忽略统一响应注解定义
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD}) // 可以放在class或方法上
|
||||
@Retention(RetentionPolicy.RUNTIME) // 定义该注解保留到运行时
|
||||
public @interface IgnoreResponseAdvice {
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>dev-protocol</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>dev-protocol-springcloud-sleuth-zipkin</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- sleuth -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.cloud</groupId>-->
|
||||
<!-- <artifactId>spring-cloud-starter-sleuth</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-zipkin</artifactId>
|
||||
</dependency>
|
||||
<!-- kafka -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,11 @@
|
||||
package org.example;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SleuthApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SleuthApplication.class, args);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.example.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.example.service.SleuthTraceInfoService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* <h1>打印跟踪信息</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/sleuth")
|
||||
public class SleuthTraceInfoController {
|
||||
|
||||
private final SleuthTraceInfoService traceInfoService;
|
||||
|
||||
public SleuthTraceInfoController(SleuthTraceInfoService traceInfoService) {
|
||||
this.traceInfoService = traceInfoService;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>打印日志跟踪信息</h2>
|
||||
* */
|
||||
@GetMapping("/trace-info")
|
||||
public void logCurrentTraceInfo() {
|
||||
traceInfoService.logCurrentTraceInfo();
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package org.example.sampler;
|
||||
|
||||
import brave.sampler.RateLimitingSampler;
|
||||
import brave.sampler.Sampler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* <h2>使用配置的方式设定抽样率</h2>
|
||||
* */
|
||||
@Configuration
|
||||
public class SamplerConfig {
|
||||
/**
|
||||
* <h2>限速采集</h2>
|
||||
* */
|
||||
@Bean
|
||||
public Sampler sampler() {
|
||||
return RateLimitingSampler.create(100);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * <h2>概率采集, 默认的采样策略, 默认值是 0.1</h2>
|
||||
// * */
|
||||
// @Bean
|
||||
// public Sampler defaultSampler() {
|
||||
// return ProbabilityBasedSampler.create(0.5f);
|
||||
// }
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.example.service;
|
||||
|
||||
import brave.Tracer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* <h1>使用代码更直观的看到 Sleuth 生成的相关跟踪信息</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SleuthTraceInfoService {
|
||||
|
||||
/** brave.Tracer 跟踪对象 */
|
||||
private final Tracer tracer;
|
||||
|
||||
public SleuthTraceInfoService(Tracer tracer) {
|
||||
this.tracer = tracer;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>打印当前的跟踪信息到日志中</h2>
|
||||
* */
|
||||
public void logCurrentTraceInfo() {
|
||||
|
||||
log.info("Sleuth trace id: [{}]", tracer.currentSpan().context().traceId());
|
||||
log.info("Sleuth span id: [{}]", tracer.currentSpan().context().spanId());
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
server:
|
||||
port: 9999
|
||||
servlet:
|
||||
context-path: /dev-protocol-springcloud-sleuth-zipkin
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: dev-protocol-springcloud-sleuth-zipkin # 应用名称也是构成 Nacos 配置管理 dataId 字段的一部分 (当 config.prefix 为空时)
|
||||
cloud:
|
||||
nacos:
|
||||
# 服务注册发现
|
||||
discovery:
|
||||
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
|
||||
server-addr: 127.0.0.1:8848
|
||||
# server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址, 集群模式
|
||||
# todo 根据你自己的业务来进行区分找到
|
||||
namespace: 1ccc74ae-9398-4dbe-b9d7-4f9addf9f40c
|
||||
# group: e-commerce
|
||||
# metadata:
|
||||
# management:
|
||||
# context-path: ${server.servlet.context-path}/actuator
|
||||
# # 配置管理
|
||||
# config:
|
||||
# prefix: imooc-e-commerce
|
||||
# file-extension: yaml # 配置内容的数据格式, 默认为 properties
|
||||
# enabled: true # 如果不想使用 Nacos 进行配置管理, 设置为 false 即可
|
||||
# group: DEFAULT_GROUP # 组, 默认为 DEFAULT_GROUP
|
||||
# namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
|
||||
# server-addr: 127.0.0.1:8848
|
||||
kafka:
|
||||
bootstrap-servers: 127.0.0.1:9092
|
||||
producer:
|
||||
retries: 3
|
||||
consumer:
|
||||
auto-offset-reset: latest
|
||||
sleuth:
|
||||
sampler:
|
||||
# ProbabilityBasedSampler 抽样策略
|
||||
probability: 1.0 # 采样比例, 1.0 表示 100%, 默认是 0.1
|
||||
# RateLimitingSampler 抽样策略, 设置了限速采集, spring.sleuth.sampler.probability 属性值无效
|
||||
rate: 100 # 每秒间隔接受的 trace 量
|
||||
zipkin:
|
||||
sender:
|
||||
type: kafka # 默认是 web
|
||||
base-url: http://localhost:9411/
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
#
|
||||
## Feign 的相关配置
|
||||
#feign:
|
||||
# # feign 开启 gzip 压缩
|
||||
# compression:
|
||||
# request:
|
||||
# enabled: true
|
||||
# mime-types: text/xml,application/xml,application/json
|
||||
# min-request-size: 1024
|
||||
# response:
|
||||
# enabled: true
|
||||
# # 禁用默认的 http, 启用 okhttp
|
||||
# httpclient:
|
||||
# enabled: false
|
||||
# okhttp:
|
||||
# enabled: true
|
||||
# # OpenFeign 集成 Hystrix
|
||||
# hystrix:
|
||||
# enabled: true
|
||||
|
||||
# 暴露端点
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
@ -0,0 +1,5 @@
|
||||
### 查看 Sleuth 跟踪信息
|
||||
GET http://127.0.0.1:9001/imooc/dev-protocol-spring-cloud-nacos/sleuth/trace-info
|
||||
Accept: application/json
|
||||
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjE3LFwidXNlcm5hbWVcIjpcIkltb29jUWlueWlAaW1vb2MuY29tXCJ9IiwianRpIjoiMGIxNzQyYWItMWU3OC00OTZjLWIyNTAtMjNkZGQ1ZGEyZTU1IiwiZXhwIjoxNjI0MjA0ODAwfQ.QKGHzohSHdYDHzUVHpe9gNPUgzfkPwrSbB-WiMWYjLlt2tr9BufzZM8bSt-whb_bd0hKoC6rkYYO0WUVR67uSML-2yaTL1xMIn8GH9Flyig3rpO4vefL3Hp2TXIpwHHa7WlKsLzcUpNk9lxWs2B5k0ICdYCH_jD5Tx6N7CzfSUG9u4fOnVeM9UFE2nX_DURupUh_DKCc2oOoMeyCSR7Ma8-Ab4WQU3r-U0YivR8G1A0kmKOIoTeRhM3LcPuxUPh3rCbrjzMg--fexRGw0O38Qsby6pz-ku2IlTyFXY6_jNOG1BZR34-jBOnaIciP1TExw9bFumeuC2GcowTHJVH1Nw
|
||||
token: imooc
|
Loading…
Reference in New Issue