+ A* Q& Q+ C4 o2 C6 k
对于身份认证和用户授权,之前写过几篇关于Shiro和Security的文章从发送口令获取源码的反馈来看,大家还是比较认可的今天给大家带来一种新的授权方式:oauth2理论OAuth是一个关于授权(authorization ( W' V0 z) E" Z( z6 X8 v2 d: P
)的开放网络标准,用来授权第三方应用获取用户数据,是目前最流行的授权机制,它当前的版本是2.0应用场景假如你正在“网站A”上冲浪,看到一篇帖子表示非常喜欢,当你情不自禁的想要点赞时,它会提示你进行登录操作。
$ d s J: E0 e5 O 打开登录页面你会发现,除了最简单的账户密码登录外,还为我们提供了微博、微信、QQ等快捷登录方式。假设选择了快捷登录,它会提示我们扫码或者输入账号密码进行登录。
2 ]; P1 R: a% D: ~* P: ` 登录成功之后便会将QQ/微信的昵称和头像等信息回填到“网站A”中,此时你就可以进行点赞操作了名词定义在详细讲解oauth2之前,我们先来了解一下它里边用到的名词定义吧:Client:客户端,它本身不会存储用户快捷登录的账号和密码,只是通过资源拥有者的授权去请求资源服务器的资源,即例子中的网站A;。
/ q9 w. r8 n; V- L( ` Resource Owner:资源拥有者,通常是用户,即例子中拥有QQ/微信账号的用户;Authorization Server:认证服务器,可以提供身份认证和用户授权的服务器,即给客户端颁发token
F. y0 D$ u/ ?; ]4 w# } 和校验token;Resource Server:资源服务器,存储用户资源的服务器,即例子中的QQ/微信存储的用户信息;认证流程
: a+ q% C1 p0 G- s8 B+ }# ?" Y) t$ G2 S 如图是oauth2官网的认证流程图,我们来分析一下:A客户端向资源拥有者发送授权申请;B资源拥有者同意客户端的授权,返回授权码;C客户端使用授权码向认证服务器申请令牌token;D认证服务器对客户端进行身份校验,认证通过后发放令牌; 8 J8 s! C2 n* m8 D
E客户端拿着认证服务器颁发的令牌去资源服务器请求资源;F资源服务器校验令牌的有效性,返回给客户端资源信息;为了大家更好的理解,阿Q特地画了一张图:
8 f' Z6 r7 E$ K0 N* i* } 到这儿,相信大家对理论知识已经掌握的差不多了,接下来我们就进入实战训练吧实战在正式开始搭建项目之前我们先来做一些准备工作:要想使用oauth2的服务,我们得先创建几张表数据库oauth2相关的建表语句可以参考官方初始化sql,也可以查看阿Q项目中的。
/ J9 U/ X" n2 g3 `- Q( p init.sql文件,回复“oauth2”获取源码。
2 V8 d7 V" r2 ?6 F 至于表结构,大家可以先大体了解下,其中字段的含义,在init.sql文件中阿Q已经做了说明oauth_client_details:存储客户端的配置信息,操作该表的类主要是JdbcClientDetailsService.java。 7 p1 {7 y8 z% `+ Y% D. g7 i; l
;oauth_access_token:存储生成的令牌信息,操作该表的类主要是JdbcTokenStore.java;oauth_client_token:在客户端系统中存储从服务端获取的令牌数据,操作该表的类主要是 3 O& i6 c5 i! J9 g F4 c$ i" B
JdbcClientDetailsService.java;oauth_code:存储授权码信息与认证信息,即只有grant_type为authorization_code时,该表才会有数据,操作该表的类主要是 ' S8 Q8 A+ Y4 o7 R% O+ ]' R3 s
JdbcAuthorizationCodeServices.java;oauth_approvals:存储用户的授权信息;oauth_refresh_token:存储刷新令牌的refresh_token
p! a4 n. b0 s ,如果客户端的grant_type不支持refresh_token,那么不会用到这张表,操作该表的类主要是JdbcTokenStore;在oauth_client_details表中添加一条数据client_id:cheetah_one //客户端名称,必须唯一, U6 t! i% e7 |, Z7 e/ \
resource_ids:product_api //客户端所能访问的资源id集合,多个资源时用逗号(,)分隔9 c0 C( q- }7 u# x% J, A- A& \* v
client_secret 2a$1
) M! e0 J2 i) ` 0$h/TmLPvXozJJHXDyJEN22ensJgaciomfpOc9js9OonwWIdAnRQeoi //客户端的访问密码
p) z9 h% J, i& J, c scope:read,write //客户端申请的权限范围,可选值包括 3 r: N V7 f3 t' ]: c' ^( j
read,write,trust若有多个权限范围用逗号(,)分隔) ~& s; Y* D0 f A4 Q, H2 C
authorized_grant_types:client_credentials,implicit,authorization_code,refresh_token,password //指定客户端支持的grant_type,可选值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔
# c) h0 d/ W6 Z web_server_redirect_uri:http:。
( b2 J: t& a4 x- V( t //www.baidu.com //客户端的重定向URI,可为空, 当grant_type为authorization_code或implicit时, 在Oauth的流程中会使用并检查与注册时填写的redirect_uri是否一致+ M6 P2 l/ T. o
access_token_validity: , V# G( r( T0 H7 F& V( H$ i0 V
43200 //设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时)! l. n. _) z4 D6 {8 ]6 {
autoapprove:false //设置用户是否自动Approval操作, 默认值为 " M& J' w3 l2 Q) g: p3 Z+ x+ i9 U
false, 可选值包括 true,false, read,write数据库中对密码进行了加密处理,大家可以在此路径下自行生成
3 y* s' f* S5 ^! z# i 用户角色相关的表也在init.sql文件中,表结构非常简单,大家自行查阅。我的初始化数据为
- P& Z& D8 O* `, I( ^2 N 依赖引入org.springframework.bootspring-boot-starter-web
, B" J2 N4 |8 Q# X+ Z7 _4 V% w >org.springframework.cloudspring-cloud-starter-security
0 E' r" ?. y3 e) f# \! W; f2 R org.springframework.cloudspring-cloud-starter-oauth2
) v0 o* J& t t1 d$ B6 X. m org.springframework.securityspring-security-jwt
/ ]! c- h- A/ p0 D4 v$ E# r 至于其它依赖,大家可以根据需要自行引入,不再赘述,回复“oauth2”获取源码资源服务配置文件对服务端口、应用名称、数据库、mybatis和日志进行了配置。 * T5 R3 \" w. Q- `3 D, c0 [
写了一个简单的控制层代码,用来模拟资源访问@RestController@RequestMapping("/product")publicclassProductController{: }% n5 g# z6 C6 w
3 u6 o- E7 G, ~* z7 j$ m3 K @GetMapping(
2 D6 _+ i0 K7 I9 E "/findAll")public String findAll(){
! P* M$ c* A4 @. A; n' v* s1 c* ? return"产品列表查询成功";" b8 r) V5 }( W/ i" B
}
8 |( Y6 A f. u4 i0 z3 a- u }
5 k/ T' U4 _ h9 L 接着创建配置类继承ResourceServerConfigurerAdapter 7 D' P# h' G3 }# T
并增加@EnableResourceServer注解开启资源服务,重写两个configure方法/**9 T5 V' Q i3 z; U) B7 z
* 指定token的持久化策略
! M1 }% ]# @. a) ~4 z * InMemoryTokenStore 表示将token存储在内存中
$ e6 D6 j# o* |$ b! L * RedisTokenStore 表示将token存储在redis中/ D, U5 l: g$ [
* JdbcTokenStore 表示将token存储在数据库中
6 } H0 |) n3 Q/ T) ?, D * @return
: G- e; ^: M% r3 c* c, v: r+ \7 k& J */ " E% s+ z$ _, ~$ ~0 `: M
@Bean1 j+ r) V' {/ z; y& n/ Z/ I
public TokenStore jdbcTokenStore(){+ k7 ~7 s+ M% V: J) ?: i
returnnewJdbcTokenStore(dataSource);. M" `# v) m5 ^/ s0 \
}8 X( J9 `. P h
' u, s* @8 F, a' U9 Y /**; |+ T& F# a" L; d
* 指定当前资源的id和token的存储策略
& |# z A: o( Z5 ~$ f+ Z * @param resources
, c6 ]; v4 }4 N# Q, x * @throws Exception
7 `' f$ s7 p: G. S+ j' X, v# n4 t */ . q; q) p& T; Q! P& G
# V/ G$ | ?& c( S5 }
@Overridepublicvoidconfigure(ResourceServerSecurityConfigurer resources) throwsException {
& u; U5 g$ X, u% o3 _3 {7 ` //此处的id可以写在配置文件中,这里我们先写死
. B9 Q H, V% N resources.resourceId("product_api").tokenStore(jdbcTokenStore());
* X. L! d* @! K }
" P: Q" u& C0 M+ l& u! Q: }' v4 h2 ~4 g* R) J
! A' V1 _$ f7 c, e9 ]; l, `" o( W( o5 s
/**0 `) J* @9 f- G r- `4 D2 ~* |
* 设置请求权限和header处理. _" n5 x2 J& I; K
* @param http
% g1 [5 i2 h" c * @throws Exception3 Y9 i+ H z" |8 t3 f
*/ # y; [6 V+ c$ H1 E. j7 W- j) e3 s3 p
1 x) y' \4 D" M3 {' `( V% m/ d @Overridepublicvoidconfigure(HttpSecurity http) throwsException {2 ~0 l- Y, x% b1 b- R4 U
//固定写法http.authorizeRequests()
, s( O1 R9 B4 c3 H : t' P1 u9 d, U
//指定不同请求方式访问资源所需的权限,一般查询是read,其余都是write.antMatchers(HttpMethod.GET,"/**").access("#oauth2.hasScope(read)"
# p* @% o B Z: o. A6 V. ] )
* z' Y4 Q" O. r3 x& Y .antMatchers(HttpMethod.POST,"/**").access("#oauth2.hasScope(write)")
; D. D7 P( w& A9 K* _! E$ ` .antMatchers(HttpMethod.PATCH, 9 J+ k# m$ r* l9 B+ C
"/**").access("#oauth2.hasScope(write)")
( M8 R. a/ B- x/ v .antMatchers(HttpMethod.PUT,"/**").access("#oauth2.hasScope(write)"
5 r* [# t9 c: \# D' C )
- S1 P! D8 c" G .antMatchers(HttpMethod.DELETE,"/**").access("#oauth2.hasScope(write)"); S* }* \: } M; Q4 f2 n; Y# V
.and(), g: f8 _2 m. p$ u# G% V: y& I
.headers().addHeaderWriter : W) Z4 ~5 D X9 K# m9 _ O( M
((request,response) -> {2 `& S; M! ^' y3 y; N# d1 X
//域名不同或者子域名不一样并且是ajax请求就会出现跨域问题//允许跨域response.addHeader("Access-Control-Allow-Origin" , Q G: n) p, V* t5 ^4 K
,"*");
& d* l$ n4 M4 h! j1 F2 n //跨域中会出现预检请求,如果不能通过,则真正请求也不会发出//如果是跨域的预检请求,则原封不动向下传递请求头信息,否则预检请求会丢失请求头信息(主要是token信息)if(request.getMethod().equals( ! I+ I- F4 D0 O7 b: E9 m
"OPTIONS")){0 p6 S, F0 J4 P5 w7 i
response.setHeader("Access-Control-Allow-Methods",request.getHeader("Access-Control-Allow-Methods" + @4 j; c+ |( [/ A5 t
));
/ q; E1 Y( A! Y* t* U' M response.setHeader("Access-Control-Allow-Headers",request.getHeader("Access-Control-Allow-Headers" 2 E1 ~. ^; V8 w* e9 [$ G% Z$ ]
));5 ^+ |1 Y* S+ c: Y
}* z: F$ ?2 z2 Z( F6 R/ F2 B
});
% x& p4 H4 V1 ^2 S }
; V% X% e" B' ?2 S. E4 _* s' L8 g 当然我们也可以配置忽略校验的url,在上边的public void configure(HttpSecurity http) throws Exception中进行配置 ) Q4 |; d; i* N
ExpressionUrlAuthorizationConfigurer
- ^6 P9 R8 b+ E2 m3 q .ExpressionInterceptUrlRegistry config = http.requestMatchers().anyRequest()
9 L- F! g. ?3 H& m+ ]+ T0 `6 j . ( F$ m1 Q& V& Q; y) i4 s( C3 T0 o, U
and()
+ r6 v3 j; r% m/ w8 ^0 a9 o# x .authorizeRequests();
8 @; Q: n1 x+ g2 f properties.getUrls().forEach(e -> {) D% O, i$ p' Q
config.antMatchers(e).permitAll();2 J l$ o! _3 @4 M0 Q
});7 c0 a! b" d# V( M1 ~) \
/ C; x3 s: P* N4 } 因为我们是需要进行校验的,所以我把对应的代码给注释掉了,大家可以回复“oauth2”下载源码自行查看然后将实现了UserDetails的SysUser和实现了GrantedAuthority的SysRole。 6 a( U, n: P' [) ^) F+ c. e# n/ Z
放到项目中,当请求发过来时,oauth2会帮我们自行校验认证服务配置文件对服务端口、应用名称、数据库、mybatis和日志进行了配置Security配置还是和之前Security+JWT组合拳的配置大同小异,不了解的可以先看下该文。 * J& y; W4 m4 P8 V5 q3 f8 C5 F
①将继承了UserDetailsService的ISysUserService的实现类SysUserServiceImpl重写loadUserByUsername方法@Overridepublic UserDetails 8 Q2 k, w+ D2 N$ N9 i9 J
loadUserByUsername(String username)throws UsernameNotFoundException {
( {9 k" n2 ~1 b/ F* h1 a returnthis.baseMapper.selectOne( m$ F+ J0 [# O
new LambdaQueryWrapper().eq(SysUser::getUsername, username));
+ Y! A# M6 N% `( E- R# a: G }
8 H | F G5 a ②继承WebSecurityConfigurerAdapter
7 \# h0 C7 @/ Z2 e( O 类,增加@EnableWebSecurity注解并重写方法/**
% y3 [% x3 R- q! Z( O* ? * 指定认证对象的来源和加密方式 z# E- P* d) U: g- p2 j3 n
* @param auth
7 U1 U5 C _2 c5 ~+ G * @throws Exception# A( T! \( h; k) Z! D# Y7 N* p+ e
*/@Overridepublic
, `3 m! q( C8 a; P: } voidconfigure(AuthenticationManagerBuilder auth)throws Exception {! _; ]( r, _+ I% f' F0 F. Q
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
+ K, n2 H# Q! l( I+ } }6 L& x: A5 p% {5 I
7 ?; p9 H7 |/ f2 o
% r8 ~1 n5 p" Y0 U& O$ a /**
: B% F! J7 E, R * 安全拦截机制(最重要)
/ h0 ]7 l5 Y& x W& M. `8 I * @param httpSecurity9 Z* h- L: `5 F# _6 [' B& `, p
* @throws Exception3 }! C. |0 V& [& ~3 v# f' Z
*/@Overridepublicvoidconfigure(HttpSecurity httpSecurity) 0 K5 ~1 q7 G& @' k! Y! v
throws Exception {0 q2 C6 \ c# d( e5 j: A
httpSecurity
l1 Q' S! Q3 g* ~0 f- L1 C+ G //CSRF禁用,因为不使用session
( a# B0 C; D u! l3 y0 I .csrf().disable()
4 e7 \6 o# C' g6 P/ M9 z .authorizeRequests()8 y3 |% h1 G3 A( O8 B& c
+ @% ]5 b9 S9 u- p, N# x //登录接口和静态资源不需要认证9 @. s. L) X5 S% s$ p8 v& V! ]6 P! E1 P
.antMatchers("/login*","/css/*").permitAll(), F/ G2 Y: }/ G, [5 p/ J
//除上面的所有请求全部需要认证通过才能访问
$ P8 r! {" `7 `6 L; H# B7 U .anyRequest().authenticated()
+ I; Q8 |# A" I5 e2 T3 ?# t0 q9 B 4 r, S6 Z% I8 w% `+ D, [' w
//返回HttpSecurity以进行进一步的自定义,证明是一次新的配置的开始
$ R+ \4 L* b% G: X .and()- ^; [! M8 v2 \6 e3 e2 a
.formLogin()8 g r# r. u! M: x
//如果未指定此页面,则会跳转到默认页面// .loginPage("/login.html") % J8 }0 ]8 C+ \% e9 w
; y% w& N2 l$ m9 U& m
.loginProcessingUrl("/login")
# s: m7 ^1 t! N# m7 n5 R- k+ i .permitAll()- \0 J& p, G1 Q" g% n2 M" \, a+ ]1 Y( x
//认证失败处理类6 {5 ]! J" O# v8 F& F" J" m
.failureHandler(customAuthenticationFailureHandler);: K+ S: Q- t3 u- u3 B
}
1 K1 r n: g" z1 B& q% V# G. u, b6 m9 u! w
@- G" l+ {, w C /**
9 x& g/ E6 r! w1 L' p * AuthenticationManager 对象在OAuth2.0认证服务中要使用,提前放入IOC容器中 \( R% G; z L; e6 a/ K
* @return
0 E- w1 Y5 `5 J% u * @throws Exception
, s, W* l2 r" u0 C% I */@Override
, q) G. a* ]& T4 G2 i# F; A @Beanpublic AuthenticationManager authenticationManagerBean()throws Exception {
8 v3 p4 [: `& X/ m returnsuper.authenticationManagerBean();7 |, u! s! w6 E. Z( Y
}) r0 O7 L* i6 q: b" r% ^
6 R8 P* J6 ^$ m. `* i4 P
AuthorizationServer配置①继承AuthorizationServerConfigurerAdapter类,增加@EnableAuthorizationServer注解开启认证服务②依赖注入,注入7个实例 ; K! y/ v6 w% z9 s
Bean对象/**1 L1 I5 l; ^/ |+ `0 n" N
* 数据库连接池对象
& Z0 N+ k( D, q& t8 c( u# i */privatefinal DataSource dataSource;
2 b# W& e* K! r+ m( p4 @# P( _; X' G, J6 H4 O% N1 {# {
/**- R) X& i f& | F9 X8 u, R1 R& C
* 认证业务对象
( F' C" I% ?" X( Z5 t8 O% h, y. ^ */privatefinal ISysUserService userService;
+ r6 A8 V& ]5 b, h: u' p6 i2 T7 v8 n6 Q n {/ F! Z$ N2 i
. E6 B3 b0 b3 s* u /**$ ]$ ~0 K; h, j+ N
* 授权码模式专用对象
8 [- v, X$ g+ j */privatefinal AuthenticationManager authenticationManager;8 U9 q" a2 G' B) C# `
" z( W' t8 B; r: E4 s
/**, J K+ s6 g. X. z" _; h0 K8 r
* 客户端信息来源
8 `9 o5 E$ ~, O% P0 C K4 r: C0 P, N * @return
5 \# n) R- A0 N0 w: t3 T # b& R( D, {5 C
*/@Beanpublic JdbcClientDetailsService jdbcClientDetailsService(){
# G% e& s$ M! u2 j. B* g returnnew JdbcClientDetailsService(dataSource);+ K1 m- G, x1 @* K/ M7 q
}
% v( U8 W. j9 x: R7 n0 X4 n4 e: q/ Y
/ V) n; }& p2 F& l# |) O; T /**
1 \7 z0 Y: @" ~8 b * token保存策略
9 V. f c% f% e+ l7 V6 r * @return
% z1 b! Y* u- h; k3 P */@Beanpublic TokenStore tokenStore(){5 V- K/ O1 R! ?* } c; u4 l
returnnew JdbcTokenStore(dataSource);
3 |/ U9 M. H8 ^ }/ n" m: ^! x9 \) X, _
+ B% f8 Z9 W, u' K* a 2 O4 e& u; R& e- @1 }# y1 ^/ a
/**' _! E8 O% `' I; d4 G. e4 Q W4 t
* 授权信息保存策略- K: Y* A+ \4 m A% e
* @return
) w9 j% H- o- w" b# ~' c */@Beanpublic ApprovalStore approvalStore(){- e- V" U( W* v0 p2 V
returnnew JdbcApprovalStore(dataSource);! e! I1 i# `- u% M0 M
}8 D5 u# ?# f B# K
+ \( W8 B ?; t0 L2 O) b$ C' y( }
0 n# Z# i' v: T v: q /**
2 @, I$ A0 a' U& i5 P( ~ * 授权码模式数据来源
0 q+ o9 t+ n) B& m5 v" s W * @return
; Q% }3 p7 h# l' y8 e$ a) S */@Beanpublic AuthorizationCodeServices authorizationCodeServices(){: V& | I% q( G, d" R1 @) @
return + S3 [2 e. y- r% j- ^! H
new JdbcAuthorizationCodeServices(dataSource);! S# u* ~" `6 T7 ]; Z! [4 y
}
: q1 B! K. k# x ③重写方法进行配置/**
/ p; n5 X( R0 b9 m, m0 H * 用来配置客户端详情服务(ClientDetailsService)
' B2 ~3 M) p8 G& o: h+ C- O5 E * 客户端详情信息在这里进行初始化
5 [3 n5 {; h/ B5 @/ H * 指定客户端信息的数据库来源8 Z3 K: c7 s# o" j, Y' ~
* & f3 K; j6 X- n' X" f4 o A
@param clients% |- G3 f. v1 q; Y' T
* @throws Exception1 g. Z! E T/ A2 @0 Q* u9 d3 I
*/@Overridepublicvoidconfigure(ClientDetailsServiceConfigurer clients) 1 {$ a, H2 E' a( R) V. _
throws Exception { [& a- c0 J8 i/ ?/ M: K. y
clients.withClientDetails(jdbcClientDetailsService());# m' j4 ~" w4 x1 P: J
}1 V& X7 `- x; f" D" s
; H8 `. j/ z/ P8 f7 Y* J /**1 j: s0 J( m* {: B) K
* 检测 token 的策略
4 s3 J9 P4 h! g2 h) B *
1 e; W2 b p0 v- E0 K" U @param security9 N$ G% H5 K' N+ J; M: Q6 @' e
* @throws Exception, {; ]6 r: Y; `" ]4 Y& L
*/@Overridepublicvoidconfigure(AuthorizationServerSecurityConfigurer security) 7 h. l! s' V2 O& i: T
throws Exception {1 f/ ^8 i, P& z6 }+ m. `8 W
security
8 A6 N5 b! x" K8 w //允许客户端以form表单的方式将token传达给我们* _+ m" M3 f ^) U
.allowFormAuthenticationForClients()
/ K W& C3 N* y1 L7 r
3 }4 @' k! l5 W7 H //检验token必须需要认证
9 l( g% r0 ]% Y* c( r .checkTokenAccess("isAuthenticated()");
' g; t* R: m$ G3 r$ B }
$ R4 t. B: V2 F3 B/ l4 K7 ?) w# b9 n
; u3 A2 |, B) z) d$ p. C
/**$ }- m( N6 r3 `% P0 X
* OAuth2.0的主配置信息; W3 e: T: x* p& u2 _$ d
* @param endpoints- _8 }: e& m; Z% Z1 c$ N( K6 e
*
- q" b" l0 ]' A% w# f& \+ j @throws Exception0 l6 ?+ w4 \# f$ O* U
*/@Overridepublicvoidconfigure(AuthorizationServerEndpointsConfigurer endpoints)throws # f/ P' Y+ U5 |/ K8 q( G8 K
Exception {% g$ p$ G5 X& {0 g' A
endpoints
- Z3 s- {! X, t, @9 d, w) s+ m //刷新token时会验证当前用户是否已经通过认证# M6 E2 w$ q" ^' J. i
.userDetailsService(userService)
2 ~) y! T+ F. |5 |2 B" J3 X- b+ f .approvalStore(approvalStore())
# \% J& I7 l4 f: G .authenticationManager(authenticationManager): t( _- b; a, x# n0 n6 Q0 n
.authorizationCodeServices(authorizationCodeServices())) A: _1 p9 n# R) K/ V# o5 I8 |* q
.tokenStore(tokenStore());- Y8 k9 Y1 |6 q( n+ H/ ]: t8 o, d3 E
}6 P; x5 P* M) n& m- h. T' b- a
) o- U: C6 _0 m E# s" D, d% h 其它关于用户表和权限表的代码可参考源码,回复“oauth2”获取源码模式授权码模式我们前边所讲的内容都是基于授权码模式,授权码模式被称为最安全的一种模式,它获取令牌的操作是在两个服务端进行的,极大的减小了令牌泄漏的风险。 / a, K+ ~% Y, j) `% q, c8 C
启动两个服务,当我们再次请求127.0.0.1:9002/product/findAll接口时会提示以下错误{/ ~% M; t7 _6 P, Z3 T0 x2 c+ b
"error": "unauthorized",
4 x3 G* m! S7 k% \3 [1 Q( u6 e "error_description"
$ Q* E- b0 W: Z( g5 A8 { : "Full authentication is required to access this resource"$ r2 D; k. D, ]3 J9 K
}$ K# w6 _9 M1 k( O0 S
①调用接口获取授权码发送127.0.0.1:9001/oauth/authorize?response_type=code&client_id=cheetah_one 2 I% M1 g" m& N* \3 j
请求,前边的路径是固定形式的,response_type=code表示获取授权码,client_id=cheetah_one表示客户端的名称是我们数据库配置的数据。
" p6 m! z4 a9 s. E% q 该页面是oauth2的默认页面,输入用户的账户密码点击登录会提示我们进行授权,这是数据库oauth_client_details表我们设置autoapprove为false起到的效果。 * d- y5 C. z* l
选择Approve点击Authorize按钮,会发现我们设置的回调地址(oauth_client_details表中的web_server_redirect_uri)后边拼接了code值,该值就是授权码。
4 a4 d$ |# S$ b. U 查看数据库发现oauth_approvals和oauth_code表已经存入数据了。拿着授权码去获取token
) C* }0 y3 V& F 获取到token之后oauth_access_token和oauth_refresh_token表中会存入数据以用于后边的认证而oauth_code表中的数据被清除了,这是因为code值是直接暴漏在网页链接上的,。
2 H* \+ s6 f- |/ E/ a! n) O oauth2为了防止他人拿到code非法请求而特意设置为仅用一次。拿着获取到的token去请求资源服务的接口,此时有两种请求方式  5 j" ~7 M! H6 A7 S$ G2 n
接下来我们再来看一下oauth2的其它模式。简化模式所谓简化模式是针对授权码模式进行的简化,它将授权码模式中获取授权码的步骤省略了,直接去请求获取token。
4 C5 u2 u, w) u3 U 流程:发送请求127.0.0.1:9001/oauth/authorize?response_type=token&client_id=cheetah_one跳转到登录页进行登录,response_type=token # P1 N8 d: Q5 @; `1 |5 d
表示获取token。输入账号密码登录之后会直接在浏览器返回token,我们就可以像授权码方式一样携带token去请求资源了。 + l1 b4 Y! ?* [
该模式的弊端就是token直接暴漏在浏览器中,非常不安全,不建议使用。密码模式密码模式下,用户需要将账户和密码提供给客户端向认证服务器申请令牌,所以该种模式需要用户高度信任客户端。 $ u+ ~; @3 X4 L! `, h% p; G) m
流程:请求如下
3 q/ U1 c: f" ~1 Y4 G 获取成功之后可以去访问资源了客户端模式客户端模式已经不太属于oauth2的范畴了,用户直接在客户端进行注册,然后客户端去认证服务器获取令牌时不需要携带用户信息,完全脱离了用户,也就不存在授权问题了
. S- g4 o$ f. K- f4 ?+ ? 发送请求如下
b$ f2 y0 F) {" H5 a0 N 获取成功之后可以去访问资源了。刷新token - M+ t0 @- g. |5 k! B
权限校验除了我们在数据库中为客户端配置资源服务外,我们还可以动态的给用户分配接口的权限①开启Security内置的动态配置在开启资源服务时给ResourceServerConfig类增加注解@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)。 ) i* `* T& u& O
②给接口增加权限@GetMapping("/findAll")@Secured("ROLE_PRODUCT")public String findAll(){
2 t" q1 Z0 ], @; ]# t+ U& G return"产品列表查询成功";1 R1 a6 _) R* Y! g* D
}
1 ~8 [( W4 V2 d$ v) F- a9 S6 a 7 z/ y4 V# n/ w
③在用户登录时设置用户权限@Overridepublic UserDetails loadUserByUsername(String username)throws UsernameNotFoundException
4 v: p% n) n/ h, c5 `* F7 w; B {
3 E0 V+ ]% S8 V5 `; A SysUser sysUser = this.baseMapper.selectOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username));' J2 C: s5 X6 L' m' t3 p
sysUser.setRoleList(AuthorityUtils.commaSeparatedStringToAuthorityList( $ ?9 U& P+ {$ p4 P
"ROLE_PRODUCT"));& ^ T; s& I8 D2 q
return sysUser;" a9 Q) X) C6 T' ?( X5 g3 j, J
}" R' u( d! t9 ^% L
) U, ~$ @' X3 z4 k 然后测试会发现可以正常访问采坑包名问题当我在创建项目的时候,给product和server两个模块设置了不同的包名,导致发送请求获取资源时报错。 2 V) w" H9 f% I4 e
经过分析得知,在登录账号时会将用户的信息存储到oauth_access_token表的authentication中,在进行token校验时会根据token_id取出该字段进行反序列化,如果此时发现包名不一致便会导致解析
' ~2 K% e, V T" G token失败,因此请求资源失败解决思路两个项目的包名改为一致;可以将用户和权限的实体抽成单独的模块,供其它模块引用;loadUserByUsername方法中使用的用户实体类不需要继承UserDetailsService。
5 d, q$ X# X' ?$ ^% y& o2 A 类,每次返回时用user类包装一下即可;数据库问题当我在进行权限校验测试时,在设置权限时发现少打了一个单词,导致请求一直出错修改完成之后继续请求,仍提示权限不足于是我将数据库中oauth_refresh_token。
3 H0 g( a, B. P3 i- n3 U 和oauth_access_token的数据清除,重新开始测试就可以了个人认为是生成token时发现数据库中token存在,故不刷新token,但进行校验时却用带有权限标识的token前去校验导致失败至于其它的小坑在这不再赘述,如果遇到问题,建议按照流程对比我的源码仔细检查,回复“oauth2”获取源码。 . ]4 P$ _3 V3 N# o: S9 `
小结本文从原理、应用场景、认证流程出发,对oauth2进行了基本的讲解,并且手把手带大家完成了项目的搭建大家在对授权码模式、简化模式、密码模式、客户端模式进行测试的同时要将重点放到授权码模式上来源:阿Q说代码。
0 R& z# \/ w0 n) t) ?$ n7 }- T6 @
: u% t' B. i9 U; v# C! B: g E, i( L
& l) G2 V9 u% [/ b2 F0 }1 Q
' v/ a' @! T- J1 G' ]2 w3 g |