oauth2的简单体验,后续会继续更新多点的内容(是自己看网课和文档,自己慢慢摸索出来的,所以写的比较杂,涉及简单的源码部分)
文章目录
一、简单体验
1. 搭建项目
没什么操作,注意控制住spring-cloud 和 spring-boot 的版本就OK。
依赖:
-pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- 安全框架-->
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-oauth2 -->
<!--这里面就会依赖 spring-cloud-starter-security 所以依赖这一个就 OK-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<!--todo 后续会使用到的-->
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
application.yaml文件
server:
port: 18001
spring:
application:
# 授权服务
name: oauth2-service
启动类:
主要是注解 @EnableAuthorizationServer
/* google 翻译
* 用于在当前应用程序上下文中启用授权服务器(即AuthorizationEndpoint和TokenEndpoint )的便利注释,
* 该上下文必须是DispatcherServlet上下文。
* 可以使用AuthorizationServerConfigurer类型的@Beans自定义服务器的许多功能
* (例如,通过扩展AuthorizationServerConfigurerAdapter )。
* 用户负责使用普通 Spring Security 功能( @EnableWebSecurity等)保护授权端点(/oauth/authorize),
* 但令牌端点(/oauth/token)将使用客户端凭据上的 HTTP 基本身份验证自动保护。
* 必须通过一个或多个 AuthorizationServerConfigurer 提供ClientDetailsService来注册客户端。
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({
AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {
}
尝试启动项目试试:
Using generated security password: 578f7a57-d39a-4470-a967-f3aa600f2b75
2022-02-28 21:10:39.959 INFO 22060 --- [ main] a.OAuth2AuthorizationServerConfiguration : Initialized OAuth2 Client
security.oauth2.client.client-id = 07d8e2f0-3829-4c48-bffc-1735fbb04eec
security.oauth2.client.client-secret = c48feec5-3e26-4546-8ef7-448a2646c65d
Will not secure Or [Ant [pattern='/oauth/token'], Ant [pattern='/oauth/token_key'], Ant [pattern='/oauth/check_token']]
按照Spring Security 的步骤,此时应该直接访问项目端口,但是却出现了 404。这是因为oauth2 版本的security 只开放了几个默认的访问路径,需要自己自定义。
/oauth/authorize: 验证
/oauth/token: 获取token
/oauth/confirm_access: 用户授权
/oauth/error: 认证失败
/oauth/check_token: 资源服务器用来校验token
/oauth/token_key: 如果jwt模式则可以用此来从认证服务器获取公钥
尝试访问 /oauth/authorize
,获取授权码,会发现这样的错误,提示“无效的客户端”。
这时我们查看Idea控制台的输出,会提示你输入的客户端Id 为 null。
2022-02-28 21:22:37.364 INFO 34584 --- [o-18001-exec-10] o.s.s.o.p.e.AuthorizationEndpoint : Handling ClientRegistrationException error:
No client with requested id: null
2. Oauth2.0中的一些概念
概念性的东西看看文档就好
Oauth2
Spring Security之Oauth2 2.0 client
Oahth2 五种授权模式介绍
接着上面:我们发现并没有指定 client_id
,那么我们通过拼接Url的形式,指定 client_id
,会发现依然不能访问。因为我们的后台并没有对客户端进行配置,授权中心是无法识别的。
3. 配置客户端
上面@EnableAuthorizationServllet
中的注解就有提到,我们需要重写AuthorizationServerConfigurerAdapter
类来实现自定义效果。
重写方法:
@Configuration
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
}
}
ClientDetailsServiceConfigurer
:(主要方法)
public class ClientDetailsServiceConfigurer extends
SecurityConfigurerAdapter<ClientDetailsService, ClientDetailsServiceBuilder<?>> {
// 内存中生成 客户配置信息的构造器
public InMemoryClientDetailsServiceBuilder inMemory() throws Exception {
InMemoryClientDetailsServiceBuilder next = getBuilder().inMemory();
setBuilder(next);
return next;
}
// 数据库中查找出来的 客户配置信息的 构造器
public JdbcClientDetailsServiceBuilder jdbc(DataSource dataSource) throws Exception {
JdbcClientDetailsServiceBuilder next = getBuilder().jdbc().dataSource(dataSource);
setBuilder(next);
return next;
}
那么我们先使用内存中配置客户信息的方式,观察InMemoryClientDetailsServiceBuilder
类(源码)。
// 继承了 客户详细信息服务生成器
public class InMemoryClientDetailsServiceBuilder extends
ClientDetailsServiceBuilder<InMemoryClientDetailsServiceBuilder> {
private Map<String, ClientDetails> clientDetails = new HashMap<String, ClientDetails>();
@Override
protected void addClient(String clientId, ClientDetails value) {
clientDetails.put(clientId, value);
}
@Override
protected ClientDetailsService performBuild() {
InMemoryClientDetailsService clientDetailsService = new InMemoryClientDetailsService();
clientDetailsService.setClientDetailsStore(clientDetails);
return clientDetailsService;
}
}
回到配置类,我们配置一个客户端Id,重启继续访问。观察页面和控制台。
clients.inMemory() // 使用内存配置方式
.withClient("client"); // 指定 客户端 Id
2022-02-28 21:47:34.872 INFO 22916 --- [io-18001-exec-1] o.s.s.o.p.e.AuthorizationEndpoint : Handling OAuth2 error:
error="unsupported_response_type", error_description="Unsupported response types: []"
# 提示 不支持这个响应类型
(补充一下,经过查阅源码,这里指定响应类型就好,系统底层会给clientid配置""的secret)
我们会发现,除了指定clientId
,还要指定其他的信息,通过观察,配置的客户端信息都是一个叫做 BaseClientDetails
(实现ClientDetails
)的类,这个类中封装着一些重要的属性。
这里只列举几个,具体自己查看:
clientId: ( 必须的)用来标识客户的Id。
secret: (需要值得信任的客户端)客户端安全码,如果有的话。
scope : 用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
authorizedGrantTypes: 此客户端可以使用的授权类型,默认为空。
authorities: 此客户端可以使用的权限(基于Spring Security authorities)。
jti: TOKEN_ID ,refreshToken标识
ati: ACCESS_TOKEN_ID,accessToken 标识
了解配置后,我们全部都配置上,重启项目访问。
配置类:
// 上面有提到 需要实现这个接口实现自定义功能
@Configuration
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 使用in-memory存储
.withClient("client") // client_id
.secret("secret") // client_secret
.authorizedGrantTypes("authorization_code") // 该client允许的授权类型
.redirectUris("http://www.baidu.com") // 不指定的话,页面访问后会提示的
.scopes("app"); // 允许的授权范围
}
}
这时我们发现,浏览器重定向到了百度的页面。但是,项目后台还是在报错。
4. 配置用户信息
还是提示响应类型不正确,并且出现了新的错误,说我们没有指定用户信息。(用户必须先通过 Spring Security 进行身份验证才能完成授权)
这里配置的用户信息,我们先配置一个简单的固定的用户。重启项目继续访问。
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 定义加密方式,以及用户校验
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(
new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User("admin", passwordEncoder().encode("admin"), Collections.emptyList());
}
}
);
}
}
这时就是熟悉的 界面,然后输入用户名密码。会发现百度页面还是正常的跳转。后台用户未授权的提示信息也没有了。
响应类型问题:我们可以在Url后面通过拼接参数指定。http://localhost:18001/oauth/authorize?client_id=client&response_type=code
点击同意后点击授权,页面正常跳转。并且观察 url 地址,发生了变化 :https://www.baidu.com/?code=naPnfX
5. 授权码获取token
到这一步。授权中的授权码就获取完毕了,接下来就是换取 token的步骤。因为框架默认不提供 token的实现,这里我们需要自己配置以下。
官网提供了三种模式:默认应该是内存实现的
我们可以查看以下 InMemoryClientDetailsService
类,使用Debug工具打一个端点。
我们访问http://localhost:18001/oauth/token
。会发现弹出一个密码框,这个有点像security的用户密码框,但是这里不是。我们随便填写信息,点击登录。
如上图,我们发现访问时,后台Debug生效了,查看断点位置,可以发现,我们输入的用户名对应的后台的 clientId
,系统会去内存中存储的 客户端 管理信息缓存中查找对应的配置信息,咱们这里当然时没有的,那么我们指定对应的 clientId
试试。 继续观察 debug .
通过Debug 我们发现,第一次 debug 位置返回的客户端配置信息,传递 到了ClientDetailsUserDetailsService
(客户详细信息用户详细信息服务)类中。上面是 查找 username 对应的 客户端,
最后创建一个security提供的 User对象(之前学习过 security应该比较熟悉)。用户名为 clientId,密码为 clientSecrect,权限为客户端配置信息的所有权限。继续 Debug.
上面大致就是返回一个 用户配置信息对象,就是刚才 返回User 实现的类。继续debug
我们发现来到了AbstractUserDetailsAuthenticationProvider
(用户详细信息身份验证提供程序),Assert.notNull
验证用户不为空。继续网下面看,我们会发现,检查用户配置信息时,抛出一个异常。
提示用户有不好的信誉历史。
我们再试一次,点进去 additionalAuthenticationChecks
方法,可以看到,走进去的错误为,我们提供的密码不正确。
上面提示我们输入的 secret
密码和 用户配置信息中对比到的不正确,猜想可能是因为在MyWebSecurityConfig
中配置了用户密码加密方式(BCryptPasswordEncoder
),然而我们的客户端信息配置时,没有使用到加密。
配置加密
// 客户配置
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 使用in-memory存储
.withClient("client") // client_id
.secret(new BCryptPasswordEncoder().encode("secret")) // client_secret 使用相同的加密方式
.authorizedGrantTypes("authorization_code") // 该client允许的授权类型
.redirectUris("http://www.baidu.com") // 不指定的话,页面访问后会提示的
.scopes("app"); // 允许的授权范围
}
继续访问,我们发现后台提示错误信息 ,这是因为请求方式不支持 post,我们换成测试工具测试。
2022-03-01 10:52:31.231 INFO 22860 --- [io-18001-exec-4] o.s.s.o.p.e.AuthorizationEndpoint : Handling OAuth2 error: error="unsupported_response_type", error_description="Unsupported response types: []"
2022-03-01 10:53:32.488 INFO 22860 --- [io-18001-exec-1] o.s.s.o.provider.endpoint.TokenEndpoint : Handling error: HttpRequestMethodNotSupportedException, Request method 'GET' not supported
工具配置
配置解释:请求体中指定授权类型,同时加上第一次测试拿到的授权码。因为第一次弹出了用户名密码框,所以这里配置 Auth信息,用户名为 ClientId,密码为 secret。
{
"access_token": "eac28570-3abd-4225-bb42-ac5faa94fcf0",
"token_type": "bearer",
"expires_in": 43117,
"scope": "app"
}
6. 校验 token
当我们获取到 access_token
后,按理来说应该可以访问接口/oauth/check_token
,但是,通过访问可以看到,我们的请求被拒绝了。提示未授权,
我们可以发现自定义的MyAuthorizationServerConfig
,只是重写了其中的一个功能(客户信息配置),不难发现,还有一个是关于授权服务器安全的配置(AuthorizationServerSecurityConfigurer security
)。查看他的属性(源码)。
public final class AuthorizationServerSecurityConfigurer extends
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
// 身份验证入口点
private AuthenticationEntryPoint authenticationEntryPoint;
// 拒绝访问处理程序
private AccessDeniedHandler accessDeniedHandler = new OAuth2AccessDeniedHandler();
// 密码编码器
private PasswordEncoder passwordEncoder; // for client secrets
// https://blog.csdn.net/xichenguan/article/details/77965208
// https://blog.csdn.net/qq_35067322/article/details/120030403
// 领域(我也不理解,可以看其他的博客)
private String realm = "oauth2/client";
// 允许客户端的表单身份验证
private boolean allowFormAuthenticationForClients = false;
// 令牌密钥访问(默认全部拒绝)
private String tokenKeyAccess = "denyAll()";
// 检查令牌访问
private String checkTokenAccess = "denyAll()";
// 仅限于 ssl
private boolean sslOnly = false;
/**
* Custom authentication filters for the TokenEndpoint. Filters will be set upstream of the default
* BasicAuthenticationFilter.
*/
// 令牌端点身份验证过滤器
private List<Filter> tokenEndpointAuthenticationFilters = new ArrayList<Filter>();
}
接下来我们给 checkTokenAccess
放行,加上以下配置
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("permitAll()"); // 全部放行
}
重启项目再次测试,会得到以下结果,以下结果就是验证通过的意思。
{
"active": true,
"exp": 1646157970,
"user_name": "admin",
"client_id": "client",
"scope": [
"app"
]
}
以下为错误信息,提示 token 未通过验证
{
"error": "invalid_token",
"error_description": "Token was not recognised"
}
1)校验 token的过程
根据控制台提示报错的信息,可以发现是CheckTokenEndpoint
这个类中做的校验,搜索该类,可以发现下面的方法,打断点进行测试.(源码)
@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
// 从指定的token 缓存中获取这个 token
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
// 返回的信息就是这里面定义的,
Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);
// gh-1070
response.put("active", true); // Always true if token exists and not expired
return response;
}
通过Debug ,可以找到更具体地校验流程实在 InMemoryTokenStore
中做地,也就是我们之前实现的 token 存储在内存中,他会拿着我们输入的toekn 作为key去内存中 查找。
这里就简单的配置就完啦,后续有时间会继续更新的,同时也是对自己的一个学习的过程。