|
$ F1 u3 N2 |' q$ w! w! h- S 1用户认证需求分析1.1用户认证与授权截至目前,项目已经完成了在线学习功能,用户通过在线学习页面点播视频进行学习如何去记录学生的学习过程 呢?要想掌握学生的学习情况就需要知道用户的身份信息,记录哪个用户在什么时间学习什么课程;如果用户要购 买课程也需要知道用户的身份信息。
1 K$ r7 i$ q2 g3 m/ a 所以,去管理学生的学习过程最基本的要实现用户的身份认证什么是用户身份认证?用户身份认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问常见的用户身份认 证表现形式有:用户名密码登录,指纹打卡等方式。
. ]( B0 ^ T$ G, I9 @ 什么是用户授权?用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没 有权限的资源将无法访问,这个过程叫用户授权1.2单点登录需求本项目包括多个子项目,如:学习系统,教学管理中心、系统管理中心等,为了提高用户体验性需要实现用户只认 证一次便可以在多个拥有访问权限的系统中访问,这个功能叫做单点登录。
% a$ n6 _) h3 R9 ?! g- b5 F& J 引用百度百科:单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统下图是SSO的示意图,用户登录学成网一次即可访问多个系统。
; z, ~" j7 u; {7 t7 A4 _ a 1.3第三方认证需求作为互联网项目难免需要访问外部系统的资源,同样本系统也要访问第三方系统的资源接口,一个场景如下:一个微信用户没有在学成在线注册,本系统可以通过请求微信系统来验证该用户的身份,验证通过后该用户便可在 本系统学习,它的基本流程如下:
* ?0 X4 O- \5 A" g7 w 从上图可以看出,微信不属于本系统,本系统并没有存储微信用户的账号、密码等信息,本系统如果要获取该用户 的基本信息则需要首先通过微信的认证系统(微信认证)进行认证,微信认证通过后本系统便可获取该微信用户的 基本信息,从而在本系统将该微信用户的头像、昵称等信息显示出来,该用户便不用在本系统注册却可以直接学 习。 4 ?0 `1 Q7 ^. n% z" p
什么是第三方认证(跨平台认证)?当需要访问第三方系统的资源时需要首先通过第三方系统的认证(例如:微信认证),由第三方系统对用户认证通 过,并授权资源的访问权限。 & e4 u7 m) L D5 F
2用户认证技术方案2.1单点登录技术方案分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如:MySQL、Redis,考虑性能要求,通常存储在Redis中,如下图
8 w) G) K/ I5 O 单点登录的特点是:1、认证系统为独立的系统2、各子系统通过Http或其它协议与认证系统通信,完成用户认证3、用户身份信息存储在Redis集群Java中有很多用户认证的框架都可以实现单点登录:1、Apache Shiro.。
5 L0 A" n; `; @2 _ 2、CAS3、Spring security CAS2.2Oauth2认证单点登录的特点是:1、认证系统为独立的系统2、各子系统通过Http或其它协议与认证系统通信,完成用户认证3、用户身份信息存储在Redis集群。 ) v( V/ a5 F# H
Java中有很多用户认证的框架都可以实现单点登录: 1、Apache Shiro.2、CAS3、Spring security CAS2.2Oauth2认证2.2.1Oauth2认证流程第三方认证技术方案最主要是解决认证协议的通用标准 问题,因为要实现 跨系统认证,各系统之间要遵循一定的接口协议。
: a2 Q0 n( h& b" b( f6 \$ T OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。
4 K* x' d! v2 Y1 i( U 互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。 & T8 m4 w0 u2 n2 P( J
参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdinOauth协议:https://tools.ietf.org/html/rfc6749下边分析一个Oauth2认证的例子,黑马程序员网站使用微信认证的过程: & K2 y" x( o7 t* o
第三方认证技术方案最主要是解决认证协议的通用标准 问题,因为要实现 跨系统认证,各系统之间要遵循一定的接口协议OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。
1 z y/ }/ F# Q( h5 n 业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
( A2 S- A) Q* d4 }5 b) v; f3 } Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用 参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdinOauth协议:。 6 v: O7 t# E- `, i: D. G8 w+ @7 h
https://tools.ietf.org/html/rfc6749下边分析一个Oauth2认证的例子,黑马程序员网站使用微信认证的过程::
( l- H; s( f6 P0 M9 L5 r5 N. {* w! M 1、客户端请求第三方授权用户进入黑马程序的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。
2 a, E K; O: ~ 点击“微信”出现一个二维码,此时用户扫描二维码,开始给黑马程序员授权。 : O1 A: Y2 j* i7 H
2、资源拥有者同意给客户端授权资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,微信会询问用户是否给授权黑马程序员访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会 颁发一个授权码,并重定向到黑马程序员的网站。
9 r4 {2 H: `" o% n 3、客户端获取到授权码,请求认证服务器申请令牌此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码4、认证服务器向客户端响应令牌认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。 2 Y4 f9 Y$ E1 i! B# s6 |8 n; x
此交互过程用户看不到,当客户端拿到令牌后,用户在黑马程序员看到已经登录成功5、客户端请求资源服务器的资源客户端携带令牌访问资源服务器的资源黑马程序员网站携带令牌请求访问微信服务器获取用户的基本信息6、资源服务器返回受保护资源 / |3 l. ^7 D5 H9 T; f/ O8 n
资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容注意:资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务资源服务器通常要请求认证 服务器来校验令牌的合法性Oauth2.0认证流程如下:。 ! E6 x% v$ t }% H3 U% ~" h
引自Oauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749
, a. q I( e2 A Oauth2包括以下角色:1、客户端本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:学成在线Android客户端、学成在线Web客户端(浏览器端)、微信客户端等2、资源拥有者通常为用户,也可以是应用程序,即该资源的拥有者。 ; U9 x9 ], h) A% u
3、授权服务器(也称认证服务器)用来对资源拥有的身份进行认证、对访问资源进行授权客户端要想访问资源需要通过认证服务器由资源拥有者授 权后方可访问4、资源服务器存储资源的服务器,比如,学成网用户管理服务器存储了学成网的用户信息,学成网学习服务器存储了学生的学习 信息,微信的资源服务存储了微信的用户信息等。
: X5 @5 ?# `4 K5 d" N3 [ 客户端最终访问资源服务器获取资源信息2.2.2Oauth2在本项目的应用Oauth2是一个标准的开放的授权协议,应用程序可以根据自己的要求去使用Oauth2,本项目使用Oauth2实现如下目标:1、学成在线访问第三方系统的资源 ' e8 z$ _& B3 q2 c/ M
2、外部系统访问学成在线的资源3、学成在线前端(客户端) 访问学成在线微服务的资源4、学成在线微服务之间访问资源,例如:微服务A访问微服务B的资源,B访问A的资源2.3Spring security Oauth2认证解决方案。
( [6 C% P) }" O4 z0 [ 本项目采用 Spring security + Oauth2完成用户认证及用户授权,Spring security 是一个强大的和高度可定制的身份验证和访问控制框架,Spring security 框架集成了Oauth2协议,下图是项目认证架构图:
& M) i) u7 I4 @; C. Y- z 1、用户请求认证服务完成认证2、认证服务下发用户身份令牌,拥有身份令牌表示身份合法3、用户携带令牌请求资源服务,请求资源服务必先经过网关4、网关校验用户身份令牌的合法,不合法表示用户没有登录,如果合法则放行继续访问。
9 F; a$ @& Z, \% s" Q9 Y' y; M* T. r 5、资源服务获取令牌,根据令牌完成授权6、资源服务完成授权则响应资源信3Spring Security Oauth2研究3.1目标本项目认证服务基于Spring Security Oauth2进行构建,并在其基础上作了一些扩展,采用JWT令牌机制,并自定义了用户身份信息的内容。
7 Y+ |! }8 M2 a/ q# i S) C 本教程的主要目标是学习在项目中集成Spring Security Oauth2的方法和流程,通过spring Security Oauth2的研究需要达到以下目标:1、理解Oauth2的授权码认证流程及密码认证的流程。 3 c; }* A- N+ I+ n
2、理解spring Security Oauth2的工作流程3、掌握资源服务集成spring Security框架完成Oauth2认证的流程3.2搭建认证服务器息3.2.1导入基础工程导入“资料”目录下的 xc-service-ucenter-auth工程,该工程是基于Spring Security Oauth2的一个二次封装的工程,导入此工程研究Oauth2认证流程。 7 b; p' m% d+ v- \8 }
3.2.2创建数据库导入资料目录下的 xc_user.sql,创建用户数据库 ; A$ n$ o) t9 E. ^7 K
以“oauth_”开头的表都是spring Security 自带的表。本项目中spring Security 主要使用oauth_client_details表: ; r& Y) K1 K% C; n3 [
client_id: 客 户 端 id resource_ids:资源id(暂时不用) client_secret:客户端密码scope:范围access_token_validity:访问token的有效期(秒) refresh_token_validity:刷新token的有效期(秒) " f. N% F$ [7 W) Y
authorized_grant_type:授权类型,authorization_code,password,refresh_token,client_credentials3.3Oauth2授权码模式 7 h$ \( K. B8 ^8 W0 {7 U0 j. v8 H. o
3.3.1Oauth2授权模式Oauth2有以下授权模式:授权码模式(Authorization Code)隐式授权模式(Implicit)密码模式(Resource Owner Password Credentials) # ^7 O/ W E) {& X" T+ l |: Z& _
客户端模式(Client Credentials)其中授权码模式和密码模式应用较多,本小节介绍授权码模式3.3.2授权码授权流程上边例举的黑马程序员网站使用微信认证的过程就是授权码模式,流程如下:1、客户端请求第三方授权 2、用户(资源拥有者)同意给客户端授权 3、客户端获取到授权码,请求认证服务器申请令牌 4、认证服务器向客户端响应令牌 5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权。 - d& R5 @8 T0 A
6、资源服务器返回受保护资源3.3.2申请授权码请求认证服务获取授权码:Get请求:localhost:40400/auth/oauth/authorize? client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://localhost# Z2 m4 W' w/ |: K2 M
6 k7 \/ p( ]) e8 E2 F. k$ f
参数列表如下:client_id:客户端id,和授权配置类中设置的客户端id一致response_type:授权码模式固定为codescop:客户端范围,和授权配置类中设置的scop一致redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。 $ G" S# U4 o6 g: }+ \. Y3 }$ G
首先跳转到登录页面: 1 r7 m! @, B4 E1 p
输入账号和密码,点击LoginSpring Security接收到请求会调用UserDetailsService接口的loadUserByUsername方法查询用户正确的密码当前导入的基础工程中将正确的密码硬编码为“123”,所以这里账号随意输入,密码输入123即可认证通过。 # U" o9 T, E [
点击“同意”。接下来返回授权码:认证服务携带授权码跳转redirect_uri
( o( ~ V/ m% E" y: q6 a7 {( l c# J 3.3.3申请令牌拿到授权码后,申请令牌Post请求:http://localhost:40400/auth/oauth/token参数如下:grant_type:授权类型,填写authorization_code,表示授权码模式。
) x" w; R! Y+ C code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致此链接需要使用 http Basic认证。 2 P5 Y2 G; f! z7 ^
什么是http Basic认证?http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编 码,放在header中请求服务端,一个例子:Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA= # Z$ U$ X! v, c) S* p, V/ R( R% J% c
WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。认证失败服务端返回 401 Unauthorized以上测试使用postman完成:http basic认证: 3 Y5 V4 c$ G7 l# ^! b. H
客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。Post请求参数:
8 \% ]" M4 I2 o 点击发送:申请令牌成功: . X0 d" @( Z/ i8 u6 @
access_token:访问令牌,携带此令牌访问资源token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
& i. l p: o) k7 ~3 E, x8 O refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间expires_in:过期时间,单位为秒scope:范围,与定义的客户端范围一致3.3.4资源服务授权3.3.4.1资源服务授权流程。 % j% a; e+ g! n$ @8 Q
资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资 源,如下图:
4 F2 u$ V4 c$ A( S% ~ 上图的业务流程如下:1、客户端请求认证服务申请令牌2、认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌3、客户端携带令牌访问资源服务客户端在Http header 中添加: Authorization:Bearer 令牌。 / F: r: q. ~" e% ] A- A
4、资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性5、令牌有效,资源服务向客户端响应资源信息3.3.4.2资源服务授权配置基本上所有微服务都是资源服务,这里我们在课程管理服务上配置授权控制,当配置了授权控制后如要访问课程信 息则必须提供令牌。 0 ]$ z) k, G0 W4 m! \" T, M* E
1、配置公钥认证服务生成令牌采用非对称加密算法,认证服务采用私钥加密生成令牌,对外向资源服务提供公钥,资源服务使 用公钥 来校验令牌的合法性将公钥拷贝到 publickey.txt文件中,将此文件拷贝到资源服务工程的classpath下。 4 p M( H6 o) p
2、添加依赖 , l% c' m! Z1 g0 s4 k2 {' ]
2、添加依赖; L t* E$ A1 J. ^ B) c
org.springframework.cloud' q7 Y3 k' d1 ]
spring‐cloud‐starter‐oauth2
& T s' v# ?6 Y1 ]3 i6 |# B6 B! r- n4 h. L$ R s
6 s, n$ M6 A q. B& ?5 [
4、在config包下创建ResourceServerConfig类:@Configuration @EnableResourceServer@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的
& g: O) K6 y7 }1 X PreAuthorize注解public class ResourceServerConfig extends ResourceServerConfigurerAdapter {//公钥private static final String PUBLIC_KEY = "publickey.txt";
- F+ Y/ A! f1 c1 {! S3 u! c' x //定义JwtTokenStore,使用jwt令牌@Beanpublic TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter);
% S& I! m$ M i& h3 h" v7 P9 r- N }//定义JJwtAccessTokenConverter,使用jwt令牌@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); 1 ?- S! x/ w5 O+ M2 ~1 w/ _
return converter;}/***获取非对称加密公钥 Key*@return 公 钥 Key*/private String getPubKey() {Resource resource = new ClassPathResource(PUBLIC_KEY); try { % X) w6 x8 |, m
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());BufferedReader br = new BufferedReader(inputStreamReader); return br.lines().collect(Collectors.joining("\n")); : X; E8 _' _, l+ _" J
} catch (IOException ioe) { return null;}}//Http安全配置,对每个到达系统的http请求链接进行校验@Overridepublic void configure(HttpSecurity http) throws Exception {
- B$ ^0 I4 Z9 I+ T/ |% Q" b2 W5 ` //所有请求必须认证通过http.authorizeRequests().anyRequest().authenticated();}}3.3.4.3资源服务授权测试这里我们测试课程图片查询get http://localhost:31200/course/coursepic/list/4028e58161bd3b380161bd3bcd2f0000 % e0 Z) R3 c! j- l2 J
请求时没有携带令牌则报错:{ M! f6 c+ K- ~; D0 X
"error": "unauthorized",& q' A# m/ U5 f
"error_description": "Full authentication is required to access this resource"0 |% S! }" g5 c1 A
}
4 V+ B0 Y% w7 ^, U " d: M! N$ p6 U" e
请求时携带令牌:在http header中添加 Authorization: Bearer 令牌 ' V6 k0 j3 E) L
当输入错误的令牌也无法正常访问资源。 ' m w# A+ O' `$ F: m* R/ D
3.3.4.4解决swagger-ui无法访问当课程管理加了授权之后再访问swagger-ui则报错: $ }9 u- z& {( n% e. _
修改授权配置类ResourceServerConfig的configure方法: 针对swagger-ui的请求路径进行放行://Http安全配置,对每个到达系统的http请求链接进行校验@Override. D: H( f; e4 r$ F# G, V A. p
public void configure(HttpSecurity http) throws Exception {( S/ ~9 T O ^
//所有请求必须认证通过http.authorizeRequests()$ T: R. s$ v+ [* E9 R+ F# P
//下边的路径放行
; ]9 _7 B' V6 E4 T- j .antMatchers("/v2/api‐docs", "/swagger‐resources/configuration/ui", "/swagger‐resources","/swagger‐resources/configuration/security", "/swagger‐ui.html","/webjars/**").permitAll()2 `8 r* }! l }! P* J; l& p2 M' y
.anyRequest().authenticated();
6 O) J+ M! }- D, o& `3 _7 u }+ G0 R+ D# k3 ^
' p4 s. w! o% {( }
注意:通过上边的配置虽然可以访问swagger-ui,但是无法进行单元测试,除非去掉认证的配置或在上边配置中添加所有请求均放行("/**")3.4Oauth2密码模式授权密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接通过用户名和密码即可申请令牌。 7 O) O. A; | @, I) n
测试如下:Post请求:http://localhost:40400/auth/oauth/token参数:grant_type:密码模式授权填写password username:账号password:密码 0 y I7 _+ p$ T- t; {' H' t! z8 x
并且此链接需要使用 http Basic认证。 . R" y6 J$ Z- {) |3 z4 X& p: J5 K) o
上边参数使用x-www-form-urlencoded方式传输,使用postman测试如下: 7 {& e# \& D7 {' F- ^% P4 ~% b
注意:当令牌没有过期时同一个用户再次申请令牌则不再颁发新令牌3.5校验令牌Spring Security Oauth2提供校验令牌的端点,如下:Get: http://localhost:40400/auth/oauth/check_token?token=。
6 x$ L1 U4 J2 _8 V0 ^ 参数:token:令牌使用postman测试如下:
# X- \/ r5 h% o) b' _% N 结果如下:{0 z4 q3 Y' q W' D6 }8 }; q2 A; s
"companyId": null,- ?" Q# ^# x% @
"userpic": null, "user_name": "mrt", "scope": [4 q: ` O% h# k. C
"app"
) m% B, _' x+ m2 u ], n8 q% c* ?" k2 F
"name": null,1 A' w. I* }! F" n) ~2 f/ U
"utype": null,. w7 W6 S; t' [3 u. S$ e/ C8 O9 d
"id": null,
- \) H% N. x) d N! b& D "exp": 1531254828,- G2 Z! G& s* ~
"jti": "6a00f227‐4c30‐47dc‐a959‐c0c147806462", "client_id": "XcWebApp"
* U' i. h% I- a% y$ q6 ] y6 m }1 \/ P- E5 e9 I- s1 O
, u( Y; s2 J% v exp:过期时间,long类型,距离1970年的秒数(new Date().getTime()可得到当前时间距离1970年的毫秒数)user_name: 用户名client_id:客户端Id,在oauth_client_details中配置scope:客户端范围,在oauth_client_details表中配置jti:与令牌对应的唯一标识。
# l: A! F1 V1 B# \ U companyId、userpic、name、utype、id:这些字段是本认证服务在Spring Security基础上扩展的用户身份信息3.5刷新令牌刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。 0 {- E0 C3 Y( Z1 I
测 试 如 下 : Post:http://localhost:40400/auth/oauth/token 参数:grant_type: 固定为 refresh_tokenrefresh_token:刷新令牌(注意不是access_token,而是refresh_token) : N+ `0 q0 R0 z) |; P( K3 C" z
刷新令牌成功,会重新生成新的访问令牌和刷新令牌,令牌的有效期也比旧令牌长 刷新令牌通常是在令牌快过期时进行刷新3.6JWT研究3.6.1JWT介绍在介绍JWT之前先看一下传统校验令牌的方法,如下图: $ ?* k. p9 G8 b& F' }! Q
问题:传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并根 据令牌获取用户的相关信息,性能低下解决:使用JWT的思路是,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。 ) k8 N1 r2 G( a, [2 w7 ]& H
JWT令牌授权过程如下图: ! h7 k8 b. p" m4 c# Y
什么是JWT?JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。
8 X" H5 y! L0 ?" Y JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改官网:https://jwt.io/标准:https://tools.ietf.org/html/rfc7519JWT令牌的优点:1、jwt基于json,非常方便解析。 1 [- Y/ U8 x2 C$ s) e C2 R
2、可以在令牌中自定义丰富的内容,易扩展3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高4、资源服务使用JWT可不依赖认证服务即可完成授权缺点:1、JWT令牌较长,占存储空间比较大3.6.1.1令牌结构 : L2 h7 z, K& O, z
通过学习JWT令牌结构为自定义jwt令牌打好基础JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz Header头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)。 - E6 N' V* h% u/ e- |
一个例子如下:下边是Header部分的内容{
0 L) k8 A1 X- o+ O( R, r/ t9 q "alg": "HS256",
4 ?1 G- H5 c3 ]( t# J; C "typ": "JWT"+ Q* N, ?5 u2 t. u( L# e
}9 W3 y9 Z+ k3 p2 a$ |8 a
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分Payload第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。 , n3 H' K) ~9 f& u" n; I! j) ^
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分一个例子:{6 H# N$ y, L7 J, _
"sub": "1234567890",
3 a9 P# T/ U; B/ i- y9 r "name": "456", "admin": true: Y- t6 V/ B6 R
}* y" b! C3 O @
。
6 i; a3 b( ^: H( } Signature第三部分是签名,此部分用于防止jwt内容被篡改这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明签名算法进行签名一个例子:HMACSHA256(& m' J: m* z; D
base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
$ e3 ~1 F& V6 S# B: \: Q
! y) b. \/ K }( l s7 o( F( _5 J- u base64UrlEncode(header):jwt令牌的第一部分base64UrlEncode(payload):jwt令牌的第二部分secret:签名所使用的密钥3.6.3JWT入门Spring Security 提供对JWT的支持,本节我们使用Spring Security 提供的JwtHelper来创建JWT令牌,校验JWT令牌等操作。 ) i! K. }8 h9 x7 w9 U
3.6.3.1生成私钥和公钥JWT令牌生成采用非对称加密算法1、生成密钥证书下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass xuechengkeystore
- n6 d$ [% Q) J4 R Keytool 是一个java提供的证书管理工具-alias:密钥的别名-keyalg:使用的hash算法-keypass:密钥的访问密码-keystore:密钥库文件名,xc.keystore保存了生成的证书 4 `3 h7 f# w+ k: p
-storepass:密钥库的访问密码查询证书信息:keytool -list -keystore xc.keystore删除别名keytool -delete -alias xckey -keystore xc.keystore
4 N$ J3 p4 P6 V! T 2、导出公钥openssl是一个加解密工具包,这里使用openssl来导出公钥信息安装 openssl:http://slproweb.com/products/Win32OpenSSL.html安装资料目录下的Win64OpenSSL-1_1_0g.exe。 / p9 R' {; [3 N+ d5 z, D
配置openssl的path环境变量,本教程配置在D:\OpenSSL-Win64\bin cmd进入xc.keystore文件所在目录执行如下命令:keytool ‐list ‐rfc ‐‐keystore xc.keystore | openssl x509 ‐inform pem ‐pubkey
4 `5 i( k" J/ y3 s5 r1 l! c , h& m$ `1 X% u9 `2 u6 {, N
输入密钥库密码: ( ]3 O& G, p# c7 {6 c
下边这一段就是公钥内容:-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAijyxMdq4S6L1Af1rtB8SjCZHNgsQG8JTfGy55eYvzG0B & U+ y' Q t6 ^* |3 O- {' K! g' N0 k
/E4AudR2prSRBvF7NYPL47scRCNPgLnvbQczBHbBug6uOr78qnWsYxHlW6Aa5dI5NsmOD4DLtSw8eX0hFyK5F j6ScYOSFBz9cd1nNTvx2+oIv0lJDcpQdQhsfgsEr1ntvWterZt/8r7xNN83gHYuZ6TM5MYvjQNBc5qC7Krs9wM7U oQuL+s0X6RlOib7/mcLn/lFLsLDdYQAZkSDx/6+t+1oHdMarChIPYT1sx9Dwj2j2mvFNDTKKKKAq0cv14Vrhz67Vj mz2yMJePDqUi0JYS2r0iIo7n8vN7s83v5uOQIDAQAB-----END PUBLIC KEY-----
% w s/ R* v+ L 将上边的公钥拷贝到文本文件中,合并为一行3.6.3.2生成jwt令牌在认证工程创建测试类,测试jwt令牌的生成与验证//生成一个jwt令牌@Test
- D/ Q7 u# ?7 o- N public void testCreateJwt(){0 d9 [1 L- `6 R/ n( K$ J3 v. ^
//证书文件
* o3 E7 m4 F; u String key_location = "xc.keystore";& Z" }# G6 a7 ]$ L8 F- K
//密钥库密码
! W% J. X# U6 ^4 \3 ?3 i9 X String keystore_password = "xuechengkeystore";
0 \7 q( v9 v @+ ^& @ L# o. S //访问证书路径
- S" q/ Q0 R: f% f$ u- q ClassPathResource resource = new ClassPathResource(key_location);
3 Q: w$ y8 p+ c //密钥工厂+ F9 e: p- s3 Y# M# Z# U
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, keystore_password.toCharArray());
8 A( W/ O! D. B7 X; S //密钥的密码,此密码和别名要匹配: i8 a) H5 I5 j. d; B4 y( `
String keypassword = "xuecheng";
1 g- Z- I1 M1 J# J* ? //密钥别名8 [$ r. {- h1 \. U' ^4 f" q
String alias = "xckey";
7 W, o+ k; e$ p6 W7 a1 | //密钥对(密钥和公钥)% \ s, ~% g( R/ W
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypassword.toCharArray());
. L9 U0 z( |/ O" J+ C7 v0 ? //私钥
! d% c. a8 l, T3 c RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();+ w- |( e" Y; y+ G$ f1 u5 z
//定义payload信息
6 R7 Y; o. g& @, }! I: z Map tokenMap = new HashMap<>(); tokenMap.put("id", "123");
. M! T) I% j6 G/ i$ j tokenMap.put("name", "mrt");
% z) O( W; h: N2 h9 T! r5 Y# d tokenMap.put("roles", "r01,r02");; r/ P6 Y8 ^: D0 m' r% x% [
tokenMap.put("ext", "1");
' a; w1 [+ N: B" t& Y2 I+ x; g //生成jwt令牌
) ` p c* _8 t( \ Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(aPrivate));
% S3 P5 I! R! B. l6 H4 L1 \ //取出jwt令牌
7 \6 u" B5 d6 c4 w) R String token = jwt.getEncoded();
) y! G5 M. _# k: T System.out.println("token="+token);6 V- T3 x3 r# A# A [$ M
}
4 J. ^3 V- J- U' z2 l V( b9 V 。 ; N' Y8 q$ E! ?4 Q
3.6.3.3验证jwt令牌//资源服务使用公钥验证jwt的合法性,并对jwt解码@Testpublic void testVerify(){//jwt令牌String token="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHQiOiIxIiwicm9sZXMiOiJyMDEscjAyIiwibmFtZSI6Im1ydCIsI mlkIjoiMTIzIn0.KK7_67N5d1Dthd1PgDHMsbi0UlmjGRcm_XJUUwseJ2eZyJJWoPP2IcEZgAU3tUaaKEHUf9wSRwaDgwhrw fyIcSHbs8oy3zOQEL8j5AOjzBBs7vnRmB7DbSaQD7eJiQVJOXO1QpdmEFgjhc_IBCVTJCVWgZw60IEW1_Lg5tqaLvCiIl26K 48pJB5f‐le2zgYMzqR1L2LyTFkq39rG57VOqqSCi3dapsZQd4ctq95SJCXgGdrUDWtD52rp5o6_0uq‐ mrbRdRxkrQfsa1j8C5IW2‐T4eUmiN3f9wF9JxUK1 XC1OQkOn‐ZTBCdqwWIygDFbU7sf6KzfHJTm5vfjp6NIA";
3 G/ Q: `; f a6 Z' [- x //公钥String publickey = "‐‐‐‐‐BEGIN PUBLIC KEY‐‐‐‐‐ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAijyxMdq4S6L1Af1rtB8SjCZHNgsQG8JTfGy55eYvzG0B/E4AudR2 prSRBvF7NYPL47scRCNPgLnvbQczBHbBug6uOr78qnWsYxHlW6Aa5dI5NsmOD4DLtSw8eX0hFyK5Fj6ScYOSFBz9cd1nNTvx 2+oIv0lJDcpQdQhsfgsEr1ntvWterZt/8r7xNN83gHYuZ6TM5MYvjQNBc5qC7Krs9wM7UoQuL+s0X6RlOib7/mcLn/lFLsLD dYQAZkSDx/6+t+1oHdMarChIPYT1sx9Dwj2j2mvFNDTKKKKAq0cv14Vrhz67Vjmz2yMJePDqUi0JYS2r0iIo7n8vN7s83v5u OQIDAQAB‐‐‐‐‐END PUBLIC KEY‐‐‐‐‐";
- z. x: U/ P l! u/ A p //校验jwtJwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));//获取jwt原始内容String claims = jwt.getClaims();
3 h5 [0 a7 r6 u) U //jwt令牌String encoded = jwt.getEncoded(); System.out.println(encoded);}4认证接口开发4.1需求分析用户登录的流程图如下:
# Z' { O+ y3 H! U/ ] 执行流程:1、用户登录,请求认证服务2、认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入Redis,并且将身份令牌写入cookie3、用户访问资源页面,带着cookie到网关4、网关从cookie获取token,并查询Redis校验token,如果token不存在则拒绝访问,否则放行
, S ~0 w# u: P" s 5、用户退出,请求认证服务,清除redis中的token,并且删除cookie中的token使用redis存储用户的身份令牌有以下作用:1、实现用户退出注销功能,服务端清除令牌后,即使客户端请求携带token也是无效的。 $ e7 ~; J) G' c; }: n
2、由于jwt令牌过长,不宜存储在cookie中,所以将jwt令牌存储在redis,由客户端请求服务端获取并在客户端存 储4.2Redis配置4.2.1安装Redis1、安装Redis服务下载Windows版本的redis:。
; o% Z" Z7 w6 B9 w8 o+ C" v https://github.com/MicrosoftArchive/redis/tags 下载Redis-x64-3.2.100版本,解压Redis-x64-3.2.100.zip进入cmd命令行,进入Redis-x64-3.2.100目录。 0 D/ W4 e" W# D7 T! s
运行:redis‐server redis.windows.conf8 |1 J8 U. r. G/ E, y5 v
出现 下图说明 redis启动成功:
8 V. ?8 h* o3 d* n% U/ u9 r- V9 ` 注册为服务:redis‐server ‐‐service‐install redis.windows‐service.conf ‐‐loglevel verbose
, r5 V7 V( O# Q" f! s% g) l 刷新服务,会看到多了一个redis服务。
1 C; Q2 ~' C5 b! X9 F 常用的redis服务命令如下: 进入redis安装目录:卸载服务:redis-server.exe --service-uninstall开启服务:redis-server.exe --service-start 8 ?% i' d- Q. y J1 W* Z7 s
停止服务:redis-server.exe --service-stop2、redis-desktop-manager下载windows版本的redis客户端:https://redisdesktop.com/download 下载 * w! n8 N4 Y: V4 n8 w+ D
redis-desktop-manager-0.9.2.806.exe安装后启动redis客户端: 配置redis链接:
: S% ?. I W+ n/ a4 {$ ^7 ?+ w4 i 连接成功:
! }, D" \: c; @7 g7 | 4.2.2redis连接配置在认证服务的application.yml文件中添加如下配置:spring:
8 K( o( Y& d B- ] application:
- }. l& o; Y; y name: xc‐service‐ucenter‐auth redis:
& I) P! d# m' R; k% g host: ${REDIS_HOST:127.0.0.1} port: ${REDIS_PORT:6379}
$ U9 }' }. V6 r* f timeout: 5000 #连接超时 毫秒
% g, W: j) X Z. I. U! ^ jedis:
# f5 ~& o; J- s1 M9 w pool:3 b% h# J; ^$ g( W
maxActive: 3& \) A: @+ [+ _/ F% l
maxIdle: 35 d: [" O0 n( G& X+ u: f1 }
minIdle: 1; P; i) s9 q* j* }
maxWait: ‐1 #连接池最大等行时间 ‐1没有限制3 B3 K4 `( A- s! @) `+ O
% e9 R8 W# e) V* a: q9 _7 j( x! B 4.2.3测试@SpringBootTest @RunWith(SpringRunner.class) public class RedisTest {0 k# t1 I/ s! P- }0 y
@Autowired
# K0 B5 t; m. S( h6 j# u private StringRedisTemplate stringRedisTemplate; @Test9 f* @7 ^' ^/ @6 o5 w: a
public void testRedis(){1 J, X, T1 {$ s2 y
//定义key
9 g4 F, d8 R- L2 N% @$ P+ D String key = "user_token:9734b68f‐cf5e‐456f‐9bd6‐df578c711390";
+ N" O7 \/ I$ N' W2 u //定义Map5 q1 K, G; u6 i
Map mapValue = new HashMap<>(); mapValue.put("id","101"); mapValue.put("username","itcast");
. X2 X! ~ n5 d* H3 N5 ~ String value = JSON.toJSONString(mapValue);
9 k, |5 W. w" d5 ~+ _. ^7 @5 S // 向 redis 中 存 储 字 符 串 stringRedisTemplate.boundValueOps(key).set(value,60, TimeUnit.SECONDS);
% B$ R' a: t, m- Q) P- s1 Y //读取过期时间,已过期返回‐22 c* C5 B. M' O
Long expire = stringRedisTemplate.getExpire(key);
0 o, `: O. Q. e4 ^7 ]* q- v //根据key获取value
. y$ ]' c+ r! e+ _& ~8 A Q String s = stringRedisTemplate.opsForValue().get(key); System.out.println(s);
2 I0 k4 x- l, D) ]5 o }
# y- ^# `; S+ {& i9 i }
. L6 s4 M6 `& x . Y9 Z* i; Y$ s9 @' R. _) a7 F" A8 P/ ]
4.3认证服务4.3.1需求分析认证服务需要实现的功能如下:1、登录接口前端post提交账号、密码等,用户身份校验通过,生成令牌,并将令牌存储到redis将令牌写入cookie2、退出接口校验当前用户的身份为合法并且为已登录状态。
6 B! a: c `, H; c6 t 将令牌从redis删除删除cookie中的令牌。业务流程如下: , S; N7 ^8 W8 X* Z3 H( [1 O+ p
4.3.2Api接口@Api(value = "用户认证",description = "用户认证接口") public interface AuthControllerApi {
: L! e0 Q- \' y* N N/ S; F, B @ApiOperation("登录")
' V& _& y7 |( s- v public LoginResult login(LoginRequest loginRequest); p& ?" r4 r: e7 R& ^/ u8 [
@ApiOperation("退出") W4 o1 E7 o! d! i% I2 F8 K5 H; d
public ResponseResult logout();3 O& R# d0 v2 B% ?: y( R! p
}
7 Y: m+ a+ _( y" p m- R) ^ 8 N! Z# ?1 @& w" U2 ^5 u
4.3.3配置参数在application.yml中配置参数auth:
* {5 p* g! r3 G tokenValiditySeconds: 1200 #token存储到redis的过期时间clientId: XcWebApp9 W. |- U/ A5 G E/ ~# ]
clientSecret: XcWebApp cookieDomain: localhost cookieMaxAge: ‐1% W1 j! C% [& V; m1 t, T6 B* ~
8 P/ `5 B# F2 w& z# P) A' d
4.3.4申请令牌测试为了不破坏Spring Security的代码,我们在Service方法中通过RestTemplate请求Spring Security所暴露的申请令牌接口来申请令牌,下边是测试代码:
) e2 @7 x8 W" m) v: W5 G" a @SpringBootTest @RunWith(SpringRunner.class) public class TestClient {
% Q: B) ~( b6 }. a; M: F" b3 f @Autowired9 r8 Q r- R7 p2 ?1 w4 B
LoadBalancerClient loadBalancerClient;# o( X5 W: H% w$ k# ~% r: E
@Autowired
0 |' I6 ]* c1 ~7 i( \% e RestTemplate restTemplate;8 H% y. t* d7 ^3 d
@Test
5 o5 t, q$ I" p$ ^! V# w1 c public void testClient(){
$ _, ^/ ?/ f9 q$ S# h //采用客户端负载均衡,从eureka获取认证服务的ip 和端口3 L" C" Y0 e) G8 B% D1 }8 C/ _
ServiceInstance serviceInstance = loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);* y3 _! g y2 f% L
URI uri = serviceInstance.getUri();
- k0 L J4 Y6 d, S1 D String authUrl = uri+"/auth/oauth/token";
( S# N9 K* N* @# V" S0 _ @SpringBootTest @RunWith(SpringRunner.class) public class TestClient {
4 d# [4 L: s0 G/ r& V; m* j @Autowired3 w9 w- `0 }, \5 x
LoadBalancerClient loadBalancerClient;) F; u- b2 X/ H" e" R
@Autowired
- T+ y' E$ |0 V& ^+ H, j RestTemplate restTemplate;0 x- l+ N0 \; Y. P
@Test
( a+ y) C, i- t6 p: ?: G3 f4 I) g public void testClient(){, w" D+ t* o9 e; R5 S, X. G
//采用客户端负载均衡,从eureka获取认证服务的ip 和端口
; V/ L* v, \" w; |) Z6 ]% K* t ServiceInstance serviceInstance = loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);0 q0 Y0 S# Q) K
URI uri = serviceInstance.getUri();+ b* _5 @6 D/ ~% @
String authUrl = uri+"/auth/oauth/token";
# Z& t8 ~1 b2 J% X6 t* \
( e: w( d9 y" b 4.3.4Dao暂时使用静态数据,待用户登录调通再连接数据库校验用户信息4.3.5Service调用认证服务申请令牌,并将令牌存储到 redis1、AuthToken创建 AuthToken模型类,存储申请的令牌,包括身份令牌、刷新令牌、jwt令牌身份令牌:用于校验用户是否认证。 : J* n8 |9 k9 J' f4 \" W/ [
刷新令牌:jwt令牌快过期时执行刷新令牌jwt令牌:用于授权@Data @ToString$ @9 V9 ], Z- g4 [
@NoArgsConstructor public class AuthToken {
) K) k p, `, X* j9 ^! c1 T8 p String access_token;//身份token
$ L: F8 Y; F3 i: Q String refresh_token;//刷新token String jwt_token;//jwt令牌3 l: o5 h5 ]% U& T1 Y& X/ ?2 V
}
' ]: y1 L" P ~0 ^/ C8 ]- e5 ^2 m0 D 8 f9 G1 J- c* ]2 m! a# m( S) U
申请令牌的service方法如下:@Service& Q! y; a" ^, z
public class AuthService {
$ h7 }/ i+ |) }, m private static final Logger LOGGER = LoggerFactory.getLogger(AuthService.class); @Value("${auth.tokenValiditySeconds}")( E; L4 r3 u. W& Z
int tokenValiditySeconds;. H4 O) [4 D/ C. O2 `, L
@Autowired
% ?/ p$ b+ S/ Z% @ RestTemplate restTemplate; @Autowired
/ Q: _3 k3 M4 ^( a LoadBalancerClient loadBalancerClient; @Autowired* p5 u5 j- p6 h7 x, V
StringRedisTemplate stringRedisTemplate;) [. r0 y$ B0 V1 t- a4 u4 J$ r
//认证方法# x" s4 u+ V Z1 s( j; ^
public AuthToken login(String username,String password,String clientId,String clientSecret){+ Q9 R' K! N9 _$ [3 m" b( q
//申请令牌9 P% f& P: ]! ?' i4 ]9 z
AuthToken authToken = applyToken(username,password,clientId, clientSecret); if(authToken == null){& s: k" x0 t# Z9 k
ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
# c) o8 G) c9 o# b) X; I' u }5 A, Q1 P1 t3 ?; m
//将 token存储到redis
/ m$ D2 \$ ]. u String access_token = authToken.getAccess_token(); String content = JSON.toJSONString(authToken);: K/ e- L, S! w, W0 A" |' B
boolean saveTokenResult = saveToken(access_token, content, tokenValiditySeconds);3 z/ `0 q6 a+ y/ q9 c( W) Y$ S
if(!saveTokenResult){& }' x1 B' W v Q7 [
ExceptionCast.cast(AuthCode.AUTH_LOGIN_TOKEN_SAVEFAIL); ^' k1 r5 ~/ \# n/ l }
}
) g# e' b# j0 R; Y/ \% o9 {- B& q return authToken;# A6 P0 W- P) L$ s% w$ a0 E, h
}
3 b) [# Y1 `' s# z: ]8 f6 c8 a7 K //存储令牌到redis
; L' }8 {; v; B+ ~- G private boolean saveToken(String access_token,String content,long ttl){. R" e# ]8 a: \( R
//令牌名称
( i3 ]1 C% R; {& Q6 S: z String name = "user_token:" + access_token;
, H4 _2 `, }/ b9 `4 p0 }0 w //保存到令牌到redis
- |# S5 N- M& t4 D stringRedisTemplate.boundValueOps(name).set(content,ttl, TimeUnit.SECONDS);
( K |+ ^( e# `7 T5 a2 @2 L //获取过期时间
, P8 r) s5 z1 y1 o5 f* q' G Long expire = stringRedisTemplate.getExpire(name); return expire>0;8 F& }$ ?8 Z8 r" g6 x4 E9 B
}
- s' s1 ]' E. l6 n1 r //认证方法0 ?$ h* b% q; `( v: Z
private AuthToken applyToken(String username,String password,String clientId,String clientSecret){
4 P, P1 p+ x# W, j. A) U //选中认证服务的地址
- g( E4 r" g: v ServiceInstance serviceInstance = loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);7 Z' o) X. H+ H& h
if (serviceInstance == null) { LOGGER.error("choose an auth instance fail");7 i( s/ w% y1 v9 |
ExceptionCast.cast(AuthCode.AUTH_LOGIN_AUTHSERVER_NOTFOUND);# w9 b+ ^1 h6 C5 l2 `' I# o6 O
}3 r) ~+ b) O; C+ o4 }
//获取令牌的url
' @0 w6 |, U; K% \1 o String path = serviceInstance.getUri().toString()+"/auth/oauth/token";; X0 n( A7 s% J$ P/ h3 Y
//定义body/ V6 w% v" x! w' p0 V( ~+ u
MultiValueMap formData = new LinkedMultiValueMap<>();
% @( Z5 j' p* [) I //授权方式
. h9 ]0 [1 q" i formData.add("grant_type", "password");
2 |, U7 @6 X3 I9 Y //账号formData.add("username",username);
+ I, w8 x8 k9 J6 _( @+ G //密码- a# L# I9 \* Z. b) ~: ^, `0 f
formData.add("password", password);' F$ O, g9 Y) }1 M+ h
//定义头
( ?& L6 A* U' F$ N# T S- X MultiValueMap header = new LinkedMultiValueMap<>(); header.add("Authorization", httpbasic(clientId,clientSecret));2 n, y2 i! [' o
//指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值1 v5 N0 i8 m% @/ J' L/ e7 u
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
( O; B; ^7 S! X! \+ H1 j: Q @Override
' F7 T4 U; b: Z+ o. _" l+ ^ public void handleError(ClientHttpResponse response) throws IOException {
) ^# I5 S- w. Y( S/ L //当响应的值为400或401时候也要正常响应,不要抛出异常if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
' a( W. G, J+ H0 ?7 M3 E super.handleError(response);. c5 P) K: Z0 P2 ^: a* J8 m
}
8 \0 t) B% h- _$ D% Q$ q }0 ^/ p: a. g, a
});
" y) A5 J' v$ e6 B6 c: N# ?: O& y) C Map map = null; try {
, m$ z8 @2 @/ O. X! S //http请求spring security的申请令牌接口, b: e) g! b* A6 C; ]2 [* E S
ResponseEntity mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,
/ E# r! z4 X/ s5 y3 D new HttpEntity(formData, header), Map.class); map = mapResponseEntity.getBody();. C+ S& A3 \8 @/ U& y" h
} catch (RestClientException e) { e.printStackTrace();8 p6 ~. W5 ^5 [. F/ Z; w
LOGGER.error("request oauth_token_password error: {}",e.getMessage()); e.printStackTrace(); ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);- t$ W# n1 J5 u7 @( U5 O
}7 f: |5 G1 G: X9 R
if(map == null ||
9 j X9 ~. U/ {: ?5 h" s map.get("access_token") == null || map.get("refresh_token") == null ||3 l, ^. ]6 m) N" k& _7 n- ^
map.get("jti") == null){//jti是jwt令牌的唯一标识作为用户身份令牌9 t& {# B" d: w+ c, R
ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);" b9 f2 P- L" |5 D: w4 Z
}( p8 o: a+ j3 Y2 S9 b" ^/ @
AuthToken authToken = new AuthToken();& `7 q+ j0 r, J
//访问令牌(jwt)
. C7 _7 y0 A0 [. |& o0 D& t/ \) p: F String jwt_token = (String) map.get("access_token");
+ P8 M W- G! `9 y( ~ //刷新令牌(jwt)+ d2 L1 z- ^4 r2 C+ U' V9 {/ T4 p
String refresh_token = (String) map.get("refresh_token");
8 x- p8 [/ x; p //jti,作为用户的身份标识" t% [" m. }4 p1 D: q. H
String access_token = (String) map.get("jti"); authToken.setJwt_token(jwt_token); authToken.setAccess_token(access_token); authToken.setRefresh_token(refresh_token); return authToken;, j- h/ f+ B% f, U) D
}/ z( }, n' ]+ O& r+ @5 U6 S
//获取httpbasic认证串, c$ t. \- |( x8 ?* n) x
private String httpbasic(String clientId,String clientSecret){% q% Z4 i% H1 B; k ]0 E( Q
//将客户端id和客户端密码拼接,按“客户端id:客户端密码” String string = clientId+":"+clientSecret;
) q) D3 V, O* L8 a2 r/ F+ h //进行base64编码7 r/ F1 P! n( F/ l
byte[] encode = Base64.encode(string.getBytes()); return "Basic "+new String(encode);
. m5 b6 j( {' O- E }
; n7 G; _3 @6 D2 N }8 \& }! P+ j8 L: ^% x$ n, H
& l; q+ ^" w* J/ a7 Q' Q
6 |( l( d% h5 i+ [2 I 4.3.6ControllerAuthController代码如下:@RestController8 p2 d/ P- B1 W' Q W& c
public class AuthController implements AuthControllerApi { @Value("${auth.clientId}")
( C7 R) K9 g* F! J String clientId; @Value("${auth.clientSecret}")) U+ |8 J2 f7 f w: b' I
String clientSecret;
8 p! [- f! I! [* Z% @ @Value("${auth.cookieDomain}") String cookieDomain; @Value("${auth.cookieMaxAge}") int cookieMaxAge;
! ?8 E8 s! d$ M* L$ P T7 Y @Value("${auth.tokenValiditySeconds}") int tokenValiditySeconds;
. D2 w* }* i3 E @Autowired
, _" f' d! }8 g$ c AuthService authService;+ E% m+ Z b( h; U# P0 l
@Override @PostMapping("/userlogin")
" ]+ e/ [- V9 r public LoginResult login(LoginRequest loginRequest) {: @8 R7 a7 F$ K5 b b- y2 i* K( c
//校验账号是否输入
2 B d) \. B/ q: m: b if(loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername())){ ExceptionCast.cast(AuthCode.AUTH_USERNAME_NONE);8 H3 k! W" V, x! ^9 W+ k
}
1 D; D) l. ~; A4 P //校验密码是否输入if(StringUtils.isEmpty(loginRequest.getPassword())){
% a* ~2 V4 G/ Y0 x" A* d3 d ExceptionCast.cast(AuthCode.AUTH_PASSWORD_NONE);
( m; `: z( Z1 r) d! [/ e }
* f$ X2 p8 V% G; r AuthToken authToken = authService.login(loginRequest.getUsername(), loginRequest.getPassword(), clientId, clientSecret);+ s0 |( P" S1 ^* G
//将令牌写入cookie
/ d. ^2 W, i0 C3 P8 ` //访问token
$ [- b: T0 K5 I# \5 E2 E String access_token = authToken.getAccess_token();6 V( g4 Z k7 M9 e1 c
//将访问令牌存储到cookie saveCookie(access_token);
- [& }3 A( H* _2 V+ w/ ^% D; N8 U! V return new LoginResult(CommonCode.SUCCESS,access_token);
o9 C2 N K( k$ h% P+ m, W }: }. b2 O/ k- K0 `8 {& M
//将令牌保存到cookie! a" \: R8 d9 u4 i" }, z, f8 |
private void saveCookie(String token){# I7 p. N) k4 s2 K
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();% ?& ] u. ^' V. O* W
//添加cookie 认证令牌,最后一个参数设置为false,表示允许浏览器获取
+ p3 w, N1 [, h2 ?4 l CookieUtil.addCookie(response, cookieDomain, "/", "uid", token, cookieMaxAge, false);
2 S+ q* f' ]% D7 z7 u }
1 \: i) K2 l* W: S; F @Override @PostMapping("/userlogout") public ResponseResult logout() {* r- e. y Z5 R/ p2 Q5 ^( d$ x2 [
return null;& G( D- K2 t# ~3 k. r
}
3 C+ _9 l% I7 T# `( `, A }5 ^4 u# G L& w2 w' z3 [
( _8 F& @" [: t7 ^
4.3.7登录url放行认证服务默认都要校验用户的身份信息,这里需要将登录url放行在WebSecurityConfig类中重写 configure(WebSecurity web)方法,如下:@Override
3 X! \% a3 v$ p* _6 w2 \. ` public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/userlogin");6 u5 L8 V0 S8 A' L
}
: @( Z' d; e5 Z, P, c8 \5 n 。 ; q. W% G( y. l' g% b D5 v7 X
4.3.8测试认证接口使用postman测试:Post请求:http://localhost:40400/auth/userlogin 1 A# l* r s$ o% }" v
4.3.9测试写入Cookiecookie最终会写到xuecheng.com域名下,可通过nginx代理进行认证,测试cookie是否写成功1、配置nginx代理在ucenter.xuecheng.com下配置代理路径。 $ q5 V( D+ ^8 B5 [" ^* h
#认证) g" t+ e! f' N5 c* c3 {7 x ^7 F
location ^~ /openapi/auth/ {5 m3 n) c5 Z7 V; A* [. E' z7 `
proxy_pass http://auth_server_pool/auth/;( g& r# z9 p+ B: ^) n# g
}7 p# S* W- k. k! v' R: m
添加:#认证服务# i3 |- ]9 V B, j, `+ l6 ?- V
upstream auth_server_pool{
! H- @( B% {- ~* P server 127.0.0.1:40400 weight=10;! z3 Y/ G7 e" O
}! s0 t2 J# A( V& g, T: @
; i" }' B% y9 [* x% t; j8 |' E 2、请求:http://ucenter.xuecheng.com/openapi/auth/userlogin观察cookie写入结果
: `7 h1 H3 `" F0 z9 ]# q4 o5 g2 P5 {
- ]2 o. c3 @( g6 H. g& J4 h T' l2 N
% V" _3 q( |+ V( X% V9 ^ |