theme: v-green
OAuth2简介
是什么?
是目前最流行的授权机制,用来授权第三方应用,获取用户数据。
简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
qq授权登录, 微信授权登录, 微博授权登录, github授权登录等
以上这是都是它的落地实现
举个例子:
用户想通过QQ登录头条,那么头条就需要qq的账号密码, 这是不行的, 所以需要另外一种授权机制, 在qq的网站上登录, 然后告知头条部分数据(至少告知头条我这里登录成功了token
), 这样头条就根据qq授权返回的信息登录头条, 等到下次, 再次访问头条后, 头条使用token
代替qq的账号密码方式登录(一般不需要再次登录了, 只要验证token
过期了没有)
这里讲到了
token
,token
一般是有过期时间的, 并且通过token
能够拿到权限列表
令牌与密码的区别
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
(1)**令牌是短期的,到期会自动失效,用户自己无法修改。**密码一般长期有效,用户不修改,就不会发生变化。
(2)**令牌可以被数据所有者撤销,会立即失效。**以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。
(3)**令牌有权限范围(scope),比如只能进小区的二号门。**对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。这就是 OAuth 2.0 的优点。
注意,只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因。
OAuth 2.0 对于如何颁发令牌的细节,规定得非常详细。具体来说,一共分成四种授权类型(authorization grant),即四种颁发令牌的方式,适用于不同的互联网场景。
所以也需要关注到token的安全问题, 不要泄漏了, 不然就危险了
OAuth 的核心就是向第三方应用颁发令牌。
四种授权模式
- 授权码(authorization-code)
- 隐藏式(implicit)
- 密码式(password)
- 客户端凭证(client credentials)
不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:
客户端 ID(client ID)
和客户端密钥(client secret)
。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
如果你做过微信授权登录一定会熟悉这个流程
第一种授权方式:授权码
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
下面以博客使用qq授权登录的过程为例
里面的qq网址不对应实际qq网址
下面才是qq授权的实际网址
https://graph.qq.com/oauth2.0/show?which=Login&display=pc&response_type=code&client_id=112233445&redirect_uri=https%3A%2F%2Fwww.52pojie.cn%2Fconnect.php%3Fmod%3Dlogin%26op%3Dcallback%26referer%3Dindex.php&state=6d9aaa4d7b6777779d1a36b76c2ddddd&scope=get_user_info%2Cadd_share%2Cadd_t%2Cadd_pic_t%2Cget_repost_list
-
博客提供一个网址给用户, 网址内容:
-
https://qq.com/oauth/authorize? response_type=code& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
-
response_type
: 告知服务端返回类型, 只能是code
或者token
, 如果是code
表明采用授权码认证模式 -
client_id
:因为认证服务器要知道是哪一个应用在请求授权,所以client_id
就是认证服务器给每个应用分配的id -
scope
:需要获得哪些授权,这个参数的值是由服务提供商定义的,不能随意填写, 这是只是只读 -
redirect_uri
:重定向地址, 这里是我们博客的地址, 而且会在博客地址后面添加授权码,让博客获取code
授权码, 所以这里应该填写我们博客获得code
的请求地址, 假设地址是:-
https://blog.com/callback?code=到时候qq那边给你填写code的具体值
-
-
-
用户使用qq的账号密码在qq的授权网址上登录
-
qq授权用户返回
code
代码, 添加到上面的redirect_uri
地址后面https://blog.com/callback?code=AUTHORIZATION_CODE , 然后重定向到我们的博客网站上面 -
博客服务器收到 code 授权码后, 请求新的请求去拿到
token
-
https://qq.com/oauth/token? client_id=CLIENT_ID& client_secret=CLIENT_SECRET& grant_type=authorization_code& code=AUTHORIZATION_CODE& redirect_uri=CALLBACK_URL
-
博客带着在qq上申请到的
client_id
和client_secret
,client_secret
就是认证服务器给client_id
分配的密钥, 让认证服务器知道是我们博客正在接收认证 -
grant_type
:授权类型,授权码模式默认为authorization_code
-
code
:授权码(一次失效), 前一次请求获得的code
, 这次带上code
去拿到token
-
redirect_uri
: 还是回调地址, 这次给你传递的token
以及一些失效信息等数据-
{ "access_token":"ACCESS_TOKEN", "token_type":"bearer", "expires_in":2592000, "refresh_token":"REFRESH_TOKEN", "scope":"read", "uid":100101 }
-
-
上面的
client_id
和client_secret
都需要去qq那边申请, 那边可能把client_id
叫做AppId
等获得授权
code
和token
这两地址中可能还有其他参数, 比如csrf
哪种校验码等第二步获得
token
的过程, 是客户端在后台自动发送的, 对用户不可见
第二种方式:隐藏式(简化模式)
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)。
现在程序员搭建博客网站都流行静态网站,例如Hexo、Jekyll等,这类技术栈有一个共同的特点就是没有后端,开发者不需要将精力放在维护后端网站上,只需要专注于内容创作即可。
流程
-
博客网站上有一个 github 的授权登录按钮, 地址
-
https://github.com/oauth/authorize? response_type=token& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read& _csrf=111
-
基本上和授权码方式一致, 除了
response_type=token
, 表示直接从授权服务器直接拿Access token
, 少了授权码的过程
-
-
用户点击按钮, 跳转到 github, 让用户登录授权, 接着会跳转到
redirect_uri
的地址, 地址详情大概是这样-
https://blog.com/callback#access_token=xxxxxxxxxxxx&token_type=bearer&expires_in=9999&_csrf=xxx
-
令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。
-
-
接下来客户端向资源服务器发送请求,这个请求不需要携带令牌,资源服务器返回一段JS脚本,客户端执行JS脚本,提取出令牌Access Token。
这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
第三种方式:密码式
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
密码模式的流程比较简单:
-
首先用户在第三方应用的登录页面输入登录凭证(用户名/密码)
-
https://oauth.qq.com/token? grant_type=password& username=USERNAME& password=PASSWORD& client_id=CLIENT_ID
-
-
第三方应用将用户的登录信息发送给授权服务器,获取到令牌Access Token
-
授权服务器检查用户的登录信息,如果没有问题,授权服务器以JSON的格式发送令牌Access Token给第三方应用
第四种方式:凭证式
最后一种方式是凭证式(client credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。
流程:
-
客户端发送一个请求到授权服务器
-
https://oauth.b.com/token? grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET
-
-
授权服务器验证通过后,会直接返回Access Token给客户端
这种方式给出的令牌,是针对客户端使用的,而不是针对用户,即有可能多个用户共享同一个令牌。
Gitee授权登录
说句实话,国内不建议使用
github
学习OAuth
授权认证, 因为墙问题, 需要梯子如果你没没有使用梯子。而是使用fast git或者是别的这种工具。那么可能存在
SSL
安全问题。这一点值得注意。推荐国内使用码云。
别问我怎么知道的哦!!!
Gitee
网站上配置
从这个地方入手, 配置好
项目配置
导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
@RestController
@Slf4j
public class HelloController {
@GetMapping("hello")
public String hello(Principal principal) {
return "hello, " + principal.getName();
}
}
spring:
security:
user:
name: zhazha
password: '{noop}123456'
oauth2:
client:
registration:
gitee:
client-name: Gitee
client-id: b9xxxxxxxxxxxxxxxxxxxxxxx
client-secret: 61xxxxxxxxxxxxxxxxxxx
authorization-grant-type: authorization_code
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
scope:
- user_info
github:
client-id: efxxxxxxxxxxxxxxxx
client-secret: 20xxxxxxxxxxxxxxxxxxxxxxxxxx
provider:
gitee:
authorization-uri: https://gitee.com/oauth/authorize
token-uri: https://gitee.com/oauth/token
user-info-uri: https://gitee.com/api/v5/user
user-name-attribute: name
上面这些
gitee
地址可以从gitee
网址拿到:
这里有很多相关配置:
spring.security.oauth2.client.registration.[registrationId]
registrationId
spring.security.oauth2.client.registration.[registrationId].client-id
clientId
spring.security.oauth2.client.registration.[registrationId].client-secret
clientSecret
spring.security.oauth2.client.registration.[registrationId].client-authentication-method
clientAuthenticationMethod
spring.security.oauth2.client.registration.[registrationId].authorization-grant-type
authorizationGrantType
spring.security.oauth2.client.registration.[registrationId].redirect-uri
redirectUri
spring.security.oauth2.client.registration.[registrationId].scope
scopes
spring.security.oauth2.client.registration.[registrationId].client-name
clientName
spring.security.oauth2.client.provider.[providerId].authorization-uri
providerDetails.authorizationUri
spring.security.oauth2.client.provider.[providerId].token-uri
providerDetails.tokenUri
spring.security.oauth2.client.provider.[providerId].jwk-set-uri
providerDetails.jwkSetUri
spring.security.oauth2.client.provider.[providerId].issuer-uri
providerDetails.issuerUri
spring.security.oauth2.client.provider.[providerId].user-info-uri
providerDetails.userInfoEndpoint.uri
spring.security.oauth2.client.provider.[providerId].user-info-authentication-method
providerDetails.userInfoEndpoint.authenticationMethod
spring.security.oauth2.client.provider.[providerId].user-name-attribute
providerDetails.userInfoEndpoint.userNameAttributeName
当你在看官方案例或者网上其他案例时会发现他们可以直接使用
github
的配置, 为什么呢?因为他有默认配置
CommonOAuth2Provider
, 配置了GITHUB
,OKTA
GITHUB { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, DEFAULT_REDIRECT_URL); builder.scope("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; }
直接访问 任意地址 会重定向到 gitee
地址:
https://gitee.com/login?redirect_to_url=https%3A%2F%2Fgitee.com%2Foauth%2Fauthorize%3Fresponse_type%3Dcode%26client_id%3Db99c3b81431764f6402e8a7ef90d15da5ccb1c657189bf4341b03f85c781082a%26scope%3Duser_info%26state%3DXOo_-t69onp4wXJLzNLQII9vtnhpXhTFIEgtZmKQGkI%253D%26redirect_uri%3Dhttp%3A%2F%2Flocalhost%3A8080%2Fcallback
踩坑: 如果你设置了
FastGit
之类的工具, 可能导致Github SSL
报错, 在认证的时候需要关闭改功能
底层都做了什么?
首先回忆下OAuth2
认证的过程, 大体上是跳转, 获取code, 获取token
- 跳转, 用户点击使用
gitee
登录, 浏览器跳转到gitee OAuth
网址 - 获取
code
,
在第一个过程中, 客户端发送了三个参数, 然后授权服务器验证成功借助客户端发送的redirect_uri携带 code 参数返回给客户端
第二步将上面几个参数一起传递给授权服务器, 其中client-secret
参数和client-id
参数需要事先去授权服务器创建第三方APP
一般这个请求由客户端内部自主请求, 用户不会在浏览器看到请求
access_token
的过程
重定向过程
小白: “这里的源码好像很复杂, 我要怎么分析?”
小黑: “分析源代码不能盲目查看,而是先要确定目标。你可以给出一些目标。”
小白: "
OAuth
认证也是认证的一个过程。在最终也是需要返回Authentication
, 但是OAuth
认证的authentication
有一些独特的地方。"小白: “比如说获取用户名。这里面
OAuth
认证里面所有的用户信息全部都是从各个不同的支持OAuth
认证的授权服务器上获取的。所以在authentication
里面可能会存在map
结构。这个map
这个说白了就是各个授权服务器上的用户信息。”小白: “不同的授权服务器有不同的结构。这个map结构其实是一个妥协”
小白: “紧接着由于是
OAuth
认证,所以它有独特的access token
机制和refresh token
机制。所以我们需要有另外一个地方去保存这两个属性。最好是数据库等等。”小白: “所以可以很明显的发现,我们需要如下步骤。”
小白: “第1个步骤是想办法从授权服务器获得
code
代码。这个步骤的目的主要是让服务器知道,你拥有获取access token
和refresh token
的权限, 当然这中间还需要而另外两个属性clientId
跟clientSecret
”小白: “第2步是想办法从授权服务器拿到
access token
和refresh token
.(有些授权服务器没有refresh token
不过没有关系。), 这个步骤主要是让 Spring security 知道自己已经认证,并且如果在access token
过期时可以借助refresh token,刷新access token
。”小白: “第3步是想办法从授权服务器拿到用户信息。这个步骤主要就是这个用于存储
SecurityContextHolder
上下文的。”
小黑: “我们配置
oauth
认证的整个过程。首先我们需要在记载上配置好我们的网页地址和回调地址。并配置好需要提供的权限。这一部基本上没有什么源码分析。”小黑: “接着我们的最终目的是什么?是拿到服务端或者说是授权服务器的
token
。如果是OAuth
的授权码模式的话。过程比较复杂。需要clientId
跟clientSecret
。”小黑: “我们的第1步是想要拿到code。也就是授权服务器提供的一个code代码。”
小黑: “那我们在我们的服务器端就要组合一个授权端的网页地址。服务端通过这个地址就能够拿到授权端的code代码。”
小白: “那授权端凭什么平白无故的给你一段code代码,让你去访问他的资源呢?”
小黑: "所以服务端需要事先去授权端注册登录并拿到两个编码
clientId
跟clientSecret
, 这个编码的作用是告知授权端我的身份,我到底是谁? "
小黑: “有了这code编码基本上是完成了80%。现在有一个问题就是授权端怎么给你提供这个code编码?”
小黑: "
OAuth
给出的方案是服务端给出一个重定向地址。让授权端回调,这个重定向地址并添加上code编码。在我们的服务端基本上达到了授权端的code编码了,这一步基本上完成。"小白: “那对应着源码的哪一段呢?”
小黑: “过程其实也很简单, spring security提供了两个过滤器,一个过滤器用于重定向
OAuth2AuthorizationRequestRedirectFilter
,另一个过滤器用于登录OAuth2LoginAuthenticationFilter
。”小黑: “相对应的就是重定向和登录两个部分, 重定向非常简单。”
小黑: “从
application
的配置类上读取到从定向的URL。然后填充一下前面的http://localhost:8080
和后面的{registrationId}
。就完成重定向地址的组合了。”
'{baseUrl}/login/oauth2/code/{registrationId}'
http://localhost:8080/login/oauth2/code/gitee
小黑: “从地下地址已经组合完成了,接下来组合的是
gitee
地址。拿到下面的地址。”
小黑: “将它组合成一个完整的地址,这些参数都是可以拿得到的。”
https://gitee.com/oauth/authorize?response_type=code&client_id=121212121&scope=user_info&state=3333333%3D&redirect_uri=http://localhost:8080/login/oauth2/code/gitee
小黑: “紧接着应该就是重定向过程。”
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
// 重定向
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
小黑: “从定向型还往
session
里面保存了一些数据。这段数据在获取code代码的之后就会被删除。说白了就是保存OAuth2AuthorizationRequest
和删除OAuth2AuthorizationRequest
,整个过程就在这里。”
HttpSessionOAuth2AuthorizationRequestRepository:
key = org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository.AUTHORIZATION_REQUEST
value = OAuth2AuthorizationRequest(的值)
小黑: “到这里的话,第一是重定向的过程基本上完成了,剩下的就是用户去输入用户名密码获得
code
并组合获取token
地址, 访问获取token
的过程。”
上面源码分析过程看的比较累, 可以直接看下图
获取code并访问获取token和用户信息地址的过程
说白了就是分析
OAuth2LoginAuthenticationFilter
的过程
分为两个流程:
- 生成: spring security生成 获取
code
的gitee
地址- 获取: 登录
gitee
地址获取token
的过程
首先你登陆账户后, gitee
会把code
发送给你, 所以下面流程刚开始就能够从request
中拿到 code
, 接着就是拿着 code
生成一个新的获取 token
的gitee
地址, 然后访问该地址获取 access_token
和 refreshToken
源码还是比较简单的, 下载下来看就行
自定义配置
自定义ClientRegistrationRepository
我们在 gitee
app
上配置的回调地址如果是 http://localhost:8080/callback
那么在本地客户端上也需要配置上这个地址
记得修改地址哦, 前面我们用了
http://localhost:8080/login/oauth2/code/gitee
, 现在改成http://localhost:8080/callback
了
gitee
还能配置多个回调地址:
这个地址相当于我们的登录请求地址, 默认的loginProcessingUrl
是/login/oauth2/code/gitee
默认情况下, http://localhost:8080/callback
请求会被当成普通请求, 只有修改loginProcessingUrl
地址才能确保当前重定向到http://localhost:8080/callback
地址时, 该请求会在AbstractAuthenticationProcessingFilter#doFilter
方法中被认定为登录请求, 进而将请求交给OAuth2LoginAuthenticationFilter#attemptAuthentication
方法去处理, 以完成登录操作.
我们可以自定义ClientRegistrationRepository
:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.loginPage("/login")
.loginProcessingUrl("/callback");
return http.build();
}
/**
* 自定义 ClientRegistrationRepository
*
* @return
*/
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(giteeClientRegistration());
}
private static ClientRegistration giteeClientRegistration() {
return ClientRegistration.withRegistrationId("gitee")
.clientId("b99c3b81431764f6402e8a7ef90d15da5ccb1c657189bf4341b03f85c781082a")
.clientSecret("6176e926f9ce31d1008e8339b6d3fad1849bf3f8124c692355ec6e07efa25241")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.userNameAttributeName("name")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://localhost:8080/callback")
.scope("user_info")
.authorizationUri("https://gitee.com/oauth/authorize")
.tokenUri("https://gitee.com/oauth/token")
.userInfoUri("https://gitee.com/api/v5/user")
.clientName("gitee")
.build();
}
自定义用户
默认情况下, spring security 读取的用户信息都是存储在OAuth2User
我们可以自定义实现一个entity
@Data
public class GiteeOAuth2User implements OAuth2User {
private List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
private Map<String, Object> attributes;
private String id;
private String name;
private String login;
private String avatarUrl;
private String email;
@Override
public <A> A getAttribute(String name) {
return OAuth2User.super.getAttribute(name);
}
@Override
public Map<String, Object> getAttributes() {
if (this.attributes == null) {
this.attributes = new HashMap<>();
this.attributes.put("id", this.getId());
this.attributes.put("name", this.getName());
this.attributes.put("login", this.getLogin());
this.attributes.put("email", this.getEmail());
}
return attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getName() {
return this.name;
}
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated();
http.oauth2Login()
.userInfoEndpoint()
.customUserType(GiteeOAuth2User.class, "gitee") // 已弃用的方法
.and()
.loginProcessingUrl("/callback");
http.oauth2Client();
return http.build();
}
底层根据customUserType
配置的GiteeOAuth2User
, 根据类型判断到底拿那个 OAuth2UserService
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = getOAuth2UserService();
OAuth2LoginAuthenticationProvider oauth2LoginAuthenticationProvider = new OAuth2LoginAuthenticationProvider(
accessTokenResponseClient, oauth2UserService);
上面的代码我们拿到的是CustomUserTypesOAuth2UserService
但你会发现, 走他们的方法会存在 GiteeOAuth2User.avatarUrl
属性是空的
因为借助 RestTemplate
无法转换avatar_url 和 avatarUrl
我们可以自定义:
http.oauth2Login()
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(userRequest -> {
DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = defaultOAuth2UserService.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
String jsonStr = JSONUtil.toJsonStr(attributes);
return JSONUtil.toBean(jsonStr, GiteeOAuth2User.class);
}))
.loginProcessingUrl("/callback")