欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring Security 解析(五) —— Spring Security Oauth2 开发

程序员文章站 2022-07-02 12:57:51
Spring Security 解析(五) —— Spring Security Oauth2 开发   在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决定先把Spring Security 、Spring Security Oaut ......

spring security 解析(五) —— spring security oauth2 开发

  在学习spring cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决定先把spring security 、spring security oauth2 等权限、认证相关的内容、原理及设计学习并整理一遍。本系列文章就是在学习的过程中加强印象和理解所撰写的,如有侵权请告知。

项目环境:

  • jdk1.8
  • spring boot 2.x
  • spring security 5.x

  前面几篇文章基本上已经把security的核心内容讲得差不多了,那么从本篇文章我们开始接触spring security oauth2 相关的内容,这其中包括后面的 spring social (其本质也是基于oauth2)。有一点要说明的是,我们是在原有的spring-security 项目上继续开发,存在一些必要的重构,但不影响前面security的功能。

一、 oauth2 与 spring security oauth2

oauth2

  有关于oauth2 的 资料,网上很多,但最值得推荐的还是 阮一峰老师的 理解oauth 2.0,在这里我就不重复描述oauth2了,但我还是有必要提下其中的重要的点:
Spring Security 解析(五) —— Spring Security Oauth2 开发

  图片中展示的流程是授权码模式的流程,其中最核心正如图片展示的一样:

  • 资源所有者(resource owner): 可以理解为用户
  • 服务提供商(provider): 分为认证服务器(authorization server)和 资源服务器(resource server)。怎么理解认证、资源服务器呢,很简单,比如我们手机某个app通过qq来登陆,在我们跳转到一个qq授权的页面以及登陆的操作都是在认证服务器上做的,后面我们登陆成功后能够看到我们的头像等信息,这些信息就是登陆成功后去资源服务器获取到的。
  • 第三方应用: 可以理解就是我们正在使用的某个app,用户通过这个app发起qq授权登陆。

spring security oauth2

   spring 官方出品的 一个实现 oauth2 协议的技术框架,后面的系列文章其实都是在解析它是如何实现oauth2的。如果各位有时间的话可以看下spring security oauth2 官方文档,我的文章分析也是依靠文档来的。

  最后,我个人总结这2者的区别:

  • oauth2 不是一门技术框架, 而是一个协议,它仅仅只是制定好了协议的标准设计思想,你可以用java实现,也可以用其他任何语言实现。
  • spring security oauth2 是一门技术框架,它是依据oauth2协议开发出来的。

一、 spring security oauth2 开发

   在微服务开发的过程中,一般会把授权服务器和资源服务器拆分成2个应用程序,所以本项目采用这种设计结构,不过在开发前,我们需要做一步重要得步骤,就是项目重构。

一、 项目重构

   为什么要重构呢?因为我们是将授权和资源2个服务器拆分了,之前开发的一些配置和功能是可以在2个服务器共用的,所以我们可以讲公共的配置和功能可以单独罗列出来,以及后面我们开发spring security oauth2 得一些公共配置(比如token相关配置)。 我们新建 security-core 子模块,将之前开发的短信等功能代码迁移到这个子模块中。最终得到以下项目结构:

Spring Security 解析(五) —— Spring Security Oauth2 开发

   迁移完成后,原先项目模块更换模块名为 security-oauth2-authorization ,即 授权服务应用,并且 在pom.xml 中引用 security-core 依赖,迁移后该模块的项目结构如下:

Spring Security 解析(五) —— Spring Security Oauth2 开发

   我们可以发现,迁移后的项目内部只有 security相关的配置代码和测试接口,以及静态的html。

二、 授权服务器开发

