OAuth2.0 实现单点登录

article/2025/10/4 19:25:06

文章目录

  • OAuth2.0 实现单点登录
    • 一、四种授权模式
    • 二、搭建验证服务器
    • 三、使用 Postman 接口测试
    • 四、基于 @EnableOAuth2Sso 实现
    • 五、基于 @EnableResourceServer 实现 🚩
      • 🍬5.1 资源服务器
      • 🍬5.2 客户端访问
      • 🍬5.3 解决远程调用
    • 六、使用 JWT 存储 Token
      • 🍒6.1 验证服务器
      • 🍒6.2 资源服务器
      • 🍒6.3 令牌中继 (远程调用)
  • 总结


提示:以下是本篇文章正文内容,SpringCloud 系列学习将会持续更新

OAuth2.0 实现单点登录

注意: 第一次接触可能会比较难,不太好理解,需要多实践和观察。

前面我们虽然使用了统一存储来解决 Session 共享问题,但是我们发现就算实现了 Session 共享,依然存在一些问题,由于我们每个服务都有自己的验证模块,实际上整个系统是存在冗余功能的、同时还有我们上面出现的问题,那么能否实现只在一个服务进行登录,就可以访问其他的服务呢?
在这里插入图片描述
实际上之前的登录模式称为多点登录,而我们希望的是实现单点登陆,因此,我们得找一个更好的解决方案。

这里我们首先需要了解一种全新的登录方式:OAuth 2.0。我们经常看到一些网站支持第三方登录,比如淘宝、咸鱼我们就可以使用支付宝进行登录,腾讯游戏可以用QQ或是微信登陆,以及微信小程序都可以直接使用微信进行登录。我们知道它们并不是属于同一个系统,比如淘宝和咸鱼都不属于支付宝这个应用,但是由于需要获取支付宝的用户信息,这时我们就需要使用 OAuth2.0 来实现第三方授权,基于第三方应用访问用户信息的权限(本质上就是给别人调用自己服务接口的权限),那么它是如何实现的呢?

一、四种授权模式

我们还是从理论开始讲解,OAuth 2.0一共有四种授权模式:

  1. 客户端模式(Client Credentials)
     这是最简单的一种模式,我们可以直接向验证服务器请求一个 Token(这里可能有些小伙伴对Token的概念不是很熟悉,Token 相当于是一个令牌,我们需要在验证服务器 (User Account And Authentication) 服务拿到令牌之后,才能去访问资源,比如用户信息、借阅信息等,这样资源服务器才能知道我们是谁以及是否成功登录了)

     当然,这里的前端页面只是一个例子,它还可以是其他任何类型的客户端,比如 App、小程序甚至是第三方应用的服务。
    在这里插入图片描述
     虽然这种模式比较简便,但是已经失去了用户验证的意义,压根就不是给用户校验准备的,而是更适用于服务内部调用的场景。

  2. 密码模式(Resource Owner Password Credentials)
     密码模式相比客户端模式,就多了用户名和密码的信息,用户需要提供对应账号的用户名和密码,才能获取到 Token。
    在这里插入图片描述
     虽然这样看起来比较合理,但是会直接将账号和密码泄露给客户端,需要后台完全信任客户端不会拿账号密码去干其他坏事,所以这也不是我们常见的。

  3. 隐式授权模式(Implicit Grant)
     首先用户访问页面时,会重定向到认证服务器,接着认证服务器给用户一个认证页面,等待用户授权,用户填写信息完成授权后,认证服务器返回 Token。
    在这里插入图片描述
     它适用于没有服务端的第三方应用页面,并且相比前面一种形式,验证都是在验证服务器进行的,敏感信息不会轻易泄露,但是 Token 依然存在泄露的风险。

  4. 授权码模式(Authrization Code)
     这种模式是最安全的一种模式,也是推荐使用的一种,比如我们手机上的很多 App 都是使用的这种模式。

     相比隐式授权模式,它并不会直接返回 Token,而是返回授权码,真正的 Token 是通过应用服务器访问验证服务器获得的。在一开始的时候,应用服务器(客户端通过访问自己的应用服务器来进而访问其他服务)和验证服务器之间会共享一个 secret,这个东西没有其他人知道,而验证服务器在用户验证完成之后,会返回一个授权码,应用服务器最后将授权码和 secret 一起交给验证服务器进行验证,并且 Token 也是在服务端之间传递,不会直接给到客户端。
    在这里插入图片描述
     这样就算有人中途窃取了授权码,也毫无意义,因为,Token 的获取必须同时携带授权码和 secret ,但是 secret 第三方是无法得知的,并且 Token 不会直接丢给客户端,大大减少了泄露的风险。

但是乍一看,OAuth 2.0 不应该是那种第三方应用为了请求我们的服务而使用的吗,而我们这里需要的只是实现同一个应用内部服务之间的认证,其实我也可以利用 OAuth2.0 来实现单点登录,只是少了资源服务器这一角色,客户端就是我们的整个系统,接下来就让我们来实现一下。

回到目录…

二、搭建验证服务器

