⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/doomwatcher/article/details/121743887 「冷环渊」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

鉴权中心服务

认识JWT

json web token 是一个开放的标准 ,它定义了一个种紧凑的,自包含的方式,用于作为json对象在各方之间安全的传输信息

  • 服务器鉴权完成之后 会生成 json 对象 发送给客户端,之后客户端和服务端传输数据都需要带上这个对象,服务器完全通过这个json对象认定客户端身份,为了防止篡改数据,服务端在生成的时候都会加上签名(加密的意思),服务器不保存session数据也就是无状态,更适合实现扩展
  • 那些环境可以考虑使用jwt呢?用户授权 ,信息交换

JWT组成部分

  • Header :头部信息

Header 由两部分组成(Token类型,加密算法的名称),并且使用的是base64的编码

  • Payload:我们想要传递的数据

Payload KV形式的诗数据 ,这里就是我们想要传递的信息(授权的话就是Token信息)

  • Signature :签名

Signature 为了得到签名 首先我们得有编码过的Header 编码过的payload 和一个密钥。签名用的算法就是header中指定的那个,之后就会对他们签名

我们需要一个签名公式

HMACSHA245(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

产生一个签名,返回一个字符串,返回给客户端,之后客户端每次访问都要带上这个字符串,进行鉴权

JWT使用.号来连接 HHH.PPPP.SSSS

授权,鉴权设计

这里我们先不考虑 gateway 网关,后续会搭建,我们的重点放在中间和右边部分

鉴权部分,我们独立实现公共的工具类,为什么?以下三点

  • JWT本质上是通过算法算出的加密字符串,也可以通过算法反向解析出来,他不依赖任何的框架,所以这个功能有可以单独提取出来的前提
  • 我们的电商系统包含多个微服务,很显然我们每个服务都需要鉴权,于是我们把这个方法提取出来,方便复用
  • 高性能鉴权,为什么不在授权中心做鉴权,首先他回头过http请求等一系列操作,我们在本地只用java的话 少去了很多步骤,性能得到倍数的增长

授权编码实现

我们创建新的一个服务来编写我们的鉴权中心

e-commerce-authority-center

导入相关的依赖

<dependencies>
<!-- spring cloud alibaba nacos discovery 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
<!-- Java Persistence API, ORM 规范 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL 驱动, 注意, 这个需要与 MySQL 版本对应 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.hyc.ecommerce</groupId>
<artifactId>e-commerce-mvc-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.5.0.RELEASE</version>
</dependency>
<!-- screw 生成数据库文档 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<dependency>
<groupId>cn.smallbun.screw</groupId>
<artifactId>screw-core</artifactId>
<version>1.0.3</version>
</dependency>
</dependencies>

导入好依赖之后我们 编写对应的配置,如注册到naocs 加入adminserver的监管,配置数据源等 这里我们使用jpa 来做orm

  • 配置编写

server:
port: 7000
servlet:
context-path: /ecommerce-authority-center

spring:
application:
name: e-commerce-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: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
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
open-in-view: false
datasource:
# 数据源
url: jdbc:mysql://127.0.0.1:3306/imooc_e_commerce?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
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: ImoocEcommerceHikariCP
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

配置完成之后,编写主启动类@EnableJpaAuditing因为我们用到 自动加入创建时间和修改时间,所以我们需要打开 jpa的自动审计功能,不然会报错

@EnableJpaAuditing //允许 jpa 的自动审计
@SpringBootApplication
@EnableDiscoveryClient
public class AuthorityApplication {
public static void main(String[] args) {
SpringApplication.run(AuthorityApplication.class, args);

}
}

test包下就测试环境是否正确

/**
* 授权中心测试入口
* 验证授权中心 环境可用性
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class AuthorityCenterApplicationTest {

@Test
public void conetextLoad() {

}
}

环境ok之后

我们去测试 数据库操作是否可用

编写实体类ecommerceUser

/*
* 用户表实体类定义
* */
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "t_ecommerce_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EcommerceUser {
/* 自增组件*/
@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;

/*自动加入更新时间 需要主启动类的注解*/
@CreatedDate
@Column(name = "update_time", nullable = false)
private Date updateTime;
}

有了实体类我们需要有数据操作的实现 于是编写Dao 接口

