SpringBoot+Security+OAuth2实现单点登录

前言

这里主要探究基于Web站点的SSO。

最后有部分类的源码解析(oauth2.4.0),还有一部分释义。

读取源码的时候建议大家把源码 clone 一下,方便自己注释和使用 spring 的调试代码。

我也是刚开始深入学习的时候所做此文章,所以内容有部分冗余,并且代码以及代码注解会比较多。

阅读当前文章需要了解 JWT、Token、Security 知识。

比如 new UsernamePasswordAuthenticationToken 有几个构造方式,以及构造方法的参数含义。

基础概念。

在OAuth2中主要有四个角色,主要如下:

  1. Resource Owner(资源拥有者):用户,即资源的拥有人,想要分享某些资 源给第三方应用
  2. Resource Server(资源服务器):放受保护资源,要访问这些资源,需要获得访问令牌
  3. Authorization server(授权(认证)服务器):授权服务器用于发放访问令牌给客户端
  4. Client(客户端应用(第三方应用)):客户端代表请求资源服务器资源的第三方程序

在实现SSO中只需要 授权服务器和客户端 就足够了。

这里的客户端可以是像postman这样的工具。

分清SecurityOAuth2JWTSSO

  • 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令牌所需的操作。请注意以下几点:

  1. 创建访问令牌时,必须存储身份验证,以便接受访问令牌的资源可以在以后引用它。

  2. 访问令牌用于加载用于授权其创建的身份验证。

创建访问令牌时,必须存储身份验证,以便接受访问令牌的资源可以在以后引用它。

访问令牌用于加载用于授权其创建的身份验证。

在创建您的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
2
3
4
5
6
<spring.security.oauth.version>2.4.0.RELEASE</spring.security.oauth.version>        
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${spring.security.oauth.version}</version>
</dependency>

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 Security WebSecurityConfigurer:

注意:授权端点/oauth/authorize(或其映射的替代)应该使用Spring Security进行保护,以便仅对经过身份验证的用户进行访问。例如使用标准的Spring安全WebSecurityConfigurer:

1
2
3
4
5
6
7
8
9
@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
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
-- 服务端
Drop table if exists oauth_client_details;
create table oauth_client_details
(
client_id varchar(256) not null comment '用于唯一标识每一个客户端(client);注册时必须填写(也可以服务端自动生成),这个字段是必须的,实际应用也有叫app_key'
primary key,
resource_ids varchar(256) null comment '客户端能访问的资源id集合,注册客户端时,根据实际需要可选择资源id,也可以根据不同的额注册流程,赋予对应的额资源id',
client_secret varchar(256) null comment '注册填写或者服务端自动生成,实际应用也有叫app_secret, 必须要有前缀代表加密方式',
scope varchar(256) null comment '指定client的权限范围,比如读写权限,比如移动端还是web端权限.指定客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔,如: “read,write”. scope的值与security.xml中配置的‹intercept-url的access属性有关系. 如‹intercept-url的配置为‹intercept-url pattern="/m/**" access=“ROLE_MOBILE,SCOPE_READ”/>则说明访问该URL时的客户端必须有read权限范围. write的配置值为SCOPE_WRITE, trust的配置值为SCOPE_TRUST. 在实际应用中, 该值一般由服务端指定, 常用的值为read,write.',
authorized_grant_types varchar(256) null comment '可选值 授权码模式:authorization_code,密码模式:password,刷新token: refresh_token, 隐式模式: implicit: 客户端模式: client_credentials。支持多个用逗号分隔.在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: “authorization_code,refresh_token”(针对通过浏览器访问的客户端); “password,refresh_token”(针对移动设备的客户端). implicit与client_credentials在实际中很少使用.',
web_server_redirect_uri varchar(256) null comment 'web_server_redirect_uri 客户端的重定向URI,可为空, 当grant_type为authorization_code或implicit时, 在Oauth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:当grant_type=authorization_code时, 第一步 从 spring-oauth-server获取 ''code’时客户端发起请求时必须有redirect_uri参数, 该参数的值必须与 web_server_redirect_uri的值一致. 第二步 用 ‘code’ 换取 ‘access_token’ 时客户也必须传递相同的redirect_uri. 在实际应用中, web_server_redirect_uri在注册时是必须填写的, 一般用来处理服务器返回的code, 验证state是否合法与通过code去换取access_token值.在spring-oauth-client项目中, 可具体参考AuthorizationCodeController.java中的authorizationCodeCallback方法.当grant_type=implicit时通过redirect_uri的hash值来传递access_token值.',
authorities varchar(256) null comment '指定用户的权限范围,如果授权的过程需要用户登陆,该字段不生效,implicit和client_credentials需要',
access_token_validity int null comment 'access_token_validity 设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时). 在服务端获取的access_token JSON数据中的expires_in字段的值即为当前access_token的有效时间值. 在项目中, 可具体参考 DefaultTokenServices.java 中属性 accessTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义.',
refresh_token_validity int null comment '设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天). 若客户端的grant_type不包括refresh_token,则不用关心该字段 在项目中, 可具体参考 DefaultTokenServices.java 中属性refreshTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义.',
additional_information varchar(4096) null comment '值必须是json格式 {"key", "value"}',
autoapprove varchar(256) null comment '默认false,适用于authorization_code模式,设置用户是否自动approval操作,设置true跳过用户确认授权操作页面,直接跳到redirect_uri [客户端重定向uri,authorization_code和implicit需要该值进行校验,注册时填写,
设置用户是否自动Approval操作, 默认值为 ‘false’, 可选值包括 ‘true’,‘false’, ‘read’,‘write’. 该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为’true’或支持的scope值,则会跳过用户Approve的页面, 直接授权. 该字段与 trusted 有类似的功能, 是 spring-security-oauth2 的 2.0 版本后添加的新属性. 在项目中,主要操作oauth_client_details表的类是JdbcClientDetailsService.java, 更多的细节请参考该类. 也可以根据实际的需要,去扩展或修改该类的实现.约束 false/true/read/write'
) comment '客户端信息'
ENGINE = InnoDB
DEFAULT CHARSET = utf8;

Drop table if exists oauth_access_token;
create table oauth_access_token
(
token_id VARCHAR(256) null comment '该字段的值是将access_token的值通过MD5加密后存储的.',
token blob null comment '存储将 OAuth2AccessToken.java 对象序列化后的二进制数据, 是真实的AccessToken的数据值.',
authentication_id VARCHAR(256) not null comment '该字段具有唯一性, 其值是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultAuthenticationKeyGenerator.java类.' PRIMARY KEY,
user_name VARCHAR(256) null comment '登录时的用户名, 若客户端没有用户名(如grant_type=“client_credentials”),则该值等于client_id',
client_id VARCHAR(256) null comment '用于唯一标识每一个客户端(client);注册时必须填写(也可以服务端自动生成),这个字段是必须的,实际应用也有叫app_key',
authentication blob null comment '存储将 OAuth2Authentication.java 对象序列化后的二进制数据.',
refresh_token VARCHAR(256) null comment '该字段的值是将refresh_token的值通过MD5加密后存储的. 在项目中,主要操作oauth_access_token表的对象是JdbcTokenStore.java. 更多的细节请参考该类.'
) comment '' ENGINE = InnoDB
DEFAULT CHARSET = utf8;

Drop table if exists oauth_refresh_token;
create table oauth_refresh_token
(
token_id VARCHAR(256) null comment '该字段的值是将refresh_token的值通过MD5加密后存储的.',
token blob null comment '存储将 OAuth2RefreshToken.java 对象序列化后的二进制数据.',
authentication blob null comment '存储将 OAuth2Authentication.java 对象序列化后的二进制数据.'
) comment '在项目中,主要操作oauth_refresh_token表的对象是JdbcTokenStore.java. (与操作oauth_access_token表的对象一样);更多的细节请参考该类.
如果客户端的grant_type不支持refresh_token,则不会使用该表.'
ENGINE = InnoDB
DEFAULT CHARSET = utf8;


Drop table if exists oauth_code;
create table oauth_code
(
code VARCHAR(256) null comment '存储服务端系统生成的code的值(未加密).',
authentication blob null comment '存储将 AuthorizationRequestHolder.java 对象序列化后的二进制数据.'
) comment '在项目中,主要操作oauth_code表的对象是JdbcAuthorizationCodeServices.java. 更多的细节请参考该类.
只有当grant_type为"authorization_code"时,该表中才会有数据产生; 其他的grant_type没有使用该表.'
ENGINE = InnoDB
DEFAULT CHARSET = utf8;

access_tokenrefresh_token 区别

access_token 是每次请求的凭证,而 refresh_token是获取access_token的凭证,当access_token过期时,就需要refresh_token去获取新的access_token

继承 AuthorizationServerConfigurerAdapter(授权服务器配置)

继承AuthorizationServerConfigurerAdapter之后可以自定义一些配置,比如:

  1. approval store
  2. authorization code services 授权码服务(保存到内存或者数据库)
  3. client details service 客户端信息存储服务

释义

注解

@FrameworkEndpoint

@Controller的同义词,但仅用于框架提供的端点(因此它永远不会与用@Controller定义的用户自己的端点冲突)。

@SessionAttributes

只能作用在类上。

