Spring Cloud 微服務中搭建 OAuth2.0 認證授權服務

Spring Cloud 微服務中搭建 OAuth2.0 認證授權服務

在使用 Spring Cloud 體系來構建微服務的過程中,用戶請求是通過網關(ZUUL 或 Spring APIGateway)以 HTTP 協議來傳輸信息,API 網關將自己註冊為 Eureka 服務治理下的應用,同時也從 Eureka 服務中獲取所有其他微服務的實例信息。搭建 OAuth2 認證授權服務,並不是給每個微服務調用,而是通過 API 網關進行統一調用來對網關後的微服務做前置過濾,所有的請求都必須先通過 API 網關,API 網關在進行路由轉發之前對該請求進行前置校驗,實現對微服務系統中的其他的服務接口的安全與權限校驗。對於微服務安全認證授權機制一塊,目前主流的解決方案有 OAuth2.0 與OIDC(OpenID Connect) 等標準協議。

OAuth2 是一個開放授權標準協議它允許用戶讓第三方應用訪問該用戶在某服務的特定私有資源,但是不提供賬號密碼信息給第三方應用。

 +--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| | | | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| | | | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| | +--------+ +---------------+
Figure 1: Abstract Protocol Flow

完整授權流程中有四個重要的角色[ RFC 6749 ]:

  1. 資源擁有者(resource owner):能授權訪問受保護資源的一個實體,可以是一個人,那我們稱之為最終用戶;
  2. 資源服務器(resource server):存儲受保護資源,客戶端通過access token請求資源,資源服務器響應受保護資源給客戶端;
  3. 授權服務器(authorization server):成功驗證資源擁有者並獲取授權之後,授權服務器頒發授權令牌(Access Token)給客戶端。
  4. 客戶端(client):第三方應用,也可以是它自己的官方應用;其本身不存儲資源,而是資源擁有者授權通過後,使用它的授權(授權令牌)訪問受保護資源,然後客戶端把相應的數據展示出來/提交到服務器。

OAuth2.0 協議根據使用不同的適用場景,定義了用於四種授權模式。

Authorization code(授權碼模式)

標準的 Server 授權模式,非常適合 Server 端的 Web 應用。一旦資源的擁有者授權訪問他們的數據之後,他們將會被重定向到 Web 應用並在 URL 的查詢參數中附帶一個授權碼(code)。在客戶端裡,該 code 用於請求訪問令牌(access_token)。並且該令牌交換的過程是兩個服務端之前完成的,防止其他人甚至是資源擁有者本人得到該令牌。另外,在該授權模式下可以通過 refresh_token 來刷新令牌以延長訪問授權時間,也是最為複雜的一種方式。

Implicit Grant(隱式模式)

該模式是所有授權模式中最簡單的一種,併為運行於瀏覽器中的腳本應用做了優化。當用戶訪問該應用時,服務端會立即生成一個新的訪問令牌(access_token)並通過URL的#hash段傳回客戶端。這時,客戶端就可以利用JavaScript等將其取出然後請求API接口。該模式不需要授權碼(code),當然也不會提供refresh token以獲得長期訪問的入口。

Resource Owner Password Credentials(密碼模式)

自己有一套用戶體系,這種模式要求用戶提供用戶名和密碼來交換訪問令牌(access_token)。該模式僅用於非常值得信任的用戶,例如API提供者本人所寫的移動應用。雖然用戶也要求提供密碼,但並不需要存儲在設備上。因為初始驗證之後,只需將 OAuth 的令牌記錄下來即可。如果用戶希望取消授權,因為其真實密碼並沒有被記錄,因此無需修改密碼就可以立即取消授權。token本身也只是得到有限的授權,因此相比最傳統的 username/password 授權,該模式依然更為安全。

Client Credentials(客戶端模式)

沒有用戶的概念,一種基於 APP 的密鑰直接進行授權,因此 APP 的權限非常大。它適合像數據庫或存儲服務器這種對 API 的訪問需求。

備註:理解 OAuth 2.0

OAuth2.0 與 OpenID Connect 開源的框架

JAVA 中開源的認證與授權框架比較知名的有 Apereo CAS,Spring Cloud Security,JBoss 開源的 Keycloak 等(.NET IdentityServer4 )。