其实当我们创建接口的时候jpa就已经有了对应的基础增删改查的方法

这里我们实现两个自定义查询方法

/**
* EcommerceUserDao 接口定义
*/
public interface EcommerceUserDao extends JpaRepository<EcommerceUser, Long> {

/*
* 根据用户名查询 EcommerceUser 对象
* 等于 select * form t_ecommerce_user where username=?
* */
EcommerceUser findByUsername(String name);

/*
* 根据用户名查询 EcommerceUser 对象
* 等于 select * form t_ecommerce_user where username=? and password=?
* */
EcommerceUser findByUsernameAndPassword(String name, String password);

}

之后创建 test service

/**
* @author : 冷环渊
* @date : 2021/12/4
* @context: EcommerceUser 相关测试
* @params : null
* @return : * @return : null
*/
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
public class EcommerUserTest {
@Autowired
EcommerceUserDao ecommerceUserDao;

/*测试 新增一个用户数据 */
@Test
public void createUserRecord() {
EcommerceUser ecommerceUser = new EcommerceUser();
//设置要插入的信息
ecommerceUser.setUsername("hyc@qq.com");
ecommerceUser.setPassword(MD5.create().digestHex("123456"));
ecommerceUser.setExtraInfo("{}");
//日志打印返回结果
log.info("server user:[{}]", JSON.toJSON(ecommerceUserDao.save(ecommerceUser)));
}

/*测试 我们编写的自定义方法 查询 刚才创建的新角色*/
@Test
public void SelectUserInfo() {
String username = "hyc@qq.com";
log.info("select userinof:[{}]", JSON.toJSON(ecommerceUserDao.findByUsername(username)));
}
}

测试相关的 方法 新增用户啊 或者是 按条件查询用户 ,测试均通过

生成RSA256的公钥 和 私钥 非对称加密算法

他通过 私钥加密 公钥解密来完成验证,目前很多的鉴权 都是 JWTRSA256的算法来加密鉴权的,如果了解不多,就是用RSA256就可以了

  • 编码

编写生成公钥密钥的测试类,创建 一些我们常用的VO对象 用来储存我们常用的一些变量,比如用户信息,公钥,密钥,一些常用的属性 放进 VO的模型里

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
/**
*
* @author : 冷环渊
* @date : 2021/12/5
* @context: RSA 非对称 加密算法
* @params : null
* @return : * @return : null
*/
public class RSATest {
@Test
public void generateKeyBytes() throws Exception {
/*获取到 RSA算法实例*/
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
/* 这里最小是 2048 低于的话 是会报错的*/
keyPairGenerator.initialize(2048);
/*
* 生成公钥对
* */
KeyPair keyPair = keyPairGenerator.generateKeyPair();
/*获取 公钥和私钥对象*/
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
log.info("private key:[{}]", Base64.encode(privateKey.getEncoded()));
log.info("public key:[{}]", Base64.encode(publicKey.getEncoded()));
}
}

  • 创建VO对象保存 我们常用且不会变化的值和对象

存储私钥 应为是私钥 所以只对鉴权中心 暴露 于是我们在鉴权服务中创建Constant包创建这个AuthotityConstant类保存信息

/**
* @author : 冷环渊
* @date : 2021/12/5
* @context: 鉴权的常量
* @params : null
* @return : * @return : null
*/
public class AuthorCanstant {
/*私钥 只暴露给 鉴权中心 不暴露给任何的其他服务*/
public static final String PRIVATE_KEY = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBA" +
"QCMXrQCudalKHJlH16YHr9mI5/xyYnkp5u2gAbMFf2xAHAyykYmixJP3CqG2a8tUwiJjjTIJXP+79Jzgjgg" +
"VbBaTakrvjeFXz9HNP1D4XD6Li+sRVjnN1iBUwIFRxiFN2EOJflA9bqeQLAge/LgAu06y3jdLLleJF7yDRuMH" +
"YedqPl9AJa5RdJmt0OgCoVOqacB7oGkFCFISm0Cwjfgq06nyiiULGZNVt8uhDxZAE4Pi2lmf3yggXCBH9AtU/2" +
"XdyxU9caQJOAbYGxd/mART/NivBjSqo60wcBnktI+booUbDKRBbWRxvfYqKWEwPOwxlJUB3l3pcLZm866Xl3qtVM" +
"XAgMBAAECggEADCGjLRkik+OK/3JWmo8Nu6YYjKz+XeSecIdgDwNXiZSgHcOdjHc4fe5pPn5RxXkHo9vGdAXIoJ/Z" +
"cGIwt5qwQx2zITSvV7eDoIPT36n8OaMEO79Cj7kYzRR/eDVMyTagDLj7ccHK/yJYFnaf5vxZxFsRdwwGeTxreD" +
"/pwZJLxjRSz1W57v5yUJNPPimNB229EogNYHIhQ8+Z7OGiilbtBIL9r6lqlz2hUAVBzXl4kOXFVI+vEodLuV2" +
"rtQXXrpO1+AgH5lZJ7ahShKbqHt/Q6uJSTKAhbsfv/iadcPjmYp2F7nnYBLf66Jln6AWUwnXrJ7XETOf/+Qcib" +
"q/5m6RjAQKBgQDruxn+kaDr5uYQMVSHog+CBRBJghJ4JklhY7ZDYJ2wN2KNHOd3mW/wUVDihVIyRFniIzsWU" +
"0lnI+4OLqNLAZOBaQB5VrjyH4fxn5b26t0xLO1d5EWcOYI8ZRhwWDWaZipe2dUMeqVVMYFeDdTdNsyGrf8x" +
"L+OVyRDiH4s4pBIs7QKBgQCYcIVFgDbrmwsP7lA9/dU9kClutY3gjEUgB2IJp2Y8S4Xhfi4NC8GqRQoMUyuqg" +
"vPHKEiTCa1EojGHS/+r4JVcSg9Wsv64SpGZ+gANxRhfYFPrbkjU4YOMaZeCGUfKR2QnD20c3I4gdQ9kU5nK52n+Y" +
"JEkAFUejg1Mhb6Fp6HDkwKBgAHYYBa3CxxtnUVpLXE2Woq5AWyh4QUhv5dMkYOrgPB9Ln9OR52PDOpDqK9tP" +
"bx4/n8fqXm+QyfUhyuDP/H5XC86JC/O9vmmN4kzp5ndMsgMwvrmK4lShet1GyDd/+VqgVBmwh0r5JlrHske" +
"sJjesfEn8YRwDIcCoOg0OQHDfwTtAoGAQfE61YvXNihFqsiOkaKCYjVAlxGWpDJJnMdU05REl4ScD6WDy" +
"kTxq/RdmmNIGmS3i8mTS3f+Khh3kG2B1ho6wkePRxP7OEGZpqAM8ef22RtUch2tB9neDBmJXtAMzCYB3xu/O" +
"aL3IHdDB0Va2/krUsz3PDmgmK0ed6HLfwm64l0CgYB+iGkMAQEwqYmcCEXKK825Q9y/u8PE9y8uaMGfsZQzDo6v" +
"V5v+reOhmZRrk5BnX+pgztbE28sS6c2vYR0RYoR90aD2GXungCPXWEMDQudHFxvSsNTCYkDynjTSlnzu9aDcfqw1" +
"UIzHog2zCquSro7tnbOMsvV5UdsLBq+WNQGgAw==";

/*默认的 token 超时时间,一天*/
public static final Integer DEFAULT_EXPIRE_DAY = 1;
}

之后是创建一些公共常用的VO模型 e-commerce-common

  • 保存 公钥到公用包 以后我们的服务 需要做授权都需要使用到

/**
* @author : 冷环渊
* @date : 2021/12/5
* @context: 通用模块的常量定义
* @params : null
* @return : * @return : null
*/
public class CommonCanstant {
/* RSA 公钥*/
public static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjF60ArnWpShyZ" +
"R9emB6/ZiOf8cmJ5KebtoAGzBX9sQBwMspGJosST9wqhtmvLVMIiY40yCVz/u/Sc4I4IFWwWk2pK743hV8/RzT9Q+F" +
"w+i4vrEVY5zdYgVMCBUcYhTdhDiX5QPW6nkCwIHvy4ALtOst43Sy5XiRe8g0bjB2Hnaj5fQCWuUXSZrdDoAqFTqmnA" +
"e6BpBQhSEptAsI34KtOp8oolCxmTVbfLoQ8WQBOD4tpZn98oIFwgR/QLVP9l3csVPXGkCTgG2BsXf5gEU/zYrwY0qqO" +
"tMHAZ5LSPm6KFGwykQW1kcb32KilhMDzsMZSVAd5d6XC2ZvOul5d6rVTFwIDAQAB";

/* JWT 中 存储用户信息到 key*/
public static final String JWT_USER_INFO_KEY = "e-commerce-user";
/*授权中心的 service-id*/
public static final String AUTHORITY_CENTER_SERVICE_ID = "e-commerce-authity-center";
}

  • 用户信息的常用VO对象

