Spring全家桶-Spring Security之OAuth2.0认证
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
文章目录
前言
我们以前都是通过用户名和密码进行登录,但是有时候我们不想通过用户名和密码登录,通过第三方的账号进行登录,我们该怎么办?OAuth为我们解决了在用户不提供密码给第三方应用的情况下,让第三方应用有权获取用户数据以及基本信息。如:使用Github,QQ,Facebook等进行访问应用。
一、OAuth是什么?
OAuth
即开放授权,是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。OAuth
允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时段内访问特定的资源(。这样,OAuth
让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
OAuth
是OpenID
的一个补充,但是完全不同的服务。
二、OAuth 1.0 & OAuth 2.0
OAuth
开始于2006年11月,当时布莱恩·库克(Blaine Cook)正在开发Twitter的OpenID
实现。与此同时,社交书签网站Ma.gnolia(Gnolia)需要一个解决方案允许使用OpenID
的成员授权Dashboard访问他们的服务。这样库克、克里斯·梅西纳(Chris Messina)和来自Ma.gnolia的拉里·哈尔夫(Larry Halff)与戴维·雷科尔顿(David Recordon)会面讨论在Twitter和Ma.gnolia API上使用OpenID
进行委托授权。他们讨论得出结论,认为没有完成API访问委托的开放标准。
2007年4月,成立了OAuth讨论组,这个由实现者组成的小组撰写了一个开放协议的提议草案。来自Google的德维特·克林顿获悉OAuth项目后,表示他有兴趣支持这个工作。2007年7月,团队起草了最初的规范。随后,Eran Hammer-Lahav加入团队并协调了许多OAuth的稿件,创建了更为正式的规范。2007年10月, OAuth核心1.0最后的草案发布了。
2008年11月,在明尼阿波利斯举行的互联网工程任务组第73次会议上,举行了OAuth的BoF讨论将该协议纳入IETF做进一步的规范化工作。这个会议参加的人很多,关于正式地授权在IETF设立一个OAuth工作组这一议题得到了广泛的支持。
2010年4月,OAuth 1.0
协议发表为RFC
5849,一个非正式RFC
。
OAuth 2.0
是OAuth
协议的下一版本,但不向下兼容OAuth 1.0
。OAuth 2.0
关注客户端开发者的简易性,同时为Web应用、桌面应用、手机和智能设备提供专门的认证流程。
Facebook的新的Graph API只支持OAuth 2.0,Google在2011年3月也宣布Google API对OAuth 2.0的支持,Windows Live也支持OAuth 2.0。
二、OAuth中的相关角色
OAuth中有4个重要的角色:
Resource Owner
:资源所有者, 通常指用户Resource Server
:资源服务器, 指存放用户受保护资源的服务器, 通常需要通过AccessToken( 访问令牌)才能进行访问。(如:QQ)Client
:客户端,指需要获取用户资源的第三方应用(如:本地应用)Authorization Server
:授权服务器, 用于验证资源所有者, 并在验证成功之后向客户端发放相关访问令牌。
三、OAuth认证流程
- 客户端要求用户提供授权许可。
- 用户同意向客户端提供授权许可。
(关键步骤)
- 客户端携带用户提供的授权许可向授权服务器申请资源服务器的访问令牌。
- 授权服务器验证客户端及其携带的授权许可, 确认有效后发放访问令牌。
- 客户端使用访问令牌向资源服务器申请资源。
- 资源服务器验证访问令牌, 确认无误后向客户端提供资源。
三、OAuth授权模式
授权授予是代表资源所有者的授权(访问其受保护的资源)的凭据,客户机使用该授权来获得访问令牌。
该规范定义了四种授权类型:
- 授权码模式( Authorization Code)
- 隐式授权模式( Implicit)
- 密码授权模式( Password Credentials)
- 客户端授权模式( Client Credentials)
1. 授权码模式
授权码模式是功能最完整、流程最严密的授权模式,它将用户引导到授权服务器进行身份验证,授权服务器将发放的访问令牌传递给客户端。
2. 隐式授权模式
隐式授权模式的客户端一般是指用户浏览器。访问令牌通过重定向的方式传递到用户浏览器中,再通过浏览器的 JavaScript 代码来获取访问令牌。由于访问令牌直接暴露在浏览器端,所以隐式授权模式可能会导致访问令牌被黑客获取,仅适用于需要临时访问的场景。
授权码模式是携带一个认证码,由客户端(第三方应用后端程序)通过认证码申请访问令牌的;而隐式授权模式则直接将访问令牌作为URL的散列部分传递给浏览器。
3. 密码授权模式
客户端直接携带用户的密码向授权服务器申请令牌。这种登录操作不再像前两种授权模式一样跳转到授权服务器进行,而是由客户端提供专用页面。如果用户信任该客户端,用户便可以直接提供密码,客户端在不储存用户密码的前提下完成令牌的申请。
4. 客户端授权模式
客户端授权模式实际上并不属于OAuth的范畴, 因为它的关注点不再是用户的私有信息或数据,而是一些由资源服务器持有但并非完全公开的数据。客户端授权模式通常由客户端提前向授权服务器申请应用公钥、密钥,并通过这些关键信息向授权服务器申请访问令牌,从而得到资源服务器提供的资源。
Spring Security 集成OAuth2.0实现github登陆
github作为开发人员都知道的一个网站,管理自己的代码,该网站上也有很多的开源项目。我们先以github为例,进行快捷登陆。
1.在github上创建应用
访问:https://github.com/settings/applications
Application name:应用名称,必填项。
Homepage URL:主页URL,必填项。在本地开发时,将其设置为http://localhost:8080即可。
Application description: 应用的说明,选填项,置空即可。
Authorization callback URL:OAuth 认证的重定向地址,必填项,本地开发可设置为http://localhost:8080/login/oauth2/code/github
点击注册按钮就会生成clientId
和clientSecret
。如图:
可以复制clientId
和clientSecret
,等会再项目中会用到。
如果创建了不知道clientSecret
就重新生成一个,复制一下即可。
2. 创建项目spring-security-oauth-github
pom.xml
<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>
<!-- oauth2 client -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<!-- oauth2 jose -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
3.添加application.yml
spring:
security:
oauth2:
client:
registration: #OAuth客户端所有属性的基础前缀
github: #ClientRegistration的唯一ID
client-id: b9cb946552f1d0f8c4ee
client-secret: 57930e8555ffaa58edfae1c7bdc37dade0e65b61
4.创建IndexController
@RestController
public class IndexController {
@GetMapping("index")
public String index(Principal principal){
return "index:" + principal.getName();
}
}
Principal
:principal对象由Spring框架自动注入, 表示当前登录的用户
5.创建启动类
public class GithubOauthApplication {
public static void main(String[] args) {
SpringApplication.run(GithubOauthApplication.class,args);
}
}
6.启动项目
运行项目,访问http://localhost:8080/index,界面将重定向到如下页面
我们通过浏览器调试工具看看请求:
index:
302代表重定向。
https://github.com/login/oauth/authorize?response_type=code&client_id=b9cb946552f1d0f8c4ee&scope=read:user&state=pFT-bRdbuLOYqGuREGfYPJUbZfCXiRc1gGtuNEWzH2g%3D&redirect_uri=http://localhost:8080/login/oauth2/code/github
登陆成功后,会重定向到:
最终会到index
成功登陆后,将返回到index,我后台通过principal.getName();获取认证的名称。
7.分析请求中的相关参数
https://github.com/login/oauth/authorize?response_type=code&client_id=b9cb946552f1d0f8c4ee&scope=read:user&state=pFT-bRdbuLOYqGuREGfYPJUbZfCXiRc1gGtuNEWzH2g%3D&redirect_uri=http://localhost:8080/login/oauth2/code/github
上面的Http请求中,我们可以看到有response_type
,client_id
等参数,解释如下:
- response_type:授权类型
- client_id:客户端id
- state:客户端状态
- scope:申请的权限范围
- redirect_uri:为授权通过后的重定向URL
http://localhost:8080/login/oauth2/code/github?code=55f65de27958e06cd346&state=pFT-bRdbuLOYqGuREGfYPJUbZfCXiRc1gGtuNEWzH2g%3D
- code:申请访问令牌必备的授权码,有效期较短。客户端拿到code之
后需要向授权服务器申请访问令牌,注意:仅可使用一次, 用完作废
Spring Security实现OAuth2原理
这个是我们登陆的时候,在index接口中进行调试显示Principal
中的相关内容。
Spring Boot加载配置
我们都知道Spring Boot为我们提供了自动配置功能。我们在配置文件(application.yml)中添加OAuth2中的配置进行相关的处理。
spring:
security:
oauth2:
client:
registration: #OAuth客户端所有属性的基础前缀
github: #ClientRegistration的唯一ID
client-id: b9cb946552f1d0f8c4ee
client-secret: 57930e8555ffaa58edfae1c7bdc37dade0e65b61
有时候我们想查看详细的日志,可以开启Spring的相关包的debug,配置如下:
logging:
level:
org.springframework: debug
我们查看
这个是自动加载配置bean
在OAuth2中有处理配置文件的处理bean
OAuth2ClientAutoConfiguration.class
:
@Configuration(
proxyBeanMethods = false
)
@AutoConfigureBefore({SecurityAutoConfiguration.class}) //安全配置
@ConditionalOnClass({EnableWebSecurity.class, ClientRegistration.class}) //启用web安全,客户注册
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@Import({OAuth2ClientRegistrationRepositoryConfiguration.class, OAuth2WebSecurityConfiguration.class}) //OAuth2客户端注册,与配置
public class OAuth2ClientAutoConfiguration {
public OAuth2ClientAutoConfiguration() {
}
}
因为我们使用的servlet,因此我们通过servlet的配置进行设置。
ClientRegistration.class
:客户端注册
public static ClientRegistration.Builder withRegistrationId(String registrationId) {
return new ClientRegistration.Builder(registrationId);
}
public static ClientRegistration.Builder withClientRegistration(ClientRegistration clientRegistration) {
return new ClientRegistration.Builder(clientRegistration);
}
public static final class Builder implements Serializable {
private Builder(String registrationId) {
this.userInfoAuthenticationMethod = AuthenticationMethod.HEADER;
this.configurationMetadata = Collections.emptyMap();
this.registrationId = registrationId;
}
private Builder(ClientRegistration clientRegistration) {
this.userInfoAuthenticationMethod = AuthenticationMethod.HEADER;
this.configurationMetadata = Collections.emptyMap();
this.registrationId = clientRegistration.registrationId;
this.clientId = clientRegistration.clientId;
this.clientSecret = clientRegistration.clientSecret;
this.clientAuthenticationMethod = clientRegistration.clientAuthenticationMethod;
this.authorizationGrantType = clientRegistration.authorizationGrantType;
this.redirectUri = clientRegistration.redirectUri;
this.scopes = clientRegistration.scopes != null ? new HashSet(clientRegistration.scopes) : null;
this.authorizationUri = clientRegistration.providerDetails.authorizationUri;
this.tokenUri = clientRegistration.providerDetails.tokenUri;
this.userInfoUri = clientRegistration.providerDetails.userInfoEndpoint.uri;
this.userInfoAuthenticationMethod = clientRegistration.providerDetails.userInfoEndpoint.authenticationMethod;
this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName;
this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri;
this.issuerUri = clientRegistration.providerDetails.issuerUri;
Map<String, Object> configurationMetadata = clientRegistration.providerDetails.configurationMetadata;
if (configurationMetadata != Collections.EMPTY_MAP) {
this.configurationMetadata = new HashMap(configurationMetadata);
}
this.clientName = clientRegistration.clientName;
}
public ClientRegistration build() {
Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null");
if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType)) {
this.validateClientCredentialsGrantType();
} else if (AuthorizationGrantType.PASSWORD.equals(this.authorizationGrantType)) {
this.validatePasswordGrantType();
} else if (AuthorizationGrantType.IMPLICIT.equals(this.authorizationGrantType)) {
this.validateImplicitGrantType();
} else if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType)) {
this.validateAuthorizationCodeGrantType();
}
this.validateScopes();
return this.create();
}
private ClientRegistration create() {
ClientRegistration clientRegistration = new ClientRegistration();
clientRegistration.registrationId = this.registrationId;
clientRegistration.clientId = this.clientId;
clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : "";
clientRegistration.clientAuthenticationMethod = this.clientAuthenticationMethod != null ? this.clientAuthenticationMethod : this.deduceClientAuthenticationMethod(clientRegistration);
clientRegistration.authorizationGrantType = this.authorizationGrantType;
clientRegistration.redirectUri = this.redirectUri;
clientRegistration.scopes = this.scopes;
clientRegistration.providerDetails = this.createProviderDetails(clientRegistration);
clientRegistration.clientName = StringUtils.hasText(this.clientName) ? this.clientName : this.registrationId;
return clientRegistration;
}
private ClientAuthenticationMethod deduceClientAuthenticationMethod(ClientRegistration clientRegistration) {
return AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType) && !StringUtils.hasText(this.clientSecret) ? ClientAuthenticationMethod.NONE : ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
}
private ClientRegistration.ProviderDetails createProviderDetails(ClientRegistration clientRegistration) {
Objects.requireNonNull(clientRegistration);
ClientRegistration.ProviderDetails providerDetails = clientRegistration.new ProviderDetails();
providerDetails.authorizationUri = this.authorizationUri;
providerDetails.tokenUri = this.tokenUri;
providerDetails.userInfoEndpoint.uri = this.userInfoUri;
providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod;
providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName;
providerDetails.jwkSetUri = this.jwkSetUri;
providerDetails.issuerUri = this.issuerUri;
providerDetails.configurationMetadata = Collections.unmodifiableMap(this.configurationMetadata);
return providerDetails;
}
//验证AuthorizationGrantType
private void validateAuthorizationCodeGrantType() {
Assert.isTrue(AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType), () -> {
return "authorizationGrantType must be " + AuthorizationGrantType.AUTHORIZATION_CODE.getValue();
});
}
//验证Implicit授权类型
private void validateImplicitGrantType() {
Assert.isTrue(AuthorizationGrantType.IMPLICIT.equals(this.authorizationGrantType), () -> {
return "authorizationGrantType must be " + AuthorizationGrantType.IMPLICIT.getValue();
});
}
//验证客户端授权
private void validateClientCredentialsGrantType() {
Assert.isTrue(AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType), () -> {
return "authorizationGrantType must be " + AuthorizationGrantType.CLIENT_CREDENTIALS.getValue();
});
}
//验证密码授权
private void validatePasswordGrantType() {
Assert.isTrue(AuthorizationGrantType.PASSWORD.equals(this.authorizationGrantType), () -> {
return "authorizationGrantType must be " + AuthorizationGrantType.PASSWORD.getValue();
});
}
//验证范围
private void validateScopes() {
if (this.scopes != null) {
Iterator var1 = this.scopes.iterator();
while(var1.hasNext()) {
String scope = (String)var1.next();
Assert.isTrue(validateScope(scope), "scope \"" + scope + "\" contains invalid characters");
}
}
}
private static boolean validateScope(String scope) {
return scope == null || scope.chars().allMatch((c) -> {
return withinTheRangeOf(c, 33, 33) || withinTheRangeOf(c, 35, 91) || withinTheRangeOf(c, 93, 126);
});
}
private static boolean withinTheRangeOf(int c, int min, int max) {
return c >= min && c <= max;
}
}
public class ProviderDetails implements Serializable {
private static final long serialVersionUID = 560L;
private String authorizationUri;
private String tokenUri;
private ClientRegistration.ProviderDetails.UserInfoEndpoint userInfoEndpoint = new ClientRegistration.ProviderDetails.UserInfoEndpoint();
private String jwkSetUri;
private String issuerUri;
private Map<String, Object> configurationMetadata = Collections.emptyMap();
ProviderDetails() {
}
public class UserInfoEndpoint implements Serializable {
private static final long serialVersionUID = 560L;
private String uri;
private AuthenticationMethod authenticationMethod;
private String userNameAttributeName;
UserInfoEndpoint() {
this.authenticationMethod = AuthenticationMethod.HEADER;
}
public String getUri() {
return this.uri;
}
public AuthenticationMethod getAuthenticationMethod() {
return this.authenticationMethod;
}
public String getUserNameAttributeName() {
return this.userNameAttributeName;
}
}
}
Spring Security
通过AuthorizationGrantType
维护认证模式
public static final AuthorizationGrantType AUTHORIZATION_CODE = new AuthorizationGrantType("authorization_code");
@Deprecated
public static final AuthorizationGrantType IMPLICIT = new AuthorizationGrantType("implicit");
public static final AuthorizationGrantType REFRESH_TOKEN = new AuthorizationGrantType("refresh_token");
public static final AuthorizationGrantType CLIENT_CREDENTIALS = new AuthorizationGrantType("client_credentials");
public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");
public static final AuthorizationGrantType JWT_BEARER = new AuthorizationGrantType("urn:ietf:params:oauth:grant-type:jwt-bearer");
OAuth2客户端注册管理类:OAuth2ClientRegistrationRepositoryConfiguration.class
:
@Configuration(
proxyBeanMethods = false
)
@EnableConfigurationProperties({OAuth2ClientProperties.class})
@Conditional({ClientsConfiguredCondition.class})
class OAuth2ClientRegistrationRepositoryConfiguration {
OAuth2ClientRegistrationRepositoryConfiguration() {
}
@Bean
@ConditionalOnMissingBean({ClientRegistrationRepository.class})
InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList(OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
return new InMemoryClientRegistrationRepository(registrations);
}
}
由上面的注册方法可以看到使用OAuth2ClientPropertiesRegistrationAdapter
进行适配处理
OAuth2ClientPropertiesRegistrationAdapter
:
public final class OAuth2ClientPropertiesRegistrationAdapter {
private OAuth2ClientPropertiesRegistrationAdapter() {
}
public static Map<String, ClientRegistration> getClientRegistrations(OAuth2ClientProperties properties) {
Map<String, ClientRegistration> clientRegistrations = new HashMap();
properties.getRegistration().forEach((key, value) -> {
ClientRegistration var10000 = (ClientRegistration)clientRegistrations.put(key, getClientRegistration(key, value, properties.getProvider()));
});
return clientRegistrations;
}
//获取客户端注册
private static ClientRegistration getClientRegistration(String registrationId, Registration properties, Map<String, Provider> providers) {
Builder builder = getBuilderFromIssuerIfPossible(registrationId, properties.getProvider(), providers);
if (builder == null) {
builder = getBuilder(registrationId, properties.getProvider(), providers);
}
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
properties.getClass();
map.from(properties::getClientId).to(builder::clientId);
properties.getClass();
map.from(properties::getClientSecret).to(builder::clientSecret);
properties.getClass();
map.from(properties::getClientAuthenticationMethod).as(ClientAuthenticationMethod::new).to(builder::clientAuthenticationMethod);
properties.getClass();
map.from(properties::getAuthorizationGrantType).as(AuthorizationGrantType::new).to(builder::authorizationGrantType);
properties.getClass();
map.from(properties::getRedirectUri).to(builder::redirectUri);
properties.getClass();
map.from(properties::getScope).as(StringUtils::toStringArray).to(builder::scope);
properties.getClass();
map.from(properties::getClientName).to(builder::clientName);
return builder.build();
}
private static Builder getBuilderFromIssuerIfPossible(String registrationId, String configuredProviderId, Map<String, Provider> providers) {
String providerId = configuredProviderId != null ? configuredProviderId : registrationId;
if (providers.containsKey(providerId)) {
Provider provider = (Provider)providers.get(providerId);
String issuer = provider.getIssuerUri();
if (issuer != null) {
Builder builder = ClientRegistrations.fromIssuerLocation(issuer).registrationId(registrationId);
return getBuilder(builder, provider);
}
}
return null;
}
private static Builder getBuilder(String registrationId, String configuredProviderId, Map<String, Provider> providers) {
String providerId = configuredProviderId != null ? configuredProviderId : registrationId;
CommonOAuth2Provider provider = getCommonProvider(providerId);
if (provider == null && !providers.containsKey(providerId)) {
throw new IllegalStateException(getErrorMessage(configuredProviderId, registrationId));
} else {
Builder builder = provider != null ? provider.getBuilder(registrationId) : ClientRegistration.withRegistrationId(registrationId);
return providers.containsKey(providerId) ? getBuilder(builder, (Provider)providers.get(providerId)) : builder;
}
}
private static String getErrorMessage(String configuredProviderId, String registrationId) {
return configuredProviderId != null ? "Unknown provider ID '" + configuredProviderId + "'" : "Provider ID must be specified for client registration '" + registrationId + "'";
}
//构建Builder
private static Builder getBuilder(Builder builder, Provider provider) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
provider.getClass();
map.from(provider::getAuthorizationUri).to(builder::authorizationUri);
provider.getClass();
map.from(provider::getTokenUri).to(builder::tokenUri);
provider.getClass();
map.from(provider::getUserInfoUri).to(builder::userInfoUri);
provider.getClass();
map.from(provider::getUserInfoAuthenticationMethod).as(AuthenticationMethod::new).to(builder::userInfoAuthenticationMethod);
provider.getClass();
map.from(provider::getJwkSetUri).to(builder::jwkSetUri);
provider.getClass();
map.from(provider::getUserNameAttribute).to(builder::userNameAttributeName);
return builder;
}
private static CommonOAuth2Provider getCommonProvider(String providerId) {
try {
return (CommonOAuth2Provider)ApplicationConversionService.getSharedInstance().convert(providerId, CommonOAuth2Provider.class);
} catch (ConversionException var2) {
return null;
}
}
CommonOAuth2Provider
提供公共的处理类:
public enum CommonOAuth2Provider {
//google
GOOGLE {
public Builder getBuilder(String registrationId) {
Builder builder = this.getBuilder(registrationId, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope(new String[]{"openid", "profile", "email"});
builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
builder.issuerUri("https://accounts.google.com");
builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
builder.userNameAttributeName("sub");
builder.clientName("Google");
return builder;
}
},
//github
GITHUB {
public Builder getBuilder(String registrationId) {
Builder builder = this.getBuilder(registrationId, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope(new String[]{"read:user"});
builder.authorizationUri("https://github.com/login/oauth/authorize");
builder.tokenUri("https://github.com/login/oauth/access_token");
builder.userInfoUri("https://api.github.com/user");
builder.userNameAttributeName("id");
builder.clientName("GitHub");
return builder;
}
},
//facebook
FACEBOOK {
public Builder getBuilder(String registrationId) {
Builder builder = this.getBuilder(registrationId, ClientAuthenticationMethod.CLIENT_SECRET_POST, "{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope(new String[]{"public_profile", "email"});
builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth");
builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token");
builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email");
builder.userNameAttributeName("id");
builder.clientName("Facebook");
return builder;
}
},
//OKTA
OKTA {
public Builder getBuilder(String registrationId) {
Builder builder = this.getBuilder(registrationId, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope(new String[]{"openid", "profile", "email"});
builder.userNameAttributeName("sub");
builder.clientName("Okta");
return builder;
}
};
//设置默认的回调地址
private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}";
private CommonOAuth2Provider() {
}
protected final Builder getBuilder(String registrationId, ClientAuthenticationMethod method, String redirectUri) {
Builder builder = ClientRegistration.withRegistrationId(registrationId);
//认证方法
builder.clientAuthenticationMethod(method);
//设置授权模式,默认是授权码模式
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
//重定向
builder.redirectUri(redirectUri);
return builder;
}
public abstract Builder getBuilder(String registrationId);
}
有上面可以看到,Spring Security
为我们提供了google,github,facebook,OKTA的处理方式并设置回调地址等。
属性维护类OAuth2ClientProperties.class
:
@ConfigurationProperties(
prefix = "spring.security.oauth2.client"
)
public class OAuth2ClientProperties implements InitializingBean {
private final Map<String, OAuth2ClientProperties.Provider> provider = new HashMap();
private final Map<String, OAuth2ClientProperties.Registration> registration = new HashMap();
public OAuth2ClientProperties() {
}
public Map<String, OAuth2ClientProperties.Provider> getProvider() {
return this.provider;
}
public Map<String, OAuth2ClientProperties.Registration> getRegistration() {
return this.registration;
}
public void afterPropertiesSet() {
this.validate();
}
public void validate() {
this.getRegistration().values().forEach(this::validateRegistration);
}
private void validateRegistration(OAuth2ClientProperties.Registration registration) {
if (!StringUtils.hasText(registration.getClientId())) {
throw new IllegalStateException("Client id must not be empty.");
}
}
public static class Provider {
//认证url
private String authorizationUri;
//tokenurl
private String tokenUri;
//userInfo url
private String userInfoUri;
//用户认证方法
private String userInfoAuthenticationMethod;
//用户名属性
private String userNameAttribute;
//jwk设置 uri
private String jwkSetUri;
//发行者 uri
private String issuerUri;
}
//读取Registration属性
public static class Registration {
//provider
private String provider;
//client id
private String clientId;
//clientSecret
private String clientSecret;
//client认证方法
private String clientAuthenticationMethod;
//认证授权类型
private String authorizationGrantType;
//重定向地址
private String redirectUri;
//授权范围
private Set<String> scope;
//客户端名称
private String clientName;
}
}
验证配置:
private void validateRegistration(OAuth2ClientProperties.Registration registration) {
if (!StringUtils.hasText(registration.getClientId())) {
throw new IllegalStateException("Client id must not be empty.");
}
}
在使用OAuth2的时候,还有一个配置类是处理OAuth2web安全的。
OAuth2WebSecurityConfiguration
:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnBean({ClientRegistrationRepository.class})
class OAuth2WebSecurityConfiguration {
OAuth2WebSecurityConfiguration() {
}
@Bean
@ConditionalOnMissingBean
OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
@Bean
@ConditionalOnMissingBean
OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnDefaultWebSecurity
static class OAuth2SecurityFilterChainConfiguration {
OAuth2SecurityFilterChainConfiguration() {
}
//创建一个安全过滤器链
@Bean
SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> {
((AuthorizedUrl)requests.anyRequest()).authenticated();
});
//设置安全oauth2Login
http.oauth2Login(Customizer.withDefaults());
//oauth2Client
http.oauth2Client();
return (SecurityFilterChain)http.build();
}
}
}
我们获取到过滤器之后,就可以和我们之前说的其他过滤器一样的处理。
进入oauth2Login()方法:
public HttpSecurity oauth2Login(Customizer<OAuth2LoginConfigurer<HttpSecurity>> oauth2LoginCustomizer) throws Exception {
oauth2LoginCustomizer.customize((OAuth2LoginConfigurer)this.getOrApply(new OAuth2LoginConfigurer()));
return this;
}
我们看到使用OAuth2LoginConfigurer
中进行处理,由于代码比较多,这里就不一一贴出了,大家进入这个类中进行查看。
还有就是通过OAuth2ClientConfigurer
OAuth2的客户端。
OAuth2ClientConfigurer.class
:
public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<OAuth2ClientConfigurer<B>, B> {
private OAuth2ClientConfigurer<B>.AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer = new OAuth2ClientConfigurer.AuthorizationCodeGrantConfigurer();
public OAuth2ClientConfigurer() {
}
public OAuth2ClientConfigurer<B> clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
((HttpSecurityBuilder)this.getBuilder()).setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
return this;
}
public OAuth2ClientConfigurer<B> authorizedClientRepository(OAuth2AuthorizedClientRepository authorizedClientRepository) {
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
((HttpSecurityBuilder)this.getBuilder()).setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository);
return this;
}
public OAuth2ClientConfigurer<B> authorizedClientService(OAuth2AuthorizedClientService authorizedClientService) {
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.authorizedClientRepository(new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService));
return this;
}
public OAuth2ClientConfigurer<B>.AuthorizationCodeGrantConfigurer authorizationCodeGrant() {
return this.authorizationCodeGrantConfigurer;
}
public OAuth2ClientConfigurer<B> authorizationCodeGrant(Customizer<OAuth2ClientConfigurer<B>.AuthorizationCodeGrantConfigurer> authorizationCodeGrantCustomizer) {
authorizationCodeGrantCustomizer.customize(this.authorizationCodeGrantConfigurer);
return this;
}
public void init(B builder) {
this.authorizationCodeGrantConfigurer.init(builder);
}
public void configure(B builder) {
this.authorizationCodeGrantConfigurer.configure(builder);
}
public final class AuthorizationCodeGrantConfigurer {
private OAuth2AuthorizationRequestResolver authorizationRequestResolver;
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private AuthorizationCodeGrantConfigurer() {
}
public OAuth2ClientConfigurer<B>.AuthorizationCodeGrantConfigurer authorizationRequestResolver(OAuth2AuthorizationRequestResolver authorizationRequestResolver) {
Assert.notNull(authorizationRequestResolver, "authorizationRequestResolver cannot be null");
this.authorizationRequestResolver = authorizationRequestResolver;
return this;
}
public OAuth2ClientConfigurer<B>.AuthorizationCodeGrantConfigurer authorizationRequestRepository(AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository) {
Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
this.authorizationRequestRepository = authorizationRequestRepository;
return this;
}
public OAuth2ClientConfigurer<B>.AuthorizationCodeGrantConfigurer accessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
public OAuth2ClientConfigurer<B> and() {
return OAuth2ClientConfigurer.this;
}
private void init(B builder) {
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(this.getAccessTokenResponseClient());
builder.authenticationProvider((AuthenticationProvider)OAuth2ClientConfigurer.this.postProcess(authorizationCodeAuthenticationProvider));
}
//进行配置
private void configure(B builder) {
OAuth2AuthorizationRequestRedirectFilter authorizationRequestRedirectFilter = this.createAuthorizationRequestRedirectFilter(builder);
builder.addFilter((Filter)OAuth2ClientConfigurer.this.postProcess(authorizationRequestRedirectFilter));
OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = this.createAuthorizationCodeGrantFilter(builder);
builder.addFilter((Filter)OAuth2ClientConfigurer.this.postProcess(authorizationCodeGrantFilter));
}
//创建createAuthorizationRequestRedirectFilter 请求重定向过滤器
private OAuth2AuthorizationRequestRedirectFilter createAuthorizationRequestRedirectFilter(B builder) {
OAuth2AuthorizationRequestResolver resolver = this.getAuthorizationRequestResolver();
OAuth2AuthorizationRequestRedirectFilter authorizationRequestRedirectFilter = new OAuth2AuthorizationRequestRedirectFilter(resolver);
if (this.authorizationRequestRepository != null) {
authorizationRequestRedirectFilter.setAuthorizationRequestRepository(this.authorizationRequestRepository);
}
RequestCache requestCache = (RequestCache)builder.getSharedObject(RequestCache.class);
if (requestCache != null) {
authorizationRequestRedirectFilter.setRequestCache(requestCache);
}
return authorizationRequestRedirectFilter;
}
//OAuth2AuthorizationRequestResolver处理
private OAuth2AuthorizationRequestResolver getAuthorizationRequestResolver() {
if (this.authorizationRequestResolver != null) {
return this.authorizationRequestResolver;
} else {
ClientRegistrationRepository clientRegistrationRepository = OAuth2ClientConfigurerUtils.getClientRegistrationRepository((HttpSecurityBuilder)OAuth2ClientConfigurer.this.getBuilder());
return new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization");
}
}
//设置拦截器
private OAuth2AuthorizationCodeGrantFilter createAuthorizationCodeGrantFilter(B builder) {
AuthenticationManager authenticationManager = (AuthenticationManager)builder.getSharedObject(AuthenticationManager.class);
OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter(OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(builder), authenticationManager);
if (this.authorizationRequestRepository != null) {
authorizationCodeGrantFilter.setAuthorizationRequestRepository(this.authorizationRequestRepository);
}
//请求缓存
RequestCache requestCache = (RequestCache)builder.getSharedObject(RequestCache.class);
if (requestCache != null) {
authorizationCodeGrantFilter.setRequestCache(requestCache);
}
return authorizationCodeGrantFilter;
}
//getAccessTokenResponseClient
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> getAccessTokenResponseClient() {
return (OAuth2AccessTokenResponseClient)(this.accessTokenResponseClient != null ? this.accessTokenResponseClient : new DefaultAuthorizationCodeTokenResponseClient());
}
}
}
下一遍文章将使用QQ进行快捷登陆。