Spring Security 解析(五) —— Spring Security Oauth2 开发
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了,但我还是有必要提下其中的重要的点:
图片中展示的流程是授权码模式的流程,其中最核心正如图片展示的一样:
- 资源所有者(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 子模块,将之前开发的短信等功能代码迁移到这个子模块中。最终得到以下项目结构:
迁移完成后,原先项目模块更换模块名为 security-oauth2-authorization ,即 授权服务应用,并且 在pom.xml 中引用 security-core 依赖,迁移后该模块的项目结构如下:
我们可以发现,迁移后的项目内部只有 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 :
```
<!-- 不是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   在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
注意在 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、收藏、转发给予支持!
推荐阅读
-
JSP 开发之Spring Security详解
-
使用Spring Security OAuth2实现单点登录
-
Spring Security 解析(七) —— Spring Security Oauth2 源码解析
-
如何用Spring Security OAuth2 实现登录互踢,面试必学
-
Spring Security OAuth2 SSO
-
Spring Security登录验证流程源码解析
-
基于Spring Security的Oauth2授权实现方法
-
spring security oauth2的token续期
-
解析spring-security权限控制和校验的问题
-
带你详细了解Spring Security的注解方式开发