JwtToken

/**
* @author : 冷环渊
* @date : 2021/12/5
* @context: 授权中心 鉴权 之后给客户端的token
* @params : null
* @return : * @return : null
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JwtToken {
/* JWT*/
private String token;


}

LoginUserinfo

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserinfo {
/*用户 id*/
private Long id;
/*用户名*/
private String username;
}

UsernameAndPassword

/**
* @author : 冷环渊
* @date : 2021/12/5
* @context:用户名和密码
* @params : null
* @return : * @return : null
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UsernameAndPassword {
/*用户名 */
private String username;
/*密码*/
private String password;
}

  • 授权服务编写

首先创建一个 接口 IJWTService

定义我们需要实现的授权方法

/**
* @author : 冷环渊
* @date : 2021/12/5
* @context: JWT 相关服务接口定义
* @params : null
* @return : * @return : null
*/
public interface IJWTService {

/*
* 生成 token 使用默认的超时时间
* */
String generateToken(String username, String password) throws Exception;

/*
* 生成 JWT Token 可以设置超时时间 单位是天
* */
String generateToken(String username, String password, Integer expireTime) throws Exception;


/*
* 注册用户并且生成 token 返回
* */
String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword) throws Exception;
}

  • 授权方法实现类

这里我们有三个方法实现

  • 默认超时时间的 生成 token
  • 自定义超时时间的设置生成token
  • 注册新用户并且生成的token返回

JWT对象生成细节:

1) 我们需要设置需要传递的对象

2)我们需要设置一个不重复的 id

3)我们需要设置超时时间

4)设置我们的加密签名

5)完成设置返回字符串对象

Jwts.builder()
//这里 claim 其实就是 jwt 的 payload 对象 --> KV
.claim(CommonCanstant.JWT_USER_INFO_KEY, JSON.toJSONString(loginUserinfo))
// jwt id 表示是 jwt的id
.setId(UUID.randomUUID().toString())
//jwt 的过期时间
.setExpiration(expireDate)
// 这里是设置加密的私钥和加密类型
.signWith(getPrivateKey(), SignatureAlgorithm.RS256)
//生成 jwt信息 返回的是一个字符串类型
.compact();
}

  • 完整代码

@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class IJWTServiceIpml implements IJWTService {

@Autowired
private 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, Integer expireTime) throws Exception {
//首先需要验证用户是否通过授权校验,即 输入的用户名和密码能否寻找到匹配数据表的记录
EcommerceUser ecommerceUser = ecommerceUserDao.findByUsernameAndPassword(username, password);
if (ecommerceUser == null) {
log.error("can not find user:[{}],[{}]", username, password);
return null;
}

//Token 中塞入对象, 即 JWT中 储存的对象,后端拿到这些信息 就可以知道那个用户在操作
LoginUserinfo loginUserinfo = new LoginUserinfo(
ecommerceUser.getId(), ecommerceUser.getUsername()
);

if (expireTime <= 0) {
expireTime = AuthorCanstant.DEFAULT_EXPIRE_DAY;
}
//计算超时时间
ZonedDateTime zdt = LocalDate.now().plus(expireTime, ChronoUnit.DAYS)
.atStartOfDay(ZoneId.systemDefault());
Date expireDate = Date.from(zdt.toInstant());

return Jwts.builder()
//这里 claim 其实就是 jwt 的 payload 对象 --> KV
.claim(CommonCanstant.JWT_USER_INFO_KEY, JSON.toJSONString(loginUserinfo))
// jwt id 表示是 jwt的id
.setId(UUID.randomUUID().toString())
//jwt 的过期时间
.setExpiration(expireDate)
// 这里是设置加密的私钥和加密类型
.signWith(getPrivateKey(), SignatureAlgorithm.RS256)
//生成 jwt信息 返回的是一个字符串类型
.compact();
}

