OAuth2.0(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth2.0 内置接口,使用时直接调用接口即可。
简而言之:用户告诉系统同意第三方应用可以使用token代替用户名密码来访问用户存在系统中的信息资源。
借鉴一个例子 外卖员要送外卖给我,而我们小区有门禁,需要认证,才能进入。我又不能将我的密码告诉外卖员,此时我可以告诉系统我授权外卖员进入小区给我送外卖,系统会给我一个token,我把toekn给外卖员,外卖员就可以使用token认证进入小区。
OAuth2.0有四种认证模式:
1、客户端凭证(client_credentials)
主要针对第三方应用,应用与应用对接认证。
client_credentials 代表使用客户端认证模式
认证主要参数:
client_id :客户端id
client_secret:客户端凭证
根据上述两个参数确认客户端的身份。
认证接口:
http://oauthservice.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
2、密码式
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
第一步,A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。
https://oauth.b.com/token? grant_type=password& username=USERNAME& password=PASSWORD& client_id=CLIENT_ID
上面 URL 中,grant_type
参数是授权方式,这里的password
表示"密码式",username
和password
是 B 的用户名和密码。
第二步,B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。
这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。
3、隐藏式
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。
第一步,A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。
https://b.com/oauth/authorize? response_type=token& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
上面 URL 中,response_type
参数为token
,表示要求直接返回令牌。
第二步,用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri
参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。
https://a.com/callback#token=ACCESS_TOKEN
上面 URL 中,token
参数就是令牌,A 网站因此直接在前端拿到令牌。
注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
4、授权码
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。
https://b.com/oauth/authorize? response_type=code& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
上面 URL 中,response_type
参数表示要求返回授权码(code
),client_id
参数让 B 知道是谁在请求,redirect_uri
参数是 B 接受或拒绝请求后的跳转网址,scope
参数表示要求的授权范围(这里是只读)。
第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri
参数指定的网址。跳转时,会传回一个授权码。
第三步,A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。
https://b.com/oauth/token? client_id=CLIENT_ID& client_secret=CLIENT_SECRET& grant_type=authorization_code& code=AUTHORIZATION_CODE& redirect_uri=CALLBACK_URL
上面 URL 中,client_id
参数和client_secret
参数用来让 B 确认 A 的身份(client_secret
参数是保密的,因此只能在后端发请求),grant_type
参数的值是AUTHORIZATION_CODE
,表示采用的授权方式是授权码,code
参数是上一步拿到的授权码,redirect_uri
参数是令牌颁发后的回调网址。
第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri
指定的网址,发送一段 JSON 数据。
令牌的使用
A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。
此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization
字段,令牌就放在这个字段里面。
更新令牌
令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。
具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
https://b.com/oauth/token? grant_type=refresh_token& client_id=CLIENT_ID& client_secret=CLIENT_SECRET& refresh_token=REFRESH_TOKEN
上面 URL 中,grant_type
参数为refresh_token
表示要求更新令牌,client_id
参数和client_secret
参数用于确认身份,refresh_token
参数就是用于更新令牌的令牌。
B 网站验证通过以后,就会颁发新的令牌。
写到这里,颁发令牌的四种方式就介绍完了。
oauth提供的接口信息:
1、身份认证接口
GET /authorize`
资源所有者要为第三方授权,首先要认证自己的身份。
该接口的请求参数以 application/x-www-form-urlencoded 格式提供
注意点:
- 身份认证接口只有授权码授权流程和包含式授权流程涉及,其他两种流程不涉及。
- 通过
response_type
来说明要获取授权码(授权码授权),还是直接获取访问令牌(包含式授权) client_id
用于认证服务器识别当前是哪个客户端在申请访问令牌。客户端的身份也要展示给资源所有者,让后者了解自己当前在给谁授权。认证服务器需要提供一个客户端备案功能,为客户端做备案,并为其分配 client_id。redirect_uri
是客户端用来接收授权码或访问令牌的地址。选填是因为客户端在认证服务器备案的时候,可能已经录入了重定向地址,那么此时就可以根据 client_id 从认证服务器查询到对应的重定向地址了scope
确定了客户端要申请哪些权限。不同的权限名用空格隔开。
权限的枚举值本身是由认证服务器定义的,因而认证服务器要在文档中详细说明其值的含义,以供客户端使用。
认证服务器有一个默认的作用域列表。当客户端没有明确申请的作用域时,则使用默认值。
客户端申请的作用域列表要展示给资源所有者确认,资源所有者可以根据情况取消或者授予更多权限给客户端。最终以资源所有者确认的权限列表为准。state
这个字段一般是客户端生成的随机数,用于防止 CSRF(跨站请求伪造)。- 对于授权码流程,这些参数值要缓存起来,以供下面获取访问令牌的流程使用。
请求示例:
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1Host: server.example.com
如果资源所有者确认授权,则重定向到客户端提供的重定向地址,并将相应的参数以 application/x-www-form-urlencoded 的格式传递给客户端
2|获取访问令牌接口
POST /token
客户端通过该接口请求访问令牌。由于访问令牌需要严格保密,这个接口通常是通过后台加密访问的(TSL)。
请求参数
注意点:
- 由于包含式授权流程在上一步的身份认证环节已经取得了访问令牌,因而不涉及该接口
- 对于授权码授权流程,要核对请求中的客户端ID、授权码、重定向地址等字段是否与上一步身份认证接口传递的值相匹配。只有核对通过才下发访问令牌。
- 对于授权码授权流程,
scope
的值在上一步的身份认证环节已由资源所有者确认,故此时不传值,取之前缓存的值。 - 对于客户端凭证授权流程,需要在请求中携带客户端凭证,以供认证服务器确认身份;对于授权码授权流程,若在之前的环节也对客户端做了认证,则 client_id 可以不填,请求中带上客户端凭证即可。
- 在访问令牌过期后,通过刷新令牌获取新的访问令牌的场景下,作用域不能超出最初资源所有者所授予的范围。若未填,则表示与最初确认的作用域相同。
请求示例:
POST /token HTTP/1.1Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
响应:
若该接口认证成功,则返回一个包含访问令牌等信息的 JSON。
此外,响应头必须包含“Cache-Control”字段,且其值为“no-store”;以及“"Pragma”字段,其值为“no-cache”。
响应字段有:
注意点:
- 令牌类型为 bearer、mac 等。客户端需要了解这些令牌类型的用法,并在之后的资源访问请求中正确地携带访问令牌。
- 过期时限表明当前令牌在多久后过期,单位为(秒)。若不返回,认证服务器应在文档中向客户端说明默认的过期时限。
- 刷新令牌给了客户端一种当访问令牌已过期,而客户端认证任有效时,获取新的访问令牌的途径。它的有效时间要比访问令牌长。
- 当客户端请求访问令牌时指明了作用域,而资源所有者没有对其做变更时,作用域可以不返回——即当前访问令牌的作用域同客户端请求的一致;若资源所有者对作用域做了变更,则返回其最终确认的作用域;若客户端没有指明作用域,则返回认证服务器设置的默认作用域。
响应示例:
HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
客户端接口
重定向接口
GET /example
在授权码授权流程和包含式授权流程中,当资源所有者确认授权后,认证服务器会通过用户代理将资源所有者重定向到客户端的重定向接口地址,并将授权码或访问令牌以 “application/x-www-form-urlencoded” 的格式附在请求中。
请求参数:
建议
注意点:
- 处于安全性考虑,建议授权码的有效期控制在 10 分钟以内。
- 由于包含式授权流程是在这一步直接返回访问令牌信息的,其 access_token、token_type、expires_in 字段的含义都可以参考认证服务器的获取访问令牌接口的响应。
- 由于包含式授权流程存在安全隐患,这里不能返回刷新令牌。
- 认证服务器应该在文档中向客户端说明响应字段的大小。客户端根据文档处理,而不要自行推测字段大小。
- 授权码授权流程中,参数通过请求参数传递;包含式授权流程中,参数通过 URI 片段(fragment)传递。网上有说法是由于浏览器跳转时,URI 片段不会发到服务器,从而可以减少令牌泄露的风险(预防中间人攻击)。
请求示例:
授权码授权
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
包含式授权
HTTP/1.1 302 Found
集成Springboot代码示例
主要配置包含三个文件
认证服务的配置
类集成extends AuthorizationServerConfigurerAdapter
package com.oauth2.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static final String RESOURCE_IDS = "order";
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients(); //允许表单登录访问
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
String finalSecret = new BCryptPasswordEncoder().encode("123456");
//配置两个客户端,一个用于password认证一个用于client认证
clients.inMemory()
//client模式
.withClient("client_1")
.resourceIds(RESOURCE_IDS)
.authorizedGrantTypes("client_credentials", "refresh_token")
.scopes("select")
.authorities("oauth2")
.secret(finalSecret)
.and()
//密码模式
.withClient("client_2")
.resourceIds(RESOURCE_IDS)
.authorizedGrantTypes("authorization_code", "refresh_token",
"password")
.scopes("select")
.authorities("oauth2")
.secret(finalSecret)
.and()
//授权码码模式
.withClient("client_3")
.resourceIds(RESOURCE_IDS)
.authorizedGrantTypes("authorization_code", "refresh_token",
"implicit")
.scopes("select")
.secret(finalSecret)
.authorities("oauth2")
.redirectUris("http://www.baidu.com")
.secret(finalSecret);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//设置token的格式,过期时间等信息
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setAccessTokenValiditySeconds(100);
defaultTokenServices.setRefreshTokenValiditySeconds(200);
defaultTokenServices.setTokenStore(endpoints.getTokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setClientDetailsService(endpoints.getClientDetailsService());
defaultTokenServices.setTokenStore(new RedisTokenStore(redisConnectionFactory));
defaultTokenServices.setAuthenticationManager(authenticationManager);
defaultTokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
//配置token 信息 启用oauth管理 允许的请求方式
endpoints.tokenServices(defaultTokenServices)
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET);
}
}
安全配置设置哪些接口以什么样的方式可以访问
package com.oauth2.config;
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.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.provisioning.InMemoryUserDetailsManager;
/**
* 安全配置
*/
@Configuration
@EnableWebSecurity //开启接口认证模式
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 创建两个内存用户
String finalSecret = new BCryptPasswordEncoder().encode("123456");
manager.createUser(User.withUsername("admin").password(finalSecret).authorities("USER").build());
manager.createUser(User.withUsername("lin").password(finalSecret).authorities("USER").build());
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**") //使HttpSecurity接收以"/login/",
// "/oauth/" "/logout/**"开头请求。
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.authorizeRequests().anyRequest().authenticated().and()
.formLogin().loginPage("/login").permitAll();
}
/**
* 配置用户签名服务 主要是user-details 机制,
*
* @param auth 签名管理器构造器,用于构建用户具体权限控制
* @throws Exception
*/ @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
}
资源安全配置 和上一个配置相差不大
package com.oauth2.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
/**
* oauth2的资源控制配置信息
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("order");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()//禁用了 csrf 功能
.authorizeRequests()//限定签名成功的请求
.antMatchers("/decision/**","/govern/**").hasAnyRole("USER","ADMIN") //必须有USER或ADMIN角色的才能访问
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/test/**").authenticated()//必须认证过后才可以访问
.anyRequest().permitAll()//其他没有限定的请求,允许随意访问
.and().anonymous();//对于没有配置权限的其他请求允许匿名访问
}
}