在使用 Spring Cloud 體系來構建微服務的過程中,用戶請求是通過網關(ZUUL 或 Spring APIGateway)以 HTTP 協議來傳輸信息,API 網關將自己註冊為 Eureka 服務治理下的應用,同時也從 Eureka 服務中獲取所有其他微服務的實例信息。搭建 OAuth2 認... ...
在使用 Spring Cloud 體系來構建微服務的過程中,用戶請求是通過網關(ZUUL 或 Spring APIGateway)以 HTTP 協議來傳輸信息,API 網關將自己註冊為 Eureka 服務治理下的應用,同時也從 Eureka 服務中獲取所有其他微服務的實例信息。搭建 OAuth2 認證授權服務,並不是給每個微服務調用,而是通過 API 網關進行統一調用來對網關後的微服務做前置過濾,所有的請求都必須先通過 API 網關,API 網關在進行路由轉發之前對該請求進行前置校驗,實現對微服務系統中的其他的服務介面的安全與許可權校驗。一般解決用戶認證與授權的方法,目前主流的解決方案有 OAuth2.0、OIDC(OpenID Connect) 、HMAC、JWT 等。
OAuth2.0 授權模式
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
Spring Security OAuth2 框架
Spring Security OAuth2 是建立在 Spring Security 的基礎之上 OAuth2.0 協議實現的一個類庫,它提供了構建 Authorization Server、Resource Server 和 Client 三種 Spring 應用程式角色所需要的功能,能夠更好的集成到 Spring Cloud 體系中。
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。
之前提到 Authorization Server、Resource Server 和 Client 之間的關係,下麵使用 Spring Security OAuth2 為 Spring Cloud 搭建認證授權服務
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(); } }
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
可以在 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
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> </properties> <dependencies> <!--Spring Security 與 Security 的 OAuth2 擴展--> <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-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!-- 將 token 存儲在 redis 中 --> <!--<dependency>--> <!--<groupId>org.springframework.boot</groupId>--> <!--<artifactId>spring-boot-starter-data-redis</artifactId>--> <!--</dependency>--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
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) 問題
可以配置,由於不是 OAuth2.0 規範定義的範疇,調試在密碼模式獲得票據的時候會報錯,不推薦。
@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 Connecti