@Override
public String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword) throws Exception {
//先去校验 用户名是否存在 如果存在 不能重复注册
EcommerceUser oldUser = ecommerceUserDao.findByUsername(usernameAndPassword.getUsername());
if (null != oldUser) {
log.error("username is registered:[{}]", oldUser.getUsername());
return null;
}
EcommerceUser ecommerceUser = new EcommerceUser();
ecommerceUser.setUsername(usernameAndPassword.getUsername());
ecommerceUser.setPassword(usernameAndPassword.getPassword()); //MD5 编码以后
ecommerceUser.setExtraInfo("{}");

//注册一个新用户 写到一个 记录表中
ecommerceUser = ecommerceUserDao.save(ecommerceUser);

log.info("regiter user success:[{}],[{}]", ecommerceUser.getUsername());

//生成 token 并且返回
return generateToken(ecommerceUser.getUsername(), ecommerceUser.getPassword());
}

/*
* 根据本地储存的私钥获取到 PrivateKey对象
* */
private PrivateKey getPrivateKey() throws Exception {

//使用给定的编码密钥创建一个新的PKCS8EncodedKeySpec。
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(new BASE64Decoder().decodeBuffer(AuthorCanstant.PRIVATE_KEY));
// 设置生成新密钥的工厂加密方式
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
//返回生成好的密钥
return keyFactory.generatePrivate(priPKCS8);
}
}

之后我们的授权都会使用到以上的方法

  • Controller

我们需要给注册用户和生成token 一个程序的入口

就是我们的 AuthorityController,这里可以用到我们之前使用的注解@IgnoreResponseAdvice我们为啥那么不让他封装呢,我们需要验证,单纯的 JwtToken对象就可以了,不需要封装和转化

@Slf4j
@RestController
@RequestMapping("/authority")
public class AuthorityConroller {
private final IJWTService ljwtService;

public AuthorityConroller(IJWTService ljwtService) {
this.ljwtService = ljwtService;
}

/*
* 从授权中心 获取 token (其实就是登陆功能) 且返回信息中没有统一响应的包装
* */
@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(ljwtService.generateToken(
usernameAndPassword.getUsername(),
usernameAndPassword.getPassword()));
}

/*注册用户并且返回注册当前用户的token 就是通过授权中心常见用户*/
@IgnoreResponseAdvice
@PostMapping("/register")
public JwtToken register(@RequestBody UsernameAndPassword usernameAndPassword) throws Exception {
log.info("register user with param:[{}]", JSON.toJSONString(usernameAndPassword));
return new JwtToken(ljwtService.registerUserAndGenerateToken(usernameAndPassword));
}
}

鉴权编码实现

这里我们打鉴权 放到公共模块里 为什么呢,这里我们不止是鉴权中心还有其他的服务也要用到鉴权服务,秉着封装的思想,我们提取公共的方法放到 Common里面

创建JWT Token解析类TokenParseUtil

/**
* @author : 冷环渊
* @date : 2021/12/5
* @context: JWT Token 解析工具类
* @params : null
* @return : * @return : null
*/
public class TokenParseUtil {

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(CommonCanstant.JWT_USER_INFO_KEY).toString(), LoginUserinfo.class
);
}

/*
* 通过公钥去解析 JWT Token
* */
private static Jws<Claims> parseToken(String token, PublicKey publicKey) {
// 用设置签名公钥,解析claims信息 token
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
}

/*
* 根据本地存储的公钥获取到 getPublicKey
* */
public static PublicKey getPublicKey() throws Exception {

//解码器 我们设置解码器 将公钥放进去
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
new BASE64Decoder().decodeBuffer(CommonCanstant.PUBLIC_KEY)
);
//创建 RSA 实例 通过示例生成公钥对象
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
}
}

这里是涉及到一个问题 ,token要是传输的不是jwt token对象,会跑出异常,没有兜底,

