|
6 k7 G4 B* R: S6 k- x! }; `7 Y
OAuth是一个关于授权(authorization)的开放网络标准,用来授权第三方应用获取用户数据,是目前最流行的授权机制,它当前的版本是 2.0应用场景假如你正在“网站 A”上冲浪,看到一篇帖子表示非常喜欢,当你情不自禁的想要点赞时,它会提示你进行登录操作。 7 h: P8 w- ^+ q R: U& H1 ~, p2 v
打开登录页面你会发现,除了最简单的账户密码登录外,还为我们提供了微博、微信、QQ 等快捷登录方式。假设选择了快捷登录,它会提示我们扫码或者输入账号密码进行登录。
, m. N" W) |7 J$ |) H8 [ 登录成功之后便会将 QQ/微信的昵称和头像等信息回填到“网站 A”中,此时你就可以进行点赞操作了名词定义在详细讲解oauth2之前,我们先来了解一下它里边用到的名词定义吧:Client :客户端,它本身不会存储用户快捷登录的账号和密码,只是通过资源拥有者的授权去请求资源服务器的资源,即例子中的网站 A;。
! G& Z% [6 H* x5 D4 X Resource Owner :资源拥有者,通常是用户,即例子中拥有 QQ/微信账号的用户;Authorization Server :认证服务器,可以提供身份认证和用户授权的服务器,即给客户端颁发token 7 D6 r) @. K' u* k& r4 r
和校验token;Resource Server :资源服务器,存储用户资源的服务器,即例子中的 QQ/微信存储的用户信息;认证流程 , I3 C* F0 c+ [' W
如图是oauth2官网的认证流程图,我们来分析一下:A 客户端向资源拥有者发送授权申请;B 资源拥有者同意客户端的授权,返回授权码;C 客户端使用授权码向认证服务器申请令牌token;D 认证服务器对客户端进行身份校验,认证通过后发放令牌;
$ n2 N" d" S: e" q6 n" h1 F E 客户端拿着认证服务器颁发的令牌去资源服务器请求资源;F 资源服务器校验令牌的有效性,返回给客户端资源信息;为了大家更好的理解,我特地画了一张图:
; c5 i. I; t( \ 到这儿,相信大家对理论知识已经掌握的差不多了,接下来我们就进入实战训练吧实战在正式开始搭建项目之前我们先来做一些准备工作:要想使用oauth2的服务,我们得先创建几张表数据库oauth2相关的建表语句可以参考官方初始化 sql,也可以查看项目中的。
% M% ^7 P- p6 H7 [. @ init.sql文件,回复“oauth2”获取源码。 ( C j( g2 H, N
至于表结构,大家可以先大体了解下,其中字段的含义,在init.sql文件已经做了说明oauth_client_details:存储客户端的配置信息,操作该表的类主要是JdbcClientDetailsService.java。 / m% d0 a% N9 w2 {$ j! n/ @
;oauth_access_token:存储生成的令牌信息,操作该表的类主要是JdbcTokenStore.java;oauth_client_token:在客户端系统中存储从服务端获取的令牌数据,操作该表的类主要是
$ A2 i+ d+ V* {3 I0 I: a/ y JdbcClientDetailsService.java;oauth_code:存储授权码信息与认证信息,即只有grant_type为authorization_code时,该表才会有数据,操作该表的类主要是 / N! S8 B; |. |4 c" q8 b
JdbcAuthorizationCodeServices.java;oauth_approvals:存储用户的授权信息;oauth_refresh_token:存储刷新令牌的refresh_token
1 K: Q8 e C7 i2 S% N ,如果客户端的grant_type不支持refresh_token,那么不会用到这张表,操作该表的类主要是JdbcTokenStore;在oauth_client_details表中添加一条数据client_id:cheetah_one //客户端名称,必须唯一5 [1 @0 K! x5 z- b4 W
resource_ids:product_api //客户端所能访问的资源id集合,多个资源时用逗号(,)分隔
% m. O5 O- J& N; u! Q; n client_secret 2a$1
1 s$ A1 _' F- E* i$ X; b% [ 0$h/TmLPvXozJJHXDyJEN22ensJgaciomfpOc9js9OonwWIdAnRQeoi //客户端的访问密码7 S; }. i" C3 ~, W1 z
scope:read,write //客户端申请的权限范围,可选值包括 6 I. b; e4 D8 W/ t) r8 H" j
read,write,trust若有多个权限范围用逗号(,)分隔, k: O) r( \" o" ~7 o4 n, N& G8 Q+ H
authorized_grant_types:client_credentials,implicit,authorization_code,refresh_token,password //指定客户端支持的grant_type,可选值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔4 L3 D/ b# H; }% p, ?3 p! ~
web_server_redirect_uri:http:。
" w0 D# x. O- W6 E% Q //www.baidu.com //客户端的重定向URI,可为空, 当grant_type为authorization_code或implicit时, 在Oauth的流程中会使用并检查与注册时填写的redirect_uri是否一致
5 l- i) D; ?7 [8 p3 A4 y access_token_validity:
5 E7 W6 H+ q8 \, p" \/ S+ [( h 43200 //设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时)
- E3 q7 E ?* W0 c0 d) D" @' J% R6 [# i autoapprove:false //设置用户是否自动Approval操作, 默认值为
! f) {* Z0 J% g4 U false, 可选值包括 true,false, read,write数据库中对密码进行了加密处理,大家可以在此路径下自行生成 - \8 ^4 w8 M1 d3 y& |
用户角色相关的表也在init.sql文件中,表结构非常简单,大家自行查阅。我的初始化数据为
. q1 {9 N. k! F' M7 [" X1 Q4 n 图片依赖引入org.springframework.bootspring-boot-starter-web
0 r! G0 r G2 p% A' I5 l >org.springframework.cloudspring-cloud-starter-security , a5 ?$ ?# g7 ]1 h
org.springframework.cloudspring-cloud-starter-oauth2 ; Y2 X+ O! B: F: O
org.springframework.securityspring-security-jwt , U! t$ K( @) k
至于其它依赖,大家可以根据需要自行引入,不再赘述,回复“oauth2”获取源码资源服务配置文件对服务端口、应用名称、数据库、mybatis和日志进行了配置。 6 A. m3 H+ h P+ o3 Z& r5 e
写了一个简单的控制层代码,用来模拟资源访问 ! ^% z' ^% {0 ~& ?
接着创建配置类继承ResourceServerConfigurerAdapter并增加@EnableResourceServer注解开启资源服务,重写两个configure方法 7 _" l2 B+ J. X9 `1 ~, M
当然我们也可以配置忽略校验的url,在上边的public void configure(HttpSecurity http) throws Exception中进行配置 p+ i1 Z! |: i3 x! M$ e" M7 s
因为我们是需要进行校验的,所以我把对应的代码给注释掉了,大家可以回复“oauth2”下载源码自行查看然后将实现了UserDetails的SysUser和实现了GrantedAuthority的SysRole。
6 c M( Z+ P# _ 放到项目中,当请求发过来时,oauth2会帮我们自行校验认证服务配置文件对服务端口、应用名称、数据库、mybatis和日志进行了配置Security 配置还是和之前Security+JWT 组合拳的配置大同小异,不了解的可以先看下该文。
9 x5 o+ d* E8 M0 i B6 v- I; E+ I E ① 将继承了UserDetailsService的ISysUserService的实现类SysUserServiceImpl重写loadUserByUsername方法
( {( e% J6 b9 _+ t ② 继承WebSecurityConfigurerAdapter类,增加@EnableWebSecurity注解并重写方法 * F7 V( G5 w) O; i: W
AuthorizationServer 配置① 继承AuthorizationServerConfigurerAdapter类,增加@EnableAuthorizationServer注解开启认证服务② 依赖注入,注入 7 个实例 " @; z$ g; C" ~& x
Bean对象
' s$ X& t2 ~4 H* q t* x" Q: } ③ 重写方法进行配置 # Q" h9 G: ^! d E8 ]/ r2 Y; x3 W
其它关于用户表和权限表的代码可参考源码,回复“oauth2”获取源码模式授权码模式我们前边所讲的内容都是基于授权码模式,授权码模式被称为最安全的一种模式,它获取令牌的操作是在两个服务端进行的,极大的减小了令牌泄漏的风险。
{4 o5 L. d8 B4 j Z 启动两个服务,当我们再次请求127.0.0.1:9002/product/findAll接口时会提示以下错误{8 w5 E- O8 { h" _8 B( }4 K
"error": "unauthorized",4 e5 X' e, |7 g: X* @8 Y F, Y0 r
"error_description" 1 C$ I/ Z2 w1 l9 x6 R4 y5 J. e# H' q
: "Full authentication is required to access this resource"
- Z7 `* a; ^8 b } P: A8 v& I @& j7 S$ J; R
① 调用接口获取授权码发送127.0.0.1:9001/oauth/authorize?response_type=code&client_id=cheetah_one - i# o/ h4 r! _6 a
请求,前边的路径是固定形式的,response_type=code表示获取授权码,client_id=cheetah_one表示客户端的名称是我们数据库配置的数据。
1 V g% t% I" \$ b 该页面是oauth2的默认页面,输入用户的账户密码点击登录会提示我们进行授权,这是数据库oauth_client_details表我们设置autoapprove为false起到的效果。
8 n) `" ~/ c4 ? 选择Approve点击Authorize按钮,会发现我们设置的回调地址(oauth_client_details表中的web_server_redirect_uri)后边拼接了code值,该值就是授权码。
, \; n( g+ l: A; C# L 查看数据库发现oauth_approvals和oauth_code表已经存入数据了。拿着授权码去获取token ' _, Q! K9 ]+ A9 O0 _) }
获取到token之后oauth_access_token和oauth_refresh_token表中会存入数据以用于后边的认证而oauth_code表中的数据被清除了,这是因为code值是直接暴漏在网页链接上的,。 0 q! T1 P) L! o7 z# V5 `7 O# J- z2 B
oauth2为了防止他人拿到code非法请求而特意设置为仅用一次。拿着获取到的token去请求资源服务的接口,此时有两种请求方式 6 P' I! Q! w6 }- I9 h% T& ^& l. c
接下来我们再来看一下oauth2的其它模式。简化模式所谓简化模式是针对授权码模式进行的简化,它将授权码模式中获取授权码的步骤省略了,直接去请求获取token。
4 ?$ K# x# ?) c$ K. Y# k 流程:发送请求127.0.0.1:9001/oauth/authorize?response_type=token&client_id=cheetah_one跳转到登录页进行登录,response_type=token
& @! z: t; f9 `' `$ [ 表示获取token。输入账号密码登录之后会直接在浏览器返回token,我们就可以像授权码方式一样携带token去请求资源了。
6 t6 G7 Y9 M$ Y! @* K: A 图片该模式的弊端就是token直接暴漏在浏览器中,非常不安全,不建议使用。密码模式密码模式下,用户需要将账户和密码提供给客户端向认证服务器申请令牌,所以该种模式需要用户高度信任客户端。
4 K% ]5 m: x) g2 i, M 流程:请求如下 ' w. Y( v; V7 Z& D$ @
获取成功之后可以去访问资源了客户端模式客户端模式已经不太属于oauth2的范畴了,用户直接在客户端进行注册,然后客户端去认证服务器获取令牌时不需要携带用户信息,完全脱离了用户,也就不存在授权问题了
, w+ n" |+ M4 S c$ b 发送请求如下 2 D5 ]# p, M5 ]
获取成功之后可以去访问资源了。刷新 token
. L) z7 t, \4 D0 L ~* Z 权限校验除了我们在数据库中为客户端配置资源服务外,我们还可以动态的给用户分配接口的权限① 开启Security内置的动态配置在开启资源服务时给ResourceServerConfig类增加注解@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)。
9 m( U% B' r8 k$ h7 L ② 给接口增加权限
! o6 X/ o9 _1 \, }8 e# {+ | ③ 在用户登录时设置用户权限 5 ~8 r1 G; U5 @
然后测试会发现可以正常访问采坑包名问题当我在创建项目的时候,给product和server两个模块设置了不同的包名,导致发送请求获取资源时报错经过分析得知,在登录账号时会将用户的信息存储到oauth_access_token。 9 Y0 Q4 d$ n& v; m! z& J+ |
表的authentication中,在进行token校验时会根据token_id取出该字段进行反序列化,如果此时发现包名不一致便会导致解析token失败,因此请求资源失败解决思路两个项目的包名改为一致;。
2 n3 G6 n3 A% C+ P: f 可以将用户和权限的实体抽成单独的模块,供其它模块引用;loadUserByUsername方法中使用的用户实体类不需要继承UserDetailsService类,每次返回时用user类包装一下即可;数据库问题
" g/ F9 w) ^1 h' ^2 Q% ]$ }- a 当我在进行权限校验测试时,在设置权限时发现少打了一个单词,导致请求一直出错修改完成之后继续请求,仍提示权限不足于是我将数据库中oauth_refresh_token和oauth_access_token。 ( {' j- N, }$ }8 H
的数据清除,重新开始测试就可以了个人认为是生成token时发现数据库中token存在,故不刷新token,但进行校验时却用带有权限标识的token前去校验导致失败至于其它的小坑在这不再赘述,如果遇到问题,建议按照流程对比我的源码仔细检查,回复“oauth2”获取源码。
2 [! l2 H1 Q3 L# I2 d8 }% b0 y! e, i
5 Q8 I: ]1 _% s; b0 j& P
4 ?% R7 j6 `8 }( L9 j$ M3 C$ h- H6 J6 N$ e) C& E4 d
|