SpringBoot-OAuth2-JWT-AuthServer
摘要
- 通过本文,你将知道如何搭建一个基于SpringBoot-OAuth2-JWT的认证服务器
- 本文基于springboot:2.4.0,项目基于Gradle-6.6.1构建
- 代码地址:https://github.com/hanqunfeng/springbootchapter/tree/master/chapter48
依赖
1 | implementation 'org.springframework.boot:spring-boot-starter-web' |
代码说明
SecurityConfig
-
用于配置可以登录系统的用户及其权限,也就是认证服务器的用户
1 | package com.example.oauth2authserverdemo.config; |
CustomSecurityProperties
-
自定义属性配置类
1 |
|
-
配置文件:
1 | #springsecurity 自定义属性 |
AuthServerConfig
-
认证服务器的token关联,本例基于jwt
-
注意这里定义的是抽象类,还没有声明客户端的配置,具体参加下文说明
1 | package com.example.oauth2authserverdemo.config; |
JwtTokenConfig
-
本例基于JWTToken
-
支持两种密钥策略,对称密钥和基于jks证书的非对称密钥
1 | package com.example.oauth2authserverdemo.config; |
JWTokenEnhancer
-
jwt自定义属性
1 | package com.example.oauth2authserverdemo.security.jwt; |
JwtTokenProperties
-
自定义的jwt相关属性类
1 | package com.example.oauth2authserverdemo.security.jwt; |
-
配置文件:
1 | #自定义jwt属性信息 |
客户端配置
-
Oauth2支持两种客户端配置方式,基于内存和基于数据库
AuthServerConfigByMemory
-
基于内存
1 | package com.example.oauth2authserverdemo.config; |
AuthServerConfigByJDBC
-
基于数据库
1 | package com.example.oauth2authserverdemo.config; |
-
为了支持jdbc需要引入依赖
1 | //mysql |
-
配置文件
1 | spring: |
-
数据表
1 | -- oauth2中规定的数据表,需要手动创建,一般项目中提供服务接口插入,参数由用户定义,在请求时会自动查询服务器中对应的参数数据匹配认证 |
-
为了方便在内存和jdbc中切换,增加了自定义属性
1 | #oauth2的client信息是基于内存还是基于数据库,如果不配置,默认为基于内存 |
jks证书创建
生成jks证书
1 | # 默认证书有效期3个月 |
导出公钥
1 | $ keytool -list -rfc --keystore oauth2_key.jks | openssl x509 -inform pem -pubkey |
-
这段就是公钥,保存到文件即可
1 | -----BEGIN PUBLIC KEY----- |
查看密钥信息
1 | $ keytool -list -v -keystore oauth2_key.jks |
OAuth2支持获取access_token的方式
-
authorization_code 验证码模式
-
implicit 隐式模式
-
password 密码模式
-
client_credentials 客户端模式
-
refresh_token 刷新token模式
Oauth2提供的默认端点(endpoints)
1 | /oauth/authorize:授权端点 AuthorizationEndpoint 按照OAuth 2.0规范的授权实现 |
验证码模式–authorization_code,最常用的模式
-
获取验证码
1 | 浏览器GET |
* 1.1 参数说明
1
2
3
4
response_type=code #授权码认证,固定值
client_id=postman #客户端id
redirect_uri=http://localhost:8080/redirect #重定向url,这个值要与配置的值一致,配置这个值时可以配置一个不存在的路径
scope=any 可选参数,指定请求范围,如果不希望跳转到确认页面而是直接通过,需要在代码中配置autoApprove(true)或者autoApprove("any")
* 1.2 跳转到登录页面,使用`admin/123456`登录
* 1.3 登录成功后跳转到认证确认页面(如果不希望跳转到确认页面而是直接通过,需要在代码中配置autoApprove(true)),点击`Authorize`按钮,页面会跳转到`http://localhost:8080/redirect?code=7ak4gI`
* 1.4 复制code的值进入第二步
-
获取access_token和refresh_token
1 | POST |
* 2.1 code存在有效期,且只能使用一次,所以这个请求只能使用一次,参数说明
1
2
3
4
5
grant_type=authorization_code #验证code并获取access_token,固定值
client_id=postman #客户端id
client_secret=postman #客户端密码
redirect_uri=http://localhost:8080/redirect #重定向url
code=7ak4gI #上一步中获取到的code值
* 2.2 响应结果如下,默认access_token有效期12小时,refresh_token有效期30天
1
2
3
4
5
6
7
8
9
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDYxMzI3MywiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJhNzFhMDJhYy02NzgyLTRiODEtOGJiMi1mMGI3ZWVkODk2YTgiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.SKl-0KbdVvv3acqLlXre66PX-fFIruTLwOkCO3peby0",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6ImE3MWEwMmFjLTY3ODItNGI4MS04YmIyLWYwYjdlZWQ4OTZhOCIsImV4cCI6MTYwNzE2MjA3MywiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI5NzRjMDRkOS03NTQ0LTRjZTEtODMzZS05NjlmODMyNmRiNmQiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.vy16MpRikSFFz_n2zJEgI5Cfy1APNDrvnuP7Sj1jqJU",
"expires_in": 43199,
"scope": "any",
"jwt-ext": "JWT 扩展信息",
"jti": "a71a02ac-6782-4b81-8bb2-f0b7eed896a8"
}
* 2.3 复制refresh_token的值进入第三步
* 2.4 请求时支持base认证方式,详见3.3
-
通过refresh_token获取新的access_token
1 | POST |
* 3.1 参数说明,这个就是refresh_token模式,需要先获取到上一次的refresh_token
1
2
3
4
grant_type=refresh_token #刷新token的参数,固定值
client_id=postman #客户端id
client_secret=postman #客户端密码
refresh_token=xxx #上一步中获取的refresh_token值
* 3.2 响应结果如下,获取到新的access_token和refresh_token,注意保存,access_token过期时,可以通过该请求重新获得新的access_token
1
2
3
4
5
6
7
8
9
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDYxMzc4NSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI5YWQ1MTBhNy00MzQyLTQ5M2UtYjQyMS1iNzNmMDU2NmU3OWYiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.L0TPZ-NDzT4hSSivQ0WR-5rPz6xoLbC_HmdvvKOzP_Y",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6IjlhZDUxMGE3LTQzNDItNDkzZS1iNDIxLWI3M2YwNTY2ZTc5ZiIsImV4cCI6MTYwNzE2MjA3MywiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI5NzRjMDRkOS03NTQ0LTRjZTEtODMzZS05NjlmODMyNmRiNmQiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.ekAST3MRc0PqW3FZgnFQv1g-HykCqA4_TuzLRTplCIM",
"expires_in": 43199,
"scope": "any",
"jwt-ext": "JWT 扩展信息",
"jti": "9ad510a7-4342-493e-b421-b73f0566e79f"
}
* 3.3 请求时支持base认证方式
1
2
# 将client_id和client_secret放到url的前面
POST http://postman:postman@localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6ImM4NWM0YjJlLTBlNTktNDJhYy05M2RlLWI2N2I0OWI1ZDU5OSIsImV4cCI6MTYwNzIyMDAxMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI2M2U2MGYxOS1iOTU1LTRlMmUtODk2Yy1mYWUxN2IyODUzMzkiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.jVZWYRjr4VwTYwbKXP-sav_08vwc8iAxzEbg0I_jVTw
或者在header中增加`Authorization`请求头,值为`Basic xxxxxxxxx`,`xxxxxxxxx`是通过`base64.encode(client_id + 空格 + client_secret)`得到。
使用Postman接口测试工具时,也可以使用其提供的认证功能[Authorization-->TYPE-->Basic Auth],直接填写用户名和密码就可以方便进行测试,注意这里用户名和密码是client_id 和 client_secret。
1
2
3
4
5
6
7
8
9
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDY4NTE3NiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJhNDhiNzZhNC1kNWM0LTRkNTYtYmQ2Yy0zOWYxODQ0MGYwNTMiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.mhNUhWk1M9p267bOsv3fYXnLGeXitvnny6mUT9FyNdw",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6ImE0OGI3NmE0LWQ1YzQtNGQ1Ni1iZDZjLTM5ZjE4NDQwZjA1MyIsImV4cCI6MTYwNzIyMDAxMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI2M2U2MGYxOS1iOTU1LTRlMmUtODk2Yy1mYWUxN2IyODUzMzkiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.DT_cBdcow7pDv_S9RTHknKLHxZAIT45Byw1Rdw93K8U",
"expires_in": 43199,
"scope": "any",
"jwt-ext": "JWT 扩展信息",
"jti": "a48b76a4-d5c4-4d56-bd6c-39f18440f053"
}
* 3.4 access_token信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//HEADER
{
"alg": "HS256",
"typ": "JWT"
}
//PAYLOAD
{
"user_name": "admin",
"jwt-ext": "JWT 扩展信息",
"scope": [
"any"
],
"exp": 1604613785,
"authorities": [
"ROLE_admin"
],
"jti": "9ad510a7-4342-493e-b421-b73f0566e79f",
"client_id": "postman"
}
implicit模式
-
仅可获取access_token,不能获取refresh_token
-
跳转到登录页面
1 | 浏览器GET |
-
然后登录,然后同意授权
-
返回结果直接拼接在redirect_uri的后面,因为是以
#
开头,所以服务器端无法收到数据,只能从浏览器中获取了1
http://localhost:8080/redirect#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDY3NTMwNSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI4ZDcyODM5NC0zODhjLTRiMjAtYjgwYy1jNTk4OWJjZmJlM2IiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.OStAhkT6fBoNLdF7vTaztwhfNV5J8K1MpE1A7pxbyCs&token_type=bearer&expires_in=43199&scope=any&jwt-ext=JWT%20%E6%89%A9%E5%B1%95%E4%BF%A1%E6%81%AF&jti=8d728394-388c-4b20-b80c-c5989bcfbe3b
-
access_token信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//HEADER
{
"alg": "HS256",
"typ": "JWT"
}
//PAYLOAD
{
"user_name": "admin",
"jwt-ext": "JWT 扩展信息",
"scope": [
"any"
],
"exp": 1604675305,
"authorities": [
"ROLE_admin"
],
"jti": "8d728394-388c-4b20-b80c-c5989bcfbe3b",
"client_id": "postman"
}
password 模式
-
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
-
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
-
获取access_token和refresh_token
1 | POST |
-
请求时支持base认证方式,认证用户名和密码是client_id 和 client_secret
-
grant_type=password,需要携带用户的用户名和密码
-
返回结果
1
2
3
4
5
6
7
8
9{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDY3NzU1MiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJlNjZiMWFjOS04M2RmLTRiNTMtYjNmYS1mMTQyYzA2MTYzMjkiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.jfBeoA-AeNaMUSWHKBktN8IOFbGJHtQSIQUgxP8zzg0",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6ImU2NmIxYWM5LTgzZGYtNGI1My1iM2ZhLWYxNDJjMDYxNjMyOSIsImV4cCI6MTYwNzIyNjM1MiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI5MDUwNTQ0Yi00OGVmLTQ2OWMtYjM3OS1lMWQyZDViOTcyYjkiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.fGDixRXD68cYdRTjprH3TM7gAGmFqDAaKxs3RTxphMs",
"expires_in": 43199,
"scope": "any",
"jwt-ext": "JWT 扩展信息",
"jti": "e66b1ac9-83df-4b53-b3fa-f142c0616329"
} -
access_token信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//HEADER
{
"alg": "HS256",
"typ": "JWT"
}
//PAYLOAD
{
"user_name": "admin",
"jwt-ext": "JWT 扩展信息",
"scope": [
"any"
],
"exp": 1604677552,
"authorities": [
"ROLE_admin"
],
"jti": "e66b1ac9-83df-4b53-b3fa-f142c0616329",
"client_id": "postman"
}
client_credentials 客户端模式
-
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。
-
获取access_token
1 | POST |
-
不需要用户的用户名和密码,只是认证客户端是否有效,返回的access_token的payload中也不包含任何用户信息(user_name,authorities)
-
没有refresh_token
-
返回结果
1
2
3
4
5
6
7
8{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqd3QtZXh0IjoiSldUIOaJqeWxleS_oeaBryIsInNjb3BlIjpbImFueSJdLCJleHAiOjE2MDQ2Nzc4NjksImp0aSI6ImU3ZjJhYTEyLTFhMWUtNGFmMC04MjJkLTkxNzg5NmYyMGMwMyIsImNsaWVudF9pZCI6InBvc3RtYW4ifQ.OHy0IUGf9KSmCDKlqq1IZ8bICAhBHFtpazwK1gcbCOI",
"token_type": "bearer",
"expires_in": 43199,
"scope": "any",
"jwt-ext": "JWT 扩展信息",
"jti": "e7f2aa12-1a1e-4af0-822d-917896f20c03"
} -
access_token信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//HEADER
{
"alg": "HS256",
"typ": "JWT"
}
//PAYLOAD
{
"jwt-ext": "JWT 扩展信息",
"scope": [
"any"
],
"exp": 1604677869,
"jti": "e7f2aa12-1a1e-4af0-822d-917896f20c03",
"client_id": "postman"
}
refresh_token模式,见验证码模式第3部分
通过已有的refresh_token获取新的access_token和refresh_token
验证token有效性–Basic Auth
1 | POST http://postman:postman@localhost:8080/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55IiwiYWxsIl0sImV4cCI6MTYwNDcxNjMyMCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiIxNjc4ZTNhYy0yYmNjLTQ4NGUtOTBkMy02ZWFjYTVkNzkyM2IiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.8piTJdoAf-4lB0Tn-yeSFWoV3WpkJkqBs7AsoNPoohw |
或者加上Header参数Authorization
值为 Basic cG9zdG1hbjpwb3N0bWFu
返回值
1 | { |
获取公钥,比如JWT的密钥
1 | GET http://postman:postman@localhost:8080/oauth/token_key |
或者加上Header参数Authorization
值为 Basic cG9zdG1hbjpwb3N0bWFu
返回值
1 | { |
自定义登录和授权页面
依赖
1 | implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' |
OAuth2Controller
1 | package com.example.oauth2authserverdemo.controller; |
login.html
-
登录页面
1 |
|
confirm_access.html
-
授权页面
1 |
|
SecurityConfig中开启自定义登录页配置
1 | http.formLogin() |
后记
至此,一个基于SpringBoot-OAuth2-JWT的认证服务器就搭建好了,如果client-server和resource-server都是基于spring-security-oauth2搭建的,则认证服务器就不需要其它配置了,client-server和resource-server的代码可以参考
oauth2-client-demo和oauth2-resource-server-demo,不过spring-security-oauth2已经不再维护了,所以建议搭建client-server和resource-server的时候,还是使用springboot官方提供的spring-boot-starter-oauth2-client
和spring-boot-starter-oauth2-resource-server
,此时还需要为认证服务增加一些功能,这部分留到后面讲解client-server和resource-server的搭建的时候再说。