Central Authentication Service (CAS) 通常稱為 CAS。 CAS是一種針對Web的企業多語言單點登錄解決方案,並嘗試成為您的身份驗證和授權需求的綜合平臺。

下面是官方的一段簡述:

CAS Enterprise Single Sign-On

  • Spring Webflow/Spring Boot Java server component.
  • 可拔插認證支持 (LDAP, Database, X.509, SPNEGO, JAAS, JWT, RADIUS, MongoDb, etc)
  • 多種協議支持 (CAS, SAML, WS-Federation, OAuth2, OpenID, OpenID Connect, REST)
  • 通過各種提供商支持多因素身份驗證 (Duo Security, FIDO U2F, YubiKey, Google Authenticator, Microsoft Azure, Authy etc)
  • 支持外部提供者的委託認證,例如: ADFS, Facebook, Twitter, SAML2 IdPs, etc.
  • Built-in support for password management, notifications, terms of use and impersonation.
  • Support for attribute release including user consent.
  • 實時監控和跟蹤應用程序行為,統計信息和日誌。
  • 用特定的認證策略管理和註冊客戶端應用程序和服務。
  • 跨平臺的客戶端支持 (Java, .Net, PHP, Perl, Apache, etc).
  • Integrations with InCommon, Box, Office365, ServiceNow, Salesforce, Workday, WebAdvisor, Drupal, Blackboard, Moodle, Google Apps, etc.

Spring Security OAuth 是建立在 Spring Security 的基礎之上 OAuth2.0 協議實現的一個類庫,它提供了構建 Authorization Server、Resource Server 和 Client 三種 Spring 應用程序角色所需要的功能。

Keycloak 官方語言來解釋,“為現代應用系統和服務提供開源的鑑權和授權訪問控制管理”。Keycloak 實現了OpenID,Auth2.0,SAML單點登錄協議,同時提供LDAP和Active Directory,以及OpenID Connect, SAML2.0 IdPs,Github,Google 等第三方登錄適配功能,能夠做到非常簡單的開箱即用。

備註:從 4.1 版開始,Spring Boot starter 將基於 Spring Boot 2 adapter。如果您使用的是較舊的 Spring Boot 版本,則可以使用 keycloak-legacy-spring-boot-starter。

Spring Security OAuth2 框架

下面使用 Spring Security OAuth2 為 Spring Cloud 搭建認證授權服務(能夠更好的集成到 Spring Cloud 體系中)。

  • ClientDetailsServiceConfigurer:定義客戶詳細信息服務的配置器。客戶端詳細信息可以被初始化,或者您可以直接引用一個現有的存儲。(client_id ,client_secret,redirect_uri 等配置信息)。
  • AuthorizationServerSecurityConfigurer:用來配置令牌端點的安全約束。
  • AuthorizationServerEndpointsConfigurer:用來配置授權以及令牌的訪問端點和令牌服務(比如:配置令牌的簽名與存儲方式)

Authorization Server

在 Authorization Server 的角色中 Spring Security OAuth2 定義了 AuthorizationServerConfigurerAdapter 配置類

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
public AuthorizationServerConfigurerAdapter() {
}
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
  • ClientDetailsServiceConfigurer:用來配置客戶端詳情信息,一般使用數據庫來存儲或讀取應用配置的詳情信息(client_id ,client_secret,redirect_uri 等配置信息)。
  • AuthorizationServerSecurityConfigurer:用來配置令牌端點(Token Endpoint)的安全與權限訪問。
  • AuthorizationServerEndpointsConfigurer:用來配置授權以及令牌(Token)的訪問端點和令牌服務(比如:配置令牌的簽名與存儲方式)
  • Resource Server
  • 在 Resource Server 的角色中 Spring Security OAuth2 定義了 ResourceServerConfigurerAdapter 配置類
public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {
public ResourceServerConfigurerAdapter() {
}
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
}
public void configure(HttpSecurity http) throws Exception {
((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
}
}
public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {
public ResourceServerConfigurerAdapter() {
}
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
}
public void configure(HttpSecurity http) throws Exception {
((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
}
}
  • ResourceServerConfigurerAdapter 用於保護 OAuth2 要開放的資源,同時主要作用於client端以及token的認證(Bearer Auth),由於後面 OAuth2 服務端後續還需要提供用戶信息,所以也是一個 Resource Server,默認攔截了所有的請求,也可以通過重新方法方式自定義自己想要攔截的資源 URL 地址。
  • 另外根據 OAuth2.0 規範,獲取票據要支持 Basic 驗證與驗證用戶的賬戶信息,比如密碼模式:
 POST /token HTTP/1.1 

Host: server.example.com
Authorization: Basic 1sZCaJks20MzpnMsPOi
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=irving&password=123456
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic 1sZCaJks20MzpnMsPOi
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=irving&password=123456
  • 可以在 WebSecurityConfigurerAdapter 類中重新相應的方法來實現。
  • AuthorizationServerConfigurerAdapter
  • ResourceServerConfigurerAdapter
  • WebSecurityConfigurerAdapter
  • Client
  • 根據 OAuth2.0 規範定義獲得票據需要提供 client_id 與 client_secret ,這個過程需要在服務端申請獲得,比我新浪與騰訊的聯合登錄就是採用的授權碼模式。一般還是要根據適用的場景給與不同的配置與作用域。
 /*
* 配置客戶端詳情信息(內存或JDBC來實現)
*
* */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//初始化 Client 數據到 DB
clients.jdbc(dataSource)
// clients.inMemory()
.withClient("client_1")

.authorizedGrantTypes("client_credentials")
.scopes("all","read", "write")
.authorities("client_credentials")
.accessTokenValiditySeconds(7200)
.secret(passwordEncoder.encode("123456"))
.and().withClient("client_2")
.authorizedGrantTypes("password", "refresh_token")
.scopes("all","read", "write")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(10000)
.authorities("password")
.secret(passwordEncoder.encode("123456"))
.and().withClient("client_3").authorities("authorization_code","refresh_token")
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("authorization_code")
.scopes("all","read", "write")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(10000)
.redirectUris("http://localhost:8080/callback","http://localhost:8080/signin")
.and().withClient("client_test")
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("all flow")
.authorizedGrantTypes("authorization_code", "client_credentials", "refresh_token","password", "implicit")
.redirectUris("http://localhost:8080/callback","http://localhost:8080/signin")
.scopes("all","read", "write")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(10000);
//https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
// clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}

  • 理解上述說的關係後,就可以來實現 OAuth2.0 的相關服務了。
  • MAVEN
 
org.springframework.boot

spring-boot-starter-parent
2.0.3.RELEASE



UTF-8
UTF-8
1.8
Finchley.RELEASE




org.springframework.cloud
spring-cloud-starter-oauth2


org.springframework.cloud
spring-cloud-starter-security


org.springframework.cloud
spring-cloud-starter-netflix-eureka-client


org.springframework.cloud
spring-cloud-starter-netflix-hystrix







org.springframework.boot
spring-boot-starter-jdbc


mysql

mysql-connector-java
8.0.11


org.springframework.boot
spring-boot-starter-test
test


SpringApplication

@

SpringCloudApplication //@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker
public class MicrosrvOauth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(MicrosrvOauth2ServerApplication.class, args);
}
}

/

*
[/oauth/authorize]
[/oauth/token]
[/oauth/check_token]
[/oauth/confirm_access]
[/oauth/token_key]
[/oauth/error]
*/
@Configuration

@EnableAuthorizationServer
//@Order(2)
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/*
@Autowired
private RedisConnectionFactory connectionFactory;
@Bean
public RedisTokenStore tokenStore() {
return new RedisTokenStore(connectionFactory);
}
*/
@Autowired
@Qualifier("dataSource")
private DataSource dataSource;
// @Bean(name = "dataSource")
// @ConfigurationProperties(prefix = "spring.datasource")
// public DataSource dataSource() {
// return DataSourceBuilder.create().build();
// }
@Bean("jdbcTokenStore")
public JdbcTokenStore getJdbcTokenStore() {
return new JdbcTokenStore(dataSource);
}
// @Bean
// public UserDetailsService userDetailsService(){
// return new UserService();
// }
/*
* 配置客戶端詳情信息(內存或JDBC來實現)
*
* */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//初始化 Client 數據到 DB
clients.jdbc(dataSource)
// clients.inMemory()
.withClient("client_1")
.authorizedGrantTypes("client_credentials")
.scopes("all","read", "write")
.authorities("client_credentials")
.accessTokenValiditySeconds(7200)
.secret(passwordEncoder.encode("123456"))
.and().withClient("client_2")
.authorizedGrantTypes("password", "refresh_token")

