Spring Cloud Oauth2实现分布式权限认证(JWT版)
环境:
JDK1.8,spring-boot(2.0.3.RELEASE),spring cloud(Finchley.RELEASE)
摘要说明:
上一节中我们oauht2+redis实现了分布式授权服务,但反过来总结下就会发现该模式还是中心化授权;即任何应用服务的接口访问都必须通过token去访问授权服务(oauth2-server)是否满足授权,即所有应用服务都必须配置授权服务获取用户信息接口;
但考虑到微服务往往意味着高并发,高流量;故这种中心化的授权服务会成为瓶颈,所以如何实现去中心化的授权服务就需要依赖本章节的JWT;
步骤:
一、什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
简单点说就是一种固定格式的字符串,通常是加密的;它由三部分组成,头部、载荷与签名,这三个部分都是json格式。
- Header 头部:JSON方式描述JWT基本信息,如类型和签名算法。使用Base64编码为字符串
- Payload 载荷: JSON方式描述JWT信息,除了标准定义的,还可以添加自定义的信息。同样使用Base64编码为字符串。
- iss: 签发者
- sub: 用户
- aud: 接收方
- exp(expires): unix时间戳描述的过期时间
- iat(issued at): unix时间戳描述的签发时间
- Signature 签名: 将前两个字符串用 . 连接后,使用头部定义的加密算法,利用**进行签名,并将签名信息附在最后。
JWT可以使用对称的加***,但更安全的是使用非对称的**;下面就让我们使用jdk自带的keytool生成非对称的公钥、私钥;
生成公钥:
keytool -genkeypair -alias spring-jwt -validity 3650 -keyalg RSA -dname "CN=Victor,OU=Karonda,O=Karonda,L=Shenzhen,S=Guangdong,C=CN" -keypass admin123456 -storepass admin123456 -keystore spring-jwt.jks
这里面主要是-keypass ** -storepass **
生产私钥:
keytool -list -rfc --keystore spring-jwt.jks | openssl x509 -inform pem -pubkey
会提示输入上述私钥的密码:
接着加下面这部分cope出生成public.cert;
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgsMnUy3qsvv9RypsSH/t
4/Kqo6xAW0yegdTBnXxbw/FJiTt9FSEg17yIm7Emg09vUJTKjUMFUMMT9fJ+r29j
LZuG88yTgQXOkqk64tUYh56M8GMKSXKRhyQdgdLQVi3lhaBh977/3aCtDerjzbkk
kFGDm8psDf26sqHj6Derka+M4V+/f4fz7CRu4QSfMGUePbT2V7mI3Y2kS0DHRl0f
K7SzbFkBr+MCBe7fO0wPklhx2W18/V7dYQe2ssd8Et+AzVI+yjbreqM+775b7Imw
bgxv1iJbi5j1JZ3AzsuViZ6OktLyCpRBGNZX7C/8xyNv9m/QjTsHJy/nSrLdbJQ5
2wIDAQAB
-----END PUBLIC KEY-----
将生成的spring-jwt.jks和 public.cert分别放到授权服务(oauth2-server)和应用服务(oauth2-client)的resource下
二、实现JWT版授权服务
在上一节的基础上想实现JWT授权服务其实很简单,只需要修改以下几点
1、公共模块(oauth2-common)
添加JWT转换器配置(JwtConfig)用户应用服务解析前端传入的jwt形式token:
@Configuration
public class JwtConfig {
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert"); // 公钥
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
}
catch (IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
}
2、授权服务(oauth2-server)
修改授权服务配置(AuthorizationServerConfiguration):
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
/**
* 认证管理器
*/
@Autowired
private AuthenticationManager authenticationManager;
/**
*
* @方法名:tokenStore
* @方法描述:自定义储存策略
* @return
* @修改描述:
* @版本:1.0
* @创建人:cc
* @创建时间:2019年11月19日 下午1:56:02
* @修改人:cc
* @修改时间:2019年11月19日 下午1:56:02
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
/**
* 定义令牌端点上的安全性约 束
*
* (non-Javadoc)
*
* @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer)
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients().tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
/**
* 用于定义客户端详细信息服务的配置程序。可以初始化客户端详细信息;
*
* (non-Javadoc)
*
* @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer)
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// clients.withClientDetails(clientDetails());
clients.inMemory().withClient("android").scopes("read").secret(DigestUtil.encrypt("android"))
.authorizedGrantTypes("password", "authorization_code", "refresh_token").and().withClient("webapp")
.scopes("read").authorizedGrantTypes("implicit").and().withClient("browser")
.authorizedGrantTypes("refresh_token", "password").scopes("read");
}
@Bean
public WebResponseExceptionTranslator webResponseExceptionTranslator() {
return new MssWebResponseExceptionTranslator();
}
/**
* 定义授权和令牌端点以及令牌服务
*
* (non-Javadoc)
*
* @see org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter#configure(org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer())
.authenticationManager(authenticationManager).exceptionTranslator(webResponseExceptionTranslator());
}
/**
* <p>
* 注意,自定义TokenServices的时候,需要设置@Primary,否则报错,
* </p>
*
* @return
*/
@Primary
@Bean
public DefaultTokenServices defaultTokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenEnhancer(jwtTokenEnhancer());
// tokenServices.setClientDetailsService(clientDetails());
// token有效期自定义设置,默认12小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24 * 7);
// tokenServices.setAccessTokenValiditySeconds(60 * 60 * 12);
// refresh_token默认30天
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24 * 7);
// tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
/**
* 定义jwt的生成方式
*
* @return JwtAccessTokenConverter
*/
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 非对称加密,但jwt长度过长
KeyPair keyPair = new KeyStoreKeyFactory(new ClassPathResource("spring-jwt.jks"), "admin123456".toCharArray())
.getKeyPair("spring-jwt");
converter.setKeyPair(keyPair);
// 对称加密
// converter.setSigningKey("admin123");
return converter;
}
}
修改资源服务配置(ResourceServerConfig):
@Configuration
@EnableResourceServer
@Order(3)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().exceptionHandling()
.authenticationEntryPoint(
(request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and().requestMatchers().antMatchers("/api/**").and().authorizeRequests().antMatchers("/api/**")
.authenticated().and().httpBasic();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}
}
3、应用服务(oauth2-client)
去除配置里授权服务的链接配置:
修改资源服务配置(ResourceServerConfig),只能有上述公共模块(oauth2-common)生成的tokenStore进行解析:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().exceptionHandling()
.authenticationEntryPoint(
(request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and().requestMatchers().antMatchers("/api/**").and().authorizeRequests().antMatchers("/api/**")
.authenticated().and().httpBasic();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore);
}
}
三、测试
1、先后启动服务:
注册中心(eureka-server),
授权服务(oauth2-server),
网关服务(oauth2-gateway),
应用服务(auth2-client)
2、进行登录,输入授权类型,用户名,密码。并设置client的名称及密码,返回token:
登录成功后我们可以看到token的长度明显比上一章长
这里面要说明的是需要注明授权类型,并输入客户端用户名及密码:
3、先后测试应用服务(oauth2-cliant)的三个接口/api/current、/api/hello、/api/query效果如下,与redis版一致:
四、jwt的优缺点
基于session和基于jwt的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而jwt是保存在客户端的。
jwt的优点:
1. 可扩展性好
应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。
2. 无状态
jwt不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。
jwt的缺点:
1. 安全性
由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。
2. 性能
jwt太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用jwt的http请求比使用session的开销大得多。
3. 一次性
无状态是jwt的特点,但也导致了这个问题,jwt是一次性的。想修改里面的内容,就必须签发一个新的jwt。
(1)无法废弃
通过上面jwt的验证机制可以看出来,一旦签发一个jwt,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个jwt,但是由于旧的jwt还没过期,拿着这个旧的jwt依旧可以登录,那登录后服务端从jwt中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的jwt,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。
(2)续签
如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变jwt的有效时间,就要签发新的jwt。最简单的一种方式是每次请求刷新jwt,即每个http请求都返回一个新的jwt。这个方法不仅暴力不优雅,而且每次请求都要做jwt的加密解密,会带来性能问题。另一种方法是在redis中单独为每个jwt设置过期时间,每次访问时刷新jwt的过期时间。
五、源码地址
上一篇: nacos gateway动态路由
推荐阅读
-
SpringBoot集成Spring security JWT实现接口权限认证
-
jwt,spring security ,feign,zuul,eureka 前后端分离 整合 实现 简单 权限管理系统 与 用户认证的实现
-
Spring Cloud下基于OAUTH2认证授权的实现示例
-
Spring Cloud OAuth2 实现用户认证及单点登录
-
Spring Cloud实战 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT实现微服务统一认证授权
-
Spring Cloud实战 | 最终篇:Spring Cloud Gateway+Spring Security OAuth2集成统一认证授权平台下实现注销使JWT失效方案
-
Spring Cloud Security OAuth2 实现分布式认证授服务测试
-
Spring Cloud Oauth2实现分布式权限认证(redis版)
-
Spring Cloud:认证 授权 OAuth2、JWT
-
Spring Cloud Oauth2实现分布式权限认证(JWT版)