第一步就是最重要的,我们需要搭建一个验证服务器,它是我们进行权限校验的核心,验证服务器有很多的第三方实现也有 Spring 官方提供的实现,这里我们使用 Spring 官方提供的验证服务器。

这里我们将最开始保存好的项目解压 (项目 Gitee 地址),首先我们在父项目中添加 SpringCloud 依赖版本管理:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.1</version><type>pom</type><scope>import</scope>
</dependency>

①接着创建一个新的项目模块 auth-service,添加依赖:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--  OAuth2.0依赖,不再内置了,所以得我们自己指定一下版本  --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version></dependency>
</dependencies>

②设置配置文件

server:port: 8500servlet:#为了防止一会在服务之间跳转导致Cookie打架(因为所有服务地址都是localhost,都会存JSESSIONID)#这里修改一下context-path,这样保存的Cookie会使用指定的路径,就不会和其他服务打架了#但是注意之后的请求都得在最前面加上这个路径context-path: /sso

③接着我们需要编写一下配置类,这里需要两个配置类,一个是 SecurityConfiguration 配置类:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().permitAll(); // 使用表单登录}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();auth.inMemoryAuthentication()   // 直接创建一个用户,懒得搞数据库了.passwordEncoder(encoder).withUser("test").password(encoder.encode("123456")).roles("USER");}@Bean   // 这里需要将AuthenticationManager注册为Bean,在OAuth配置中使用@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Bean@Overridepublic UserDetailsService userDetailsServiceBean() throws Exception {return super.userDetailsServiceBean();}
}

④编写 OAuth2Configuration 配置类

@EnableAuthorizationServer   //开启验证服务器
@Configuration
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {@Resourceprivate AuthenticationManager manager;@ResourceUserDetailsService service;private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();/*** 这个方法是对客户端进行配置,一个验证服务器可以预设很多个客户端,* 之后这些指定的客户端就可以按照下面指定的方式进行验证* @param clients 客户端配置工具*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory()   // 这里我们直接硬编码创建,当然也可以像Security那样自定义或是使用JDBC从数据库读取.withClient("web")   // 客户端名称,随便起就行.secret(encoder.encode("654321"))      // 只与客户端分享的secret,随便写,但是注意要加密.autoApprove(false)    // 自动审批,这里关闭,要的就是一会体验那种感觉.scopes("book", "user", "borrow")     // 授权范围,这里我们使用全部all.redirectUris("http://localhost:8081/login", "http://localhost:8082/login", "http://localhost:8083/login")// 这里重定向页面可以不写,不写默认所有页面都可以重定向。但是如果写了,就必须都写上,否则访问没有写的页面不会重定向而是报错.authorizedGrantTypes("client_credentials", "password", "implicit", "authorization_code", "refresh_token");//授权模式,一共支持5种,除了之前我们介绍的四种之外,还有一个刷新Token的模式//这里我们直接把五种都写上,方便一会实验,当然各位也可以单独只写一种一个一个进行测试//现在我们指定的客户端就支持这五种类型的授权方式了}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) {security.passwordEncoder(encoder)    // 编码器设定为BCryptPasswordEncoder.allowFormAuthenticationForClients()  // 允许客户端使用表单验证,一会我们POST请求中会携带表单信息.checkTokenAccess("permitAll()");     // 允许所有的Token查询请求}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.userDetailsService(service).authenticationManager(manager);// 由于SpringSecurity新版本的一些底层改动,这里需要配置一下authenticationManager,才能正常使用password模式}
}

⑤启动服务器
在这里插入图片描述

回到目录…

三、使用 Postman 接口测试

然后我们使用 Postman 进行接口测试:

①客户端模式(Client Credentials)

首先我们从最简单的客户端模式进行测试,客户端模式只需要提供 id 和 secret 即可直接拿到 Token,注意需要再添加一个 grant_type 来表明我们的授权方式,默认请求路径为 http://localhost:8500/sso/oauth/token
在这里插入图片描述

发起请求后,可以看到我们得到了 Token,它是以 JSON 格式给到我们的:
在这里插入图片描述

我们还可以访问 http://localhost:8500/sso/oauth/check_token?token= 来验证我们的 Token 是否有效:
在这里插入图片描述

可以看到 active 为 true,表示我们刚刚申请到的 Token 是有效的。
在这里插入图片描述

回到目录…

②密码模式(Resource Owner Password Credentials)

接着我们来测试一下第二种 password 模式,我们还需要提供具体的用户名和密码,授权模式定义为password 即可:
在这里插入图片描述

接着我们需要在请求头中添加 Basic 验证信息,这里我们直接填写 id 和 secret 即可:
在这里插入图片描述

可以看到在请求头中自动生成了 Basic 验证相关内容:
在这里插入图片描述

响应成功,得到 Token 信息,并且这里还多出了一个 refresh_token,这是用于刷新 Token 的,我们之后会进行讲解。
在这里插入图片描述

查询Token信息之后还可以看到登录的具体用户以及角色权限等。
在这里插入图片描述

回到目录…

③隐式授权模式(Implicit Grant)

接着我们来看隐式授权模式,这种模式我们需要在验证服务器上进行登录操作,而不是直接请求Token,验证登录请求地址:http://localhost:8500/sso/oauth/authorize?client_id=web&response_type=token

注意 response_type 一定要是 token 类型,这样才会直接返回 Token,浏览器发起请求后,可以看到熟悉而又陌生的界面,没错,实际上这里就是使用我们之前讲解的 SpringSecurity 进行登陆,当然也可以配置一下记住我之类的功能,这里就不演示了:
在这里插入图片描述

但是登录之后我们发现出现了一个错误:
在这里插入图片描述
这是因为登录成功之后,验证服务器需要将结果给回客户端,所以需要提供客户端的回调地址,这样浏览器就会被重定向到指定的回调地址并且请求中会携带 Token 信息,这里我们随便配置一个回调地址:

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("web").secret(encoder.encode("654321")).autoApprove(false).scopes("book", "user", "borrow").redirectUris("http://localhost:8081/login")   // 可以写多个,当有多个时需要在验证请求中指定使用哪个地址进行回调.authorizedGrantTypes("client_credentials", "password", "implicit", "authorization_code", "refresh_token");
}

接着重启验证服务器,再次访问:
在这里插入图片描述
可以看到这里会让我们选择哪些范围进行授权,就像我们在微信小程序中登陆一样,会让我们授予用户信息权限、支付权限、信用查询权限等,我们可以自由决定要不要给客户端授予访问这些资源的权限,这里我们全部选择授予:
在这里插入图片描述
授予之后,可以看到浏览器被重定向到我们刚刚指定的回调地址中,并且携带了 Token 信息,现在我们来校验一下看看:
在这里插入图片描述

可以看到,Token 也是有效的。

回到目录…

④授权码模式(Authrization Code)

最后我们来看看第四种最安全的授权码模式,这种模式其实流程和上面是一样的,但是请求的是 code 类型:http://localhost:8500/sso/oauth/authorize?client_id=web&response_type=code

可以看到访问之后,依然会进入到回调地址,但是这时给的就是授权码了,而不是直接给 Token,那么这个 Token 该怎么获取呢?
请添加图片描述

按照我们之前讲解的原理,我们需要携带授权码和 secret 一起请求,才能拿到 Token,正常情况下是由回调的服务器进行处理,这里我们就在 Postman 中进行,我们复制刚刚得到的授权码,接口依然是http://localhost:8500/sso/oauth/token
在这里插入图片描述
可以看到结果也是正常返回了 Token 信息:
在这里插入图片描述

这样我们四种最基本的 Token 请求方式就实现了。

最后还有一个是刷新令牌使用的,当我们的 Token 过期时,我们就可以使用这个 refresh_token 来申请一个新的 Token:
在这里插入图片描述

但是执行之后我们发现会直接出现一个内部错误:
在这里插入图片描述

在这里插入图片描述
查看日志发现,这里还需要在 Security 配置类中,单独配置一个 UserDetailsService,并将其实例注册为 Bean

@Bean
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {return super.userDetailsServiceBean(); // 上面已经配置好了
}

然后在 OAuth2 配置类的 Endpoint 中添加一条:

@Resource
UserDetailsService service;@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.userDetailsService(service) // 新增的,上面已经配置好了.authenticationManager(manager);
}

最后再次尝试刷新 Token:
在这里插入图片描述
OK,成功刷新 Token,返回了一个新的。

回到目录…

四、基于 @EnableOAuth2Sso 实现

前面我们将验证服务器已经搭建完成了,现在我们将我们的服务作为单点登录应用直接实现单点登录,SpringCloud 为我们提供了客户端的直接实现,我们只需要添加一个注解和少量配置即可将我们的服务作为一个单点登陆应用,使用的是第四种授权码模式

一句话来说就是,这种模式只是将验证方式由原本的默认登录形式改变为了统一在授权服务器登陆的形式。

①首先客户端添加依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version>
</dependency>

②在客户端的启动类上添加 @EnableOAuth2Sso 注解:

@EnableOAuth2Sso
@SpringBootApplication
public class BookApplication {public static void main(String[] args) {SpringApplication.run(BookApplication.class, args);}
}

③客户端的配置文件:

security:oauth2:client:#不多说了client-id: webclient-secret: 654321#Token获取地址access-token-uri: http://localhost:8500/sso/oauth/token#验证页面地址user-authorization-uri: http://localhost:8500/sso/oauth/authorizeresource:#Token信息获取和校验地址token-info-uri: http://localhost:8500/sso/oauth/check_token

④现在我们就开启图书服务,调用图书接口:http://localhost:8081/book/5,它会给我们重定向到 http://localhost:8500/sso/login
在这里插入图片描述

登录后,它又将我们重定向到该页面,我们可以给访问页面进行授权:
在这里插入图片描述

可以看到在发现没有登录验证时,会直接跳转到授权页面,进行授权登录,之后才可以继续访问图书服务:
在这里插入图片描述
那么用户信息呢?是否也一并保存过来了?我们这里直接获取一下 SpringSecurity 的 Context 查看用户信息:

@RequestMapping("/book/{bid}")
Book findBookById(@PathVariable("bid") int bid){//通过SecurityContextHolder将用户信息取出SecurityContext context = SecurityContextHolder.getContext();System.out.println(context.getAuthentication());return service.getBookById(bid);
}

可以看到信息:
在这里插入图片描述
这里使用的不是之前的UsernamePasswordAuthenticationToken也不是RememberMeAuthenticationToken,而是新的OAuth2Authentication,它保存了验证服务器的一些信息,以及经过我们之前的登陆流程之后,验证服务器发放给客户端的 Token 信息,并通过 Token 信息在验证服务器进行验证获取用户信息,最后保存到 Session 中,表示用户已验证,所以本质上还是要依赖浏览器存 Cookie 的。

但是我们发现一个问题,就是由于SESSION不同步,每次切换不同的服务进行访问都会重新导验证服务器去验证一次:
在这里插入图片描述
在这里插入图片描述
我们可以这样解决:
方案一:像之前一样做 SESSION 统一存储
方案二:设置 context-path 路径,每个服务单独设置,就不会打架了

但是这样依然没法解决服务间调用的问题,所以仅仅依靠单点登陆的模式不太行。
在这里插入图片描述

回到目录…

五、基于 @EnableResourceServer 实现 🚩

前面我们讲解了将我们的服务作为单点登陆应用直接实现单点登陆,那么现在我们如果是以第三方应用进行访问呢?这时我们就需要将我们的服务作为资源服务了,作为资源服务就不会再提供验证的过程,而是直接要求请求时携带 Token,而验证过程我们这里就继续用 Postman 来完成,这才是我们常见的模式。

一句话来说,跟上面相比,我们只需要携带 Token 就能访问这些资源服务器了,客户端被独立了出来,用于携带 Token 去访问这些服务。

🍬5.1 资源服务器

①首先客户端添加依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version>
</dependency>

②在客户端的启动类上添加 @EnableResourceServer 注解:

@EnableResourceServer
@SpringBootApplication
public class BookApplication {public static void main(String[] args) {SpringApplication.run(BookApplication.class, args);}
}

③客户端的配置文件:

security:oauth2:client:client-id: webclient-secret: 654321resource:# 因为资源服务器得验证你的Token是否有访问此资源的权限以及用户信息,所以只需要一个验证地址token-info-uri: http://localhost:8500/sso/oauth/check_token

回到目录…

🍬5.2 客户端访问

①配置完成后,我们启动服务器,直接访问会发现:
在这里插入图片描述

②这是由于我们的请求头中没有携带 Token 信息,现在有两种方式可以访问此资源:

  • 方案一:在 URL 后面添加 access_token 请求参数,值为 Token
    这种方式比较简单,我们可以通过 Postman 测试 密码模式 或 客户端模式,来获取 Token 值
    在这里插入图片描述

  • 方案二:在请求头中添加 Authorization,值为 Bearer + Token
    这种我们需要使用 Postman 来完成:
    在这里插入图片描述
    添加验证信息后,会帮助我们转换成请求头信息:
    在这里插入图片描述
    访问成功:
    在这里插入图片描述

③我们接着来看如何对资源服务器进行深度自定义,我们可以为其编写一个配置类,比如我们现在希望用户授权了某个 Scope 才可以访问此服务:

@Configuration
public class ResourceConfiguration extends ResourceServerConfigurerAdapter { //继承此类进行高度自定义@Overridepublic void configure(HttpSecurity http) throws Exception {  //这里也有HttpSecurity对象,方便我们配置SpringSecurityhttp.authorizeRequests().anyRequest().access("#oauth2.hasScope('book')");  //添加自定义规则//Token必须要有我们自定义scope授权才可以访问此资源}
}

在这里插入图片描述

可以看到当没有对应的 scope 授权时,那么会直接返回 insufficient_scope 错误:
在这里插入图片描述

④尝试获取 Session 中的 Security 信息:运行访问会报错

@GetMapping("/book/{bid}")
public Book findBookById(@PathVariable("bid") int bid, HttpSession session) {// 通过SecurityContextHolder将用户信息取出SecurityContext context = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");System.out.println(context.getAuthentication());return bookService.getBookById(bid);
}

不知道各位是否有发现,实际上资源服务器完全没有必要将 Security 的信息保存在 Session 中了,因为现在只需要将 Token 告诉资源服务器,那么资源服务器就可以联系验证服务器,得到用户信息,就不需要使用之前的 Session 存储机制了,所以你会发现 HttpSession 中没有 SPRING_SECURITY_CONTEXT,现在 Security 信息都是通过连接资源服务器获取。

回到目录…

🍬5.3 解决远程调用

方案一:使用 OAuth2RestTemplate 远程调用

之前我们使用的 RestTemplate 在服务调用时没有携带 Token 信息,我们得想个办法把用户传来的 Token 信息在进行远程调用时也携带上。因此,我们可以直接使用 OAuth2RestTemplate,它会在请求其他服务时携带当前请求的 Token 信息。它继承自 RestTemplate,这里我们直接定义一个 Bean

@Configuration
public class RestTemplateConfig {@ResourceOAuth2ClientContext context;@Bean@LoadBalanced  // 负载均衡public OAuth2RestTemplate restTemplate(){return new OAuth2RestTemplate(new ClientCredentialsResourceDetails(), context);}
}

接着我们直接替换掉之前的 RestTemplate 即可:

@Service
public class BorrowServiceImpl implements BorrowService {@Resourceprivate BorrowMapper borrowMapper;@Resourceprivate OAuth2RestTemplate restTemplate;@Overridepublic UserBorrowView getBorrowViewByUid(int uid) {List<Borrow> borrowList = borrowMapper.getBorrowsByUid(uid);User user = restTemplate.getForObject("http://userservice/user/" + uid, User.class);List<Book> bookList = borrowList.stream().map(b -> restTemplate.getForObject("http://bookservice/book/" + b.getBid(), Book.class)).collect(Collectors.toList());return new UserBorrowView(user, bookList);}
}

可以看到服务成功调用了:同样,请求头中必须携带 Token 信息
在这里插入图片描述

方案二:通过 Feign 实现远程调用

首先我们先把服务注册到 Nacos 中,再实现客户端接口 bookClientuserClient,最后调用客户端接口的方法 userClient.findUserById(uid) 实现远程调用。

配置完成之后,又出现刚刚的问题了,OpenFeign 也没有携带 Token 进行访问:
在这里插入图片描述

那么怎么配置 Feign 携带 Token 访问呢? 遇到这种问题直接去官方查:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#oauth2-support,非常简单,两个配置就搞定:

feign:oauth2:# 开启Oauth支持,这样就会在请求头中携带Token了enabled: true# 同时开启负载均衡支持load-balanced: true

重启服务器,可以看到结果OK了:
在这里插入图片描述

这样我们就成功将之前的三个服务作为资源服务器了,然后让 (浏览器、小程序、App、第三方服务等) 作为客户端来访问,并且也是需要先去验证服务器进行验证然后再通过携带 Token 进行访问,这种模式是我们比较常见的模式

回到目录…

六、使用 JWT 存储 Token

官网:https://jwt.io

JSON Web Token 令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑和自成一体的方式,用于在各方之间作为 JSON 对象安全地传输信息。这些信息可以被验证和信任,因为它是数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 进行公钥/私钥对进行签名。

实际上,我们之前都是携带 Token 向资源服务器发起请求后,资源服务器由于不知道我们 Token 的用户信息,所以需要向验证服务器询问此 Token 的认证信息,这样才能得到 Token 代表的用户信息,但是各位是否考虑过,如果每次用户请求都去查询用户信息,那么在大量请求下,验证服务器的压力可能会非常的大。而使用 JWT 之后,Token 中会直接保存用户信息,这样资源服务器就不再需要询问验证服务器,自行就可以完成解析,我们的目标是不联系验证服务器就能直接完成验证。

JWT 令牌的格式如下:
在这里插入图片描述

一个 JWT 令牌由三部分组成:标头(Header)、有效载荷(Payload) 和 签名(Signature)。在传输的时候,会将 JWT 的3部分分别进行 Base64 编码后进行连接形成最终需要传输的字符串。

  • 标头:包含一些元数据信息,比如 JWT 签名所使用的加密算法,还有类型,这里统一都是 JWT。
  • 有效载荷:包括用户名称、令牌发布时间、过期时间、JWT ID 等,当然我们也可以自定义添加字段,我们的用户信息一般都在这里存放。
  • 签名:首先需要指定一个密钥,该密钥仅仅保存在服务器中,保证不能让其他用户知道。然后使用 Header 中指定的算法对 Header 和 Payload 进行 base64 加密之后的结果通过密钥计算哈希值,然后就得出一个签名哈希。这个会用于之后验证内容是否被篡改。

知识补充

  1. Base64:就是包括小写字母a-z大写字母A-Z数字0-9符号"+""/" 一共 64 个字符的字符集(末尾还有1个或多个 = 用来凑够字节数),任何的符号都可以转换成这个字符集中的字符,这个转换过程就叫做 Base64编码,编码之后会生成只包含上述 64 个字符的字符串。相反,如果需要原本的内容,我们也可以进行 Base64解码,回到原有的样子。
    在这里插入图片描述
    注意:Base64 不是加密算法,只是一种信息的编码方式而已。

  2. 加密算法:加密算法分为对称加密和非对称加密,其中对称加密(Symmetric Cryptography)比较好理解,就像一把锁配了两把钥匙一样,这两把钥匙你和别人都有一把,然后你们直接传递数据,都会把数据用锁给锁上,就算传递的途中有人把数据窃取了,也没办法解密,因为钥匙只有你和对方有,没有钥匙无法进行解密,但是这样有个问题,既然解密的关键在于钥匙本身,那么如果有人不仅窃取了数据,而且对方那边的治安也不好,于是顺手就偷走了钥匙,那你们之间发的数据不就凉凉了吗。

    因此,非对称加密(Asymmetric Cryptography) 算法出现了,它并不是直接生成一把钥匙,而是生成一个公钥和一个私钥,私钥只能由你保管,而公钥交给对方或是你要发送的任何人都行,现在你需要把数据传给对方,那么就需要使用私钥进行加密,但是,这个数据只能使用对应的公钥进行解密,相反,如果对方需要给你发送数据,那么就需要用公钥进行加密,而数据只能使用私钥进行解密,这样的话就算对方的公钥被窃取,那么别人发给你的数据也没办法解密出来,因为需要私钥才能解密,而只有你才有私钥。

    因此,非对称加密的安全性会更高一些,包括HTTPS的隐私信息正是使用非对称加密来保障传输数据的安全(当然HTTPS并不是单纯地使用非对称加密完成的,感兴趣的可以去了解一下)

    对称加密和非对称加密都有很多的算法,比如对称加密,就有:DES、IDEA、RC2,非对称加密有:RSA、DAS、ECC

  3. 不可逆加密算法:常见的不可逆加密算法有MD5, HMAC, SHA-1, SHA-224, SHA-256, SHA-384, 和SHA-512, 其中SHA-224、SHA-256、SHA-384,和SHA-512我们可以统称为SHA2加密算法,SHA加密算法的安全性要比MD5更高,而SHA2加密算法比SHA1的要高,其中SHA后面的数字表示的是加密后的字符串长度,SHA1默认会产生一个160位的信息摘要。经过不可逆加密算法得到的加密结果,是无法解密回去的,也就是说加密出来是什么就是什么了。本质上,其就是一种哈希函数,用于对一段信息产生摘要,以防止被篡改。

    实际上这种算法就常常被用作信息摘要计算,同样的数据通过同样的算法计算得到的结果肯定也一样,而如果数据被修改,那么计算的结果肯定就不一样了。

这里我们就可以利用 jwt,将我们的 Token 采用新的方式进行存储:
在这里插入图片描述

回到目录…

🍒6.1 验证服务器

这里我们使用最简单的一种方式,对称密钥,我们需要对验证服务器的配置进行修改:

①向 SecurityConfiguration 配置类添加两个 Bean

@Bean("tokenConverter")
public JwtAccessTokenConverter tokenConverter(){  // Token转换器,将其转换为JWTJwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setSigningKey("lbwnb");   // 这个是对称密钥,一会资源服务器那边也要指定为这个return converter;
}@Bean
public TokenStore tokenStore(@Qualifier("tokenConverter") JwtAccessTokenConverter converter){  // Token存储方式现在改为JWT存储return new JwtTokenStore(converter);  // 传入刚刚定义好的转换器
}

②向 OAuth2Configuration 配置类添加两条配置方法:

@Resource
TokenStore store;
@Resource
JwtAccessTokenConverter converter;private AuthorizationServerTokenServices serverTokenServices(){  // 这里对AuthorizationServerTokenServices进行一下配置DefaultTokenServices services = new DefaultTokenServices();services.setSupportRefreshToken(true);   // 允许Token刷新services.setTokenStore(store);   // 添加刚刚的TokenStoreservices.setTokenEnhancer(converter);   // 添加Token增强,其实就是JwtAccessTokenConverter,增强是添加一些自定义的数据到JWT中return services;
}@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.tokenServices(serverTokenServices())   //设定为刚刚配置好的AuthorizationServerTokenServices.userDetailsService(service).authenticationManager(manager);
}

然后我们就可以重启验证服务器了:
在这里插入图片描述
可以看到成功获取了 AccessToken,但是这里的格式跟我们之前的格式就大不相同了,因为现在它是 JWT 令牌,我们可以对其进行一下 Base64解码:
在这里插入图片描述

🍒6.2 资源服务器

现在我们对三个资源服务器进行配置:将之前的 security 配置更换如下

security:oauth2:resource:jwt:key-value: lbwnb # 注意这里要跟验证服务器的密钥一致,这样算出来的签名才会一致

重启服务器,调用接口发现:访问成功!
在这里插入图片描述
在这里插入图片描述

🍒6.3 令牌中继 (远程调用)

如果我们直接用 JWT 进行远程调用时,就会发现如下报错:访问此资源需要完全身份验证

feign.FeignException$Unauthorized: [401] during [GET] to [http://userservice/user/1] [UserClient#findUserById(int)]: [{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}]

所以我们需要在 borrowservice 模块添加一个拦截器,配置令牌中继

@Configuration
public class RequestInterceptorConfig implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)SecurityContextHolder.getContext().getAuthentication().getDetails();requestTemplate.header("Authorization","Bearer" + details.getTokenValue());}
}

request Headers 标准的请求头

  • Authorization:在HTTP中,服务器可以对一些资源进行认证保护,如果你要访问这些资源,就要提供用户名和密码,这个用户名和密码就是在 Authorization 头中附带的,格式是 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9......

JWT授权为啥要在 Authorization标头里加个Bearer 呢?

  • 设计 API 授权,或者调用第三方 API 时,经常会接触到:Authorization : Bearer Token
  • 这是因为 W3C 的 HTTP 1.0 规范格式是:Authorization: <type> <authorization-parameters>

所以 Bearer 是授权的类型,常见的授权类型还有:

  • Basic 用于 http-basic 认证;
  • Bearer 常见于 OAuth 和 JWT 授权;
  • Digest MD5 哈希的 http-basic 认证 (已弃用)
  • AWS4-HMAC-SHA256 AWS 授权

JWT官方推荐格式:https://jwt.io/introduction

再次重启,访问资源:远程调用成功!
在这里插入图片描述

回到目录…

总结

简单地说,cookie、session、token (JWT)之间的关系?

  1. 当用户在浏览器登录一个网站后,服务器会生成一个 session,并将 Session ID 发给浏览器。
  2. 浏览器 (客户端) 下次访问该网站的任何页面时,会把 Session ID 放入 cookie 中,发给服务器。
  3. 服务器收到后,会查看内存中是否有该 session。如果有,则可以浏览,反之重新登录。但是这种模式只适合单服务器,多服务器就没法查了。如果做Session服务器又怕单点失效,集群效率又低。
  4. 此时,token 就可以解决这个问题。 当用户登录后,服务器直接提供给浏览器一个 token。下次浏览器访问任何一个服务时,只需要提供这个 token 就可以了。
  5. token 是经过服务器签名的,任何服务器都可以识别出这个签名。

OAuth2.0 如何实现第三方登录授权?

  1. 假如现在你想登录小红书,但又不想注册,你可以选择使用微信登录,此时就发起了 OAuth2.0 的授权流程。
  2. 此时,小红书会带着相关证明向微信申请获取该用户的资料许可。
  3. 微信审核通过后,会发给小红书一个 token 令牌。
  4. 小红书就可以拿着这个 token 向微信的数据服务器发送请求获取用户的昵称等资料,然后显示到页面中。

回到目录…


总结:
提示:这里对文章进行总结:
本文是对OAuth2.0的学习,了解了它的4种授权方式,学会了如何搭建验证服务器,使用 postman对四种模式作了接口测试。并且学会了资源服务器实现单点登录的两种方式,也成功解决了远程调用时没有携带 token 的问题。最后也学会了使用 JWT 存储 Token,大大提高了安全性。


http://chatgpt.dhexx.cn/article/tdOpBi91.shtml

相关文章

单点登录(SSO)-- 实现单点登录的几种方式

1.为什么需要单点登录 三个角度&#xff1a; 1.1 方便用户的使用&#xff1a;用户登录一次&#xff0c;可以使用不同的服务和页面&#xff0c;省了忘记密码的痛苦1.2 简化开发&#xff1a;SSO让开发人员只要开发一个通用的身份验证框架&#xff0c;就不用为身份验证操心了1.3 …

面试系列:单点登录的知识(一)

大家好&#xff0c;我是车辙&#xff0c;由于目前接手的业务涉及到了单点登录&#xff0c;所以一直在疯狂的去补充这方面的知识。也写下了这篇面试形式的文章&#xff0c;写的不好大家轻点 Diss。 面试开始 在焦急的等待中&#xff0c;一位看上去比较年轻的小伙子走了过来。我…

单点登录实现的几种方式及原理【单点登录】

文章目录 一、什么是单点登录二、单点登录原理三、单点登录实现方式1.基于CookieRedis的单点登录2.分布式session方式实现单点登录3.token验证4.session广播5.CAS 中央认证服务 一、什么是单点登录 单点登录的英文名叫做&#xff1a;Single Sign On&#xff08;简称SSO&#x…

什么是单点登录?怎么样实现单点登录

单点登录SSO&#xff08;Single Sign On&#xff09;说得简单点就是在一个多系统共存的环境下&#xff0c;用户在一处登录后&#xff0c;就不用在其他系统中登录&#xff0c;也就是用户的一次登录能得到其他所有系统的信任。单点登录在大型网站里使用得非常频繁&#xff0c;例如…

什么是单点登录

一、什么是单点登录&#xff1f; 单点登录的英文名叫做&#xff1a;Single Sign On&#xff08;简称SSO&#xff09;。 在初学/以前的时候&#xff0c;一般我们就单系统&#xff0c;所有的功能都在同一个系统上。 后来&#xff0c;我们为了合理利用资源和降低耦合性&#xff…

什么是单点登录?单点登录的三种实现方式

单点登录SSO&#xff08;Single Sign On&#xff09;说得简单点就是在一个多系统共存的环境下&#xff0c;用户在一处登录后&#xff0c;就不用在其他系统中登录&#xff0c;也就是用户的一次登录能得到其他所有系统的信任。单点登录在大型网站里使用得非常频繁&#xff0c;例如…

什么是单点登录(SSO)?单点登录(SSO)到底什么意思?【附逻辑Demo实例】

在程序开发中&#xff0c;特别是网站类开发&#xff0c;会接触到单点登录(SSO)&#xff0c;什么是单点登录&#xff1f;&#xff0c;单点登录(SSO)有什么用&#xff1f;下面软币网小编来来介绍一下&#xff1a; 一、什么是单点登录&#xff1f; 单点登录的英文名叫做&#xf…

分布式系统服务单点问题的探讨

分布式系统服务单点问题的探讨 无状态服务的单点问题有状态服务的单点问题 在分布式系统中&#xff0c;单点问题是一个比较常见的问题&#xff0c;对于单点问题可以分为有状态服务的单点问题和无状态服务的单点问题。 无状态服务的单点问题 对于无状态的服务&#xff0c;单点问…

08-微服务版单点登陆系统(SSO)实践

文章目录 单点登陆系统简介背景分析单点登陆系统概述单点登陆系统解决方案设计 单点登陆系统初步设计服务设计工程结构设计 SSO父工程创建及初始化创建父工程父工程pom文件初始配置 系统基础服务工程设计及实现业务描述表结构设计工程数据初始化创建系统服务工程并初始化Pojo对…

IDEA同时打开多个项目之解决方法

对于我们开发人员来说&#xff0c;有时想在一个窗口中打开多个项目&#xff0c;这时打开新项目时只有两个选项&#xff1a; 覆盖本窗口项目/在新窗口打开 解决方法&#xff1a; 1、点击左上角 File--->Project Structure&#xff1a; 2、然后选择Modules这一栏&#xff0c…

idea一个工作空间打开多个项目

很多时候&#xff0c;多个项目之间并不是parent和module的关系&#xff0c;如果不是parent和module的关系&#xff0c;idea一个工作空间只能打开一个项目&#xff0c;这样的话&#xff0c;如果有多个项目&#xff0c;来回切换视图让人烦不胜烦。下面介绍个在idea一个工作空间打…

Idea打开多个项目文件时,不自动识别maven项目

问题描述 idea打开多个项目文件&#xff08;直接打开的多个项目的文件夹&#xff09;&#xff0c;却没有识别出maven项目。网上查阅&#xff0c;解决方案为&#xff1a; 选中项目的pom.xml文件,右键-" add as maven project"。 但由于项目过多&#xff0c;不方便一个…

idea在一个窗口打开多个项目

1、.将两个项目或多个项目放在同一个文件夹下&#xff08;等同于eclipse中将多个项目放在工作空间workspace下&#xff09;,lz在D盘下创建了ideaCloudProject文件夹&#xff0c;并在该文件夹下创建了一个cloud项目和一个vue项目&#xff0c;如下图&#xff1a; 2、用idea打开该…

一个IDEA界面如何同时打开多个项目

第一步&#xff1a;先导入其中一个工程 第二步&#xff1a;点击File->Project Structure 第三步&#xff1a;导入模块 最后点击Apply即可完成一个IDEA界面同时打开多个项目的需求。

怎么在IDEA的一个窗口中打开多个项目

之前一直使用的是sts,最近比较有空&#xff0c;学习一下idea的使用。 习惯了eclipse,myeclipse,sts等工具&#xff0c;想在一个窗口管理多个项目&#xff0c;只用import就可以了。但今天使用Intellij IDEA时&#xff0c;新建了一个工程&#xff0c;然后我又想从github上把我以前…

IDEA中如何在一个窗口中打开多个项目

idea在一个窗口如何打开多个项目 在使用Spring进行微服务开发时&#xff0c;需要同时打开很多个项目&#xff0c;但是如果每一个项目都单独一个窗口的话&#xff0c;切换就很麻烦&#xff0c;最方便的就是在IDEA一个窗口中管理多个项目。 在IDEA之中&#xff0c;它的一个项目就…

idea同时打开多个项目

想在idea中同时打开多个项目&#xff0c;如下图 打开项目后一般无法识别多个文件&#xff0c;可以按照下面的步骤操作 查看项目中maven插件是否打开 查看maven是否配置 查看右侧是否有maven选项&#xff0c;如果没有就先添加maven选项&#xff0c;添加方案步骤提供下面三种方…

使用IDEA打开eclipse项目

文章目录 点击左上角的File→New→Project from Existing Sources。 选择到要打开的项目路劲&#xff0c;选择好后点击OK 先勾选 Import project from external model 后选择Eclipse后点击Next。 无需设置&#xff0c;直接点击Next。 继续点击Next。 继续Next。 选择运行项目的…

idea关闭多个项目合并在tab栏显示

macos bigsur使用新版idea 2021.2 打开多个项目合并到tab栏&#xff1a; 对于屏幕比较小多了一行感觉比较别扭&#xff0c;以及全屏下暂时没适应……好几次都感觉找不到桌面…… 关闭&#xff1a;这个关闭是mac级别的关闭&#xff0c;并不是idea的setting macos - 系统偏好 …

IDEA设置多窗口打开项目

问题&#xff1a; 解决IDEA打开项目总是在当前窗口下&#xff0c;无法同事打开两个窗口的问题。【可能是我自己默认配置的问题】 解决&#xff1a; 菜单栏File&#xff0c;进入Setting栏 如上图所示&#xff0c;配置即可~~ 转载于:https://www.cnblogs.com/hyc-ana/p/9187153.h…