Spring Security Oauth2.0实现,同时思考相应的替代方案
OAuth2.0是目前一个很通用的认证授权协议,可以说是中大型框架必不可少的一部分 目前市面上的程序框架也不少,稍后会做一篇专门的比对篇,也是自己在技术选型是的一点经验之谈,大家请期待。同时,springsecurity专题也告一段落,感兴趣的同学可以看这几篇,在此也推荐一个研究很深入博主:https://felord.cn
从Spring Security 5.2开始,已经取消了认证服务器的相关功能。
Spring社区好不容易搞出来一个OAuth2.0集大成者Spring Security5.2,竟然不支持实现认证服务器,只对客户端和资源服务器予以支持。给出的理由是:Spring Security作为框架不应该提供产品级别的支持。
说白了我们Spring社区的框架都是为了开发者而存在的,认证服务器是一个产品,我们不是商业机构,不做产品。而且目前有很多的这种产品了,我们就不开发了。比如:Keycloak、Okta。2019-11-14/spring-security-oauth-2-0-roadmap-update
最后说了一句:大家可以针对“不支持认证服务器”的决策进行反馈,Spring社区还会听取意见。结果,评论下面一堆反对意见,对Keycloak口诛笔伐。
因此目前的建议是:
-
如果要实现“认证服务器”功能,还是使用Spring Security OAuth项目。这也是本章要为大家介绍的内容。 -
如果是新建项目,可以考虑使用Spring Security5.2实现OAuth2.0客户端和资源服务器,而认证服务器采用其他框架实现比如 -
如果是老项目,暂时不要追新,不要做Spring Security OAuth项目到Spring Security5.2的迁移工作。
由于目前Spring社区官方尚未给出最终的新版认证服务器的解决方案,如果我们要依托Spring Security来建立身份认证系统的话,还是需要依托已过期的Spring Security Oauth项目。
当然,也可以自己选择另一款身份认证系统来进行使用。
考虑到长远的使用和较复杂的功能、应用场景要求,后续本人可能会选择一款较轻量级的国产开源软件去整合自己的框架。
概念
OAuth定义了四个角色:
-
资源所有者 能够授予对受保护资源的访问权限的实体。如果资源所有者是一个人,则称为用户。 -
资源服务器 托管受保护资源的服务器,能够接受并使用访问令牌响应受保护的资源请求。 -
客户 代表以下用户提出受保护资源请求的应用程序:资源所有者及其授权。客户一词 不暗示任何特定的实现特征(例如应用程序是否在服务器,台式机或其他服务器上执行设备)。 -
授权服务器 成功后,服务器向客户端发布访问令牌对资源所有者进行身份验证并获得授权。
OAuth 2.0 提供者
OAuth 2.0提供者机制负责公开OAuth 2.0受保护的资源。配置包括建立OAuth 2.0客户端,可以独立地或代表用户访问其受保护的资源。提供者通过管理和验证用于访问受保护资源的OAuth 2.0令牌来实现这一点。在适用的情况下,提供者还必须为用户提供一个接口,以确认客户端可以访问受保护的资源(即确认页面)。
OAuth 2.0 提供者实现类
OAuth 2.0中的提供者角色实际上是在授权服务和资源服务之间进行划分的,虽然它们有时是在同一个应用程序中,但在Spring Security OAuth中,您可以选择在两个应用程序之间进行拆分,并且拥有多个共享授权服务的资源服务。令牌的请求由Spring MVC控制器端点来处理,而对受保护资源的访问由标准Spring安全请求过滤器处理。为了实现OAuth 2.0授权服务器,Spring安全过滤器链中需要以下端点:
-
[ AuthorizationEndpoint
][AuthorizationEndpoint] 用于服务请求的授权。默认URL:/oauth/authorize
. -
[ TokenEndpoint
][TokenEndpoint] 用于服务访问令牌的请求。默认URL:/oauth/token
.
下面的过滤器需要实现OAuth 2.0资源服务器:
-
[ OAuth2AuthenticationProcessingFilter
][OAuth2AuthenticationProcessingFilter] 用于为请求提供一个经过身份验证的访问令牌进行身份验证。
授权服务器配置
@EnableAuthorizationServer注释用于配置OAuth 2.0授权服务器机制,以及任何实现AuthorizationServerConfigurer的@ bean(有一个方便的适配器实现提供了空方法的实现) 。下面的特性被委托给由Spring创建的配置器,并传递给AuthorizationServerConfigurer`:
-
ClientDetailsServiceConfigurer
: 定义客户端详细信息服务的配置程序。客户端细节可以被初始化,也可以直接引用现有的存储。 -
AuthorizationServerSecurityConfigurer
: 定义令牌端点上的安全约束。 -
AuthorizationServerEndpointsConfigurer
: 定义授权和令牌端点和令牌服务。
配置客户端详细信息
ClientDetailsServiceConfigurer(来自您的AuthorizationServerConfigurer的回调)可以用于定义客户端详细信息服务的内存或JDBC实现。客户端的重要属性是:
-
clientId
:(必需)客户id。 -
secret
: (需要信任的客户)客户的密钥,如果有的话。 -
scope
: 客户受限制的范围。如果作用域是未定义的或空的(默认的),客户端不受范围限制。 -
authorizedGrantTypes
: 授权给客户端使用的授权类型。默认值是空的。 -
authorities
: 授权给客户的部门。(通常是 Spring Security authorities).
客户端详细信息可以在运行的应用程序中更新,通过直接访问底层存储(例如JdbcClientDetailsService案例中的数据库表)或通过ClientDetailsManager接口(ClientDetailsService的两个实现都实现了)。
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
public AuthorizationServerConfigurerAdapter() {
}
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
//配置类实现部分代码
@Configuration
@RequiredArgsConstructor
@EnableAuthorizationServer
public class LocalAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final ClientDetailsService clientDetailsServiceImpl;
@Override
@SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
clients.withClientDetails(clientDetailsServiceImpl);
}
...
}
//ClientDetailsServiceImpl 定义数据库实现
@Service
public class ClientDetailsServiceImpl extends JdbcClientDetailsService {
@Override
public ClientDetails loadClientByClientId(String clientId) {
super.setSelectClientDetailsSql(
String.format("select * from ****", TenantContextHolder.getTenantId()));
return super.loadClientByClientId(clientId);
}
}
管理令牌
[AuthorizationServerTokenServices
][AuthorizationServerTokenServices] 接口定义了管理OAuth 2.0令牌所必需的操作。请注意以下几点:
-
当创建访问令牌时,必须存储身份验证,以便稍后接受访问令牌的资源可以引用它。 -
访问令牌被用来加载用于授权其创建的身份验证。
public interface AuthorizationServerTokenServices {
OAuth2AccessToken createAccessToken(OAuth2Authentication var1) throws AuthenticationException;
OAuth2AccessToken refreshAccessToken(String var1, TokenRequest var2) throws AuthenticationException;
OAuth2AccessToken getAccessToken(OAuth2Authentication var1);
}
您可能需要考虑使用具有许多策略的DefaultTokenServices来更改访问令牌的格式和存储。默认情况下,它通过随机值创建令牌,并处理所有的东西,除了它委托给TokenStore的令牌的持久性。默认存储是[在内存中实现的][InMemoryTokenStore],但还有一些其他实现可用。下面是对每种实现方式的一些讨论。
-
默认的
InMemoryTokenStore
对于单个服务器来说是完美的(例如,在失败的情况下,低流量和没有热交换到备份服务器)。大多数项目都可以从这里开始,并可能在开发模式中使用这种方式,从而能很容易的启动一个没有依赖关系的服务器。 -
JdbcTokenStore
和JDBC版本是同一种东西,它使用关系数据库来存储令牌数据。如果您可以在服务器之间共享一个数据库,那么可以使用JDBC版本,如果只有一个服务器,则可以扩展相同服务器的实例,如果有多个组件,则可以使用授权和资源服务器。为了使用JdbcTokenStore
,您需要将 “spring-jdbc” 配置到classpath中. -
JSON Web Token (JWT) version 将所有关于grant的数据编码到令牌本身(因此没有任何后端存储,这是一个重要的优势)。一个缺点是,您不能很容易地撤销访问令牌,因此它们通常在短时间内被授予,而撤销则在刷新令牌中处理。另一个缺点是,如果您在其中存储了大量用户凭证信息,则令牌可以变得相当大。
JwtTokenStore
并不是真正的“存储”,因为它没有保存任何数据,但是它在DefaultTokenServices
中扮演了转换betweeen令牌值和身份验证信息的角色。 -
云户中的做法是将令牌信息存储在redis中
@Service
public class YunhoRedisTokenStore implements TokenStore {
}
TokenStore需要在配置类中设置:
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenStore(tokenStore)
.tokenEnhancer(tokenEnhancer).userDetailsService(userDetailsService) .authorizationCodeServices(authorizationCodeServices).authenticationManager(authenticationManagerBean)
.reuseRefreshTokens(false).pathMapping("/oauth/confirm_access", "/token/confirm_access")
.exceptionTranslator(new webResponseExceptionTranslator());
}
JWT令牌
要使用JWT令牌,您需要在授权服务器中使用JwtTokenStore
。资源服务器还需要能够解码令牌,这样JwtTokenStore
就依赖于 JwtAccessTokenConverter
,并且授权服务器和资源服务器都需要相同的实现。该令牌是默认签名的,并且资源服务器还必须能够验证签名,因此需要与授权服务器(共享私钥或对称密钥)相同的对称(签名)密钥,或者它需要与授权服务器(公私或非对称密钥)中的私钥(签名密钥)相匹配的公钥(验证器密钥)。公钥(如果可用)由/oauth/token_key
端点上的授权服务器公开,该端点在默认情况下是安全的,具有访问规则“denyAll()”。您可以通过向AuthorizationServerSecurityConfigurer
中注入标准的SpEL表达式来打开它。“permitAll()”可能已经足够了,因为它是一个公钥。
要使用JwtTokenStore
,您需要在classpath上使用“Spring -security-jwt”(您可以在相同的github存储库中找到它,它与Spring OAuth相同,但有不同的发布周期)。
授权类型
AuthorizationEndpoint
支持的授权类型可以通过AuthorizationServerEndpointsConfigurer
配置。默认情况下,除密码外,所有的授权类型都是受支持的(请参阅下面关于如何切换的详细信息)。以下属性影响授权类型:
-
authenticationManager
: 通过注入一个AuthenticationManager
来打开密码授权。 -
userDetailsService
: 如果您注入了一个UserDetailsService
,或者在全局上配置了一个(例如,在GlobalAuthenticationManagerConfigurer
中),那么刷新令牌授权将包含对用户详细信息的检查,以确保帐户仍然处于活动状态。 -
authorizationCodeServices
: 为身份验证代码授予定义授权代码服务(AuthorizationCodeServices
)的实例)。 -
implicitGrantService
: 在imlpicit授权期间管理状态。 -
tokenGranter
: theTokenGranter
(完全控制授予和忽略上面的其他属性)
配置的端点url
AuthorizationServerEndpointsConfigurer
有一个pathMapping()
方法。它需要两个参数:
-
端点的默认(框架实现)URL路径。 -
需要的自定义路径(以“/”开头)
框架提供的路径是 /oauth/authorize
(授权端点), /oauth/token
(令牌端点), /oauth/confirm_access
(用户在这里获得批准), /oauth/error
(用于呈现在授权服务器错误), /oauth/check_token
(用于资源服务器解码访问令牌。), and /oauth/token_key
(如果使用JWT令牌,公开公钥进行令牌验证).
N.B. 应该使用Spring Security保护授权端点/oauth/authorize
(或其映射的替代),以便只有经过身份验证的用户才能访问它。例如:使用标准的 Spring Security WebSecurityConfigurer
:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/login").permitAll().and()
// default protection for all resources (including /oauth/authorize)
.authorizeRequests()
.anyRequest().hasRole("USER")
// ... more configuration, e.g. for form login
}
注意:如果您的授权服务器也是一个资源服务器,那么还有另一个安全过滤器链,它的优先级较低,控制了API资源。对于那些需要通过访问令牌来保护的请求,您需要它们的路径不能与主用户所面对的过滤器链中的那些相匹配,所以一定要包含一个请求matcher,它只挑选出上面的
WebSecurityConfigurer
中的非Api资源。
在@Configuration
支持中,使用客户端机密的HTTP基本身份验证,Spring OAuth默认为您保护令牌端点。
定制用户界面
大多数授权服务器端点主要是由机器使用的,但是有一些资源需要一个UI,而这些资源是GET for /oauth/confirm_access
和 /oauth/error
的HTML响应。它们在框架中使用了简单的页面实现,因此授权服务器的大多数真实实例都希望提供自己的实现,这样它们就可以控制样式和内容。您所需要做的就是为这些端点提供一个带有@RequestMappings
的Spring MVC控制器,而框架默认值将在调度程序中降低优先级。在/oauth/confirm_access
端点中,您可以预期一个AuthorizationRequest
绑定到会话,该请求将携带需要获得用户批准的所有数据(默认的实现是AuthorizationRequest
,因此要查看那里的起始点以复制)。您可以从该请求中获取所有的数据,并按您喜欢的方式呈现它,然后所有用户需要做的就是将批准或拒绝授予的信息发布回 /oauth/authorize
。请求参数直接传递给AuthorizationEndpoint
中的UserApprovalHandler
,这样您就可以随意地解释数据了。默认的UserApprovalHandler
取决于您是否在AuthorizationServerEndpointsConfigurer
中提供了ApprovalStore
(在这种情况下,它是ApprovalStoreUserApprovalHandler
)或not(在这种情况下,它是一个TokenStoreUserApprovalHandler
)。标准审批处理程序接受以下内容:
-
TokenStoreUserApprovalHandler
: 通过user_oauth_approval
的一个简单的yes/no决策等于“true”或“false”。 -
ApprovalStoreUserApprovalHandler
: 一组scope.*
参数键与所请求的作用域相等。参数的值可以是“true”或“approved”(如果用户批准了授权),则用户被认为拒绝了该范围。如果至少有一个范围被批准,那么授权是成功的。
注意:不要忘记将CSRF保护包含在您为用户呈现的表单中。Spring Security在默认情况下期望一个名为“_csrf”的请求参数(它提供了请求属性中的值)。
执行SSL
普通HTTP可以用于测试,但是授权服务器只能在生产中使用SSL。您可以在一个安全的容器或代理的后面运行该应用程序,如果您正确地设置了代理和容器(这与OAuth2无关),那么它应该可以正常工作。您还可能希望使用Spring Security requiresChannel()
约束来保护端点。对于/authorize
端点,你要做的是作为你正常的应用程序安全的一部分。对于/token
端点,在AuthorizationServerSecurityConfigurer
中有一个标记,您可以使用sslOnly()
方法进行设置。在这两种情况下,安全通道设置都是可选的,但如果它在不安全的通道上检测到请求,则会导致Spring Security重定向到它认为是安全通道的安全通道。
自定义错误处理
授权服务器中的错误处理使用标准的Spring MVC特性,即端点中的@ExceptionHandler
方法。用户还可以为端点本身提供一个WebResponseExceptionTranslator
,这是改变响应内容的最佳方式,而不是改变响应的方式。在授权端点的情况下,在令牌端点和OAuth错误视图(/oauth/error
)的情况下,将异常委托委托给HttpMesssageConverters
(可以添加到MVC配置)。为HTML响应提供了whitelabel错误端点,但是用户可能需要提供一个自定义实现(例如,只需添加一个@RequestMapping("/oauth/error")
的 @Controller
)。
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.exceptionTranslator(new webResponseExceptionTranslator());//自定义异常处理
}
资源服务器配置
资源服务器(可以与授权服务器或单独的应用程序相同)提供由OAuth2令牌保护的资源。Spring OAuth提供了一个实现此保护的Spring安全身份验证过滤器。您可以在 @Configuration
类上使用@EnableResourceServer
来切换它,并使用ResourceServerConfigurer
配置它(必要时)。他可以配置以下功能:
-
tokenServices
: 定义令牌服务的bean (ResourceServerTokenServices实例)。 -
resourceId
: 资源的id(可选,如果存在推荐并将由auth服务器验证)。 -
资源服务器的其他扩展点(例如从传入请求中提取令牌的 tokenExtractor
)。 -
请求受保护资源的请求者(默认为所有) -
受保护资源的访问规则(默认为“已验证”) -
Spring Security中 HttpSecurity
配置程序允许的受保护资源的其他定制。
@EnableResourceServer
注解自动将OAuth2AuthenticationProcessingFilter
类型的过滤器添加到Spring Security过滤器链中。
您的ResourceServerTokenServices
是与授权服务器的契约的另一半。如果资源服务器和授权服务器都在同一个应用程序中,并且使用DefaultTokenServices
,那么您就不必对此进行过多的思考,因为它实现了所有必要的接口,因此它是自动一致的。如果您的资源服务器是一个单独的应用程序,那么您必须确保与授权服务器的功能相匹配,并提供一个知道如何正确解码ResourceServerTokenServices
。与授权服务器一样,您可以经常使用 DefaultTokenServices
,而选择主要通过TokenStore
(后端存储或本地编码)来表示。另一种选择是RemoteTokenServices
,它是一个Spring OAuth特性(不是规范的一部分),允许资源服务器通过授权服务器上的HTTP资源(/oauth/check_token
)来解码令牌。如果资源服务器中没有大量的流量(每个请求都必须与授权服务器进行验证),或者如果您有能力缓存结果,那么RemoteTokenServices
是很方便的。要使用/oauth/check_token
端点,您需要在AuthorizationServerSecurityConfigurer
中通过更改它的访问规则来公开它(默认为“denyAll()”)。例如
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')")
.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
在这个例子中,我们配置了/oauth/check_token
端点和/oauth/token_key
端点(因此可信资源可以获得JWT验证的公钥)。这两个端点通过使用客户端凭证的HTTP基本身份验证保护。
配置一个oauth表达式处理程序。
您可能想要利用Spring Security的基于表达式的访问控制。表达式处理程序将默认在@enableresourceserver设置中注册。表达式包括 #oauth2.clientHasRole, #oauth2.clientHasAnyRole, 和_#oath2.denyClient_ 可以根据oauth客户端的角色来提供访问(请参阅完整列表的 OAuth2SecurityExpressionMethods
)。
常见的内置表达式
表达式根对象的基类是SecurityExpressionRoot
。这提供了一些通用的表达式,可在Web和方法安全性中使用。
表达 | 描述 |
---|---|
hasRole([role]) |
返回true 当前委托人是否具有指定角色。这是的同义词hasAuthority([authority]) |
hasAnyRole([role1,role2]) |
返回true 当前委托人是否具有提供的任何角色(以逗号分隔的字符串列表形式)。这是的同义词hasAnyAuthority([authority1,authority2]) |
hasAuthority([authority]) |
true 如果当前主体具有指定的权限,则返回。这是的同义词hasRole([role]) |
hasAnyAuthority([authority1,authority2]) |
返回true 当前委托人是否具有提供的任何角色(以逗号分隔的字符串列表形式提供)hasAnyRole([role1,role2])``hasAnyRole([role1,role2]) |
principal |
允许直接访问代表当前用户的主体对象 |
authentication |
允许直接访问Authentication 从SecurityContext |
permitAll |
总是评估为 true |
denyAll |
总是评估为 false |
isAnonymous() |
返回true 当前委托人是否为匿名用户 |
isRememberMe() |
返回true 当前主体是否是“记住我”的用户 |
isAuthenticated() |
true 如果用户不是匿名的,则返回 |
isFullyAuthenticated() |
返回true 如果用户不是匿名或记得,我的用户 |
hasPermission(Object target, Object permission) |
返回true 用户是否有权访问给定目标的给定权限。例如,hasPermission(domainObject, 'read') |
hasPermission(Object targetId, String targetType, Object permission) |
返回true 用户是否有权访问给定目标的给定权限。例如,hasPermission(1, 'com.example.domain.Message', 'read') |
原文始发于微信公众号(云户):Spring Security Oauth2.0实现,同时思考相应的替代方案
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/25768.html