16、SpringBoot单点登录SSO数据库实现

    首先以一张图形式说明单点认证的整个流程:

   前一篇文章中,我们使用了内存写死的模式实现用户的授权和资源的保护,当然token以及clients信息存储有多种方式,有inmemory内存模式、redis存储模式、jdbc存储模式、jwt存储模式、jwk存储模式等等。是实际的生产环境,为了安全我们会将客户信息以及token信息存储在数据库,以便服务器之间共享和保证数据断电安全。这篇文章中,我们将告知如何实现以jdbc(即数据)将授权信息存储到数据库,将授权信息存储到数据库并用数据库中的用户名和密码验证数据用户权限;

一、回顾以内存模式

@Order(1)          // 使用注解方式使bean的加载顺序得到控制,配置改类被加载的顺序,优先级为1
@EnableWebSecurity // 包含@Configuration,@EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security
public class SecurityConfig extends WebSecurityConfigurerAdapter{
     /**
      * Override this method to configure the HttpSecurity. Typically subclasses should not invoke this method by calling super as it may override their configuration. The default configuration is:
      * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
      */
      @Override
    protected void configure(HttpSecurity http) throws Exception {
           http.requestMatchers()
               //.antMatchers("/login", "/oauth/authorize", "/oauth/accessToken", "/auth/oauth/token")                  // 这些页面不需要授权
               .antMatchers("/login")                                                                                   // 这些页面不需要授权
              .and()
              .authorizeRequests()
               .anyRequest().authenticated()                                                                            // 其他所有页面必须授权
              .and()
               .formLogin()                                                                                             //定制登录表单
                        //.loginPage("/login")                                                                          //设置登录url
                        //.defaultSuccessUrl("/home")                                                                   //设置登录成功默认跳转url
                        .permitAll();
    }
     /**
      * 1、创建用户
      * 2、验证用户
      */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {     
     // 内存中创建[验证]一个用户名为john,密码为123的用户,第三方应用登录的时候必须指定改用户名和密码通过认证后才发放code授权码
     // 第三方应用凭借用户授权(使用用户名和密码)获取的授权码换取access_token然后通过access_token获取用户信息
     // 角色名会默认被添加上"ROLE_"前缀,如下USER角色,实际的名称为ROLE_USER
     auth.inMemoryAuthentication()                                 // 内存验证模式
             .withUser("john")                                     // 创建的用户名
             .password(passwordEncoder().encode("123"))          // 验证的用户密码
             .roles("USER")                                        // 改用户的角色
             .and()                                                // 级联
             .withUser("lixx")                                     // 创建用户lixx
             .password(passwordEncoder().encode("dw123456")) // 用户密码
             .roles("ADMIN", "USER");                          // 用户角色为ROLE_ADMIN和ROLE_USER有两种角色
    }
   
    /**
     * 拦截URL,设置忽略安全校验的静态资源拦截
     * Override this method to configure WebSecurity. For example, if you wish to ignore certain requests.
     */
    @Override
     public void configure(WebSecurity web) throws Exception {
     web
          .ignoring()                                              // 忽略如下请求
          .antMatchers("/resources/**");                       // 忽略以/resources/打头的请求
     }
     /**
     * 创建加密对象,对密码进行加密
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

这里的重点是:

protected void configure(AuthenticationManagerBuilder auth) throws Exception 

使用内存授权模式并在内存中创建了两个用户join,密码是123(存储的时候需要加密),另一个用户是lixx,密码是dw123456,角色为ADMIN和USER,会自动添加上ROLE_前缀;

并使用了

protected void configure(HttpSecurity http) throws Exception

做授权保护,指定哪些页面可以访问,哪些页面需要授权;

二、以数据库存储模式

如果是以数据库方式存储先关的授权、token访问、token期限等,必须实现建立起oauth2的相关表,

1、在数据库中,建立相关表,创建语句如下:

mysql建表语句如下:

-- used in tests that use HSQL
create table oauth_client_details (
client_id VARCHAR(128) PRIMARY KEY,
resource_ids VARCHAR(128),
client_secret VARCHAR(128),
scope VARCHAR(128),
authorized_grant_types VARCHAR(128),
web_server_redirect_uri VARCHAR(128),
authorities VARCHAR(128),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(128)
);

create table oauth_client_token (
token_id VARCHAR(128),
token blob,
authentication_id VARCHAR(128) PRIMARY KEY,
user_name VARCHAR(128),
client_id VARCHAR(128)
);

create table oauth_access_token (
token_id VARCHAR(128),
token blob,
authentication_id VARCHAR(128) PRIMARY KEY,
user_name VARCHAR(128),
client_id VARCHAR(128),
authentication blob,
refresh_token VARCHAR(128)
);

create table oauth_refresh_token (
token_id VARCHAR(128),
token blob,
authentication blob
);

create table oauth_code (
code VARCHAR(128), authentication blob
);

create table oauth_approvals (
userId VARCHAR(128),
clientId VARCHAR(128),
scope VARCHAR(128),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);

jdbc对应建表语句(根据不同数据库修正LONGVARBINARY对应类型):

-- used in tests that use HSQL
create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY,
  resource_ids VARCHAR(256),
  client_secret VARCHAR(256),
  scope VARCHAR(256),
  authorized_grant_types VARCHAR(256),
  web_server_redirect_uri VARCHAR(256),
  authorities VARCHAR(256),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)
);

create table oauth_client_token (
  token_id VARCHAR(256),
  token LONGVARBINARY,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

create table oauth_access_token (
  token_id VARCHAR(256),
  token LONGVARBINARY,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication LONGVARBINARY,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token LONGVARBINARY,
  authentication LONGVARBINARY
);

create table oauth_code (
  code VARCHAR(256), authentication LONGVARBINARY
);

create table oauth_approvals (
    userId VARCHAR(256),
    clientId VARCHAR(256),
    scope VARCHAR(256),
    status VARCHAR(10),
    expiresAt TIMESTAMP,
    lastModifiedAt TIMESTAMP
);

表各个字段含义如下:

表名

字段名

字段说明

oauth_client_details

client_id

主键,必须唯一,不能为空. 

用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务端自动生成). 

对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appKey,与client_id是同一个概念.

resource_ids

客户端所能访问的资源id集合,多个资源时用逗号(,)分隔,如: "unity-resource,mobile-resource". 

该字段的值必须来源于与security.xml中标签‹oauth2:resource-server的属性resource-id值一致. 在security.xml配置有几个‹oauth2:resource-server标签, 则该字段可以使用几个该值. 

在实际应用中, 我们一般将资源进行分类,并分别配置对应的‹oauth2:resource-server,如订单资源配置一个‹oauth2:resource-server, 用户资源又配置一个‹oauth2:resource-server. 当注册客户端时,根据实际需要可选择资源id,也可根据不同的注册流程,赋予对应的资源id.

client_secret

用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成). 

对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念.

scope

指定客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔,如: "read,write". 

scope的值与security.xml中配置的‹intercept-url的access属性有关系. 如‹intercept-url的配置为

‹intercept-url pattern="/m/**" access="ROLE_MOBILE,SCOPE_READ"/>

则说明访问该URL时的客户端必须有read权限范围. write的配置值为SCOPE_WRITE, trust的配置值为SCOPE_TRUST. 

在实际应该中, 该值一般由服务端指定, 常用的值为read,write.

authorized_grant_types

指定客户端支持的grant_type,可选值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔,如: "authorization_code,password". 

在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: "authorization_code,refresh_token"(针对通过浏览器访问的客户端); "password,refresh_token"(针对移动设备的客户端). 

implicit与client_credentials在实际中很少使用.

web_server_redirect_uri

客户端的重定向URI,可为空, 当grant_type为authorization_code或implicit时, 在Oauth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:

  • 当grant_type=authorization_code时, 第一步 从 spring-oauth-server获取 'code'时客户端发起请求时必须有redirect_uri参数, 该参数的值必须与web_server_redirect_uri的值一致. 第二步 用 'code' 换取 'access_token' 时客户也必须传递相同的redirect_uri. 

    在实际应用中, web_server_redirect_uri在注册时是必须填写的, 一般用来处理服务器返回的code, 验证state是否合法与通过code去换取access_token值. 

    spring-oauth-client项目中, 可具体参考AuthorizationCodeController.java中的authorizationCodeCallback方法.

  • 当grant_type=implicit时通过redirect_uri的hash值来传递access_token值.如:

    http://localhost:7777/spring-oauth-client/implicit#access_token=dc891f4a-ac88-4ba6-8224-a2497e013865&token_type=bearer&expires_in=43199

    然后客户端通过JS等从hash值中取到access_token值.

authorities

指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔, 如: "ROLE_UNITY,ROLE_USER". 

对于是否要设置该字段的值,要根据不同的grant_type来判断, 若客户端在Oauth流程中需要用户的用户名(username)与密码(password)的(authorization_code,password), 

则该字段可以不需要设置值,因为服务端将根据用户在服务端所拥有的权限来判断是否有权限访问对应的API. 

但如果客户端在Oauth流程中不需要用户信息的(implicit,client_credentials), 

则该字段必须要设置对应的权限值, 因为服务端将根据该字段值的权限来判断是否有权限访问对应的API. 

(请在spring-oauth-client项目中来测试不同grant_type时authorities的变化)

access_token_validity

设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时). 

在服务端获取的access_token JSON数据中的expires_in字段的值即为当前access_token的有效时间值. 

在项目中, 可具体参考DefaultTokenServices.java中属性accessTokenValiditySeconds. 

在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义.

refresh_token_validity

设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天). 

若客户端的grant_type不包括refresh_token,则不用关心该字段 在项目中, 可具体参考DefaultTokenServices.java中属性refreshTokenValiditySeconds. 

 

在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义.

additional_information

这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据,如:

{"country":"CN","country_code":"086"}

按照spring-security-oauth项目中对该字段的描述 

Additional information for this client, not need by the vanilla OAuth protocol but might be useful, for example,for storing descriptive information. 

(详见ClientDetails.java的getAdditionalInformation()方法的注释)

在实际应用中, 可以用该字段来存储关于客户端的一些其他信息,如客户端的国家,地区,注册时的IP地址等等.

create_time

数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)

archived

用于标识客户端是否已存档(即实现逻辑删除),默认值为'0'(即未存档). 

对该字段的具体使用请参考CustomJdbcClientDetailsService.java,在该类中,扩展了在查询client_details的SQL加上archived = 0条件 (扩展字段)

trusted

设置客户端是否为受信任的,默认为'0'(即不受信任的,1为受信任的). 

该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为0,则会跳转到让用户Approve的页面让用户同意授权, 

若该字段为1,则在登录后不需要再让用户Approve同意授权(因为是受信任的). 

对该字段的具体使用请参考OauthUserApprovalHandler.java. (扩展字段)

autoapprove

设置用户是否自动Approval操作, 默认值为 'false', 可选值包括 'true','false', 'read','write'. 

该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为'true'或支持的scope值,则会跳过用户Approve的页面, 直接授权. 

该字段与 trusted 有类似的功能, 是 spring-security-oauth2 的 2.0 版本后添加的新属性.

 在项目中,主要操作oauth_client_details表的类是JdbcClientDetailsService.java, 更多的细节请参考该类. 

也可以根据实际的需要,去扩展或修改该类的实现.

oauth_client_token

create_time

数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)

token_id

从服务器端获取到的access_token的值.

token

这是一个二进制的字段, 存储的数据是OAuth2AccessToken.java对象序列化后的二进制数据.

authentication_id

该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 

具体实现请参考DefaultClientKeyGenerator.java类.

user_name

登录时的用户名

client_id

 

 该表用于在客户端系统中存储从服务端获取的token数据, 在spring-oauth-server项目中未使用到. 

对oauth_client_token表的主要操作在JdbcClientTokenServices.java类中, 更多的细节请参考该类.

oauth_access_token

create_time

数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)

token_id

该字段的值是将access_token的值通过MD5加密后存储的.

token

存储将OAuth2AccessToken.java对象序列化后的二进制数据, 是真实的AccessToken的数据值.

authentication_id

该字段具有唯一性, 其值是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultAuthenticationKeyGenerator.java类.

user_name

登录时的用户名, 若客户端没有用户名(如grant_type="client_credentials"),则该值等于client_id

client_id

 

authentication

存储将OAuth2Authentication.java对象序列化后的二进制数据.

refresh_token

该字段的值是将refresh_token的值通过MD5加密后存储的.

 在项目中,主要操作oauth_access_token表的对象是JdbcTokenStore.java. 更多的细节请参考该类.

oauth_refresh_token

create_time

数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)

token_id

该字段的值是将refresh_token的值通过MD5加密后存储的.

token

存储将OAuth2RefreshToken.java对象序列化后的二进制数据.

authentication

存储将OAuth2Authentication.java对象序列化后的二进制数据.

 在项目中,主要操作oauth_refresh_token表的对象是JdbcTokenStore.java. (与操作oauth_access_token表的对象一样);更多的细节请参考该类. 

如果客户端的grant_type不支持refresh_token,则不会使用该表.

oauth_code

create_time

数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)

code

存储服务端系统生成的code的值(未加密).

authentication

存储将AuthorizationRequestHolder.java对象序列化后的二进制数据.

 在项目中,主要操作oauth_code表的对象是JdbcAuthorizationCodeServices.java. 更多的细节请参考该类. 

只有当grant_type为"authorization_code"时,该表中才会有数据产生; 其他的grant_type没有使用该表.

2、引入对应的包

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <!-- 基本信息 -->
  <modelVersion>4.0.0</modelVersion>
  <artifactId>spring-auth-server</artifactId>
  <name>认证授权服务</name>
  <description>认证授权服务</description>
 
  <!-- 父项目 -->
  <parent>
    <groupId>com.easystudy</groupId>
    <artifactId>spring-cloud-oauther2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
 
  <!-- 强依赖 -->
  <dependencies>
     <!-- 公共信息包 -->
     <dependency>
          <groupId>com.easystudy</groupId>
          <artifactId>spring-auth-common</artifactId>
          <version>0.0.1-SNAPSHOT</version>
     </dependency>
     <!-- spring cloud 客户注册 -->
     <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
     </dependency>
     <!-- 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-hystrix</artifactId>
          <version>1.4.0.RELEASE</version>
     </dependency>
     <!-- 远程调用 -->
     <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-feign</artifactId>
          <version>1.4.0.RELEASE</version>
     </dependency>
     <!-- 负载 -->
     <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-ribbon</artifactId>
          <version>1.4.0.RELEASE</version>
     </dependency>
     <!-- redis -->
     <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>
     <!-- jpa注解 -->
     <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-jpa</artifactId>
     </dependency>
     <!-- mysql -->
    <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
     </dependency>
     <!-- 阿里巴巴druid数据库连接池 -->
     <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid-spring-boot-starter</artifactId>
          <version>1.1.9</version>
     </dependency>
    <!-- 使用webjar管理前端资源,此处引入bootstrap和jquery方便演示 -->
     <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>jquery</artifactId>
          <version>2.1.1</version>
     </dependency>
     <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>bootstrap</artifactId>
          <version>3.3.7</version>
     </dependency>
     <!-- 辅助定位静态资源,省略 webjar的版本:http://localhost:8080/webjars/jquery/3.1.0/jquery.js路径中可以省略3.1.0 -->
     <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>webjars-locator</artifactId>
          <version>0.32</version>
     </dependency>
  </dependencies>
</project>

 

3、数据源配置

application.yml配置如下所示:

#服务配置
server:
  #监听端口
  port: 8762
  servlet:
    context-path: /auth
spring:
  application:
    #服务名称
    name: auth2.0-center
  #分布式系统跟踪服务
  zipkin:
      base-url:http://localhost:8763
  #数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/zuul-auth?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      # 连接池配置----------------------
      #初始连接池大小
      initial-size: 5
      #最小闲置连接
      min-idle: 5
      #最大活动连接大小
      max-active: 20
      #配置获取连接等待超时的时间
      max-wait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      # 验证连接有效性
      validation-query: SELECT 'x'
      validation-query-timeout: 6
      #申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
      test-while-idle: true
      #这里建议配置为TRUE,防止取到的连接不可用。获取链接的时候,不校验是否可用,开启会有损性能
      test-on-borrow: false
      #归还链接到连接池的时候校验链接是否可用
      test-on-return: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      filters: stat,wall,log4j
      #慢SQL记录:开启合并sql、开启慢查询语句,5000毫秒
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  #redis配置
  redis:
    #redis地址
    host: 127.0.0.1
    #redis端口,默认6379
    port: 6379
    #redis校验密码
    #password: 123456
   
#eureka集群配置
eureka:
  instance:
    #将IP注册到Eureka Server上,如果不配置就是机器的主机名
    prefer-ip-address: true
    #实例名定义为:"ip:port" 如果spring.cloud.client.ipAddress获取不到则使用spring.cloud.client.ip_address
    #instance-id: ${spring.cloud.client.ip_address}:${server.port}
    #隔多久去拉取服务注册信息,m默认30s
    registry-fetch-interval-seconds: 30
    #client发送心跳给server端的频率,m默认30s,如果server端leaseExpirationDurationInSeconds
    #后没有收到client的心跳,则将摘除该instance
    lease-renewal-interval-in-seconds: 10
    #表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,
    #在这个时间内若没收到下一次心跳,则将移除该instance,默认是90秒
    lease-expiration-duration-in-seconds: 30
  #eureka客户端配置
  client:
      #注册到eureka服务器地址
      service-url:
        #可以配置多个
        #defaultZone: http://mss-eureka1:9010/eureka/,http://mss-eureka2:9011/eureka/
        defaultZone:http://localhost:8761/eureka/
# ----Spring Boot Actuator:监控系统配置
endpoints:
  health:
    sensitive: false
    enabled: false
  shutdown:
    #Spring Boot Actuator的shutdown endpoint默认是关闭的
    enabled: true
    #自定义api地址:host:port/shutdown就可以实现优雅停机
    path: /shutdown
    #使用密码验证-项目中添加的有Spring Security,所有通过HTTP暴露的敏感端点都会受到保护
    #默认情况下会使用基本认证(basic authentication,用户名为user,密码为应用启动时在控制台打印的密码)
    sensitive: true
management:
  security:
    #刷新时,关闭安全验证
    enabled: false
# ----Spring Boot Actuator:监控系统配置
#安全校验
security:
  oauth2:
    resource:
      #本来spring security的基础上使用了spring security oauth2,控制/api下的请求
      #但是spring security的资源控制和spring securtiy oauth2的资源控制会互相覆盖
      #如果配置添加了security.oauth2.resource.filter-order=3,则使用spring security的控制,反之则为oauth2的控制
      filter-order: 3
#系统日志配置
logging:
  #日志路径
  config: classpath:logback.xml
  #不同组件的日志显示级别
  level:
    org:
      springframework:
        web: info
#feign 默认关闭熔断,请看HystrixFeignConfiguration
feign:
  hystrix:
    #启用熔断机制
    enabled: true
#熔断配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            #设置API网关中路由转发请求的HystrixCommand执行超时时间
            #在zuul配置了熔断fallback的话,熔断超时也要配置,不然如果你配置的ribbon超时时间大于熔断的超时,那么会先走熔断,相当于你配的ribbon超时就不生效了
            #这里面ribbon和hystrix是同时生效的,哪个值小哪个生效,另一个就看不到效果了
            timeoutInMilliseconds: 60000
           
#ribbon负载均衡配置
ribbon:
  #设置路由转发请求的时候,创建请求连接的超时时间
  ReadTimeout: 30000
  #用来设置路由转发请求的超时时间
  ConnectTimeout: 60000
  # 最大重试次数
  MaxAutoRetries: 2
  # 重试下一服务次数[排除第一台服务器]
  MaxAutoRetriesNextServer: 0

注意:server.servlet.context-path为配置的项目上下文路径,可以理解为项目名称(同tomcat的项目名,访问指定的项目必须带上改路径:http://localhost:8080/项目名/login.html),当然这里可以不用配置druid(因为没有使用到)

3、修改http security配置

将内存模式修改为JDBC数据库模式:

package com.easystudy.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import com.easystudy.service.impl.UserDetailsServiceImpl;

/**
* 安全服务配置(Spring Security http URL拦截保护)::
* URL强制拦截保护服务,可以配置哪些路径不需要保护,哪些需要保护。默认全都保护
* 继承了WebSecurityConfigurerAdapter之后,再加上几行代码,我们就能实现以下的功能:
* 1、要求用户在进入你的应用的任何URL之前都进行验证
* 2、创建一个用户名是“user”,密码是“password”,角色是“ROLE_USER”的用户
* 3、启用HTTP Basic和基于表单的验证
* 4、Spring Security将会自动生成一个登陆页面和登出成功页面
*
* @EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security。
* 继承了WebSecurityConfigurerAdapter之后,再加上几行代码,我们就能实现以下的功能:
* 1、要求用户在进入你的应用的任何URL之前都进行验证
* 2、创建一个用户名是“user”,密码是“password”,角色是“ROLE_USER”的用户
* 3、启用HTTP Basic和基于表单的验证
* 4、Spring Security将会自动生成一个登陆页面和登出成功页面
* 默认页面:
* 登录页面:/login
* 注销页面:/login?logout
* 错误页面:/login?error
*
* 与ResourceServerConfigurerAdapter区别
* 1、ResourceServerConfigurerAdapter被配置为不同的端点(参见antMatchers),而WebSecurityConfigurerAdapter不是。
*   这两个适配器之间的区别在于,RealServServer配置适配器使用一个特殊的过滤器来检查请求中的承载令牌,以便通过OAuth2对请求进行认证。
*   WebSecurityConfigurerAdapter适配器用于通过会话对用户进行身份验证(如表单登录)
* 2、WebSecurityConfigurerAdapter是默认情况下spring security的http配置,
*   ResourceServerConfigurerAdapter是默认情况下spring security oauth2的http配置
*   在ResourceServerProperties中,定义了它的order默认值为SecurityProperties.ACCESS_OVERRIDE_ORDER - 1;是大于1的,
*   即WebSecurityConfigurerAdapter的配置的拦截要优先于ResourceServerConfigurerAdapter,优先级高的http配置是可以覆盖优先级低的配置的。
*   某些情况下如果需要ResourceServerConfigurerAdapter的拦截优先于WebSecurityConfigurerAdapter需要在配置文件中添加
*   security.oauth2.resource.filter-order=99
*   
*   
重点:
@@如果使用jdbc存储客户信息,必须做如下粗啊哦做:
(1)建立如下数据表oauth_client_details,auth2会从oauth_client_details查询授权秘钥信息(在哪一个数据库中建立,就看你配置的数据源是哪一个)
(2)往对应表中插入记录(一般是第三方app注册之后返回给第三方app的clientid和秘钥信息
          这个是默认的类的表,一般用它默认的即可,我们这边就需要根据以上的字段配置相关的内容,如下:
insert into oauth_client_details(client_id,resource_ids,client_secret,scope,authorized_grant_types,
                                 web_server_redirect_uri,authorities,access_token_validity,
                                 refresh_token_validity,autoapprove)
                        values('test_client_id', 'test_resource_id', 'test_client_secret',
                               'user_info','authorization_code', 'http://localhost:8082/ui/login',
                               'ROLE_ADMIN', 3600, 7200, 'true');
注意:scope客户受限的范围。如果范围未定义或为空(默认值),客户端不受范围限制。read write all
    authorities授予客户的授权机构(普通的Spring Security权威机构)。
    authorized_grant_types:有:authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔,如:  "authorization_code,refresh_token".
    additional_information默认为NULL,否则必须为json格式Map<String,Object>形式的字符串,例如:{"systemInfo":"Atlas System"}
*/

@Configuration
@EnableWebSecurity          // 创建了一个WebSecurityConfigurerAdapter,且自带了硬编码的order=3,使用spring security而不是auth
//@Order(1)                 // 定义拦截器配置拦截次序,高于ResourceServerConfigurerAdapter
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    // 自定义用户服务-校验用户是否合法,实现改接口,spring即可获取对应用户的角色、权限等信息,然后可以拦截URL判断是否具有对应权限
    // 具体是否可以访问对应URL配置可以在HttpSecurity中配置
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    
    /**
     * 密码加密器:将用户密码进行加密
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用BCrypt进行密码的hash
        return new BCryptPasswordEncoder();
    }

    /**
     * 不定义没有password grant_type即密码授权模式(总共四种授权模式:授权码、implicat精简模式、密码、client credentials)
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 如果有要忽略拦截校验的静态资源,在此处添加
     * 忽略任何以”/resources/”开头的请求,这和在XML配置http@security=none的效果一样
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // TODO Auto-generated method stub
        web
        .ignoring()
        .antMatchers("/resources/**");    
    }

    /**
     * 允许对特定的http请求基于安全考虑进行配置,默认情况下,适用于所有的请求,
     * 但可以使用requestMatcher(RequestMatcher)或者其它相似的方法进行限制
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 如:将所有的url访问权限设定为角色名称为"ROLE_USER"
        // http.authorizeRequests().antMatchers("/").hasRole("USER").and().formLogin();
        
        // 启用HTTP Basic和基于表单的验证
        http.authorizeRequests()                            // 定义权限配置
                //.antMatchers("/login").permitAll()
                .anyRequest().authenticated()                // 任何请求都必须经过认证才能访问--登录后可以访问任意页面
            .and()
                .formLogin()                                // 定制登录表单
                    //.loginPage("/login")                    // 设置登录url
                    //.failureUrl("/login?error")
                    //.defaultSuccessUrl("/home")                // 设置登录成功默认跳转url
                    .permitAll()                            //允许任何人访问登录url
            .and()
                    .logout().permitAll()
            .and()
                .csrf().disable()                            // 禁止跨域请求
                .httpBasic();                                // 进行http Basic认证                    
    }
    
    /**
     * 系统安全用户验证模式:
     * 1、使用内存模式创建验证
     * 2、使用数据库创建验证,实现userDetailsService接口即可
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 将验证过程交给自定义
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
        
        // 内存创建用户:写死不利于项目实际应用
        // 验证的时候就是通过创建的用户名、密码、角色进行验证的
        // 创建一个用户名是“user”,密码是“password”,角色是“ROLE_USER”的用户
        // 创建一个用户名是“admin”,密码是“123456”,角色是“ROLE_ADMIN以及ROLE_USER”的用户
        //auth
        //    .inMemoryAuthentication()
        //        .withUser("user").password("password").roles("USER")              // 在内存中的验证(memory authentication)叫作”user”的用户
        //    .and()
        //        .withUser("admin").password("123456").roles("ADMIN", "USER");    // 在内存中的验证(memory authentication)叫作”admin”的管理员用户
    }
}

4、自定义用户信息实现

    实际的项目中,我们的登录用户数据可能存在数据库中,也可能是存放在ladap或其他微服务接口中,springcloud oauth2给我们提供了一个UserDetailsService接口,在项目中,我们需要自行实现这个接口来获取用户信息,Spring Auth2提供了获取UserDetails的方式,只要实现UserDetailsService接口即可, 最终生成用户和权限共同组成的UserDetails,在这里就可以实现从自定义的数据源中获取用户信息。这里我使用的feign远程接口调用的方式实现用户信息的获取,真实的用户服务有eureka中的user-service提供;

package com.easystudy.service.impl;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.easystudy.error.ReturnValue;
import com.easystudy.model.Rights;
import com.easystudy.model.Role;
import com.easystudy.model.RoleRights;
import com.easystudy.service.RightsService;
import com.easystudy.service.RoleRightsService;
import com.easystudy.service.RoleService;
import com.easystudy.service.UserService;

/**
* 实际的项目中,我们的登录用户数据可能存在数据库中,也可能是存放在ladap或其他微服务接口中,
* springcloud oauth2给我们提供了一个UserDetailsService接口,在项目中,
* 我们需要自行实现这个接口来获取用户信息
* 提供了获取UserDetails的方式,只要实现UserDetailsService接口即可,
* 最终生成用户和权限共同组成的UserDetails,在这里就可以实现从自定义的数据源
* 中获取用户信息
* @author Administrator
*
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserService userService;                    // 用户服务
    @Autowired
    private RoleService roleService;                    // 角色服务
    @Autowired
    private RoleRightsService roleRightsService;        // 权限服务
    @Autowired
    private RightsService rightsService;                // 权限服务


    /**
     * 通过用户名获取用户信息给oauth2
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查找用户
        ReturnValue<com.easystudy.model.User> userResult = userService.findByUsername(username);
        if (userResult.getError() != 0) {
            throw new UsernameNotFoundException("用户:" + username + ",不存在!");
        }
        
        // 查找的结果bean拷贝到userVo
        com.easystudy.model.User userVo = new com.easystudy.model.User();
        BeanUtils.copyProperties(userResult.getValue(),userVo);
        
        // 用户用户id查找对应的角色
        ReturnValue<List<Role>> roleResult = roleService.getRolesByUserId(userVo.getId());
        
        // 设置用户权限
        Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
        if (0 == roleResult.getError()){            
            // 获取所有角色权限
            List<Role> roleVoList = roleResult.getValue();
            for (Role role : roleVoList){
                
                // 角色必须是ROLE_开头,可以在数据库中设置---WebSecurityConfig也必须对应进行http验证,即保持一致---
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
                grantedAuthorities.add(grantedAuthority);
                
                // 其他角色-每一个权限
                ReturnValue<List<RoleRights>> perResult = roleRightsService.getRoleRights(role.getId());
                if (0 == perResult.getError()){
                    List<RoleRights> permissionList = perResult.getValue();
                    for (RoleRights roleRight : permissionList) {
                        ReturnValue<Rights> right = rightsService.findByRightId(roleRight.getRight_id());
                        if(0 == right.getError()){
                            GrantedAuthority authority = new SimpleGrantedAuthority(right.getValue().getName());
                            grantedAuthorities.add(authority);
                        }
                    }
                }
            }
        }
        
        // 标识位设置
        boolean enabled = true;                         // 可用性 :true:可用 false:不可用
        boolean accountNonExpired = true;                 // 过期性 :true:没过期 false:过期
        boolean credentialsNonExpired = true;             // 有效性 :true:凭证有效 false:凭证无效
        boolean accountNonLocked = true;                 // 锁定性 :true:未锁定 false:已锁定
        
        return new User(userVo.getUsername(), userVo.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);
    }
}

这里实现的功能是:

    通过用户名获取改用户的所有角色,然后通过角色获取改用户的所有权限(其实,改步骤可以通过user-service一步可以获取:通过用户名获取用户的权限列表即可),然后根据用户名、密码(注意密码必须使用BCrypt方式加密,否则验证用户失败)、用户具有的授权数组生成spring security用户,提供给Spring security进行用户验证和权限访问控制;

4、token授权认证服务配置

只要经过授权认证的用户才能访问对应的功能或页面,所以必须配置授权信息,授权相关的信息以数据库方式存储,AuthorizationServerConfigurerAdapter实现如下并使用注解标注为授权服务器:

package com.easystudy.config;

import java.util.concurrent.TimeUnit;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import com.easystudy.service.impl.UserDetailsServiceImpl;

/**
* 创建授权配置信息:认证服务器,进行认证和授权,声明一个认证服务器,当用此注解后,应用启动后将自动生成几个Endpoint
* AuthorizationServerConfigurer包含三种配置:
* ClientDetailsServiceConfigurer:client客户端的信息配置,
* client信息包括:clientId、secret、scope、authorizedGrantTypes、authorities
* (1)scope:表示权限范围,可选项,用户授权页面时进行选择
* (2)authorizedGrantTypes:有四种授权方式
*         Authorization Code:用验证获取code,再用code去获取token(用的最多的方式,也是最安全的方式)
*         Implicit: 隐式授权模式
*         Client Credentials (用來取得 App Access Token)
*         Resource Owner Password Credentials
* (3)authorities:授予client的权限
*/
@Configuration
@EnableAuthorizationServer            // oauth2分为资源服务和认证授权服务,可以分开,可以统一进程
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private DataSource dataSource;

    // token存储数据库-使用jdbc存储客户token信息
    @Bean
    public JdbcTokenStore jdbcTokenStore(){
        return new JdbcTokenStore(dataSource);
    }

    // 声明 ClientDetails实现:定义授权的请求的路径的Bean--JDBC存储客户信息
    @Bean
    public ClientDetailsService JdbcClientDetails() {
        // 使用JdbcClientDetailsService客户端详情服务
        // Jdbc实现客户端详情服务,数据源dataSource不做叙述,使用框架默认的表
        return new JdbcClientDetailsService(dataSource);
    }

    /**
     * 授权认证端点拦截验证:
     * 声明授权和token的端点以及token的服务的一些配置信息,比如采用什么存储方式、token的有效期等:
     * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。
     * 包含以下的端点(以上这些endpoint都在源码里的endpoint包里面):
     * 1、AuthorizationEndpoint 根据用户认证获得授权码,有下面两个方法
     *   -/oauth/authorize - GET
     *   -/oauth/authorize - POST
     * 2、TokenEndpoint 客户端根据授权码获取 token
     *   -/oauth/token - GET
     *   -/oauth/token - POST
     * 3、CheckTokenEndpoint 资源服务器用来校验token
     *   -/oauth/check_token
     * 4、WhitelabelApprovalEndpoint 显示授权服务器的确认页
     *      -/oauth/confirm_access
     * 5、WhitelabelErrorEndpoint 显示授权服务器的错误页
     *   -/oauth/error
     * 6、/oauth/token_key:如果jwt模式则可以用此来从认证服务器获取公钥
     * 这些端点有个特点,如果你自己实现了上面的方法,他会优先使用你提供的方法,利用这个特点,通常都会根据自己的
     * 需要来设计自己的授权确认页面,例如使用 QQ 登录微博的认证页面
     * 在官方的示例中,通过下面代码直接指定了视图:
     * registry.addViewController("/oauth/confirm_access").setViewName("authorize");
     *
     * 授权类型:
     * 通过AuthorizationServerEndpointsConfigurer来进行配置,默认情况下,支持除了密码外的所有授权类型,相关授权类型的一些类:
     * (1)authenticationManager:直接注入一个AuthenticationManager,自动开启密码授权类型
     * (2)userDetailsService:如果注入UserDetailsService,那么将会启动刷新token授权类型,会判断用户是否还是存活的
     * (3)authorizationCodeServices:AuthorizationCodeServices的实例,auth code 授权类型的服务
     * (4)implicitGrantService:imlpicit grant
     * (5)tokenGranter:
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                 // token存储在数据库中-生产环境使用以免服务器崩溃
                 .tokenStore(jdbcTokenStore())
                 // 从数据查用户授权信息
                 .userDetailsService(userDetailsService);
                 
        // 配置TokenServices参数
        // Spring Cloud Security OAuth2通过DefaultTokenServices类来完成token生成、过期等 OAuth2 标准规定的业务逻辑
        // 而DefaultTokenServices又是通过TokenStore接口完成对生成数据的持久化        
        endpoints.tokenServices(defaultTokenServices());
    }
    
    /**
     * 用来配置令牌端点(Token Endpoint)的安全约束
     * 声明安全约束,哪些允许访问,哪些不允许访问
     * /oauth/token
     * 1、这个如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter来保护
     * 2、如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允许获取token
        security.tokenKeyAccess("permitAll()");
        // isAuthenticated()检查access_token需要进行授权
        security.checkTokenAccess("isAuthenticated()");
        // 允许以form形式提交表单信息,否则向/oauth/token或/oauth/accessToken端点获取access_token的时候报错401
        security.allowFormAuthenticationForClients();
    }
    
    /**
     * AuthorizationServerConfigurer 的一个回调配置项:
     * client的信息的读取:在ClientDetailsServiceConfigurer类里面进行配置,
     * 可以有in-memory、jdbc等多种读取方式,jdbc需要调用JdbcClientDetailsService类,
     * 此类需要传入相应的DataSource.
     * 用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,
     * 你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //clients.withClientDetails(clientDetails());
        // 这个地方指的是从jdbc查出数据来[存储]
        clients.withClientDetails(JdbcClientDetails());
    }

    /**
     * Spring Cloud Security OAuth2通过DefaultTokenServices类来完成token生成、过期等 OAuth2 标准规定的业务逻辑,
     * 而DefaultTokenServices又是通过TokenStore接口完成对生成数据的持久化。在上面的demo中,TokenStore的默认实现为
     * InMemoryTokenStore,即内存存储。 对于Client信息,ClientDetailsService接口负责从存储仓库中读取数据,在上面的
     * demo中默认使用的也是InMemoryClientDetialsService实现类。说到这里就能看出,要想使用数据库存储,只需要提供这些接口的
     * 实现类即可。庆幸的是,框架已经为我们写好JDBC实现了,即JdbcTokenStore和JdbcClientDetailsService
     * <p>注意,自定义TokenServices的时候,需要设置@Primary,否则报错,</p>
     *
     * token存储方式共有三种分别是:
     * (1)InMemoryTokenStore:存放内存中,不会持久化
     * (2)JdbcTokenStore:存放数据库中
     * (3)Jwt: json web token
     */
    @Primary
    @Bean
    public DefaultTokenServices defaultTokenServices(){
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(jdbcTokenStore());                                            // 如果存储在jdbc中就需要建立token存储表
        tokenServices.setSupportRefreshToken(true);                                                // 支持更换token
        tokenServices.setClientDetailsService(JdbcClientDetails());                            // jdbc具体的秘钥认证服务-如果存储在jdbc中就需要建立oauth_client_details表
        //tokenServices.setClientDetailsService(InMemClientDetails());                    // 内存秘钥认证服务
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(15)); // token有效期自定义设置,30天
        tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));// 默认30天
        return tokenServices;
    }
}

5、Feign远程接口调用

以获取用户信息接口定义如下:

package com.easystudy.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import com.easystudy.error.ReturnValue;
import com.easystudy.model.User;
import com.easystudy.service.impl.UserServiceImpl;

/**
* @FeignClient指定调用user-service服务的对应接口
* @author Administrator
*/
@FeignClient(name = "user-service",fallback = UserServiceImpl.class)
public interface UserService {
    @GetMapping("user/findByUserName/{username}")
    public ReturnValue<User> findByUsername(@PathVariable("username") String username);
}

错误降级接口实现如下:

package com.easystudy.service.impl;
import org.springframework.stereotype.Service;

import com.easystudy.error.ErrorCode;
import com.easystudy.error.ReturnValue;
import com.easystudy.model.User;
import com.easystudy.service.UserService;

import lombok.extern.slf4j.Slf4j;


@Service
@Slf4j            // 不想每次都写private final Logger logger = LoggerFactory.getLogger(XXX.class);
                // @Slf4j注入后找不到变量log,那就给IDE安装lombok插件
public class UserServiceImpl implements UserService {
    @Override
    public ReturnValue<User> findByUsername(String username) {
        log.info("调用{}失败","findByUsername");
        return new ReturnValue<User>(ErrorCode.ERROR_SERVER_ERROR, "调用findByUsername接口失败");
    }
}

6、启动入口实现

/**
 * 简单理解一下OAuth2,你要登录一个XX网站下片,但是下片需要注册登录成为这个网站的会员,你不想注册,
 * 还好网站提供了qq登录,你选择qq登录。那么问题来了,你选择qq登录时,会跳转到qq的登录页面,输入qq
 * 账号密码,注意,这时登录qq与这个XX网站是没关系的,这是qq做的登录页,登录时qq会问你是否允许该XXX
 * 网站获取你的个人信息如头像、昵称等,你勾选同意,登录成功后就又回到了XX网站的某个页面了。这是一个什么
 * 流程呢,其实是一个XX网站试图获取你个人信息、然后问你是否同意的一个流程。你同意了,qq才会允许第三方
 * 网站获取你的个人信息
 * 使用:
 * 1、通过client_id请求授权码
 * 请求:
 * http://localhost:8762/auth/oauth/authorize?response_type=code&client_id=test_client_id&redirect_uri=http://localhost:8082/ui/login
 * 响应:
 * http://localhost:8082/ui/login?code=uB0dHT
 * 2、通过授权码换取token
 * http://localhost:8762/auth/oauth/token?client_id=test_client_id&client_secret=test_client_secret&scope=user_info&grant_type=authorization_code&code=uB0dHT&redirect_uri=http://localhost:8082/ui/login

   注意,改接口必须以post方式提交,测试可以用postman,将后面字段逐一以键值对形式填写并选择post方式提交,否则报错(不支持get方式)
 * 3、
 */
@SpringBootApplication       // SpringBoot应用
@EnableDiscoveryClient       // 开启服务发现功能
@EnableFeignClients          // 开启Feign
public class AuthServerApplication {
     public static void main(String[] args) {
          SpringApplication.run(AuthServerApplication.class, args);
     }
}

三、测试

    经过以上步骤,我们的认证服务器已经搭建完成,启动AuthServerApplication,然后根据如下测试流程测试,在测试前我们做一个假设:假如我要访问你的网站(http://localhost:8082/ui/login),你要求必须经过授权中心(http://localhost:8762)统一认证才能访问对应资源或接口,所以

(1)第一步,访问你的网站你会跳转至认证中心的授权链接,你提供你程序向授权中心注册时候返回的client_id,redirect_uri获取授权码,注意改步骤是GET请求(这个过程需要你提供用户名和密码);

你网站的发起的请求:

http://localhost:8762/auth/oauth/authorize?response_type=code&client_id=test_client_id&redirect_uri=http://localhost:8082/ui/login

验证注册信息有效后响应(回复授权码):

http://localhost:8082/ui/login?code=uB0dHT

 

(2)第二步、通过第一步获取的授权码以及client_id、client_secret、scope权限范围、redirect_uri换取token令牌

发起请求,注意这里是POST请求(使用postman工具即可,注意数据部分放在body以键值对形式隔开,不是header部分):

http://localhost:8762/auth/oauth/token?client_id=test_client_id&client_secret=test_client_secret&scope=user_info&grant_type=authorization_code&code=uB0dHT&redirect_uri=http://localhost:8082/ui/login

回复响应(json中的一个字符串,这里能配置refresh_token是因为表中设置了authorized_grant_types为“authorization_code,refresh_token”,也就是说可以获取access_token并可以刷新token):

access_token:2bccded3-1dd2-4f44-bb82-aea659c514ee

refresh_token: 2260389e-db44-4100-b05d-4f5af77e1802

 

(3)第三步、通过第二步获取的access_token访问你的网站获取信息(获取的token在所有服务上都是可信的)

请求你的网站:

http://localhost:8082/ui/login?access_token=2bccded3-1dd2-4f44-bb82-aea659c514ee

你的接口回复:

该回复可以任意,根据你的需要设置;

 

(4)第四步、当access_token过期(如配置access_token过期1天,换取token的refresh_token则为30天,过期后29天之内都可以使用refresh_token换取access_token刷新获取新的令牌),此时必须带上client_id、client_secret、refresh_token并指定grant_type为refresh_token, 重点中的重点(这里坑了我3-4天时间):改请求必须以post请求发送且在postman中放在body设置部分(header和body都可以设置键值对,没看到,只设置了post方式,把给body的键值对写在了header部分,惭愧!)

请求:

http://localhost:8762/auth/oauth/token?client_id=test_client_id&grant_type=refresh_token&refresh_token=2260389e-db44-4100-b05d-4f5af77e1802

{
    "access_token": "6fc18d5d-4978-4c3d-8f9b-e2416b0e123c",
    "token_type": "bearer",
    "refresh_token": "2260389e-db44-4100-b05d-4f5af77e1802",
    "expires_in": 3599,
    "scope": "app"
}

回复了一个新的token和以及没有过期的refresh_token,过期应该会返回新的refresh_token;

 

四、问题

1、获取授权码,然后换取token过程是否安全

   oauth 的核心思想就是要让第三方在不知道用户名密码的情况下完成鉴权,但是没有密码用户名组合根本不可能有效鉴权, oauth 实际的过程是一个李代桃疆的手法,在第一方用你的原始用户名和密码组合,生成另外一对名称密码组合,这个阶段叫做获取 code 和 state,这对组合送到第二方也就是你的资源所在地,同样较验一遍,如果合格,给你生成一个带有时效性的 access token, 第三方在有效期内拿着这个 access token 跳过第一方直接请求第二方的资源,至于为什么不直接返回 access token? 是因为如果使用 code 方式的话,服务器获得用户授权后通过 302 跳转到你的 callback URI 上,并在 url query 上带上用于交换 accesd token 的 code ,你在浏览器地址栏就可以看到这个code ,已经暴露有可能被不法应用,所以在 url 上直接返回 access token 是不安全的,而client拿到code以后换取access token是client后台对认证服务器的访问,并且需要clientID和client secret,不依赖浏览器,access token不会暴露出去.

2、再次强调

    使用测试工具测试的情况下,比如我用的POSTMAN(其他工具一样),各个URL必须使用对应的方式如GET或POST,注意POST方式发送数据必须填写在body部分的键值对下面而不是header部分(header和body都可以填写键值对且postman都以表格方式让你填写),否则返回错误码:401 !!!!!!!。

3、系统表数据

关于系统数据表插入的内容见下一章节--用户服务;

 

快来成为我的朋友或合作伙伴,一起交流,一起进步!
QQ群:961179337
微信:lixiang6153
邮箱:lixx2048@163.com
公众号:IT技术快餐
更多资料等你来拿!

 

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝壳里的沙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值