Spring Cloud OAuth2 + JWT 实现

Spring Cloud OAuth2介绍

Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以⽤来做多个微服务的统⼀认证(验证身份合法性)授权(验证权限)。通过向OAuth2服务(统⼀认证授权服务)发送某个类型的grant_type进⾏集中认证和授权,从⽽获得access_token(访问令牌),⽽这个token是受其他微服务信任的。

注意:使⽤OAuth2解决问题的本质是,引⼊了⼀个认证授权层,认证授权层连接了资源的拥有者,在授权层⾥⾯,资源的拥有者可以给第三⽅应⽤授权去访问我们的某些受保护资源。

Spring Cloud OAuth2构建微服务统一认证服务思路

在这里插入图片描述
注意:在我们统一认证的场景中,Resource Server其实就是我们的各种受保护的微服务,微服务中的各种API访问接口就是资源,发起http请求的浏览器就是Client客户端(对应为第三方应用)

搭建认证服务器(Authorization Server)

认证服务器(Authorization Server),负责颁发token。

pom.xml

<dependencies>
 <!--导⼊Eureka Client依赖-->
 <dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-netflix-eurekaclient</artifactId>
 </dependency>
 <!--导⼊spring cloud oauth2依赖-->
 <dependency>
 
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-oauth2</artifactId>
 <exclusions>
 <exclusion>

<groupId>org.springframework.security.oauth.boot</groupId>
 <artifactId>spring-security-oauth2-autoconfigure</artifactId>
 </exclusion>
 </exclusions>
 </dependency>
 
 <dependency>
 <groupId>org.springframework.security.oauth.boot</groupId>
 <artifactId>spring-security-oauth2-
autoconfigure</artifactId>
 <version>2.1.11.RELEASE</version>
 </dependency>
 
 <dependency>
<groupId>org.springframework.security.oauth</groupId>
 <artifactId>spring-security-oauth2</artifactId>
 <version>2.3.4.RELEASE</version>
 </dependency>
 </dependencies>

认证服务器配置类

/**
* 当前类为Oauth2 server的配置类(需要继承特定的⽗类
AuthorizationServerConfigurerAdapter)
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器功能
public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {

 @Autowired
 private AuthenticationManager authenticationManager;
 
 /**
 * 认证服务器最终是以api接⼝的⽅式对外提供服务(校验合法性并⽣成令牌、校验令牌等)
 * 那么,以api接⼝⽅式对外的话,就涉及到接⼝的访问权限,我们需要在这⾥进⾏必要的配置
 * @param security
 * @throws Exception
 */
 @Override
 public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
	 super.configure(security);
	 // 相当于打开endpoints 访问接⼝的开关,这样的话后期我们能够访问该接⼝ 
	 security
		 // 允许客户端表单认证
		 .allowFormAuthenticationForClients()
		 // 开启端⼝/oauth/token_key的访问权限(允许)
		 .tokenKeyAccess("permitAll()")
		 // 开启端⼝/oauth/check_token的访问权限(允许)
		 .checkTokenAccess("permitAll()");
 }
 
	/**
	 * 客户端详情配置,
	 * ⽐如client_id,secret
	 * 当前这个服务就如同QQ平台,拉勾⽹作为客户端需要qq平台进⾏登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾⽹
	 * 颁发client_id等必要参数,表明客户端是谁
	 * @param clients
	 * @throws Exception
	 */
	 @Override
	 public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
	 super.configure(clients);
	 clients.inMemory()// 客户端信息存储在什么地⽅,可以在内存中,可以在数据库⾥
		 .withClient("client_lagou") // 添加⼀个client配置,指定其client_id
		 .secret("abcxyz") // 指定客户端的密码/安全码
		 .resourceIds("autodeliver") // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置⼀样
		 // 认证类型/令牌颁发模式,可以配置多个在这⾥,但是不⼀定都⽤,具体使⽤哪种⽅式颁发token,需要客户端调⽤的时候传递参数指定
		.authorizedGrantTypes("password","refresh_token")
		 // 客户端的权限范围,此处配置为all全部即可
		 .scopes("all");
	 }

	/**
	 * 认证服务器是玩转token的,那么这⾥配置token令牌管理相关(token此时就是⼀个字符串,当下的token需要在服务器端存储,
	 * 那么存储在哪⾥呢?都是在这⾥配置)
	 * @param endpoints
	 * @throws Exception
	 */
	 @Override
 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
 	super.configure(endpoints);
 	endpoints
		 .tokenStore(tokenStore()) // 指定token的存储⽅法	
		 .tokenServices(authorizationServerTokenServices()) // token服务的⼀个描述,可以认为是token⽣成细节的描述,⽐如有效时间多少等
		 .authenticationManager(authenticationManager) //指定认证管理器,随后注⼊⼀个到当前类使⽤即可
		 .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
 }

/*
 该⽅法⽤于创建tokenStore对象(令牌存储对象)
 token以什么形式存储
 */
 public TokenStore tokenStore(){
 	return new InMemoryTokenStore();
 }

/**
 * 该⽅法⽤户获取⼀个token服务对象(该对象描述了token有效期等信息)
 */
 public AuthorizationServerTokenServices authorizationServerTokenServices() {
	 // 使⽤默认实现
	 DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
	 defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
	 defaultTokenServices.setTokenStore(tokenStore());
	 // 设置令牌有效时间(⼀般设置为2个⼩时)
	 defaultTokenServices.setAccessTokenValiditySeconds(20);
	 // access_token就是我们请求资源需要携带的令牌
	 // 设置刷新令牌的有效时间
	
	defaultTokenServices.setRefreshTokenValiditySeconds(259200); //3天
	 return defaultTokenServices;
 }
}

关于三个configure方法

  • configure(ClientDetailsServiceConfigurer clients)
    ⽤来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这⾥进⾏初始化,你能够把客户端详情信息写死在这⾥或者是通过数据库来存储调取详情信息
  • configure(AuthorizationServerEndpointsConfigurer endpoints)
    ⽤来配置令牌(token)的访问端点和令牌服务(token services)
  • configure(AuthorizationServerSecurityConfigurer oauthServer)
    ⽤来配置令牌端点的安全约束.

关于 TokenStore

  • InMemoryTokenStore
    默认采⽤,它可以完美的⼯作在单服务器上(即访问并发量 压⼒不⼤的情况下,并且它在失败的时候不会进⾏备份),⼤多数的项⽬都可以使⽤这个版本的实现来进⾏ 尝试,你可以在开发的时候使⽤它来进⾏管理,因为不会被保存到磁盘中,所以更易于调试。
  • JdbcTokenStore
    这是⼀个基于JDBC的实现版本,令牌会被保存进关系型数据库。使⽤这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使⽤这个版本的时候请注意把"spring-jdbc"这个依赖加⼊到你的 classpath当中。
  • JwtTokenStore
    这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进⾏编码(因此对于后端服务来说,它不需要进⾏存储,这将是⼀个重⼤优势),缺点就是这个令牌占⽤的空间会⽐较⼤,如果你加⼊了⽐较多⽤户凭证信息,JwtTokenStore 不会保存任何数据。

认证服务器安全配置类

/**
* 该配置类,主要处理⽤户名和密码的校验等事宜
*/
@Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {
 /**
 * 注册⼀个认证管理器对象到容器
 */
 @Bean
 @Override
 public AuthenticationManager authenticationManagerBean() throws Exception {
 	return super.authenticationManagerBean();
 }
 
/**
 * 密码编码对象(密码不进⾏加密处理)
 * @return
 */
 @Bean
 public PasswordEncoder passwordEncoder() {
 	return NoOpPasswordEncoder.getInstance();
 }

@Autowired
 private PasswordEncoder passwordEncoder;
 /**
 * 处理⽤户名和密码验证事宜
 * 1)客户端传递username和password参数到认证服务器
 * 2)⼀般来说,username和password会存储在数据库中的⽤户表中
 * 3)根据⽤户表中数据,验证当前传递过来的⽤户信息的合法性
 */
 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	 // 在这个⽅法中就可以去关联数据库了,当前我们先把⽤户信息配置在内存中
	 // 实例化⼀个⽤户对象(相当于数据表中的⼀条⽤户记录)
	 UserDetails user = new User("admin","123456",new ArrayList<>());
	 auth.inMemoryAuthentication()
	.withUser(user).passwordEncoder(passwordEncoder);
 }
}

测试

  • 获取token:http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_lagou
  • endpoint:/oauth/token
  • 获取token携带的参数
    client_id:客户端id
    client_secret:客户单密码
    grant_type:指定使⽤哪种颁发类型,password
    username:⽤户名
    password:密码

在这里插入图片描述

校验token:http://localhost:9999/oauth/check_token?token=a9979518-838c-49ff-b14a-ebdb7fde7d08
在这里插入图片描述

刷新token:http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lagou&client_secret=abcxyz&refresh_token=8b640340-30a3-4307-93d4-ed60cc54fbc8
在这里插入图片描述

资源服务器(希望访问被认证的微服务)Resource Server配置

资源服务配置类

@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
 
 private String sign_key = "lagou123"; // jwt签名密钥
 /**
 * 该⽅法⽤于定义资源服务器向远程认证服务器发起请求,进⾏token校验等事宜
 * @param resources
 * @throws Exception
 */
 @Override
 public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
	 // 设置当前资源服务的资源id
	 resources.resourceId("autodeliver");
	 // 定义token服务对象(token校验就应该靠token服务对象)
	 RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
	// 校验端点/接⼝设置
	remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
	 // 携带客户端id和客户端安全码
	 remoteTokenServices.setClientId("client_lagou");
	 remoteTokenServices.setClientSecret("abcxyz");
	 // 别忘了这⼀步
	 resources.tokenServices(remoteTokenServices);
 }

/**
 * 场景:⼀个服务中可能有很多资源(API接⼝)
 * 某⼀些API接⼝,需要先认证,才能访问
 * 某⼀些API接⼝,压根就不需要认证,本来就是对外开放的接⼝
 * 我们就需要对不同特点的接⼝区分对待(在当前configure⽅法中完成),设置是否需要经过认证
 *
 * @param http
 * @throws Exception
 */
 @Override
 public void configure(HttpSecurity http) throws Exception {
	 http // 设置session的创建策略(根据需要创建即可)
	
		.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
		 .and()
		 .authorizeRequests()
		.antMatchers("/autodeliver/**").authenticated() //autodeliver为前缀的请求需要认证
		 .antMatchers("/demo/**").authenticated() //demo为前缀的请求需要认证
		 .anyRequest().permitAll(); //其他请求不认证
 	}
}

JWT改造统⼀认证授权中⼼的令牌存储机制

JWT令牌介绍

通过上边的测试我们发现,当资源服务和授权服务不在⼀起时资源服务使⽤RemoteTokenServices 远程请求授权 服务验证token,如果访问量较⼤将会影响系统的性能。

解决上边问题: 令牌采⽤JWT格式即可解决上边的问题,⽤户认证通过会得到⼀个JWT令牌,JWT令牌中已经包括了⽤户相关的信 息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法⾃⾏完成令牌校验,⽆需每次都请求认证 服务完成授权。

1)什么是JWT?
JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519),它定义了⼀种简介的、⾃包含的协议格式,⽤于 在通信双⽅传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使⽤HMAC算法或使⽤RSA的公 钥/私钥对来签名,防⽌被篡改。

