OAuth2介绍
Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以用来做多个微服务的统一认证(验证身份合法性)授权(验证权限)。通过向OAuth2服务(统一认证授权服务)发送某个类型的grant_type进行集中认证和授权,从而获得 access_token(访问令牌),而这个token是受其他微服务信任的。
注意:使用OAuth2解决问题的本质是,引用了一个认证授权层,认证授权层连接了资源的拥有者,在授权层里面,资源的拥有者可以给第三方应用授权去访问我们的某些受保护资源。
构建微服务统一认证服务思路
注意:在我们统一认证的场景中,Resource Server其实就是我们的各种受保护的微服务,微服务中的各种API访问接口就是资源,发起http请求的浏览器就是Client 客户端(对应为第三方应用)
A、搭建认证服务器
认证服务器(Authorization Server),负责颁发token
1、新建项目并引入依赖
项目名称为:lagou-cloud-oauth-server-9999
<!--导入Eureka Client依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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>
<!--引入security对oauth2的支持-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
2、添加配置文件
文件名application.yml, (构建认证服务器,配置文件无特别之处)
server:
port: 9999
Spring:
application:
name: lagou-cloud-oauth-server
eureka:
client:
serviceUrl: # eureka server的路径
defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
instance:
#使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
prefer-ip-address: true
#自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
3、启动类无特殊之处
@SpringBootApplication
@EnableDiscoveryClient
@EntityScan("com.lagou.edu.pojo")
public class OauthServerApplication9999 {
public static void main(String[] args) {
SpringApplication.run(OauthServerApplication9999.class,args);
}
}
4、认证服务器配置类
开发一些与oauth2有关的代码,创建一个OauthServerC onfiger类需要继承特定的父类 AuthorizationServerConfigurerAdapter(认证服务适配器),开启认证服务器功能,认证服务是以接口的方式对外提供服务,那么对外服务的接口也要设置权限(开启权限配置信息),玩的就是token,
1、先要知道客户端对应的client_id对应的用户密码(用于与带过来验证的信息进行对比),
2、token的创建及存储,
3、token验证通过则授权出去。
/**
* 当前类为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的存储方法
// token对象服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
.tokenServices(authorizationServerTokenServices())
// 指定认证管理器,随后注入一个到当前类使用即可
// 先在安全配置类中通过注入bean对象(也就是认证管理器),然后再引入主配置类这里
.authenticationManager(authenticationManager)
// 获取token、或者请求token验证时候所接受的请求方式
.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个小时)
// access_token就是我们请求资源需要携带的令牌
defaultTokenServices.setAccessTokenValiditySeconds(20);
// 设置刷新令牌的有效时间
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
}
关于三个configure方法
configure(ClientDetailsServiceConfigurer clients)
用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息 ,因为客户端通过这里来认证,进行令牌颁发的,所以这里要对客户端进行一些必要信息的分配,如client_id、对应的用户名密码等,在服务端也要进行存储,当访问过来的时候也是要拿着client_id匹配的用户名密码进行校验。表明客户端是谁
configure(AuthorizationServerEndpointsConfigurer endpoints)
用来配置令牌(token)的访问端点和令牌服务(token services)
configure(AuthorizationServerSecurityConfigurer oauthServer)
用来配置令牌端点的安全约束
关于 TokenStore
InMemoryTokenStore
默认采用,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
JdbcTokenStore
这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把”spring-jdbc”这个依赖加入到你的 classpath 当中。
JwtTokenStore
这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重优大势,都是由客户端来存储),缺点就是这个令牌占用的空间会比较大, 如果你加入了比较多用户凭证信息,JwtTokenStore 不会保存任何数据。
5、认证服务器安全配置类
安全配置类,主要处理用户名和密码的校验等事宜,只有用户名和密码校验合法了,才会进行令牌的颁发。
/**
* 该配置类,主要处理用户名和密码的校验等事宜
*/
@Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {
/**
* 注册一个认证管理器对象到容器(然后主配置类才能自动注入进行引用)
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码编码对象(密码不进行加密处理)
* 如果有进行加密的话,这里也要用同样的算法进行加密
* @return 先通过bean注入到容器,然后通过容器方式注入
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
// 通过容器注入bean实例,否则会报错找不到的。
@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);
}
}
创建好安全配置类(进行校验用户合法性,并通过认证管理器进行认证)后,再将认证管理器添加入主配置类中
6、测试
1、endpoint:/oauth/token (获取令牌的通讯端点)
2、获取token携带的参数
client_id | 客户端id |
---|---|
client_secret | 客户单密码 |
grant_type | 指定使用哪种颁发类型 |
password username | 用户名 |
password | 密码 |
把上面的access_token拷贝出来放到下面的token
B、校验token:
后续只要传递token即可
http://localhost:9999/oauth/check_token?token=a9979518-838c-49ff-b14a-ebdb7fde7d08
C、刷新token:
只要刷新令牌不过期就可以用刷新令牌继续续约。可以通过网关操作,网关进行拦截,看有没有效,没有效就用刷新令牌继续刷新。进行续约。
http://localhost:9999/oauth/token?grant_type=refresh_token&c lient_id=client_lagou&client_secret=abcxyz&refresh_token=8b640340-30a3- 4307-93d4-ed60cc54fbc8
B、资源服务器的构建
将业务微服务改为资源微服务
将业务微服务改造成资源微服务,就需要引入aouth2的依赖,然后创建一个Resource Server(资源服务的配置类,这个和认证服务配置类的套路一样的),也就是资源服务器(希望访问被认证的微服务)
目前用lagou-service-autodeliver-8096这个微服务来改造。资源服务器向远程认证服务器发起请求,进行token校验等事宜
1、业务微服务引入aouth2依赖
<!--导入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>
<!--引入security对oauth2的支持-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
2、创建资源服务器配置类
通过RemoteTokenServices对象来帮我们向 远程服务对象发起请求,但是呢先得告诉它必要的验证信息,要向谁(那个端点)发起请求,携带客户端id和客户端安全码(和认证服务器的保持一致)。
@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");
// 别忘了这一步(没有返回值,那么把new的对象放入到形参当中)
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
.and()
.authorizeRequests()
//匹配URL,如果匹配的话都要经过认证
.antMatchers("/autodeliver/**").authenticated() // autodeliver为前缀的请求需要认证
.antMatchers("/demo/**").authenticated() // demo为前缀的请求需要认证
// 其他请求不用认证
.anyRequest().permitAll(); // 其他请求不认证
}
}
如果需要存储的就是有状态的,如果不需要存储,而是存到客户端,客户端过来时候自动带过来的,就是无状态的
到此,认证服务器和资源服务器已经打通了。
思考:当我们第一次登陆之后,认证服务器颁发token并将其存储在认证服务器中,后期我们访问资源服务器时会携带token,资源服务器会请求认证服务器验证token有效性,如果资源服务器有很多,那么认证服务器压力会很大……
另外,资源服务器向认证服务器check_token,获取的也是用户信息 UserInfo,能否把用户信息存储到令牌中,让客户端一直持有这个令牌,令牌的验证也在资源服务器进行,这样避免和认证服务器频繁的交互…… 我们可以考虑使用JWT进行改造,使用JWT机制之后资源服务器不需要访问认证服务器……
JWT
通过 JWT 改造统一认证授权中心的令牌存储机制
JWT令牌介绍
当资源服务和授权服务不在一起时资源服务使用 RemoteTokenServices 远程请求授权服务验证token,如果访问量较大将会影响系统的性能。
解决方案: 令牌采用JWT格式即可解决上边的问题,a、用户认证通过会得到一个 JWT令牌(返回给客户端),JWT令牌中已经包括了用户相关的信息,b、客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。
1、什么是JWT?
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对 来签名,防止被篡改。
2、JWT令牌结构
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
Header
头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或 RSA),例如
Payload
放的就是用户相关数据(用户名、密码、…)
Signature
签名用的,防止令牌被篡改()
是将前两部分进行拼在一起通过秘钥进行加密,用于传过来后进行验证是否一致,不一致就不给过。(这个秘钥认证服务和资源服务都知道的)
认证服务器配置类JWT改造
用于认证的微服务,也就是lagou-cloud-oauth-server-9999这个微服务,将对OauthServerConfiger类进行改造,添加如下内容。(不需要引入其他的依赖坐标了)
1、增加jwt令牌
将生成的令牌存储到jwt当中,(不需要存储这种令牌,因为是无状态,是在客户端中的,)
private String sign_key = "lagou123"; // jwt签名密钥
/*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/** 这个令牌转换器是帮助生成jwt令牌的,但是生成过程中是需要秘钥的,
*在这里,我们可以把签名密钥传递进去给转换器对象
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
// 验证时 使用的密钥,和签名密钥保持一致
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));
return jwtAccessTokenConverter;
}
选用了是对称方式的算法。
2、同时修改 JWT令牌服务方法
在authorizationServerTokenServices方法中新增一行jwt令牌代码
具体如下:
// 针对jwt令牌的添加
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
资源服务器配置类校验JWT令牌
也就是lagou-service-autodeliver-8096这个微服务, 通过添加本地tokenStore ,不需要和远程认证服务器交互,添加有 // jwt令牌都是进行新增的内容。对ResourceServerConfiger类进行操作。
1、生成&使用 jwt令牌
添加秘钥,生成jwt令牌,生成过程中通过jwt转换器,在秘钥的情况下,生成jwt令牌
@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(){
// 使用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;
}
}