分布式系统认证解决方案SpringSecurityOAuth2.0(一)认证授权
分布式系统认证解决方案SpringSecurityOAuth2.0(二)分布式系统认证流程分析与实现
分布式系统认证解决方案SpringSecurityOAuth2.0(三)资源服务器使用Redis令牌、JWT令牌认证及RSA非对称加密算法
分布式系统认证解决方案SpringSecurityOAuth2.0(四)整合网关认证授权
使用JWT令牌格式的话,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务器完成授权,节省开销。
JSON Web Token(JWT)是一个开放的行业标准,定义了一种简洁的、自包含的协议格式,用于在通信双方传递JSON对象,传递的信息经过数字签名可以被验证和信任,JWT可以使用HMAC算法或者使用RSA的公钥私钥对来签名,防止被修改。
一个JWT实际上就是一个字符串,它由三部分组成:
注意:
secret
是保存在服务器端的,secret就是用来进行JWT的签发和验证,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了。
JWT介绍及使用
<!-- spring-security-jwt -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
将认证服务器中的TokenStore
拷到资源服务中:
配置到ResourceServer
中:
注意:token秘钥需要与认证服务器中的一致。
CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) NOT NULL COMMENT '客户端ID',
`resource_ids` varchar(256) DEFAULT NULL COMMENT '资源ID集合,多个资源时用英文逗号分隔',
`client_secret` varchar(256) DEFAULT NULL COMMENT '客户端密匙',
`scope` varchar(256) DEFAULT NULL COMMENT '客户端申请的权限范围',
`authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '客户端支持的grant_type',
`web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '重定向URI',
`authorities` varchar(256) DEFAULT NULL COMMENT '客户端所拥有的SpringSecurity的权限值,多个用英文逗号分隔',
`access_token_validity` int(11) DEFAULT NULL COMMENT '访问令牌有效时间值(单位秒)',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '更新令牌有效时间值(单位秒)',
`additional_information` varchar(4096) DEFAULT NULL COMMENT '预留字段',
`autoapprove` varchar(256) DEFAULT NULL COMMENT '用户是否自动Approval操作',
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='客户端信息';
INSERT INTO `seata`.`oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('c1', 'all', '$2a$10$n/wCt3PAUiFRwWmygwphaODTGf4nZku6UJQh1Lyy8YKmi1cfGtrDW', 'ROLE_ADMIN,ROLE_USER,ROLE_API,ALL', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', NULL, NULL, NULL, NULL, 'false');
INSERT INTO `seata`.`oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('c2', 'all', '$2a$10$4ITes3sAlqV9B2lUB3nJA.9cd16aGC8cyEP9VNpa6pr8Ag9CqzyJy', 'ROLE_ADMIN,ROLE_USER,ROLE_API,ALL', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', NULL, NULL, NULL, NULL, 'false');
CREATE TABLE `oauth_code` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`CODE` varchar(256) DEFAULT NULL COMMENT '授权码(未加密)',
`authentication` blob COMMENT 'AuthorizationRequestHolder.java对象序列化后的二进制数据',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='授权码';
将原来我们保存在内存中的客户端详细信息存储到数据库中:
注意:客户端秘钥是经过我们BCrypt
加密的。
GET请求申请授权码:http://localhost:8081/oauth2/oauth/authorize?client_id=c1&client_secret=order&response_type=code&scope=ALL&redirect_uri=http://www.baidu.com
注意:scope范围要是数据库中存储的。
可以看到授权码存储在数据库中:
使用RSA非对称加密方式生成公钥私钥
创建一个目录,存放jks文件,在该目录下执行:keytool -genkeypair -alias com.lsh.rsa -keyalg RSA -keypass rsapassword -keystore rsa_first.jks -storepass rsapassword
查看证书:keytool -list -keystore rsa_first.jks
输入创建证书时的秘钥库访问密码。
需要将jks文件导出,才能获得我们需要的公钥/私钥
信息。
导出证书是用openssl
工具,openssl是一个加解密工具包,来导出公钥信息:
修改原来的对称加密的accessTokenConverter()
方法:
/**创建jks文件时的别名-alias ims.abc.com*/
public String rsaAlias = "rsafirst";
/**创建jks文件时的访问密码-keypass */
public String rsaPassword = "rsapassword";
/**RSA证书 */
public String certificateFileName = "rsa_first.jks";
/**
* 使用JWT存储令牌 RSA非对称加密
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
//使用JWT存储令牌
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//创建 密钥存储密钥工厂
ClassPathResource classPathResource = new ClassPathResource(certificateFileName);
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,rsaPassword.toCharArray());
//设置密钥对(私钥) 此处传入的是创建jks文件时的别名-alias 和 秘钥库访问密码
jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair(rsaAlias));
//用户身份验证转换器 给Token中加入额外的扩展数据
DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) jwtAccessTokenConverter.getAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
//在资源服务 验证JWT时 使用公钥进行解密 如:Order服务
return jwtAccessTokenConverter;
}
/**
* @author :LiuShihao
* @date :Created in 2021/8/24 9:03 上午
* @desc :用户身份验证转换器,简单理解就是可以给Token中加入额外的扩展数据
*/
@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
LinkedHashMap response = new LinkedHashMap();
String name = authentication.getName();
response.put("user_name", name);
response.put("user_age",18);
//根据自己的情况增加 扩展数据
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
//权限
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
}
修改原来使用对称加密的accessTokenConverter()
方法,使用公钥
解密:
/**存放公钥的文件名 */
public String first_public = "first_public.txt";
public String second_public = "second_public.txt";
/**
* 使用JWT存储令牌 RSA非对称加密
*
* 使用私钥 在认证服务器进行加密
* 使用公钥 在资源服务器解析JWT时解密
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
//使用JWT存储令牌 (普通令牌)
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//公钥(读取public.txt的公钥信息)
Resource resource = new ClassPathResource(publicFileName);
String publicKey = null;
try {
publicKey = inputStream2String(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
//设置验证秘钥(公钥)
jwtAccessTokenConverter.setVerifierKey(publicKey);
return jwtAccessTokenConverter;
}
public String inputStream2String(InputStream is) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(is));
StringBuffer buffer = new StringBuffer();
String line = "";
while ((line = in.readLine()) != null) {
buffer.append(line);
}
return buffer.toString();
}
我们发现令牌的长度变得很长。
调用授权服务器校验令牌:
可以看到我们自定义内容也解析出来了。
使用RSA算法生成了两个jks证书:
在授权服务器我们使用rsa_first.jks
证书进行加密:
在资源服务器使用rsa_second.jks
证书的公钥
进行解密: