微服务认证系列一:SpringCloud OAuth2
SpringCloud OAuth2
微服务架构下统一认证思路
-
基于Session的认证方式
在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要 在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用 服务需要将session信息带过去,否则会重新认证。我们可以使用Session共享、 Session黏贴等方案。
Session方案也有缺点,比如基于cookie,移动端不能有效使用等
-
基于token的认证方式
基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token存在任意地方,并且可以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求 都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。
OAuth2开放授权协议/标准
OAuth2介绍
OAuth(开放授权)是一个开放协议/标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。
OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0
OAuth2协议⻆色和流程
以矿池为例,矿池需要开发使用微信登陆这个功能, 那么首先是需要提前到微信平台进行登记的,然后微信平台会颁发一些参数给矿池,后续上线进行授权登录的时候(刚才打开授 权⻚面)需要携带这些参数
-
client_id :客户端id(微信最终相当于一个认证授权服务器,矿池就相当于一个客户端了,所以会给一个客户端id),相当于账号
-
secret:相当于密码
- 资源所有者(Resource Owner):可以理解为用户自己
- 客户端(Client):我们想登陆的网站或应用,比如矿池
- 认证服务器(Authorization Server):可以理解为微信
- 资源服务器(Resource Server):可以理解为微信
OAuth2的颁发Token授权方式
- 授权码(authorization-code)
- 密码式(password)提供用户名+密码换取token令牌
- 隐藏式(implicit)
- 客户端凭证(client credentials)
授权码模式使用到了回调地址,是最复杂的授权方式,微博、微信、QQ等第三方登录就是这种模式。我们重点讲解接口对接中常使用的password密码模式(提供用户 名+密码换取token)。
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
-
新建项目:zhsl-cloud-oauth-server-9100
-
pom.xml
<dependencyManagement> <dependencies> <!--spring cloud依赖管理,引入了Spring Cloud的版本--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-commons</artifactId> </dependency> <!--SCA --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--导入nacos依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
application.yml配置
server: port: 9100 spring: application: name: zhsl-cloud-oauth-server cloud: nacos: discovery: # 集群中各节点信息都配置在这里(域名-VIP-绑定映射到各个实例的地址信息) server-addr: 127.0.0.1:8848
-
认证服务的配置类
package com.example.zhslcloudoauthserver9100.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; /** * @Description: 当前类为Oauth2 server的配置类(需要继承特定的父类AuthorizationServerConfigurerAdapter) * @Author: wanping * @Date: 6/20/22 **/ @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()// 允许客户端表单认证 .tokenKeyAccess("permitAll()")// 开启端口/oauth/token_key的访问权限(允许) .checkTokenAccess("permitAll()");// 开启端口/oauth/check_token的访问权限(允许) } /** * 客户端详情配置, * 比如client_id,secret * 当前这个服务就如同微信平台,矿池作为客户端需要微信平台进行登录授权认证等,提前需要到微信平台注册,微信平台会给矿池 * 颁发client_id等必要参数,表明客户端是谁 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { super.configure(clients); clients.inMemory() // 客户端信息存储在什么地方,可以在内存中,可以在数据库里 .withClient("client_zhsl") // 添加一个client配置,指定其client_id .secret("abcdef") // 指定客户端的密码/安全码 .resourceIds("autodeliver") // 指定客户端 所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样 // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定 .authorizedGrantTypes("password","refresh_token") .scopes("all");// 客户端的权限范围,此处配置为all全部即可 } /** * 认证服务器是玩转token的,那么这里配置token令牌管理相关(token此时就是一个字符串,当下的token需要在服务器端存储, * 那么存储在哪里呢?都是在这里配置) * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { super.configure(endpoints); endpoints.tokenStore(tokenStore()) // 指定token的存储方法 // token服 务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等 .tokenServices(authorizationServerTokenServices()) // 指定认证管理器,随后注入一个到当前类使用即可 .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; } }
-
TokenStore的实现实现方式
-
InMemoryTokenStore
默认采用,它可以完美的工作在单服务器上(即访问并发量 压力不大 的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以 使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行 管理,因为不会被保存到磁盘中,所以更易于调试。
-
JdbcTokenStore
这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用 这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用 这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath 当中。
-
JwtTokenStore
这个版本的全称是 JSON Web Token(JWT),它可以 把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存 储,这将是一个重大优势),缺点就是这个令牌占用的空间会比较大, 如果你加入了比较多用户凭证信息,JwtTokenStore 不会保存任何数 据。
-
-
-
认证服务器安全配置类
package com.example.zhslcloudoauthserver9100.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.ArrayList; /** * @Description: 该配置类,主要处理用户名和密码的校验等事宜 * @Author: wanping * @Date: 6/20/22 **/ @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://127.0.0.1:9100/oauth/token?client_secret=abcdef&grant_type=password&username=admin&password=123456&client_id=client_zhsl
- endpoint:/oauth/token
- 获取token携带的参数
- client_id:客户端id
- client_secret:客户单密码
- grant_type:指定使用哪种颁发类型,password
- username:用户名
- password:密码
- 获取token:http://127.0.0.1:9100/oauth/token?client_secret=abcdef&grant_type=password&username=admin&password=123456&client_id=client_zhsl
如上图,可得知,认证服务器搭建成功