18.Oauth2-微服务认证
1.Oauth2
OAuth 2.0授权框架支持第三方支持访问有限的HTTP服务,通过在资源所有者和HTTP服务之间进行一个批准交互来代表资源者去访问这些资源,或者通过允许第三方应用程序以自己的名义获取访问权限。
为了方便理解,可以想象OAuth2.0就是在用户资源和第三方应用之间的一个中间层,它把资源和第三方应用隔开,使得第三方应用无法直接访问资源,从而起到保护资源的作用。
为了访问这种受保护的资源,第三方应用(客户端)在访问的时候需要提供凭证。即,需要告诉OAuth2.0你是谁你要做什么。
用户可以将用户名和密码告诉第三方应用,让第三方应用直接以你的名义去访问,也可以授权第三方应用去访问。
例如,微信公众平台开发,在微信公众平台开发过程中当我们访问某个页面,页面可能弹出一个提示框应用需要获取我们的个人信息问是否允许,点确认其实就是授权第三方应用获取我们在微信公众平台的个人信息,这里微信网页授权就是使用的OAuth2.0。
-
第三方应用程序(Third-party application): 又称之为客户端(client),我们自己开发的各种客户端,对我们自己的项目来说,QQ、微信、支付宝等是第三方应用程序。
-
HTTP 服务提供商(HTTP service): 我们开发的项目以及 QQ、微信、支付宝、钉钉等都可以称之为“服务提供商”。
-
资源所有者(Resource Owner): 又称之为用户(user),拥有账号密码的人。
-
用户代理(User Agent): 用来访问资源,比如浏览器,代替用户去访问这些资源。
-
认证服务器(Authorization server): 即服务提供商专门用来处理认证的服务器,主要就是实现登录、授权功能。
-
资源服务器(Resource server): 即服务提供商存放用户生成的资源的服务器,比如电商中的商品模块、订单模块等,是用来处理具体业务的服务器。
OAuth2.0协议流程描述了四种角色之间的交互过程,如下图所示。
简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
-
令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
-
令牌可以被数据所有者撤销,会立即失效。
-
令牌有权限范围(scope),对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。这就是 OAuth 2.0 的优点。
注意,只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因。
1.1 开放平台
开放平台(Open Platform)在软件行业和网络中,开放平台是指软件系统通过公开其应用程序编程接口(API)或函数(function)来使外部的程序可以增加该软件系统的功能或使用该软件系统的资源,而不需要更改该软件系统的源代码。
在互联网时代,把网站的服务封装成一系列计算机易识别的数据接口开放出去,供第三方开发者使用,这种行为就叫做Open API,提供开放API的平台本身就被称为开放平台。
第一种是技术性的开放,例如百度、腾讯、阿里巴巴等,例如阿里可以提供标准化的应用软件,但是数百万形形色色的卖家对于个性化要求的软件,并不是一个公司的力量可以满足的,所以就把这些需求开放给众多的第三方开发者的方式。再例如google的基于Linux平台的开源手机操作系统就被认为会很快打败Nokia塞班系统。这一种技术性开放平台虽然目前来看跟B2C企业的开放平台关系不大,但是也能从一定程度上说明开放平台是互联网企业的趋势。
第二种开放平台是指软件系统通过公开其应用程序编程接口(API)或函数(function)来使外部的程序可以增加该软件系统的功能或使用该软件系统的资源,而不需要更改该软件系统的源代码。B2C企业开放平台又包含两种形式,A:淘宝商城、日本乐天这种纯平台的模式,即自己不碰商品的进销存,全部由入驻商家来做;B:美国亚马逊、当当网、京东商城这种“自营+联营”的模式。
1.2 开放平台交互模型
三个角色:
-
资源拥有者:用户
-
客户端:各种app、浏览器
-
服务提供方:包含两个角色
认证服务器
资源服务器
1.2.1 认证服务器
认证服务器负责对用户进行认证,并授权给客户端权限。一般的认证都是通过对账号密码进行验证实现,而难点在于怎么进行授权。比如我们使用第三方登录 "哔哩哔哩",可以看到如使用 QQ 登录的授权页面上有 "哔哩哔哩将获取以下权限" 的字样以及权限信息
认证服务器需要知道请求授权的客户端的身份以及该客户端请求的权限。常见的做法是为每一个客户端预先分配一个 id,并给每个 id 对应一个名称以及权限信息。这些信息可以写在认证服务器上的配置文件里,今后客户端每次打开授权页面的时候,客户端需要将该id发送到认证服务器,0Auth2.0就可以用来自动给客户端分配id,同时完成配置文件的自动更新。
1.3 OAuth2 开放平台
开放平台是由 OAuth2.0 协议发展而来的一个产品,它的作用是让客户端自己去这上面进行注册、申请,通过之后系统自动分配 客户端id ,并完成配置的自动更新。
客户端要完成申请,通常需要申请人填写客户端程序的类型(Web、App、微信小程序、支付宝小程序等等)、企业信息、营业执照、法人信息以及想要获取权限等信息,申请需要得到得到服务提供上的审核通过之后,开发平台才会自动分配一个客户端id给客户端。
在通过审核之后,第三方应用在进行认证时,就会想需要获取到的权限信息展示到页面上,例如哔哩哔哩获取QQ权限。授权成功之后认证服务器需要把产生的 access_token 发送给客户端,客户端才能访问具体的资源(头像、性别之类的),大致过程如下:
-
让客户端在开放平台提交申请时候,填写一个 网址,例如:www.baidu.com,此网址主要用来获取认证码。
-
当有用户授权成功之后,认证服务器将页面重定向到这个网址,并将生成的 access_token拼接到该网址后面,例如:www.baidu.com?access_token=123
-
客户端接收到access_token,之后客户端就可以拿着这个token去获取需要的数据了
1.3.1 令牌
传统项目向服务端请求数据,服务端需要频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,这样效率非常低下,怎么提高效率呢?Token便应运而生。
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
1.3.2 Access Token
Access Token 是客户端访问资源服务器的令牌。拥有这个令牌代表着得到用户的授权,即具备了访问资源的权限。同时这个授权应该是临时的,只能在一定期限内使用。主要原因是因为Access Token 在使用的过程中很有可能会泄露,被不法分子利用获取我们的数据。所以Access Token应该只能在某个期限内使用,这样可以降低因 Access Token 泄露而带来的风险。
1.4 认证模式
OAuth2.0中定义了四种授权模式:
-
authorization code 授权码模式
-
implicit 简化模式
-
resource owner password credentials 密码模式
-
client credentials 客户端模式
常见模式:授权码、密码模式
1.4.1 授权码模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式,code保证了token的安全性,即使code被拦截,由于没有secret,也是无法通过code获得token的。
角色行为与功能
-
资源所有者
只需要允许或拒绝第三方应用获得授权
-
第三方应用
申请成为资源服务器的第三方应用
获取资源服务器提供的资源
-
授权服务器
提供授权许可code、令牌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><groupId>com.woniuxy</groupId><artifactId>oauth2</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging>
</project>
创建auth-server认证服务器模块
导入依赖
导入依赖版本如下
<properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.3.7.RELEASE</spring-boot.version><spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
</properties>
oauth2依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.4.RELEASE</version>
</dependency>
创建用户信息配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter{//密码编码器@Beanpublic BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 基于内存的用户信息@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication() //内存认证.withUser("zhangsan") //用户名.password(passwordEncoder().encode("123")) //密码.authorities("ROLE_ADMIN"); //角色}
}
创建客户端配置类,配置客户端信息
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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 javax.annotation.Resource;@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter{@Resourceprivate BCryptPasswordEncoder passwordEncoder;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {//配置客户端clients.inMemory() //内存方式.withClient("client") //客户端名字.secret(passwordEncoder.encode("secret")) //客户端秘钥.authorizedGrantTypes("authorization_code")//授权类型.scopes("all") //授权范围.redirectUris("http://www.baidu.com"); //回调网址,携带授权码}
}
在application.yml文件中配置以下信息
server:port: 8000
spring:application:name: oauth
启动项目进行登录
localhost:8000/login
进入登录页面,输入账号:zhangsan,密码:123进行登录
登录成功之后向服务器发送请求获取授权码,在地址栏上输入以下内容回车
http://localhost:8080/oauth/authorize?client_id=client&response_type=code
可以看到一个授权页面,询问用户是否进行授权
授权成功之后会重定向到AuthorizationServerConfiguration配置类中指定的地址,并以参数的方式携带授权码
通过postman发送请求向服务器获取token
地址栏填写:http://client:secret@localhost:8000/oauth/token
填写客户端账号密码
填写授权类型、授权码,发送请求
成功之后在postman上可以看到以下信息
表示成功
注意:每个授权码只能使用一次
1.4.2 密码模式
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
修改AuthorizationServerConfiguration配置类,添加密码模式
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {//配置客户端clients.inMemory() .withClient("client").secret(passwordEncoder.encode("secret")).authorizedGrantTypes("authorization_code","password") //添加密码授权模式.scopes("all") //授权范围.redirectUris("http://www.woniuxy.com");
}
在postman中新开一个请求,地址栏中填写:http://localhost:8080/oauth/token
密码授权模式要求以请求头的方式提交客户端账号密码,并且需要对账号密码进行base64加密,因此选择Authorization选项卡,设置TYPE为"Basic Auth",并填写客户端账号密码
在请求体中设置授权类型、用户账号密码参数
发送请求测试
可以发现此时并不支持密码模式,即使在AuthorizationServerConfiguration配置类中指定了密码模式。
原因是此时代码中缺少对密码模式的支持,在oauth2中需要添加AuthenticationManager对象对密码模式进行支持。
在WebSecurityConfiguration配置类中配置 AuthenticationManager
// 配置 AuthenticationManager(密码模式需要该对象进行账号密码校验)
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();
}
在AuthorizationServerConfiguration类中注入AuthenticationManager,并重写以下方法
// 认证管理器
@Autowired
private AuthenticationManager authenticationManager;//配置使用的 AuthenticationManager 实现用户认证的功能
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManager);
}
重启项目再次发送请求获取token
整合JWT
导入了oauth2依赖就自动导入的JWT相关依赖,因此不用单独导入JWT,只需要进行设置就行
创建TokenConfiguration配置类
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.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;@Configuration
public class TokenConfiguration {// 密码private static String SIGNING_KEY="www.woniuxy.com";// token转换器@Beanpublic JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);return jwtAccessTokenConverter;}// 令牌存储策略:jwt方式@Beanpublic TokenStore tokenStore(){return new JwtTokenStore(accessTokenConverter());}
}
在AuthorizationServerConfiguration配置类中注入相关对象
@Resource
private TokenStore tokenStore;@Resource
private JwtAccessTokenConverter jwtAccessTokenConverter;@Resource
private ClientDetailsService clientDetailsService;
在AuthorizationServerConfiguration配置类中编写token服务方法,该方法主要用来设置
private AuthorizationServerTokenServices tokenServices(){// 创建服务对象DefaultTokenServices services = new DefaultTokenServices();// 设置客户端详情服务services.setClientDetailsService(clientDetailsService);// 支持刷新令牌services.setSupportRefreshToken(true);// 不重复使用refreshtoken,每次刷新之后只能用新的refreshtoken才能继续刷新services.setReuseRefreshToken(false);// 设置令牌存储策略services.setTokenStore(tokenStore);// 设置令牌增强TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));services.setTokenEnhancer(tokenEnhancerChain);// 设置令牌过期时间services.setAccessTokenValiditySeconds(600);services.setRefreshTokenValiditySeconds(6000);return services;
}
修改configure(AuthorizationServerEndpointsConfigurer endpoints)方法,添加token服务
//配置使用的 AuthenticationManager 实现用户认证的功能
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManager) // 认证管理器.tokenServices(tokenServices()); // 配置token服务
}
重启项目发送请求获取token
如果想要获取到refreshtoken,可以修改AuthorizationServerConfiguration配置类,添加refresh_token授权方式
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {//配置客户端clients.inMemory() .withClient("client").secret(passwordEncoder.encode("secret")) .authorizedGrantTypes("authorization_code","password","refresh_token") .scopes("all").redirectUris("http://www.woniuxy.com");
}
重启项目测试
整合数据库(user)
建表SQL
create database sc default character set=utf8;DROP TABLE IF EXISTS `perms`;
CREATE TABLE `perms` (`id` int(11) DEFAULT NULL,`name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `perms` VALUES (3001,'user:add'),(3002,'user:del'),(3003,'user:find'),(3004,'user:update'),(3005,'goods:add'),(3006,'goods:find'),(3007,'goods:del'),(3008,'goods:update');DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (`id` int(11) DEFAULT NULL,`name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `role` VALUES (2001,'ROLE_ADMIN'),(2002,'ROLE_USER');DROP TABLE IF EXISTS `role_perms`;
CREATE TABLE `role_perms` (`rid` int(11) DEFAULT NULL,`pid` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `role_perms` VALUES (2001,3001),(2001,3003),(2001,3004),(2002,3005),(2002,3006),(2002,3007),(2002,3008);DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (`id` int(11) DEFAULT NULL,`username` varchar(20) DEFAULT NULL,`password` varchar(64) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `user` VALUES (1001,'zhangsan','$2a$10$pINVnd8.cXScFXCxI2x4cem4fOexA2J5TNY/Mx2CjN6mJuYGBNG0m'),(1002,'wangwu','wangwu');DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (`uid` int(11) DEFAULT NULL,`rid` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERT INTO `user_role` VALUES (1001,2001),(1002,2002),(1003,2002);
auth-server的pom.xml中引入mybatis
<!--mybatis-->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version>
</dependency>
在application.yml中配置mybatis参数
mybatis:type-aliases-package: com.woniuxy.authserver.entitymapper-locations: classpath:/mapper/*.xml
创建Perms、Role、User实体类,注意:实体类必须实现序列化接口,不然运行过程中可能会报Failed to find access token for token错误
import lombok.Data;@Data
public class Perms implements Serializable {private static final long serialVersionUID = 1L;private int id;private String name;
}
import lombok.Data;
import java.util.List;@Data
public class Role implements Serializable {private static final long serialVersionUID = 1L;private int id;private String name;private List<Perms> perms;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails, Serializable {private static final long serialVersionUID = 1L;private int id;private String username;private String password;private List<Role> roles;// 返回当前用户的所有角色、权限信息@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {log.debug("获取用户角色权限信息");// 新建集合List<GrantedAuthority> grantedAuthorities = new ArrayList<>();// 遍历rolefor(Role role : this.roles){// 放入角色信息grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));// 遍历当前角色的所有权限信息for(Perms perms : role.getPerms()){grantedAuthorities.add(new SimpleGrantedAuthority(perms.getName()));}}log.debug(grantedAuthorities.toString());return grantedAuthorities;}// 获取用户名@Overridepublic String getUsername() {return this.username;}// 账号是否过期 true表示未过期 false表示过期@Overridepublic boolean isAccountNonExpired() {return true;}// 账号是否被锁定 true表示未锁定 false表示锁定@Overridepublic boolean isAccountNonLocked() {return true;}// 凭证是否过期 true表示未过期 false表示过期@Overridepublic boolean isCredentialsNonExpired() {return true;}// 用户是否被禁用 true表示未禁用 false表示禁用@Overridepublic boolean isEnabled() {return true;}
}
创建UerMapper接口
import com.woniuxy.springsecurity.entity.User;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface UserMapper {public User findByName(String username);
}
在resources目录下创建mapper文件夹,并在该文件夹下创建Mapper文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.woniuxy.authserver.mapper.UserMapper" ><select id="findByName" resultMap="user_map">select * from user where username = #{username}</select><resultMap id="user_map" type="User"><id column="id" property="id"></id><result column="username" property="username"></result><result column="password" property="password"></result><collection property="roles" ofType="Role" column="id" select="findRolesByUid"></collection></resultMap><select id="findRolesByUid" resultMap="role_map">select r.id,r.name from user_role ur,role r where ur.rid = r.id and ur.uid = #{id}</select><resultMap id="role_map" type="Role"><id column="id" property="id"></id><result column="name" property="name"></result><collection property="perms" ofType="Perms" column="id" select="findPermsByRid"></collection></resultMap><select id="findPermsByRid" resultType="Perms">select p.id,p.name from role_perms rp,perms p where rp.pid = p.id and rp.rid = #{rid}</select>
</mapper>
创建CustomUserDetailsServiceImpl类实现UserDetailsService接口
import com.woniuxy.authserver.entity.User;
import com.woniuxy.authserver.mapper.UserMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service
public class CustomUserDetailsServiceImpl implements UserDetailsService {@Resourceprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//1.查询用户User user = userMapper.findByName(username);//2.判断if (user == null) throw new UsernameNotFoundException("用户不存在");//3.返回用户信息return user;}
}
在配置类WebSecurityConfiguration中注入UserDetailsService对象,并修改configure(AuthenticationManagerBuilder auth)反方指定用户信息从数据库中获取
@Resource
private UserDetailsService userDetailsService;@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {//auth//.inMemoryAuthentication() //内存认证//.withUser("zhangsan") //用户名//.password(passwordEncoder().encode("123")) //密码//.authorities("ROLE_ADMIN"); //角色auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
重启auth-server服务,进行认证
封装用户id
在生成token时可以将用户id封装到token中,以便后期使用
修改TokenConfiguration类中的accessTokenConverter()方法,在创建转换器时重写enhance方法
// token转换器
@Bean
public JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(){@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {final Map<String,Object> map = new HashMap<>();// 从认证对象中得到用户信息User user = (User) authentication.getUserAuthentication().getPrincipal();// 将用户id放到token中map.put("uid", user.getId());((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(map);// 返回return super.enhance(accessToken, authentication);}};jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);return jwtAccessTokenConverter;
}
利用postman进行测试
返回的结果中可以看到用户id,token中也包含了用户id
检验token是否过期
在org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint类中定义了校验token的接口/oauth/check_token,该接口可以用来校验token是否合法、是否过期、是否是伪造的
@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);if (token == null) {throw new InvalidTokenException("Token was not recognised");}if (token.isExpired()) {throw new InvalidTokenException("Token has expired");}OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);// gh-1070response.put("active", true); // Always true if token exists and not expiredreturn response;
}
只是该接口oauth2默认情况下是不对外公开的,如果要使用该接口那就必须手动配置开启,在AuthorizationServerConfiguration配置类中重写以下方法
//设置 /oauth/check_token 端点,通过认证后可访问。
//该端点对应 CheckTokenEndpoint类,用于校验访问令牌的有效性。
//在客户端访问资源服务器时,会在请求中带上访问令牌。
//在资源服务器收到客户端的请求时,会使用请求中的访问令牌,找授权服务器确认该访问令牌的有效性。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {// 默认是denyAll():拒绝所有oauthServer.checkTokenAccess("permitAll()");
}
checkTokenAccess常用值有三种:
-
denyAll():拒绝所有请求,不开放该接口
-
isAuthenticated():只对完成认证之后的请求开放
-
permitAll():对所有请求开放
测试:登录成功之后在Postman中发送请求进行测试
接口url:http://localhost:8080/oauth/check_token
返回的结果中包含了用户的用户名、权限等信息,还包括了token是否可用的信息
如果返回以下信息表示token已经过期
而如果返回以下信息表示token非法
通过refresh_token获取新token
获取token和刷新token使用的是同一个接口,所以地址栏url还是
http://local:8080/oauth/token
只是grant_type需要换成refresh_token,然后将之前的refresh token作为参数传递给后台
还是需要将客户端id、密码以base64编码放到请求头中
发送请求得到结果
根据结果可以知道,token和refresh_token都会自动刷新,这样做的好处是当token过期时通过程序调用刷新接口,获取到新的token和refresh_token,实现自动续期。
refresh_token如果过期会得到以下结果
refresh_token过期就需要重新登录
1.5 资源服务器
创建resource子模块,导入相关依赖
设置父子关系
创建OAuth2ResourceServerConfig配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 设置请求,需要认证后访问.anyRequest().authenticated();}
}
创建controller
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/resource")
public class ResourceController {@RequestMapping("/info")public String info(){return "success";}
}
配置application.yml
server:port: 8001
spring:application:name: resource
security:oauth2:# OAuth2 Client 配置,对应 OAuth2ClientProperties 类client:client-id: clientclient-secret: secret# OAuth2 Resource 配置,对应 ResourceServerProperties 类resource:token-info-uri: http://127.0.0.1:8000/oauth/check_token # 获得 Token 信息的 URL# 访问令牌获取 URL,自定义的access-token-uri: http://127.0.0.1:8000/oauth/token
management:endpoints:web:exposure:include: '*'
启动resource资源服务器
先进行认证,得到token和refresh_token
localhost:8000/oauth/token
然后将得到的token放到请求资源服务器的请求头中
发送请求后可以发现报500错误,查看resource控制台可以发现以下信息
org.springframework.web.client.HttpClientErrorException$Forbidden: 403 : [{"timestamp":"2022-05-07T03:40:14.063+00:00","status":403,"error":"Forbidden","message":"","path":"/oauth/check_token"}]
根据信息提示:没有权限访问 /oauth/check_token,该URL是认证服务器用来校验token是否合法的接口。资源服务器在接收到请求时会获取到token,然后调用认证服务器的/oauth/check_token接口去检验token,但是此时认证服务器还没有开放该端口(默认关闭),所以造成了403无法访问。
到认证服务器的AuthorizationServerConfiguration配置类中开启/oauth/check_token
//设置 /oauth/check_token 端点,通过认证后可访问。
//该端点对应 CheckTokenEndpoint类,用于校验访问令牌的有效性。
//在客户端访问资源服务器时,会在请求中带上访问令牌。
//在资源服务器收到客户端的请求时,会使用请求中的访问令牌,找授权服务器确认该访问令牌的有效性。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {// 默认是denyAll():拒绝所有oauthServer.checkTokenAccess("isAuthenticated()");
}
重启认证服务器
重新进行认证得到token,然后用新的token再访问资源服务器
看到success表明成功
角色权限管理
在资源服务器主启动类上添加@EnableGlobalMethodSecurity注解,开启spring security权限注解的支持
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceApplication {public static void main(String[] args) {SpringApplication.run(ResourceApplication.class, args);}
}
在resource/info接口方法上添加注解@PreAuthorize并指定角色或权限
@RequestMapping("/info")
@PreAuthorize("hasRole('USER')")
public String info(){return "success";
}
利用postman再次访问该接口
得到不允许访问的结果,表明角色权限管理生效
1.6 整合数据库(client)
建表SQL
CREATE TABLE `clientdetails` (`appId` VARCHAR(128) NOT NULL,`resourceIds` VARCHAR(256) DEFAULT NULL,`appSecret` VARCHAR(256) DEFAULT NULL,`scope` VARCHAR(256) DEFAULT NULL,`grantTypes` VARCHAR(256) DEFAULT NULL,`redirectUrl` VARCHAR(256) DEFAULT NULL,`authorities` VARCHAR(256) DEFAULT NULL,`access_token_validity` INT(11) DEFAULT NULL,`refresh_token_validity` INT(11) DEFAULT NULL,`additionalInformation` VARCHAR(4096) DEFAULT NULL,`autoApproveScopes` VARCHAR(256) DEFAULT NULL,PRIMARY KEY (`appId`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_access_token` (`token_id` VARCHAR(256) DEFAULT NULL,`token` BLOB,`authentication_id` VARCHAR(128) NOT NULL,`user_name` VARCHAR(256) DEFAULT NULL,`client_id` VARCHAR(256) DEFAULT NULL,`authentication` BLOB,`refresh_token` VARCHAR(256) DEFAULT NULL,PRIMARY KEY (`authentication_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_approvals` (`userId` VARCHAR(256) DEFAULT NULL,`clientId` VARCHAR(256) DEFAULT NULL,`scope` VARCHAR(256) DEFAULT NULL,`status` VARCHAR(10) DEFAULT NULL,`expiresAt` TIMESTAMP NULL DEFAULT NULL,`lastModifiedAt` TIMESTAMP NULL DEFAULT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_client_details` (`client_id` VARCHAR(128) NOT NULL,`resource_ids` VARCHAR(256) DEFAULT NULL,`client_secret` VARCHAR(256) DEFAULT NULL,`scope` VARCHAR(256) DEFAULT NULL,`authorized_grant_types` VARCHAR(256) DEFAULT NULL,`web_server_redirect_uri` VARCHAR(256) DEFAULT NULL,`authorities` VARCHAR(256) DEFAULT NULL,`access_token_validity` INT(11) DEFAULT NULL,`refresh_token_validity` INT(11) DEFAULT NULL,`additional_information` VARCHAR(4096) DEFAULT NULL,`autoapprove` VARCHAR(256) DEFAULT NULL,PRIMARY KEY (`client_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_client_token` (`token_id` VARCHAR(256) DEFAULT NULL,`token` BLOB,`authentication_id` VARCHAR(128) NOT NULL,`user_name` VARCHAR(256) DEFAULT NULL,`client_id` VARCHAR(256) DEFAULT NULL,PRIMARY KEY (`authentication_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_code` (`code` VARCHAR(256) DEFAULT NULL,`authentication` BLOB
) ENGINE=INNODB DEFAULT CHARSET=utf8;CREATE TABLE `oauth_refresh_token` (`token_id` VARCHAR(256) DEFAULT NULL,`token` BLOB,`authentication` BLOB
) ENGINE=INNODB DEFAULT CHARSET=utf8;
在表 oauth_client_details 中增加一条客户端配置记录,在填入时可以按照AuthorizationServerConfiguration配置类中的客户端配置进行配置
配置的效果如下:
注:各字段解释说明
-
client_id:客户端标识
-
client_secret:客户端安全码。注意安全码不能是明文需要加密,此处可以写一段程序,然后使用BCryptPasswordEncoder为客户端安全码加密,得到加密之后的安全码,再写入到数据库中,例如:
-
System.out.println(new BCryptPasswordEncoder().encode("secret"));
-
scope:客户端授权范围
-
authorized_grant_types:客户端授权类型,支持多种类型,多种类型之间用逗号隔开
-
web_server_redirect_uri:服务器回调地址
创建实体类User、Role、Perms
在auth-server模块的pom.xml中引入mybatis相关依赖
<!-- spring-boot-starter-jdbc 内置了HikariCP 连接池,所以使用该连接池连接数据库 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 获取application.yml文件中的配置 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional>
</dependency>
application.yml文件中添加数据库相关配置
server:port: 8000
spring:application:name: oauthdatasource:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/sc?useUnicode=true&characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: roothikari:minimum-idle: 5maximum-pool-size: 10auto-commit: true #自动提交pool-name: MYHIKARICPconnection-test-query: SELECT 1 #测试是否能连接上数据库的SQL语句main:#true,后定义的bean会覆盖之前定义的相同名称的bean,生成dataSource替换掉原生的dataSourceallow-bean-definition-overriding: true
创建数据库配置类DataSourceConfiguration,主要配置用到的数据源,用HikariCP连接池的数据源替换到spring内置的数据源。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;import javax.sql.DataSource;@Configuration
public class DataSourceConfiguration {@Bean @Primary //根据application.yml中的配置信息创建dataSource@ConfigurationProperties(prefix = "spring.datasource")//import javax.sql.DataSource;public DataSource dataSource() {//创建dataSourcereturn DataSourceBuilder.create().build();}
}
在TokenConfiguration配置类中把token存储策略改成JDBC方式,将jwt存放到数据库中DataSource
@Resource
private DataSource dataSource;// 令牌存储策略:jwt方式
@Bean
public TokenStore tokenStore(DataSource dataSource){//return new JwtTokenStore(accessTokenConverter());return new JdbcTokenStore(dataSource);
}
修改AuthorizationServerConfiguration配置类,添加ClientDetailsService clientDetailsService(DataSource dataSource)方法,让程序通过DataSource从数据库中获取到客户端信息
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {//在数据库中去获取客户端信息(oauth_client_details表)return new JdbcClientDetailsService(dataSource);
}
修改configure(ClientDetailsServiceConfigurer clients)方法指定到数据库获取客户端信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {//配置客户端//clients//.inMemory() //内存方式//.withClient("client") //客户端名字//.secret(passwordEncoder.encode("secret")) //客户端秘钥//.authorizedGrantTypes("authorization_code","password","refresh_token")//.scopes("all") //授权范围//.redirectUris("http://www.woniuxy.com"); //回调网址clients.withClientDetails(clientDetailsService);
}
完成之后重启项目,再次进行认证测试
正常情况下,测试完毕之后会在数据库的oauth_access_token 表中会增加一个记录,这个记录就是浏览器获取到的token和refresh token
角色、权限管理测试
在resource服务的controller中添加以下方法
@RequestMapping("/message")
@PreAuthorize("hasRole('ADMIN')")
public String message(){return "message";
}@RequestMapping("/data")
@PreAuthorize("hasAuthority('user:add')")
public String data(){return "data";
}
@RequestMapping("/test")
@PreAuthorize("hasAuthority('user:del')")
public String test(){return "test";
}
启动resource服务,通过postman分别测试info、message、data、test接口,如果只有message、data接口可以访问,那么说明角色、权限管理成功。
相关文章:

18.Oauth2-微服务认证
1.Oauth2 OAuth 2.0授权框架支持第三方支持访问有限的HTTP服务,通过在资源所有者和HTTP服务之间进行一个批准交互来代表资源者去访问这些资源,或者通过允许第三方应用程序以自己的名义获取访问权限。 为了方便理解,可以想象OAuth2.0就是在用…...

vue和node使用websocket实现数据推送,实时聊天
需求:node做后端根据websocket,连接数据库,数据库的字段改变后,前端不用刷新页面也能更新到数据,前端也可以发送消息给后端,后端接受后把前端消息做处理再推送给前端展示 1.初始化node,生成pac…...

汽车电子笔记之:基于AUTOSAR的多核监控机制
目录 1、概述 2、系统监控的目标 2.1、任务的状态机 2.2、任务服务函数 2.3、任务周期性事件 2.4、时间监控的指标 2.5、时间监控的原理 2.6、CPU负载率监控原理 2.6.1、设计思路 2.6.2、监控方法的评价 3、基于WDGM模块热舞时序监控方法 3.1、活跃监督 3.2、截至时…...

GDB 源码分析 -- 断点源码解析
文章目录 一、断点简介1.1 硬件断点1.2 软件断点 二、断点源码分析2.1 断点相关结构体2.1.1 struct breakpoint2.1.2 struct bp_location 2.2 断点源码简介2.3 break设置断点2.4 enable break2.5 disable breakpoint2.6 delete breakpoint2.7 info break 命令源码解析 三、Linu…...

SpringMVC概述与简单使用
1.SpringMVC简介 SpringMVC也叫做Spring web mvc,是 Spring 框架的一部分,是在 Spring3.0 后发布的。 2.SpringMVC优点 1.基于 MVC 架构 基于 MVC 架构,功能分工明确。解耦合, 2.容易理解,上手快;使用简单。 就可以…...

传输层—UDP原理详解
目录 前言 1.netstat 2.pidof 3.UDP协议格式 4.UDP的特点 5.面向数据报 6.UDP的缓冲区 7.UDP使用注意事项 8.基于UDP的应用层协议 总结 前言 在之前的文章中为大家介绍了关于网络协议栈第一层就是应用层,包含套接字的使用,在应用层编码实现服务…...

CK-GW06-E03与汇川PLC的EtherNet/IP通信
准备阶段: CK-GWO6-E03网关POE交换机网线汇川PLC编程软件汇川AC801-0221-U0R0型号PLC 1.打开汇川PLC编程软件lnoProShop(V1.6.2)SP2 新建工程,选择对应的PLC型号,编程语言选择为“结构化文本(ST)语言”,然…...

UI界面自动化BagePage
常用basepage模块代码 # -*- coding: utf-8 -*- # Desc: UI自动化测试的一些基础浏览器操作方法# 第三方库导入 import time from logging import config import randomimport allure from selenium.webdriver.common.alert import Alert from selenium.webdriver.remote.webe…...

北京开发APP的费用明细
开发APP项目时,在功能确定后需要知道有哪些可能的费用,安排项目预算。北京开发APP的费用明细可能会包括以下几个部分,每个部分都会产生一些费用。今天和大家分享APP费用明细有哪些,希望对大家有所帮助。北京木奇移动技术有限公司&…...

2023年MySQL核心技术第一篇
目录 一 . 存储:一个完整的数据存储过程是怎样的? 1.1 数据存储过程 1.1.1 创建MySQl 数据库 1.1.1.1 为什么我们要先创建一个数据库,而不是直接创建数据表? 1.1.1.2基本操作部分 1.2 选择索引问题 二 . 字段:这么多的…...

通讯协议056——全网独有的OPC HDA知识一之接口(十一)IOPCHDA_Playback
本文简单介绍OPC HDA规范的IOPCHDA_Playback(可选)接口方法,更多通信资源请登录网信智汇(wangxinzhihui.com)。 此接口支持历史服务器的播放功能。这提供了从历史服务器获得初始数据集的能力,然后获得历史数据的持续更新。这与异…...

数学建模:数据的预处理
🔆 文章首发于我的个人博客:欢迎大佬们来逛逛 文章目录 数据预处理数据变换数据清洗缺失值处理异常值处理 数据预处理 数据变换 常见的数据变换的方式:通过某些简单的函数进行数据变换。 x ′ x 2 x ′ x x ′ log ( x ) ∇ f ( x k )…...

Linux土遁术之监测监测进程打开文件
分析问题过程中,追踪进程打开的文件可以在许多不同情况下有用,体现在以下几个方面: 故障排除和调试: 当程序出现问题、崩溃或异常行为时,追踪进程打开的文件可以帮助您找出问题的根本原因。这有助于快速定位错误&…...

css让多个盒子强制自动等宽
1.width: calc( 100 / n% ) 2.display:flex; flex:1;width:100px; 3.display:grid;grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); 但是其中某一个内容较长的时候 会破坏1:1:1的平衡 这个时候发现附件名字过长导致不等比例,通过查看阮一峰flex文…...

【高危】Apache Airflow Spark Provider 反序列化漏洞 (CVE-2023-40195)
zhi.oscs1024.com 漏洞类型反序列化发现时间2023-08-29漏洞等级高危MPS编号MPS-qkdx-17bcCVE编号CVE-2023-40195漏洞影响广度广 漏洞危害 OSCS 描述Apache Airflow Spark Provider是Apache Airflow项目的一个插件,用于在Airflow中管理和调度Apache Spar…...

树模型与集成学习:LightGBM
目录 树模型与集成学习 LightGBM 的贡献 LightGBM 的贡献:单边梯度抽样算法 LightGBM 的贡献:直方图算法 LightGBM 的贡献:互斥特征捆绑算法 LightGBM 的贡献:深度限制的 Leaf-wise 算法 树模型与集成学习 树模型是非常好的…...

PHP多语言代入电商平台api接口采集拼多多根据ID获取商品详情原数据示例
拼多多商品详情原数据API接口的作用是获取拼多多电商平台上某一商品的详细信息,包括商品的标题、价格、库存、图片、描述、包邮信息、销量、评价、优惠券等数据。通过该API接口可以获取到商品的原始数据,用于分析、筛选和展示商品信息。 pinduoduo.item…...

数据结构(Java实现)-二叉树(下)
获取二叉树的高度 检测值为value的元素是否存在(前序遍历) 层序遍历 判断一棵树是不是完全二叉树 获取节点的路径 二叉树的最近公共祖先...

如何利用 SmartX 存储性能测试工具 OWL 优化性能管理?
作者:深耕行业的 SmartX 金融团队 张瑞松 运维人员在日常管理集群时,有时难免会产生这样的困惑: 新业务准备上线,在具备多套存储的情况下,应如何选择承载业务的存储环境? 业务虚拟机刚上线时运行速度很快…...

固定资产管理措施怎么写
固定资产管理措施是指企业在进行固定资产管理时所采取的各种措施和方法。以下是一些常见的固定资产管理措施: 建立完善的固定资产管理制度。制定明确的资产采购、使用、维护、报废等流程和标准,确保资产管理的规范性和透明度。 采用先进的资产管理…...

C语言中typedef和const的区别
昨天面试的时候面试官问了这个问题,平时也有见过这两个类型限定词,可能是因为不怎么使用的原因,当问到它们的区别时,我不知道从哪个方面开始回答。 用 typedef 定义新的类型名 类型定义的书写格式为 typedef 类型 被定义的类型名 …...

大数据系列教程之 Kafka基础
kafka概述 一、kafka概述 1.1 定义1.2 消息队列 1.2.1 传统消息队列的应用场景1.2.2 消息队列的两种形式1.3 Kafka 基础架构二、kafka安装部署 2.1安装部署 2.1.1.jar包下载2.1.2.解压到指定的文件夹下2.1.3.创建两个文件夹以供后续使用2.1.4. 修改配置文件 (1&…...

【Go 基础篇】Go语言日期与时间函数详解:时间的掌控与转化
Go语言是一种快速、简洁且高效的编程语言,它在处理日期与时间方面提供了丰富的标准库函数。本文将详细介绍Go语言中处理日期与时间的函数,涵盖常用的日期时间操作、格式化、时区转换等内容,并介绍time.Time结构体中的相关方法。 时间的表示与…...

抽象工厂模式:创建相关对象族
欢迎阅读设计模式系列的第四篇文章!在前面的文章中,我们已经学习了设计模式的基本概念以及工厂方法模式的应用。 今天,我们将继续深入,介绍第三个模式——抽象工厂模式。 抽象工厂模式简介 抽象工厂模式是一种创建型设计模式&a…...

uniapp:安卓permission权限表及setting表
安卓permission权限表 权限名称说明android.permission.WRITE_USER_DICTIONARY允许应用程序向用户词典中写入新词android.permission.WRITE_SYNC_SETTINGS写入 Google 在线同步设置android.permission.WRITE_SOCIAL_STREAM读取用户的社交信息流android.permission.WRITE_SMS允…...

汽车服务门店小程序模板制作指南
在数字化时代,一个小程序的力量不可忽视。它不仅是展示品牌形象和提供用户服务的重要工具,更是扩大客户群体和提高营收的关键手段。对于汽车服务门店来说,拥有一个精美且功能齐全的小程序,更将成为你在竞争激烈的市场中的重要武器…...

Apache SeaTunnel 2.3.3 版本发布,CDC 支持 Schema Evolution!
时隔两个月, Apache SeaTunnel 终于迎来大版本更新。此次发布的 2.3.3 版本在功能和性能上均有较大优化改进,其中大家期待已久的 CDC Schema evolution(DDL 变更同步)、主键 Split 拆分、JDBC Sink 自动建表功能、SeaTunnel Zeta …...

工厂方法模式的概述和使用
目录 一、工厂方法模式概述1. 定义2. 使用动机 二、工厂方法模式结构1. 模式结构2. 时序图 三、工厂方法模式的使用实例四、工厂方法模式的优缺点五、工厂方法模式在Java中应用 原文链接 一、工厂方法模式概述 1. 定义 工厂方法模式(Factory Method Pattern)又称为工厂模式&…...

HP惠普星15青春版/惠普小欧笔记本电脑15s-du1008tx原装出厂Win11系统
适用型号:15s-du1007tx、15s-du1008tx、15s-du1009tx、15s-du1010tx、15s-du1011tx、15s-du1012tx、15s-du1013tx 自带所有驱动、出厂主题壁纸LOGO、Office办公软件、惠普电脑管家等预装程序 所需要工具:32G或以上的U盘 文件格式:ISO 文件大…...

聊聊检索增强,LangChain一把梭能行吗?
背景 ChatGPT诞生之初,大家仿佛从中看到了未来:可以拿着大语言模型(LLM)这把锤子,锤遍业务上的钉子。其中最被看好的场景,莫过于搜索,不仅是微软、谷歌、百度这样的大公司将LLM用到自己的搜索业…...