Oauth2实现分布式鉴权
为什么需要oauth2?
编码永远都是为了解决生产中的问题,想要理解为什么需要OAuth2,当然要从实际生活出发。
举个例子:小区的业主点了一份外卖,但是小区的门禁系统不给外卖人员进入,此时想要外卖员进入只能业主下来开门或者告知门禁的密码。
密码告知外卖员岂不是每次都能凭密码进入小区了,这明显造成了安全隐患。
那么有没有一种方案:既能不泄露密码,也能让外卖小哥进入呢?
于是此时就想到了一个授权机制,分为以下几个步骤:
- 门禁系统中新增一个授权按钮,外卖小哥只需要点击授权按钮呼叫对应业主
- 业主收到小哥的呼叫,知道小哥正在要求授权,于是做出了应答授权
- 此时门禁系统弹出一个密码(类似于access_token),有效期30分钟,在30分钟内,小哥可以凭借这个密码进入小区。
- 小哥输入密码进入小区
另外这个授权的密码不仅可以通过门禁,还可以通过楼下的门禁,这就非常类似于网关和微服务了。
令牌和密码的区别?
上述例子中令牌和密码的作用是一样的,都可以进入小区,但是存在以下几点差异:
- 时效不同:令牌一般都是存在过期时间的,比如30分钟后失效,这个是无法修改的,除非重新申请授权;而密码一般都是永久的,除非主人去修改
- 权限不同:令牌的权限是有限的,比如上述例子中,小哥获取了令牌,能够打开小区的门禁、业主所在的楼下门禁,但是可能无法打开其它幢的门禁;
- 令牌可以撤销:业主可以撤销这个令牌的授权,一旦撤销了,这个令牌也就失效了,无法使用;但是密码一般不允许撤销。
什么是OAuth2?
OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无需将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。
采用令牌(token)的方式可以让用户-灵活的对第三方应用授权或者收回权限。
OAuth2 是 OAuth 协议的下一版本,但不向下兼容 OAuth 1.0。
传统的 Web 开发登录认证一般都是 基于 session 的,但是在前后端分离的架构中继续使用 session 就会有许多不便,因为移动端(Android、iOS、微信小程序等)要么不支持 cookie(微信小程序),要么使用非常不便,对于这些问题,使用 OAuth2 认证都能解决。
对于大家而言,我们在互联网应用中最常见的 OAuth2 应该就是各种第三方登录了,例如 QQ 授权登录、微信授权登录、微博授权登录、GitHub 授权登录等等。
OAuth2.0的四种模式?
OAuth2.0协议一共支持 4 种不同的授权模式:
- 授权码模式:常见的第三方平台登录功能基本都是使用这种模式。
- 简化模式:简化模式是不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌(token),如果网站是纯静态页面则可以采用这种方式。
- 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务提供商就是同一家公司,自己做前后端分离登录就可以采用这种模式。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权,严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
1、授权码模式
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
令牌获取的流程如下:
上图中涉及到两个角色,分别是客户端、认证中心,客户端负责拿令牌,认证中心负责发放令牌。
但是,不是所有客户端都有权限请求令牌的,需要事先在认证中心申请,比如微信并不是所有网站都能直接接入,而是要去微信后台开通这个权限。
至少要提前向认证中心申请的几个参数如下:
- client_id:客户端唯一id,认证中心颁发的唯一标识
- client_secret:客户端的秘钥,相当于密码
- scope:客户端的权限
- redirect_uri:授权码模式使用的跳转uri,需要事先告知认证中心。
1、请求授权码
客户端需要向认证中心拿到授权码,比如第三方登录使用微信,扫一扫登录那一步就是向微信的认证中心获取授权码。
请求的url如下:
/oauth/authorize?client_id=&response_type=code&scope=&redirect_uri=
上述这个url中携带的几个参数如下:
- client_id:客户端的id,这个由认证中心分配,并不是所有的客户端都能随意接入认证中心
- response_type:固定值为code,表示要求返回授权码。
- scope:表示要求的授权范围,客户端的权限
- redirect_uri:跳转的uri,认证中心同意或者拒绝授权跳转的地址,如果同意会在uri后面携带一个
code=xxx
,这就是授权码
2、返回授权码
第1步请求之后,认证中心会要求登录、是否同意授权,用户同意授权之后直接跳转到redirect_uri
(这个需要事先在认证中心申请配置),授权码会携带在这个地址后面,如下:
http://xxxx?code=NMoj5y
上述链接中的NMoj5y
就是授权码了。
3、请求令牌
客户端拿到授权码之后,直接携带授权码发送请求给认证中心获取令牌,请求的url如下:
/oauth/token?
client_id=&
client_secret=&
grant_type=authorization_code&
code=NMoj5y&
redirect_uri=
相同的参数同上,不同参数解析如下:
- grant_type:授权类型,授权码固定的值为authorization_code
- code:这个就是上一步获取的授权码
4、返回令牌
认证中心收到令牌请求之后,通过之后,会返回一段JSON数据,其中包含了令牌access_token,如下:
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101
}
access_token则是颁发的令牌,refresh_token是刷新令牌,一旦令牌失效则携带这个令牌进行刷新。
简化模式
这种模式不常用,主要针对那些无后台的系统,直接通过web跳转授权,流程如下图:
这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
1、请求令牌
客户端直接请求令牌,请求的url如下:
/oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=
这个url正是授权码模式中获取授权码的url,各个参数解析如下:
- client_id:客户端的唯一Id
- response_type:简化模式的固定值为token
- scope:客户端的权限
- redirect_uri:跳转的uri,这里后面携带的直接是令牌,不是授权码了。
2、返回令牌
认证中心认证通过后,会跳转到redirect_uri,并且后面携带着令牌,链接如下:
https://xxxx#token=NPmdj5
#token=NPmdj5
这一段后面携带的就是认证中心携带的,令牌为NPmdj5。
密码模式
密码模式也很简单,直接通过用户名、密码获取令牌,流程如下:
1、请求令牌
认证中心要求客户端输入用户名、密码,认证成功则颁发令牌,请求的url如下:
/oauth/token?
grant_type=password&
username=&
password=&
client_id=&
client_secret=Copy to clipboardErrorCopied
参数解析如下:
- grant_type:授权类型,密码模式固定值为password
- username:用户名
- password:密码
- client_id:客户端id
- client_secret:客户端的秘钥
2、返回令牌
上述认证通过,直接返回JSON数据,不需要跳转,如下:
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101
}Copy to clipboardErrorCopied
access_token则是颁发的令牌,refresh_token是刷新令牌,一旦令牌失效则携带这个令牌进行刷新。
客户端模式
适用于没有前端的命令行应用,即在命令行下请求令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
流程如下:
1、请求令牌
请求的url为如下:
/oauth/token?
grant_type=client_credentials&
client_id=&
client_secret=Copy to clipboardErrorCopied
参数解析如下:
- grant_type:授权类型,客户端模式固定值为client_credentials
- client_id:客户端id
- client_secret:客户端秘钥
2、返回令牌
认证成功后直接返回令牌,格式为JSON数据,如下:
{
"access_token": "ACCESS_TOKEN",
"token_type": "bearer",
"expires_in": 7200,
"scope": "all"
}
基于场景与代码理解基本角色
快递速查App 相当于 第三方客户端 ,需要在授权服务器进行配置
京东物流相当于 资源服务器
京东授权 相当于授权服务器
以maven 形式 创建父工程 simple-token pom 文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.glls</groupId>
<artifactId>simple-token</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>authorization-server</module>
<module>resource-server</module>
<module>mytest</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
先创建一个简单的授权服务器 authorization-server
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simple-token</artifactId>
<groupId>com.glls</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>authorization-server</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
授权服务器 有三个重要的配置
1.security的配置
package com.glls.auth.config;
import com.glls.auth.model.MyUserDetails;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @date 2022/12/9
* @desc security安全配置
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//1.配置加密方式
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//2.注入 认证管理器 AuthenticationManager 这个bean 在 密码模式会使用到
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
//3. 这里为了方便测试 咱们以硬编码的方式 配置用户
@Bean
public UserDetailsService userDetailsService(){
UserDetailsService userDetailsService = new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(username.equals("user")){
//硬编码的形式 设置 user 用户 密码 user 角色权限 user, student
MyUserDetails user = new MyUserDetails();
user.setUsername("user");
user.setPassword(passwordEncoder().encode("user"));
user.setPerms("user,student");
return user;
}
if(username.equals("admin")){
//硬编码的形式 设置 user 用户 密码 user 角色权限 user, student
MyUserDetails admin = new MyUserDetails();
admin.setUsername("admin");
admin.setPassword(passwordEncoder().encode("admin"));
admin.setPerms("admin");
return admin;
}
return null;
}
};
return userDetailsService;
}
//4. 配置安全拦截策略
// 由于 有 授权码模式 需要表单确认授权 所以 开启表单提交模式
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin() // 开启表单提交模式
.and()
.logout();
}
}
1.1 MyUserDetails
package com.glls.auth.model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MyUserDetails implements UserDetails {
private String username;
private String password;
private String perms;
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Stream.of(perms.split(",")).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPerms() {
return perms;
}
public void setPerms(String perms) {
this.perms = perms;
}
}
2.令牌的存储策略配置
package com.glls.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
/**
* @date 2022/12/9
* @desc 令牌的配置
*/
@Configuration
public class TokenConfig {
// 配置令牌的存储策略
@Bean
public TokenStore tokenStore(){
// 令牌支持多种方式的存储 比如 内存模式 redis JWT
//这里 咱们暂时采用内存模式 方便演示 但是要注意 内存模式 服务器一重启 令牌就失效了
return new InMemoryTokenStore();
}
}
3.oauth2的核心配置类
package com.glls.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
/**
* @date 2022/12/9
* @desc
* Oauth2的 认证授权配置类 需要满足两个条件
* 1. 继承AuthorizationServerConfigurerAdapter
* 2.标注 @EnableAuthorizationServer 注解
*
* 需要实现三个方法
*
* public void configure(AuthorizationServerSecurityConfigurer security)
*
* public void configure(ClientDetailsServiceConfigurer clients)
*
* public void configure(AuthorizationServerEndpointsConfigurer endpoints)
*/
@Configuration
@EnableAuthorizationServer
public class AuthConfig extends AuthorizationServerConfigurerAdapter {
// 令牌端点 安全约束配置
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 主要是 一些端点的权限配置 比如 /oauth/token 等 对哪些开放
security
// 开启 /oauth/token_key 验证端口权限访问
.tokenKeyAccess("permitAll()")
// 开启 /oauth/check_token token验证的端口访问权限
.checkTokenAccess("permitAll()")
//支持 client_id 和 client_secret 的 登录认证
.allowFormAuthenticationForClients();
}
// 客户端的配置
// 并不是所有的客户端 都有权限向认证服务申请令牌的 首先 认证服务要知道你是谁, 你有什么资格、?
// 因此 一些必要的配置 是 认证服务分配给你的 比如 客户端的id 秘钥 权限
// 客户端的存储配置 也支持多种方式 比如 内存 数据库 对应的接口是ClientDetailsService
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
//客户端详情配置 配置 ClientId ClientSecret
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 这里为了方便演示 把 client信息 配置到 内存中
clients.inMemory() // 暂时把client 信息放在内存中
.withClient("client1") //客户端唯一 id
.secret(passwordEncoder.encode("123")) //客户端secret
.resourceIds("resource1") // 给客户端分配的资源权限 对应的是 资源服务的id
//允许的授权方式 一共有五种
//授权码模式
//密码模式
//客户端模式
//简化模式
//令牌刷新
.authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")
//授权范围
.scopes("scope1","scope2")
//是否自动弹出允许授权的页面 true 则 直接返回授权码,false 则需要用户确认授权 才返回授权码
.autoApprove(false)
.redirectUris("http://www.baidu.com") // 实际 应该是往第三方应用的接口跳转 把授权码传递过去
//可以配置多个client
.and()
.withClient("client2") //客户端唯一 id
.secret(passwordEncoder.encode("123")) //客户端secret
.resourceIds("resource1") // 给客户端分配的资源权限 对应的是 资源服务的id
.authorizedGrantTypes("authorization_code","password","client_credentials","implicit","refresh_token")
//授权范围
.scopes("scope1")
//是否自动弹出允许授权的页面 true 则 直接返回授权码,false 则需要用户确认授权 才返回授权码
.autoApprove(false)
.redirectUris("http://www.baidu.com"); // 实际 应该是往第三方应用的接口跳转 把授权码传递过去
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore tokenStore;
//授权码模式的service
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}
//令牌管理服务的配置
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
//客户端的配置策略
defaultTokenServices.setClientDetailsService(clientDetailsService);
//支持令牌刷新
defaultTokenServices.setSupportRefreshToken(true);
//令牌的存储方式
defaultTokenServices.setTokenStore(tokenStore);
//access_token 令牌的过期时间
defaultTokenServices.setAccessTokenValiditySeconds(300);
//refresh_token令牌的过期时间
defaultTokenServices.setRefreshTokenValiditySeconds(350);
return defaultTokenServices;
}
//令牌访问端点的配置
//目前 这里有四个配置
//1. 密码模式所需要的 authenticationManager
//2.授权码模式 需要的 authorizationCodeServices
//3.令牌管理服务
//4./oauth/token 申请令牌的请求 只允许post
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 密码模式需要的配置
.authenticationManager(authenticationManager)
//授权码模式 需要的 authorizationCodeServices
.authorizationCodeServices(authorizationCodeServices())
//无论何种模式 都需要 令牌管理服务
.tokenServices(tokenServices())
//只允许 post 请求 访问 令牌
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
}
创建 测试的服务 test-service 测试 授权码模式 密码模式 校验token 等操作
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>oauth2-simple</artifactId>
<groupId>com.glls.java2212</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-service</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.35.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.57</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
相关工具类和测试类
package com.glls.test;
import com.gargoylesoftware.htmlunit.*;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import java.io.IOException;
import java.net.URL;
import java.util.*;
public class HttpUtil {
public static WebClient webClient = new WebClient(BrowserVersion.FIREFOX_52);
public static Page send(HttpMethod httpMethod, String url, Map<String, String> heads, String body, String... params) throws IOException {
WebRequest webRequest = new WebRequest(new URL(url), httpMethod);
if (httpMethod.equals(HttpMethod.GET)) {
url += buildGetParam(params);
} else {
webRequest.setRequestParameters(buildPostParam(params));
}
if (body != null) {
webRequest.setRequestBody(body);
}
if (heads != null) {
webRequest.setAdditionalHeaders(heads);
}
Page page = webClient.getPage(webRequest);
System.out.println(page.getWebResponse().getContentAsString());
return page;
}
private static List<NameValuePair> buildPostParam(String... params) {
List<NameValuePair> pairs = new ArrayList<>();
for (int i = 0; i < params.length; i += 2) {
pairs.add(new NameValuePair(params[i], params[i + 1]));
}
return pairs;
}
private static String buildGetParam(String... params) {
if (params == null || params.length == 0) {
return "";
}
String param = "?";
for (int i = 0; i < params.length; i += 2) {
if (!param.equals("?")) {
param += "&";
}
String key = params[i];
String value = params[i + 1];
if (key.equals("id") && value == null) {
continue;
}
param += key + "=" + value;
}
return param;
}
}
package com.glls.test;
import com.alibaba.fastjson.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class RootUtil {
public static Map<String, String> buildMap(String... args) {
Map<String, String> map = new HashMap<>();
for (int i = 0; i < args.length; i += 2) {
map.put(args[i], args[i + 1]);
}
return map;
}
public static JSONObject buildJson(String... args) {
JSONObject jsonObject = new JSONObject();
for (int i = 0; i < args.length; i += 2) {
jsonObject.put(args[i], args[i + 1]);
}
return jsonObject;
}
}
测试类 在这里 模拟 请求 获取授权码 申请token 等操作
package com.glls.test;
import com.gargoylesoftware.htmlunit.HttpMethod;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.Map;
public class TT {
//认证服务器地址
public static String authorization_server = "http://127.0.0.1:7001/";
//资源服务器地址
public static String resource_server = "http://127.0.0.1:8001/";
//授权码模式
/**
* 思考 为什么授权码模式不直接发送token呢 为什么还需要先发送授权码 再根据授权码取获取token 呢
* 这里需要注意的是 授权码模式 其实是有两次 认证的 ,第一次 需要对用户进行认证 比如 用户扫码授权 或者 输入用户名密码 其实就是
* 第一次 认证,第一次 认证通过 就可以申请获取授权码, 授权服务器返回授权码之后 ,客户端再根据授权码 客户端id 客户端secret 去认证
* 申请token 而且 第一次 认证用户信息返回的 授权码是一次性的,只能根据这个授权码 申请一次token ,所以安全性大大提高
*
*
* */
@Test
public void getTokenByCode() throws IOException {
//标准模式
//浏览器访问
//http://127.0.0.1:7001/oauth/authorize?client_id=client1&response_type=code&scope=scope1&redirect_uri=http://www.baidu.com
//重定向结果
//https://www.baidu.com/?code=weZyHn
String[] params = new String[]{
"client_id", "client1",
"client_secret", "123",
"grant_type", "authorization_code",
"code", "ZfikzR",//这个code是从getCode()方法获取的
"redirect_uri", "http://www.baidu.com"
};
HttpUtil.send(HttpMethod.POST, authorization_server + "oauth/token", null, null, params);
//{"access_token":"415b010c-4891-41ef-90ce-1653ecd37658","token_type":"bearer","refresh_token":"02021d0e-34ed-42eb-8abc-f495b2dcd512","expires_in":133,"scope":"all"}
// {"access_token":"b991f7a7-65b9-42d3-9c77-0adc940e74c6","token_type":"bearer","refresh_token":"b3dc80b4-5722-4405-928f-d9e50a9f71e6","expires_in":299,"scope":"scope1"}
}
//简单模式
@Test
public void simpleMode() {
//这个一般给纯前端的项目用,没有后台的
//浏览器访问
//http://127.0.0.1:3001/oauth/authorize?client_id=client1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
//重定向结果
//https://www.baidu.com/#access_token=415b010c-4891-41ef-90ce-1653ecd37658&token_type=bearer&expires_in=60
}
//密码模式
@Test
public void getTokenByPassword() throws IOException {
//直接用用户的账号密码去申请权限,会把密码泄露给客户端
String[] params = new String[]{
"client_id", "client1",
"client_secret", "123",
"grant_type", "password",
"username", "user",
"password", "user"
};
HttpUtil.send(HttpMethod.POST, authorization_server + "oauth/token", null, null, params);
//{"access_token":"8234703e-455e-45aa-a13b-958469998818","token_type":"bearer","refresh_token":"eb95dca0-b054-4b3c-9899-1c89375454fd","expires_in":279,"scope":"scope1 scope2"}
}
//客户端模式 对客户端很信任 不管是谁来使用 都给他授权 不关心用户 只关心客户端
@Test
public void getTokenByClient() throws IOException {
//直接用客户端id去申请权限
String[] params = new String[]{
"client_id", "client1",
"client_secret", "123",
"grant_type", "client_credentials" // 客户端授权
};
HttpUtil.send(HttpMethod.POST, authorization_server + "oauth/token", null, null, params);
//{"access_token":"4ed1469d-4e66-4379-acbc-22170079b831","token_type":"bearer","expires_in":299,"scope":"all"}
}
//校验token
@Test
public void checkToken() throws IOException {
String[] params = new String[]{
"token", "a87b6a2e-bc58-44d9-8ec3-96a93c59cb18",
};
HttpUtil.send(HttpMethod.POST, authorization_server + "oauth/check_token", null, null, params);
//{"aud":["resource1"],"user_name":"admin","scope":["all"],"active":true,"exp":1582620698,"authorities":["admin"],"client_id":"client1"}
}
//刷新token
@Test
public void refreshToken() throws IOException {
//使用有效的refresh_token去重新生成一个token,之前的会失效
String[] params = new String[]{
"client_id", "client1",
"client_secret", "123",
"grant_type", "refresh_token",
"refresh_token", "eb95dca0-b054-4b3c-9899-1c89375454fd"
};
HttpUtil.send(HttpMethod.POST, authorization_server + "oauth/token", null, null, params);
//{"access_token":"a87b6a2e-bc58-44d9-8ec3-96a93c59cb18","token_type":"bearer","refresh_token":"eb95dca0-b054-4b3c-9899-1c89375454fd","expires_in":300,"scope":"scope1 scope2"}
}
//使用token访问resource
@Test
public void getResourceByToken() throws IOException {
Map<String, String> head = RootUtil.buildMap("Authorization", "Bearer 0d354a25-a140-4bee-9200-e93f35adc1ed");
HttpUtil.send(HttpMethod.POST, resource_server + "/admin", head, null);
}
}
授权码模式 流程图
两次认证 第一次认证是用户身份认证 表现形式是 用户输入用户名密码 或者 用户扫码 允许client获取权限 注意用户的账号密码是不经过client的 那个登录页面是授权服务器提供的 , 第一次认证成功,授权服务器返回 授权码, client 拿着授权码 申请token ,申请到token 之后,再携带token 访问资源服务器,这里的token 如果不是jwt token 的话 还需要资源服务器携带token 去授权服务器认证
也就是 简单token 需要下图的 第八步 和 第九步
密码模式
用户对Client很信任 先把凭证信息(用户名密码) 给 client 即这里的快递速查App ,然后 Client 拿着用户名密码去找授权服务器申请令牌
客户端模式
授权服务器不关心用户 只关心客户端, 所以只对客户端进行认证,客户端在申请token 时 只需要传递 客户端id 和 客户端secret
改造成JWT Token
就没有了 资源服务器携带token 再去授权服务器校验token 的过程了,资源服务器可以自己解析校验token
什么是JWT?
OAuth2.0体系中令牌分为两类,分别是透明令牌、不透明令牌。
不透明令牌则是令牌本身不存储任何信息,比如一串UUID,上篇文章中使用的InMemoryTokenStore就类似这种。
因此资源服务拿到这个令牌必须调调用认证授权服务的接口进行令牌的校验,高并发的情况下延迟很高,性能很低,正如上篇文章中资源服务器中配置的校验,如下:
透明令牌本身就存储这部分用户信息,比如JWT,资源服务可以调用自身的服务对该令牌进行校验解析,不必调用认证服务的接口去校验令牌。
JWT相信大家都有了解,分为三部分,分别是头部、载荷、签名,如下
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYzODYwNTcxOCwiYXV0aG9yaXRpZXMiOlsiUk9MRV91c2VyIl0sImp0aSI6ImRkNTVkMjEzLThkMDYtNGY4MC1iMGRmLTdkN2E0YWE2MmZlOSIsImNsaWVudF9pZCI6Im15anN6bCJ9.
koup5-wzGfcSVnaaNfILwAgw2VaTLvRgq2JVnIHYe_Q
头部定义了JWT基本信息,如类型和签名算法。
载荷包含了一些基本信息(签发时间、过期时间…),另外还可以添加一些自定义的信息,比如用户的部分信息。
签名部分将前两个字符串用 . 连接后,使用头部定义的加密算法,利用密钥进行签名,并将签名信息附在最后。
OAuth2.0认证授权服务搭建
OAuth2.0分为认证授权中心、资源服务,认证中心用于颁发令牌,资源服务解析令牌并且提供资源。
普通 token存入数据库
数据库需要存储 客户端 与 code 与 token
jwt token 与 数据库
数据库只需要存储 客户端 与 code , 注意 没有把 jwt token 存入数据库
微服务中使用oauth2 进行鉴权
下图是一种情况 ZUUL 没有进行权限校验
微服务中使用oauth2 进行鉴权
下图是另一种情况 ZUUL 进行权限校验 在网关层 进行权限校验 可以灵活地配置校验的粒度
JWT**,资源服务可以调用自身的服务对该令牌进行校验解析,不必调用认证服务的接口去校验令牌。
JWT相信大家都有了解,分为三部分,分别是头部、载荷、签名,如下
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYzODYwNTcxOCwiYXV0aG9yaXRpZXMiOlsiUk9MRV91c2VyIl0sImp0aSI6ImRkNTVkMjEzLThkMDYtNGY4MC1iMGRmLTdkN2E0YWE2MmZlOSIsImNsaWVudF9pZCI6Im15anN6bCJ9.
koup5-wzGfcSVnaaNfILwAgw2VaTLvRgq2JVnIHYe_Q
头部定义了JWT基本信息,如类型和签名算法。
载荷包含了一些基本信息(签发时间、过期时间…),另外还可以添加一些自定义的信息,比如用户的部分信息。
签名部分将前两个字符串用 . 连接后,使用头部定义的加密算法,利用密钥进行签名,并将签名信息附在最后。
OAuth2.0认证授权服务搭建
OAuth2.0分为认证授权中心、资源服务,认证中心用于颁发令牌,资源服务解析令牌并且提供资源。
普通 token存入数据库
数据库需要存储 客户端 与 code 与 token
[外链图片转存中…(img-hgz8ZE0C-1690600869494)]
jwt token 与 数据库
数据库只需要存储 客户端 与 code , 注意 没有把 jwt token 存入数据库
[外链图片转存中…(img-gx01tCV3-1690600869495)]
微服务中使用oauth2 进行鉴权
下图是一种情况 ZUUL 没有进行权限校验
[外链图片转存中…(img-yaoJFRfe-1690600869495)]
微服务中使用oauth2 进行鉴权
下图是另一种情况 ZUUL 进行权限校验 在网关层 进行权限校验 可以灵活地配置校验的粒度
[外链图片转存中…(img-BmO4wOFQ-1690600869496)]