2)JWT令牌结构
JWT令牌由三部分组成,每部分中间使⽤点(.)分隔,⽐如:xxxxx.yyyyy.zzzzz

  • Header
    头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA),例如
    在这里插入图片描述
    将上边的内容使⽤Base64Url编码,得到⼀个字符串就是JWT令牌的第⼀部分。
  • Payload
    第⼆部分是负载,内容也是⼀个json对象,它是存放有效信息的地⽅,它可以存放jwt提供的现成字段,⽐ 如:iss(签发者),exp(过期时间戳), sub(⾯向的⽤户)等,也可⾃定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第⼆部分负载使⽤Base64Url编码,得到⼀个字符串就是JWT令牌的第⼆部分。 ⼀个例⼦:
    在这里插入图片描述
  • Signature
    第三部分是签名,此部分⽤于防⽌jwt内容被篡改。 这个部分使⽤base64url将前两部分进⾏编码,编码后使⽤点(.)连接组成字符串,最后使⽤header中声明 签名算法进⾏签名。
    在这里插入图片描述
    base64UrlEncode(header):jwt令牌的第⼀部分。
    base64UrlEncode(payload):jwt令牌的第⼆部分。
    secret:签名所使⽤的密钥。

认证服务器端JWT改造(改造主配置类)

/*
 该⽅法⽤于创建tokenStore对象(令牌存储对象)
 token以什么形式存储
 */
 public TokenStore tokenStore(){
	 //return new InMemoryTokenStore();
	 // 使⽤jwt令牌
	 return new JwtTokenStore(jwtAccessTokenConverter());
 }
 /**
 * 返回jwt令牌转换器(帮助我们⽣成jwt令牌的)
 * 在这⾥,我们可以把签名密钥传递进去给转换器对象
 * @return
 */
 public JwtAccessTokenConverter jwtAccessTokenConverter() {
	 JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
	 jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
	 jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使⽤的密钥,和签名密钥保持⼀致
	 return jwtAccessTokenConverter;
 }

修改 JWT 令牌服务方法

在这里插入图片描述

资源服务器校验JWT令牌

不需要和远程认证服务器交互,添加本地tokenStore

@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
 	private String sign_key = "lagou123"; // jwt签名密钥
 	/**
	 * 该⽅法⽤于定义资源服务器向远程认证服务器发起请求,进⾏token校验等事宜
	 * @param resources
	 * @throws Exception
	 */
	 @Override
	 public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
		 // jwt令牌改造
		resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true);// ⽆状态设置
	 }
 /**
 * 场景:⼀个服务中可能有很多资源(API接⼝)
 * 某⼀些API接⼝,需要先认证,才能访问
 * 某⼀些API接⼝,压根就不需要认证,本来就是对外开放的接⼝
 * 我们就需要对不同特点的接⼝区分对待(在当前configure⽅法中完成),设置是否需要经过认证
 *
 * @param http
 * @throws Exception
 */
 @Override
 public void configure(HttpSecurity http) throws Exception {
	 http // 设置session的创建策略(根据需要创建即可)
		.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
		 .and()
		 .authorizeRequests()
		 .antMatchers("/autodeliver/**").authenticated() //autodeliver为前缀的请求需要认证
		 .antMatchers("/demo/**").authenticated() // demo为前缀的请求需要认证
		 .anyRequest().permitAll(); // 其他请求不认证
 }

/*
 该⽅法⽤于创建tokenStore对象(令牌存储对象)
 token以什么形式存储
 */
 public TokenStore tokenStore(){
	 //return new InMemoryTokenStore();
	 // 使⽤jwt令牌
	 return new JwtTokenStore(jwtAccessTokenConverter());
 }
 /**
 * 返回jwt令牌转换器(帮助我们⽣成jwt令牌的)
 * 在这⾥,我们可以把签名密钥传递进去给转换器对象
 * @return
 */
 public JwtAccessTokenConverter jwtAccessTokenConverter() {
	 JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
	 jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
	 jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使⽤的密钥,和签名密钥保持⼀致
	 return jwtAccessTokenConverter;
 	}
 }

版权声明:本文为jiqiren1994原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。