.scopes("all","read", "write")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(10000)
.authorities("password")
.secret(passwordEncoder.encode("123456"))
.and().withClient("client_3").authorities("authorization_code","refresh_token")
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("authorization_code")
.scopes("all","read", "write")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(10000)
.redirectUris("http://localhost:8080/callback","http://localhost:8080/signin")
.and().withClient("client_test")
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("all flow")
.authorizedGrantTypes("authorization_code", "client_credentials", "refresh_token","password", "implicit")
.redirectUris("http://localhost:8080/callback","http://localhost:8080/signin")
.scopes("all","read", "write")
.accessTokenValiditySeconds(7200)
.refreshTokenValiditySeconds(10000);
//https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
// clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// endpoints
// .tokenStore(new RedisTokenStore(redisConnectionFactory))
// .authenticationManager(authenticationManager);
endpoints.authenticationManager(authenticationManager)
//配置 JwtAccessToken 轉換器
// .accessTokenConverter(jwtAccessTokenConverter())
//refresh_token 需要 UserDetailsService is required
// .userDetailsService(userDetailsService)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.tokenStore(getJdbcTokenStore());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
//curl -i -X POST -H "Accept: application/json" -u "client_1:123456" http://localhost:5000/oauth/check_token?token=a1478d56-ebb8-4f21-b4b6-8a9602df24ec
oauthServer.tokenKeyAccess("permitAll()") //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
.checkTokenAccess("isAuthenticated()") //url:/oauth/check_token allow check token
.allowFormAuthenticationForClients();
}
/**
* 使用非對稱加密算法來對Token進行簽名
* @return
*/
@Bean

public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(
new ClassPathResource("keystore.jks"), "foobar".toCharArray())
.getKeyPair("test");
converter.setKeyPair(keyPair);
return converter;
}
}
/*
* 提供 user 信息,所以 oauth2-server 也是一個Resource Server
* */
@Configuration
@EnableResourceServer
//@Order(3)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
// @Override
// public void configure(HttpSecurity http) throws Exception {
// http
// // Since we want the protected resources to be accessible in the UI as well we need
// // session creation to be allowed (it's disabled by default in 2.0.6)
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
// .and()
// .requestMatchers().anyRequest()
// .and()
// .anonymous()
// .and()
// .authorizeRequests()
//// .antMatchers("/product/**").access("#oauth2.hasScope('select') and hasRole('ROLE_USER')")
// .antMatchers("/user/**").authenticated();//必須認證過後才可以訪問
// }
// @Override
// public void configure(HttpSecurity http) throws Exception {
// http.requestMatchers().anyRequest()
// .and()
// .authorizeRequests()
// .antMatchers("/api/**").authenticated();
// }
}
@Configuration
@EnableWebSecurity
//@Order(1)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean

public UserDetailsService userDetailsService(){
return new UserService();
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("irving")
.password(passwordEncoder().encode("123456"))
.roles("read");
// auth.userDetailsService(userDetailsService())
// .passwordEncoder(passwordEncoder());
}
// @Bean
// public static NoOpPasswordEncoder passwordEncoder() {
// return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
// }
@Override
protected void configure(HttpSecurity http) throws Exception {
// http
// .formLogin().loginPage("/login").permitAll()
// .and()
// .requestMatchers()
// .antMatchers("/", "/login", "/oauth/authorize", "/oauth/confirm_access")
// .and()
// .authorizeRequests()
// .anyRequest().authenticated();
// http.requestMatchers()
// .antMatchers("/login", "/oauth/authorize")
// .and()
// .authorizeRequests()
// .anyRequest().authenticated()
// .and()
// .formLogin().permitAll();
// http.csrf().disable();
//不攔截 oauth 開放的資源
http.requestMatchers()
.anyRequest()
.and()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

}
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/me")
public Principal user(Principal principal) {
return principal;
}
@GetMapping("/{name}")
public String getUserName(@PathVariable String name) {
return "hello,"+ name;
}
}
  • application.yml
#logging:
# level:
# root: DEBUG
logging:
level:
org.springframework: INFO #INFO
org.springframework.security: DEBUG
spring:
application:
name: microsrv-oauth2-server
datasource:
url: jdbc:mysql://XXX.XXX.XXX.XXX:3306/oauth2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
username: root
password: "!TEST"
driver: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minIdle: 10
idle-timeout: 10000
maximumPoolSize: 30
server:
port: 5000
config:
oauth2:
# openssl genrsa -out jwt.pem 2048
# openssl rsa -in jwt.pem
privateKey: |
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDNQZKqTlO/+2b4ZdhqGJzGBDltb5PZmBz1ALN2YLvt341pH6i5

