上期详细介绍了OAuth2的相关知识点,重点在OAuth2的授权模式(其中最重要的是授权码模式),这期我们就通过Demo的方式来实践下OAuth2认证协议。
仓库地址:https://github.com/yoyocraft/oauth2-demo
SQL脚本 && Postman测试脚本:https://github.com/yoyocraft/oauth2-demo/tree/master/doc
作者环境:
- MacOS
- Open JDK 11
- Nacos 2.2.2
- Maven 3.6.1
- MySQL 8.0.31
- Spring Boot 2.7.0
- Spring Cloud 2021.0.1
- Spring Cloud Alibaba 2021.0.1.0
- IntelliJ IDEA Community Edition 2023.1.3
一、初始化项目
1.1 创建Maven项目
pom.xml
核心内容:进行版本控制和工具类引入
<properties> |
1.2 数据库环境搭建
OAuth2环境需要的数据库脚本可以从官方仓库中找到:https://github.com/spring-attic/spring-security-oauth/blob/main/spring-security-oauth2/src/test/resources/schema.sql
此处作者稍稍做了优化,添加了模拟数据,下面是数据库(oauth)脚本文件内容schema.sql
/* |
上述数据库中,除user
表之外,其余的表都是OAuth2的必备表,user
表是可以根据实际业务去自定义的
1.3 Nacos环境准备
此处我们选择Nacos集群环境,具体的Nacos集群搭建过程可以查看官网教程
(最权威、最新的),如果你是MacOS(M系列芯片)用户,集群搭建有些许不同,具体的过程可以在本站搜索MacOS搭建Nacos集群
。
二、OAuth2模块
2.1 搭建环境
创建OAuth2模块,pom.xml
核心内容:
<properties> |
PS:不一定非要使用JPA,只是OAuth2模块的SQL查询使用JPA会比较方便
配置文件application.yml
内容
server: |
PS:具体的数据库配置信息和Nacos地址信息以实际环境为准
2.2 创建实体类
前面说过,user
表是我们自定义的,OAuth2并不知道有这张表的存在,因此我们需要去创建对应的实体类去操作这张表,其余的表格都是有OAuth2内部操作,不需要我们操心。
User.java
package com.juzi.oauth2.pojo; |
PS:此处为了操作方便,就只操作上述的几个字段,其余字段可以自行扩展哦~
2.3 OAuth2 + SpringSecurity 配置
SpringSecurity认证流程链:
因为我们的用户数据存储在数据库中,需要让SpringSecurity知道去数据库中查询用户信息,然后才能认证授权。因此我们需要去实现接口org.springframework.security.core.userdetails.UserDetailsService
,实现其中的loadUserByUsername()
方法,从方法名也可以看出,是根据用户名去查询用户。
那么此时我们还需要知道用什么去查询,JDBC?MyBatis?我们刚刚引入的JPA这个时候就派上用场了,我们只需要定义一个接口,继承org.springframework.data.jpa.repository.JpaRepository
接口,指定实体类和主键类型,定义查询方法就可以了。
UserRepository.java
package com.juzi.oauth2.repo; |
至此,我们定义了从数据库查询用户信息的入口,接下来就应该实现UserDetailService
接口,让SpringSecurity从数据库查询用户信息,进行认证授权了。
UserDetailServiceImpl.java
package com.juzi.oauth2.service; |
但是实现了接口就能够让SpringSecurity从数据库查询用户了吗?还不行,我们还需要将我们实现的类注册到SpringSecurity中,也就是配置。此处呢,我们需要编写配置类,继承org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
配置类,重写其中的配置方法。
PS:尽管WebSecurityConfigurerAdapter在Spring Security 5.7.1及更新版本或者Spring Boot 2.7.0及更新版本已经被弃用了,但是依然有很多项目使用它。当然,你也可以使用官方推荐的基于组件的(component-based)的安全配置去实现配置类,难度也不是很大,就是换了中写法而已啦。
WebSecurityConfig.java
package com.juzi.oauth2.config; |
首先需要明确一点,用户密码是绝对不可以以明文的形式存储数据库的,此处我们采用BCryptPasswordEncoder的形式去加密存储,只需要注册一个PasswordEncoder Bean即可。
protected void configure(AuthenticationManagerBuilder auth)throws Exception{ |
上述代码就是在将我们实现的从数据库查询用户信息的类注册到SpringSecurity中,使得SpringSecurity调用我们实现的loadUserByUserName方法查询,同时还指定了密码的加密方式。
public void configure(WebSecurity web){ |
上述代码实在配置认证授权需要忽略的路径,以及配置http的简要内容,比如禁用csrf、开启跨域等等。
|
上述代码是在注册AuthenticationManager类,至于作用嘛,我们先按下不表,等到进行OAuth2配置时自然就知道了。
至此为止,我们已经配置完了SpringSecurity,接下来就要配置OAuth2了。准备好了吗?发车了!
配置OAuth2需要我们去继承org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
类,复写其中的configure
方法。
Oauth2Config.java
package com.juzi.oauth2.config; |
PS:我们这里采取默认的数据源,当然可以自定义数据源,比如Druid数据源。
/** |
因为我们的数据都存放在数据库中,因此需要指定token和client相关信息的存放位置为Jdbc,在本项目中也就是MySQL。
|
上述代码是在配置默认的token管理器,注册了我们在上面提到的token存储位置的bean,设置了token过期时间等。
|
上述代码是在配置OAuth2中的Client Details的存储位置,这次设置为MySQL。
|
上述代码是在进行安全配置,比如允许表单认证、允许客户端认证等。
|
因为OAuth2模块也是要从数据库读取用户信息和存取token的,因此需要在上述的终端配置endpoints中注册相关的bean,除此之外,还需要注册我们刚刚声明的认证管理器类AuthenticationManager,用于OAuth2认证。
至此OAuth2的配置也就结束了,接下来就是编写启动类,测试启动了(仅仅启动,并不做任何的请求,因为还有模块没写呢~)。
Oauth2Application.java
package com.juzi.oauth2; |
需要标注服务发现、资源服务器等注解。
三、通用模块
这一模块主要为了网关模块和用户模块做准备,定义通用返回类、返回工具类等公共类;这一模块不需要引入任何依赖,因此只需要默认的pom.xml
文件即可。
通用返回类CommonResponse.java
package com.juzi.common.resp; |
返回值枚举类RespCodeEnum.java
package com.juzi.common.resp; |
返回工具类RespUtils.java
package com.juzi.common.resp; |
四、用户模块
4.1 搭建环境
pom.xml
核心内容:
<properties> |
application.yml
server: |
PS:Mysql、Nacos、Redis的具体配置还需要根据实际情形来调整哦~
4.2 业务需求
1)注册部分
- 账号密码注册
- 电话验证码注册
- 第三方平台注册
2)登录部分
- 账号密码登录
3)用户信息部分
- 绑定电话
4.3 创建实体类和对应操作数据库Repo
用户模块我们需要操作两张表,分别是user
和oauth_client_details
,因此我们需要创建两个实体类。
User.java
package com.juzi.user.pojo.po; |
OAuth2Client.java
package com.juzi.user.pojo.po; |
创建完实体类,就应该创建对应的JPA Repo类了
UserRepository.java
package com.juzi.user.repo; |
OAuth2ClientRepository.java
package com.juzi.user.repo; |
PS:不要忘记标注注解@Repository
除此之外,我们还需要创建两个枚举类,分别是注册类别(RegisterType)、权限授予类别(AuthGrantType)
RegisterType.java
package com.juzi.user.pojo.enums; |
按照我们本项目的处理,注册类别分别是账号密码、手机验证码、第三方平台
AuthGrantType.java
package com.juzi.user.pojo.enums; |
4.4 创建配置类和工具类
首先就是Redis的配置类RedisConfig.java
,主要是配置Redis中的key和value的序列化方式
package com.juzi.user.config; |
因为我们要使用RestTemplate
来发送HTTP请求,因此还需要配置RestTemplate,在此之前,我们先配置HttpClient
,目的就是替换掉RedisTemplate底层使用的URL
Connection(JDK),适用于复杂场景。
HttpClientConfig.java
package com.juzi.user.config; |
至此,我们可以开始进行RestTemplate的配置了,RestTemplateConfig.java
package com.juzi.user.config; |
声明两个RestTemplate的原因是因为我们需要有两部分请求,一部分请求第三方平台的接口,这是外部接口,是不需要进行负载均衡的;而还有一部分请求是请求内部OAuth2接口,这是内部接口,是需要进行负载均衡的( @LoadBalanced)
PS:负载均衡的配置会在网关模块中进行
本项目中,我们还需要进行第三方平台(此处选择的是GITEE)的注册登录,因为还需要创建对应的配置类,并且以配置的形式注入。
package com.juzi.user.config; |
具体的在application.yml
中的配置,等到测试的时候再去申请对应的值即可。
至此,配置类完结,接下来就是操作Redis的工具类的编写,仅仅为了操作Redis的方便,RedisCommonProcessor.java
package com.juzi.user.utils; |
4.5 用户服务类,实现具体的业务逻辑
4.5.1 登录注册部分
首先创建接口(UserRegisterLoginService)及其实现类(UserRegisterLoginServiceImpl)
package com.juzi.user.service; |
1)账号密码注册
实现账号密码注册业务流程:
- 判断用户是否存在(依据userName或者clientId是否存在)
- 如果存在,返回错误信息(用户已经存在)
- 如果不存在,就是新用户注册
- 先进行密码的加密(BCryptPasswordEncoder)
- 构建OAuth2Client信息
- 保存 user, oauth2 信息到数据库中,需要内部开启事务,因为需要保证最后返回数据时能够查询到最新的消息,因此事务只能在保存到数据库这个方法中开启和关闭
- 在Redis中保存用户信息
- 返回 user、token信息
首先我们需要在Repo类中添加查询方法,根据userName和clientId来查询用户信息
在UserRepository
中加入
User findByUserName(String username); |
在OAuth2ClientRepository
中加入
OAuth2Client findByClientId(String clientId); |
然后思考,UserRegisterLoginServiceImpl类中需要操作Repo和Redis,同时还需要向OAuth2服务申请token,因此需要注入这些类
|
在UserRegisterLoginService中定义接口:
CommonResponse namePasswordRegister(User user); |
在UserRegisterLoginServiceImpl中进行实现:
|
📢:注意需要在保存数据库信息的方法内部开启事务,才能够保证在返回信息是查询到最新的信息。
PS:此处使用了Map进行了简单结果处理,也可以定义VO类进行结果的封装;除此之外,具体的OAuth2Client信息的封装(尤其是ClientId、ClientSecret)、uid的生成可以自定义,只要统一一套规范即可。
2)手机验证码注册
PS:这部分需要短信模块先进行验证码的生成、保存、发送,具体的sms-service模块可以看:https://github.com/yoyocraft/oauth2-demo/tree/master/sms-service,因为这部分需要买云厂商的服务,我们就mock数据了,直接往Redis里存入一对键值对(手机号-验证码)
实现手机验证码注册的业务逻辑:
- 发送验证码,存储验证码(这步此处略过)
- 从Redis中查询验证码,与用户传入的验证码比对
- 若不一致,返回错误信息
- 若一致:
- 以验证码为用户的密码进行加密,主要是作为ClientSecret存储
- 根据手机号查找用户,若用户存在则更新ClientSecret即可
- 若不存在:
- 封装User和OAuth2Client信息
- 保存 user, oauth2 信息到数据库中
- 保存用户信息到Redis中
- 返回user、token信息
首先添加Repo方法:根据手机号查询用户、根据ClientId更新用户ClientSecret信息
在UserRepository
中添加
User findByUserPhone(String phoneNumber); |
在OAuth2ClientRepository
中添加
/** |
然后在UserRegisterLoginService类中定义接口:
CommonResponse phoneCodeRegister(String phoneNumber,String code); |
在UserRegisterLoginServiceImpl类中实现:
|
3)第三方平台注册
第三方平台(Gitee)注册业务逻辑:
- 组装tokenUrl,向第三方平台请求token
- 拿着token向第三方平台请求用户信息
- 封装User和OAuth2Client信息
- 保存 user, oauth2 信息到数据库中
- 保存用户信息到Redis中
- 返回user、token信息
在UserRegisterLoginService类中添加接口:
CommonResponse thirdPlatformGiteeRegister(HttpServletRequest request); |
在ServiceImpl中实现接口:
|
第三方平台(Gitee)clientId, clientSecret等参数的申请和配置
1)前往Gitee个人主页设置页面(https://gitee.com/profile/account_information)
2)点击侧边栏的「数据管理 => 第三方应用」选项
3)点击右上角创建应用
4)点击创建应用后会得到一组clientId和clientSecret
5)将得到的clientId和clientSecret复制下来,修改用户模块下的application.yml文件,添加如下内容
third-party:
gitee:
clientId: please replace it with your own
clientSecret: please replace it with your own
call-back: http://localhost:9001/user/register/gitee
token-url: https://gitee.com/oauth/token?grant_type=authorization_code&client_id=%s&client_secret=%s&redirect_uri=%s&code=%s
user-url: https://gitee.com/api/v5/user?access_token=%s
state: GITEE上述内容中clientId、clientSecret、call-back请换成实际场景下的真实值。
至此申请完毕!
4)账号密码登录
账号密码登录业务逻辑:
- 根据userName查询用户
- 若不存在,返回错误信息
- 若存在,刷新缓存用户的过期时间
- 返回user、token信息
在UserRegisterLoginService类中定义接口:
CommonResponse login(String username,String password); |
在UserRegisterLoginServiceImpl类中实现接口:
|
4.5.2 用户信息部分
首先创建接口(UserInfoService)及其实现类(UserInfoServiceImpl)
package com.juzi.user.service; |
1)查看手机绑定状态
查看手机绑定状态业务逻辑:
- 先从缓存查询用户信息,若能查找到,判断用户是否绑定手机
- 否则从数据库查询用户信息,判断用户是否绑定手机
在UserInfoService类中添加接口:
CommonResponse checkPhoneBindStatus(String personId); |
在UserInfoServiceImpl中实现接口:
|
2)用户绑定手机
用户绑定手机业务逻辑:
- 发送验证码,存储验证码(这步此处略过)
- 从Redis中查询验证码,与用户传入的验证码比对
- 若不一致,返回错误信息
- 若一致:
- 根据用户id查询用户信息
- 判断用户是否存在
- 若不存在:返回错误信息
- 若存在:
- 根据id修改用户手机号
- 返回成功信息
在User Repo类中添加修改手机号的方法:
|
在UserInfoService中定义接口:
CommonResponse bindPhone(String personId,String phoneNumber,String code); |
在UserInfoServiceImpl中实现接口:
|
4.6 用户HTTP接口层
UserRegisterLoginController.java
package com.juzi.user.controller; |
UserInfoController.java
package com.juzi.user.controller; |
DemoController.java
,该接口用于测试gateway网关的拦截
package com.juzi.user.controller; |
4.7 用户模块启动类
package com.juzi.user; |
五、网关模块
5.1 搭建环境
创建Gateway模块,pom.xml
核心内容:
<properties> |
PS:上述依赖中io.netty.netty-resolver-dns-native-macos依赖需要根据实际环境添加
除了使用open-feign外,还可以使用Dubbo等RPC框架
application.yml
server: |
5.2 环境配置
网关层是需要调用远程接口,此处我们使用OpenFeign,接下来需要配置Feign,主要是完成对消息的解析
FeignConfig.java
package com.juzi.gateway.feign.config; |
5.3 远程接口定义
网关层面需要调用OAuth2的校验token接口:
OAuth2ServerClient
package com.juzi.gateway.feign.api; |
注意FeignClient注解中定义的name属性要和OAuth2模块的名称一致,最终是依靠注册中心Nacos来实现RPC的。
5.4 网关层过滤器
AuthFilter.java
package com.juzi.gateway.feign.filter; |
定义路径白名单,直接放行的路径;同时需要对需要校验的路径进行OAuth2的token检验。
📢:Feign接口是需要进行懒加载,否则和Gateway启动过程会产生死锁。
5.5 启动类
GatewayApplication.java
package com.juzi.gateway; |
注意:需要标注@EnableFeignClients注解,开启Feign的客户端调用。
六、Postman测试
6.1 预先启动环境
需要先启动Nacos集群(至少一个节点存活)、Redis、MySQL
6.2 启动项目
- Gateway
- OAuth2
- User
6.3 Postman接口测试
接口一:用户账号密码注册
注册接口:
校验token接口,拿着上一步返回的access_token:
数据库信息:
接口二:手机号验证码注册
首先需要在Redis中存入事先的手机号和验证码,此处我存入的手机号是18266668888,验证码是666666
注册接口:
校验token:……
数据库信息:……
接口三:第三方平台注册
首先我们需要在浏览器地址栏输入以下网址:
https://gitee.com/oauth/authorize/?client_id=%s&redirect_uri=http://localhost:9001/user/register/gitee&response_type=code&state=GITEE |
其中的client_id为之前申请的client_id,替换即可,redirect_uri需要根据实际的情形调整
请求之后会跳转到以下页面:
在点击同意授权之后,就会请求redirect_uri,并返回如下信息:
token校验:……
数据库数据:
接口四:用户账号密码登录
登录接口:
token校验:……
接口五:查看手机绑定状态
接口:
注意📢:上述的personId在本次代码中是以用户在数据库中的id + 10000000组成的,具体的需要根据代码逻辑调整
接口六:用户绑定手机
接口:
数据库信息:
接口七:测试用户服务有无token的情况
1)不携带token的情况
2)携带token
七、写在最后
除上述模块之外,还可以加入canel,实现延迟双删,实现数据双写一致性。具体的可以查看https: //github.com/yoyocraft/oauth2-demo/tree/master/canal-service。
上述只是OAuth2实践的一种形式,还有很多其他的形式,多阅读、多实践!