一、简介
前面的 gitee 授权登录主要向大家展示了 OAuth2 中客户端的工作模式。对于大部分的开发者而言,日常接到的OAuth2都是开发客户端,例如接入00登录、接入微信登录等。不过也有少量场景,可能需要开发者提供授权服务器与资源服务器,接下来我们就通过一个完整的案例演示如何搭建授权服务器资源服务器。
搭建授权服务器,我们可以选择一些现成的开源项目,直接运行即可,例如:
一、KeycLoak: RedFat 公司提供的开源工具,提供了很多实用功能,倒如单点登录、支持OpenID、可视化后台管理等。
二、Apache oltu: Apache 上的开源项目、最近几年没怎么维护了。
接下来我们将搭建一个包含授权服务器、资源服务器以及客户端在内的 0Auth2 案例
项目规划首先把项目分为三部分:
。授权服务器:采用较早的 spring-cloud-starter-oauth2 来搭建授权服务器
。资源服务器:采用最新的 Sring Security 5.x 搭建资源服务器,
。客户端:采用最新的 Spring Security5.X 搭建客户端
二、授权服务器搭建
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<!-- web 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringSecurity依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- oauth2 服务端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
2.1 基于内存客户端和令牌存储
我们暂时将认证信息放到内存中,保护四种认证 授权码 简化模式 密码模式 客户端模式
2.1.1 授权码模式
当用户点击同意授权后,会重新redirect 一个页面,携带一个code ,我们使用code 最终换取 token令牌
@Configuration
@EnableAuthorizationServer //指定当前应用为授权服务器
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
@Autowired
public AuthorizationServerConfiguration(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
// 用来配置授权服务器可以为哪些客户端授权 client_id,secret redirect_url
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() //基于内存,后期可以入库查出来
.withClient("client-lq")
// .secret(passwordEncoder.encode("secret-lq"))
.secret("secret-lq")
.redirectUris("https://www.baidu.com") // 重定向的url
.authorizedGrantTypes("authorization_code") // 授权服务器支持的模式 仅支持授权码模式
.scopes("read:user");// 令牌允许获取的资源权限
}
// 授权码模式
// 1、请求用户是否授权 /oauth/authorize
// 完整路径: http://localhost:8082/oauth/authorize?client_id=client-lq&response_type=code&redirect_url=https://www.baidu.com
// 2、授权之后根据授权码获取令牌 /oauth/token id secret redirectUri code
// 完整路径: curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=9uqkzJ&redirect_uri=https://www.baidu.com' "http://client-lq:secret-lq@127.0.0.1:8082/oauth/token"
}
2.1.1.1 security 配置
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password(passwordEncoder().encode("123456")).roles("ADMIN").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()// 开启表单登录
.and()
.csrf().disable();
}
}
2.1.1.2 授权码模式获取 code
我们通过请求用户是否授权 /oauth/authorize 标准接口固定uri,这一步用来换取 code
完整url:
http://localhost:8082/oauth/authorize?client_id=client-lq&response_type=code&redirect_url=https://www.baidu.com
在浏览器中回车后,得到一个用户同意授权的界面
当我们点击同意后,会重定向 https://www.baidu.com 后面接了一个参数为 code
2.1.1.3 获取token
标准接口 /oauth/token
我们将第一步获取到的 code 用来换取对应的 token ,这一步我们需要用 post 请求,我们可以使用 curl 或者 apifox
对应的地址为:http://client-lq:secret-lq@127.0.0.1:8082/oauth/token
可能在以为这个client-lq:secret-lq 是什么,这是 OATH2会将clientid:secret 拼接后,然后进行加密返回我们的code
client-lq 为我们的clientId 这个我们配置写的什么这里就写什么,保持一致
secret-lq 为secret 这个需要加密,如果不加密会有问题,也是我们自己配置的,保持一致
我们使用apifox 请求接口,有几个固定参数
grant_type :authorization_code
code 为第一步获取到的值
redirect_uri:https://www.baidu.com
cookie 需要自己在浏览器登录后,放进去,否则会有登录会拦截;
2.1.2 简化模式
增加参数,implicit 经测试,该模式需要自己写script 脚本,暂时不支持
2.1.2.1 服务端配置
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() //基于内存,后期可以入库查出来
.withClient("client-lq")
.secret(passwordEncoder.encode("secret-lq"))
//.secret("secret-lq")
.redirectUris("https://www.baidu.com") // 重定向的url
.authorizedGrantTypes("authorization_code","refresh_token",) // 授权服务器支持的模式 .scopes("read:user");// 令牌允许获取的资源权限
}
2.1.2.2 测试效果
2.1.3 密码模式
使用用户名密码,换取token ,需要设置userdetailService,请求参数需要有需要增加username password用户登录的信息 ,注意主要暴露一个 AuthenticationManager 认证管理器
2.1.3.1 服务端配置
//security 配置
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
// 认证服务器配置
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() //基于内存,后期可以入库查出来
.withClient("client-lq")
.secret(passwordEncoder.encode("secret-lq"))
.redirectUris("https://www.baidu.com") // 重定向的url
.authorizedGrantTypes("authorization_code","refresh_token","implicit","password")
.scopes("read:user");
}
2.1.3.2 测试效果
grant_type 需要为password
2.1.4 客户端模式
增加参数:client_credentials
该模式是对客户端高度信任,是同一家公司,直接颁发凭证;容易泄露,慎重
2.1.4.1 服务端配置
// 认证服务器配置
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() //基于内存,后期可以入库查出来
.withClient("client-lq")
.secret(passwordEncoder.encode("secret-lq"))
.redirectUris("https://www.baidu.com") // 重定向的url
.authorizedGrantTypes("authorization_code","refresh_token","implicit""client_credentials")
.scopes("read:user");
}
2.1.4.2 测试效果
grant_type 需要为client_credentials
2.2 令牌刷新
令牌刷新机制是为了token 不过期,我们动态刷新延长token 失效时间,这个模式适用于 授权码模式 ,简化模式,密码模式,客户端模式 ;属于几个模式共有;
需要注意该接口需要指定 userDetailService,否则会提示 Handling error: IllegalStateException, UserDetailsService is required.
2.2.1 服务端配置
增加 refresh_token 参数
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.userDetailsService(userDetailsService);
}
2.2.2 查看效果
授权码模式多返回了一个参数refresh_token ,我们那这个这个参数去刷新
2.2.3 apifox 刷新token
请求完整连接:http://client-lq:secret-lq@127.0.0.1:8082/oauth/token
参数:refresh_token,grant_type =refresh_token,我们看到每一次刷新,这个token都会发生改变;
2.3 基于数据库客户端和令牌存储
因基于数据库持久化需要用到表结构,索性就放到下一个章节,里面包含持久化认证服务器和授权服务器放到一块,有兴趣的小伙伴看下一章连接;
三、授权错误总结
当我们将 secret 设置为明文,这个时候我们用apifox请求获取token接口,这个时候会提示401
3.2 错误2
Handling error: IllegalStateException, UserDetailsService is required.
说明是UserDetailsService没有设置进入,需要设置
endpoints.userDetailsService(userDetailsService);
3.2 错误3
password grant type not supported from token endpoint
说明是authenticationManager 没有设置,我们将之暴露出来,设置到 认证服务器配置中
endpoints.authenticationManager(authenticationManager);