mO1V9cX5Ty1LM70fKfnIoYUP4KCE33dPnC7LkUwE/myh1zM6m8cbL5cYFPyP099t
hbVxzJkjHWqywvQih/qOOjliomKbM9pxG8Z1dB26hL9dSAZuA8xExjlPmQIDAQAB
AoGAImnYGU3ApPOVtBf/TOqLfne+2SZX96eVU06myDY3zA4rO3DfbR7CzCLE6qPn
yDAIiW0UQBs0oBDdWOnOqz5YaePZu/yrLyj6KM6Q2e9ywRDtDh3ywrSfGpjdSvvo
aeL1WesBWsgWv1vFKKvES7ILFLUxKwyCRC2Lgh7aI9GGZfECQQD84m98Yrehhin3
fZuRaBNIu348Ci7ZFZmrvyxAIxrV4jBjpACW0RM2BvF5oYM2gOJqIfBOVjmPwUro
bYEFcHRvAkEAz8jsfmxsZVwh3Y/Y47BzhKIC5FLaads541jNjVWfrPirljyCy1n4
sg3WQH2IEyap3WTP84+csCtsfNfyK7fQdwJBAJNRyobY74cupJYkW5OK4OkXKQQL
Hp2iosJV/Y5jpQeC3JO/gARcSmfIBbbI66q9zKjtmpPYUXI4tc3PtUEY8QsCQQCc
xySyC0sKe6bNzyC+Q8AVvkxiTKWiI5idEr8duhJd589H72Zc2wkMB+a2CEGo+Y5H
jy5cvuph/pG/7Qw7sljnAkAy/feClt1mUEiAcWrHRwcQ71AoA0+21yC9VkqPNrn3
w7OEg8gBqPjRlXBNb00QieNeGGSkXOoU6gFschR22Dzy
-----END RSA PRIVATE KEY-----
# openssl rsa -in jwt.pem -pubout
publicKey: |
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNQZKqTlO/+2b4ZdhqGJzGBDlt
b5PZmBz1ALN2YLvt341pH6i5mO1V9cX5Ty1LM70fKfnIoYUP4KCE33dPnC7LkUwE
/myh1zM6m8cbL5cYFPyP099thbVxzJkjHWqywvQih/qOOjliomKbM9pxG8Z1dB26
hL9dSAZuA8xExjlPmQIDAQAB
-----END PUBLIC KEY-----
eureka:
instance:
preferIpAddress: true
# instanceId: ${spring.cloud.client.ipAddress}:${server.port}
client:
serviceUrl:
defaultZone: http://10.255.131.162:8000/eureka/,http://10.255.131.163:8000/eureka/,http://10.255.131.164:8000/eureka/

運行測試

  • 客戶端模式
POST http://localhost:5000/oauth/token HTTP/1.1
Authorization: Basic Y2xpZW50XzE6MTIzNDU2
cache-control: no-cache
Postman-Token: 86fd25cd-406d-4db1-a67a-eda3cf760ba5
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: localhost:5000
content-type: application/x-www-form-urlencoded
accept-encoding: gzip, deflate
content-length: 29

Connection: keep-alive
grant_type=client_credentials
HTTP/1.1 200
{"access_token":"a1478d56-ebb8-4f21-b4b6-8a9602df24ec","token_type":"bearer","expires_in":1014,"scope":"all read write"}
  • 密碼模式
POST http://localhost:5000/oauth/token HTTP/1.1
Authorization: Basic Y2xpZW50X3Rlc3Q6MTIzNDU2
cache-control: no-cache
Postman-Token: f97aca16-e2ea-4dda-b51f-eb95caa57560
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: localhost:5000
content-type: application/x-www-form-urlencoded
grant_type=password&scope=all&username=irving&password=123456
HTTP/1.1 200
{"access_token":"dfe36394-8592-472f-b52b-24739811f6ee","token_type":"bearer","refresh_token":"c150594f-7d00-44cc-bbce-49e1a6e83552","expires_in":7190,"scope":"all"}
  • 獲取資源信息
