SpringCloud整合SpringSecurity实现单点登录之OAuth2.0协议开放授权

OAuth2.0简介

授权流程

  • 流程图
    OAuth2.0协议授权流程

角色分析

  • 客户端(Client)

    本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如Web客户端(浏览器)、微信客户端

  • 资源拥有者(Resource Owner)

    通常为用户,也可以是应用程序,即该资源的拥有者

  • 授权服务器(Authrization Server)

    用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌(access_token),作为客户端访问资源的凭据。如微信认证服务器

  • 资源服务器(Resource Server)

    存储资源的服务器,如微信存放用户信息的资源服务器

  • 注意事项
    服务提供商需要预先给准入的接入方一个身份,用于接入时的凭据

    client_id:客户端标识
    client_secret:客户端秘钥

    因此,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者和客户端

授权方式(grant_type)

  • 参考链接

    https://www.jianshu.com/p/0d61b23f41cf

  • 简介

    不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的

授权码(authorization_code)

  • 简介

    授权码(authorization_code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌

  • 特点

    最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

  • 例图
    授权码模式流程图

  • 流程详解

    1. A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。
    
      
      https://b.com/oauth/authorize?
        response_type=code&
        client_id=CLIENT_ID&
        redirect_uri=CALLBACK_URL&
        scope=read
    
    
    
    

    (1) response_type参数表示要求返回授权码(code)
    (2) client_id参数让 B 知道是谁在请求(客户端标识)
    (3) redirect_uri参数是 B 接受或拒绝请求后的跳转网址
    (4) scope参数表示要求的授权范围(这里是只读)

    1. 用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。
    
      
      https://a.com/callback?code=AUTHORIZATION_CODE
    
    
    

    (1) code参数就是授权码

    1. A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。
    
      
      https://b.com/oauth/token?
      client_id=CLIENT_ID&
      client_secret=CLIENT_SECRET&
      grant_type=authorization_code&
      code=AUTHORIZATION_CODE&
      redirect_uri=CALLBACK_URL
    
    
    
    

    (1) client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求)
    (2) grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码
    (3) code参数是上一步拿到的授权码
    (4) redirect_uri参数是令牌颁发后的回调网址

    1. B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据
    
      
      {    
        "access_token":"ACCESS_TOKEN",
        "token_type":"bearer",
        "expires_in":2592000,
        "refresh_token":"REFRESH_TOKEN",
        "scope":"read",
        "uid":100101,
        "info":{...}
      }
    
    
    

    (1) access_token字段就是令牌,A 网站在后端拿到了

隐藏式(implicit)

  • 简介

    Web 应用是纯前端应用,没有后端。必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)

  • 特点

    这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了

  • 例图
    隐藏式授权流程图

  • 流程详解

    1. A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。
    
      
      https://b.com/oauth/authorize?
        response_type=token&
        client_id=CLIENT_ID&
        redirect_uri=CALLBACK_URL&
        scope=read
    
    
    

    (1) response_type参数为token,表示要求直接返回令牌

    1. 用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站
    
      
      https://a.com/callback#token=ACCESS_TOKEN
    
    
    

    (1) token参数就是令牌,A 网站因此直接在前端拿到令牌

    注意事项:令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险

密码式(password)

  • 简介

    如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)

  • 特点

    这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用

  • 流程详解

    1. A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌
    
      
      https://oauth.b.com/token?
        grant_type=password&
        username=USERNAME&
        password=PASSWORD&
        client_id=CLIENT_ID
    
    
    

    (1) grant_type参数是授权方式,这里的password表示"密码式"
    (2) username和password是 B 的用户名和密码

    1. B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌
    
      
      {    
        "access_token":"ACCESS_TOKEN",
        "token_type":"bearer",
        "expires_in":2592000,
        "refresh_token":"REFRESH_TOKEN",
        "scope":"read",
        "uid":100101,
        "info":{...}
      }
    
    
    

客户端凭证(client_credentials)

  • 简介

    最后一种方式是凭证式(client_credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。

  • 特点

    这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

  • 流程详解

    1. A 应用在命令行向 B 发出请求。
    
      https://oauth.b.com/token?
        grant_type=client_credentials&
        client_id=CLIENT_ID&
        client_secret=CLIENT_SECRET
    
    
    

    (1) grant_type参数等于client_credentials表示采用凭证式

    (2) client_id和client_secret用来让 B 确认 A 的身份

    1. B 网站验证通过以后,直接返回令牌。
  • 令牌的使用

    A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。

    此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization字段,令牌就放在这个字段里面。

    
      # ACCESS_TOKEN就是拿到的令牌。
      curl -H "Authorization: Bearer ACCESS_TOKEN" \
      "https://api.b.com"
    
    

刷新令牌(refresh_token)

  • 简介

    令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。

  • 实现流程

    B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh_token 字段)。令牌到期前,用户使用 refresh_token 发一个请求,去更新令牌。

    
      
      https://b.com/oauth/token?
        grant_type=refresh_token&
        client_id=CLIENT_ID&
        client_secret=CLIENT_SECRET&
        refresh_token=REFRESH_TOKEN
    
    
    

    (1) grant_type参数为refresh_token表示要求更新令牌

    (2) client_id参数和client_secret参数用于确认身份

    (3) refresh_token参数就是用于更新令牌的令牌

  • 特点

    如果访问令牌受到劫持,由于它的存在是短时间的,所以对访问令牌的滥用是控制在一定范围内的。

    如果刷新令牌被劫持,基本上无害的,攻击者需要得到 client_id,secrect_id (通常存储在服务器上),再加上刷新令牌才可以进行操作

