问题描述
黑马商城是一个微服务项目,其中user-service模块包含用户登录,验证密码后生成JWT并返回给客户端。hm-gateway网关模块则用于统一鉴权,需要验证JWT并解析出其中的用户信息。
该项目JWT使用的是rs256签名算法,即非对称加密算法。也就是说只有用户模块需要用私钥去签名生成JWT,项目中也给出了使用keytool生成密钥对文件hmall.jks的命令,如下:
keytool -genkeypair -alias hmall -keyalg RSA -keypass hmall123 -keystore hmall.jks -storepass hmall123
命令中的参数与配置文件中的一一对应,有密钥别名、加密算法、密钥密码、密钥输出路径、密钥库密码,生成的hmall.jks放在resources文件夹下。该文件包含私钥以及证书(证书中包含公钥)
只有在用户登录时,才需要使用RSA私钥进行签名,在网关中只用公钥就可以验证签名和解析JWT,因此将hmall.jks文件的副本放在网关服务中是不安全的,增加了私钥泄露的可能,也违背了非对称加密算法的初衷(个人观点,如有不当请指出)。因此,我们需要将hmall.jks中的证书导出来,交给网关服务。(目前大部分工具都支持直接用证书验证签名)
代码修改流程
1. 首先,从密钥对文件hmall.jks中导出证书(或公钥),使用如下命令(需要注意在hmall.jks所在目录下执行)
keytool -exportcert -alias hmall -keystore hmall.jks -file hmall.cer -storepass hmall123 -rfc
# 导出文件名为hmall.cer,rfc参数指定以PEM格式输出(不加则默认DER格式)
2. 然后,把导出的hmall.cer证书文件放到hm-gateway服务下的resources目录下,并把原来的密钥对文件删除。
3. 修改 application.yaml 配置文件,将密钥对文件位置换为证书文件位置。
server:
port: 8080
hm:
jwt:
location: classpath:hmall.cer
auth:
excludePaths:
- /search/**
- /users/login
- /items/**
- /hi
# keytool -genkeypair -alias hmall -keyalg RSA -keypass hmall123 -keystore hmall.jks -storepass hmall123
4. 修改 SecurityConfig.java 配置文件,将获取密钥对的代码删除,换成获取公钥的代码,如下:
@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 获取公钥
* @param jwtProperties
* @return
* @throws CertificateException
* @throws IOException
*/
@Bean
public PublicKey publicKey(JwtProperties jwtProperties) throws CertificateException, IOException {
Certificate certificate = CertificateFactory
.getInstance("X.509")
.generateCertificate(jwtProperties.getLocation().getInputStream());
return certificate.getPublicKey();
}
}
5. 修改 JwtTool.java 文件,创建签名器的时候使用公钥而不是密钥对。
@Component
public class JwtTool {
private final JWTSigner jwtSigner;
public JwtTool(PublicKey publicKey) {
this.jwtSigner = JWTSignerUtil.createSigner("rs256", publicKey);
}
/**
* 解析token
*
* @param token token
* @return 解析刷新token得到的用户信息
*/
public Long parseToken(String token) {
// 1.校验token是否为空
if (token == null) {
throw new UnauthorizedException("未登录");
}
// 2.校验并解析jwt
JWT jwt;
try {
jwt = JWT.of(token).setSigner(jwtSigner);
} catch (Exception e) {
throw new UnauthorizedException("无效的token", e);
}
// 2.校验jwt是否有效
if (!jwt.verify()) {
// 验证失败
throw new UnauthorizedException("无效的token");
}
// 3.校验是否过期
try {
JWTValidator.of(jwt).validateDate();
} catch (ValidateException e) {
throw new UnauthorizedException("token已经过期");
}
// 4.数据格式校验
Object userPayload = jwt.getPayload("user");
if (userPayload == null) {
// 数据为空
throw new UnauthorizedException("无效的token");
}
// 5.数据解析
try {
return Long.valueOf(userPayload.toString());
} catch (RuntimeException e) {
// 数据格式有误
throw new UnauthorizedException("无效的token");
}
}
}
小结
这样进一步加强了私钥的安全,即使公钥泄露,只是可以验证签名和解析JWT,但如果没有私钥,是无法篡改JWT的。