简介
参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth 协议:https://tools.ietf.org/html/rfc6749
下边分析一个Oauth2认证的例子,网站使用微信认证的过程:
注意:资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务资源服务器通常 要请求认证服务器来校验令牌的合法性。
引自Oauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749
推荐大神博客:OAuth2.0 说明
https://ruanyifeng.com/
根据我们之前的学习, OAuth是一个开放的授权标准,而Spring Security Oauth2是对OAuth2协议的一种实现框架。下面我们来搭建自己的Spring Security OAuth2的服务框架。
OAuth2的服务提供方包含两个服务,即授权服务(Authorization Server,也叫 做认证服务)和资源服务(Resource Server),使用Spring Security OAuth2的时 候,可以选择在同一个应用中来实现这两个服务,也可以拆分成多个应用来实现同 一组授权服务。
授权服务(Authorization Server)应包含对接入端以及登入用户的合法性进行验 证并颁发token等功能,对令牌的请求断点由Spring MVC控制器进行实现,下面是 配置一个认证服务必须的endpoints:
这一阶段的目的是配置出给客户颁发access_token的服务。这一步主要在授权服务模快中完成
首先,在启动类或者任意一个@Configuration声明的启动类中打开@EnableAuthorizationServer
注释,这个注解是Spring Security打开OAuth认证服务的基础注解。
然后创建配置类继承AuthorizationServerConfigurerAdapter。之前我们配置Spring Security时,利用了WebSecurityConfigurerAdapter注入一个配置对象来完成对基础认证授权功能的配置。在使用OAuth2时,Spring Security也提供了一个类似的适配器来帮助我们完成配置。
package com.tuling.security.distributed.uaa.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
@Configuration
public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
}
AuthorizationServerConfigurerAdapter要求配置以下几个类,这几个类是由Spring创建的独立的配置对象,它们会被Spring传入AuthorizationServerConfigurer中进行配置。
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
}
这三个配置也是整个授权认证服务中最核心的配置。
ClientDetailsServiceConfigurer能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),ClientDetailsService负责查找ClientDetails,一个ClientDetails代表一个需要接入的第三方应用,例如我们上面提到的OAuth流程中的百度。ClientDetails中有几个重要的属性如下:
Client Details客户端详情,能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如访问mysql,就提供了JdbcClientDetailsService)或者通过自己实现ClientRegisterationService接口(同时也可以实现ClientDetailsService接口)来进行定制。
示例中我们暂时使用内存方式存储客户端详情信息,配置如下:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//内存配置的方式配置用户信息
clients.inMemory()//内存方式
.withClient("c1") //client_id
.secret(new BCryptPasswordEncoder().encode("secret"))//客户端秘钥
.resourceIds("order")//客户端拥有的资源列表
.authorizedGrantTypes("authorization_code",
"password", "client_credentials", "implicit", "refresh_token")//该client允许的授权类型
.scopes("all")//允许的授权范围
.autoApprove(false)//跳转到授权页面
.redirectUris("http://www.baidu.com");//回调地址
// .and() //继续注册其他客户端
// .withClient()
// ...
// 加载自定义的客户端管理服务 // clients.withClientDetails(clientDetailsService);
}
管理令牌
AuthorizationServerTokenService接口定义了一些对令牌进行管理的必要操作,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。
实现一个AuthorizationServerTokenServices这个接口,需要继承DefaultTokenServices这个类。该类中包含了一些有用的实现。你可以使用它来修改令牌的格式和令牌的存储。默认情况下,他在创建一个令牌时,是使用随机值来进行填充的。这个类中完成了令牌管理的几乎所有的事情,唯一需要依赖的是spring容器中的一个TokenStore接口实现类来定制令牌持久化。而这个TokenStore,有一个默认的实现,就是ImMemoryTokenStore,这个类会将令牌保存到内存中。除此之外,还有几个默认的TokenStore实现类可以使用。
所以我们下面的步骤首先是要定义一个TokenStore
1、注入TokenConfig
我们先定义一个TokenConfig,往Spring容器中注入一个InMemoryTokenStore,生成一个普通令牌。
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
//使用基于内存的普通令牌
return new InMemoryTokenStore();
}
2、注入AuthorizationServerTokenService
在AuthorizationServer中定义AuthorizationServerTokenServices
@Autowired
private TokenStore tokenStore;
//会通过之前的ClientDetailsServiceConfigurer注入到Spring容器中
@Autowired
private ClientDetailsService clientDetailsService;
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService); //客户端详情服务
service.setSupportRefreshToken(true); //允许令牌自动刷新
service.setTokenStore(tokenStore); //令牌存储策略-内存
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
AuthorizationServerEndpointsConfigurer这个对象的实例可以完成令牌服务以及令牌服务各个endpoint配置。
配置授权类型(Grant Types)
AuthorizationServerEndpointsConfigurer对于不同类型的授权类型,也需要配置不同的属性。
配置授权断点的URL(Endpoint URLS):
AuthorizationServerEndpointsConfifigurer这个配置对象首先可以通过pathMapping()方法来配置断点URL的链接地址。即将oauth默认的连接地址替代成其他的URL链接地址。例如spring security默认的授权同意页面/auth/confirm_access非常简陋,就可以通过passMapping()方法映射成自己定义的授权同意页面。
框架默认的URL链接有如下几个:
/oauth/authorize :授权端点
/auth/token :令牌端点
/oauth/confirm_access :用户确认授权提交的端点
/oauth/error : 授权服务错误信息端点。
/oauth/check_token :用于资源服务访问的令牌进行解析的端点
/oauth/token_key :使用Jwt令牌需要用到的提供公有密钥的端点。
需要注意的是,这几个授权端点应该被Spring Security保护起来只供授权用户访问。
在AuthorizationServer配置令牌访问端点
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// .pathMapping("/oauth/confirm_access","/customer/confirm_access")//定制授权同意页面
.authenticationManager(authenticationManager)//认证管理器
.userDetailsService(userDetailsService)//密码模式的用户信息管理
.authorizationCodeServices(authorizationCodeServices)//授权码服务
.tokenServices(tokenService())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
//设置授权码模式的授权码如何存取,暂时用内存方式。
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
//JdbcAuthorizationCodeServices
}
AuthorizationServerSecurityConfifigurer , 用来配置令牌端点(Token Endpoint)的安全约束,在AuthorizationServer中配置如下:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") // oauth/token_key公开
.checkTokenAccess("permitAll()") // oauth/check_token公开
.allowFormAuthenticationForClients(); // 表单认证,申请令牌
}
OAuth2的授权服务配置是大家使用Spring Security OAuth最头疼的地方。其实具体的配置方式可以不用着重记忆,翻翻API基本能看懂大概。但是这三块核心的配置对象一定要理解记忆。
1、ClientDetailsServiceConfigurer 配置客户端信息。
2、AuthorizationServerEndpointsConfigurer 配置令牌服务。首选需要配置token如何存取,以及客户端支持哪些类型的token。然后不同的令牌服务需要不同的其他服务。authorization_code类型需要配置authorizationCodeServices来管理授权码,password类型需要UserDetailsService来验证用户身份。
3、AuthorizationServerSecurityConfigurer 对相关endpoint定义一些安全约束。
完成上面的OAuth配置后,还要注意添加之前Spring Security相关的安全配置。这也是跟之前的Sprnig Security整合的关键。
package com.tuling.security.distributed.uaa.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* 注入一个自定义的配置
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private TokenStore tokenStore;
@Bean
public PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
//从父类加载认证管理器
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(User.withUsername("admin").password(passwordEncoder().encode("admin")).authorities("mobile","salary").build()
,User.withUsername("manager").password(passwordEncoder().encode("manager")).authorities("salary").build()
,User.withUsername("worker").password(passwordEncoder().encode("worker")).authorities("worker").build());
return userDetailsManager;
}
//配置用户的安全拦截策略
@Override
protected void configure(HttpSecurity http) throws Exception {
//链式配置拦截策略
http.csrf().disable()//关闭csrf跨域检查
.authorizeRequests()
.anyRequest().authenticated() //其他请求需要登录
.and() //并行条件
.formLogin(); //可从默认的login页面登录,并且登录后跳转到main.html
}
}
用postman访问相关接口获取token。
前面完成的授权服务实际上是OAuth协议中最复杂的部分,他规定了三方在互不信任的假设下如何进行担保认证。而到了资源服务这一步,其实就比较简单了。资源服务只要在访问资源之前,进行令牌验证即可。
这个注解是Spring Security打开OAuth资源服务的基础注解,可以在启动类或者任意一个@Configuration声明的启动类中打开这个注释。
然后,与之前的配置方式类似,Spring Security也提供了ResourceServerConfigurerAdapter适配器来协助完成资源服务器的配置。这个适配器提供了多个configure方法,对以下两个核心对象进行配置。
ResourceServerSecurityConfigurer中主要包含:
HttpSecurity,这个配置与Spring Security类似:
@EnableResourceServer注解会自动增加一个类型为OAuth2AuthenticationProcessingFilter的过滤器链。
ResourceServerConfig示例内容如下:
package com.tuling.security.distributed.salary.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
@Configuration
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_SALARY = "salary";
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_SALARY) //资源ID
.tokenServices(tokenServices()) //使用远程服务验证令牌的服务
.stateless(true); //无状态模式
}
//配置安全策略
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //校验请求
.antMatchers("/order/**") // 路径匹配规则。
.access("#oauth2.hasScope('all')") // 需要匹配scope
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
//配置access_token远程验证策略。
public ResourceServerTokenServices tokenServices(){
// DefaultTokenServices services = new DefaultTokenServices();
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
services.setClientId("c1");
services.setClientSecret("secret");
return services;
}
}
这里需要注意的是ResourceServerSecurityConfigurer的tokenServices()方法,设定了一个token的管理服务。其中,如果资源服务和授权服务是在同一个应用程序上,那可以使用DefaultTokenServices,这样的话,就不用考虑关于实现所有必要的接口一致性的问题。而如果资源服务器是分离的,那就必须要保证能够有匹配授权服务提供的ResourceServerTokenServices,他知道如何对令牌进行解码。
令牌解析方法:使用DefaultTokenServices在资源服务器本地配置令牌存储、解码、解析方式。使用RemoteTokenServices资源服务器通过HTTP请求来解码令牌,每次都请求授权服务器端点/oauth/check_token。这时需要授权服务将这个端点暴露出来,以便资源服务进行访问。所以这里要注意下授权服务的下面这个配置:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")// /oauth/token_key 允许访问
.checkTokenAccess("permitAll()") // /oauth/check_token 允许访问
}
而这个/oauth/check_token端点可以获取到access_token对应到的客户信息。
然后我们编写一个简单的薪水查询接口:
package com.tuling.security.distributed.salary.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("salary")
public class SalaryController {
@GetMapping("query")
@PreAuthorize("hasAuthority('salary')")//需要授权客户端拥有order资源才可以访问。
public String query(){
return "salary info";
}
}
以Spring Security的方式添加安全访问控制策略。
package com.tuling.security.distributed.salary.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/salary/**")
// .hasAuthority("salary") //这里采用了注解的方法级权限配置。
.authenticated()
.anyRequest().permitAll();
}
}
这里使用了@EnableGlobalMethodSecurity方法打开了基于注解的方法级别的权限验证。
到这里,我们的资源服务器就算配置完成了。下面我们来访问资源服务器的salary接口进行测试。测试时要注意,在向资源服务器提交access_token时,需要在请求的headers上添加一个Authorization参数来提交令牌,而令牌的内容需要先加上token的类型Bearer,然后空格,再加上access_token。
首先,直接访问资源路径不带任何参数。http://localhost:53021/resource/salary/query 会返回一个错误内容:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
然后,我们随意提交一个错误的访问令牌。这里要注意的是,在向资源服务器提交access_token时,需要在请求的headers上添加一个Authorization参数来提交令牌,而令牌的内容需要先加上token的类型,是Bearer。然后空格,再加上access_token。
然后,我们重新申请一个正确的access_token,重新访问资源
测试到这里要注意的有两点
一是,要总结下在我们示例代码中验证的资源的要素包含了哪些,这些都是OAuth认证流程中需要注意的概念。包括 clientDetails, resourceId,scope,authorities(其实还可以有roles,只是roles是相当于ROLE_{rolename}格式的资源)。
另一点是关于TokenStore对象。到目前为止,我们在资源服务器中并没有配置TokenStore对象,也就是说,资源服务器并不知道access_token有什么意义。他需要使用RemoteTokenServices将令牌拿到授权服务器上去进行验证才会知道access_token代表的客户信息。这一点在请求量加大后,显然会加重系统的网络负担以及运行效率。而这一点,也是后面的JWT令牌需要解决的问题。