Spring Cloud Security OAuth2

环境介绍

  • 技术方案
    分布式认证技术方案

  • 两个服务

    OAuth2.0的服务提供方涵盖两个服务,即授权服务(Authorization Server,也叫认证服务)和资源服务(Resource Server),使用Spring Security OAuth2时,可以选择将两者整合在同一个应用程序中实现,也可以选择建立使用一个授权服务的多个资源服务

  • 授权服务(Authorization Server)

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

    1. AuthorizationEndpoint服务于认证请求。默认URL:/oauth/authorize

    2. TokenEndpoint服务于访问令牌的请求。默认URL:/oauth/token

  • 资源服务(Resource Server)

    包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴权等,下面的过滤器用于实现OAuth2.0资源服务:

    1. OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。
  • 认证流程图
    案例项目认证流程图

环境搭建

父工程

  • IDEA创建工程

    1. 打开idea创建项目

      File -> New -> Project

    2. 创建Maven工程

      Maven -> Next

    3. 指定工程名

      GroupId com.nbsp.security
      ArtifactId 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 http://maven.apache.org/xsd/maven‐
      4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.itheima.security</groupId>
        <artifactId>distributed‐security</artifactId>
        <version>1.0‐SNAPSHOT</version>
        <packaging>pom</packaging>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.3.RELEASE</version>
        </parent>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
            <java.version>1.8</java.version>
        </properties>
    
        <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</groupId>
                    <artifactId>javax.servlet-api</artifactId>
                    <version>3.1.0</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.47</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>
            <finalName>${project.name}</finalName>
            <resources>
                <resource>
                    <directory>src/main/resources</directory>
                    <filtering>true</filtering>
                    <includes>
                        <include>**/*</include>
                    </includes>
                </resource>
    
                <resource>
                    <directory>src/main/java</directory>
                    <includes>
                        <include>**/*.xml</include>
                    </includes>
                </resource>
            </resources>
    
            <plugins>
                <!--<plugin>-->
                <!--<groupId>org.springframework.boot</groupId>-->
                <!--<artifactId>spring-boot-maven-plugin</artifactId>-->
                <!--</plugin>-->
    
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
    
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <configuration>
                        <encoding>utf-8</encoding>
                        <useDefaultDelimiters>true</useDefaultDelimiters>
                    </configuration>
                </plugin>
            </plugins>
        </build>
      </project>
    
    
  • 注意事项

    注意引入pom内容中"-“符号,一定是英文下的”-",否则项目不会自动下载maven仓库

UAA授权服务器工程

  • IDEA创建distributed-security-uaa工程

    1. 创建子工程

      File -> Project Structure -> Modules -> + -> New Module -> Maven
      IDEA创建distributed-security工程子模块_1

    2. 选择父工程
      IDEA创建distributed-security工程子模块_2

  • 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 http://maven.apache.org/xsd/maven‐
      4.0.0.xsd">
    
        <parent>
          <artifactId>distributed‐security</artifactId>
          <groupId>com.itheima.security</groupId>
          <version>1.0‐SNAPSHOT</version>
        </parent>
    
        <modelVersion>4.0.0</modelVersion>
        <artifactId>distributed‐security‐uaa</artifactId>
        
        <dependencies>
            <!--<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>
      </project>
    
    
  • 工程结构
    distributed-security-uaa工程结构图

  • 启动类

    
      @SpringBootApplication
      @EnableDiscoveryClient
      @EnableHystrix
      @EnableFeignClients(basePackages = {"com.itheima.security.distributed.uaa"})
      public class UAAServer {
        public static void main(String[] args) {
          SpringApplication.run(UAAServer.class, args);
        }
      }
    
    
    
  • 配置文件

    在resources下创建application.properties

    
      spring.application.name=uaa‐service
      server.port=53020
      spring.main.allow‐bean‐definition‐overriding = true
      logging.level.root = debug
      logging.level.org.springframework.web = info
      spring.http.encoding.enabled = true
      spring.http.encoding.charset = UTF‐8
      spring.http.encoding.force = true
    
    
      server.tomcat.remote_ip_header = x‐forwarded‐for
      server.tomcat.protocol_header = x‐forwarded‐proto
      server.use‐forward‐headers = true
      server.servlet.context‐path = /uaa
      spring.freemarker.enabled = true
      spring.freemarker.suffix = .html
      spring.freemarker.request‐context‐attribute = rc
      spring.freemarker.content‐type = text/html
      spring.freemarker.charset = UTF‐8
      spring.mvc.throw‐exception‐if‐no‐handler‐found = true
      spring.resources.add‐mappings = false
      spring.datasource.url = jdbc:mysql://localhost:3306/user_db?useUnicode=true
      spring.datasource.username = root
      spring.datasource.password = mysql
      spring.datasource.driver‐class‐name = com.mysql.jdbc.Driver
    
      #eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
      #eureka.instance.preferIpAddress = true
      #eureka.instance.instance‐id = ${spring.application.name}:${spring.cloud.client.ipaddress}:${
      spring.application.instance_id:${server.port}}
      management.endpoints.web.exposure.include = refresh,health,info,env
    
      feign.hystrix.enabled = true
      feign.compression.request.enabled = true
      feign.compression.request.mime‐types[0] = text/xml
      feign.compression.request.mime‐types[1] = application/xml
      feign.compression.request.mime‐types[2] = application/json
      feign.compression.request.min‐request‐size = 2048
      feign.compression.response.enabled = true
    

