首页 > 基础资料 博客日记
OAuth2 详细介绍!
2023-07-24 15:20:10基础资料围观305次
目录
一、文章介绍
如今很多互联网应用中,OAuth2 是一个非常重要的认证协议,很多场景下都会用到它,Spring Security 对 OAuth2 协议提供了相应的支持。开发者非常方便的使用 OAuth2 协议 。
- OAuth2 简介
- 四种授权模式
- Spring Security OAuth2
- GitHub 授权登录
- 授权服务器与资源服务器
- 使用 JWT
二、OAth2
2.1 简介
OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源 (如头像、照片、视频等),并且在这个过程中无须将用户名和密码提供给第三方应用。通过令牌 (token) 可以实现这一功能。每一个令牌授权一个特定的网站在特定的时间段内允许可访问特定的资源。OAuth 让用户可以授权第三方网站灵活访问它们存储在另外一些资源服务器上的特定信息,而非所有的内容。对于用户而言,我们在互联网应用中最常见的 OAuth 应用就是各种第三方登录,例如 QQ授权登录、微信授权登录、微博授权登录、GitHub 授权登录等。
例如用户想登录 Ruby China ,传统方式是使用用户名和密码但是这样并不安全,因为网站会存储你的用户名密码,这样可能会导致密码泄露。这种授权方式安全隐患很大如果使用 OAuth 协议就能很好地解决这一问题。
oAuth2协议解决了多个网站登录问题,账号密码不安全的问题,比如一些小众的网站就可以不用注册登录,使用 oAuth2,也就是通过第三方向要访问的网站发送请求获取Token,第三方网站每次请求写到Token就可以访问到内容。
注意:OAuth2 是 OAuth 协议的下一个版本,但不兼容 OAth 1.0 ,OAth2 关注客户端开发者的简易性,同时为 Web 应用、桌面应用、移动设备、 IOT 设备提供专门的认证流程。
以上大致流程,角色不完善。
oAuth2就是对用户的信息进步的保护,如很多喜欢将所有密码设置为同样的,就可能会泄露,但是通过已经注册过的网站用户信息,来进行授权给第三方网站信息进行登录则免去了注册,oAuth主要做的就是认证保护用户隐私安全。
2.2 OAuth2 授权总体流程
角色梳理 :第三方应用 <--------> 存储用户私密信息应用 ----------> 授权服务器 ------> 资源服务器
整体流程如下:(图片来自 RFC6749 文档 https://tools.ietf.org/html/rfc6749)
# 官网流程
- (A) 用户打开客户端以后,客户端要求用户给予授权。
- (B) 用户同意给予客户端授权。
- (C) 客户端使用上一步获得的授权,向认证服务器申请令牌。
- (D) 认证服务器对用户端进行认证以后,确认无误,同意发放令牌。
- (E) 客户端使用令牌,向资源服务器申请资源。
- (F) 资源服务器确认令牌无误,同意向客户端开放资源。# 例子流程
- 1.用户打开第三方网站如 (京东),用户点击了微信授权登录,此时 就会跳转到 微信的授权页面。
- 2.用户点击授权给京东后,进行授权认证,授权成功会进行回调到京东回调页面。
- 3.授权页面会发起请求向授权服务器索要授权令牌。
- 4.授权服务器将授权令牌进行返回,用户此时可以在第三方网站(京东)向 微信服务器携带令牌获取部分用户信息 。
- 5.用户此时可以在第三方网站(京东)向 微信服务器携带令牌获取部分用户信息 。
- 6.资源服务器将资源返回给第三方网站
从上图中我们可以看出六个步骤之中,B是关键,即用户怎么才能给于客户端授权。同时会发现 OAuth2 中包含四种不同角色:
- Client :第三方应用。
- Resource Owner:资源所有者。
- Authorizetion Server :授权服务器。
- Resource Server :资源服务器。
授权服务器和资源服务器可以放一起,但是在如今的互联网和分布的推动下,都是分别存储。
2.3 四种授权模式
OAuth2 协议一种支持四种不同的授权模式:
授权码模式
:常见的第三方平台登录功能基本都是使用这种模式。(安全性高)简化模式
:简化模式是不需要第三方服务端(客户端)参与,直接在浏览器中向授权服务器申请令牌(token),如果网站是纯静态页面,则可以采用这种方式。密码模式
:密码模式是用户把用户名/密码直接告诉客户端,客户端使用在这些信息项授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务器提供商就是同一家公司。客户端模式
:客户端模式是指客户端使用自己的名义而不是用户的名义向授权服务器提供申请授权。严格来说,客户端模式并不能算作 OAuth 协议解决问题的一种解决方案,但是对于开发者而言,在一些为移动端提供的授权服务器上使用这种模式还是非常方便的。
用的最多的就是 最多的就是授权码模式
无论那种授权模式,其授权流程都是相似的,只不过在个别步骤上有差异而已。如上图所示。
1.授权码模式
授权码模式(Authorization Code ) 是功能最完整,流程最严密、最全并且使用最广泛的一种 OAuth2 授权模式。同时也是最复杂的一种授权模式,它的特点就是通过客户端的后台服务器,与服务器提供商的认证服务器进行交互。其具体授权流程如下
(图片来自 RFC6749 文档 https://tools.ietf.org/html/rfc6749)
- Third-party application :第三方应用程序,简称 "客户端" (client);
- Resource Owner :资源所有者,简称 "用户" (User);
- User Agent :用户代理,是指浏览器;
- Authorization Server:认证服务器,即服务器专门用来处理认证的服务器;
- Resource Server :资源服务器,即服务端存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
- Redirection URL :重定向的 URI 就是授权后要回到的网站
# 授权流程
- AB 两步在进行获取授权码
- 1.用户点击授权登录,通过浏览器,会先到Github授权服务器
- 2.授权服务器此时会有一张页面是否允许授权登录
- 3.允许后才是把客户端信息发送给 授权服务器.
- 4.授权服务根据客户端密钥,授权模式等进行认证,成功后会颁发一个对象数据返回,如授权码模式就会颁发一个授权码给浏览器,最终浏览器跟重定向会我们的客户端。
- 5.客户端会再一次发送请求带着授权码以及重定向的URI再次想授权服务器获取令牌
- 6.授权服务器根据授权码进行比较,如果成功将返回一个令牌。
-
注意:授权码是一次性的。认证服务器和授权服务器是可以放在一起
主要分为两部:1.客户端向授权服务器索要授权码,2.获取授权码后再次向授权服务器携带授权码索要令牌。
在最后,拿到令牌后,再拿着令牌向资源服务器索要资源。
具体流程如下:
- (A) 用户访问第三方应用,第三方应用通过浏览器导向认证服务器。
- (B)用户选择是否给予用户端授权。
- (C)假设用户给予授权,认证服务器将用户导向客户端事先指定的 "重定向URI"(redirection URI),同时附上一个授权码。
- (D)客户端收到授权码,附上早先的 "重定向URI",向认证服务器申请令牌。这一步是在客户端的后端的服务器上完成的。对用户不可见。
- (E)认证服务器对授权码和重定向URI ,确认无误后。想客户端发送令牌(access token ) 和更新令牌(refresh token)。
核心参数:
https://wx.com/oauth/authorize?response_type=code&client_id=CLENT_Id&redirect_uri=http://www.baidu.com&scope=read
2.简化模式
简化模式(implicit
grant type ) 不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了 "授权码" 这个步骤,因此得名。所有步骤在浏览器中完成,令牌访问者是可见的,而客户端不需要认证。其具体的授权流程如图所示
(图片来自 RFC6749 文档 http://tools.ietf.org/html/rfc6749)
这种模式令牌解析比较都在浏览器不是在安全,spring sercuirty 不支持。
- (A)第三方应用将用户导向认证服务器。
- (B)用户决定是否给予客户端授权。
- (C)假设用户给予授权,认证服务器将用户导向客户端指定的 “重定向URI”,并在URI的Hash部分包含了访问令牌。# token 。(也就是# 路径方式)
- (D)浏览器向资源服务器发出请求,其中包含上一步收到的 Hash 值。
- (E)资源服务器返回一个网页,其中包含的代码可以取出 Hash值中的令牌。
- (F)浏览器执行上一步获得脚本,提取出令牌。
- (G)浏览器将令牌发给客户端。
核心参数:
https://wx.com/auth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=http://www.baidu.com&scope=read
3.密码模式
密码模式(Resource Owner Password Credentials Grant) 中,用户向客户端提供自己的用户名和密码,客户端使用这些信息,向” 服务提供商“ 索要授权。这种模式中,用户必现把自己的密码给客户端,但是客户端不得存储密码,这通常在用户对客户端高度信任的清下,比如客户端是操作系统的一部分,或者由一个相同公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。其具体授权流程图所示
(图片来自 RFC6749 https://tools.ieft.org/html/rfc6749)
具体步骤如下:
- (A)用户向客户端提供用户名和密码
- (B)客户端用户和密码发给认证服务器,后者请求令牌
- (C)认证服务器确认无误后,向客户端提供访问令牌。
核心参数:
https://wx/com/token?grant_type=password&username=Username&password=PASSWORD&client_id =CLIENT_ID
4. 客户端模式
客户端模式(Client Credentials Grant) 指客户端以自己的名义,而不是用户的名义,向 ”服务提供商“进行认证,严格地说,客户端模式并不属于 OAuth框架索要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求 ”服务提供商“ 提供服务,其实不存在授权问题。
步骤如下:
- (A)客户端想认证服务器进行身份认证,并要求一个访问令牌。
- (B)认证服务器确认无误后,向客户端提供访问令牌。
https://wx.com/token?grant_type=client_credentials&client_idCLIENT_ID&client_secret=CLEINT_SECRET
这种方式没有授权的过程,是直接那种密钥和客户端编号向要授权服务器去要。这种适合于有凭证信息,有某种协议,直接要直接颁发令牌。
2.4 OAuth2 标准接口
- /oauth/authorize:授权端点 ,固定授权入口路径 ,也是授权服务器的用户允许授权的页面
- /oauth/token :获取令牌端点
- /oauth/confirm_access:用户确认授权提交端点
- /oauth/error:授权服务错误信息端点
- /oauth/check_token:用于资源服务访问的令牌解析端点
- /oauth/token_key:提供公有密钥的端点,如果使用 JWT 令牌的话
以上是规范化固定接口 ,端口就是路径
2.5 GitHub 授权登录
该项目是实现创建一个应用,调用第三方Github方式进行认证操作 。
主要流程:
-
-
- 1.需要获取到GitHub授权认证,并获取到GitHub的 ClientId 和 sererc
- 2.创建一个应用使用GitHub授权方式认证。
-
1.创建 OAuth应用
访问 github 并登录,在 Sign in to GitHub · GitHub 中找到Developer settings 选项
- 创建 OAuth App并输入基本信息:
- Applicatiion name :应用名称
- Homepage URL :授权的服务器路径
- Application Description :介绍
- Authorization callback URL :就是颁发令牌之后返回的地址
认证成功后会生成 Client_id 和secret 在授权登录时需要使用到
2.项目开发
1.创建 SpringBoot 应用 ,并引入依赖
<!--security oAuth2 的客户端依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!--security 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.创建测试 Controller
@RestController
public class IndexController {
@GetMapping("/getOAuthInfo")
public DefaultOAuth2User getOAuthInfo(){
/**
* 使用 OAuth2 认证成功后也会被放入到 Session中
*/
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (DefaultOAuth2User) authentication.getPrincipal();
}
}
3.配置 Security 配置类
@Configuration
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and() // 支持 oAuth2 和其他方式同时开启认证方式
.oauth2Login(); // 使用 oAuth2 认证 需要在配置文件中配置认证服务
}
}
4.配置配置文件
# 注册客户端编号
spring.security.oauth2.client.registration.github.client-id=178210b433b87f612163
# 注册客户端密钥
spring.security.oauth2.client.registration.github.client-secret=9cf77bbbf0b164113bc0554d5130f65adbe60b37
# 授权成功的重定向密钥
spring.security.oauth2.client.registration.github.redirect-uri=http://localhost:8080/login/oauth2/code/github
可以指定如下信息
5.代码测试
点击 GitHub登录后会跳转到 GitHub的授权页面 ,携带必要的参数
6.总结
如果向让自己开发的应用使用oAuth2 进行授权,首先第一点需要在第三方应用注册 获取到clientId 和 client secret,第二点创建 SpringBoot 应用 添加 spring-security-client 依赖,客户端依赖并配置授权登录认证方式即可。
3.原理分析
众所周知,Spring Security 的底层实现是通过大量的Filter 实现的,那么OAuth2的实现也离不开 Filter。
OAuth2LoginAuthenticationFilter | 处理 OAuth2 认证的 | NO |
OAuth2AuthorizationCodeGrantFilter | 处理OAuth2认证中授权码 | NO |
1.oauth2Login 源码分析
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.CustomUserTypesOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.DelegatingOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* An {@link AbstractHttpConfigurer} for OAuth 2.0 Login,
* which leverages the OAuth 2.0 Authorization Code Grant Flow.
*
* <p>
* OAuth 2.0 Login provides an application with the capability to have users log in
* by using their existing account at an OAuth 2.0 or OpenID Connect 1.0 Provider.
*
* <p>
* Defaults are provided for all configuration options with the only required configuration
* being {@link #clientRegistrationRepository(ClientRegistrationRepository)}.
* Alternatively, a {@link ClientRegistrationRepository} {@code @Bean} may be registered instead.
*
* <h2>Security Filters</h2>
*
* The following {@code Filter}'s are populated:
*
* <ul>
* <li>{@link OAuth2AuthorizationRequestRedirectFilter}</li>
* <li>{@link OAuth2LoginAuthenticationFilter}</li>
* </ul>
*
* <h2>Shared Objects Created</h2>
*
* The following shared objects are populated:
*
* <ul>
* <li>{@link ClientRegistrationRepository} (required)</li>
* <li>{@link OAuth2AuthorizedClientRepository} (optional)</li>
* <li>{@link GrantedAuthoritiesMapper} (optional)</li>
* </ul>
*
* <h2>Shared Objects Used</h2>
*
* The following shared objects are used:
*
* <ul>
* <li>{@link ClientRegistrationRepository}</li>
* <li>{@link OAuth2AuthorizedClientRepository}</li>
* <li>{@link GrantedAuthoritiesMapper}</li>
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not configured
* and {@code DefaultLoginPageGeneratingFilter} is available, then a default login page will be made available</li>
* </ul>
*
* @author Joe Grandja
* @author Kazuki Shimizu
* @since 5.0
* @see HttpSecurity#oauth2Login()
* @see OAuth2AuthorizationRequestRedirectFilter
* @see OAuth2LoginAuthenticationFilter
* @see ClientRegistrationRepository
* @see OAuth2AuthorizedClientRepository
* @see AbstractAuthenticationFilterConfigurer
*/
public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> extends
AbstractAuthenticationFilterConfigurer<B, OAuth2LoginConfigurer<B>, OAuth2LoginAuthenticationFilter> {
private final AuthorizationEndpointConfig authorizationEndpointConfig = new AuthorizationEndpointConfig();
private final TokenEndpointConfig tokenEndpointConfig = new TokenEndpointConfig();
private final RedirectionEndpointConfig redirectionEndpointConfig = new RedirectionEndpointConfig();
private final UserInfoEndpointConfig userInfoEndpointConfig = new UserInfoEndpointConfig();
private String loginPage;
private String loginProcessingUrl = OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI;
/**
* Sets the repository of client registrations.
*
* @param clientRegistrationRepository the repository of client registrations
* @return the {@link OAuth2LoginConfigurer} for further configuration
*/
public OAuth2LoginConfigurer<B> clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
return this;
}
/**
* Sets the repository for authorized client(s).
*
* @since 5.1
* @param authorizedClientRepository the authorized client repository
* @return the {@link OAuth2LoginConfigurer} for further configuration
*/
public OAuth2LoginConfigurer<B> authorizedClientRepository(OAuth2AuthorizedClientRepository authorizedClientRepository) {
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
this.getBuilder().setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository);
return this;
}
/**
* Sets the service for authorized client(s).
*
* @param authorizedClientService the authorized client service
* @return the {@link OAuth2LoginConfigurer} for further configuration
*/
public OAuth2LoginConfigurer<B> authorizedClientService(OAuth2AuthorizedClientService authorizedClientService) {
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.authorizedClientRepository(new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService));
return this;
}
@Override
public OAuth2LoginConfigurer<B> loginPage(String loginPage) {
Assert.hasText(loginPage, "loginPage cannot be empty");
this.loginPage = loginPage;
return this;
}
@Override
public OAuth2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) {
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty");
this.loginProcessingUrl = loginProcessingUrl;
return this;
}
/**
* Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization Server's Authorization Endpoint.
*
* @return the {@link AuthorizationEndpointConfig}
*/
public AuthorizationEndpointConfig authorizationEndpoint() {
return this.authorizationEndpointConfig;
}
/**
* Configures the Authorization Server's Authorization Endpoint.
*
* @param authorizationEndpointCustomizer the {@link Customizer} to provide more options for
* the {@link AuthorizationEndpointConfig}
* @return the {@link OAuth2LoginConfigurer} for further customizations
*/
public OAuth2LoginConfigurer<B> authorizationEndpoint(Customizer<AuthorizationEndpointConfig> authorizationEndpointCustomizer) {
authorizationEndpointCustomizer.customize(this.authorizationEndpointConfig);
return this;
}
/**
* Configuration options for the Authorization Server's Authorization Endpoint.
*/
public class AuthorizationEndpointConfig {
private String authorizationRequestBaseUri;
private OAuth2AuthorizationRequestResolver authorizationRequestResolver;
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
private AuthorizationEndpointConfig() {
}
/**
* Sets the base {@code URI} used for authorization requests.
*
* @param authorizationRequestBaseUri the base {@code URI} used for authorization requests
* @return the {@link AuthorizationEndpointConfig} for further configuration
*/
public AuthorizationEndpointConfig baseUri(String authorizationRequestBaseUri) {
Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty");
this.authorizationRequestBaseUri = authorizationRequestBaseUri;
return this;
}
/**
* Sets the resolver used for resolving {@link OAuth2AuthorizationRequest}'s.
*
* @since 5.1
* @param authorizationRequestResolver the resolver used for resolving {@link OAuth2AuthorizationRequest}'s
* @return the {@link AuthorizationEndpointConfig} for further configuration
*/
public AuthorizationEndpointConfig authorizationRequestResolver(OAuth2AuthorizationRequestResolver authorizationRequestResolver) {
Assert.notNull(authorizationRequestResolver, "authorizationRequestResolver cannot be null");
this.authorizationRequestResolver = authorizationRequestResolver;
return this;
}
/**
* Sets the repository used for storing {@link OAuth2AuthorizationRequest}'s.
*
* @param authorizationRequestRepository the repository used for storing {@link OAuth2AuthorizationRequest}'s
* @return the {@link AuthorizationEndpointConfig} for further configuration
*/
public AuthorizationEndpointConfig authorizationRequestRepository(AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository) {
Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
this.authorizationRequestRepository = authorizationRequestRepository;
return this;
}
/**
* Returns the {@link OAuth2LoginConfigurer} for further configuration.
*
* @return the {@link OAuth2LoginConfigurer}
*/
public OAuth2LoginConfigurer<B> and() {
return OAuth2LoginConfigurer.this;
}
}
/**
* Returns the {@link TokenEndpointConfig} for configuring the Authorization Server's Token Endpoint.
*
* @return the {@link TokenEndpointConfig}
*/
public TokenEndpointConfig tokenEndpoint() {
return this.tokenEndpointConfig;
}
/**
* Configures the Authorization Server's Token Endpoint.
*
* @param tokenEndpointCustomizer the {@link Customizer} to provide more options for
* the {@link TokenEndpointConfig}
* @return the {@link OAuth2LoginConfigurer} for further customizations
* @throws Exception
*/
public OAuth2LoginConfigurer<B> tokenEndpoint(Customizer<TokenEndpointConfig> tokenEndpointCustomizer) {
tokenEndpointCustomizer.customize(this.tokenEndpointConfig);
return this;
}
/**
* Configuration options for the Authorization Server's Token Endpoint.
*/
public class TokenEndpointConfig {
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private TokenEndpointConfig() {
}
/**
* Sets the client used for requesting the access token credential from the Token Endpoint.
*
* @param accessTokenResponseClient the client used for requesting the access token credential from the Token Endpoint
* @return the {@link TokenEndpointConfig} for further configuration
*/
public TokenEndpointConfig accessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Returns the {@link OAuth2LoginConfigurer} for further configuration.
*
* @return the {@link OAuth2LoginConfigurer}
*/
public OAuth2LoginConfigurer<B> and() {
return OAuth2LoginConfigurer.this;
}
}
/**
* Returns the {@link RedirectionEndpointConfig} for configuring the Client's Redirection Endpoint.
*
* @return the {@link RedirectionEndpointConfig}
*/
public RedirectionEndpointConfig redirectionEndpoint() {
return this.redirectionEndpointConfig;
}
/**
* Configures the Client's Redirection Endpoint.
*
* @param redirectionEndpointCustomizer the {@link Customizer} to provide more options for
* the {@link RedirectionEndpointConfig}
* @return the {@link OAuth2LoginConfigurer} for further customizations
*/
public OAuth2LoginConfigurer<B> redirectionEndpoint(Customizer<RedirectionEndpointConfig> redirectionEndpointCustomizer) {
redirectionEndpointCustomizer.customize(this.redirectionEndpointConfig);
return this;
}
/**
* Configuration options for the Client's Redirection Endpoint.
*/
public class RedirectionEndpointConfig {
private String authorizationResponseBaseUri;
private RedirectionEndpointConfig() {
}
/**
* Sets the {@code URI} where the authorization response will be processed.
*
* @param authorizationResponseBaseUri the {@code URI} where the authorization response will be processed
* @return the {@link RedirectionEndpointConfig} for further configuration
*/
public RedirectionEndpointConfig baseUri(String authorizationResponseBaseUri) {
Assert.hasText(authorizationResponseBaseUri, "authorizationResponseBaseUri cannot be empty");
this.authorizationResponseBaseUri = authorizationResponseBaseUri;
return this;
}
/**
* Returns the {@link OAuth2LoginConfigurer} for further configuration.
*
* @return the {@link OAuth2LoginConfigurer}
*/
public OAuth2LoginConfigurer<B> and() {
return OAuth2LoginConfigurer.this;
}
}
/**
* Returns the {@link UserInfoEndpointConfig} for configuring the Authorization Server's UserInfo Endpoint.
*
* @return the {@link UserInfoEndpointConfig}
*/
public UserInfoEndpointConfig userInfoEndpoint() {
return this.userInfoEndpointConfig;
}
/**
* Configures the Authorization Server's UserInfo Endpoint.
*
* @param userInfoEndpointCustomizer the {@link Customizer} to provide more options for
* the {@link UserInfoEndpointConfig}
* @return the {@link OAuth2LoginConfigurer} for further customizations
*/
public OAuth2LoginConfigurer<B> userInfoEndpoint(Customizer<UserInfoEndpointConfig> userInfoEndpointCustomizer) {
userInfoEndpointCustomizer.customize(this.userInfoEndpointConfig);
return this;
}
/**
* Configuration options for the Authorization Server's UserInfo Endpoint.
*/
public class UserInfoEndpointConfig {
private OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;
private Map<String, Class<? extends OAuth2User>> customUserTypes = new HashMap<>();
private UserInfoEndpointConfig() {
}
/**
* Sets the OAuth 2.0 service used for obtaining the user attributes of the End-User from the UserInfo Endpoint.
*
* @param userService the OAuth 2.0 service used for obtaining the user attributes of the End-User from the UserInfo Endpoint
* @return the {@link UserInfoEndpointConfig} for further configuration
*/
public UserInfoEndpointConfig userService(OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
Assert.notNull(userService, "userService cannot be null");
this.userService = userService;
return this;
}
/**
* Sets the OpenID Connect 1.0 service used for obtaining the user attributes of the End-User from the UserInfo Endpoint.
*
* @param oidcUserService the OpenID Connect 1.0 service used for obtaining the user attributes of the End-User from the UserInfo Endpoint
* @return the {@link UserInfoEndpointConfig} for further configuration
*/
public UserInfoEndpointConfig oidcUserService(OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService) {
Assert.notNull(oidcUserService, "oidcUserService cannot be null");
this.oidcUserService = oidcUserService;
return this;
}
/**
* Sets a custom {@link OAuth2User} type and associates it to the provided
* client {@link ClientRegistration#getRegistrationId() registration identifier}.
*
* @param customUserType a custom {@link OAuth2User} type
* @param clientRegistrationId the client registration identifier
* @return the {@link UserInfoEndpointConfig} for further configuration
*/
public UserInfoEndpointConfig customUserType(Class<? extends OAuth2User> customUserType, String clientRegistrationId) {
Assert.notNull(customUserType, "customUserType cannot be null");
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
this.customUserTypes.put(clientRegistrationId, customUserType);
return this;
}
/**
* Sets the {@link GrantedAuthoritiesMapper} used for mapping {@link OAuth2User#getAuthorities()}.
*
* @param userAuthoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the user's authorities
* @return the {@link UserInfoEndpointConfig} for further configuration
*/
public UserInfoEndpointConfig userAuthoritiesMapper(GrantedAuthoritiesMapper userAuthoritiesMapper) {
Assert.notNull(userAuthoritiesMapper, "userAuthoritiesMapper cannot be null");
OAuth2LoginConfigurer.this.getBuilder().setSharedObject(GrantedAuthoritiesMapper.class, userAuthoritiesMapper);
return this;
}
/**
* Returns the {@link OAuth2LoginConfigurer} for further configuration.
*
* @return the {@link OAuth2LoginConfigurer}
*/
public OAuth2LoginConfigurer<B> and() {
return OAuth2LoginConfigurer.this;
}
}
@Override
public void init(B http) throws Exception {
OAuth2LoginAuthenticationFilter authenticationFilter =
new OAuth2LoginAuthenticationFilter(
OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()),
OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(this.getBuilder()),
this.loginProcessingUrl);
this.setAuthenticationFilter(authenticationFilter);
super.loginProcessingUrl(this.loginProcessingUrl);
if (this.loginPage != null) {
// Set custom login page
super.loginPage(this.loginPage);
super.init(http);
} else {
Map<String, String> loginUrlToClientName = this.getLoginLinks();
if (loginUrlToClientName.size() == 1) {
// Setup auto-redirect to provider login page
// when only 1 client is configured
this.updateAuthenticationDefaults();
this.updateAccessDefaults(http);
String providerLoginPage = loginUrlToClientName.keySet().iterator().next();
this.registerAuthenticationEntryPoint(http, this.getLoginEntryPoint(http, providerLoginPage));
} else {
super.init(http);
}
}
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient =
this.tokenEndpointConfig.accessTokenResponseClient;
if (accessTokenResponseClient == null) {
accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
}
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = getOAuth2UserService();
OAuth2LoginAuthenticationProvider oauth2LoginAuthenticationProvider =
new OAuth2LoginAuthenticationProvider(accessTokenResponseClient, oauth2UserService);
GrantedAuthoritiesMapper userAuthoritiesMapper = this.getGrantedAuthoritiesMapper();
if (userAuthoritiesMapper != null) {
oauth2LoginAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper);
}
http.authenticationProvider(this.postProcess(oauth2LoginAuthenticationProvider));
boolean oidcAuthenticationProviderEnabled = ClassUtils.isPresent(
"org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader());
if (oidcAuthenticationProviderEnabled) {
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService = getOidcUserService();
OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider =
new OidcAuthorizationCodeAuthenticationProvider(accessTokenResponseClient, oidcUserService);
JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = this.getJwtDecoderFactoryBean();
if (jwtDecoderFactory != null) {
oidcAuthorizationCodeAuthenticationProvider.setJwtDecoderFactory(jwtDecoderFactory);
}
if (userAuthoritiesMapper != null) {
oidcAuthorizationCodeAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper);
}
http.authenticationProvider(this.postProcess(oidcAuthorizationCodeAuthenticationProvider));
} else {
http.authenticationProvider(new OidcAuthenticationRequestChecker());
}
this.initDefaultLoginFilter(http);
}
@Override
public void configure(B http) throws Exception {
OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter;
if (this.authorizationEndpointConfig.authorizationRequestResolver != null) {
authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
this.authorizationEndpointConfig.authorizationRequestResolver);
} else {
String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri;
if (authorizationRequestBaseUri == null) {
authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
}
authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), authorizationRequestBaseUri);
}
if (this.authorizationEndpointConfig.authorizationRequestRepository != null) {
authorizationRequestFilter.setAuthorizationRequestRepository(
this.authorizationEndpointConfig.authorizationRequestRepository);
}
RequestCache requestCache = http.getSharedObject(RequestCache.class);
if (requestCache != null) {
authorizationRequestFilter.setRequestCache(requestCache);
}
http.addFilter(this.postProcess(authorizationRequestFilter));
OAuth2LoginAuthenticationFilter authenticationFilter = this.getAuthenticationFilter();
if (this.redirectionEndpointConfig.authorizationResponseBaseUri != null) {
authenticationFilter.setFilterProcessesUrl(this.redirectionEndpointConfig.authorizationResponseBaseUri);
}
if (this.authorizationEndpointConfig.authorizationRequestRepository != null) {
authenticationFilter.setAuthorizationRequestRepository(
this.authorizationEndpointConfig.authorizationRequestRepository);
}
super.configure(http);
}
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl);
}
@SuppressWarnings("unchecked")
private JwtDecoderFactory<ClientRegistration> getJwtDecoderFactoryBean() {
ResolvableType type = ResolvableType.forClassWithGenerics(JwtDecoderFactory.class, ClientRegistration.class);
String[] names = this.getBuilder().getSharedObject(ApplicationContext.class).getBeanNamesForType(type);
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
}
if (names.length == 1) {
return (JwtDecoderFactory<ClientRegistration>) this.getBuilder().getSharedObject(ApplicationContext.class).getBean(names[0]);
}
return null;
}
private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() {
GrantedAuthoritiesMapper grantedAuthoritiesMapper =
this.getBuilder().getSharedObject(GrantedAuthoritiesMapper.class);
if (grantedAuthoritiesMapper == null) {
grantedAuthoritiesMapper = this.getGrantedAuthoritiesMapperBean();
if (grantedAuthoritiesMapper != null) {
this.getBuilder().setSharedObject(GrantedAuthoritiesMapper.class, grantedAuthoritiesMapper);
}
}
return grantedAuthoritiesMapper;
}
private GrantedAuthoritiesMapper getGrantedAuthoritiesMapperBean() {
Map<String, GrantedAuthoritiesMapper> grantedAuthoritiesMapperMap =
BeanFactoryUtils.beansOfTypeIncludingAncestors(
this.getBuilder().getSharedObject(ApplicationContext.class),
GrantedAuthoritiesMapper.class);
return (!grantedAuthoritiesMapperMap.isEmpty() ? grantedAuthoritiesMapperMap.values().iterator().next() : null);
}
private OAuth2UserService<OidcUserRequest, OidcUser> getOidcUserService() {
if (this.userInfoEndpointConfig.oidcUserService != null) {
return this.userInfoEndpointConfig.oidcUserService;
}
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2UserService.class, OidcUserRequest.class, OidcUser.class);
OAuth2UserService<OidcUserRequest, OidcUser> bean = getBeanOrNull(type);
if (bean == null) {
return new OidcUserService();
}
return bean;
}
private OAuth2UserService<OAuth2UserRequest, OAuth2User> getOAuth2UserService() {
if (this.userInfoEndpointConfig.userService != null) {
return this.userInfoEndpointConfig.userService;
}
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2UserService.class, OAuth2UserRequest.class, OAuth2User.class);
OAuth2UserService<OAuth2UserRequest, OAuth2User> bean = getBeanOrNull(type);
if (bean == null) {
if (!this.userInfoEndpointConfig.customUserTypes.isEmpty()) {
List<OAuth2UserService<OAuth2UserRequest, OAuth2User>> userServices = new ArrayList<>();
userServices.add(new CustomUserTypesOAuth2UserService(this.userInfoEndpointConfig.customUserTypes));
userServices.add(new DefaultOAuth2UserService());
return new DelegatingOAuth2UserService<>(userServices);
} else {
return new DefaultOAuth2UserService();
}
}
return bean;
}
private <T> T getBeanOrNull(ResolvableType type) {
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
if (context == null) {
return null;
}
String[] names = context.getBeanNamesForType(type);
if (names.length == 1) {
return (T) context.getBean(names[0]);
}
return null;
}
private void initDefaultLoginFilter(B http) {
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageGeneratingFilter.class);
if (loginPageGeneratingFilter == null || this.isCustomLoginPage()) {
return;
}
loginPageGeneratingFilter.setOauth2LoginEnabled(true);
loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(this.getLoginLinks());
loginPageGeneratingFilter.setLoginPageUrl(this.getLoginPage());
loginPageGeneratingFilter.setFailureUrl(this.getFailureUrl());
}
@SuppressWarnings("unchecked")
private Map<String, String> getLoginLinks() {
Iterable<ClientRegistration> clientRegistrations = null;
ClientRegistrationRepository clientRegistrationRepository =
OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder());
ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class);
if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
}
if (clientRegistrations == null) {
return Collections.emptyMap();
}
String authorizationRequestBaseUri = this.authorizationEndpointConfig.authorizationRequestBaseUri != null ?
this.authorizationEndpointConfig.authorizationRequestBaseUri :
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
Map<String, String> loginUrlToClientName = new HashMap<>();
clientRegistrations.forEach(registration -> loginUrlToClientName.put(
authorizationRequestBaseUri + "/" + registration.getRegistrationId(),
registration.getClientName()));
return loginUrlToClientName;
}
private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLoginPage) {
RequestMatcher loginPageMatcher = new AntPathRequestMatcher(this.getLoginPage());
RequestMatcher faviconMatcher = new AntPathRequestMatcher("/favicon.ico");
RequestMatcher defaultEntryPointMatcher = this.getAuthenticationEntryPointMatcher(http);
RequestMatcher defaultLoginPageMatcher = new AndRequestMatcher(
new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher)),
new LoginUrlAuthenticationEntryPoint(providerLoginPage));
DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint());
return loginEntryPoint;
}
private static class OidcAuthenticationRequestChecker implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
(OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains(OidcScopes.OPENID)) {
OAuth2Error oauth2Error = new OAuth2Error(
"oidc_provider_not_configured",
"An OpenID Connect Authentication Provider has not been configured. " +
"Check to ensure you include the dependency 'spring-security-oauth2-jose'.",
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
}
}
}
1.编写 oauth2Login() 方法后 ,HttpSecurity 底层 会创建 OAuth2LoginConfigurer 配置类,并且将他应用。
2. OAuth2LoginConfigurer 这个类中它的继承Filter 类型就是 OAuth2LoginAuthenticationFilter ,用于做OAuth2 认证的Filter 。
3.通过clientRegistrationRepository 获取到注册的第三方Provider 客户端信息, 默认是基于内存的
4.通过 authorizedClientRepository 生成授权成功之后的存储授权信息的库
5.在这里里还会进行添加一些其他所需要的参数信息,进行装配。最终认证的方法在 OAuth2LoginAuthenticationFilter类中。
2.OAuth2LoginAuthenticationFilter 源码分析
OAuth 2.0登录AbstractAuthenticationProcessingFilter的实现。 此身份验证筛选器处理授权代码授予流的OAuth 2.0授权响应,并将OAuth2LoginAuthenticationToken委托给AuthentizationManager以登录最终用户。 OAuth 2.0授权响应的处理方式如下: 假设最终用户(资源所有者)已授予客户端访问权限,授权服务器将把代码和状态参数附加到redirect_uri(在授权请求中提供),并将最终用户的用户代理重定向回该筛选器(客户端)。 然后,此筛选器将使用收到的代码创建OAuth2LoginAuthenticationToken,并将其委托给AuthentizationManager进行身份验证。 成功身份验证后,将创建OAuth2AuthenticationToken(代表最终用户主体),并使用OAuth2AauthorizedClientRepository与授权客户端关联。
以上是 OAuth2LoginAuthenticationFilter 的介绍 ,它是实现 OAuth2 的Filter ,他会将验证成功的结果 OAuth2LoginAuthenticationToken 交给 AuthentizationManager 进行处理 。
1.默认定义好了授权成功后回调的URI路径
2.核心认证方法 attemptAuthentication 在进行认证授权和授权码验证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AuthorizationRequest authorizationRequest =
this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
if (authorizationRequest == null) {
OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
"Client Registration not found with Id: " + registrationId, null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.build()
.toUriString();
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(
clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(authenticationDetails);
OAuth2LoginAuthenticationToken authenticationResult =
(OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);
OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(
authenticationResult.getPrincipal(),
authenticationResult.getAuthorities(),
authenticationResult.getClientRegistration().getRegistrationId());
oauth2Authentication.setDetails(authenticationDetails);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(),
oauth2Authentication.getName(),
authenticationResult.getAccessToken(),
authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
return oauth2Authentication;
}
3.首先获取到请求参数的 code 和state 值
4.验证请求参数是否有效 验证参数中是否 code 和state 并验证是否有值 。
5.移除之前授权信息,并将授权信息返回
6.判断授权客户端信息是否null
7.获取当前注册的授权服务器类型
8.根据注册类型从缓存 InMemoryClientRegistrationRepository 中获取当前注册过的授权服务器信息
9.拼装重定向URI
10.将uri和参数进行转成,获取授权码信息
11.创建 OAuth2LoginAuthenticationToken
12.将封装好的 OAuth2LoginAuthenticationToken 交给 AuthenticationManager 进行认证。认证是有OAuth2LoginAuthenticationProvider 进行认证 调用 authenticate方法,在 authenticate ;里会调用 OAuth2AuthorizationCodeAuthenticationProvider的 authenticate 进行验证码认证。
OAuth2LoginAuthenticationProvider 的 authenticate
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken loginAuthenticationToken =
(OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (loginAuthenticationToken.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains("openid")) {
// This is an OpenID Connect Authentication Request so return null
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead
return null;
}
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try {
authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange(),
oauth2User,
mappedAuthorities,
accessToken,
authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult;
}
OAuth2AuthorizationCodeAuthenticationProvider 的 authenticate
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
(OAuth2AuthorizationCodeAuthenticationToken) authentication;
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationResponse();
if (authorizationResponse.statusError()) {
throw new OAuth2AuthorizationException(authorizationResponse.getError());
}
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationRequest();
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
throw new OAuth2AuthorizationException(oauth2Error);
}
OAuth2AccessTokenResponse accessTokenResponse =
this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
OAuth2AuthorizationCodeAuthenticationToken authenticationResult =
new OAuth2AuthorizationCodeAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken(),
accessTokenResponse.getAdditionalParameters());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
}
13.最终他将获取的数据进行存储,并Authentication 进行封装成进行返回 ,不管什么类型授权方式最后转化为的对象都是相同的
3.原理总结
通过原理分析得知,当使用OAuth2 进行认证时,如果是授权码模式,则是 授权携带客户端信息向第三方应用发送请求跳转授权页面,如果用户允许授权,则会获取授权码,获取授权码后,跳转到重定向页面,此时浏览器会再次发送请求获取令牌。
由此得出,自定义的第三方应用是在获取的授权码后,进行验证授权码的合法性,在向第三方服务器发送请求获取令牌的方式。
三、Spring Security OAuth2
Spring Security 对 OAuth2 提供了很好的支持,这使得我们在 Spring Security 中使用 OAuth2 非常地方便,然后由于历史原因,Spring Security 对 OAuth2 的支持比较混乱,这里简单梳理一下。
大概十年前,Spring 引入一个社区驱动的开源项目 Spring Security OAuth ,并将其纳入 Spring 项目组合中。到今天为止,这个项目已经发展为一个成熟的项目,可以支持大部分的OAuth 规范,包括资源服务器,客户端和授权服务器等 。
然而早期的项目存在一些问题,例如:
-
- OAuth 是在早前完成的,开发者无法预料未来的变化以及这些代码到底要被怎么使用。这导致很多 Spring 项目提供了自己的 OAuth 支持,也就带来了 OAuth 支持的碎片化。
- 最早的 OAuth 项目同时支持 OAuth1.0 和 OAuth 2.0,而现在 OAuth1.0 早已经不再使用,可以放弃了。
- 现在我们有更多的库可以选择,可以在这些库的基础上去开发,以便更好地支持JWT 等新技术。
基于以上这些原因,官方决定重写 Spring Security OAuth ,以便更好地协调 Spring 和 Auth ,并简化代码库,使 Spring 的 OAuth 支持更加灵活。然后,在重写的过程中,发生了不少波折。
2018年1月30日,Spring 官方发一个通知,表示要逐渐停止现有的 OAuth2支持 ,然后在 Spring Security 5 中构建下一代 OAuth2.0 支持。这么做的原因是因为当时 OAuth2 的落地方案比较混乱,在 Spring Security OAuth 、Spring Colud Security 、Spring Boot 1.5.x 以及当时最新的Spring Security 5.x 中都提供了对 OAuth2 的实现。以至于当开发者需要使用 OAuth2 时,不得不问,到底那个依赖合适呢?
所以 Spring 官方决定有必要将 OAuth2.0 的支持统一到一个项目中,以便为用户提供明确的选择,并避免任何潜在的混乱,同时 OAuth2.0 的开发文档也要重新编写,以方便开发人员学习。所有的决定在 Spring Security 5 中开始,构建下一代 OAuth2.0的支持、从哪个时候起,Spring Security OAuth 项目就正式处于维护模式。官方将提供至少一年的错识/安全修复程序,并且会考虑添加次要功能,但不会添加主要功能。同时将 Spring Security OAuth 中的所有功能重构到 Spring Security 5.x 中 。
到了2019年11月14日,Spring 官方又发布一个通知,这次的通知首先表示 Spring Security OAuth 在迁往 Spring Security 5.x 的过程非常顺利,大多分迁工程工作已经完成了,剩下的将在5.3 版本中完成迁移,在迁移的过程中添加许多新功能。包括对 OpenID Connection1.0 支持 。同时还宣布将不在支持授权服务器,不支持的原因有两个:
- 在2019年,已经有大量的商业和开源授权服务器可用。
- 授权服务器是使用一个库来构建产品,而 Spring Security 作为框架,并不适合做这件事情 。
一石激起千层浪,许多开发者表示对此难以接受。这件事在 Spring 社区引起了激烈的讨论,好在 Spring 官方愿意倾听来自社区的声音。
到了2020年4月15日,Spring 官方宣布启动 Spring Authorization server 项目 。这是一个由 Spring Security 团队领导的社区驱动的项目,致力于向 Spring 社区提供 Authorization Server 支持,也就是说,Spring 又重新支持授权服务器了。
2020年8月21日,Spring uthorization Server 0.0.1 正式发布!
这就是 OAuth2 在 Spring 家族中的发展历程了。在后面的学习中,客户端和资源服务器都将采用最新的方式来构建,授权服务器依然采用旧的方式来构建。因为目前的 Spring Authentication Server 0.0.1 功能较少且 BUG 较多 。
一般来说,当我们在项目中使用 OAuth2 时,都是开发客户端,授权服务器和资源服务器由外部提供。例如我们想在自己搭建网站上集成 GitHub 第三方登录,只需要开发自己客户端即可,认证服务器和授权服务器由 GitHub 提供的 。
3.1 授权、资源服务器
前面的 GitHub 授权登录主要向大家展示了 OAuth2 中客户端的工作模式。对于大部分的开发者而言,日常接触到的 OAuth2 都是客户端,例如接入 QQ登录 、接入微信登录等。不过也有少量场景,可能需要开发者提供授权服务器与资源服务器,接下来我们就通过一个完整的案例演示如何搭建授权服务器与资源服务器。
搭建授权服务器,我们可以选择一些线程的开源项目,直接运行即可,例如 :
-
- Keycloak :RedFat 公司提供的开源工具,提供了很多实用功能,例如单点登录、支持OpenID、可视化后台管理等。
- Apache Oltu:Apache 上的开源项目,最近几年没怎么维护了。
接下来我们将搭建一个包含授权服务器、资源服务器以及客户端在内的 OAuth2 案例。
-
- 授权服务器:采用较早的 spring-cloud-starter-oauth2 来搭建授权服务器。
- 资源服务器:采用最新的 Spring Security 5.x 搭建资源服务器。
- 客户端:采用最新的 Spring Security 5.x 搭建客户端。
注意:Spring Boot 的版本不能过高 ,因为 Spring-cloud-starter-oatuh2 的版本不能过高 。
1.基于内存授权服务器搭建
做授权服务器的话,
1.需要提供一个授权登录的功能
2.注册功能,其他服务器向授权服务器注册的功
1.1 基于内存客户端和令牌存储
创建 SpringBoot 应用 ,并引入依赖
<properties>
<spring-boot.version>2.2.5.RELEASE</spring-boot.version>
</properties>
<dependencies>
<!--spring web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring cloud oauth 授权服务器 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<!--spring security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
编写配置类 ,添加 security 配置类以及 oAuth 配置类
package com.bjpowenrode.security.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.builders.WebSecurity;
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;
/**
* @author 千城丶Y
* @className : WebSecuriyConfig
* @PACKAGE_NAME : com.bjpowenrode.security.config
* @date : 2022/9/21 18:19
* @Description TODO 基础的webSecurity 配置
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder (){
return new BCryptPasswordEncoder();
}
/**
* 基于内存的身份验证
*/
@Override
@Bean
public UserDetailsService userDetailsService (){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password(passwordEncoder().encode("123")).roles("admin").build());
return inMemoryUserDetailsManager ;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Bean
@Override // 将内容 authorizationManager 暴露
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
/**
* 基础的web 安全配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable() ;
}
}
package com.bjpowenrode.oauth.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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;
/**
* @author 千城丶Y
* @className : AuthorizationServerConfig
* @PACKAGE_NAME : com.bjpowenrode.oauth.config
* @date : 2022/9/21 18:28
* @Description TODO 授权服务器配置
*/
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder ;
private final UserDetailsService userDetailsService ;
/**
* 注入进来 认证管理器
*/
private final AuthenticationManager authenticationManager ;
public AuthorizationServerConfig(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService, AuthenticationManager authenticationManager) {
this.passwordEncoder = passwordEncoder;
this.userDetailsService = userDetailsService;
this.authenticationManager = authenticationManager;
}
/**
* 对授权客户端的配置
*
* 用来配置授权服务器可以为哪些客户端授权 // id secret redirectURI 使用那种授权模式
*
* {
* "access_token": "becac041-faf4-4b2d-8c58-8b10a38f290f",
* "token_type": "bearer",
* "refresh_token": "4752515d-f528-4932-a255-d20cdb5e95c4",
* "expires_in": 43199,
* "scope": "read:user"
* }
* */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory() // 表示基于内存
.withClient("clientId" ) // 设置客户端编号
.secret(passwordEncoder.encode("secret")) // 设置客户端密钥 ,这里官方规定secret必现加密 可以使用 passwordEncode
.redirectUris("http://www.baidu.com") // 重定向URI
// authorization_code 授权码 refresh_token 刷新令牌 implicit 简化模式 password 密码模式 client_credentials 客户端模式
// 暂不支持简化模式, 因为需要解析一段脚本来获取令牌
.authorizedGrantTypes("authorization_code","refresh_token","implicit","password","client_credentials") // 表示使用那种授权模式 可以设置支持多种,授权码模式
.scopes("read:user");// 令牌允许获取资源权限
}
// 授权码这种模式:
// 1.请求用户是否授权 /oauth/authorize
// 完整路径 : http://localhost:8080/oauth/authorize?client_id=clientId&response_type=code&redirect_uri=http://www.baidu.com
// 2.授权之后根据获取的授权码获取令牌 /oauth/token 参数 : client_id secret redirect_uri code
// 完整路径 :curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=2qewq&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8080/oauth/token"
// 3.支持令牌刷新 /oauth/token 参数 id secret 授权类型 :refresh_token 刷新的令牌:refresh_token
// 完整路径 :curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=refresh_token&refresh_token=2qewq&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8080/oauth/token"
/**
* 开启刷新令牌功能 实现刷新令牌必现指定userDetailsService ,有了它,刷新令牌时就不需要在获取授权码了 刷新令牌和 获取令牌的路径是一样的 参数多了
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.userDetailsService(userDetailsService); // 开启刷新令牌必现指定
endpoints.authenticationManager(authenticationManager); // 密码模式需要注入 authenticationManager
}
}
启动服务,登录之后进行授权获取
1.先登录
2.访问授权路径 获取授权码
3.允许授权后会从客户端授权码,并跳转到重定向URI
4.根据授权码,申请令牌
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d ' grant_type=authorization_code&code=IWvwq&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8080/oauth/token"
注意授权码是一次性的
PostMian请求 方法:
使用介绍:Spring Cloud OAuth2中访问/oauth/token报401 Unauthorized问题的解决(POSTMAN) - mangoubiubiu - 博客园
路径:http://client:secret@localhost:8080/oauth/token
参数:
- grant_type:授权类型,填写authorization_code,表示授权码模式
- code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
- client_id:客户端标识
- redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
- scope:授权范围。
还需要在Authorzation 写上你配的client_id 和 client-secret
5.刷新令牌
首先需要开启了刷新令牌功能
刷新令牌的路径和获取令牌的路径是一样的
支持令牌刷新 /oauth/token 参数 id secret 授权类型 :refresh_token 刷新的令牌:refresh_token
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=refresh_token&refresh_token=2qewq&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8080/oauth/token"
Postmain 使用
6.简化模式
7.密码模式
8.密码模式刷新令牌
9.客户端模式获取令牌
2 基于数据库客户端和令牌存储
将客户端信息和令牌信息转为数据库存储
在上面的案例中,TokenStore 的默认实现 为 InMemoryTokenStore 即内存存储,对于 Client 信息,ClientDetailsService 接口负责从存储仓库中读取数据,在上面的案例中默认使用的也是 InMemoryClientDetailsService 实现类。
如果要想使用数据库存储,只要提供这些接口的实现类即可,而框架已经为我们写好 JdbcTokenStore 和 JdbcDetailsService
建表:
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
# 注意:并用BLOB 替换语句中的 LONGVARBINARY 类型
-- used in tests that use HSQL
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
create table oauth_client_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
create table oauth_access_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication BLOB,
refresh_token VARCHAR(256)
);
create table oauth_refresh_token (
token_id VARCHAR(256),
token BLOB,
authentication BLOB
);
create table oauth_code (
code VARCHAR(256), authentication LONGVARBINARY
);
create table oauth_approvals (
userId VARCHAR(256),
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);
-- customized oauth_client_details table
create table ClientDetails (
appId VARCHAR(256) PRIMARY KEY,
resourceIds VARCHAR(256),
appSecret VARCHAR(256),
scope VARCHAR(256),
grantTypes VARCHAR(256),
redirectUrl VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(256)
);
-- 写入客户端信息
INSERT INTO `oauth_client_details` VALUES ('clientId', '', '$2a$10$rvtuV.bPeFA5zfowgr9OJuQSj07AFJfHl5Y7QND0thykzlB7S8hW6', 'read', 'authorization_code,refresh_token', 'http://www.baidu.com', NULL, NULL, NULL, NULL, NULL);
主要用到表:
oauth_client_details : 客户端信息 oauth_client_details 的 auhoapprove 是否自动授权
oauth_client_token :存放令牌信息
1.引入依赖
<!--引入数据库依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
2.编写配置文件
# 添加数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/oauth?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=admin
3.编写数据库信息实现
package com.bjpowenrode.oauth.config;
import jdk.nashorn.internal.parser.Token;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;
/**
* @author 千城丶Y
* @className : JdbcAuthorizationServerConfig
* @PACKAGE_NAME : com.bjpowenrode.oauth.config
* @date : 2022/9/21 23:17
* @Description TODO 基于数据库的客户端信息和令牌的 授权服务器配置
*/
@Configuration
@EnableAuthorizationServer
public class JdbcAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager ;
private final PasswordEncoder passwordEncoder ;
private final DataSource dataSource ;
public JdbcAuthorizationServerConfig(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, DataSource dataSource) {
this.authenticationManager = authenticationManager;
this.passwordEncoder = passwordEncoder;
this.dataSource = dataSource;
}
/**
* 声明基于数据库实现的存储方式
*/
@Bean
public TokenStore tokenStore (){
return new JdbcTokenStore(dataSource);
}
/**
* 声明 基于数据库实现的ClientDetails 用于存储和查询客户端信息和令牌
*/
@Bean
public ClientDetailsService detailsService (){
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder); // 用于加密和解密 密钥
return jdbcClientDetailsService;
}
/**
* 配置使用数据库实现
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager) ; // 认证管理器
endpoints.tokenStore(tokenStore()); // 配置令牌存储为数据库存储
// 配置 TokenService 参数
DefaultTokenServices tokenServices = new DefaultTokenServices(); // 修改默认生成令牌服务
tokenServices.setTokenStore(endpoints.getTokenStore()); // 基于数据库令牌生成
tokenServices.setSupportRefreshToken(true); // 是否支持刷新令牌
tokenServices.setReuseRefreshToken(true); // 是否支持重复使用刷新令牌 (直到过期)
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());// 设置客户端信息
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer()); // 用来控制令牌存储增强策略
// 访问令牌的默认有效 (以秒为单位) 。过期的令牌为零或负数。
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)) ;// 30天
// 刷新令牌的有效性(以秒为单位)。如果小于或等于零,则令牌将不会过期
tokenServices.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(3));// 3天
endpoints.tokenServices(tokenServices); // 使用配置令牌服务
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(detailsService()); // 使用JDBC存储
}
}
4.启动测试,发现数据库中已经存储相关令牌。
5.总结
切换方式主要是通过 TokenStore 它是一个接口有多种实现方式
3.资源服务器搭建
资源服务器不仅需要引入资源服务器依赖还需要引入授权服务器依赖,因为资源服务器需要认证请求所携带的令牌,令牌验证可以通过 http请求或者两者基于同一个数据库查询方式,或者使用JWT的方式,两者使用相同的 secret 。
资源服务器接受到请求后首先需要进行令牌校验,当客户端拿着颁发的额令牌向资源服务器索要资源时,资源服务器就需要验证令牌,验证令牌可以通过http请求向授权访问器校验,但这样不是百分百能获取的,还有一种方式是通过授权服务器和资源服务器共享同一个数据库,数据库中存放这令牌信息资源服务器直接根据数据库进行校验就可以了,可以使用redis解决分布式的方式,也就是黑板模式,引入授权服的依赖就是为了访问数据库进行令牌机制.
1.引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bjpowernode</groupId>
<artifactId>ch022-spring-security-oauth2-resource-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ch022-spring-security-oauth2-resource-server</name>
<description>ch022-spring-security-oauth2-resource-server</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>
<dependencies>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--授权服务器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--资源服务器依赖-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<!--spring security 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.bjpowernode.Ch022SpringSecurityOauth2ResourceServerApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2.创建资源
package com.bjpowernode.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 千城丶Y
* @className : IndexController
* @PACKAGE_NAME : com.bjpowernode.controller
* @date : 2022/9/21 23:59
* @Description TODO
*/
@RestController
public class IndexController {
@GetMapping("/hello")
public String hello(){
return "hello!";
}
}
3.编写资源服务器配置类
package com.bjpowernode.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
/**
* @author 千城丶Y
* @className : ResourceServer
* @PACKAGE_NAME : com.bjpowernode.config
* @date : 2022/9/22 0:00
* @Description TODO 资源配置服务器
*/
@Configuration
@EnableResourceServer // 启动资源服务器
public class ResourceServer extends ResourceServerConfigurerAdapter {
private final DataSource dataSource;
public ResourceServer(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 基于数据库的认证
* @return
*/
@Bean
public TokenStore tokenStore (){
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore());
}
}
4.编写配置文件
# 应用名称
spring.application.name=ch022-spring-security-oauth2-resource-server
# 应用服务 WEB 访问端口
server.port=8081
# 添加数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/oauth?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=admin
5.启动测试,生成令牌之后带有令牌访问:
curl -H "Authorization:Bearer deff--213-21312-213jk213-1231j2k" http://localhost:8081/hello
测试时,需要在 请求头中携带 Authorization:Bearer deff--213-21312-213jk213-1231j2k
四、JWT的应用
将授权服务器和资源服务器的类型令牌类型都替换为JWT的方式,这种方式效率最高。
资源服务器验证授权令牌常见的两种方式都有弊端:
1.通过http请求向授权服务器方式请求认证,可能会受到网络的问题,
2.通过将令牌存放到数据库或redis中,资源服务器和授权服务器使用同一个,这样如果分布式分库就会有问题’
3.所以最好是使用jwt ,使用jwt的好处,就是jwt有自己一套规范,授权办法的jwt令牌,资源服务器只需要使用相对的jwt规范进行验证就行
jwt生成是三部分,最核心的就是密钥,它是根据密钥进行生成和解析的,所有授权服务器和认证服务器的密钥要是相同的
4.1 授权服务器颁发 JWT 令牌
1.配置颁发 JWT 令牌
JWT header.payload.sin + 秘钥OPSTATA jwt+Resource ServerOPSATA
package com.bjpowenrode.oauth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.jwt.Jwt;
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.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;
/**
* @author 千城丶Y
* @className : JdbcAuthorizationServerConfig
* @PACKAGE_NAME : com.bjpowenrode.oauth.config
* @date : 2022/9/21 23:17
* @Description TODO 基于JWT令牌的 授权服务器配置
*/
@Configuration
@EnableAuthorizationServer
public class JWTAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager ;
private final PasswordEncoder passwordEncoder ;
private final DataSource dataSource ;
public JWTAuthorizationServerConfig(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, DataSource dataSource) {
this.authenticationManager = authenticationManager;
this.passwordEncoder = passwordEncoder;
this.dataSource = dataSource;
}
/**
* 声明基于jwt实现令牌的存储方式
*/
@Bean
public TokenStore tokenStore (){
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 声明 基于数据库实现的ClientDetails 用于存储和查询客户端信息和令牌
*/
@Bean
public ClientDetailsService detailsService (){
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder); // 用于加密和解密 密钥
return jdbcClientDetailsService;
}
/**
* 配置使用基于 jwt 方式颁发令牌,同时配置 jwt转换器
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(jwtAccessTokenConverter())
.authenticationManager(authenticationManager);
}
// 使用同一个密钥来编码 JWT OAuth2 令牌
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter (){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("123"); // 可以采用属性注入方式,生产中建议加密 ,密钥的key 很重要
return jwtAccessTokenConverter;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(detailsService()); // 使用JDBC存储
}
}
2.启动服务,根据授权码获取令牌
4.2.使用 JWT 令牌资源服务器
1.配置资源服务器解析 jwt
/**
* @author 千城丶Y
* @className : ResourceServer
* @PACKAGE_NAME : com.bjpowernode.config
* @date : 2022/9/22 0:00
* @Description TODO 基于JWT令牌认证 ,资源配置服务器
*/
@Configuration
@EnableResourceServer // 启动资源服务器
public class JWTResourceServer extends ResourceServerConfigurerAdapter {
private final DataSource dataSource;
public JWTResourceServer(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 基于JWT的认证
* @return
*/
@Bean
public TokenStore tokenStore (){
return new JwtTokenStore(jwtAccessTokenConverter());
}
// 使用同一个密钥来编码 JWT OAuth2 令牌
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter (){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("123"); // 可以采用属性注入方式,生产中建议加密 ,密钥的key 很重要
return jwtAccessTokenConverter;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore());
}
}
2.启动测试,通过jwt测试访问资源
curl -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjM4MjA5MTYsInVzZXJfbmFtZSI6InJvb3QiLCJhdXRob3JpdGllcyI6WyJST0xFX2FkbWluIl0sImp0aSI6IjQyYmNiMDIzLTJmZTAtNDk1My05Y2Q3LWExNmVlNGU1N2RkMyIsImNsaWVudF9pZCI6ImNsaWVudElkIiwic2NvcGUiOlsicmVhZCJdfQ._wMSXarEFbtEjZmXEzv2qfPni4cpiIm4krXWLy6c_-Y" http://localhost:8081/hello
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签: