微服務架構下統⼀認證思路主要有兩種形式:
1、基于 Session 的認證⽅式在分布式的環境下,基于 session 的認證會出現⼀個問題,每個應⽤服務都需要在session中存儲⽤戶身份信息,通過負載均衡将本地的請求分配到另⼀個應⽤服務需要将 session 信息帶過去,否則會重新認證。我們可以使⽤ Session 共享、Session 黏貼等⽅案。Session ⽅案也有缺點,⽐如基于 cookie ,移動端不能有效使⽤等
2、基于 token 的認證⽅式。基于token的認證⽅式,服務端不⽤存儲認證數據,易維護擴展性強, 客戶端可以把token 存在任意地⽅,并且可以實現 web 和 app 統⼀認證機制。其缺點也很明顯,token 由于⾃包含信息,因此⼀般數據量較⼤,⽽且每次請求 都需要傳遞,因此⽐較占帶寬。另外,token 的簽名驗簽操作也會給 cpu 帶來額外的處理負擔。
下面我們就基于 token 的認證⽅式。采用 OAuth2 框架來實現。
oauth2 開放授權協議/标準OAuth(開放授權)是⼀個開放協議/标準,允許⽤戶授權第三⽅應⽤訪問他們存儲在另外的服務提供者上的信息,⽽不需要将⽤戶名和密碼提供給第三⽅應⽤或分享他們數據的所有内容。允許⽤戶授權第三⽅應⽤訪問他們存儲在另外的服務提供者上的信息,⽽不需要将⽤戶名和密碼提供給第三⽅應⽤或分享他們數據的所有内容。
OAuth2 協議流程圖如下:
1、客戶端請求用戶授權
2、用戶确認授權
3、客戶端收到授權許可後,向認證服務器申請令牌
4、認證服務器驗證授權許可,向客戶端返回有效令牌
5、客戶端攜帶有效令牌訪問資源服務器
6、資源服務器從認證服務器中驗證有效令牌。
7、驗證通過後,返回對應的資源給客戶端。
什麼情況下需要使⽤ OAuth2 ?第三⽅授權登錄的場景:⽐如,我們經常登錄⼀些⽹站或者應⽤的時候,可以選擇使⽤第三⽅授權登錄的⽅式,⽐如:微信授權登錄、QQ授權登錄、微博授權登錄等,這是典型的 OAuth2 使⽤場景。單點登錄的場景:如果項⽬中有很多微服務或者公司内部有很多服務,可以專⻔做⼀個認證中⼼(充當認證平台⻆⾊),所有的服務都要到這個認證中⼼做認證,隻做⼀次登錄,就可以在多個授權範圍内的服務中⾃由串⾏。
Spring Cloud OAuth2 JWT 實現Spring Cloud OAuth2 是 Spring Cloud 體系對OAuth2協議的實現,可以⽤來做多個微服務的統⼀認證(驗證身份合法性)授權(驗證權限)。通過向OAuth2服務(統⼀認證授權服務)發送某個類型的 grant_type 進⾏集中認證和授權,從⽽獲得 access_token(訪問令牌),⽽這個 token 是受其他微服務信任的。
使⽤ OAuth2 解決問題的本質是,引⼊了⼀個認證授權層,認證授權層連接了資源的擁有者,在授權層⾥⾯,資源的擁有者可以給第三⽅應⽤授權去訪問我們的某些受保護資源。
搭建認證服務器創建一個新的的模塊,service-oauth-hw-9900。
依賴
pom 文件中依賴如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.11.RELEASE</version>
</dependency>
<!--引入security對oauth2的支持-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-eureka-client</artifactId>
</dependency>
配置文件
server:
port: 9900
spring:
application:
name: service-oauth-hw
zipkin:
base-url: http://127.0.0.1:8771 # zipkin server的請求地址
sender:
# web 客戶端将蹤迹日志數據通過網絡請求的方式傳送到服務端,另外還有配置
# kafka/rabbit 客戶端将蹤迹日志數據傳遞到mq進行中轉
type: web
sleuth:
sampler:
# 采樣率 1 代表100%全部采集 ,默認0.1 代表10% 的請求蹤迹數據會被采集
# 生産環境下,請求量非常大,沒有必要所有請求的蹤迹數據都采集分析,對于網絡包括server端壓力都是比較大的,可以配置采樣率采集一定比例的請求的蹤迹數據進行分析即可
probability: 1
eureka:
client:
serviceUrl: # eureka server的路徑
defaultZone: http://quellanan.a:8761/eureka/,http://quellanan.b:8762/eureka/ #把 eureka 集群中的所有 url 都填寫了進來,也可以隻寫一台,因為各個 eureka server 可以同步注冊表
instance:
prefer-ip-address: true #使用ip注冊
#分布式鍊路追蹤
logging:
level:
org.springframework.web.servlet.DispatcherServlet: debug
org.springframework.cloud.sleuth: debug
啟動類
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceOauthHw9900Application {
public static void main(String[] args) {
SpringApplication.run(ServiceOauthHw9900Application.class, args);
}
}
自定義一個 OauthServerConfiger。當前類為Oauth2 server的配置類(需要繼承特定的父類 AuthorizationServerConfigurerAdapter)
@Configuration
@EnableAuthorizationServer //開啟認證服務器功能
public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
/**
* 客戶端詳情配置,
* 比如client_id,secret
* 當前這個服務就如同QQ平台,拉勾網作為客戶端需要qq平台進行登錄授權認證等,提前需要到QQ平台注冊,QQ平台會給拉勾網
* 頒發client_id等必要參數,表明客戶端是誰
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 客戶端信息存儲在什麼地方,可以在内存中,可以在數據庫裡
clients.inMemory()
// 添加一個client配置,指定其client_id
.withClient("quellanan")
//指定客戶端的密碼/安全碼
.secret("abcdefg")
//指定客戶端所能訪問資源id清單,此處的資源id是需要在具體的資源服務器上也配置一樣
.redirectUris("*")
//認證類型/令牌頒發模式,可以配置多個在這裡,但是不一定都用,具體使用哪種方式頒發token,需要客戶端調用的時候傳遞參數指定
.authorizedGrantTypes("password","refresh_token")
//客戶端的權限範圍,此處配置為all全部即可
.scopes("all");
}
/**
* 認證服務器最終是以api接口的方式對外提供服務(校驗合法性并生成令牌、校驗令牌等)
* 那麼,以api接口方式對外的話,就涉及到接口的訪問權限,我們需要在這裡進行必要的配置
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
// 相當于打開endpoints 訪問接口的開關,這樣的話後期我們能夠訪問該接口
security
// 允許客戶端表單認證
.allowFormAuthenticationForClients()
// 開啟端口/oauth/token_key的訪問權限(允許)
.tokenKeyAccess("permitAll()")
// 開啟端口/oauth/check_token的訪問權限(允許)
.checkTokenAccess("permitAll()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
endpoints
// 指定token的存儲方法
.tokenStore(tokenStore())
// token服務的一個描述,可以認為是token生成細節的描述,比如有效時間多少等
.tokenServices(authorizationServerTokenServices())
// 指定認證管理器,随後注入一個到當前類使用即可
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
}
/*
該方法用于創建tokenStore對象(令牌存儲對象)
token以什麼形式存儲
*/
public TokenStore tokenStore(){
return new InMemoryTokenStore();
// 使用jwt令牌
//return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 該方法用戶獲取一個token服務對象(該對象描述了token有效期等信息)
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默認實現
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true); // 是否開啟令牌刷新
defaultTokenServices.setTokenStore(tokenStore());
// 針對jwt令牌的添加
//defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
// 設置令牌有效時間(一般設置為2個小時)
defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我們請求資源需要攜帶的令牌
// 設置刷新令牌的有效時間
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
}
configure(ClientDetailsServiceConfifigurer clients):⽤來配置客戶端詳情服務(ClientDetailsService),客戶端詳情信息在 這⾥進⾏初始化,你能夠把客戶端詳情信息寫死在這⾥或者是通過數據庫來存儲調取詳情信息
confifigure(AuthorizationServerEndpointsConfifigurer endpoints):⽤來配置令牌(token)的訪問端點和令牌服務(token services)
confifigure(AuthorizationServerSecurityConfifigurer oauthServer):⽤來配置令牌端點的安全約束.
關于 TokenStoreInMemoryTokenStore默認采⽤,它可以完美的⼯作在單服務器上(即訪問并發量 壓⼒不⼤的情況下,并且它在失敗的時候不會進⾏備份),⼤多數的項⽬都可以使⽤這個版本的實現來進⾏ 嘗試,你可以在開發的時候使⽤它來進⾏管理,因為不會被保存到磁盤中,所以更易于調試。
JdbcTokenStore這是⼀個基于JDBC的實現版本,令牌會被保存進關系型數據庫。使⽤這個版本的實現時, 你可以在不同的服務器之間共享令牌信息,使⽤這個版本的時候請注意把"springjdbc"這個依賴加⼊到你的 classpath當中。JwtTokenStore 這個版本的全稱是 JSON Web Token(JWT),它可以把令牌相關的數據進⾏編碼(因此對于後端服務來說,它不需要進⾏存儲,這将是⼀個重⼤優勢),缺點就是這個令牌占⽤的空間會⽐較⼤,如果你加⼊了⽐較多⽤戶憑證信息,JwtTokenStore 不會保存任何數據。
然後再自定義有一個配置類,主要處理用戶名和密碼的校驗等事宜。
@Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
//@Autowired
//private JdbcUserDetailsService jdbcUserDetailsService;
/**
* 注冊一個認證管理器對象到容器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密碼編碼對象(密碼不進行加密處理)
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/**
* 處理用戶名和密碼驗證事宜
* 1)客戶端傳遞username和password參數到認證服務器
* 2)一般來說,username和password會存儲在數據庫中的用戶表中
* 3)根據用戶表中數據,驗證當前傳遞過來的用戶信息的合法性
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在這個方法中就可以去關聯數據庫了,當前我們先把用戶信息配置在内存中
// 實例化一個用戶對象(相當于數據表中的一條用戶記錄)
UserDetails user = new User("admin","123456",new ArrayList<>());
auth.inMemoryAuthentication()
.withUser(user).passwordEncoder(passwordEncoder);
//auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
}
}
JWT 改造統⼀認證授權中⼼的令牌存儲機制
JWT 令牌介紹
通過上邊的測試我們發現,當資源服務和授權服務不在⼀起時資源服務使⽤RemoteTokenServices 遠程請求授權 服務驗證token,如果訪問量較⼤将會影響系統的性能。
解決上邊問題: 令牌采⽤JWT格式即可解決上邊的問題,⽤戶認證通過會得到⼀個JWT令牌,JWT令牌中已經包括了⽤戶相關的信 息,客戶端隻需要攜帶JWT訪問資源服務,資源服務根據事先約定的算法⾃⾏完成令牌校驗,⽆需每次都請求認證 服務完成授權。
什麼是JWT?JSON Web Token(JWT)是⼀個開放的⾏業标準(RFC 7519),它定義了⼀種簡介的、⾃包含的協議格式,⽤于 在通信雙⽅傳遞json對象,傳遞的信息經過數字簽名可以被驗證和信任。JWT可以使⽤HMAC算法或使⽤RSA的公 鑰/私鑰對來簽名,防⽌被篡改。
JWT令牌結構JWT 令牌由三部分組成,每部分中間使⽤點(.)分隔,⽐如:xxxxx.yyyyy.zzzzz
header。頭部包括令牌的類型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA),例如
{
"alg": "HS256",
"typ": "JWT"
}
将上邊的内容使⽤Base64Url編碼,得到⼀個字符串就是JWT令牌的第⼀部分。
Payload。第⼆部分是負載,内容也是⼀個json對象,它是存放有效信息的地⽅,它可以存放jwt提供的現成字段,⽐ 如:iss(簽發者),exp(過期時間戳), sub(⾯向的⽤戶)等,也可⾃定義字段。 此部分不建議存放敏感信息,因為此部分可以解碼還原原始内容。 最後将第⼆部分負載使⽤Base64Url編碼,得到⼀個字符串就是JWT令牌的第⼆部分。 ⼀個例⼦:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature。第三部分是簽名,此部分⽤于防⽌jwt内容被篡改。 這個部分使⽤base64url将前兩部分進⾏編碼,編碼後使⽤點(.)連接組成字符串,最後使⽤header中聲明 簽名算法進⾏簽名。
HMACSHA256(
base64UrlEncode(header) "."
base64UrlEncode(payload),
secret)
secret:簽名所使⽤的密鑰。
認證服務器端JWT改造(改造主配置類)
/*
該方法用于創建tokenStore對象(令牌存儲對象)
token以什麼形式存儲
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌轉換器(幫助我們生成jwt令牌的)
* 在這裡,我們可以把簽名密鑰傳遞進去給轉換器對象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 簽名密鑰
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 驗證時使用的密鑰,和簽名密鑰保持一緻
jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
return jwtAccessTokenConverter;
}
修改 JWT 令牌服務⽅法
/**
* 該方法用戶獲取一個token服務對象(該對象描述了token有效期等信息)
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默認實現
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true); // 是否開啟令牌刷新
defaultTokenServices.setTokenStore(tokenStore());
// 針對jwt令牌的添加
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
// 設置令牌有效時間(一般設置為2個小時)
defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我們請求資源需要攜帶的令牌
// 設置刷新令牌的有效時間
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
我們在實際工作中,token 鑒權的方式是很常見的現在,這一套解決方案也可以直接使用到項目中,小夥伴們趕緊學習起來吧。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!