一、概述
关于oauth2,其实是一个规范,对理论运行模式不清楚的可以参考下面链接
参考:
http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
https://oauth.net/2/
使用oauth2,主要就是以下三个组件
- 配置资源服务器
- 配置认证服务器
- 配置spring security
前两点是oauth2的主体内容,spring security oauth2是建立在spring security基础之上的,所以有一些体系是公用的。
oauth2根据使用场景不同,分成了4种模式
授权码模式(authorization code)
简化模式(implicit)
密码模式(resource owner password credentials)
客户端模式(client credentials)
本文重点讲解接口对接中常使用的密码模式(以下简称password模式)和客户端模式(以下简称client模式)。
授权码模式使用到了回调地址,是最为复杂的方式,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。
简化模式不常用。
二、环境准备
代码:https://github.com/lexburner/oauth2-demo
项目:client-credentials
工具:maven + jdk1.8 + redis
参考资料:https://www.cnkirito.moe/categories/Spring-Security-OAuth2/
三、代码解析
1、开启Debug日志
logging:
level:
org.springframework.security: DEBUG
2、maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 将token存储在redis中 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
定义两个接口作为访问的资源
@RestController
public class TestEndpoints {
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
//for debug
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "product id : " + id;
}
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
//for debug
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "order id : " + id;
}
}
3、配置资源服务器、授权服务器
@Configuration
public class OAuth2ServerConfig {
private static final String DEMO_RESOURCE_ID = "order";
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
// Since we want the protected resources to be accessible in the UI as well we need
// session creation to be allowed (it's disabled by default in 2.0.6)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.anonymous()
.and()
.authorizeRequests()
.antMatchers("/product/**").permitAll() // 【1】
.antMatchers("/order/**").authenticated(); // 配置order访问控制,必须认证过后才可以访问
// @formatter:on
}
}
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
UserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置两个客户端,一个用于password认证一个用于client认证
clients.inMemory()
.withClient("client_1")
.resourceIds(DEMO_RESOURCE_ID)
.authorizedGrantTypes("client_credentials")
.scopes("select")
.authorities("oauth2")
.secret("123456")
.and()
.withClient("client_2")
.resourceIds(DEMO_RESOURCE_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("select")
.authorities("oauth2")
.secret("123456");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(new RedisTokenStore(redisConnectionFactory))
.tokenStore(new InMemoryTokenStore())
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
// 允许 GET、POST 请求获取 token,即访问端点:oauth/token
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
endpoints.reuseRefreshTokens(true);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
//允许表单认证
oauthServer.allowFormAuthenticationForClients();
}
}
}
【1】暴露一个商品查询接口(/product/),后续不做安全限制;一个订单查询接口(/order/),后续添加访问控制。
资源服务器配置,需要继承 ResourceServerConfigurerAdapter,通过 @EnableResourceServer 注解启用
授权服务器配置,需要继承 AuthorizationServerConfigurerAdapter,通过 @EnableAuthorizationServer 注解启用
简单说下spring security oauth2的认证思路。
- client模式,没有用户的概念,直接与认证服务器交互,用配置中的客户端信息去申请accessToken,客户端有自己的 client_id,client_secret 对应于用户的 username,password,而客户端也拥有自己的authorities,当采取client模式认证时,对应的权限也就是客户端自己的authorities。
- password模式,自己本身有一套用户体系,在认证时需要带上自己的用户名和密码,以及客户端的 client_id,client_secret。此时,accessToken所含的权限是用户本身的权限,而不是客户端的权限。
我对于两种模式的理解便是,如果你的系统已经有了一套用户体系,每个用户也有了一定的权限,可以采用password模式;如果仅仅是接口的对接,不考虑用户,则可以使用client模式。
4、配置spring security基础配置
我们在概述环节就说了,Oauth2 是基于 spring security 的一个扩展,并不是独立于 spring security ,需要依赖 spring security 中的一些基础配置完成整个认证授权功能。
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 【1】
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user_1").password("123456").authorities("USER")
.and()
.withUser("user_2").password("123456").authorities("USER");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
// 【2】
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.antMatchers("/oauth/*").permitAll();
// @formatter:on
}
}
【1】配置的是认证信息, AuthenticationManagerBuilder 这个类,就是AuthenticationManager的建造者, 我们只需要向这个类中, 配置用户信息, 就能生成对应的AuthenticationManager, 这个类也提过,是用户身份的管理者, 是认证的入口。此处我们为认证管理器指定采用的认证数据源为内存,并且添加两个用户信息 user_1、user_2
【2】HttpSecurity 配置,主要功能就是对用户的请求进行拦截。通过重写
configure(HttpSecurity http)
方法就可以按照场景需求自定义认证和授权。主要配置Security的认证策略, 每个模块配置使用and结尾。常用api参考:https://my.oschina.net/liuyuantao/blog/1922347
此处的例子,拦截所有的用户请求
四、实验环境
1、获取token
启动springboot应用就可以发现多了一些自动创建的endpoints:
{[/oauth/authorize]}
{[/oauth/authorize],methods=[POST]
{[/oauth/token],methods=[GET]}
{[/oauth/token],methods=[POST]} // 重点关注一下/oauth/token,它是获取的token的endpoint
{[/oauth/check_token]}
{[/oauth/error]}
2、使用 postman 访问
password模式:
响应如下:
{
"access_token": "7544c14f-3d01-4da8-8b59-ac490fd3e346",
"token_type": "bearer",
"refresh_token": "1ae3d3e6-9cbb-4463-ab59-83525b4650c3",
"expires_in": 43199,
"scope": "select"
}
client模式:
响应如下:
{
"access_token": "652b8bd0-79ab-4c0e-993d-e618b59c090f",
"token_type": "bearer",
"expires_in": 43199,
"scope": "select"
}
访问两个资源接口
由于商品查询接口(/product/)不做安全限制,所以不传递token也是可以访问的
订单查询接口(/order/),添加了访问控制限制,所以不传token会报错
《1》商品查询接口 - 不带token请求支援(验证是否有访问限制)
http://localhost:8080/product/1
响应如下:
product id : 1
《2》订单查询接口 - 未带token进行访问
响应如下:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
《3》 订单查询接口 - 带token进行访问(此处随便用哪个模式的token均可)
http://localhost:8080/order/1?access_token=652b8bd0-79ab-4c0e-993d-e618b59c090f
响应如下:
order id : 1