@ModelAttribute注解作用在方法上或者方法的参数上,表示将被注解的方法的返回值或者是被注解的参数作为Model的属性加入到Model中,然后Spring框架自会将这个Model传递给ViewResolverModel的生命周期只有一个http请求的处理过程,请求处理完后,Model就销毁了。

如果想让参数在多个请求间共享,那么可以用到要说到的@SessionAttribute注解

例如

1
2
3
4
5
6
7
8
9
@Controller
@SessionAttributes("name")
public class SessionController {
@RequestMapping("session")
public String sessions(Model model,HttpSession session){
model.addAttribute("name", "name");
session.setAttribute("myName", "myName");
return "session";
}

上面的代码将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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package org.springframework.security.oauth2.provider.endpoint;
// ********************8
/**
* <p>
* 实现来自OAuth2规范的授权端点。接受授权请求,并且
* 理授权类型为授权码的用户批准。
* {@link TokenEndpoint Token Endpoint}, except in the implicit grant type (where they come from the Authorization
* Endpoint via <code>response_type=token</code>.
* </p>
*
* <p>
* 理授权类型为授权码的用户批准。
* 理授权类型为授权码的用户批准。
* </p>
*
* @deprecated See the <a href="https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide">OAuth 2.0 Migration Guide</a> for Spring Security 5.
*/
@FrameworkEndpoint
@SessionAttributes({AuthorizationEndpoint.AUTHORIZATION_REQUEST_ATTR_NAME, AuthorizationEndpoint.ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME})
@Deprecated
public class AuthorizationEndpoint extends AbstractEndpoint {
static final String AUTHORIZATION_REQUEST_ATTR_NAME = "authorizationRequest";

static final String ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME = "org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.ORIGINAL_AUTHORIZATION_REQUEST";

private AuthorizationCodeServices authorizationCodeServices = new InMemoryAuthorizationCodeServices();

private RedirectResolver redirectResolver = new DefaultRedirectResolver();

private UserApprovalHandler userApprovalHandler = new DefaultUserApprovalHandler();

private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore();

private OAuth2RequestValidator oauth2RequestValidator = new DefaultOAuth2RequestValidator();

private String userApprovalPage = "forward:/oauth/confirm_access";

private String errorPage = "forward:/oauth/error";

private Object implicitLock = new Object();

public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) {
this.sessionAttributeStore = sessionAttributeStore;
}

public void setErrorPage(String errorPage) {
this.errorPage = errorPage;
}
//oauth/authorize这个请求只支持授权码code模式和Implicit隐式模式
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {

// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
// parameters map will be stored without change in the AuthorizationRequest object once it is created.
// 通过Oauth2RequestFactory构建AuthorizationRequest
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

Set<String> responseTypes = authorizationRequest.getResponseTypes();
//oauth/authorize这个请求只支持授权码code模式和Implicit隐式模式
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}

if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}

try {
//Oauth2授权的第一步就是要确保用户是否已经登陆,然后才会
//这里体现的是SecurityContext中是否包涵了已经授权的Authentication身份
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
//通过ClientDetailsService检索ClientDetails
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());

// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
//确保requst中有重定向redirect_uri
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);

// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
// 校验client请求的是一组有效的scope,通过比对表oauth_client_details
oauth2RequestValidator.validateScope(authorizationRequest, client);

// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
//预同意处理(ApprovalStoreUserApprovalHandler)
//1. 校验所有的scope是否已经全部是自动同意授权,如果全部自动授权同意,则设置authorizationRequest
//中属性approved为true,否则走2
//2. 查询client_id下所有oauth_approvals,校验在有效时间内Scope授权的情况,如果在有效时间内Scope授权全部同意,
//则设置authorizationRequest中属性approved为true,否则为false
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
// 这里待优化
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);

// Validation is all done, so we can check for auto approval...
// 如果预授权结果是同意,直接将code重定向到redirect_uri
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}

// Store authorizationRequest AND an immutable Map of authorizationRequest in session
// which will be used to validate against in approveOrDeny()
model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));

//否则跳转到授权页面
//授权页面是由WhitelabelApprovalEndpoint类生成的
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}

}
// ***其它方式大同小异
}

AuthorizationEndpoint中采用两种常用的UserApprovalHandler策略,一种是细精粒度的基于ScopeApprovalStoreUserApprovalHandler,一种是粗粒度的基于tokenTokenStoreUserApprovalHandler。两种策略不同之处就是在授权页面上,基于ScopeApprovalStoreUserApprovalHandler策略需要为每一个Scope授权同意,而粗粒度的基于tokenTokenStoreUserApprovalHandler因为是基于token的,所以体现在页面上的只有一个同意或者拒绝的按钮来表示是否对用户的操作进行授权。

本文地址: https://github.com/maxzhao-it/blog/post/38356/