一、maven依赖

  在 security-core 模块的pom.xml 中引用 以下依赖:

        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-security</artifactid>
        </dependency>
        <dependency>
            <groupid>org.springframework.security</groupid>
            <artifactid>spring-security-jwt</artifactid>
        </dependency>
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-data-redis</artifactid>
        </dependency>
        <!-- 不是starter,手动配置 -->
        <dependency>
            <groupid>org.springframework.security.oauth</groupid>
            <artifactid>spring-security-oauth2</artifactid>
            <!--请注意下 spring-authorization-oauth2 的版本 务必高于 2.3.2.release,这是官方的一个bug:
            java.lang.nosuchmethoderror: org.springframework.data.redis.connection.redisconnection.set([b[b)v
            要求必须大于2.3.5 版本,官方解释:https://github.com/bug9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth2/open
            -->
            <version>2.3.5.release</version>
        </dependency>

  这里新增 spring-security-oauth2 依赖,一个针对token 的存储策略分别引用了 redis 和 jwt 依赖。

注意这里 spring-security-oauth2 版本必须高于 2.3.5 版本 ,否自使用 redis 存储token 策略会报出:
org.springframework.data.redis.connection.redisconnection.set([b[b)v 异常

   security-oauth2-authorization 模块的 pom 引用 security-core :
```

mysql
mysql-connector-java


org.springframework.boot
spring-boot-starter-jdbc



com.zhc
security-core
0.0.1-snapshot


org.springframework.security.oauth
spring-security-oauth2


    <!-- 不是starter,手动配置 -->
    <dependency>
        <groupid>org.springframework.security.oauth</groupid>
        <artifactid>spring-security-oauth2</artifactid>
        <!--请注意下 spring-authorization-oauth2 的版本 务必高于 2.3.2.release,这是官方的一个bug:
        java.lang.nosuchmethoderror: org.springframework.data.redis.connection.redisconnection.set([b[b)v
        要求必须大于2.3.5 版本,官方解释:https://github.com/bug9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth2/open
        -->
        <version>2.3.5.release</version>
    </dependency>

##### 二、配置授权认证 @enableauthorizationserver

&emsp;&emsp;在spring security oauth2 中有一个 **@enableauthorizationserver** ,只要我们 在项目中引用到了这个注解,那么一个基本的授权服务就配置好了,但是实际项目中并不这样做。比如要配置redis和jwt 2种存储token策略共存,通过继承 **authorizationserverconfigureradapter** 来实现。 下列代码是我的一个个性化配置:

@configuration
@enableauthorizationserver
public class authorizationserverconfiguration extends authorizationserverconfigureradapter {

@resource
private authenticationmanager authenticationmanager;  // 1、引用 authenticationmanager 支持 password 授权模式

private final map<string, tokenstore> tokenstoremap; // 2、获取到系统所有的 token存储策略对象 tokenstore ,这里我配置了 redistokenstore 和 jwttokenstore

@autowired(required = false)
private accesstokenconverter jwtaccesstokenconverter; // 3、 jwt token的增强器

/**
 *  4、由于存储策略时根据配置指定的,当使用redis策略时,tokenenhancerchain 是没有被注入的,所以这里设置成 required = false
  */
@autowired(required = false)
private tokenenhancerchain tokenenhancerchain; // 5、token的增强器链

@autowired
private passwordencoder passwordencoder;

@value("${spring.security.oauth2.storetype}")
private string storetype = "jwt";  // 6、通过获取配置来判断当前使用哪种存储策略,默认jwt

@autowired
public authorizationserverconfiguration(map<string, tokenstore> tokenstoremap) {
    this.tokenstoremap = tokenstoremap;
}

@override
public void configure(clientdetailsserviceconfigurer clients) throws exception {
    //7、 配置一个客户端,支持客户端模式、密码模式和授权码模式
    clients.inmemory()  // 采用内存方式。也可以采用 数据库方式
            .withclient("client1") // clientid 
            .authorizedgranttypes("client_credentials", "password", "authorization_code", "refresh_token") // 授权模式
            .scopes("read") // 权限范围 
            .redirecturis("http://localhost:8091/login") // 授权码模式返回code码的回调地址
            // 自动授权,无需人工手动点击 approve
            .autoapprove(true)  
            .secret(passwordencoder.encode("123456"))
            .and()
            .withclient("client2")
            .authorizedgranttypes("client_credentials", "password", "authorization_code", "refresh_token")
            .scopes("read")
            .redirecturis("http://localhost:8092/login")
            .autoapprove(true)
            .secret(passwordencoder.encode("123456"));
}

@override
public void configure(authorizationserverendpointsconfigurer endpoints) throws exception {
    // 设置token存储方式,这里提供redis和jwt
    endpoints
            .tokenstore(tokenstoremap.get(storetype + "tokenstore"))
            .authenticationmanager(authenticationmanager);
    if ("jwt".equalsignorecase(storetype)) {
        endpoints.accesstokenconverter(jwtaccesstokenconverter)
                .tokenenhancer(tokenenhancerchain);
    }
}

@override
public void configure(authorizationserversecurityconfigurer oauthserver) throws exception {
    oauthserver// 开启/oauth/token_key验证端口无权限访问
            .tokenkeyaccess("permitall()")
            // 开启/oauth/check_token验证端口认证权限访问
            .checktokenaccess("isauthenticated()")
            //允许表单认证    请求/oauth/token的,如果配置支持allowformauthenticationforclients的,且url中有client_id和client_secret的会走clientcredentialstokenendpointfilter
            .allowformauthenticationforclients();
}

}
```
  这里的配置分3部分:

  • clientdetailsserviceconfigurer: 配置客户端信息。 可以采用内存方式、jdbc方式等等,我们还可以像userdetailsservice一样定制clientdetailsservice。
  • authorizationserverendpointsconfigurer : 配置 授权节点信息。这里主要配置 tokenstore
  • authorizationserversecurityconfigurer: 授权节点的安全配置。 这里开启/oauth/token_key验证端口无权限访问(单点客户端启动时会调用该接口获取jwt的key,所以这里设置成无权限访问)以及 /oauth/token配置支持allowformauthenticationforclients(url中有client_id和client_secret的会走clientcredentialstokenendpointfilter)
三、配置 tokenstore

  在配置授权认证时,依赖注入了 tokenstore 、jwtaccesstokenconverter、tokenenhancerchain,但这些对象是如何配置并注入到spring 容器的呢?且看下面代码:

@configuration
public class tokenstoreconfig {
   /**
    * redis连接工厂
    */
   @resource
   private redisconnectionfactory redisconnectionfactory;

   /**
    * 使用redistokenstore存储token
    *
    * @return tokenstore
    */
   @bean
   @conditionalonproperty(prefix = "spring.security.oauth2", name = "storetype", havingvalue = "redis")
   public tokenstore redistokenstore() {
       return new redistokenstore(redisconnectionfactory);
   }

   @bean
   passwordencoder passwordencoder(){
       return passwordencoderfactories.createdelegatingpasswordencoder();
   }

   /**
    * jwt的配置
    *
    * 使用jwt时的配置,默认生效
    */
   @configuration
   @conditionalonproperty(prefix = "spring.security.oauth2", name = "storetype", havingvalue = "jwt", matchifmissing = true)
   public static class jwttokenconfig {

       @resource
       private securityproperties securityproperties;
       /**
        * 使用jwttokenstore存储token
        * 这里通过 matchifmissing = true 设置默认使用 jwttokenstore
        *
        * @return tokenstore
        */
       @bean
       public tokenstore jwttokenstore() {
           return new jwttokenstore(jwtaccesstokenconverter());
       }

       /**
        * 用于生成jwt
        *
        * @return jwtaccesstokenconverter
        */
       @bean
       public jwtaccesstokenconverter jwtaccesstokenconverter() {
           jwtaccesstokenconverter accesstokenconverter = new jwtaccesstokenconverter();
           //生成签名的key,这里使用对称加密
           accesstokenconverter.setsigningkey(securityproperties.getoauth2().getjwtsigningkey());
           return accesstokenconverter;
       }

       /**
        * 用于扩展jwt
        *
        * @return tokenenhancer
        */
       @bean
       @conditionalonmissingbean(name = "jwttokenenhancer")
       public tokenenhancer jwttokenenhancer() {
           return new jwttokenenhance();
       }

       /**
        * 自定义token扩展链
        *
        * @return tokenenhancerchain
        */
       @bean
       public tokenenhancerchain tokenenhancerchain() {
           tokenenhancerchain tokenenhancerchain = new tokenenhancerchain();
           tokenenhancerchain.settokenenhancers(arrays.aslist(new jwttokenenhance(), jwtaccesstokenconverter()));
           return tokenenhancerchain;
       }
   }
}

  注意: 该配置类适用于均适用于授权和资源服务器,所以该配置类是放在 security-core 模块

四、新增 application.yml 配置
spring:
  redis:
    host: 127.0.0.1
    port: 6379
  security:
    oauth2:
      storetype: redis
      jwt:
        signingkey: oauth2
五、启动测试

1、 授权码模式:grant_type=authorization_code

  (1)浏览器*问/oauth/authorize 获取授权码:
http://localhost:9090/oauth/authorize?response_type=code&client_id=client1&scope=read&state=test&redirect_uri=http://localhost:8091/login
如果是没有登陆过,则跳转到登陆界面(这里账户密码登陆和短信验证码登陆均可),成功跳转到 我们设置的回调地址(我们这个是单点登陆客户端),我们可以从浏览器地址栏看到 code码

  (2)postman请求/oauth/token 获取token:
localhost:9090/oauth/token?grant_type=authorization_code&code=i4ge7b&redirect_uri=http://localhost:8091/login

Spring Security 解析(五) —— Spring Security Oauth2 开发

   注意在 authorization 填写 client信息,下面是 curl 请求:

curl -x post \
 'http://localhost:9090/oauth/token?grant_type=authorization_code&code=q38nnc&redirect_uri=http://localhost:8091/login' \
 -h 'accept: */*' \
 -h 'accept-encoding: gzip, deflate' \
 -h 'authorization: basic y2xpzw50mtoxmjm0nty=' \
 -h 'cache-control: no-cache' \
 -h 'connection: keep-alive' \
 -h 'content-length: ' \
 -h 'content-type: application/x-www-form-urlencoded' \
 -h 'cookie: remember-me=; jsessionid=f6f6de2968113dde4613091e998d77f4' \
 -h 'host: localhost:9090' \
 -h 'postman-token: f37b9921-4efe-44ad-9884-f14e9bd74bce,3c80ffe3-9e1c-4222-a2e1-9694bff3510a' \
 -h 'user-agent: postmanruntime/7.16.3' \
 -h 'cache-control: no-cache'

   响应报文:
{ "access_token": "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyj1c2vyx25hbwuioii5mdaxiiwic2nvcguiolsicmvhzcjdlcjlehaioje1njg2ndy0nzksimf1dghvcml0awvzijpbimfkbwluil0simp0asi6imy5zdbhnmzhltaxowytngu5ny1immi4lwi1otnlnjbizjk0niisimnsawvudf9pzci6imnsawvuddeilcj1c2vybmftzsi6ijkwmdeifq.4bjg_lggzt2rjr0vzxtsmsk71eiudgvrqsl_opsg8va", "token_type": "bearer", "refresh_token": "eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyj1c2vyx25hbwuioii5mdaxiiwic2nvcguiolsicmvhzcjdlcjhdgkioijmowqwytzmys0wmtlmltrlotctyjjioc1intkzztywymy5ndyilcjlehaioje1nzexotuynzksimf1dghvcml0awvzijpbimfkbwluil0simp0asi6iju1ntrmyjdkltbhzgitngi4ms1iognllwiwotk2njm1oti4mcisimnsawvudf9pzci6imnsawvuddeilcj1c2vybmftzsi6ijkwmdeifq.ta1frc46xrkngl3y_n72rm0nz5qcewh3zjfmr7ckhq4", "expires_in": 43199, "scope": "read", "username": "9001", "jti": "f9d0a6fa-019f-4e97-b2b8-b593e60bf946" }

2、 密码模式: grant_type=password

postman:
http://localhost:9090/oauth/token?username=user&password=123456&grant_type=password&scope=read&client_id=client1&client_secret=123456

curl:

curl -x post \
  'http://localhost:9090/oauth/token?username=user&password=123456&grant_type=password&scope=read&client_id=client1&client_secret=123456' \
  -h 'accept: */*' \
  -h 'accept-encoding: gzip, deflate' \
  -h 'cache-control: no-cache' \
  -h 'connection: keep-alive' \
  -h 'content-length: ' \
  -h 'cookie: remember-me=; jsessionid=f6f6de2968113dde4613091e998d77f4' \
  -h 'host: localhost:9090' \
  -h 'postman-token: f41c7e67-1127-4b65-87ed-21b3e00cfae3,08168e2e-1818-42f8-b4c4-cafd4aa0edc4' \
  -h 'user-agent: postmanruntime/7.16.3' \
  -h 'cache-control: no-cache'
  

3、 客户端模式 : grant_type=client_credentials

postman:

   localhost:9090/oauth/token?scope=read&grant_type=client_credentials

   注意在 authorization 填写 client信息,下面是 curl 请求:

curl:

curl -x post \
  'http://localhost:9090/oauth/token?scope=read&grant_type=client_credentials' \
  -h 'accept: */*' \
  -h 'accept-encoding: gzip, deflate' \
  -h 'authorization: basic y2xpzw50mtoxmjm0nty=' \
  -h 'cache-control: no-cache' \
  -h 'connection: keep-alive' \
  -h 'content-length: 35' \
  -h 'content-type: application/x-www-form-urlencoded' \
  -h 'cookie: remember-me=; jsessionid=f6f6de2968113dde4613091e998d77f4' \
  -h 'host: localhost:9090' \
  -h 'postman-token: a8d3b4a2-7aee-4f0d-8959-caa99a412012,f5e41385-b2b3-48d2-aa65-8b1d1c075cab' \
  -h 'user-agent: postmanruntime/7.16.3' \
  -h 'cache-control: no-cache' \
  -d 'username=zhoutaoo&password=password'

   授权服务开发,我们继续延用之前的security配置,包括 userdetailsservice及其登陆配置,在其基础上我们新增了授权配置,完成整个授权服务的搭建及测试。

三、 资源服务器开发

  由于资源服务器是个权限的应用程序,我们新建 security-oauth2-authentication 子模块作为资源服务器应用。

一、maven依赖

   security-oauth2-authentication 模块 pom 引用 security-core:

       <dependency>
           <groupid>com.zhc</groupid>
           <artifactid>security-core</artifactid>
           <version>0.0.1-snapshot</version>
           <exclusions>
               <exclusion>
                   <groupid>org.springframework.security.oauth</groupid>
                   <artifactid>spring-security-oauth2</artifactid>
               </exclusion>
           </exclusions>
       </dependency>

       <!-- 不是starter,手动配置 -->
       <dependency>
           <groupid>org.springframework.security.oauth</groupid>
           <artifactid>spring-security-oauth2</artifactid>
           <!--请注意下 spring-authorization-oauth2 的版本 务必高于 2.3.2.release,这是官方的一个bug:
           java.lang.nosuchmethoderror: org.springframework.data.redis.connection.redisconnection.set([b[b)v
           要求必须大于2.3.5 版本,官方解释:https://github.com/bug9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth2/open
           -->
           <version>2.3.5.release</version>
       </dependency>
二、配置授权服务 @enableresourceserver

   整个资源服务的配置主要分3个点:

  • @enableresourceserver 必须的,是整个资源服务器的基础
  • tokenstore 由于授权服务器采用了不同的tokenstore,所以我们解析token也得根据配置的存储策略来
  • httpsecurity 一般来说只要是资源服务器,其内部的接口均需要认证后才可访问,这里简单配置了以下。
@configuration
@enableresourceserver  // 1
public class resourceserverconfiguration extends resourceserverconfigureradapter {

   private final map<string,tokenstore> tokenstoremap;

   @value("${spring.security.oauth2.storetype}")
   private string storetype = "jwt";

   @autowired
   public resourceserverconfiguration(map<string, tokenstore> tokenstoremap) {
       this.tokenstoremap = tokenstoremap;
   }

   @override
   public void configure(resourceserversecurityconfigurer resources) {
       resources.tokenstore(tokenstoremap.get(storetype + "tokenstore"));  //  2
   }

   @override
   public void configure(httpsecurity http) throws exception {
       http
               .requestmatchers().anyrequest()
               .and()
               .anonymous()
               .and()
               .authorizerequests()
               //配置oauth2访问(测试接口)控制,必须认证过后才可以访问
               .antmatchers("/oauth2/**").authenticated();  // 3 
   }
}
三、配置 application.yml

   由于授权服务器采用不同tokenstore,所以这里也要引用 其 配置:

spring:
 redis:
   host: 127.0.0.1
   port: 6379
 security:
   oauth2:
     storetype: jwt
     jwt:
       signingkey: oauth2
四、测试接口
@restcontroller
@requestmapping("/oauth2")
@enableglobalmethodsecurity(prepostenabled = true)
@slf4j
public class testendpoints {

    @getmapping("/getuser")
    @preauthorize("hasanyauthority('user')")
    public string getuser() {
        authentication authentication = securitycontextholder.getcontext().getauthentication();
        return "user: " + authentication.getprincipal().tostring();
    }
}
五、启动测试

   我们将从授权服务器获取到的token进行访问测试接口:

postman:
http://localhost:8090/oauth2/getuser?access_token=eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzy29wzsi6wyjyzwfkil0simv4cci6mtu2ody0odeymcwianrpijoindq0nwq1zdktywzlmc00n2y1ltk0ngitzteynzi1nzi1m2m1iiwiy2xpzw50x2lkijoiy2xpzw50msisinvzzxjuyw1lijoiy2xpzw50msj9.ponicmjy2ex7jlxvagslen89eyfpypbw-l4f_cyk17k

curl:
```

curl -x get 'http://localhost:8090/oauth2/getuser?access_token=eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzy29wzsi6wyjyzwfkil0simv4cci6mtu2ody0odeymcwianrpijoindq0nwq1zdktywzlmc00n2y1ltk0ngitzteynzi1nzi1m2m1iiwiy2xpzw50x2lkijoiy2xpzw50msisinvzzxjuyw1lijoiy2xpzw50msj9.ponicmjy2ex7jlxvagslen89eyfpypbw-l4f_cyk17k' -h 'accept: /' -h 'accept-encoding: gzip, deflate' -h 'cache-control: no-cache' -h 'connection: keep-alive' -h 'cookie: remember-me=; jsessionid=f6f6de2968113dde4613091e998d77f4' -h 'host: localhost:8090' -h 'postman-token: 07ec53c7-9051-439b-9603-ef0fe93664fa,e4a5b46e-feb7-4bf8-ab53-0c33aa44f661' -h 'user-agent: postmanruntime/7.16.3' -h 'cache-control: no-cache'

```

四、 个人总结

   spring security oauth2 就是一套标准的oauth2实现,我们可以通过开发进一步的了解oauth2的,但整体上涉及到的技术还是很多的,比如redis、jwt等等。本文仅仅只是简单的演示spring security oauth2 demo,希望对你有帮助,如果你还对想深入解析下spring security oauth2,那么请继续关注我,后续会解析其原理。

   本文介绍spring security oauth2开发的代码可以访问代码仓库 ,项目的github 地址 : https://github.com/bug9/spring-security

         如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!