JWT的基本概念
◆JSON Web Token (JWT)是一个开放标准,它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。
那些场景使用:会话状态保持,用户授权,信息交换。
JWT的结构及其含义
◆JWT由三个部分组成: Header(头部)、Payload(数据JSON)、 Signature(签名) ,且用圆点连接xxxx.yyy.zzzz
◆Header :由两部分( Token类型、加密算法名称)组成,并使用Base64编码,例:
{
'alg': "HS256", //签名的算法
'typ': "JWT" //令牌的类型
}
◆Payload : KV形式的数据,即你想传递的数据(授权的话就是Token信息)
◆Signature :为了得到签名部分,你必须有编码过的Header、 编码过的payload、 一个秘钥,签名算法是Header中指定的那个,然对它们签名即可。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
搭建授权,鉴权微服务:首先是授权方面
同样的步骤,先创建子模块:
pom.xml示例
<?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">
<parent>
<artifactId>e-commerce-springcloud</artifactId>
<groupId>com.taluohui.ecommerce</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>e-commerce-authority-center</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 模块名及描述信息 -->
<name>e-commerce-authority-center</name>
<description>授权中心</description>
<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.taluohui.ecommerce</groupId>
<artifactId>e-commerce-mvc-config</artifactId>
<version>1.0-SNAPSHOT</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>
<!--
SpringBoot的Maven插件, 能够以Maven的方式为应用提供SpringBoot的支持,可以将
SpringBoot应用打包为可执行的jar或war文件, 然后以通常的方式运行SpringBoot应用
-->
<build>
<finalName>${artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
配置文件bootstrap.yml示例
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: 22d40198-8462-499d-a7fe-dbb2da958648
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/ecommerce?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
username: shuai
password: shuai
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
启动项AuthorityCenterApplication
/**
* <h1>授权中心启动入口</h1>
*/
@EnableJpaAuditing //允许Jpa的自动审计
@EnableDiscoveryClient
@SpringBootApplication
public class AuthorityCenterApplication {
public static void main(String[] args) {
SpringApplication.run(AuthorityCenterApplication.class, args);
}
}
实体类entity/EcommerceUser(需要自己创建数据库,和对应的表)
/**
* <h1>用户表实体类定义</h1>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "t_ecommerce_user")
public class EcommerceUser 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;
}
dao/EcommerceUserDao接口类
/**
* <h1>EcommerceUser Dao 接口定义</h1>
*/
public interface EcommerceUserDao extends JpaRepository<EcommerceUser, Long> {
/**
* <h2>根据用户名查询 EcommerceUser 对象</h2>
* select * from t_ecommerce_user where username = ?
*/
EcommerceUser findByUsername(String username);
/**
* <h2>根据用户名和密码查询 EcommerceUser 对象</h2>
* select * from t_ecommerce_user where username = ? and where password = ?
*/
EcommerceUser findByUsernameAndPassword(String username, String password);
}
测试
在test文件下创建包com.taluohui.ecommerce,添加测试类AuthorityCenterApplicationTests
/**
* <h1>授权中心测试入口</h1>
* 验证授权中心环境可用性
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class AuthorityCenterApplicationTests {
@Test
public void contextLoad() {
}
}
创建包service,创建测试类
/**
* <h1>EcommerceUser 相关的测试</h1>
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class EcommerceUserTest {
@Autowired
private EcommerceUserDao ecommerceUserDao;
@Test
public void createUserRecord() {
EcommerceUser ecommerceUser = new EcommerceUser();
ecommerceUser.setUsername("shuai");
ecommerceUser.setPassword(MD5.create().digestHex("12345678"));
ecommerceUser.setExtraInfo("{}");
log.info("save user: [{}]",
JSON.toJSON(ecommerceUserDao.save(ecommerceUser)));
}
}
生成RSA256公钥私钥对
可以通过以下方法生成公钥和私钥,用于之后的加密
/**
* <h1>RSA 非对称加密算法:生成公钥和私钥</h1>
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class RSATest {
@Test
public void generateKeyBytes() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
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()));
}
}
将私钥保存在鉴权模块的常量信息中
在鉴权模块中创建包constant,并创建AuthorityConstant
/**
* <h1>授权需要使用的一些常量信息</h1>
*/
public final class AuthorityConstant {
/** RSA私钥,除了授权中心以外,不暴露给任何的客户端 */
public static final String PRIVATE_KEY = "私钥,特别长";
/** 默认的 Token 超时时间,一天 */
public static final Integer DEFAULT_EXPIRE_DAY= 1;
}
将公钥保存在通用模块的常量信息中
在通用模块中创建包constant,并创建CommonConstant
/**
* <h1>通用模块常量定义</h1>
*/
public final class CommonConstant {
/** RSA公钥 */
public static final String PUBLIC_KEY = "公钥,不是很长";
/** 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-authority-center";
}
定义一些用于鉴权中心的对象
在通用模块vo包下中定义一些对象
/**
* <h1>用户名和密码</h1>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UsernameAndPassword {
/** 用户名 */
private String username;
/** 密码 */
private String password;
}
/**
* <h1>授权中心鉴权之后给客户端的 Token</h1>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JwtToken {
/** JWT */
private String token;
}
/**
* <h1>登录用户信息</h1>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserInfo {
/** 用户 id */
private Long id;
/** 用户名 */
private String username;
}
基于JWT+RSA256的授权
JWT 相关服务接口定义
主要的三个共功能:
- 生成 JWT Token,使用默认的超时时间
- 生成指定超时时间的 Token, 单位是天
- 注册用户并生成 Token 返回
完成service方面的代码
/**
* <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;
}
具体实现impl/JWTServiceImpl
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class JWTServiceImpl implements IJWTService {
private final EcommerceUserDao ecommerceUserDao;
public JWTServiceImpl(EcommerceUserDao 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 {
//首先需要验证用户是否能够通过授权校验,即输入的用户名和密码能否匹配数据表记录
EcommerceUser 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 K:JWT_USER_INFO_KEY V:用户信息
.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 {
//先去校验用户名是否存在,如果存在,不能重复注册
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("register user success: [{}], [{}]", ecommerceUser.getUsername(),
ecommerceUser.getId());
//生成 token 并返回
return generateToken(ecommerceUser.getUsername(), ecommerceUser.getPassword());
}
private PrivateKey getPrivateKey() throws Exception {
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(
new BASE64Decoder().decodeBuffer(AuthorityConstant.PRIVATE_KEY));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(priPKCS8);
}
}
对外暴露的授权服务controller
实际上就是登录和注册的接口
/**
* <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("request user with param: [{}]", JSON.toJSONString(usernameAndPassword));
return new JwtToken(ijwtService.registerUserAndGenerateToken(usernameAndPassword));
}
}
鉴权功能的开发
在通用模块中添加工具包以及鉴权工具类TokenParseUtil
就是将JWT中的用户id和用户名解析出来
对于公钥私钥可以保存在文件服务器中,这样更安全一些
/**
* <h1>JWT Token 解析工具类</h1>
*/
public class TokenParseUtil {
/**
*<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
);
}
/**
*<h2>通过公钥去解析 JWT Token</h2>
*/
public static Jws<Claims> parseToken(String token, PublicKey publicKey) {
return Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);
}
/**
*<h2>根据本地存储的公钥获取到 PublicKey 对象</h2>
*/
private static PublicKey getPublicKey() throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
new BASE64Decoder().decodeBuffer(CommonConstant.PUBLIC_KEY)
);
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
}
}
测试授权和鉴权功能的可用性
/**
* <h1>JWT 相关服务测试类</h1>
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class JWTServiceTest {
@Autowired
private IJWTService ijwtService;
@Test
public void testGenerateAndParseToken() throws Exception {
String jwtToken = ijwtService.generateToken(
"shuai",
"25d55ad283aa400af464c76d713c07ad"
);
log.info("jwt token is: [{}]", jwtToken);
LoginUserInfo userInfo = TokenParseUtil.parseUserInfoFromToken(jwtToken);
log.info("parse token: [{}]", JSON.toJSONString(userInfo));
}
}
###获取 Token ---- 登录功能实现
POST http://localhost:7000/ecommerce-authority-center/authority/token
Content-Type: application/json
{
"username": "shuai",
"password": "25d55ad283aa400af464c76d713c07ad"
}
###注册用户并返回Token ---- 注册功能实现
POST http://localhost:7000/ecommerce-authority-center/authority/register
Content-Type: application/json
{
"username": "shuai1we",
"password": "25d55ad283aa400af464c76d713c07ad"
}
screw开源工具 生成数据库文档
依赖
<!-- 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>
代码
/**
* <h1>数据库表文档生成</h1>
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class DBDocTest {
@Autowired
private ApplicationContext applicationContext;
@Test
public void buildDBDoc() {
DataSource dataSourceMysql = applicationContext.getBean(DataSource.class);
EngineConfig engineConfig = EngineConfig.builder()
//生成文件路径
.fileOutputDir("/Users/duzin/IdeaProjects/e-commerce-springcloud")
//生成之后是否自动打开
.openOutputDir(false)
//文件类型
.fileType(EngineFileType.HTML)
.produceType(EngineTemplateType.freemarker)
.build();
//生成文档配置,包含自定义版本号、描述等等
Configuration config = Configuration.builder()
//版本号,随便给
.version("1.0.0")
//工程描述
.description("e-commerce-springcloud")
//数据源配置
.dataSource(dataSourceMysql)
//引擎的配置
.engineConfig(engineConfig)
//想要生成想要忽略哪些表的配置
.produceConfig(getProduceConfig())
.build();
//执行生成
new DocumentationExecute(config).execute();
}
/**
*<h2>配置想要生成的数据表和想要忽略的数据表</h2>
*/
private ProcessConfig getProduceConfig() {
//想要忽略的数据表
List<String> ignoreTableName = Collections.singletonList("undo_log");
//忽略表前缀, 忽略a、b开头的数据表
List<String> ignorePrefix = Arrays.asList("a", "b");
//忽略表后缀
List<String> ignoreSuffix = Arrays.asList("_test", "_Test");
return ProcessConfig.builder()
//根据名称指定生成
.designatedTableName(Collections.emptyList())
//根据表前缀生成
.designatedTablePrefix(Collections.emptyList())
//根据表后缀生成
.designatedTableSuffix(Collections.emptyList())
//根据名称指定忽略
.ignoreTableName(ignoreTableName)
//根据表前缀忽略
.ignoreTablePrefix(ignorePrefix)
//根据表后缀忽略
.ignoreTableSuffix(ignoreSuffix)
.build();
}
}
基于Token与基于服务器的身份认证对比
基于服务器的身份认证
◆最为传统的做法,客户端存储Cookie (一般是Session id ),服务器存储Session
◆Session 是每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存(redis)中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大
◆在不同域名之前切换时,请求可能会被禁止;即跨域问题
基于Token ( JWT )的身份认证
◆JWT与Session的差异相同点是,它们都是存储用户信息;然而, Session是在服务器端的,而JWT是在客户端的
◆JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力
其实可以将两者进行结合,将JWT保存在redis中。
两者优缺点的对比
◆解析方法: JWT使用算法直接解析得到用户信息; Session需要额外的数据映射,实现匹配
◆管理方法: JWT只有过期时间的限制; Session 数据保存在服务器,可控性更强
◆跨平台: JWT就是一-段字符串,可以任意传播; Session跨平台需要有统- -的解析平台,较为繁琐
◆时效性: JWT -旦生成,独立存在,很难做特殊控制; Session时效性完全由服务端的逻辑说了算