Order订单资源工程

  • IDEA创建distributed‐security‐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 http://maven.apache.org/xsd/maven‐
      4.0.0.xsd">
    
        <parent>
          <artifactId>distributed‐security</artifactId>
          <groupId>com.itheima.security</groupId>
          <version>1.0‐SNAPSHOT</version>
        </parent>
    
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>distributed‐security‐order</artifactId>
    
        <dependencies>
          <!‐‐<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>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
          </dependency>
    
        </dependencies>
      </project>
    
    
    
  • 工程结构
    distributed‐security‐order工程结构图

  • 启动类

    
      
    
    
  • 配置文件

    在resources中创建application.properties

    
      spring.application.name=order‐service
      server.port=53021
      spring.main.allow‐bean‐definition‐overriding = true
      logging.level.root = debug
      logging.level.org.springframework.web = info
      spring.http.encoding.enabled = true
      spring.http.encoding.charset = UTF‐8
      spring.http.encoding.force = true
      server.tomcat.remote_ip_header = x‐forwarded‐for
      server.tomcat.protocol_header = x‐forwarded‐proto
      server.use‐forward‐headers = true
      server.servlet.context‐path = /order
    
      spring.freemarker.enabled = true
      spring.freemarker.suffix = .html
      spring.freemarker.request‐context‐attribute = rc
      spring.freemarker.content‐type = text/html
      spring.freemarker.charset = UTF‐8
      spring.mvc.throw‐exception‐if‐no‐handler‐found = true
      spring.resources.add‐mappings = false
      #eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
      #eureka.instance.preferIpAddress = true
    
      #eureka.instance.instance‐id = ${spring.application.name}:${spring.cloud.client.ipaddress}:${
      spring.application.instance_id:${server.port}}
      management.endpoints.web.exposure.include = refresh,health,info,env
      feign.hystrix.enabled = true
      feign.compression.request.enabled = true
      feign.compression.request.mime‐types[0] = text/xml
      feign.compression.request.mime‐types[1] = application/xml
      feign.compression.request.mime‐types[2] = application/json
      feign.compression.request.min‐request‐size = 2048
      feign.compression.response.enabled = true
    
    

授权服务器配置

AuthorizationServerConfigurerAdapter(认证配置)

新增授权服务器配置类
  • 在Config包下创建AuthorizationServerConfig

    
      //ClientDetailsService和AuthorizationCodeServices从数据库读取数据
      @Configuration
      @EnableAuthorizationServer
      public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        
        @Autowired
        private TokenStore tokenStore;
    
        @Autowired
        private JwtAccessTokenConverter accessTokenConverter;
    
        @Autowired
        private ClientDetailsService clientDetailsService;
    
        @Autowired
        private AuthorizationCodeServices authorizationCodeServices;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        /**
        * 1.客户端详情相关配置
        */
        @Bean
        public PasswordEncoder passwordEncoder() {
          return new BCryptPasswordEncoder();
        }
    
        @Bean
        public ClientDetailsService clientDetailsService(DataSource dataSource) {
    
          ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
          ((JdbcClientDetailsService)clientDetailsService).setPasswordEncoder(passwordEncoder());
          return clientDetailsService;
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
          clients.withClientDetails(clientDetailsService);
        }
    
        /**
        * 2.配置令牌服务(token services)
        */
        @Bean
        public AuthorizationServerTokenServices tokenService() {
          DefaultTokenServices service=new DefaultTokenServices();
          service.setClientDetailsService(clientDetailsService);
          service.setSupportRefreshToken(true);//支持刷新令牌
          service.setTokenStore(tokenStore); //绑定tokenStore
          TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
          tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
          service.setTokenEnhancer(tokenEnhancerChain);
          service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
          service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
          return service;
        }
    
    
        /**
        * 3.配置令牌(token)的访问端点
        */
        @Bean
        public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
          return new JdbcAuthorizationCodeServices(dataSource);//设置授权码模式的授权码如何存取
        }
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    
          endpoints.authenticationManager(authenticationManager)
                  .authorizationCodeServices(authorizationCodeServices)
                  .tokenServices(tokenService())
                  .allowedTokenEndpointRequestMethods(HttpMethod.POST);
        }
        
        /**
        * 4.配置令牌端点(Token Endpoint)的安全约束
        */
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security){
          security
          .tokenKeyAccess("permitAll()")
          .checkTokenAccess("permitAll()")
          .allowFormAuthenticationForClients()//允许表单认证
          ;
    
        }
      }
    
    
    
    
  • 用 @EnableAuthorizationServer 注解并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权服务器

  • 重写3个配置方法

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

    
      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 {}
      }
    
    

    重写配置方法作用:

    1. AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。

    2. ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,可以把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。

    3. AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束。

配置详解
配置客户端详细信息
  • 基本概念

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

    cleint-id:(必须的)用来标识客户的Id

    client-secret:(需要值得信任的客户端)客户端安全码,如果有的话

    scopes:用来限制客户端的访问范围,在换取的 token 的时候会带上 scope 参数,只有在 scopes 定义内的,才可以正常换取 token,如果为空(默认)的话,那么客户端拥有全部的访问范围。

    authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。常见授权类型:
    authorization_code:授权码类型。
    implicit:隐式授权类型。
    password:资源所有者(即用户)密码类型。
    client_credentials:客户端凭据(客户端ID以及Key)类型。
    refresh_token:通过以上授权获得的刷新令牌来获取新的令牌。
    accessTokenValiditySeconds:token 的有效期

    authorities:此客户端可以使用的权限(基于Spring Security authorities)。

    客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者**通过自己实现ClientRegistrationService接口(同时你也可以实现 ClientDetailsService 接口)**来进行管理

  • 内存中配置客户端详情信息

    
    
      @Override
      public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
          
          //使用存在内存中配置两个客户端应用的通行证,相当于硬编码了,正式环境下的做法是持久化到数据库中,比如 mysql 中
          clients.inMemory()//使用in-memory存储
                  .withClient("c1")//client_id
                  .secret(new BCryptPasswordEncoder().encode("secret"))//客户端密钥
                  .resourceIds("res1")  //接入资源列表
                  // 常用授权类型:authorization_code,password,implicit,client_credentials,refresh_token
                  .authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")//该client允许的授权类型,refresh_token指通过以上授权获得的刷新令牌来获取新的令牌。
                  .redirectUris("http://www.baidu.com")//验证回调地址
                  .scopes("all")//允许授权范围
                  .autoApprove(false)//false会跳转到授权页面让用户进行授权,true则不跳转
                  .and()//若配置多个客户端详情信息需要通过此方法进行连接
                  .withClient("c2")
                  .secret(new BCryptPasswordEncoder().encode("secret"))
                  .resourceIds("res2")
                  .authorizedGrantTypes("authorization_code", "refresh_token")
                  .redirectUris("http://www.baidu.com")
                  .scopes("all")
                  .autoApprove(false);
      }
    
    
    
  • 官方数据库结构中配置客户端详情信息

    1. 案例代码
    
    
        @Autowired
        private DataSource dataSource;
    
        //用来配置客户端详情服务(ClientDetailsService),通过数据库来存储调取详情信息
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
            clients.jdbc(dataSource);
            
        }
    
        
    
    1. 数据库表结构
    
    
      DROP TABLE IF EXISTS `oauth_client_details`;
    
      CREATE TABLE `oauth_client_details` (
        `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端标
        识',
        `resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
        COMMENT '接入资源列表',
        `client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
        COMMENT '客户端秘钥',
        `scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
        `authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT
        NULL,
        `web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT
        NULL,
        `authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
        `access_token_validity` int(11) NULL DEFAULT NULL,
        `refresh_token_validity` int(11) NULL DEFAULT NULL,
        `additional_information` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
        `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE
        CURRENT_TIMESTAMP(0),
        `archived` tinyint(4) NULL DEFAULT NULL,
        `trusted` tinyint(4) NULL DEFAULT NULL,
        `autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
        PRIMARY KEY (`client_id`) USING BTREE
      ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '接入客户端信息' ROW_FORMAT = Dynamic;
    
    
      INSERT INTO `oauth_client_details` VALUES ('c1', 'res1',
      '$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm', 'ROLE_ADMIN,ROLE_USER,ROLE_API',
      'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com',
      NULL, 7200, 259200, NULL, '2019‐09‐09 16:04:28', 0, 0, 'false');
      INSERT INTO `oauth_client_details` VALUES ('c2', 'res2',
      '$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm', 'ROLE_API',
      'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com',
      NULL, 31536000, 2592000, NULL, '2019‐09‐09 21:48:51', 0, 0, 'false');
    
    
    1. 注意事项

    client_secret 字段不能直接是 secret 的原始值,需要经过加密。因为是用的 BCryptPasswordEncoder,所以最终插入的值应该是经过 BCryptPasswordEncoder.encode()之后的值

  • 自定义数据库结构中配置客户端详情信息

    1. 配置类代码
    
      @Override
      public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
          // 配置客户端应用通信证, 用于client认证
          clients.withClientDetails(getClientDetails());
        
      }
    
      @Bean   // 声明ApplyClientDetailService
      public ApplyClientDetailService getClientDetails() {
          return new ApplyClientDetailService();
      }
    
    
    
    1. 自定义获取客户端详情类
    
        import org.apache.tomcat.jdbc.pool.DataSource;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.security.oauth2.provider.ClientDetails;
        import org.springframework.security.oauth2.provider.ClientDetailsService;
        import org.springframework.security.oauth2.provider.ClientRegistrationException;
        import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
        import oauth.security.client.service.ApplyService;
    
        /**
         *
         * 1. 对Client的访问主要依靠JdbcClientDetailsService类的实现,必须使用官方给出的数据库结构,
         * 如果想自定义数据库结构,可以根据需求重写ClientDetailsService类的实现
         * 
         * 
         * 2. 在配置客户端中,使用了ApplyClientDetailService类,是自定义的获取Client的一个类,实现ClientDetailsService接口,
         * 重写loadClientByClientId方法
         **/
        public class ApplyClientDetailService implements ClientDetailsService {
    
            @Autowired
            private ApplyService applyService;
    
            @Autowired
            private DataSource dataSource;
    
            @Override
            public ClientDetails loadClientByClientId(String applyName) throws ClientRegistrationException {
    
                /*
                // 使用mybatic验证client是否存在 ,根据需求写sql
                Map clientMap = applyService.findApplyById(applyName);
    
                if(clientMap == null) {
                    throw new ClientRegistrationException("应用" + applyName + "不存在!");
                }*/
    
        //        MyJdbcClientDetailsService jdbcClientDetailsService= new MyJdbcClientDetailsService(dataSource, "authentication");
                JdbcClientDetailsService jdbcClientDetailsService= new JdbcClientDetailsService(dataSource);
                ClientDetails clientDetails = jdbcClientDetailsService.loadClientByClientId(applyName);
    
                return clientDetails;
            }
        }
    
    
管理令牌
  • 基本概念

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

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

    InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

    JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的classpath当中

    JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的

  • 内存中保存令牌(Token)

    1. 定义TokenStore

    在config包下定义TokenConfig,用InMemoryTokenStore,生成一个普通的令牌

    
    
      @Configuration
      public class TokenConfig {
    
        @Bean
        public TokenStore tokenStore() {
          return new InMemoryTokenStore();
        }
      }
    
    
    
    
    
    1. 定义AuthorizationServerTokenServices

    在AuthorizationServerConfig(认证服务器配置类)中定义AuthorizationServerTokenServices

    
    
      @Autowired
      private TokenStore tokenStore;
    
      @Autowired
      private ClientDetailsService clientDetailsService;
    
      @Bean
      public AuthorizationServerTokenServices tokenService() {
    
        DefaultTokenServices service=new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);
        service.setSupportRefreshToken(true);
        service.setTokenStore(tokenStore);// 指定 token 的存储方式,普通jwt方式存储
        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
      }
    
    
    
  • 数据库保存令牌(Token)

    1. 案例代码
    
      @Autowired
      AuthenticationManager authenticationManager;
    
      @Autowired
      private DataSource dataSource;
    
      // 初始化JdbcTokenStore
      @Autowired
      public TokenStore getTokenStore() {
          return new JdbcTokenStore(dataSource);
      }
    
    
      //用来配置令牌(token)的访问端点和令牌服务(token services)
      @Override
      public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
          
          endpoints
                  .tokenStore(getTokenStore())   // 数据库保存token
                  .authenticationManager(authenticationManager); //调用此方法才能支持 password 模式。
      }
    
    
    
    1. 数据库表结构
    
      -- oauth_code表,Spring Security OAuth2使用,用来存储授权码
      DROP TABLE IF EXISTS `oauth_code`;
      CREATE TABLE `oauth_code` (
        `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
        `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
        `authentication` blob NULL,
        INDEX `code_index`(`code`) USING BTREE
      ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
    
    
    
    
  • Redis保存令牌(Token)

    
        
        @Autowired
        AuthenticationManager authenticationManager;
    
        @Autowired
        RedisConnectionFactory redisConnectionFactory;
    
        //用来配置令牌(token)的访问端点和令牌服务(token services)
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints
                    .tokenStore(new RedisTokenStore(redisConnectionFactory))// 指定 token 的存储方式,redis保存token
                    .authenticationManager(authenticationManager);    //调用此方法才能支持 password 模式。
    
        }
    
    
    
    
  • 普通JWT保存令牌(Token)

    
        
        @Autowired
        AuthenticationManager authenticationManager;
    
        @Autowired
        public UserDetailsService nbspUserDetailsService;
    
    
        //用来配置令牌(token)的访问端点和令牌服务(token services)
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
            endpoints
                .tokenStore(jwtTokenStore())  // 指定 token 的存储方式,普通jwt方式存储
                .accessTokenConverter(jwtAccessTokenConverter())
                .userDetailsService(nbspUserDetailsService)//设置用户验证服务,自定义用户实现类实现 UserDetailsService接口,重写loadUserByUsername方法,返回用户信息对象
                .authenticationManager(authenticationManager);//调用此方法才能支持 password 模式
    
        }
    
        @Bean
        public TokenStore jwtTokenStore() {
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
    
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
            accessTokenConverter.setSigningKey("dev");
            return accessTokenConverter;
        }
    
    
    
    
  • 增强JWT保存令牌(Token)

    1. JwtTokenConfig配置类
    
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.security.oauth2.provider.token.TokenEnhancer;
        import org.springframework.security.oauth2.provider.token.TokenStore;
        import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
        import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
    
        //自动注入JWT配置
        @Configuration
        public class JwtTokenConfig {
    
            @Bean//@Bean就放在方法上,就是让方法去产生一个Bean,然后交给Spring容器
            public TokenStore jwtTokenStore() {
                return new JwtTokenStore(jwtAccessTokenConverter());
            }
    
            @Bean
            public JwtAccessTokenConverter jwtAccessTokenConverter() {
                JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
                accessTokenConverter.setSigningKey("dev");//  Sets the JWT signing key
                return accessTokenConverter;
            }
    
            @Bean
            public TokenEnhancer jwtTokenEnhancer(){
            return new JWTokenEnhancer();
            }
        }
    
    
    
    
    
    1. JWTokenEnhancer增强配置类
    
    
        import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
        import org.springframework.security.oauth2.common.OAuth2AccessToken;
        import org.springframework.security.oauth2.provider.OAuth2Authentication;
        import org.springframework.security.oauth2.provider.token.TokenEnhancer;
    
        import java.util.HashMap;
        import java.util.Map;
    
        /**
        * 如果想在 JWT 中加入额外的字段(比方说用户的其他信息),
        * 需要自定义增强实现类,实现 spring security oauth2提供的 TokenEnhancer 增强器接口
        * 重写enhance方法(RedisToken 的方式同样可以)
        * 
        */
        public class JWTokenEnhancer implements TokenEnhancer {
    
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
                Map<String, Object> info = new HashMap<>();
                info.put("jwt-ext", "JWT 扩展信息");
                ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
                return oAuth2AccessToken;
            }
        }
    
    
    
    1. 在AuthorizationServerConfig(认证服务器配置类)中定义AuthorizationServerEndpointsConfigurer
    
    
        // 由于JWT配置已经抽取至JwtTokenConfig配置类,项目启动后预先加载,这里可以直接注入
        @Autowired
        private TokenStore jwtTokenStore;
    
        @Autowired//默认按类型注入,若存在多个实例,需要结合@Qualifier注解指定注入Bean的名称
        private JwtAccessTokenConverter jwtAccessTokenConverter;
    
        @Autowired
        private TokenEnhancer jwtTokenEnhancer;
        
        @Autowired
        AuthenticationManager authenticationManager;
    
        @Autowired
        public UserDetailsService nbspUserDetailsService;
    
    
        //用来配置令牌(token)的访问端点和令牌服务(token services)
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
            TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
            List<TokenEnhancer> enhancerList = new ArrayList<>();
            enhancerList.add(jwtTokenEnhancer);
            enhancerList.add(jwtAccessTokenConverter);
            enhancerChain.setTokenEnhancers(enhancerList);
            endpoints.tokenStore(jwtTokenStore)// 指定 token 的存储方式,增强jwt方式存储
                    .accessTokenConverter(jwtAccessTokenConverter)
                    .tokenEnhancer(enhancerChain)
                    .userDetailsService(nbspUserDetailsService)//设置用户验证服务,自定义用户实现类实现 UserDetailsService接口,重写loadUserByUsername方法,返回用户信息对象
                    .authenticationManager(authenticationManager);//调用此方法才能支持 password 模式
    
        }
    
    
    
  • 常见自定义重写实现类(补充)

    1. 重写UserDetailsService接口实现类
    
        import com.alibaba.fastjson.JSON;
        import com.cjs.sso.domain.MyUser;
        import com.cjs.sso.entity.SysPermission;
        import com.cjs.sso.entity.SysUser;
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.security.core.authority.SimpleGrantedAuthority;
        import org.springframework.security.core.userdetails.UserDetails;
        import org.springframework.security.core.userdetails.UserDetailsService;
        import org.springframework.security.core.userdetails.UsernameNotFoundException;
        import org.springframework.security.crypto.password.PasswordEncoder;
        import org.springframework.stereotype.Service;
        import org.springframework.util.CollectionUtils;
    
        import java.util.ArrayList;
        import java.util.List;
    
        /**
        * 
        * 一般需要重写,自定义用户详情实现类,需要实现UserDetailsService接口,重写loadUserByUsername方法,
        * 通过用户名查找用户信息,并返回用户详情信息,包括用户名,加密后的密码,和权限集合
        *
        */
        @Slf4j
        @Service
        public class NbspUserDetailsService implements UserDetailsService {
    
            @Autowired
            private PasswordEncoder passwordEncoder;
    
            @Autowired
            private UserService userService;
    
            @Autowired
            private PermissionService permissionService;
    
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                SysUser sysUser = userService.getByUsername(username);
                if (null == sysUser) {
                    log.warn("用户{}不存在", username);
                    throw new UsernameNotFoundException(username);
                }
                List<SysPermission> permissionList = permissionService.findByUserId(sysUser.getId());
                List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
                if (!CollectionUtils.isEmpty(permissionList)) {
                    for (SysPermission sysPermission : permissionList) {
                        authorityList.add(new SimpleGrantedAuthority(sysPermission.getCode()));
                    }
                }
    
                MyUser myUser = new MyUser(sysUser.getUsername(), passwordEncoder.encode(sysUser.getPassword()), authorityList);
    
                log.info("登录成功!用户: {}", JSON.toJSONString(myUser));
    
                return myUser;
    
                //返回默认用户详情信息
                //return new org.springframework.security.core.userdetails.User(username,password, authorities);
    
            }
        }
    
    
    
    
    1. 重写User类
    
        import lombok.Data;
        import org.springframework.security.core.GrantedAuthority;
        import org.springframework.security.core.userdetails.User;
    
        import java.util.Collection;
    
        /**
        * 大部分时候直接用User即可不必扩展
        * 
        */
        @Data
        public class MyUser extends User {
    
            private Integer departmentId;   //  举个例子,部门ID
    
            private String mobile;  //  举个例子,假设我们想增加一个字段,这里我们增加一个mobile表示手机号
    
            public MyUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
                super(username, password, authorities);
            }
    
            public MyUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
                super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
            }
        }
    
    
令牌访问端点配置
  • 基本概念

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

  • 配置授权类型(Grant Types)

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

    authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。

    **userDetailsService:**如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现,或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对象),当你设置了这个之后,那么 “refresh_token” 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。

    **authorizationCodeServices:**这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 “authorization_code” 授权码类型模式。

    implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。

    **tokenGranter:当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,**即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。

  • 配置授权端点的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保护起来只供授权用户访问。

  • 在AuthorizationServerConfig(认证服务器配置类)配置令牌访问端点

    
      @Autowired
      private AuthorizationCodeServices authorizationCodeServices;
    
      @Autowired
      private AuthenticationManager authenticationManager;
    
      @Override
      public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
        .authenticationManager(authenticationManager)//支持 password 模式
        .authorizationCodeServices(authorizationCodeServices)//支持 authorization_code 模式
        .tokenServices(tokenService())//定义令牌服务,见上文内存管理令牌定义
        .allowedTokenEndpointRequestMethods(HttpMethod.POST);
      }
    
      @Bean
      public AuthorizationCodeServices authorizationCodeServices() { //设置授权码模式的授权码如何存取,暂时采用内存方式
    
        return new InMemoryAuthorizationCodeServices();
      }
    
    
令牌端点的安全约束
  • 基本概念

    AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束

  • 在AuthorizationServerConfig(认证服务器配置类)中配置令牌端点安全约束


  @Override
  public void configure(AuthorizationServerSecurityConfigurer security){
    
    security
    .tokenKeyAccess("permitAll()")//tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个endpoint完全公开。isAuthenticated()-允许已授权用户访问获取 token 接口
    .checkTokenAccess("permitAll()") //这里checkToken这个endpoint完全公开。isAuthenticated()-允许已授权用户访问 checkToken 接口
    .allowFormAuthenticationForClients();//允许客户端访问 OAuth2 授权接口,否则请求 token 会返回 401。(允许表单认证)
  }


配置总结
  • 授权服务配置分成三大块

    1. 完成认证,它首先得知道客户端信息从哪儿读取,因此要进行客户端详情配置

    2. 颁发token,那必须得定义token的相关endpoint,以及token如何存取,以及客户端支持哪些类型的token,因此要进行配置令牌(token)的访问端点和令牌服务(token services)

    3. 暴露除了一些endpoint,那对这些endpoint可以定义一些安全上的约束等,因此要进行令牌端点的安全约束配置

WebSecurityConfigurerAdapter(web安全配置)

新增安全配置类
  • 参考链接

    https://stackoverflow.com/questions/29721098/enableglobalmethodsecurity-vs-enablewebsecurity

  • 在Config包下创建WebSecurityConfig

    
      @Configuration
      //@EnableWebSecurity
      @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
      public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
          @Bean
          public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
          }
    
          @Bean
          public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
          }
    
          //安全拦截机制(最重要)
          @Override
          protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/r/r1").hasAnyAuthority("p1")
            .antMatchers("/login*").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin();
          }
      }
    
    
    
配置详解
@EnableGlobalMethodSecurity详解
  • 注解作用

    Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限

  • 注解说明

    1. @EnableGlobalMethodSecurity(securedEnabled=true)

      开启@Secured 注解过滤权限

    2. @EnableGlobalMethodSecurity(jsr250Enabled=true)

      开启@RolesAllowed 注解过滤权限

    3. @EnableGlobalMethodSecurity(prePostEnabled=true)

      使用表达式时间方法级别的安全性 4个注解可用

      (1) @PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问
      (2) @PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
      (3) @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
      (4) @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值

资源服务器配置

ResourceServerConfigurerAdapter(资源配置)

新增资源服务配置类
  • 参考链接

  • 在Config包下创建ResouceServerConfig

    
    
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.http.SessionCreationPolicy;
      import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
      import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
      import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
      import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
      import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
    
      @Configuration
      @EnableResourceServer
      @EnableGlobalMethodSecurity(prePostEnabled = true)
      public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
    
          public static final String RESOURCE_ID = "res1";
    
          @Override
          public void configure(ResourceServerSecurityConfigurer resources) {
    
            resources.resourceId(RESOURCE_ID)
            .tokenServices(tokenService())
            .stateless(true);
          }
    
          @Override
          public void configure(HttpSecurity http) throws Exception {
    
            http
            .authorizeRequests()
            .antMatchers("/**").access("#oauth2.hasScope('all')")
            .and().csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
          }
    
          //资源服务令牌解析服务
          @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;
          }
    
         
      }
    
    
    
配置详解
  • 配置流程

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

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

ResourceServerSecurityConfigurer配置
  • 配置说明

    1. tokenServices

    ResourceServerTokenServices类的实例,用来实现令牌服务

    1. tokenStore

    TokenStore类的实例,指定令牌如何访问,与tokenServices配置可选

    1. resourceId

    资源服务ID,该属性可选,但是推荐设置并在授权服务中进行验证

    1. 其他

    其他拓展属性如tokenExtractor令牌提取器用来提取请求中的令牌

HttpSecurity配置
  • 配置说明

    1. 配置受保护资源路径

    请求匹配器用来设置需要进行保护的资源路径,默认情况下是保护资源服务的全部路径

    1. 配置访问规则

    通过http.authorizeRequests()来设置受保护资源的访问规则

    1. 其他自定义权限保护规则

    其他自定义权限保护规则通过HttpSecurity进行配置

验证token
  • 简介

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

  • 令牌解析方法

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

    使用授权服务器的/oauth/check_token端点你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问,这在授权服务配置中已经提到了,如下是我们在授权服务中配置的/oauth/check_token和/oauth/token_key两个端点

    
    
      @Override
      public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
          security
                .tokenKeyAccess("permitAll()")// /oauth/token_key 安全配置
                .checkTokenAccess("permitAll()") // /oauth/check_token 安全配置
      }
    
    
    
    

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

    
    
      //资源服务令牌解析服务
      @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;
      }
    
      @Override
      public void configure(ResourceServerSecurityConfigurer resources) {
    
        resources.resourceId(RESOURCE_ID)
        .tokenServices(tokenService())
        .stateless(true);
      }
    
    
    
编写资源
  • 创建订单资源访问类

    
      @RestController
      public class OrderController {
    
        @GetMapping(value = "/r1")
        @PreAuthorize("hasAnyAuthority('p1')")
        public String r1(){
          return "访问资源1";
        }
      }
    
    
    
    
    
    
添加安全访问控制
  • 创建安全访问配置类

    
      @Configuration
      @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
      public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
          //安全拦截机制(最重要)
          @Override
          protected void configure(HttpSecurity http) throws Exception {
    
              http.csrf().disable()
              .authorizeRequests()
              // .antMatchers("/r/r1").hasAuthority("p2")
              // .antMatchers("/r/r2").hasAuthority("p2")
              .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
              .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
              ;
          }
      }
    
    
测试
  • 密码方式

    1. 申请令牌
      密码模式测试_申请令牌

    2. 请求资源

    按照oauth2.0协议要求,请求资源需要携带token,如下

    token参数名称为:Authorization
    token参数值为:Bearer token值
    

    密码模式测试_请求资源

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值