其实这里这问题其实也不成立,应为你没有传入token对象,我们这里抛出异常是正确的,也不会影响其他服务,之后搭配sentinel和豪猪哥 可以实现异常重启等等,这里我们就先不编写兜底方法,以解析jwt token为主。

验证鉴权授权

我们写一个 test 类来测试 授权和鉴权拿到对象,是否有效

/**
* @author : 冷环渊
* @date : 2021/12/5
* @context: JWT 相关测试类
* @params : null
* @return : * @return : null
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class JWTServiceTest {

@Autowired
private IJWTService ijwtService;

@Test
public void testGenerateAndParseToken() throws Exception {
String jwtToken = ijwtService.generateToken(
"hyc@qq.com", "e10adc3949ba59abbe56e057f20f883e"
);
log.info("jwt token is:[{}]", jwtToken);
LoginUserinfo userinfo = TokenParseUtil.parseUserInfoFromToken(jwtToken);
log.info("userinfo by jwt prase token :[{}]", JSON.toJSONString(userinfo));
}
}

启动测试查看结果

eyJhbGciOiJSUzI1NiJ9

.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjExLFwidXNlcm5hbWVcIjpcImh5Y0BxcS5jb21cIn0iLCJqdGkiOiIzNDgwNjdjMi00MTBlLTQ3MjItYmM3ZS02NWQyYmNmYTRkN2MiLCJleHAiOjE2Mzg3MjAwMDB9

.ZbFl81MkIipJSULZLf4F2X2Fb0q1TwhHIMT7nyZsZVwUxXyZnK54RlzoGM_b-kMUdKO_Tab-qEeOT6Jn--FiKmbOziWXiBx3a-k5ipthMJx0Fez-X8Acty-Pg7zukNalugiLxGb5ophQoVQWRTDmv2hytGHqiV71HVyErznkJa36QQr6QsjXqlJleo3BBt-6BFzdTFPLUmdTEJ4XsmZBa_acUDGBhY0_tU2gYtKBWhwvMCknuyCcV-_GVI5EvgMIKRpeFSZrWfTsDG2y1MFcyzjKE6jnzek-YwT3XkzQ8eGzUbiOlaU_Zx5OJah-UtrKwqlAw9WbO71pNgEBefdsYw

这是封装好的 JWT Token 这里我们可以看到三个点分别分割 了 header和payload以及签名,和我们之前讲的 结构一模一样,

userinfo by jwt prase token :[{"id":11,"username":"hyc@qq.com"}]

获取到的我们放在 jwt 里面需要传递的对象

验证对外提供的接口是否好用

这里我们编写 http脚本来测试对外题提供的接口是否有用

  • Token 方法

# 获取 Token -- 登录功能实现
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token
Content-Type: application/json

{
"username": "hyc@qq.com",
"password": "e10adc3949ba59abbe56e057f20f883e"
}
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token

HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 05 Dec 2021 15:35:52 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
"token": "eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjExLFwidXNlcm5hbWVcIjpcImh5Y0BxcS5jb21cIn0iLCJqdGkiOiIxNDU1M2FjZi1lZmE5LTQ4OTgtOTliYS1hNzA4NWI4MjU4MzAiLCJleHAiOjE2Mzg3MjAwMDB9.AlOpo6uf97R20ZLojXeun-3MK8DpSYlWxEygvDrtQeWaM9R0iKx-iW1VXnK6WoEntvqPxIrmPA7khjl3dXPa8kQHtdq-LVO7BDuZZDiQyZ64ZS7A9jWZr5JReSWBUSR1YUnsOvBRMkx4JVcAF3_W7nHwd722FFzOZRCr72hLHQIKpsugKtqjMEtaiEW0vcqphCYRJTAO_rQx1Lb1eVVg_Ufur0qSlKkV5dSJ0x3x9mc9UZRckwN0rrP7wQxZcrxJvKTfX7CkRRSO-CxZbG4WLokSaMtaGBMWU-7KGq7HSCZ0yuOgbbLdouHncsp6VD2tNLFdWSdJ_whCIbZxfX8R7w"
}

获取 token 成功

这里他没有被响应包裹,证明我们之前的选择屏蔽注解也生效了,很符合我们的预期

  • 验证如果记录数据表没有是否会返回null

# 获取 Token -- 登录功能实现
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token
Content-Type: application/json
# 随便写的id

{
"username": "hyc1111@qq.com",
"password": "e10adc3949ba59abbe56e057f20f883e"
}

返回结果 也符合我们预期 是 null

POST http://127.0.0.1:7000/ecommerce-authority-center/authority/token

HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 05 Dec 2021 15:40:44 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
"token": null
}

  • register

# 注册用户并返回 Token -- 注册功能实现
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register
Content-Type: application/json

{
"username": "hyc@qq.com",
"password": "e10adc3949ba59abbe56e057f20f883e"
}

这个用户之前是注册过的,我们来看一下是否会返回我们预期的处理

POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register

HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 05 Dec 2021 15:42:00 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
"token": null
}

  • 现在我们去注册一个新的用户

# 注册用户并返回 Token -- 注册功能实现
POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register
Content-Type: application/json

{
"username": "hyc11@qq.com",
"password": "e10adc3949ba59abbe56e057f20f883e"
}

符合预期结果,创建了我们预期的对象,这个时候我们去看一下数据表

POST http://127.0.0.1:7000/ecommerce-authority-center/authority/register

HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 05 Dec 2021 15:42:57 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
"token": "eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEyLFwidXNlcm5hbWVcIjpcImh5YzExQHFxLmNvbVwifSIsImp0aSI6IjMxNDc0NmIwLTMyOGYtNDZkNS05ZTIwLTg3YjI0OWY1ZjZkOCIsImV4cCI6MTYzODcyMDAwMH0.MKxk-Q4BG5kaYFAsLiy13trtk_gDFmCKORpdE4EAwgSVecXFQcYfT1VvqSAKvoQLFsSlQAxOR5elV8CFOoKwAomwqdyyghZp63NKJ2smRbg3Y-4jWBzFVsUgcjOY2fwh7oNTdHEsWmLBYAh5r0hm_MysZsUEsE-cwb3sw8NSMk1OZp0J6tcRras7V1Uw5xXH8OnCoq2cUfdynJMHS29EzJT1TFPb8unVQ_A1RWodsHdK3n1Bl4wFbJjMtnHx7vzOeAUSNJx1XpAGdo0xYHK6HBpS9E1KBS3x1AnYFONM0DKd4-_QxMkBW1kkg2uWrRpf3GYZF20FKxXgmBAPHGZhew"
}

对象生成,功能验证一切正常

鉴权服务中心总结

对比基于Token与基于服务器的身份认证

传统:

  • 最为传统的做法,客户端储存 cookie 一般是 Session id 服务器存储 Session
  • Session 是每次用户认证通过以后 ,服务器需要创建一条记录保存用户信息,通常是在内存中(也可以放在redis中),随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大
  • 不同域名之前切换的时候,请求可能会被禁止,即跨越问题

基于token:

  • JWT与Session的差异相同点是,他们都是存储用户信息。然而Session是在服务器端的,而JWT是在客户端的
  • JWT方式将用户状态分散到了客户端中,可以明显减轻请服务器的内存压力,服务端只需要用算法解析客户端的token就可以得到信息

  • 两者优缺点的对比
  • 解析方法:JWT使用算法直接解析得到用户信息;Session需要额外的数据映射。实现匹配
  • 管理方法:JWT只有过期时间的限制,Session 数据保存在服务器,可控性更强
  • 跨平台:JWT就是一段字符串,可以任意传播,Session跨平台需要有统一的解析平台,较为繁琐
  • 时效性:JWT一旦生成 独立存在,很难做到特殊的控制;Session时效性完全由服务端的逻辑说了算

TIPS :各自都有优缺点,都是登陆和授权的解决方案

文章目录
  1. 1. 鉴权中心服务
    1. 1.1. 认识JWT
    2. 1.2. JWT组成部分
    3. 1.3. 授权,鉴权设计
    4. 1.4. 授权编码实现
    5. 1.5. 生成RSA256的公钥 和 私钥 非对称加密算法
    6. 1.6. 鉴权编码实现
    7. 1.7. 验证鉴权授权
    8. 1.8. 验证对外提供的接口是否好用
    9. 1.9. 鉴权服务中心总结
      1. 1.9.1. 传统:
      2. 1.9.2. 基于token: