Oauth2(上)

一熟吏、簡介

1.1 業(yè)務場景

公司原來使用的是自建的用戶登陸系統(tǒng),但是只有登陸功能娶吞,沒有鑒權(quán)功能垒迂。

現(xiàn)公司有如下業(yè)務場景:

  1. 需要接入各大智能音箱,音箱需要通過標準的Oauth2授權(quán)碼模式獲取令牌從而拿到服務器資源妒蛇;
  2. 后臺管理界面需要操作權(quán)限 机断;
  3. 后期要做開發(fā)者平臺,需要授權(quán)碼模式绣夺。

所以在以上業(yè)務場景下開始自建Oauth2框架吏奸,框架需要兼容公司原有的用戶登陸系統(tǒng)。

1.3 Oauth2框架

Oauth2擴展了Security的授權(quán)機制陶耍。

二奋蔚、相關概念

2.1 單點登陸

即一個token可以訪問多個微服務。

2.2 授權(quán)方式

①授權(quán)碼模式

第三方應用通過客戶端進行登錄,如果通過github賬號進行登錄泊碑,那么第三方應用會跳轉(zhuǎn)到github的資源服務器地址坤按,攜帶了client_id、redirect_uri馒过、授權(quán)類型(code模式)和state(防止csrf攻擊的token,可以不填)臭脓。隨后資源服務器會重定向到第三方應用url并攜帶code和state參數(shù),隨后第三方應用攜帶code腹忽、client_id和client_secret再去請求授權(quán)服務器来累,先驗證code是否有效,有效則發(fā)放認證token窘奏,攜帶該token可以取資源服務器上的資源佃扼。

授權(quán)碼模式(authorization code)是功能最完整、流程最嚴密的授權(quán)模式蔼夜,code保證了token的安全性兼耀,即使code被攔截,由于沒有app_secret求冷,也是無法通過code獲得token的瘤运。

如當我們登陸CSDN的時候,可以使用第三方Github賬號密碼進行登陸并獲取頭像等信息匠题。

首先需要注冊CSDN的信息

  • 應用名稱
  • 應用網(wǎng)站
  • 重定向標識 redirect_uri
  • 客戶端標識 client_id
  • 客戶端秘鑰 client_secret

如github認證服務器中可以對客戶端進行注冊拯坟,需要填寫應用名稱、網(wǎng)站地址韭山、應用描述和重定向地址郁季。這樣github就記錄了該應用并產(chǎn)生一個client_id和client_secret。


獲取令牌流程圖如下:

優(yōu)點

  • 不會造成我們的賬號密碼泄漏
  • Token不會暴露給前端瀏覽器

看下測試實例

# 指定授權(quán)方式為code模式钱磅,攜帶客戶端id梦裂、重定向地址等信息訪問。
GET https://oauth.marssenger.com/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_ADMIN&redirect_uri=http://www.baidu.com
# 會跳轉(zhuǎn)到登陸頁面盖淡,輸入賬號密碼年柠。如果信息正常,會攜帶code跳轉(zhuǎn)到重定向地址褪迟。
https://www.baidu.com/?code=YEQCZO
# 然后攜帶code訪問授權(quán)服務器冗恨,就可以獲取到令牌了。
https://oauth.marssenger.com/oauth/token?client_id=c1&client_secret=123456&grant_type=authorization_code&code=YEQCZO&redirect_uri=http://www.baidu.com
# 最終得到令牌如下
{
    "access_token": "ey......Jgw",
    "token_type": "bearer",
    "refresh_token": "ey......J-A",
    "expires_in": 86399,
    "scope": "ROLE_ADMIN",
    "cre": 1622694842,
    "jti": "fd970e49-082f-492e-9418-b21b45452f2d"
}

access_token:訪問令牌味赃,攜帶此令牌訪問資源
token_type:有MAC Token與Bearer Token兩種類型掀抹,兩種的校驗算法不同,RFC 6750建議Oauth2采用 Bearer Token心俗。
refresh_token:刷新令牌傲武,使用此令牌可以延長訪問令牌的過期時間。
expires_in:過期時間,單位為秒谱轨。
scope:范圍,與定義的客戶端范圍一致吠谢。
cre:自定義添加的令牌創(chuàng)建日期
jti: jwt的唯一身份標識土童,主要用來作為一次性token,從而回避重放攻擊。

為什么需要使用code去換取token工坊,而不是直接返回token献汗?

  1. 如果直接獲取token,那么client_secret需要寫在url中王污,這樣容易造成客戶端秘密泄漏罢吃。
  2. 如果重定向地址是http協(xié)議傳輸?shù)模赡軐е耤ode被截獲泄漏昭齐,但是code只能使用一次尿招,所以如果code失效,可以及時發(fā)現(xiàn)被攻擊阱驾。code換取token這一步一般使用的是https協(xié)議就谜,避免被中間人攻擊。

The code exchange step ensures that an attacker isn’t able to intercept the access token, since the access token is always sent via a secure backchannel between the application and the OAuth server.

②簡化模式

第三方應用通過客戶端進行登錄里覆,通過github賬號訪問資源服務器丧荐,認證完成后重定向到redirect_uri并攜帶token,省略了通過授權(quán)碼再去獲取token的過程喧枷。

適用于公開的瀏覽器單頁應用虹统,令牌直接從授權(quán)服務器返回,不支持刷新令牌隧甚,且沒有code安全保證车荔,令牌容易因為被攔截竊聽而泄露。

看下測試實例

# 指定授權(quán)方式為token模式戚扳,攜帶客戶端id夸赫、重定向地址等信息訪問。
GET https://oauth.marssenger.com/oauth/authorize?client_id=c1&response_type=token&scope=ROLE_ADMIN&redirect_uri=http://www.baidu.com
# 直接獲取到了access_token咖城,不支持刷新令牌
https://www.baidu.com/#access_token=ey......u0Q&token_type=bearer&expires_in=86399&cre=1622695736&jti=13a726b2-70d4-421e-8d5b-3a26233214cc

③密碼模式

直接向第三方應用提供資源服務器的賬號密碼茬腿,第三方應用通過賬號密碼請求獲取資源服務器上的資源。會向第三方應用暴露賬號密碼宜雀,除非特別信任該應用切平。

看下測試實例

# 指定授權(quán)方式為password,攜帶客戶端id密碼辐董、用戶賬號密碼等信息訪問悴品。
GET https://oauth.marssenger.com/oauth/token?client_id=c1&client_secret=123456&grant_type=password&username=admin&password=abc123&user_type=admin
# 獲取令牌
{
    "access_token": "ey......_SA",
    "token_type": "bearer",
    "refresh_token": "ey......brw",
    "expires_in": 86399,
    "scope": "ROLE_ADMIN ROLE_APPLICATION",
    "cre": 1622691146,
    "jti": "c31a69bc-0eba-4e93-8f78-c0f8c04a2b11"
}

④客戶端模式

不通過資源所有者,直接以第三方應用的秘鑰和id獲取資源服務器的token。

看下測試實例

# 指定授權(quán)方式為client_credentials苔严,攜帶客戶端id和密碼進行訪問定枷。
GET https://oauth.marssenger.com/oauth/token?client_id=c1&client_secret=123456&grant_type=client_credentials
# 獲取令牌
{
    "access_token": "ey......zMQ",
    "token_type": "bearer",
    "expires_in": 86399,
    "scope": "ROLE_ADMIN ROLE_APPLICATION",
    "cre": 1622697938,
    "jti": "8f962403-c7d8-4d4b-974f-7896a0a31389"
}

2.3 JWT令牌

Oauth2原生的token是一串隨機的hash字符串,存在兩個問題:

  • token驗證需要遠程調(diào)用認證服務器届氢,效率低
  • token無法攜帶用戶數(shù)據(jù)语盈;

因此使用JWT來取代原生的token转唉。

JWT全稱為Json Web Token,使用一種特殊格式的token,token有特定含義硝逢,分為三部分:

  • 頭部Header:包括令牌的類型(即JWT)及使用的哈希算法(如HMAC帆锋、SHA256或RSA)旁仿。
  • 載荷Payload:存放有效信息踏枣,如iss(簽發(fā)者)、exp(過期時間)丸凭、sub(授權(quán)用戶)和創(chuàng)建時間等福扬,也可以自定義字段方便擴展。
  • 簽名Signature:是對前兩部分的數(shù)字簽名惜犀,防止被篡改忧换。

這三部分均用base64Url進行編碼,并使用.進行分隔向拆,一個典型的jwt格式的token類似xxxxx.yyyyy.zzzzz亚茬。認證服務器通過對稱或非對稱的加密方式利用payload生成signature,并在header中申明簽名方式浓恳。這樣jwt可以實現(xiàn)分布式的token驗證功能刹缝,即資源服務器通過事先維護好的對稱或者非對稱密鑰(非對稱的話就是認證服務器提供的公鑰),直接在本地驗證token颈将,這種去中心化的驗證機制非常適合分布式架構(gòu)梢夯。jwt相對于傳統(tǒng)的token來說,解決以下兩個痛點:

  • 通過驗證簽名晴圾,對于token的驗證可以直接在資源服務器本地完成颂砸,不需要連接認證服務器;
  • 在payload中可以包含用戶相關信息死姚,這樣就輕松實現(xiàn)了token和用戶信息的綁定人乓;
    如果認證服務器頒發(fā)的是jwt格式的token,那么資源服務器就可以直接自己驗證token的有效性并綁定用戶都毒,這無疑大大提升了處理效率且減少了單點隱患色罚。

總結(jié):Header申明算法、Payload是用戶信息账劲、對Payload加密得到Signature戳护,三部分用base64編碼后通過"."連接組合為token金抡;驗證token時只需要根據(jù)header中的算法對Payload(默認是HMAC SHA256算法)進行驗證。

JWT優(yōu)點:

  • jwt基于json腌且,非常方便使用梗肝;
  • 可以在令牌中自定義豐富的內(nèi)容,易擴展铺董;
  • 通過非對稱加密算法和數(shù)字簽名技術(shù)巫击,JWT防止篡改,安全性高柄粹;
  • 資源服務使用JWT可不依賴認證服務器即可完成授權(quán)喘鸟。

JWT缺點:

  • 在有效期內(nèi)匆绣,token是無法作廢的驻右,用戶的簽退更多是一個客戶端的簽退,服務端token仍然有效崎淳,你只要使用這個token堪夭,仍然可以登陸系統(tǒng)。另外一個問題是續(xù)簽問題拣凹,當然你也可以通過redis去記錄token狀態(tài)森爽,并在用戶訪問后更新這個狀態(tài),但這就是硬生生把jwt的無狀態(tài)搞成有狀態(tài)了嚣镜,而這些在傳統(tǒng)的session+cookie機制中都是不需要去考慮的爬迟。

JWT安全加強

  • 避免網(wǎng)絡劫持,HTTP協(xié)議使用header傳遞JWT容易泄露菊匿,使用HTTPS協(xié)議傳輸更安全付呕。
  • 私鑰存放在服務器端,保證服務器不被攻破跌捆。
  • JWT可以被暴力破解徽职,所以需要保證秘鑰復雜度,定期更換秘鑰佩厚。

以上是理論姆钉,下面來看結(jié)合實際

# 通過密碼模式請求已經(jīng)搭建完成的授權(quán)服務器
POST https://oauth.marssenger.com/oauth/token?client_id=c1&client_secret=123456&grant_type=password&username=admin&password=abc123&user_type=admin

# 得到令牌如下,token因為太長省略了部分抄瓦。
{
    "access_token": "ey......t_SA",
    "token_type": "bearer",
    "refresh_token": "ey......mbrw",
    "expires_in": 86399,
    "scope": "ROLE_ADMIN ROLE_APPLICATION",
    "cre": 1622691146,
    "jti": "c31a69bc-0eba-4e93-8f78-c0f8c04a2b11"
}

對其中的access_token進行解析:

# 令牌中的完整access_token如下
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiIzNyIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX0FQUExJQ0FUSU9OIl0sImNyZSI6MTYyMjY5MTE0NiwiZXhwIjoxNjIyNzc3NTQ2LCJhdXRob3JpdGllcyI6WyJiYWNrOnVzZXI6dXBkYXRlIiwiYmFjazpjb250ZW50OnVwZGF0ZSIsImJhY2s6Y29udGVudDpsaXN0IiwiYmFjazp1c2VyOmxpc3QiLCJiYWNrOnN5czphbGwiLCJST0xFX3VzZXIiLCJiYWNrOmNvbnRlbnQ6YWxsIiwiYmFjazpjb250ZW50OmFkZCIsImJhY2s6dXNlcjphbGwiLCJiYWNrOnVzZXI6YWRkIiwiYmFjazp1c2VyOmRlbGV0ZSIsImJhY2s6Y29udGVudDpkZWxldGUiXSwianRpIjoiYzMxYTY5YmMtMGViYS00ZTkzLThmNzgtYzBmOGMwNGEyYjExIiwiY2xpZW50X2lkIjoiYzEifQ.lZXI8rhN6XUgbHaXZa6zK2GAdI2nruT_LZpAtBMRIIuQddKu8827juVBqx498Orb3MNC7RzFV_cv365SlE_TaUJ09tW0jnd-8kdPaRIGt11SIg2Jik8EQ3l_t8_XOtZhq6TUjKfPZQfo0egXUO70QzyC9JPFGZQPAUYvwNCZMC0qBkYuI4paUWQoMh0yML25eVMIMf_fTPgxFFicEVzc78yO4PqUrXc-WGlZkRRx6EPyrIhtXVY0uHmBORKlnbPcVDVkcYnLXTUcVumtWRGUw4zsHjGLAWkiUC2ISvBUl5DVQStd9B5R_FzLWuWLNlskFaZ8npbKA9XuUH_CKxt_SA"

# access_token由兩個"."分割為三部分潮瓶,分別為Header,Payload和Signature钙姊,通過base64解密Header和Payload后得到:
Header:{"alg":"RS256","typ":"JWT"}
Payload:{
    "aud":[
        "res1"
    ],
    "user_name":"37",
    "scope":[
        "ROLE_ADMIN",
        "ROLE_APPLICATION"
    ],
    "cre":1622691146,
    "exp":1622777546,
    "authorities":[
        "back:user:update",
        "back:content:update",
        "back:content:list",
        "back:user:list",
        "back:sys:all",
        "ROLE_user",
        "back:content:all",
        "back:content:add",
        "back:user:all",
        "back:user:add",
        "back:user:delete",
        "back:content:delete"
    ],
    "jti":"c31a69bc-0eba-4e93-8f78-c0f8c04a2b11",
    "client_id":"c1"
}

# Signature為數(shù)字簽名筋讨,即對內(nèi)容的摘要通過私鑰進行加密,然后在客戶端通過公鑰解密并與摘要進行對比摸恍,保證內(nèi)容不會被篡改悉罕。Header中攜帶了使用的加密算法信息赤屋。

2.4 網(wǎng)關

有些架構(gòu)方案中,認證服務負責認證壁袄,網(wǎng)關負責校驗認證和鑒權(quán)类早,其他API服務負責處理自己的業(yè)務邏輯。安全相關的邏輯只存在于認證服務和網(wǎng)關服務中嗜逻,其他服務只是單純地提供服務而沒有任何安全相關邏輯涩僻。

但是個人覺得這樣網(wǎng)關承擔的責任太大,且每次業(yè)務邏輯改變后需要同時修改網(wǎng)關的代碼或者將數(shù)據(jù)庫刷新到網(wǎng)關內(nèi)存中栈顷。所以為了方便起見逆日,目前還是將權(quán)限信息和鑒權(quán)邏輯放到自己的業(yè)務中。優(yōu)化工作后續(xù)再做萄凤,反正對于整套Oauth2搭建來說室抽,將認證和鑒權(quán)工作放到gateway中只是小意思。

目前網(wǎng)關最大的作用就是路由請求了靡努,同時可以設置黑名單進行過濾坪圾。

2.5 密鑰配置

由于需要簽名摘要,所以認證服務器需要配置密鑰惑朦,這里使用RS256進行加密兽泄。配置密鑰的方式有好多種。

  • 方式一:最簡單的就是直接將公鑰和私鑰寫在認證服務配置文件中漾月,在項目啟動時從配置文件讀取病梢。這樣公鑰可以直接寫在資源服務中,也可以通過提供接口的方式讓資源服務來請求獲取公鑰梁肿。

  • 方式二:通過生成SSL證書的方式蜓陌,將證書放到資源路徑下,然后認證服務運行時讀取并解析證書栈雳,獲取公鑰和私鑰护奈。這種情況下公鑰就必須通過提供接口的方式讓資源服務來請求獲取公鑰。推薦采取這種方式哥纫。

  • 方式三:通過jjwt框架生成密鑰霉旗,每次重啟都會更換隨機密鑰≈В可以開放公鑰接口給其他資源服務厌秒。這種很方便,但是并不推薦擅憔,因為每次重啟認證服務都需要重啟資源服務鸵闪,且會導致之前的token全部失效。

2.6 服務劃分

將Oauth2服務劃分為了兩部分暑诸,一個是認證服務蚌讼,一個是用戶中心辟灰,就是將用戶相關的部分拿出來新建一個用戶服務。所有令牌相關的操作都在認證服務中完成篡石,所有用戶相關的操作都在用戶中心完成芥喇。

認證服務需要訪問用戶的信息,可以通過Feign調(diào)用用戶中心的接口獲取資源凰萨;用戶中心用于處理用戶的相關操作继控,所以是一個資源服務,外部請求需要鑒權(quán)后才能進行操作胖眷。

三武通、部署

3.1 建表語句

-- used in tests that use HSQL

DROP TABLE IF EXISTS oauth_client_details;
CREATE TABLE oauth_client_details (
  client_id VARCHAR(256) NOT NULL COMMENT '客戶端標識',
  resource_ids VARCHAR(256) NULL DEFAULT NULL COMMENT '接入資源列表',
  client_secret VARCHAR(256) NULL DEFAULT NULL COMMENT '客戶端秘鑰',
  scope VARCHAR(256) NULL DEFAULT NULL COMMENT '客戶端權(quán)限',
  authorized_grant_types VARCHAR(256) NULL DEFAULT NULL COMMENT '授權(quán)模式',
  web_server_redirect_uri VARCHAR(256) NULL DEFAULT NULL COMMENT '重定向地址',
  authorities VARCHAR(256) NULL DEFAULT NULL COMMENT '指定用戶的權(quán)限范圍,如果授權(quán)的過程需要用戶登陸珊搀,該字段不生效冶忱,implicit和client_credentials需要',
  access_token_validity int(11) NULL DEFAULT NULL COMMENT '令牌有效時間',
  refresh_token_validity int(11) NULL DEFAULT NULL COMMENT '更新令牌有效時間',
  additional_information VARCHAR(4096) COMMENT '可空',
  autoapprove VARCHAR(256) COMMENT '是否手動確認授權(quán),默認false',
  PRIMARY KEY (client_id) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '接入客戶端信息';

DROP TABLE IF EXISTS oauth_code;
CREATE TABLE oauth_code (
    create_time timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    code varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    authentication blob NULL,
    INDEX code_index(code) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

create table oauth_client_token (

  token_id VARCHAR(256),

  token LONGVARBINARY,

  authentication_id VARCHAR(256) PRIMARY KEY,

  user_name VARCHAR(256),

  client_id VARCHAR(256)

);

create table oauth_access_token (

  token_id VARCHAR(256),

  token LONGVARBINARY,

  authentication_id VARCHAR(256) PRIMARY KEY,

  user_name VARCHAR(256),

  client_id VARCHAR(256),

  authentication LONGVARBINARY,

  refresh_token VARCHAR(256)

);

create table oauth_refresh_token (

  token_id VARCHAR(256),

  token LONGVARBINARY,

  authentication LONGVARBINARY

);


create table oauth_approvals (

userId VARCHAR(256),

clientId VARCHAR(256),

scope VARCHAR(256),

status VARCHAR(10),

expiresAt TIMESTAMP,

lastModifiedAt TIMESTAMP

);

-- customized oauth_client_details table

create table ClientDetails (

  appId VARCHAR(256) PRIMARY KEY,

  resourceIds VARCHAR(256),

  appSecret VARCHAR(256),

  scope VARCHAR(256),

  grantTypes VARCHAR(256),

  redirectUrl VARCHAR(256),

  authorities VARCHAR(256),

  access_token_validity INTEGER,

  refresh_token_validity INTEGER,

  additionalInformation VARCHAR(4096),

  autoApproveScopes VARCHAR(256)

);

RBAC表:

CREATE TABLE `tb_permission` (

  `id` bigint(20) NOT NULL AUTO_INCREMENT,

  `parent_id` bigint(20) DEFAULT NULL COMMENT '父權(quán)限',

  `name` varchar(64) NOT NULL COMMENT '權(quán)限名稱',

  `enname` varchar(64) NOT NULL COMMENT '權(quán)限英文名稱',

  `url` varchar(255) NOT NULL COMMENT '授權(quán)路徑',

  `description` varchar(200) DEFAULT NULL COMMENT '備注',

  `created` datetime NOT NULL,

  `updated` datetime NOT NULL,

  PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8 COMMENT='權(quán)限表';


CREATE TABLE `tb_role` (

  `id` bigint(20) NOT NULL AUTO_INCREMENT,

  `parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',

  `name` varchar(64) NOT NULL COMMENT '角色名稱',

  `enname` varchar(64) NOT NULL COMMENT '角色英文名稱',

  `description` varchar(200) DEFAULT NULL COMMENT '備注',

  `created` datetime NOT NULL,

  `updated` datetime NOT NULL,

  PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';


CREATE TABLE `tb_role_permission` (

  `id` bigint(20) NOT NULL AUTO_INCREMENT,

  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',

  `permission_id` bigint(20) NOT NULL COMMENT '權(quán)限 ID',

  PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8 COMMENT='角色權(quán)限表';


CREATE TABLE `tb_user` (

  `id` bigint(20) NOT NULL AUTO_INCREMENT,

  `username` varchar(50) NOT NULL COMMENT '用戶名',

  `password` varchar(64) NOT NULL COMMENT '密碼食棕,加密存儲',

  `phone` varchar(20) DEFAULT NULL COMMENT '注冊手機號',

  `email` varchar(50) DEFAULT NULL COMMENT '注冊郵箱',

  `account_non_expired` tinyint DEFAULT 1 COMMENT '賬戶沒有過期',

  `account_non_locked` tinyint DEFAULT 1 COMMENT '用戶沒有被鎖定',

  `credentials_non_expired` tinyint DEFAULT 1 COMMENT '憑證沒有過期',

  `enabled` tinyint DEFAULT 1 COMMENT '賬戶是否可用',

  `created` datetime NOT NULL,

  `updated` datetime NOT NULL,

  PRIMARY KEY (`id`),

  UNIQUE KEY `username` (`username`) USING BTREE,

  UNIQUE KEY `phone` (`phone`) USING BTREE,

  UNIQUE KEY `email` (`email`) USING BTREE

) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用戶表';


CREATE TABLE `tb_user_role` (

  `id` bigint(20) NOT NULL AUTO_INCREMENT,

  `user_id` bigint(20) NOT NULL COMMENT '用戶 ID',

  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',

  PRIMARY KEY (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用戶角色表';

3.2 密鑰配置

方式一

寫死在配置文件中朗和,啟動時直接讀取配置就行了错沽。

jwt:
  publicKey: "-----BEGIN PUBLIC KEY-----MIIXXXXXXXXQAB-----END PUBLIC KEY-----"
  privateKey: "-----BEGIN PRIVATE KEY-----MIIXXXXXXXD43js=-----END PRIVATE KEY-----"
  expiration: 3600000
  header: JWTHeaderName

方式二

步驟一:生成證書

在jdk的bin目錄下使用如下命令

keytool -genkey -alias jwt -keyalg RSA -keystore uaacenter.jks

然后設置keystore password和key password即可簿晓,我這里設置的都是uaacenter。

將得到的證書uaacenter.jks放到resources目錄下千埃。

步驟二:認證服務中解析證書

import cn.hutool.core.codec.Base64;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;

import javax.annotation.Resource;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

@Configuration
public class TokenConfig {

    @Resource
    private Environment environment;

    @Resource
    private KeyPair keyPair;

    @Bean
    public KeyPair keyPair(){
        String location = environment.getProperty("key-store.location");
        String storepass = environment.getProperty("key-store.storepass");
        String keypass = environment.getProperty("key-store.keypass");
        String alias = environment.getProperty("key-store.alias");
        ClassPathResource resource = new ClassPathResource(location);
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, storepass.toCharArray());
        return keyStoreKeyFactory.getKeyPair(alias,keypass.toCharArray());
    }

    @Bean
    public RSAPublicKey publicKey() {
        RSAPublicKey aPublic = (RSAPublicKey) keyPair.getPublic();

        System.out.println(Base64.encode(aPublic.getEncoded()));

        return aPublic;
    }

    @Bean
    public RSAPrivateKey privateKey(){
        RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();

        System.out.println(Base64.encode(aPrivate.getEncoded()));

        return aPrivate;
    }

    /**
     * 將JWT作為令牌
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * JWT配置
     *
     * @return
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//        converter.setSigningKey(SIGNING_KEY); //對稱秘鑰憔儿,資源服務器使用該秘鑰來驗證
        converter.setKeyPair(keyPair);

        return converter;
    }

}

步驟三:開放公鑰請求接口

@RestController
@Slf4j
public class AuthController {

    @Resource
    private RSAPublicKey publicKey;

    /**
     * 獲取公鑰接口(不鑒權(quán))
     * @return
     */
    @GetMapping("/feign/uaa/publicKey")
    public String publicKey() {
        return "-----BEGIN PUBLIC KEY-----" + Base64.encode(publicKey.getEncoded()) + "-----END PUBLIC KEY-----";
    }

}

方式三

需要導入依賴

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

@Configuration
public class KeyPairConfig {
    
    private final KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);
    @Bean
    public RSAPublicKey publicKey() {
        RSAPublicKey aPublic = (RSAPublicKey) keyPair.getPublic();

        System.out.println(Base64.encode(aPublic.getEncoded()));

        return aPublic;
    }

    @Bean
    public RSAPrivateKey privateKey(){
        RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();

        System.out.println(Base64.encode(aPrivate.getEncoded()));

        return aPrivate;
    }
}

3.3 認證服務

依賴

    <properties>
        <spring-boot.version>2.2.5.RELEASE</spring-boot.version>
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
        <mybatis-plus.version>3.2.0</mybatis-plus.version>
    </properties>

    <dependencies>
        <!-- springcloud依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- springboot依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </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>
        </dependency>

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- 數(shù)據(jù)庫依賴 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- 通用工具類 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>
        <!-- jjwt工具類 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>

        <!-- 靜態(tài)webjar資源 -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>4.5.3</version>
        </dependency>

        <!-- 二方庫依賴 -->
        <dependency>
            <groupId>com.marssenger.hifun</groupId>
            <artifactId>common</artifactId>
            <version>1.4.18-RELEASES</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring-boot.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

該版本的Spring-security存在漏洞CVE-2022-22978,這個漏洞是由于 RegexRequestMatcher 正則表達式配置權(quán)限的特性放可,當規(guī)則中包含帶點號的正則表達式時谒臼,攻擊者可以通過構(gòu)造惡意數(shù)據(jù)包繞過身份認證。
可以用5.4.11版本的包替換

<!-- spring security 安全認證 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.4.11</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.4.11</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>5.4.11</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
    <version>5.4.11</version>
</dependency>

注解

@EnableAuthorizationServer 注解來告訴spring框架自動配置一些關于AuthorizationEndpoint以及一些關于AuthorizationServer security的配置耀里。同時蜈缤,來配置訪問的client的一些細節(jié)
@EnableResourceServer 注解來告訴spring框架自動配置一些關于resource server的配置,比如啟用OAuth2AuthenticationProcessingFilter來檢查進來的request有沒有有效的accesstoken冯挎。

配置文件

server:
  servlet:
    session:
      timeout: 20s
  port: 9501
  
spring:
  profiles:
    active: dev
  application:
    name: security-uaa
  cloud:
    config:
      label: master
      name: ${spring.application.name}
      discovery:
        enabled: true
        service-id: config-server
  thymeleaf:
    prefix: classpath:/views/
    suffix: .html
    cache: false
  datasource:
    url: jdbc:mysql://192.168.32.225:3306/uaa_server?useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: hxr
  main:
    allow-bean-definition-overriding: true
  redis:
    host: 116.62.148.11
    port: 6380
    password:
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1s
        min-idle: 0
    session:
      store-type: redis

eureka:
  client:
    service-url:
      defaultZone: http://192.168.32.230:8761/eureka
  instance:
    prefer-ip-address: true
    health-check-url-path: /actuator/health

management:
  endpoints:
    web:
      exposure:
        include: refresh,health,info,env

feign:
  httpclient:
    connection-timeout: 2000

jwt:
  publicKey: "-----BEGIN PUBLIC KEY-----MIIBIjANB......wIDAQAB-----END PUBLIC KEY-----"
  privateKey: "-----BEGIN PRIVATE KEY-----MIIEvQIBADAN......WKGoHLD43js=-----END PRIVATE KEY-----"
  expiration: 3600000
  header: JWTHeaderName

key-store:
  location: uaacenter.jks
  storepass: uaacenter
  alias: uaacenter
  keypass: uaacenter

實體類

@Component
@Data
@Accessors(chain = true)
public class MyUserDetails implements UserDetails {

    private String username;
    private String password;
    boolean accountNonExpired = true; // 賬戶沒有過期
    boolean accountNonLocked = true; //賬戶沒被鎖定 (是否凍結(jié))
    boolean credentialsNonExpired = true; //密碼沒有過期
    boolean enabled = true; //賬戶是否可用(是否被刪除)
    Collection<? extends GrantedAuthority> authorities; //用戶權(quán)限集合

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}
@Data
@Accessors(chain = true)
public class TbUserPO {

//    /**
//     * 用戶名
//     */
//    private String username;
    /**
     * 密碼
     */
    private String password;
    /**
     * 賬戶沒有過期
     */
    boolean accountNonExpired = true;
    /**
     * 賬戶沒被鎖定 (是否凍結(jié))
     */
    boolean accountNonLocked = true;
    /**
     * 密碼沒有過期
     */
    boolean credentialsNonExpired = true;
    /**
     * 賬戶是否可用(是否被刪除)
     */
    boolean enabled = true;

    /*--------------------------------------------*/

    /**
     * 用戶id
     */
    private Integer id;
    /**
     * 用戶名
     */
    private String username;
    /**
     * 手機號
     */
    private String phone;
    /**
     * 郵箱
     */
    private String email;

}

配置類

AuthorizationServerConfigurerAdapter類的三個重載方法的配置參數(shù)

  • ClientDetailsServiceConfigurer:用來配置客戶端詳情服務底哥,客戶端詳情信息在這里進行初始化,可以把客戶端詳情信息寫死在這里或者通過數(shù)據(jù)庫來存儲調(diào)取詳情信息房官。
  • AuthorizationServerEndpointsConfigurer:用來配置令牌(token) 的訪問端點和令牌服務(token services)趾徽。
  • AuthorizationServerSecurityConfigurer:用來配置令牌端點的安全約束(權(quán)限)。

①配置登錄頁面和允許訪問的路徑

import oauth2.utils.PermitAllUrl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 認證管理器
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //跨域請求偽造防御失效
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/administrator/getInfo").hasAnyAuthority("/users/")
                .antMatchers(PermitAllUrl.permitAllUrl("/feign/**","/uaa/geetest/**","/uaa/hxrlogin/**","/uaa/findPassword/**","/uaa/error/**","/test","/forward")).permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/uaa/hxrlogin/pages/loginTel.html").loginProcessingUrl("/uaa/login");
//                .and().exceptionHandling().authenticationEntryPoint(new MyLoginUrlAuthenticationEntryPoint())
//                .sessionManagement()
//                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

}

這里將登陸頁面設置為路徑/uaa/hxrlogin/pages/loginTel.html翰守,然后通過路徑映射映射到resource下的靜態(tài)頁面孵奶,即public/pages/loginTel.html

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;

@Configuration
public class WebMvcConfigurerAdapter implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
//        registry.addViewController("/").setViewName("login");
//        registry.addViewController("/login.html").setViewName("login");
//        registry.addViewController("/uaa/hxrlogin/pages").setViewName("");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/uaa/hxrlogin/**").addResourceLocations("classpath:hxrlogin/");
//        registry.addResourceHandler("/uaa/public/**").addResourceLocations("classpath:public/");
    }
}

②基礎配置

配置token存儲方式、JWT令牌配置蜡峰、客戶端配置了袁、認證管理器配置朗恳、令牌增強配置

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Arrays;

/**
 * @Description:
 * @Author: CJ
 * @Data: 2020/6/13 17:01
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    //token存儲方式
    @Resource
    private TokenStore tokenStore;
    //JWT令牌配置
    @Resource
    private JwtAccessTokenConverter accessTokenConverter;

    //客戶端詳情服務
    @Autowired
    private ClientDetailsService clientDetailsService;

    //認證管理器
    @Autowired
    private AuthenticationManager authenticationManager;


    /**
     * 將客戶端信息存儲到數(shù)據(jù)庫
     *
     * @param dataSource
     * @return
     */
    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        clientDetailsService.setPasswordEncoder(bCryptPasswordEncoder);
        return clientDetailsService;
    }

    /**
     * 客戶端配置
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
//        clients.inMemory()//使用內(nèi)存存儲
//                .withClient("c1") //客戶端id
//                .secret(bCryptPasswordEncoder.encode("abc123"))//設置密碼
//                .resourceIds("res1")//可訪問的資源列表
//                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")//該client允許的授權(quán)類型
//                .scopes("all")//允許的授權(quán)范圍
//                .autoApprove(false)//false跳轉(zhuǎn)到授權(quán)頁面,true不跳轉(zhuǎn)
//                .redirectUris("http://www.baidu.com");//設置回調(diào)地址
    }

    @Resource
    private MyTokenEnhancer myTokenEnhancer;

    /**
     * 令牌管理服務
     *
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService); //客戶端詳情服務
        services.setSupportRefreshToken(true); //支持刷新令牌
        services.setTokenStore(tokenStore); //令牌的存儲策略
        //令牌增強,設置JWT令牌
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(myTokenEnhancer,accessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);

//        services.setAccessTokenValiditySeconds(7200); //令牌默認有效時間2小時
//        services.setRefreshTokenValiditySeconds(259200); //刷新令牌默認有效期3天
        return services;
    }

    /**
     * 設置授權(quán)碼模式的授權(quán)碼如何存取载绿,暫時采用內(nèi)存方式
     *
     * @return
     */
//    @Bean
//    public AuthorizationCodeServices authorizationCodeServices(){
//        return new InMemoryAuthorizationCodeServices();
//    }

    @Resource
    private AuthorizationCodeServices authorizationCodeServices;

    /**
     * 授權(quán)碼存儲到數(shù)據(jù)庫
     *
     * @param dataSource
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    /**
     * 令牌訪問端點配置
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)//認證管理器
                .authorizationCodeServices(authorizationCodeServices)//授權(quán)碼服務
                .tokenServices(tokenServices()) //令牌管理服務(設置令牌存儲方式和令牌類型JWT)
                .allowedTokenEndpointRequestMethods(HttpMethod.POST)

                .pathMapping("/oauth/authorize","/uaa/oauth/authorize")
                .pathMapping("/oauth/token","/uaa/oauth/token")
                .pathMapping("/oauth/confirm_access","/uaa/oauth/confirm_access");
    }

    /**
     * 對授權(quán)端點接口的安全約束
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()") // /auth/token_key是公開的
                .checkTokenAccess("permitAll()") // /auth/check_token是公開的
                .allowFormAuthenticationForClients(); //允許表單認證(申請令牌)
    }

}

Feign調(diào)用用戶服務

@FeignClient(name = "SECURITY-USER")
public interface UserCenterFeign {

    /**
     * 以下是認證中心遠程調(diào)用用戶中心的接口
     * @return
     */
    @GetMapping(path = "/feign/user/getTbUser")
    TbUserPO getTbUser(@RequestParam("username") String username);

    @GetMapping(path = "/feign/user/getRoleCodes")
    List<String> getRoleCodes(@RequestParam("username") String username);

    @PostMapping(path = "/feign/user/getAuthorities")
    List<String> getAuthorities(@RequestBody List<String> roleCodes);

}

3.4 用戶服務

依賴

   <properties>
        <spring-boot.version>2.2.5.RELEASE</spring-boot.version>
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
        <mybatis-plus.version>3.2.0</mybatis-plus.version>
    </properties>

    <dependencies>
        <!-- spring-cloud相關 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <!-- spring-boot相關 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 自動創(chuàng)建數(shù)據(jù)庫表 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- redis相關 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- rabbitMQ相關 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <!-- 數(shù)據(jù)庫相關 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- swagger配置 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.8.0</version>
        </dependency>

        <!-- 工具依賴 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>

        <!-- jwt工具類 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring-boot.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

配置文件

server:
  port: 9801

spring:
  application:
    name: security-user
  profiles:
    active: dev
  cloud:
    config:
      label: master
      name: ${spring.application.name}
      discovery:
        enabled: true
        service-id: config-server
  datasource:
    url: jdbc:mysql://192.168.32.225:3306/uaa_server?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=UTC&allowMultiQueries=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: hxr
  redis:
    host: 116.62.148.11
    port: 6380
    password:
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1s
        min-idle: 0
  rabbitmq:
    host: 116.62.148.11
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    # 發(fā)送確認
    publisher-confirms: true
    # 發(fā)送回調(diào)
    publisher-returns: true
    # 消費手動確認
    listener:
      simple:
        acknowledge-mode: manual
  jpa:
    #配置數(shù)據(jù)庫類型
    database: mysql
    #指定數(shù)據(jù)庫的引擎
    database-platform: org.hibernate.dialect.MySQL57Dialect
    #配置是否打印sql
    show-sql: true
    #Hibernate相關配置
    hibernate:
      #配置級聯(lián)等級
      #      ddl-auto: create
      ddl-auto: update
    open-in-view: false
#    jackson:
#      property-naming-strategy: CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES


eureka:
  client:
    service-url:
      defaultZone: http://192.168.32.230:8761/eureka
  instance:
    prefer-ip-address: true
    health-check-url-path: /actuator/health

management:
  endpoints:
    web:
      exposure:
        include: refresh,health,info,env
  endpoint:
    health:
      show-details: always

feign:
  httpclient:
    connection-timeout: 2000

jwt:
  # 老用戶系統(tǒng)公鑰
  publicKey: "MII......wIDAQAB"

配置類

令牌配置

@Configuration
public class TokenConfig {

//    private static final String SIGNING_KEY = "uaa123";

    @Resource
    private UaaFeign uaaClient;

    @Resource
    private Environment environment;

    @Bean
    public PublicKey hifunPublicKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        String hifunPublicKey = environment.getProperty("jwt.publicKey");
        System.out.println("***publicKeyStr:" + hifunPublicKey);
        byte[] keyBytes = (new BASE64Decoder()).decodeBuffer(hifunPublicKey);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey rsaPublicKey = keyFactory.generatePublic(keySpec);
        return rsaPublicKey;
    }

    /**
     * 將Jwt作為令牌
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 配置Jwt令牌(秘鑰)
     *
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
//        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        MyJwtAccessTokenConverter converter = new MyJwtAccessTokenConverter();
//        converter.setSigningKey(SIGNING_KEY);

        String publicKey = uaaClient.publicKey();
        System.out.println("publicKey: " + publicKey);
        converter.setVerifierKey(publicKey);
        converter.setVerifier(new RsaVerifier(publicKey));

        return converter;
    }

}

基礎配置

import oauth2.config.auth.rewrite.MyAccessDeniedHandler;
import oauth2.config.auth.rewrite.MyAuthExceptionEntryPoint;
import oauth2.config.auth.rewrite.MyTokenExtractor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

import javax.annotation.Resource;

@Configuration
public class ResourceServerConfig {

    private static final String RESOURCE_ID = "res1";

    @Resource
    private TokenStore tokenStore;

    @Resource
    private MyAuthExceptionEntryPoint myAuthExceptionEntryPoint;

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Resource
    private MyTokenExtractor myTokenExtractor;

    @Configuration
    @EnableResourceServer
    public class UserServerConfig extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId(RESOURCE_ID)
                    .tokenStore(tokenStore)
                    .stateless(true)
                    .tokenExtractor(myTokenExtractor)
                    .authenticationEntryPoint(myAuthExceptionEntryPoint)
                    .accessDeniedHandler(myAccessDeniedHandler);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.csrf().disable().authorizeRequests()
//                    .antMatchers("/order/**").access("#oauth2.hasScope('ROLE_ADMIN')");
                    .antMatchers("/user/getTbUser**", "/user/getRoleCodes", "/user/getAuthorities","/uc/permission").permitAll()
                    .antMatchers("/user/**").hasAnyAuthority("hifun")/*access("#oauth2.hasScope('ROLE_USER')")*/
                    .antMatchers("/administrator/**").hasAnyAuthority("/users/");
        }
    }

}

Feign

老用戶體系的接口

@FeignClient(name = "hifun-service-user")
public interface HifunFeign {

    /**
     * 驗證密碼
     * @param id
     * @param password
     * @return
     */
    @PostMapping(path = "/password/check", consumes = "application/json")
    String check(@RequestParam("id") Integer id, @RequestBody String password);

    /**
     * 根據(jù)手機號獲取用戶信息
     * @param mobile
     * @return
     */
    @GetMapping(path = "/user/mobile")
    String mobile(@RequestParam("mobile") String mobile);

    /**
     * 根據(jù)用戶id獲取用戶信息
     * @param id
     * @return
     */
    @GetMapping(path = "/user/{id}")
    String getUserInfo(@PathVariable("id") Integer id);


}

獲取認證服務的公鑰

@FeignClient("SECURITY-UAA")
public interface UaaFeign {

    @GetMapping(path = "/feign/uaa/publicKey")
    String publicKey();

}

過期token接口

@RestController
@Api(value = "需要攔截的token")
public class ExpiredTokenController {

    @Resource
    private RedisTemplate<String,Object> redisTemplate;

    @Resource
    private RedisKeyConfig redisKeyConfig;

    @GetMapping(path = "/feign/user/getExpiredToken")
    public Map<Object,Object> getExpiredToken(){
//        redisTemplate.opsForHash().putIfAbsent(redisKeyConfig.getExpiredTokenKey(),"admin",1601455413);
//        Map<Object, Object> entries = redisTemplate.opsForHash().entries(redisKeyConfig.getExpiredTokenKey());
//        System.out.println("feign調(diào)用成功" + entries);
        return redisTemplate.opsForHash().entries(redisKeyConfig.getExpiredTokenKey());
    }

}

3.5 Gateway網(wǎng)關

依賴

   <properties>
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

配置文件

server:
  port: 8100

spring:
  application:
    name: gateway-server
  profiles:
    active: dev
  cloud:
    gateway:
#      default-filters:  #全局過濾器
      #        - name: Hystrix
      #          args:
      #           name: fallbackcmd  #使用HystrixCommand打包剩余的過濾器僻肖,并命名為fallbackcmd
      #           fallbackUri: forward:/fallback  #配置fallbackUri,降級邏輯被調(diào)用
      discovery:
        locator:
          enabled: true
      routes:
        - id: SECURITY-UAA
          uri: lb://SECURITY-UAA
          predicates:
            - Path=/uaa/**
          filters:
            - PreserveHostHeader
        #           - RewritePath=/uaa(?<segment>/?.*), $\{segment}UAA-CENTER
        - id: SECURITY-USER
          uri: lb://SECURITY-USER
          predicates:
            - Path=/uc/**
          filter:
            - PreserveHostHeader
      globalcors: #跨域配置
        corsConfigurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods: "*"

eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://hxr:hxr123@192.168.33.236:8761/eureka/
  instance:
    prefer-ip-address: true

#設置feign客戶端負載均衡和超時時間(OpenFeign默認支持ribbon)
ribbon:
  #開啟ribbon負載均衡
  eureka:
    enabled: true
  # ribbon請求連接的超時時間,默認值5000
  ConnectTimeout: 1000
  # 負載均衡超時時間,默認值5000
  ReadTimeout: 1000
  # 是否開啟重試
  OkToRetryOnAllOperations: true
  # 重試期間卢鹦,實例切換次數(shù)
  MaxAutoRetriesNextServe: 2
  # 當前實例重試次數(shù)
  MaxAutoRetries: 1


feign:
  hystrix:
    enabled: false # 開啟Feign的熔斷功能
#  斷路器的超時時間需要大于ribbon的超時時間臀脏,不然不會觸發(fā)重試
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 60000 # 設置hystrix的超時時間為60000ms

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

cron:
  black-ip: 0 0/5 * * * ?
  sync_expired_token: 0 0/5 * * * ?

注意:如果放到springcloud框架中,授權(quán)碼模式登陸需要經(jīng)過nginx和gateway才會到達微服務冀自,而在gateway中會對請求進行重定向揉稚,并將請求頭中的信息進行改寫。授權(quán)碼模式下重定向的地址會讀取請求頭中的信息熬粗,所以最終重定向地址會指向微服務而不是nginx搀玖。所以在gateway中進行轉(zhuǎn)發(fā)時,不能改變請求頭中的信息驻呐,需要在gateway的配置文件中添加攔截器PreserveHostHeader灌诅。

最終在框架中進行重定向的地址指向nginx。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載含末,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者猜拾。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市佣盒,隨后出現(xiàn)的幾起案子挎袜,更是在濱河造成了極大的恐慌,老刑警劉巖肥惭,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盯仪,死亡現(xiàn)場離奇詭異,居然都是意外死亡蜜葱,警方通過查閱死者的電腦和手機全景,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牵囤,“玉大人爸黄,你說我怎么就攤上這事”记常” “怎么了馆纳?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長汹桦。 經(jīng)常有香客問我鲁驶,道長,這世上最難降的妖魔是什么舞骆? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任钥弯,我火速辦了婚禮径荔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘脆霎。我一直安慰自己总处,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布睛蛛。 她就那樣靜靜地躺著鹦马,像睡著了一般。 火紅的嫁衣襯著肌膚如雪忆肾。 梳的紋絲不亂的頭發(fā)上荸频,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音客冈,去河邊找鬼旭从。 笑死,一個胖子當著我的面吹牛场仲,可吹牛的內(nèi)容都是我干的和悦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼渠缕,長吁一口氣:“原來是場噩夢啊……” “哼鸽素!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起褐健,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤付鹿,失蹤者是張志新(化名)和其女友劉穎澜汤,沒想到半個月后蚜迅,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡俊抵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年谁不,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片徽诲。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡刹帕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谎替,到底是詐尸還是另有隱情偷溺,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布钱贯,位于F島的核電站挫掏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏秩命。R本人自食惡果不足惜尉共,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一褒傅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧袄友,春花似錦殿托、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鸠按,卻和暖如春唾戚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背待诅。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工叹坦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人卑雁。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓募书,卻偏偏與公主長得像,于是被迫代替她去往敵國和親测蹲。 傳聞我的和親對象是個殘疾皇子莹捡,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容