GET http://localhost:5000/api/user/me?access_token=a1478d56-ebb8-4f21-b4b6-8a9602df24ec HTTP/1.1
Host: localhost:5000
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Date: Fri, 20 Jul 2018 09:21:32 GMT
Content-Length: 674
{"authorities":[{"authority":"client_credentials"}],"details":{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":null,"tokenValue":"a1478d56-ebb8-4f21-b4b6-8a9602df24ec","tokenType":"Bearer","decodedDetails":null},"authenticated":true,"userAuthentication":null,"credentials":"","oauth2Request":{"clientId":"client_1","scope":["all","read","write"],"requestParameters":{"grant_type":"client_credentials"},"resourceIds":[],"authorities":[{"authority":"client_credentials"}],"approved":true,"refresh":false,"redirectUri":null,"responseTypes":[],"extensions":{},"refreshTokenRequest":null,"grantType":"client_credentials"},"clientOnly":true,"principal":"client_1","name":"client_1"}

問題

  • There is no PasswordEncoder mapped for the id “null”問題
  • 一般是老的項目升到 Spring Boot 2.0 依賴的是 Spring 5,相關的依賴都發生了較大的改動 Spring Security 5.0 New Features ,Spring Security 重構了 PasswordEncoder 相關的算法 ,原先默認配置的 PlainTextPasswordEncoder(明文密碼)被移除了,替代的 BCryptPasswordEncoder ,Client 與 Resource Server 中設計密碼的相關都需要採用新的的編碼方式(上述代碼已採用)。
//兼容老版本 明文存儲
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
  • method_not_allowed(Request method 'GET' not supported) 問題
  • 可以配置,第三方 Client 拿到 access_token 後,如何發送給 Resouce Server 主要有三種方式[ RFC6750 中定義 ] (一般獲取 token 時不採用):
  • URI Query Parameter.
  • Authorization Request Header Field.
  • Form-Encoded Body Parameter.
@Configuration
public class OAuthSecurityConfig extends AuthorizationServerConfigurerAdapter {
...
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
...

endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);// add get method
...
endpoints.tokenServices(tokenServices);
}
...
}
  • Token 存儲 DB 報錯問題
  • 檢查數據庫 token 相關的字段是否是二進制數據類型(默認是:token LONGVARBINARY),數據庫的腳本可以在 Spring Security OAuth2 官方的項目中找到:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
