SpringBoot+Security+OAuth2实现单点登录
前言
这里主要探究基于Web站点的SSO。
最后有部分类的源码解析(oauth2.4.0),还有一部分释义。
读取源码的时候建议大家把源码 clone 一下,方便自己注释和使用 spring 的调试代码。
我也是刚开始深入学习的时候所做此文章,所以内容有部分冗余,并且代码以及代码注解会比较多。
阅读当前文章需要了解 JWT、Token、Security
知识。
比如 new UsernamePasswordAuthenticationToken
有几个构造方式,以及构造方法的参数含义。
基础概念。
在OAuth2中主要有四个角色,主要如下:
- Resource Owner(资源拥有者):用户,即资源的拥有人,想要分享某些资 源给第三方应用
- Resource Server(资源服务器):放受保护资源,要访问这些资源,需要获得访问令牌
- Authorization server(授权(认证)服务器):授权服务器用于发放访问令牌给客户端
- Client(客户端应用(第三方应用)):客户端代表请求资源服务器资源的第三方程序
在实现SSO
中只需要 授权服务器和客户端 就足够了。
这里的客户端可以是像postman
这样的工具。
分清Security
、OAuth2
、JWT
、SSO
SSO
是一种解决方案,我们只需要按照这种方案去实现它。OAuth2
是一种协议,我们只是可以用它来做单点登录。OAuth2
服务端主要作用就是授权。JWT
是授权时生成令牌用的。Security
是用户安全访问框架,用来做访问权限控制。
实现
一些配置
具体配置在gitee上。
OAuth 2.0中的提供者角色实际上是在授权服务和资源服务之间分割的,虽然这些服务有时驻留在同一个应用程序中,但是使用Spring Security OAuth,您可以选择将它们分割为两个应用程序,也可以拥有多个共享授权服务的资源服务。对令牌的请求由Spring MVC控制器端点处理,对受保护资源的访问由标准Spring安全请求过滤器处理。为了实现OAuth 2.0授权服务器,Spring安全过滤器链需要以下端点:
AuthorizationEndpoint is used to service requests for authorization. Default URL: /oauth/authorize.
TokenEndpoint is used to service requests for access tokens. Default URL: /oauth/token.
The following filter is required to implement an OAuth 2.0 Resource Server:
The OAuth2AuthenticationProcessingFilter is used to load the Authentication for the request given an authenticated access token.
授权服务器配置
在配置授权服务器时,必须考虑客户端用于从最终用户获取访问令牌的授权类型(例如,授权代码、用户凭据、刷新令牌)。
服务器的配置用于提供客户端详细信息服务和令牌服务的实现,并全局启用或禁用该机制的某些方面。
但是,请注意,可以为每个客户端专门配置权限,以便能够使用特定的授权机制和访问授权。
例如,仅仅因为您的提供者被配置为支持“客户端凭证”授权类型,并不意味着特定的客户端被授权使用该授权类型。
@EnableAuthorizationServer
注释用于配置OAuth 2.0
授权服务器机制,以及实现AuthorizationServerConfigurer
的任何@ bean(有一个带有空方法的便利适配器实现)。
以下特性被委托给由Spring创建并传递到AuthorizationServerConfigurer
的独立配置器:
ClientDetailsServiceConfigurer
:定义客户端详细信息服务的配置程序。可以初始化客户端详细信息,也可以仅引用现有存储。AuthorizationServerSecurityConfigurer
:定义令牌端点上的安全约束。AuthorizationServerEndpointsConfigurer
:定义授权和令牌端点以及令牌服务。
提供者配置的一个重要方面是授权代码提供给OAuth客户端的方式(在授权代码授权中)。授权代码由OAuth客户机通过将最终用户引导到一个授权页面来获得,用户可以在该页面中输入自己的凭证,从而将授权代码从提供者授权服务器重定向回OAuth客户机。OAuth 2规范中对此进行了详细说明。
客户端配置细节
ClientDetailsServiceConfigurer
(来自AuthorizationServerConfigurer
的回调)可用于定义客户机详细信息服务的内存或JDBC实现。客户的重要属性是
clientId
: (required) the client id.secret
: (required for trusted clients) the client secret, if any.受信任的客户端必须要有scope
: The scope to which the client is limited. If scope is undefined or empty (the default) the client is not limited by scope.客户端受限制的范围。如果作用域未定义或为空(缺省值),则客户端不受作用域的限制。authorizedGrantTypes
: Grant types that are authorized for the client to use. Default value is empty.authorities
: Authorities that are granted to the client (regular Spring Security authorities).常规spring 安全权限
Tokens
管理
AuthorizationServerTokenServices
接口定义了管理OAuth 2.0令牌所需的操作。请注意以下几点:
创建访问令牌时,必须存储身份验证,以便接受访问令牌的资源可以在以后引用它。
访问令牌用于加载用于授权其创建的身份验证。
创建访问令牌时,必须存储身份验证,以便接受访问令牌的资源可以在以后引用它。
访问令牌用于加载用于授权其创建的身份验证。
在创建您的AuthorizationServerTokenServices
实现时,您可能希望考虑使用DefaultTokenServices
,它有许多可以插入的策略来更改访问令牌的格式和存储。默认情况下,它通过随机值创建令牌,并处理除将其委托给TokenStore
的令牌的持久性之外的所有事情。默认存储是内存中的实现,但是还有其他一些可用的实现。下面是对它们的描述和讨论
默认的
InMemoryTokenStore
对于单个服务器来说是非常合适的(例如,在出现故障的情况下,低流量和没有到备份服务器的热交换)。大多数项目都可以从这里开始,并可能在开发模式中以这种方式进行操作,以便轻松启动没有依赖项的服务器。JdbcTokenStore
是相同事物的JDBC版本,它将令牌数据存储在关系数据库中。如果可以在服务器之间共享数据库,则使用JDBC版本;如果只有一个服务器,则使用同一个服务器的放大实例;如果有多个组件,则使用授权和资源服务器。要使用JdbcTokenStore
,您需要在类路径上使用“spring-jdbc”。该存储的JSON Web Token(JWT) version将有关授权的所有数据编码到令牌本身(因此根本没有后端存储,这是一个显著的优点)。一个缺点是您无法轻松地撤销访问令牌,因此通常会在短时间内授予它们,并在refresh令牌上处理撤销。另一个缺点是,如果您在令牌中存储大量用户凭据信息,则令牌可能会变得非常大。
JwtTokenStore
并不是真正的“存储”,因为它不持久化任何数据,但是它发挥相同的作用翻译之间的令牌的价值观和认证中的信息DefaultTokenServices
.。
注意:JDBC服务的模式没有随库一起打包(因为在实践中您可能想要使用太多的变体),但是有一个示例可以从github的测试代码开始。确保
@EnableTransactionManagement
,以防止在创建令牌时客户端应用程序争用相同行时发生冲突。还要注意,示例模式有显式的主键声明——这些声明在并发环境中也是必需的。
JWT Tokens
要使用JWT tokens
,您需要在授权服务器中有一个JwtTokenStore
。资源服务器还需要能够解码令牌,以便JwtTokenStore
依赖于JwtAccessTokenConverter
,授权服务器和资源服务器都需要相同的实现。默认情况下,签署的标记和资源服务器也能够验证签名,所以它需要相同的对称(签名)密钥授权服务器(共享密钥,或对称密钥),或者它需要公钥(匹配键)相匹配的私钥(键)签署授权服务器(公私合营或非对称密钥)。公钥(如果可用)由/oauth/token_key
端点上的授权服务器公开,该端点在默认情况下是安全的,并且具有访问规则“denyAll()”。您可以通过将一个标准SpEL表达式注入AuthorizationServerSecurityConfigurer
(例如,“permitAll()”可能就足够了,因为它是一个公钥)。
要使用JwtTokenStore
,需要依赖“Spring -security-jwt”(您可以在与Spring OAuth相同的github存储库中找到它,但是使用不同的发布周期)。
一般在依赖中
1 | <spring.security.oauth.version>2.4.0.RELEASE</spring.security.oauth.version> |
Grant Types 授权类型
AuthorizationEndpoint
支持的授权类型可以通过AuthorizationServerEndpointsConfigurer
进行配置。默认情况下,除了密码之外,所有的授权类型都是受支持的。下列属性会影响授权类别:
authenticationManager
:通过注入authenticationManager打开密码授予。userDetailsService
:如果您注入了userDetailsService
,或者全局配置了userDetailsService
(例如在GlobalAuthenticationManagerConfigurer
中),则刷新令牌授予将包含对用户详细信息的检查,以确保帐户仍然处于活动状态authorizationCodeServices
:为auth代码授权定义授权代码服务(authorizationCodeServices
实例)。在
imlpicit
(授权码简化模式)授予期间管理状态。tokenGranter
:tokenGranter
(完全控制授予并忽略上面的其他属性)
配置Endpoint URLs
The AuthorizationServerEndpointsConfigurer
has a pathMapping()
method. It takes two arguments:
- The default (framework implementation) URL path for the endpoint 端点的默认(框架实现)URL路径
- The custom path required (starting with a “/“) 需要的自定义路径(以“/”开头)
The URL paths provided by the framework are /oauth/authorize
(the authorization endpoint),
/oauth/token
(the token endpoint),
/oauth/confirm_access
(user posts approval for grants here), 用户在这里发布对授权的批准
/oauth/error
(used to render errors in the authorization server), 用于在授权服务器中呈现错误
/oauth/check_token
(used by Resource Servers to decode access tokens),资源服务器用于解码访问令牌 and
/oauth/token_key
(exposes public key for token verification if using JWT tokens).如果使用JWT令牌,则公开用于令牌验证的公钥
N.B. the Authorization endpoint
/oauth/authorize
(or its mapped alternative) should be protected using Spring Security so that it is only accessible to authenticated users. For instance using a standard Spring SecurityWebSecurityConfigurer
:注意:授权端点
/oauth/authorize
(或其映射的替代)应该使用Spring Security
进行保护,以便仅对经过身份验证的用户进行访问。例如使用标准的Spring
安全WebSecurityConfigurer
:
1 | @Override |
Note: if your Authorization Server is also a Resource Server then there is another security filter chain with lower priority controlling the API resources. Fo those requests to be protected by access tokens you need their paths not to be matched by the ones in the main user-facing filter chain, so be sure to include a request matcher that picks out only non-API resources in the
WebSecurityConfigurer
above.注意:如果您的授权服务器也是资源服务器,那么还有另一个安全过滤器链,它的优先级较低,控制着API资源。为了让这些请求受到访问令牌的保护,您需要让它们的路径与主要的面向用户的过滤器链中的路径不匹配,所以一定要包含一个请求匹配器,它只选择上面的
WebSecurityConfigurer
中的非api资源。
The token endpoint is protected for you by default by Spring OAuth in the @Configuration
support using HTTP Basic authentication of the client secret. This is not the case in XML (so it should be protected explicitly).
缺省情况下,在使用客户端机密的HTTP基本身份验证的@Configuration
支持中,Spring OAuth
为您保护令牌端点。在XML中不是这样(所以应该显式地保护它)。
自定义错误处理(Customizing the Error Handling)
授权服务器中的错误处理使用标准的Spring MVC
特性,即endpoints
本身中的@ExceptionHandler
方法。
用户还可以为endpoints
本身提供一个WebResponseExceptionTranslator
,这是改变响应内容(而不是它们呈现方式)的最佳方式。
异常的呈现在 token endpoint
的情况下委托给HttpMesssageConverters
(可以将其添加到MVC配置中),在 teh authorization endpoint
的情况下委托给OAuth错误视图(/ OAuth /error
”。
whitelabel
错误endpoint
是为HTML
响应提供的,但是用户可能需要提供一个自定义实现(例如,只需添加一个’ @Controller
和@RequestMapping("/oauth/error")
)。
Mapping User Roles to Scopes
将用户角色映射到范围,大多数情况下,都需要自己定义角色和角色的范围。
If you use a DefaultOAuth2RequestFactory
in your AuthorizationEndpoint
you can set a flag checkUserScopes=true
to restrict permitted scopes to only those that match the user’s roles.
如果在AuthorizationEndpoint
中使用DefaultOAuth2RequestFactory
,那么您可以设置一个标记checkuserscope =true
,将允许的范围限制为仅匹配用户角色的范围。
你也可以将一个OAuth2RequestFactory
注入到“TokenEndpoint
”中,但是只有在你安装了一个“TokenEndpointAuthenticationFilter
”的时候才有效(例如,使用密码授权)——你只需要在HTTP的“BasicAuthenticationFilter
”之后添加这个过滤器。
当然,您也可以实现您自己的规则来将作用域映射到角色,并安装您自己的“OAuth2RequestFactory
”版本。
“AuthorizationServerEndpointsConfigurer
”允许您注入一个定制的“OAuth2RequestFactory
”,这样,如果您使用“@EnableAuthorizationServer
”,就可以使用该特性来设置工厂。
SSO 基本设计
应用/中间件
应用/模块/对象 | 说明 |
---|---|
客户端 | 需要登录的站点 |
SSO站点-登录 | 提供登录的页面 |
SSO站点-登出 | 提供注销登录的入口 |
SSO服务-登录 | 提供登录服务 |
SSO服务-登录状态 | 提供登录状态校验/登录信息查询的服务 |
SSO服务-登出 | 提供用户注销登录的服务 |
数据库 | 存储用户账户信息 |
缓存 | 存储用户的登录信息 |
用户登录状态
用户登录成功之后,生成Token
交给客户端保存。如果是浏览器,就保存在Cookie
或者本地缓存中。如果是手机App就保存在App本地缓存中。
用户在浏览需要登录的页面时,客户端将Token
提交给SSO服务校验登录状态/获取用户登录信息
对于登录信息的存储,建议采用Redis,使用Redis集群来存储登录信息,既可以保证高可用,又可以线性扩充。同时也可以让SSO服务满足负载均衡/可伸缩的需求。
对象 | 说明 |
---|---|
token | JWT 生成 token,包含权限信息 |
登录信息 | 通常是将UserId,UserName缓存起来 |
参考
https://segmentfault.com/a/1190000016738030
数据库
spring 给出的 test 数据库
1 | -- 服务端 |
access_token
与 refresh_token
区别
access_token
是每次请求的凭证,而 refresh_token
是获取access_token
的凭证,当access_token
过期时,就需要refresh_token
去获取新的access_token
。
继承 AuthorizationServerConfigurerAdapter
(授权服务器配置)
继承AuthorizationServerConfigurerAdapter
之后可以自定义一些配置,比如:
approval store
authorization code services
授权码服务(保存到内存或者数据库)client details service
客户端信息存储服务
释义
注解
@FrameworkEndpoint
@Controller
的同义词,但仅用于框架提供的端点(因此它永远不会与用@Controller
定义的用户自己的端点冲突)。
@SessionAttributes
只能作用在类上。
@ModelAttribute
注解作用在方法上或者方法的参数上,表示将被注解的方法的返回值或者是被注解的参数作为Model
的属性加入到Model
中,然后Spring框架自会将这个Model
传递给ViewResolver
。Model
的生命周期只有一个http
请求的处理过程,请求处理完后,Model
就销毁了。
如果想让参数在多个请求间共享,那么可以用到要说到的@SessionAttribute
注解
例如
1 |
|
上面的代码将Model
中的name
参数保存到了session
中(如果Model
中没有name
参数,而session
中存在一个name
参数,那么SessionAttribute
会讲这个参数塞进Model
中)
@SessionAttribute
有两个参数:
String[] value:要保存到session中的参数名称
Class[] typtes:要保存的参数的类型,和value中顺序要对应上
所以可以这样写:@SessionAttributes(types = {String.class,String.class},value={“attr1”,”attr2”})
它的做法大概可以理解为将Model中的被注解的attrName
属性保存在一个SessionAttributesHandler
中,在每个RequestMapping
的方法执行后,这个SessionAttributesHandler
都会将它自己管理的“属性”从Model中写入到真正的HttpSession
;同样,在每个RequestMapping
的方法执行前,SessionAttributesHandler
会将HttpSession
中的被@SessionAttributes
注解的属性写入到新的Model中。
如果想删除session
中共享的参数,可以通过SessionStatus.setComplete()
,这句只会删除通过@SessionAttribute
保存到session
中的参数
@JsonValue 序列化之后只返回这一个值
用在属性或者get方法上。
@JsonProperty 把当前属性名称序列化为另一个名称
当接受application/json编码格式的参数时,同样需要接收参数为this_name的参数.
但是当用表单提交时,则必须传thisName或ThisName才能接收
源码解析
AuthorizationEndpoint
1 | package org.springframework.security.oauth2.provider.endpoint; |
AuthorizationEndpoint
中采用两种常用的UserApprovalHandler
策略,一种是细精粒度的基于Scope
的ApprovalStoreUserApprovalHandler
,一种是粗粒度的基于token
的TokenStoreUserApprovalHandler
。两种策略不同之处就是在授权页面上,基于Scope
的ApprovalStoreUserApprovalHandler
策略需要为每一个Scope
授权同意,而粗粒度的基于token
的TokenStoreUserApprovalHandler
因为是基于token
的,所以体现在页面上的只有一个同意或者拒绝的按钮来表示是否对用户的操作进行授权。