告别重复登录!手把手教你用OAuth2实现丝滑单点登录
什么是单点登录?
单点登录(Single Sign On),简称SSO,定义是在多个应用系统中,用户只需登录一次就可以访问所有相互信任的应用系统。
为什么需要做单点登录系统呢?
-
阿里系的淘宝和天猫,显而易见这是两个系统,但是在使用过程中,只要你登录了淘宝,同时也意味着登录了天猫,如果每个子系统都需要登录认证,用户早就疯了,所以单点登录解决的问题就是,用户只需要登录一次就可以访问所有相互信任的应用系统(提升了用户体验,同时减少了需要管理多个账户密码的麻烦)。
-
对于公司内部来说,所有的子系统都使用了一套认证授权流程,其他系统不用重复开发登录模块,节省了开发成本;
同父域SSO(同一级域名不同二级域名)
具有相同一级域名的多个网站,以百度为例,他们都有一个一级域名(baidu.com),当第一次登录完成后,会将用户信息存储于Cache中间件(Redis)上 , 使用将身份唯一标识写入Cookie中,域名指定为baidu.com,那么在访问百度其他子系统时的时候就会带有此标识,通过标识去redis中获取用户信息,从而实现子系统免登。
这种方式虽然实现简单,但是不能实现跨父域实现免登。
非同一父域名的情况下,该如何实现单点登录?
统一认证中心实现单点登录
由图可知,通过统一认证授权方式实现单点登录,需要有一个独立的认证系统。
用户第一次访问应用系统时,由于还未登录,被引导到认证系统中进行登录,认证系统接受用户名密码等安全信息,生成访问令牌(ticket)。用户通过ticket访问应用系统,应用系统接受到请求之后会访问认证系统检查ticket的合法性,如果检查通过,用户就可以在不用再次登录的情况下访问应用系统资源。
什么是oauth2?
Oauth是一个开发标准(协议),该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无需将用户名和密码提供给第三方应用,实现这一功能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据,Oauth2 是 Oauth协议的下一版本,但不向下兼容Oauth 1.0;
一、应用场景
场景一:用户登录京东,可以使用qq或微信登录,两家公司的数据如何互通?
场景二:用户登录淘宝后,无需登录就可访问旗下的多个网站
为什么要用Oauth2?
- 让用户跳过繁琐的注册过程,提高用户体验;
- 采用令牌的方式可以让用户灵活的对第三方应用授权或者收回权限(1. token是有有效期的,2.可以划定权限范围);
- 传统的Web开发登录认证一般都是基于session的,但是在前后端分离的架构中继续使用session就会有许多不便,因为移动端(Android、IOS、小程序等)要么不支持cookie, 要么使用非常不便;
二、名词定义
-
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。oAuth是Open Authorization的简写。
(1)Client:客户端(client),即你访问的第三方应用(京东)。
(2)Resource Owner:资源所有者,即用户。
(3)Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器(微信)。
(4)Resource server:资源服务器,即服务提供商(微信)存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
Oauth2与sso的区别和联系(三方登录和单点登录)
oauth | sso | |
---|---|---|
区别 | 开放授权,允许第三方应用代表用户获得访问权限。可以作为web应用、桌面应用和手机等设备提供专门的认证流程。例如,用qq账号登录豆瓣、美团、大众点评;用支付宝账号登录淘宝、天猫等。 | sso多用于多个应用之间的切换,例如百度论坛、百度知道、百度云、百度文库等,在其中一个系统中登录, 切换到另一个系统的时候,不必再次输入用户名密码 |
一处注册,处处注册(不同公司账号互通) | 一次登录,处处登录(同个公司不同系统) | |
联系 | 通过Oauth2的授权码模式可以实现sso单点登录**;** |
三、OAuth的思路
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。“客户端"不能直接登录"服务提供商”,只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。
四、运行流程
OAuth 2.0的运行流程如下图,摘自RFC 6749。
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
不难看出来,上面六个步骤之中,B是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源。
下面一一讲解客户端获取授权的四种模式。
五、oAuth2.0的授权模式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
1.用户访问应用前端页面(验证是否登录)。
2.访问的页面将请求重定向到认证服务器。
3.认证服务器等待用户授权(输入账号、密码)。
4.用户授权,认证服务器验证(Clinet_id和身份认证),生成一个code(授权码)--->认证前端--->应用前端--->应用服务器。
5.应用服务器使用client_id、client_secret、code去认证服务器获取token,refresh_token(我们默认应用服务器和认证服务器之间的通信是安全的)。
6.然后,应用服务器拿到code, 并用client_id去后台查询对应的client_secret
7.验证token,访问真正的资源页面
1.用户访问应用页面,(验证是否已登录,如果未登录)--->重定向到认证服务器页面
2.认证服务器给用户一个认证页面,等待用户授权(用户输入账号密码)
3.认证中心前端把数据返回其服务器,验证其账号和密码,返回前端token
4.认证中心前端重定向(重定向地址是第二步传入的)返回应用前端,把数据--->应用前端。
5.应用前端使用token访问应用的服务器,--->验证token是否合法or过期。
6.如果token验证通过,可以成功访问资源。
1.用户访问用页面时,输入第三方认证所需要的信息(认证中心账号密码),直接在应用的前端页面输入账号密码(不安全,需要对这个第三方极度的信任)
2.应用服务器使用账号密码,访问认证中心来获取token
3.认证服务器授权通过,拿到token,验证token,访问真正的资源页面
1.用户访问应用客户端
2.通过客户端定义的验证方法,拿到token,(无需用户授权)
3.验证token,访问资源
在这种模式下,已经与用户没有关系了,单纯的数据客户端以自己的名义要求资源提供商提供服务,不存在授权问题。
通过Oauth2(授权码模式)实现单点登录
A系统(单点登录客户端)首次访问受保护的资源触发单点登录流程说明
1、用户通过浏览器访问A系统被保护的资源链接
2、A系统判断当前会话是否登录,如果没有登录则跳转到A系统登录地址/login
3、A系统首次接收到/login请求时没有state和code参数,此时A系统拼接系统配置的单点登录服务器授权url(oauth/),并重定向至授权链接。
4、单点登录服务器判断此会话是否登录,如果没有登录,那么返回单点登录服务器的登录页面。
5、用户在登录页面填写用户名、密码等信息执行登录操作。
6、单点登录服务器校验用户名、密码并将登录信息设置到上下文会话中。
7、单点登录服务器重定向到A系统的/login链接,此时链接带有code和state参数。
8、A系统再次接收到/login请求,此请求携带state和code参数,系统A通过OAuth2RestTemplate请求单点登录服务端/oauth/token接口获取token。
9、A系统获取到token后,首先会对token进行解析,并使用配置的公钥对token进行校验(非对称加密),如果校验通过,则将token设置到上下文,下次访问请求时直接从上下文中获取。
10、A系统处理完上下问会话之后重定向到登录前请求的受保护资源链接。
B系统(单点登录客户端)访问受保护的资源流程说明
1、用户通过浏览器访问B系统被保护的资源链接
2、B系统判断当前会话是否登录,如果没有登录则跳转到B系统登录地址/login
3、B系统首次接收到/login请求时没有state和code参数,此时B系统拼接系统配置的单点登录服务器授权url,并重定向至授权链接。
4、单点登录服务器判断此会话是否登录,因上面访问A系统时登陆过,所以此时不会再返回登录界面。
5、单点登录服务器重定向到B系统的/login链接,此时链接带有code和state参数。
6、B系统再次接收到/login请求,此请求携带state和code参数,系统B通过OAuth2RestTemplate请求单点登录服务端/oauth/token接口获取token。
7、B系统获取到token后,首先会对token进行解析,并使用配置的公钥对token进行校验(非对称加密),如果校验通过,则将token设置到上下文,下次访问请求时直接从上下文中获取。
8、B系统处理完上下问会话之后重定向到登录前请求的受保护资源链接。
spring-security-oauth2 单点登录代码实现流程说明:
1、用户通过浏览器访问单点登录被保护的资源链接
2、SpringSecurity通过上下文判断是否登录(SpringSecurity单点登录服务端和客户端默认都是基于session的),如果没有登录则跳转到单点登录客户端地址/login
3、单点登录客户端OAuth2ClientAuthenticationProcessingFilter拦截器通过上下文获取token,因第一次访问单点登录客户端/login时,没有code和state参数,所以抛出UserRedirectRequiredException异常
4、单点登录客户端捕获UserRedirectRequiredException异常,并根据配置文件中的配置,组装并跳转到单点登录服务端的授权链接/oauth/authorize,链接及请求中会带相关配置参数
5、单点登录服务端收到授权请求,根据session判断是否此会话是否登录,如果没有登录则跳转到单点登录服务器的统一登录界面(单点登录服务端也是根据session判断是否登录的,在这里为了解决微服务的session集群共享问题,引入了spring-session-data-redis)
6、用户完成登录操作后,单点登录服务端重定向到单点登录客户端的/login链接,此时链接带有code和state参数
7、再次用到第三步的OAuth2ClientAuthenticationProcessingFilter拦截器通过上下文获取token,此时上下文中肯定没有token,所以会通过OAuth2RestTemplate请求单点登录服务端/oauth/token接口使用重定向获得的code和state换取token
8、单点登录客户端获取到token后,首先会对token进行解析,并使用配置的公钥对token进行校验(非对称加密),如果校验通过,则将token设置到上下文,下次访问请求时直接从上下文中获取。
9、单点登录客户端处理完上下问会话之后重定向到登录前请求的受保护资源链接。
oauth2环境搭建
一、oauth表结构说明
1.oauth_client_details【核心表】
存储客户端的配置信息
字段名 | 字段说明 |
---|---|
client_id | 主键,必须唯一,不能为空. 用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务 端自动生成). 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫 appKey,与client_id是同一个概念. |
resource_ids | 客户端所能访问的资源id集合,多个资源时用逗号(,)分隔,如: “unity-resource,mobile- resource”. 该字段的值必须来源于与security.xml中标签?oauth2:resource-server的属性 resource-id值一致. 在security.xml配置有几个?oauth2:resource-server标签, 则该字段可以 使用几个该值. 在实际应用中, 我们一般将资源进行分类,并分别配置对应 的?oauth2:resource-server,如订单资源配置一个?oauth2:resource-server, 用户资源又配置 一个?oauth2:resource-server. 当注册客户端时,根据实际需要可选择资源id,也可根据不同的 注册流程,赋予对应的资源id. |
client_secret | 用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成). 对于不同的 grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是 同一个概念. |
scope | 指定客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔,如: “read,write”. scope的值与security.xml中配置的?intercept-url的access属性有关系. 如?intercept-url的配置为?intercept-url pattern=“/m/**” access=“ROLE_MOBILE,SCOPE_READ”/>则说明访问该URL时的客户端必须有read权限范 围. write的配置值为SCOPE_WRITE, trust的配置值为SCOPE_TRUST. 在实际应该中, 该值一 般由服务端指定, 常用的值为read,write. |
authorized_grant_types | 指定客户端支持的grant_type,可选值包括 authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个 grant_type用逗号(,)分隔,如: “authorization_code,password”. 在实际应用中,当注册时,该字 段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: “authorization_code,refresh_token”(针对通过浏览器访问的客户端); “password,refresh_token”(针对移动设备的客户端). implicit与client_credentials在实际中 很少使用. |
web_server_redirect_uri | 客户端的重定向URI,可为空, 当grant_type为authorization_code或implicit时, 在Oauth的流 程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:当 grant_type=authorization_code时, 第一步 从 spring-oauth-server获取 'code’时客户端发 起请求时必须有redirect_uri参数, 该参数的值必须与 web_server_redirect_uri的值一致. 第 二步 用 ‘code’ 换取 ‘access_token’ 时客户也必须传递相同的redirect_uri. 在实际应用中, web_server_redirect_uri在注册时是必须填写的, 一般用来处理服务器返回的code, 验证 state是否合法与通过code去换取access_token值.在spring-oauth-client项目中, 可具体参考 AuthorizationCodeController.java中的authorizationCodeCallback方法.当 grant_type=implicit时通过redirect_uri的hash值来传递access_token值. 如:http://localhost:7777/spring-oauth-client/implicit#access_token=dc891f4a-ac88- 4ba6-8224-a2497e013865&token_type=bearer&expires_in=43199然后客户端通过JS等从 hash值中取到access_token值. |
authorities | 指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔, 如: "ROLE_ |
access_token_validity | 设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间 值(60 * 60 * 12, 12小时). 在服务端获取的access_token JSON数据中的expires_in字段的值 即为当前access_token的有效时间值. 在项目中, 可具体参考DefaultTokenServices.java中属 性accessTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端 自定义.refresh_token_validity 设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天). 若客户端的grant_type不包 括refresh_token,则不用关心该字段 在项目中, 可具体参考DefaultTokenServices.java中属 性refreshTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端 自定义. |
additional_information | 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的 数据,如:{“country”:“CN”,“country_code”:“086”}按照spring-security-oauth项目中对该字段 的描述 Additional information for this client, not need by the vanilla OAuth protocol but might be useful, for example,for storing descriptive information. (详见 ClientDetails.java的getAdditionalInformation()方法的注释)在实际应用中, 可以用该字段来 存储关于客户端的一些其他信息,如客户端的国家,地区,注册时的IP地址等等.create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
autoapprove | 设置用户是否自动Approval操作, 默认值为 ‘false’, 可选值包括 ‘true’,‘false’, ‘read’,‘write’. 该 字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为’true’或 支持的scope值,则会跳过用户Approve的页面, 直接授权. 该字段与 trusted 有类似的功能, 是 spring-security-oauth2 的 2.0 版本后添加的新属性. 在项目中,主要操作 oauth_client_details表的类是JdbcClientDetailsService.java, 更多的细节请参考该类. 也可 以根据实际的需要,去扩展或修改该类的实现. |
2.oauth_approvals
存储用户的授权信息
字段名 | 字段说明 |
---|---|
userId | 登录的用户名 |
clientId | 客户端ID |
scope | 申请的权限范围 |
status | 状态(Approve或Deny) |
expiresAt | 过期时间 |
lastModifiedAt | 最终修改时间 |
3.oauth_client_token
该表用于在客户端系统中存储从服务端获取的token数据
在spring-oauth-server项目中未使用到. 对oauth_client_token表的主要操作在JdbcClientTokenServices.java类中, 更多的细节请参考该类.
字段名 | 字段说明 |
---|---|
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
token_id | 从服务器端获取到的access_token的值. |
token | 这是一个二进制的字段, 存储的数据是OAuth2AccessToken.java对象序列化后的二进制数据. |
authentication_id | 该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultClientKeyGenerator.java类. |
user_name | 登录时的用户名 |
client_id |
4.oauth_access_token
存储生成的令牌信息
字段名 | 字段说明 |
---|---|
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
token_id | 从服务器端获取到的access_token的值. |
token | 这是一个二进制的字段, 存储的数据是OAuth2AccessToken.java对象序列化后的二进制数据. |
authentication_id | 该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultClientKeyGenerator.java类. |
user_name | 登录时的用户名 |
client_id | |
authentication | 存储将OAuth2Authentication.java对象序列化后的二进制数据. |
refresh_token | 该字段的值是将refresh_token的值通过MD5加密后存储的. 在项目中,主要操作oauth_access_token表的对象是JdbcTokenStore.java. 更多的细节请参考该类 |
5.oauth_refresh_token
存储刷新令牌的refresh_token
字段名 | 字段说明 |
---|---|
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
token_id | 该字段的值是将refresh_token的值通过MD5加密后存储的. |
token | 存储将OAuth2RefreshToken.java对象序列化后的二进制数据 |
authentication | 存储将OAuth2RefreshToken.java对象序列化后的二进制数据 |
6.oauth_code
存储授权码信息与认证信息,即只有grant_type为authorization_code时,该表才会有数据
字段名 | 字段说明 |
---|---|
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
code | 存储服务端系统生成的code的值(未加密). |
authentication | 存储将AuthorizationRequestHolder.java对象序列化后的二进制数据. |
二、OAuth2.0认证中心搭建
1.目录结构

2.添加依赖
<!-- Spring Security OAuth2-->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--扩展包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<!--阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
3.yml文件
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8080
servlet:
# 应用的访问路径
context-path: /auth-server
session:
cookie:
name: auth-server
path: /
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
url: jdbc:mysql://192.168.1.152:3306/lx-oauth2?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&useAffectedRows=true&allowMultiQueries=true
username: root
password: root
4.Spring Security配置
/**
* spring-security配置类
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin()
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
4.自定义认证类
@Slf4j
@Component
public class UserServiceImpl implements UserDetailsService {
@Autowired
private ISysLoginService sysLoginService;
/**
* 根据用户名获取用户信息、角色、权限信息
*
* @param username 用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StringUtils.isEmpty(username)) {
throw new UsernameNotFoundException("用户名不能为空!");
}
log.info("当前登录用户名:【{}】", username);
// 获取用户角色信息
QueryWrapper<SysLoginEntity> userWrapper = new QueryWrapper<>();
userWrapper.eq("user_code", username);
SysLoginEntity sysLogin = sysLoginService.getOne(userWrapper);
if (sysLogin == null) {
throw new UsernameNotFoundException("账号或密码错误!");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_PRODUCT_ADMIN");
authorities.add(authority);
return new User(sysLogin.getUserCode(), new BCryptPasswordEncoder().encode(sysLogin.getPassword()), authorities);
}
}
4.Oauth配置类
@Configuration
@EnableAuthorizationServer
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean
public ClientDetailsService clientDetailsService1() {
return new JdbcClientDetailsService(dataSource);
}
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
//授权码模式数据来源
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new JdbcAuthorizationCodeServices(dataSource);
}
@Bean
public ApprovalStore approvalStore(){
return new JdbcApprovalStore(dataSource);
}
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.checkTokenAccess("isAuthenticated()");//
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService1());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.authorizationCodeServices(authorizationCodeServices())
.approvalStore(approvalStore())
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.authenticationManager(authenticationManager);
}
}
三、应用服务搭建
1.目录结构

2.添加依赖
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
3.yml文件
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8081
servlet:
# 应用的访问路径
context-path: /auth-resource
session:
cookie:
name: auth-resource
path: /
security:
oauth2:
client:
client-id: MemberOrder
client-secret: 123456
# 访问令牌获取 URL
access-token-uri: http://localhost:8080/auth-server/oauth/token
user-authorization-uri: http://localhost:8080/auth-server/oauth/authorize
resource:
token-info-uri: http://localhost:8080/auth-server/oauth/check_token
4.Spring Security配置
/**
* spring-security配置类
*/
@Configuration
@EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()//.antMatchers("oauth/**").permitAll()
.anyRequest().authenticated();
}
}
oAuth2.0的四种授权模式测试
1.授权码(authorization code)
1.1申请授权码
http://localhost:8080/auth-server/oauth/authorize?response_type=code&client_id=codeTest&redirect_uri=https://www.baidu.com&scope=all
1.2返回结果
https://www.baidu.com/?code=e5wZHy
1.3请求令牌
http://localhost:8080/oauth/token?grant_type=authorization_code&client_id=codeTest&client_secret=order123&code=WIiuMh&redirect_uri=https://www.baidu.com
1.4认证中心颁发令牌
{
"access_token": "94da108a-657d-4dce-a323-303c15a1131b",
"token_type": "bearer",
"refresh_token": "6876f080-acc8-4a51-8ec3-d862736bc228",
"expires_in": 604562,
"scope": "all",
"addtion_mobile": "135xxxxxxxx",
"user_id": 1,
"username": "admin"
}
2.隐藏式(implicit)
2.1申请授权接口
http://localhost:8080/oauth/authorize?response_type=token&client_id=codeTest&redirect_uri=https://www.baidu.com&scope=all
2.2认证中心直接返回结果
https://www.baidu.com/#access_token=94da108a-657d-4dce-a323-303c15a1131b&token_type=bearer&expires_in=604497&addtion_mobile=135xxxxxxxx&user_id=1&username=admin
3.密码式(resource owner password credentials)
3.1通过用户名和密码直接请求token接口
http://localhost:8080/oauth/token?grant_type=password&client_id=codeTest&username=admin&password=admin123&scope=all&client_secret=order123
3.2认证中心直接返回令牌
{
"access_token": "94da108a-657d-4dce-a323-303c15a1131b",
"token_type": "bearer",
"refresh_token": "6876f080-acc8-4a51-8ec3-d862736bc228",
"expires_in": 604393,
"scope": "all",
"addtion_mobile": "135xxxxxxxx",
"user_id": 1,
"username": "admin"
}
4.客户端模式(client credentials)
4.1请求获取token接口
http://localhost:8080/oauth/token?grant_type=client_credentials&client_id=codeTest&client_secret=order123
4.2返回结果
{
"access_token": "e597eed0-d721-4d92-82e9-ef254b74faca",
"token_type": "bearer",
"expires_in": 604799,
"scope": "all"
}
6.令牌的使用
6.1请求
http://localhost:8080/auth-server/oauth/check_token?token=e0256c17-14ab-4ce8-a050-f8e6174caab8
6.2返回结果
{
"exp": 1679108950,
"user_name": "admin",
"authorities": [
"ROLE_PRODUCT_ADMIN"
],
"client_id": "codeTest1",
"scope": [
"all"
]
}
7.更新令牌
7.1请求:
http://localhost:8080/auth-server/oauth/token?grant_type=refresh_token&client_id=codeTest&client_secret=order123&refresh_token=6876f080-acc8-4a51-8ec3-d862736bc228
7.2返回结果
{
"access_token": "b98d49bc-ab1e-45ce-87b0-3ff9e2d5393a",
"token_type": "bearer",
"refresh_token": "ae642a16-3b83-4132-92cd-9384cf811753",
"expires_in": 604799,
"scope": "all",
"addtion_mobile": "135xxxxxxxx",
"user_id": 1,
"username": "admin"
}
通过上述流程,我们发现用户的认证实际上还是通过security的认证流程实现,通过session的方式,那如果是前后端分离,前端是小程序的时候呢?
JWT的组成
- JWT token的格式:header.payload.signature;
- header中用于存放签名的生成算法;
{
"alg": "HS256",
"typ": "JWT"
}
- payload中用于存放数据,比如过期时间、用户名、用户所拥有的权限等;
{
"exp": 1572682831,
"user_name": "macro",
"authorities": [
"admin"
],
"jti": "c1a0645a-28b5-4468-b4c7-9623131853af",
"client_id": "admin",
"scope": [
"all"
]
}
- signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败。
JWT实例
- 这是一个JWT的字符串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzI2ODI4MzEsInVzZXJfbmFtZSI6Im1hY3JvIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiYzFhMDY0NWEtMjhiNS00NDY4LWI0YzctOTYyMzEzMTg1M2FmIiwiY2xpZW50X2lkIjoiYWRtaW4iLCJzY29wZSI6WyJhbGwiXX0.x4i6sRN49R6JSjd5hd1Fr2DdEMBsYdC4KB6Uw1huXPg
- 可以在该网站上获得解析结果:jwt.io/
demo改造为下发jwt令牌
认证中心:
/**
* 密钥
*/
private final static String key = "123456";
@Bean
public TokenStore tokenStore() {
//return new JdbcTokenStore(dataSource);
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 令牌增强
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(key);
return jwtAccessTokenConverter;
}
//AuthorizationServerEndpointsConfigurer配置下增加如下
.tokenEnhancer(jwtAccessTokenConverter())
//AuthorizationServerSecurityConfigurer配置增加
.tokenKeyAccess("permitAll()")
资源服务器:
yml增加:
jwt:
key-uri: http://localhost:8080/auth-server/oauth/token_key