2018-07-19 22:31:29.574 DEBUG 20084 --- [nio-5000-exec-6] .s.s.o.p.c.ClientCredentialsTokenGranter : Getting access token for: client_1
2018-07-19 22:31:29.574 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL query
2018-07-19 22:31:29.574 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [select token_id, token from oauth_access_token where authentication_id = ?]
2018-07-19 22:31:29.575 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
2018-07-19 22:31:29.623 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.datasource.DataSourceUtils : Returning JDBC Connection to DataSource
2018-07-19 22:31:29.623 DEBUG 20084 --- [nio-5000-exec-6] o.s.s.o.p.token.store.JdbcTokenStore : Failed to find access token for authentication org.springframework.security.oauth2.provider.OAuth2Authentication@f5d4467d: Principal: client_1; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: TRUSTED_CLIENT
2018-07-19 22:31:29.623 DEBUG 20084 --- [nio-5000-exec-6] o.s.b.f.s.DefaultListableBeanFactory : Returning cached instance of singleton bean 'scopedTarget.clientDetailsService'
2018-07-19 22:31:29.623 DEBUG 20084 --- [nio-5000-exec-6] o.s.b.f.s.DefaultListableBeanFactory : Returning cached instance of singleton bean 'scopedTarget.clientDetailsService'
2018-07-19 22:31:29.623 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL query
2018-07-19 22:31:29.623 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [select token_id, token from oauth_access_token where token_id = ?]
2018-07-19 22:31:29.623 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
2018-07-19 22:31:29.650 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.datasource.DataSourceUtils : Returning JDBC Connection to DataSource
2018-07-19 22:31:29.650 INFO 20084 --- [nio-5000-exec-6] o.s.s.o.p.token.store.JdbcTokenStore : Failed to find access token for token ad587601-e0fd-4dea-8fcc-75144eb74101
2018-07-19 22:31:29.650 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
2018-07-19 22:31:29.650 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)]
2018-07-19 22:31:29.650 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
2018-07-19 22:31:29.651 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.support.lob.DefaultLobHandler : Set bytes for BLOB with length 691
2018-07-19 22:31:29.651 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.support.lob.DefaultLobHandler : Set bytes for BLOB with length 1627
2018-07-19 22:31:29.665 DEBUG 20084 --- [nio-5000-exec-6] o.s.jdbc.datasource.DataSourceUtils : Returning JDBC Connection to DataSource
2018-07-19 22:31:29.665 DEBUG 20084 --- [nio-5000-exec-6] s.j.s.SQLErrorCodeSQLExceptionTranslator : Unable to translate SQLException with Error code '1366', will now try the fallback translator
2018-07-19 22:31:29.665 DEBUG 20084 --- [nio-5000-exec-6] o.s.j.s.SQLStateSQLExceptionTranslator : Extracted SQL state class 'HY' from value 'HY000'
2018-07-19 22:31:29.665 DEBUG 20084 --- [nio-5000-exec-6] .m.m.a.ExceptionHandlerExceptionResolver : Resolving exception from handler [public org.springframework.http.ResponseEntity org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map) throws org.springframework.web.HttpRequestMethodNotSupportedException]: org.springframework.jdbc.UncategorizedSQLException: PreparedStatementCallback; uncategorized SQLException for SQL [insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)]; SQL state [HY000]; error code [1366]; Incorrect string value: '\\xAC\\xED\\x00\\x05sr...' for column 'token' at row 1; nested exception is java.sql.SQLException: Incorrect string value: '\\xAC\\xED\\x00\\x05sr...' for column 'token' at row 1
2018-07-19 22:31:29.665 DEBUG 20084 --- [nio-5000-exec-6] .m.m.a.ExceptionHandlerExceptionResolver : Invoking @ExceptionHandler method: public org.springframework.http.ResponseEntity org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.handleException(java.lang.Exception) throws java.lang.Exception
2018-07-19 22:31:29.667 ERROR 20084 --- [nio-5000-exec-6] o.s.s.o.provider.endpoint.TokenEndpoint : Handling error: UncategorizedSQLException, PreparedStatementCallback; uncategorized SQLException for SQL [insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)]; SQL state [HY000]; error code [1366]; Incorrect string value: '\\xAC\\xED\\x00\\x05sr...' for column 'token' at row 1; nested exception is java.sql.SQLException: Incorrect string value: '\\xAC\\xED\\x00\\x05sr...' for column 'token' at row 1
org.springframework.jdbc.UncategorizedSQLException: PreparedStatementCallback; uncategorized SQLException for SQL [insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)]; SQL state [HY000]; error code [1366]; Incorrect string value: '\\xAC\\xED\\x00\\x05sr...' for column 'token' at row 1; nested exception is java.sql.SQLException: Incorrect string value: '\\xAC\\xED\\x00\\x05sr...' for column 'token' at row 1
  • 票據存 DB 還是 Redis
  • 根據 QPS 來吧,現階段我們就是使用 DB 來存儲,當然 Redis 或 MongoDB 都是比較好的選擇(因為 Token 是臨時性的,還涉及 Token 的刷新 ,驗證合法性,過期等機制,操作會很頻繁)。
/*
@Autowired
private RedisConnectionFactory connectionFactory;
@Bean
public RedisTokenStore tokenStore() {
return new RedisTokenStore(connectionFactory);
}
*/
@Autowired
@Qualifier("dataSource")
private DataSource dataSource;
@Bean("jdbcTokenStore")
public JdbcTokenStore getJdbcTokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// endpoints
// .tokenStore(new RedisTokenStore(redisConnectionFactory))
// .authenticationManager(authenticationManager);
endpoints.authenticationManager(authenticationManager)
//配置 JwtAccessToken 轉換器
// .accessTokenConverter(jwtAccessTokenConverter())
//refresh_token 需要 UserDetailsService is required
// .userDetailsService(userDetailsService)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.tokenStore(getJdbcTokenStore());
}
  • GitHub 代碼

REFER:

https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/

http://projects.spring.io/spring-security-oauth/docs/oauth2.html

如何構建安全的微服務應用

https://www.cnblogs.com/exceptioneye/p/9341011.html

https://oauth.net/2/

https://github.com/jeansfish/RFC6749.zh-cn


分享到:


相關文章: