Spring Cloud Security OAuth2

一、环境介绍

Spring-Security-OAuth2是对OAuth2的一种实现,并且跟之前学习的Spring Security相辅相成,与Spring Cloud体系的集成也非常便利。

OAuth2.0的服务提供方涵盖两个服务,即授权服务(Authorization Server,也即认证服务)和资源服务(Resource Server),使用Spring Security OAuth2.0的时候,可以选择把它们放在同一个应用中实现,也可以选择建立使用同一个授权服务的多个资源服务。

授权服务(Authorization Server)应包含对接入端以及登录用户的合法性进行验证并颁发token等功能,对令牌的请求端点由Spring MVC控制器进行实现,下面配置一个认证服务必须要实现的endpoints:

  • AuthorizationEndpoint 服务于认证请求,默认URL:/login/authorize。
  • TokenEndpoint 服务于访问令牌的请求,默认URL:/login/token。
  • OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。

本篇教程分别创建UAA授权服务(也可叫认证服务)和订单资源服务。

授权服务、订单资源服务

认证流程如下:
1、客户端请求UAA授权服务进行认证。
2、认证通过后由UAA颁发令牌。
3、客户端携带令牌Token请求资源服务。
4、资源服务校验令牌的合法性,合法即返回资源信息。

二、环境搭建

2.1 父工程

创建maven父工程distributed-security,pom依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<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.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pengjs.book.admin.distributed.security</groupId>
    <artifactId>distributed-security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>distributed-security</name>
    <packaging>pom</packaging>
    <description>distributed-security</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <modules>
        <module>distributed-security-uaa</module>
        <module>distributed-security-order</module>
        <module>distributed-security-discovery</module>
        <module>distributed-security-gateway</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>javax.servlet.jsp</groupId>
                <artifactId>javax.servlet.jsp-api</artifactId>
                <version>2.3.1</version>
                <scope>provided</scope>
            </dependency>

            <dependency>
                <groupId>javax.interceptor</groupId>
                <artifactId>javax.interceptor-api</artifactId>
                <version>1.2</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.60</version>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.0</version>
            </dependency>

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.47</version>
            </dependency>

            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.0.10.RELEASE</version>
            </dependency>

            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.1.3.RELEASE</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <!--父模块,编译插件-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2.2 UAA授权服务工程

1、创建distributed-security-uaa作为授权服务工程,依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<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.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.pengjs.book.admin.distributed.security</groupId>
        <artifactId>distributed-security</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.pengjs.book.admin.distributed.security.uaa</groupId>
    <artifactId>distributed-security-uaa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>distributed-security-uaa</name>
    <description>distributed-security-uaa</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </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>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-javanica</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</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-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

工程结构如下:

UAA认证服务工程结构

2、启动类

@SpringBootApplication
@EnableHystrix
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.pengjs.book.admin.distributed.security.uaa"})
public class DistributedSecurityUaaApplication {

    public static void main(String[] args) {
        SpringApplication.run(DistributedSecurityUaaApplication.class, args);
    }

}

3、配置文件application.yml

server:
  port: 53020
  servlet:
    context-path: /uaa
  tomcat:
    remote-ip-header: x-formarded-for
    protocol-header: x-formarded-proto
  use-forward-headers: true

spring:
  application:
    name: uaa-service
  http:
    encoding:
      enabled: true
      charset: UTF-8
      force: true
  main:
    allow-bean-definition-overriding: true
  freemarker:
    enabled: true
    suffix: .html
    request-context-attribute: rc
    content-type: text/html
    charset: UTF-8
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///oauth2?useUnicode=true&amp;characterEncoding=utf8
    username: root
    password: root


logging:
  level:
    root: debug
    org:
      springframework:
        web: info

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:53000/eureka/
  instance:
    preferIpAddress: true
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance-id:${server.port}}

management:
  endpoints:
    web:
      exposure:
        include: refresh,health,info,env

feign:
  hystrix:
    enabled: true
  compression:
    request:
      enabled: true
      mime-types[0]: text/html
      mime-types[1]: application/xml
      mime-types[2]: application/json
      min-request-size: 2048
    response:
      enabled: true

2.3 Order订单资源服务工程

本工程为Order订单服务工程,访问工程的资源需要通过认证。
1、创建Order工程,pom依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<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.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.pengjs.book.admin.distributed.security</groupId>
        <artifactId>distributed-security</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.pengjs.book.admin.distributed.security.order</groupId>
    <artifactId>distributed-security-order</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>distributed-security-order</name>
    <description>distributed-security-order</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.interceptor</groupId>
            <artifactId>javax.interceptor-api</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

工程结构如下:

Order订单资源服务工程结构

2、配置文件application.yml

server:
  port: 53021
  servlet:
    context-path: /order
  tomcat:
    remote-ip-header: x-formarded-for
    protocol-header: x-formarded-proto
  use-forward-headers: true

spring:
  application:
    name: order-service
  http:
    encoding:
      enabled: true
      charset: UTF-8
      force: true
  main:
    allow-bean-definition-overriding: true
  freemarker:
    enabled: true
    suffix: .html
    request-context-attribute: rc
    content-type: text/html
    charset: UTF-8
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///oauth2?useUnicode=true&amp;characterEncoding=utf8
    username: root
    password: root


logging:
  level:
    root: debug
    org:
      springframework:
        web: info

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:53000/eureka/
  instance:
    preferIpAddress: true
    instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance-id:${server.port}}

management:
  endpoints:
    web:
      exposure:
        include: refresh,health,info,env

feign:
  hystrix:
    enabled: true
  compression:
    request:
      enabled: true
      mime-types[0]: text/html
      mime-types[1]: application/xml
      mime-types[2]: application/json
      min-request-size: 2048
    response:
      enabled: true

三、授权服务配置

3.1 EnableAuthorizationServer

可以使用@EnableAuthorizationServer注解并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0授权服务器。

在config包下创建AuthorizationServer

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    // 略...
}

AuthorizationServerConfigurerAdapter要求配置以下几个类,这几个类是由Spring创建的独立的配置对象,他们会被Spring传入AuthorizationServerConfigurer中进行配置。

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
}
  • ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者通过数据库来存储和查询详情信息。
  • AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
  • AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束。

3.2 配置客户端详情信息

ClientDetailsServiceConfigurer能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),ClientDetailsService负责找到ClientDetails,而ClientDetails有几个重要的如下:

  • clientId:(必须的)用来标识客户端的id。
  • secret:(需要知得新人的客户端)客户端安全嘛码,如果有的话。
  • scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系型数据库表中,皆可以使用JdbcClientDetailsService)或者通过自己实现ClientRegistrationService接口(同时你也可以实现ClientDetailsService接口)来进行管理。
这里暂时使用内存方式存储客户端详情信息,配置爱如下:

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        // clientDetailsService使用jdbc查询数据库的方式
        // clients.withClientDetails(clientDetailsService);

        // 使用in-memory存储
        clients.inMemory()
                // client_id
                .withClient("c1")
                // 客户端秘钥
                .secret(new BCryptPasswordEncoder().encode("secret"))
                // 客户端可以访问的资源列表
                .resourceIds("res1")
                // 该client允许的授权范围(所有支持的5种)
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
                // 允许的授权范围(客户端的权限),user-service、read等标识
                .scopes("all")
                // false:授权码模式,跳转到授权的页面,如果是true,不用跳转页面
                .autoApprove(false)
                // 加上验证回调地址
                .redirectUris("http://www.baidu.com");
    }

3.3 管理令牌

AuthorizationServerTokenServices接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。

自己可以创建AuthorizationServerTokenServices这个接口的实现,则需要继承DefaultTokenServices(实现了AuthorizationServerTokenServices)这个类,里面已经包含了一些有用的实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个TokenStore接口来实现的以外,这个类几乎帮你做了所有的事情。并且TokenStore这个接口有一个默认的时间,它就是InMemoryTokenStore,如其命名,所有的令牌是被保存在内存中。除了使用这个以外,你还可以使用以下其他的预定义实现,下面有几个版本,他们都实现了TokenStore接口:

  • InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为他不会被保存到磁盘中,更便于调试。
  • JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以死在不同服务器之间共享领牌子信息,使用这个版本的时候,请注意把“spring-jdbc”这个依赖加入到pom中。
  • JwtTokenStore:这个版本的全称是JSON Web Token(JWT),它把令牌相关的数据惊醒编码(因此对于后端服务来说,他不需要进行存储,这将是一个重大优势),但是它有一个缺点,就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点是这个令牌占用的空间会比较大,如果你加入了比较多的用户凭证信息。JwtTokenStore不会保存任何数据,但是它在转换令牌值以及授权信息方面与DefaultTokenServices所扮演的角色是一样的。

3.3.1 定义TokenConfig

在config包下定义TokenConfig,这里暂时先使用InMemoryTokenStore,生成一个普通的令牌。

@Configuration
public class TokenConfig {
   /**
     * 临牌的存储策略
     * @return TokenStore
     */
    @Bean
    public TokenStore tokenStore() {
        // 使用内存方式存储令牌(普通令牌)
        return new InMemoryTokenStore();
    }
}

3.3.2 定义AuthorizationServerTokenServices

在AuthorizationServer中定义AuthorizationServerTokenServices

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

   /**
     * 令牌管理服务
     * @return
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        // 客户端详情服务
        service.setClientDetailsService(clientDetailsService);
        // 是否产生支持刷新令牌
        service.setSupportRefreshToken(true);
        // 令牌存储策略
        service.setTokenStore(tokenStore);

        // 设置令牌增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        // 令牌桶默认有效期2小时
        service.setAccessTokenValiditySeconds(7200);
        // 刷新令牌默认有效期3天
        service.setRefreshTokenValiditySeconds(259200);
        return service;
    }

3.4 令牌访问端点配置

AuthorizationServerEndpointsConfigurer这个对象的实例可以完成令牌服务以及令牌endpoint配置。

3.4.1 配置授权类型(Grant Types)

AuthorizationServerEndpointsConfigurer通过设定以下属性决定支持的授权类型(Grant Types)

  • authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个AuthenticationManager对象。
  • userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的UserDetailsService接口的实现,或者你可以把这个东西设置到全局域上面去(例如GlobalAuthenticationManagerConfigurer这个配置对象),当你设置了这个之后,那么“refresh_token”即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账号的话。
  • authorizationCodeServices:这个属性是用来设置授权码服务的(即AuthorizationCodeServices的实例对象),主要用于“authorization_code”授权码类型模式。
  • implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
  • tokenGranter:当你设置了这个属性(即TokenGranter接口实现),那么授权将交由你来完全掌控,并且会忽略掉上面的几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。

3.4.2 配置授权端点URL(Endpoint URLs)

AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做pathMapping()的方法用来配置端点URL链接,它有两个参数:

  • 第一个参数:String类型,这个端点URL的默认链接。
  • 第二个参数:String类型,你要进行替换的URL链接。

以上的参数都将以“/”字符开始的字符串,框架的默认URL链接如下列表,可以作为这个pathMapping()方法的第一个参数:

  • /oauth/authorize:授权端点。
  • /oauth/token:令牌端点。
  • /oauth/confirm_access:用户确认授权提交端点。
  • /oauth/error:授权服务错误信息端点。
  • /oauth/check_token:用于资源访问的令牌解析端点。
  • /oauth/token_key:提供公有秘钥的端点,如果你使用JWT令牌的话。

需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问。

AuthorizationServer中配置爱令牌访问端点:

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

   /**
     * 令牌访问端点
     * tokenService():令牌管理服务
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // authenticationManager:认证管理器,密码模式
        // authorizationCodeServices:授权码服务
        endpoints.authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices)
                // 令牌管理服务,都需要
                .tokenServices(tokenService())
                // 允许的POST提交
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

3.5 令牌访问端点的安全约束

AuthorizationServerSecurityConfigurer 用来配置令牌端点(Token Endpoint)的安全约束,在AuthorizationServer中配置如下:

    /**
     * 令牌端点的安全配置
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // /oauth/token_key: 这个endpoint当使用jwttoken且使用非对称加密时,资源服务器用于获取公钥而开放的,这里指这个endpoint完全公开
        security.tokenKeyAccess("permitAll()")
                // oauth/check_token: checkToken这个endpoint完全公开
                .checkTokenAccess("permitAll()")
                // 允许表单认证
                .allowFormAuthenticationForClients();
    }
  • tokenKeyAccess("permitAll()"):“/oauth/token_key”这个endpoint当使用jwttoken且使用非对称加密时,资源服务器用于获取公钥而开放的,这里指这个endpoint完全公开。
  • checkTokenAccess("permitAll()"):“auth/check_token“checkToken这个endpoint完全公开。
  • allowFormAuthenticationForClients():允许表单认证。

授权服务配置总结:授权服务配置分成三大块,可以关联记忆。
既然要完成认证,它首先得先知道客户端信息从哪里读取,因此要进行客户端详情配置。
既然要颁发token,那必须得定义token的相关endpoint,以及token如何存取,以及客户端支持哪些类型的token。
既然暴露了一些endpoint,那对这些endpoint可以定义一些安全上的约束等等。

3.6 web安全配置

/**
 * @EnableGlobalMethodSecurity(securedEnabled = true) 启用基于注解的安全性,可以使用@Secured注解
 */
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 密码编码器(采用什么方式比对)
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证管理器
     * @return
     * @throws Exception
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /**
     * 安全拦截机制(最重要)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 屏蔽CSRF控制,即spring security不再限制CSRF(跨站请求伪造)
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();

    }
}

四、授权码模式

4.1 授权码模式介绍

如下图是授权码模式交互图:

授权码模式交互

(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,浏览器重定向到授权服务器,重定向时会附加客户端的身份信息,如:/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

参数列表如下:

  • client_id:客户端准入标识。
  • response_type:授权码模式固定为code。
  • scope:客户端授权。
  • redirect_uri:跳转URI,当授权码申请成功后跳转到此地址,并在后边带上code参数(授权码)。

(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
(3)授权浏览器将授权码(AuthorizationCode)经浏览器发送给client(通过redirect_uri)
(4)客户端拿到授权码向授权服务器索要访问access_token,请求如下:
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com

参数列表如下:

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写authorization_code,表示授权码模式。
  • code:授权码,就是刚刚获取到的授权码,注意:授权码只是用一次就无效了,需要重新申请。
  • redirect_uri:申请授权码时的跳转url,一定要和申请授权码时用的redirect_uri一致。

4.2 授权码模式演示

4.2.1 授权登录获取授权码访问

浏览器访问如下URL:http://127.0.0.1:53020/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

自动跳转到授权登录页面:

授权登录页面
重定向到登录页面
授权页面
跳转授权页面
授权地址跳转
带上获取到的授权码根据redirect_uri跳转到百度

4.2.2 使用授权码去申请token

curl --location --request POST 'http://127.0.0.1:53020/uaa/oauth/token' \
--form 'client_id=c1' \
--form 'client_secret=secret' \
--form 'grant_type=authorization_code' \
--form 'code=6Q2nz7' \
--form 'redirect_uri=http://www.baidu.com'

响应结果:

{
    "access_token": "04255fc1-2e95-4419-a819-1bab1bfed73f",
    "token_type": "bearer",
    "refresh_token": "d983f5f3-78df-4ee6-9088-edc6098ffbfe",
    "expires_in": 6925,
    "scope": "all"
}
使用授权码去申请token
与配置中的对应
令牌过期时间

一个授权码只能使用一次:

一个授权码只能使用一次

五、简化模式

5.1 简化模式介绍

下图是简化模式交互图:

简化模式交互

1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器重定向到授权服务器,重定向时会附加客户端的身份信息,如:/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com,参数描述同授权码模式注意response_type=token表示简化模式
(2)浏览器出现授权服务器授权页面,之后将用户信息同意授权。
(3)授权服务器将授权令牌以hash的形式存放在重定向的URI的fargument参数中发送给浏览器。
注:fargument主要用来标识URI所标识资源里的某个资源,在URI的末尾通过(#)作为fargument的开头,其中#不属于fargument的值,如https://domain/index#L18这个URI中L18就是fargument的值。大家只需要知道js通过响应浏览器地址栏变化的方式来获取到fargument就行了。
一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。

5.2 简化模式演示

浏览器中输入URI:http://127.0.0.1:53020/uaa/oauth/authorize?client_id=c1&response_type=token&scpoe=all&redirect_uri=http://www.baidu.com(注意:response_type=token 简化模式)

简化模式授权页面
授权
带上access_token回到redirect_uri指定的页面

https://www.baidu.com/#access_token=c22bc67a-e4f8-45e9-81ec-c8022a4a4d74&token_type=bearer&expires_in=5979&scope=all

六、密码模式

6.1 密码模式介绍

下图是密码模式交互图:

密码模式交互

(1)资源拥有者将用户名、密码发送给客户端
(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123

参数列表如下:

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写password表示密码模式。
  • username:支援拥有者的用户名。
  • password:支援拥有者密码。

(3)授权服务器将令牌(access_token)发送给client

这种模式十分简单,但是却意味着直接将用户敏感信息邪路给了client,因此这就说明这种模式只能用于client使我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。

注意:如果要让密码模式生效时需要在授权模式中配置的

密码授权模式配置

6.2 密码模式演示

使用POST方式:http://127.0.0.1:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123

密码模式响应结果

七、客户端模式

7.1 客户端模式介绍

客户端模式交互

(1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)
(2)确认客户端身份无误后,将令牌(access_token)发送给client,请求如下:/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials

参数列表如下:

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写client_credentials表示客户端模式。

这种模式是最方便但最不安全的模式,因此者及要求我们对client完全的信任,而client本省也是安全的,因此这种模式一般用来提供给我们完全信任的服务器端服务,比如,合作方系统对接,拉取一组用户信息。

7.2 客户端模式演示

POST方式:http://127.0.0.1:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials

客户端模式演示

八、资源服务测试

8.1 资源服务配置

@EnableResourceServer注解到一个@Configuration配置类上,并且必须使用ResourceServerConfig这个配置对象来进行配置(可以继承自ResourceServerConfigurerAdapter然后覆写其中的方法,参数就是这个对象的实例),下面是一些可以配置的属性。

ResourceServerSecurityConfigurer中主要包括:

  • tokenServicesResourceServerTokenServices类的实例,用来实现令牌服务。
  • tokenStoreTokenStore类的实例,指定令牌如何访问,与tokenServices配置可选。
  • resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务器中进行验证。
  • 其他的拓展属性例如tokenExtractor令牌提取器,用来提取请求中的令牌。

HttpSecurity配置这个与Spring Security类似:

  • 请求匹配器,用来设置需要惊醒保护的资源路径,默认的情况下是保护资源服务的全部路径。
  • 通过http.authorizeRequests()来设置受保护资源的访问规则。
  • 其他的自定义权限保护规则通过HttpSecurity来进行配置。

@EnableResourceServer注解自动增加了一个类型为OAuth2AuthenticationProcessingFilter的过滤器链

编写ResourceServerConfig

@Configuration
@EnableResourceServer
// 或者是单独配置WebSecurityConfig也可以
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    /**
     * 资源列表,与服务端的resourceIds一致
     */
    private static final String RESOURCE_ID = "res1";

    /**
     * 从TokenConfig中注入tokenStore
     */
    @Autowired
    private TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        // 资源id
        resources.resourceId(RESOURCE_ID)
             // 验证令牌的服务
             .tokenServices(tokenService())
             .stateless(true);
    }

    /**
     * 资源访问策略
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**")
                .access("#oauth2.hasScope('all')") // 所有的请求都必须有scope=all,跟服务端一致
                .and().csrf().disable() // 关闭CSRF
                // 基于token的方式,session就不用再记录了
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }


    /**
     * 资源服务令牌解析服务
     * @return
     */
    @Bean
    public ResourceServerTokenServices tokenService() {
        // 使用远程服务请求授权服务校验token,必须制定校验token的URL、client_id、client_secret
        RemoteTokenServices service = new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }

}

8.2 验证token

ResourceServerTokenServices是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序中的话,你可以使用DefaultTokenServices(ResourceServerTokenServices的实现类),这样的话,你就不用考虑关于实现所有必要的接口的一致性问题。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的ResourceServerTokenServices,它知道如何对令牌进行解码。

令牌解析方法:

  1. 使用DefaultTokenServices在资源服务器本地配置令牌存储、解码、解析方式。
  2. 使用RemoteTokenServices资源服务器通过Http请求来解码令牌,每次都请求授权服务器端点/oauth/check_token

使用授权服务的/oauth/check_token端点,你需要在石泉服务中将这个端点暴露出去,一遍资源服务可以进行访问,这在咱们的授权服务配置中已经提到了,下面是一个例子,我们在授权服务中配置了/oauth/check_token/oauth/check_key两个端点:

    /**
     * 令牌端点的安全配置
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // /oauth/token_key: 这个endpoint当使用jwttoken且使用非对称加密时,资源服务器用于获取公钥而开放的,这里指这个endpoint完全公开
        security.tokenKeyAccess("permitAll()")
                // oauth/check_token: checkToken这个endpoint完全公开
                .checkTokenAccess("permitAll()")
                // 允许表单认证
                .allowFormAuthenticationForClients();
    }

在资源服务中配置RemoteTokenServices,在ResourceServerConfig中配置:

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        // 资源id
        resources.resourceId(RESOURCE_ID)
                // 验证令牌的服务
                .tokenServices(tokenService())
                .stateless(true);
    }

    /**
     * 资源服务令牌解析服务
     * @return
     */
    @Bean
    public ResourceServerTokenServices tokenService() {
        // 使用远程服务请求授权服务校验token,必须制定校验token的URL、client_id、client_secret
        RemoteTokenServices service = new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }

调用授权服务check_token示例:
POST方式:http://127.0.0.1:53020/uaa/oauth/check_token?token=92f32994-c429-4314-a96d-3a85078f4328

调用授权服务check_token示例

8.3 编写资源

在controller包下编写OrderController,此controller表示订单资源的访问类:

@RestController
public class OrderController {

    /**
     * 流程:当携带token访问这个资源的时候,会通过远程的service去请求地址
     * http://localhost:53020/uaa/oauth/check_token校验token是否合法
     * @return
     */
    @GetMapping(value = "/r1")
    // 拥有p1权限方可访问此URL
    @PreAuthorize("hasAnyAuthority('p1')")
    public String r1() {
        // 通过spring security api获取当前登录用户
        // UserDto userDto = (UserDto) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return username + "访问资源1";
        // return userDto.getUsername() + "访问资源1";
        // return JSON.toJSONString(userDto) + "访问资源1";
    }

    @GetMapping(value = "/r2")
    // 拥有p2权限方可访问此URL
    @PreAuthorize("hasAnyAuthority('p2')")
    public String r2() {
        UserDto userDto = (UserDto) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        // return userDto.getUsername() + "访问资源2";
        return JSON.toJSONString(userDto) + "访问资源2";
    }

}

8.4 测试

  1. 申请令牌,这里使用密码方式http://127.0.0.1:53020/uaa/oauth/token
    响应结果:
{
    "access_token": "e6f8f518-8ec6-4b79-94cd-fee1ee689030",
    "token_type": "bearer",
    "refresh_token": "629e37b4-7115-4ccd-9d78-c91f047bc646",
    "expires_in": 6616,
    "scope": "all"
}
申请令牌
  1. 请求资源
    按照OAuth2.0协议要求,请求资源需要携带token,参数名称为Authorization,值为Bearer token值,POST方式http://localhost:53021/order/r1
请求资源

8.5 添加安全访问控制

/**
 * 安全访问控制
 */
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 安全拦截机制(最重要)
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                // 先在是基友方法的授权,基于web的授权可以屏蔽掉
                .authorizeRequests()
                // .antMatchers("/r/r1").hasAnyAuthority("p1")
                // .antMatchers("/r/r2").hasAnyAuthority("p2")
                .antMatchers("/r/**").authenticated() // 所有/r/**的请求都必须认证通过
                .anyRequest().permitAll(); // 除了/r/**,其他的请求都可以访问
    }
}
权限不对的情况
token无效的情况

推荐阅读更多精彩内容