1.?微服務網關整合?OAuth2.0?設計思路分析
網關整合 OAuth2.0 ?有兩種思路,一種是授權服務器生成令牌, ?所有請求統一?在網關層驗證,判斷權限等操作;另一種是由各資源服務處理,網關只做請求?轉發?。??比較常用的是第一種,把?API?網關作為?OAuth2.0?的資源服務器角?色,實現接入客戶端權限攔截、令牌解析并轉發當前登錄用戶信息給微服務,?這樣下游微服務就不需要關心令牌格式解析以及?OAuth2.0 相關機制了。
網關在認證授權體系里主要負責兩件事:
?(1)作為?OAuth2.0?的資源服務器?角色,實現接入方訪問權限攔截。
?(2)令牌解析并轉發當前登錄用戶信息?(明文?token)給微服務
微服務拿到明文?token(明文?token?中包含登錄用戶的?身份和權限信息)后也需要做兩件事:
?(1)用戶授權攔截(看當前用戶是否有?權訪問該資源)?
? (2)將用戶信息存儲進當前線程上下文(有利于后續業務邏?輯隨時獲取當前用戶信息)
?
2.?搭建微服務授權中心
授權中心的認證依賴:
- ????????第三方客戶端的信息
- ????????微服務的信息
- ????????登錄用戶的信息
創建微服務?tulingmall-authcenter
2.1?引入依賴?
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId>
</dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId>
</dependency>
2.2?添加 yml 配置?
server:port: 9999
spring:application:name: tulingmall-authcenter#配置nacos注冊中心地址cloud:nacos:discovery:server-addr: 192.168.65.103:8848 #注冊中心地址namespace: 6cd8d896-4d19-4e33-9840-26e4bee9a618 #環境隔離datasource:url: jdbc:mysql://tuling.com:3306/tlmall_oauth?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8username: rootpassword: rootdruid:initial-size: 5 #連接池初始化大小min-idle: 10 #最小空閑連接數max-active: 20 #最大連接數web-stat-filter:exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" #不統計這些請求數據stat-view-servlet: #訪問監控網頁的登錄用戶名和密碼login-username: druidlogin-password: druid
2.3?配置授權服務器
基于 DB 模式配置授權服務器存儲第三方客戶端的信息
@Configuration
@EnableAuthorizationServer
public class TulingAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate DataSource dataSource;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {// 配置授權服務器存儲第三方客戶端的信息 基于DB存儲 oauth_client_detailsclients.withClientDetails(clientDetails());}@Beanpublic ClientDetailsService clientDetails(){return new JdbcClientDetailsService(dataSource);}}
?在?oauth_client_details?中添加第三方客戶端信息(client_id????client_secret?????scope?等等)
CREATE TABLE `oauth_client_details` (`client_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`access_token_validity` int(11) NULL DEFAULT NULL,`refresh_token_validity` int(11) NULL DEFAULT NULL,`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
?
基于內存模式配置授權服務器存儲第三方客戶端的信息?
//TulingAuthorizationServerConfig.java
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {// 配置授權服務器存儲第三方客戶端的信息 基于DB存儲 oauth_client_details// clients.withClientDetails(clientDetails());/***授權碼模式*http://localhost:9999/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all** password模式* http://localhost:8080/oauth/token?username=fox&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all**/clients.inMemory()//配置client_id.withClient("client")//配置client-secret.secret(passwordEncoder.encode("123123"))//配置訪問token的有效期.accessTokenValiditySeconds(3600)//配置刷新token的有效期.refreshTokenValiditySeconds(864000)//配置redirect_uri,用于授權成功后跳轉.redirectUris("http://www.baidu.com")//配置申請的權限范圍.scopes("all")/*** 配置grant_type,表示授權類型* authorization_code: 授權碼* password: 密碼* refresh_token: 更新令牌*/.authorizedGrantTypes("authorization_code","password","refresh_token");}
2.4 ?配置?SpringSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Autowiredprivate TulingUserDetailsService tulingUserDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 實現UserDetailsService獲取用戶信息auth.userDetailsService(tulingUserDetailsService);}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {// oauth2 密碼模式需要拿到這個beanreturn super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().permitAll().and().authorizeRequests().antMatchers("/oauth/**").permitAll().anyRequest().authenticated().and().logout().permitAll().and().csrf().disable(); }
}
?獲取會員信息?,此處通過 feign?從 tulingmall-member?獲取會員信息?,需要配置?feign?,核心代碼:
@Slf4j
@Component
public class TulingUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 加載用戶信息if(StringUtils.isEmpty(username)) {log.warn("用戶登陸用戶名為空:{}",username);throw new UsernameNotFoundException("用戶名不能為空");}UmsMember umsMember = getByUsername(username);if(null == umsMember) {log.warn("根據用戶名沒有查詢到對應的用戶信息:{}",username);}log.info("根據用戶名:{}獲取用戶登陸信息:{}",username,umsMember);// 會員信息的封裝 implements UserDetailsMemberDetails memberDetails = new MemberDetails(umsMember);return memberDetails;}@Autowiredprivate UmsMemberFeignService umsMemberFeignService;public UmsMember getByUsername(String username) {// fegin獲取會員信息CommonResult<UmsMember> umsMemberCommonResult = umsMemberFeignService.loadUserByUsername(username);return umsMemberCommonResult.getData();}
}@FeignClient(value = "tulingmall-member",path="/member/center")
public interface UmsMemberFeignService {@RequestMapping("/loadUmsMember")CommonResult<UmsMember> loadUserByUsername(@RequestParam("username") String username);
}public class MemberDetails implements UserDetails {private UmsMember umsMember;public MemberDetails(UmsMember umsMember) {this.umsMember = umsMember;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {//返回當前用戶的權限return Arrays.asList(new SimpleGrantedAuthority("TEST"));}@Overridepublic String getPassword() {return umsMember.getPassword();}@Overridepublic String getUsername() {return umsMember.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return umsMember.getStatus()==1;}public UmsMember getUmsMember() {return umsMember;}
}
修改授權服務配置,支持密碼模式
//TulingAuthorizationServerConfig.java @Autowiredprivate TulingUserDetailsService tulingUserDetailsService;@Autowiredprivate AuthenticationManager authenticationManagerBean;@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {//使用密碼模式需要配置endpoints.authenticationManager(authenticationManagerBean).reuseRefreshTokens(false) //refresh_token是否重復使用.userDetailsService(tulingUserDetailsService) //刷新令牌授權包含對用戶信息的檢查.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST請求}/*** 授權服務器安全配置* @param security* @throws Exception*/@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {//第三方客戶端校驗token需要帶入 clientId 和clientSecret來校驗security.checkTokenAccess("isAuthenticated()").tokenKeyAccess("isAuthenticated()");//來獲取我們的tokenKey需要帶入clientId,clientSecret//允許表單認證security.allowFormAuthenticationForClients();}
?2.5?測試模擬用戶登錄
授權碼模式
授權碼(authorization?code)方式,指的是第三方應用先申請一個授權碼,然?后再用該碼獲取令牌。
這種方式是最常用的流程,安全性也最高,它適用于那些有后端的 Web??應?用。授權碼通過前端傳送,令牌則是儲存在后端,而且所有與資源服務器的通?信都在后端完成。這樣的前后端分離,可以避免令牌泄漏。
適用場景:?目前市面上主流的第三方驗證都是采用這種模式
它的步驟如下:
(A)用戶訪問客戶端,后者將前者導向授權服務器。
(B)用戶選擇是否給予客戶端授權。
(C)假設用戶給予授權,授權服務器將用戶導向客戶端事先指定的"重定向URI"( redirection URI),同時附上一個授權碼。
(D)客戶端收到授權碼,附上早先的"重定向?URI"?,向授權服務器申請令 牌。這一步是在客戶端的后臺的服務器上完成的,對用戶不可見。
(E)授權服務器核對了授權碼和重定向?URI?,確認無誤后,向客戶端發送?訪問令牌(access token)和更新令牌(?refresh token)。
http://localhost:9999/oauth/authorize?response_type=code&client_id=client?&redirect_uri=http://www.baidu.com&scope=all
獲取到?code
?
密碼模式
如果你高度信任某個應用,RFC?6749 ?也允許用戶把用戶名和密碼,直接告訴該?應?用?。?該?應?用就?使?用你?的?密?碼?,?申?請?令?牌?,?這?種?方?式?稱?為?" 密?碼?式?"?(password)。
在這種模式中,用戶必須把自己的密碼給客戶端,但是客戶端不得儲存密碼。?這通常用在用戶對客戶端高度信任的情況下,?比如客戶端是操作系統的一部?分,或者由一個著名公司出品。而授權服務器只有在其他授權模式無法執行的?情況下,才能考慮使用這種模式。
適用場景:?自家公司搭建的授權服務器
測試獲取?token
http://localhost:9999/oauth/token?username=test&password=test&grant_type=password&client_id=clien?t&client_secret=?123123&scope=all
測試校驗?token?接口?
?因為授權服務器的?security?配置需要攜帶?clientId?和?clientSecret?,可以采用?basic?Auth??的方?式發請求
?注意:?傳參是?token
2.6?配置資源服務器
@Configuration
@EnableResourceServer
public class TulingResourceServerConfig extends ResourceServerConfigurerAdapter {@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated();}
}@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/getCurrentUser")public Object getCurrentUser(Authentication authentication) {return authentication.getPrincipal();}
}
測試攜帶token?訪問資源
或者請求頭配置?Authorization
OAuth?2.0?是當前業界標準的授權協議,它的核心是若干個針對不同場景的令牌頒發和管?理流程;而?JWT?是一種輕量級、?自包含的令牌,可用于在微服務間安全地傳遞用戶信息。
2.7 Spring Security Oauth2 整合 JWT
JSON Web Token(JWT)是一個開放的行業標準(RFC?7519),它定義了一種簡介的、?自包含的協議格式,用于在通信雙方傳遞?json?對象,傳遞的信息經過數?字簽名可以被驗證和信任。JWT?可以使用?HMAC?算法或使用?RSA?的公鑰/私鑰對?來簽名,防止被篡改。 官網:JSON Web Tokens - jwt.io
JWT?令牌的優點:
- ????????jwt 基于?json?,非常方便解析。
- ????????可以在令牌中自定義豐富的內容,易擴展。
- ????????通過非對稱加密算法及數字簽名技術,JWT?防止篡改,安全性高。
- ????????資源服務使用JWT?可不依賴認證服務即可完成授權。
- ????????通過非對稱加密算法及數字簽名技術,JWT?防止篡改,安全性高。
- ????????可以在令牌中自定義豐富的內容,易擴展。
缺點:
????????JWT?令牌較長,?占存儲空間比較大。
JWT:指的是 JSON?Web?Token?,?由?header.payload.signture???組成。不存在簽名的?JWT?是?不安全的,存在簽名的?JWT?是不可竄改的。
JWS:指的是簽過名的?JWT?,即擁有簽名的?JWT。
JWK:既然涉及到簽名,就涉及到簽名算法,對稱加密還是非對稱加密,那么就需要加密?的 密鑰或者公私鑰對。此處我們將 JWT??的密鑰或者公私鑰對統一稱為 JSON?WEB?KEY?,即?JWK。
JWT?組成
一個?JWT?實際上就是一個字符串,它由三部分組成,頭部(header)?、載荷?(payload)與簽名(signature)。
頭部(header)
頭部用于描述關于該?JWT?的最基本的信息:類型(即?JWT)以及簽名所用的?算法(如?HMACSHA256 或?RSA)等。
這也可以被表示成一個?JSON 對象:
{"alg": "HS256","typ": "JWT"
}
?然后將頭部進行?base64 加密(該加密是可以對稱解密的),構成了第一部分:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9?
載荷(payload)
第二部分是載荷,就是存放有效信息的地方。這個名字像是特指飛機上承載的?貨品,這些有效信息包含三個部分:
- 標準中注冊的聲明(建議但不強制使用)
????????iss: jwt 簽發者
????????sub: jwt 所面向的用戶
????????aud:?接收?jwt?的一方
????????exp:?jwt?的過期時間,這個過期時間必須要大于簽發時間
????????nbf: ?定義在什么時間之前,該?jwt 都是不可用的.
????????iat: jwt?的簽發時間
????????jti:?jwt?的唯一身份標識,主要用來作為一次性?token,從而回避重放攻擊。
- 公共的聲明
????????公共的聲明可以添加任何的信息,一般添加用戶的相關信息或?其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.
- 私有的聲明
????????私有聲明是提供者和消費者所共同定義的聲明,一般不建議存?放敏感信息,因為?base64 是對稱解密的,意味著該部分信息可以歸類為明文?信息。
定義一個?payload:
{"sub": "1234567890","name": "John Doe","iat": 1516239022
}
?然后將其進行?base64 加密,得到?Jwt?的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ???????
簽名(signature)
jwt?的第三部分是一個簽證信息,這個簽證信息由三部分組成:
- header (base64 后的)
- payload (base64 后的)
- secret(鹽,一定要保密)
這個部分需要?base64 加密后的?header 和?base64 加密后的?payload?使用.連接?組成的字符串,然后通過?header?中聲明的加密方式進行加鹽?secret?組合加?密,然后就構成了?jwt?的第三部分:
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'fox'); // khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME
?將這三部分用.連接成一個完整的字符串,構成了最終的?jwt:
?eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME
注意:secret?是保存在服務器端的,jwt?的簽發生成也是在服務器端的,secret 就是用來進行?jwt?的簽發和?jwt?的驗證,所以,它就是你服務端的私鑰,在任何?場景都不應該流露出去。一旦客戶端得知這個?secret, ?那就意味著客戶端是可以 自我簽發?jwt?了。
JWT 應用場景???????
- 一次性驗證
????????比如用戶注冊后需要發一封郵件讓其激活賬戶,通常郵件中需要有一個鏈接,這個鏈接需?要具備以下的特性:能夠標識用戶,該鏈接具有時效性〈(通常只允許幾小時之內激活)?,不?能被篡改以激活其他可能的賬戶…這種場景就和?jwt?的特性非常貼近,jwt?的 payload???中固定?的參數:?iss?簽發者和?exp?過期時間正是為其做準備的。
- restful?api?的無狀態認證
????????使用?jwt?來做?restful?api?的身份認證也是值得推崇的一種使用方案。客戶端和服務端共享?secret;過期時間由服務端校驗,客戶端定時刷新;簽名信息不可被修改。
- 使用?jwt?做單點登錄+會話管理(不推薦) ??????????token+redis
????????jwt?是無狀態的,在處理注銷,續約問題上會變得非常復雜
引入依賴
<!--spring secuity對jwt的支持 spring cloud oauth2已經依賴,可以不配置-->
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-jwt</artifactId><version>1.0.9.RELEASE</version>
</dependency>
?添加?JWT?配置
@Configuration
public class JwtTokenStoreConfig {@Beanpublic TokenStore jwtTokenStore(){return new JwtTokenStore(jwtAccessTokenConverter());}@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter(){JwtAccessTokenConverter accessTokenConverter = newJwtAccessTokenConverter();//配置JWT使用的秘鑰accessTokenConverter.setSigningKey("123123");return accessTokenConverter;}
}
?在授權服務器配置中指定令牌的存儲策略為?JWT
//TulingAuthorizationServerConfig.java@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;@Autowired
private TulingUserDetailsService tulingUserDetailsService;@Autowired
private AuthenticationManager authenticationManagerBean;@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {//使用密碼模式需要配置endpoints.authenticationManager(authenticationManagerBean).tokenStore(tokenStore) //指定token存儲策略是jwt.accessTokenConverter(jwtAccessTokenConverter).reuseRefreshTokens(false) //refresh_token是否重復使用.userDetailsService(tulingUserDetailsService) //刷新令牌授權包含對用戶信息的檢查.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST請求
}
密碼模式測試:
http://localhost:9999/oauth/token?username=test&password=test&grant_type=password&client_id=clien?t&client_secret=123123&scope=all
將?access_token?復制到?JSON Web Tokens - jwt.io的?Encoded?中打開,可以看到會員認證信息
?測試校驗?token
測試獲取?token_key?
?
測試刷新?token