Spring Cloud 微服务中搭建 OAuth2.0 认证授权服务

匿名 (未验证) 提交于 2019-12-02 21:53:52

在使用 Spring Cloud 体系来构建微服务的过程中,用户请求是通过网关(ZUUL 或 Spring APIGateway)以 HTTP 协议来传输信息,API 网关将自己注册为 Eureka 服务治理下的应用,同时也从 Eureka 服务中获取所有其他微服务的实例信息。搭建 OAuth2 认证授权服务,并不是给每个微服务调用,而是通过 API 网关进行统一调用来对网关后的微服务做前置过滤,所有的请求都必须先通过 API 网关,API 网关在进行路由转发之前对该请求进行前置校验,实现对微服务系统中的其他的服务接口的安全与权限校验。一般解决用户认证与授权的方法,目前主流的解决方案有 OAuth2.0OIDC(OpenID Connect)HMACJWT 等。

备注关于 RESTFUL API 安全认证方式的一些总结

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

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的认证(

另外根据 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(); }

可以配置,由于不是 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 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.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) 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.common.exceptions.OAuth2Exception> 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 代码

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!