文章目录
- 认证的具体实现
- 环境的搭建
- 基础版授权服务搭建
- 引入依赖
- 创建数据表
- yml配置
- 配置SpringSecurity
- 定义认证授权的配置类
- 授权服务器存储客户端信息
- 修改授权服务配置,支持密码模式
- 基础版授权服务测试
- 授权码模式测试
- 密码模式测试
- **测试校验token接口**
- 整合JWT
- 使用jwt基础功能
- 使用非对称加密
- 扩展JWT中的存储内容
- 搭建User登录服务
- 搭建Gateway网关
- 快速搭建网关服务
- 思路分析
- 过滤
- 获取token
- 校验token
- 验证通过后
认证的具体实现
本文是基于SpringBoot2 + SpringSecurityOAuth2.0版本实现的
参考 代码地址
# 确定不拉代码 一边看代码一边看文档吗
git clone https://gitee.com/deimkf/authcenter.git
在线流程图
环境的搭建
创建一个父工程,主要做版本控制
<?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>org.example</groupId><artifactId>tl-authcenter</artifactId><version>1.0-SNAPSHOT</version><modules><module>hs-common</module><module>hs-authcenter</module></modules><packaging>pom</packaging><name>tl-authcenter</name><description>搭建一个OAuth2.0 密码模式的认证项目</description><properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><mysql-connector.version>8.0.15</mysql-connector.version><druid.version>1.1.10</druid.version><mybatis.version>3.5.3</mybatis.version><mybatis-plus.version>3.3.2</mybatis-plus.version><swagger2.version>2.7.0</swagger2.version><!-- 微服务技术栈版本 --><spring-boot.version>2.3.12.RELEASE</spring-boot.version><spring-cloud.version>Hoxton.SR12</spring-cloud.version><spring-cloud-alibaba.version>2.2.9.RELEASE</spring-cloud-alibaba.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><!--Spring Cloud 相关依赖--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><!--Spring Cloud Alibaba 相关依赖--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency><!--集成druid连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid.version}</version></dependency><!-- MyBatis--><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>${mybatis.version}</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><!--Mysql数据库驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql-connector.version}</version></dependency><!--Swagger-UI API文档生产工具--><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>${swagger2.version}</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>${swagger2.version}</version></dependency></dependencies></dependencyManagement></project>
创建一个common公共模块
<?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><artifactId>hs-common</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><name>hs-common</name><description>通用工程</description><parent><groupId>org.example</groupId><artifactId>tl-authcenter</artifactId><version>1.0-SNAPSHOT</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-commons</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>2.4</version></plugin></plugins></build></project>
并指定请求响应的具体格式
基础版授权服务搭建
引入依赖
<?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.example</groupId><artifactId>tl-authcenter</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>hs-authcenter</artifactId><version>1.0-SNAPSHOT</version><name>hs-authcenter</name><description>认证授权服务器</description><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!-- 公共模块--><dependency><groupId>org.example</groupId><artifactId>hs-common</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--nacos 注册中心 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!-- spring security oauth2--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency><!-- openfeign 服务远程调用 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!-- 数据库相关 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><!--Swagger-UI API文档生产工具 User对象需要用到Swagger相关的注释 --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.3.2.RELEASE</version><configuration><excludes><exclude><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId></exclude></excludes></configuration><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build>
</project>
创建数据表
在oauth_client_details中添加第三方客户端信息(client_id client_secret scope等等)
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`
(`client_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`access_token_validity` int(11) NULL DEFAULT NULL,`refresh_token_validity` int(11) NULL DEFAULT NULL,`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details`
VALUES ('client', NULL, '$2a$10$CE1GKj9eBZsNNMCZV2hpo.QBOz93ojy9mTd9YQaOy8H4JAyYKVlm6', 'all','authorization_code,password,refresh_token', 'http://www.baidu.com', NULL, 3600, 864000, NULL, NULL);
INSERT INTO `oauth_client_details`
VALUES ('hs-gateway', '', '$2a$10$4gbIfJBDuLtzB8EnLnP24eKQIMfXKPD6qJ8Lzklx5h9XeEt.VM/0C', 'read,write','password,refresh_token', NULL, NULL, 3600, 864000, NULL, NULL);
INSERT INTO `oauth_client_details`
VALUES ('hs-user', NULL, '$2a$10$APF9tE9z9Z74rcFZlUjvTeGpmH2XP1BdVTVrT6CLzTtSUVDNt2uJW', 'read,write','password,refresh_token', NULL, NULL, 3600, 864000, NULL, NULL);
这里的密文是通过SpringSecurity提供的加密类得到的
public static void main(String[] args) {PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();System.out.println(passwordEncoder.encode("123123"));
}
yml配置
server:port: 9999
spring:application:name: hs-authcenter-server#配置nacos注册中心地址cloud:nacos:discovery:server-addr: 127.0.0.1:8848 #注册中心地址username: nacospassword: nacosdatasource:url: jdbc:mysql://localhost:3306/oauth-server?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8username: rootpassword: 1234druid:initial-size: 5 #连接池初始化大小min-idle: 10 #最小空闲连接数max-active: 20 #最大连接数web-stat-filter:exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" #不统计这些请求数据stat-view-servlet: #访问监控网页的登录用户名和密码login-username: druidlogin-password: druid
配置SpringSecurity
这里就是SpringSecurity相关的配置
package com.hs.auth.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 9:50*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Autowiredprivate HushangUserDetailsService hushangUserDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 实现UserDetailsService获取用户信息auth.userDetailsService(hushangUserDetailsService);}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {// oauth2 密码模式需要拿到这个beanreturn super.authenticationManagerBean();}// SpringSecurity的基础配置,指定/oauth/**请求放行,比如进行授权、获取token等等都是/oauth开头的请求@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().permitAll().and().authorizeRequests().antMatchers("/oauth/**").permitAll().anyRequest().authenticated().and().logout().permitAll().and().csrf().disable();}
}
这里需要我们创建一个UserDetailsService
接口类型的bean,能够根据username获取到用户信息,我这里简单实现,直接写死一个 UserDetails
返回,先测试再优化
@Slf4j
@Component
public class HushangUserDetailsService implements UserDetailsService {@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {UserDetails user = User.withUsername("hushang").password(passwordEncoder.encode("123456")).roles("user").build();return user;}
}
按照正常的处理,应该是授权服务通过OpenFeign从user-server微服务获取用户信息信息,详细实现如下
import com.hs.authcenter.entity.User;
import com.hs.authcenter.entity.UserDetailsWrap;
import com.hs.authcenter.feign.UserFeignService;
import com.hs.common.api.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 10:01*/
@Slf4j
@Component
public class HushangUserDetailsService implements UserDetailsService {@Autowiredprivate UserFeignService userFeignService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 通过OpenFeign 远程调用user微服务获取用户相关的信息CommonResult<User> commonResult = userFeignService.queryUser(username);User user = commonResult.getData();if (user == null) {return null;}// 对user进行一个封装// 之所以要封装一下,是为了后续JWT生成token时,能往token保存更多user相关的信息return new UserDetailsWrap(user);}
}
package com.hs.authcenter.entity;import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.Arrays;
import java.util.Collection;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 15:21*/
@Data
public class UserDetailsWrap implements UserDetails {private User user;public UserDetailsWrap(User user) {this.user = user;}public UserDetailsWrap() {}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {//返回当前用户的权限return Arrays.asList(new SimpleGrantedAuthority(user.getRole()));}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return user.getStatus() == 1;}
}
定义认证授权的配置类
自定义一个配置类,添加@EnableAuthorizationServer
注解,并继承AuthorizationServerConfigurerAdapter
类,使用ctrl+O快捷键重写父类中的方法
import org.springframework.context.annotation.Configuration;
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;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 9:10*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {/*** 认证服务器的安全配置* @param security* @throws Exception*/@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {super.configure(security);}/*** 配置客户端属性* @param clients* @throws Exception*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {super.configure(clients);}/*** 配置授权服务器端点的非安全特性:如token store、token* @param endpoints* @throws Exception*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {super.configure(endpoints);}
}
接下来就是各个方法详细的实现
授权服务器存储客户端信息
授权码模式,先获取code,在调用获取token的url:
http://localhost:9999/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
password模式:
http://localhost:8080/oauth/token?username=hushang&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
首先是真实情况下的使用,去查询DB获取第三方Client信息
package com.hs.auth.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.client.JdbcClientDetailsService;import javax.sql.DataSource;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 9:10*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate DataSource dataSource;@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {super.configure(security);}/*** 配置客户端属性* @param clients* @throws Exception*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {// 配置授权服务器存储第三方客户端的信息 基于DB存储 oauth_client_detailsclients.withClientDetails(clientDetails());}@Beanpublic ClientDetailsService clientDetails(){// JdbcClientDetailsService就会去操作oauth_client_details数据表return new JdbcClientDetailsService(dataSource);}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {super.configure(endpoints);}
}
当然,也可以方便测试,直接使用基于内存的方式,往内存中整一个client信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {// 配置授权服务器存储第三方客户端的信息 基于DB存储 oauth_client_details// clients.withClientDetails(clientDetails());clients.inMemory()//配置client_id.withClient("client")//配置client-secret,passwordEncoder在SpringSecurity配置文件中会定义该bean对象,在这里直接@Autowired注入即可.secret(passwordEncoder.encode("123123"))//配置访问token的有效期.accessTokenValiditySeconds(3600)//配置刷新token的有效期.refreshTokenValiditySeconds(864000)//配置redirect_uri,用于授权成功后跳转.redirectUris("http://www.baidu.com")//配置申请的权限范围.scopes("all")/*** 配置grant_type,表示授权类型* authorization_code: 授权码* password: 密码* refresh_token: 更新令牌*/.authorizedGrantTypes("authorization_code","password","refresh_token");}
修改授权服务配置,支持密码模式
package com.hs.auth.config;import com.hs.auth.service.HushangUserDetailsService;
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.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.client.JdbcClientDetailsService;import javax.sql.DataSource;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 9:10*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate DataSource dataSource;/*** 我们自定义的查询用户信息的service类*/@Autowiredprivate HushangUserDetailsService hushangUserDetailsService;/*** 在SpringSecurity配置文件中,往Spring容器中添加了一个AuthenticationManager类型的bean*/@Autowiredprivate AuthenticationManager authenticationManagerBean;/*** 认证服务器的安全配置* @param security* @throws Exception*/@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {// 第三方客户端校验token需要带入 clientId 和clientSecret来校验security.checkTokenAccess("isAuthenticated()")// 来获取我们的tokenKey需要带入clientId,clientSecret.tokenKeyAccess("isAuthenticated()");//允许表单认证security.allowFormAuthenticationForClients();}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.withClientDetails(clientDetails());}@Beanpublic ClientDetailsService clientDetails(){return new JdbcClientDetailsService(dataSource);}/*** 配置授权服务器端点的非安全特性:如token store、token* @param endpoints* @throws Exception*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManagerBean)// refresh_token是否重复使用.reuseRefreshTokens(false)// 刷新令牌授权包含对用户信息的检查.userDetailsService(hushangUserDetailsService)// 支持GET,POST请求.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);}
}
基础版授权服务测试
授权码模式测试
调用localhost:9999/oauth/authorize
接口,携带请求类型为code授权码、client_id为client scope范围为all我们数据库中插入了该数据、因为我们还没有启动客户端,redirect_uri回调地址就先用百度的
访问url:http://localhost:9999/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
需要进行登录,用户名:hushang,密码:123456
我们直接选择Approve
接下来我们就会得到一个code
得到code之后,再获取token,发送请求http://localhost:9999/oauth/token?grant_type=authorization_code&client_id=client&client_secret=123123&scope=all&code=e2u3kv&redirect_uri=http://www.baidu.com
密码模式测试
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
如下,直接使用用户名和密码进行获取token
测试获取token,grant_type为password,并携带用户的用户名和密码、client_id+client_secret+scope这些都是要和客户端注册时的信息对应上
http://localhost:9999/oauth/token?username=hushang&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
测试校验token接口
因为授权服务器的security配置需要携带clientId和clientSecret,可以采用basic Auth的方式发请求
http://localhost:9999/oauth/check_token?token=50f43ec9-2852-4f80-8109-bed9a1c0a956
整合JWT
使用jwt基础功能
这里使用的是jwt的对称加密方式
创建一个jwt的配置类
@Configuration
public class JwtTokenStoreConfig {/*** JWT 加密密钥key*/private final String signingKey = "123123";@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter(){// 往Spring容器中添加一个JwtAccessTokenConverter类型的bean对象JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();jwtAccessTokenConverter.setSigningKey(signingKey);return jwtAccessTokenConverter;}@Beanpublic TokenStore jwtTokenStore(){// 往Spring容器中添加一个TokenStore类型的Bean对象// 而 JwtTokenStore 需要用到上面方法中的JwtAccessTokenConverterreturn new JwtTokenStore(jwtAccessTokenConverter());}
}
接下来修改认证授权的配置类,在最后添加两行jwt相关的代码
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.authenticationManager(authenticationManagerBean)// refresh_token是否重复使用.reuseRefreshTokens(false)// 刷新令牌授权包含对用户信息的检查.userDetailsService(hushangUserDetailsService)// 支持GET,POST请求.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)//指定token存储策略是jwt.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
}
接下来发送请求进行测试
http://localhost:9999/oauth/token?username=hushang&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
现在得到的token就是jwt生成 的token了
我们可以拿access_token中的数据去JWT的官网解析一下
使用非对称加密
使用对称加密的流程是:gateway网关需要对每一次请求,都要调用授权服务器进行token校验
http://localhost:9999/oauth/check_token?token=50f43ec9-2852-4f80-8109-bed9a1c0a956
如果使用非对称加密,那么gateway网关启动时从授权服务器拿一次公钥,以后的请求就直接在网关中进行token验证,直接使用公钥对token进行校验,就省了请求授权服务器进行token校验的请求了
第一步:生成jks 证书文件
我们使用jdk自动的工具生成,指定密钥生成的位置需要提前创建目录
命令格式
keytool
-genkeypair 生成密钥对
-alias jwt(别名)
-keypass 123456(别名密码)
-keyalg RSA(生证书的算法名称,RSA是一种非对称加密算法)
-keysize 1024(密钥长度,证书大小)
-validity 365(证书有效期,天单位)
-keystore D:/jwt/jwt.jks(指定生成证书的位置和证书名称)
-storepass 123456(获取keystore信息的密码)
-storetype (指定密钥仓库类型)
使用 “keytool -help” 获取所有可用命令
keytool -genkeypair -alias jwt -keyalg RSA -keysize 2048 -keystore D:/jwt/jwt.jks
执行结果
查看公钥信息
keytool -list -rfc --keystore jwt.jks | openssl x509 -inform pem -pubkey
因为windows不能使用openssl命令,我就直接使用的git命令窗执行的,但是这里有中文显示问题,不过结果还是正常输出了
将生成的jwt.jks文件cope到授权服务器的resource目录下
第二步:授权服务中增加jwt的属性配置类
在yml配置文件中添加配置
hs:jwt:keyPairName: jwt.jkskeyPairAlias: jwtkeyPairSecret: 123456keyPairStoreSecret: 123456
创建一个读取上面配置的类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;@Data
@ConfigurationProperties(prefix = "hs.jwt")
public class JwtCAProperties {/*** 证书名称*/private String keyPairName;/*** 证书别名*/private String keyPairAlias;/*** 证书私钥*/private String keyPairSecret;/*** 证书存储密钥*/private String keyPairStoreSecret;}
在JWT配置文件中导入上面创建的java类
@Configuration
@EnableConfigurationProperties(value = JwtCAProperties.class) // 添加该注解
public class JwtTokenStoreConfig {private final String signingKey = "123123";@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter(){JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();jwtAccessTokenConverter.setSigningKey(signingKey);return jwtAccessTokenConverter;}@Beanpublic TokenStore jwtTokenStore(){return new JwtTokenStore(jwtAccessTokenConverter());}
}
接下来就不使用上面的对称加密方式了,改为使用非对称加密的方式
@Configuration
@EnableConfigurationProperties(value = JwtCAProperties.class)
public class JwtTokenStoreConfig {/*** JWT 对称加密密钥key*/// private final String signingKey = "123123";/*** 注入证书properties配置信息*/@Autowiredprivate JwtCAProperties jwtCAProperties;@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter(){// 往Spring容器中添加一个JwtAccessTokenConverter类型的bean对象JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();// 使用对称加密方式// jwtAccessTokenConverter.setSigningKey(signingKey);//配置JWT使用的秘钥 非对称加密jwtAccessTokenConverter.setKeyPair(keyPair());return jwtAccessTokenConverter;}@Beanpublic TokenStore jwtTokenStore(){// 往Spring容器中添加一个TokenStore类型的Bean对象// 而 JwtTokenStore 需要用到上面方法中的JwtAccessTokenConverterreturn new JwtTokenStore(jwtAccessTokenConverter());}/*** 根据我们生成的证书,创建一个KeyPair对象* @return 非对称加密对象*/@Beanpublic KeyPair keyPair() {KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource(jwtCAProperties.getKeyPairName()), jwtCAProperties.getKeyPairSecret().toCharArray());return keyStoreKeyFactory.getKeyPair(jwtCAProperties.getKeyPairAlias(), jwtCAProperties.getKeyPairStoreSecret().toCharArray());}
}
接下来发送请求进行测试
http://localhost:9999/oauth/token?username=hushang&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
现在得到的token就是jwt 使用非对称加密算法 生成 的token了
现在就需要公钥才能token校验通过
扩展JWT中的存储内容
有时候我们需要扩展JWT中存储的内容,根据自己业务添加字段到Jwt中。
继承TokenEnhancer实现一个JWT内容增强器
import com.hs.common.entity.UserDetailsWrap;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;import java.util.HashMap;
import java.util.Map;/*** @Description: 对JWT生成的token进行增强* @Author 胡尚* @Date: 2024/7/26 15:14*/
public class JwtTokenEnhancer implements TokenEnhancer {@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {// 该对象就是我们自定义HushangUserDetailsService 返回的UserDetails对象UserDetailsWrap userDetails = (UserDetailsWrap)authentication.getPrincipal();final Map<String, Object> additionalInfo = new HashMap<>();final Map<String, Object> retMap = new HashMap<>();//todo 这里暴露UserId到Jwt的令牌中,后期可以根据自己的业务需要 进行添加字段additionalInfo.put("userId",userDetails.getUser().getId());additionalInfo.put("userName",userDetails.getUser().getUsername());retMap.put("additionalInfo", additionalInfo);((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(retMap);return accessToken;}
}
在JwtTokenStoreConfig中配置TulingTokenEnhancer
/**
* token的增强器 根据自己业务添加字段到Jwt中
* @return
*/
@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {return new JwtTokenEnhancer();
}
在授权服务器配置中配置JWT的内容增强器
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate DataSource dataSource;@Autowiredprivate HushangUserDetailsService hushangUserDetailsService;@Autowiredprivate AuthenticationManager authenticationManagerBean;@Autowired@Qualifier("jwtTokenStore")private TokenStore tokenStore;@Autowiredprivate JwtAccessTokenConverter jwtAccessTokenConverter;/*** 对jwt生成的token增强,添加等多的用户信息至token中*/@Autowiredprivate JwtTokenEnhancer jwtTokenEnhancer;@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.checkTokenAccess("isAuthenticated()").tokenKeyAccess("isAuthenticated()");security.allowFormAuthenticationForClients();}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.withClientDetails(clientDetails());}@Beanpublic ClientDetailsService clientDetails(){return new JdbcClientDetailsService(dataSource);}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {//配置JWT的内容增强器TokenEnhancerChain enhancerChain = new TokenEnhancerChain();List<TokenEnhancer> delegates = new ArrayList<>();delegates.add(jwtTokenEnhancer);delegates.add(jwtAccessTokenConverter);enhancerChain.setTokenEnhancers(delegates);endpoints.authenticationManager(authenticationManagerBean).reuseRefreshTokens(false).userDetailsService(hushangUserDetailsService).allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST).tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter)// jwt token增强,添加更多的 用户信息至token中.tokenEnhancer(enhancerChain);}
}
测试 验证
搭建User登录服务
Controller层代码
@RestController
@RequestMapping("/user")
@Api(tags = "UserController", description = "用户登录")
@Slf4j
public class UserController {@Autowiredprivate UserService userService;@ApiOperation("用户登录")@RequestMapping(value = "/login", method = RequestMethod.POST)@ResponseBodypublic CommonResult<Map> login(@RequestParam String username,@RequestParam String password,HttpServletRequest request){TokenInfo tokenInfo = userService.login(username, password);if (tokenInfo == null) {return CommonResult.validateFailed("用户名或密码错误");}Map<String, String> tokenMap = new HashMap<>();tokenMap.put("token", tokenInfo.getAccess_token());tokenMap.put("refreshToken",tokenInfo.getRefresh_token());// TODO 用户信息存redisreturn CommonResult.success(tokenMap);}// 编写一个接口,给授权服务器通过用户名查询用户信息@ApiOperation("查询用户信息")@GetMapping( "/queryUser")@ResponseBodypublic CommonResult<User> queryUser(@RequestParam String username, HttpServletRequest request){QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();userQueryWrapper.eq("username", username);User user = userService.getOne(userQueryWrapper);return CommonResult.success(user);}
}
Service层中的方法
package com.hs.user.service.impl;import com.hs.common.api.TokenInfo;
import com.hs.user.constant.MDA;
import com.hs.user.entity.User;
import com.hs.user.mapper.UserMapper;
import com.hs.user.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;/*** <p>* 用户表 服务实现类* </p>** @author 胡尚* @since 2024-07-26*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {@Autowiredprivate RestTemplate restTemplate;@Overridepublic TokenInfo login(String username, String password) {ResponseEntity<TokenInfo> response;try{//远程调用认证服务器 进行用户登陆response = restTemplate.exchange(MDA.OAUTH_LOGIN_URL, HttpMethod.POST, wrapOauthTokenRequest(username,password), TokenInfo.class);TokenInfo tokenInfo = response.getBody();log.info("根据用户名:{}登陆成功:TokenInfo:{}",username,tokenInfo);return tokenInfo;}catch (Exception e) {log.error("根据用户名:{}登陆异常:{}",username,e.getMessage());e.printStackTrace();return null;}}/*** 方法实现说明:封装用户到认证中心的请求头 和请求参数* @author:smlz* @param userName 用户名* @param password 密码* @return:* @exception:* @date:2020/1/22 15:32*/private HttpEntity<MultiValueMap<String, String>> wrapOauthTokenRequest(String userName, String password) {//封装oauth2 请求头 clientId clientSecretHttpHeaders httpHeaders = wrapHttpHeaders();//封装请求参数MultiValueMap<String, String> reqParams = new LinkedMultiValueMap<>();reqParams.add(MDA.USER_NAME,userName);reqParams.add(MDA.PASS,password);reqParams.add(MDA.GRANT_TYPE,MDA.PASS);reqParams.add(MDA.SCOPE,MDA.SCOPE_AUTH);//封装请求参数HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(reqParams, httpHeaders);return entity;}/*** 方法实现说明:封装请求头* @author:smlz* @return:HttpHeaders* @exception:* @date:2020/1/22 16:10*/private HttpHeaders wrapHttpHeaders() {HttpHeaders httpHeaders = new HttpHeaders();httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);httpHeaders.setBasicAuth(MDA.CLIENT_ID,MDA.CLIENT_SECRET);return httpHeaders;}
}
Service方法中使用到的常量类
public class MDA {/*** 会员服务第三方客户端(这个客户端在认证服务器配置好的oauth_client_details)*/public static final String CLIENT_ID = "hs-user";/*** 会员服务第三方客户端密码(这个客户端在认证服务器配置好的oauth_client_details)*/public static final String CLIENT_SECRET = "123123";/*** 认证服务器登陆地址*/public static final String OAUTH_LOGIN_URL = "http://hs-authcenter-server/oauth/token";public static final String USER_NAME = "username";public static final String PASS = "password";public static final String GRANT_TYPE = "grant_type";public static final String SCOPE = "scope";public static final String SCOPE_AUTH = "read";
}
搭建Gateway网关
快速搭建网关服务
引入依赖
<!-- gateway网关 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency><!-- nacos服务注册与发现 -->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置文件编写
server:port: 8080
spring:application:name: hs-gateway-server#配置nacos注册中心地址cloud:nacos:discovery:server-addr: 127.0.0.1:8848 #注册中心地址username: nacospassword: nacosgateway:routes:- id: user_routeuri: lb://hs-user-serverpredicates:- Path=/user/** # 断言,路径相匹配的进行路由
进行测试,从网关发送请求,路由到user服务
思路分析
接下来我们需要在网关层对所有请求做全局统一认证,主要步骤如下所示:
-
过滤掉不需要认证的url,比如
/user/login
, 或者/oauth/**
-
获取token。
从请求头中获取token:Authorization value: bearer xxxxxxx
或者从请求参数中解析token: access_token
-
校验token
gateway服务启动时从授权服务器获取公钥
拿到token后,通过公钥校验
校验失败或超时抛出异常
-
验证通过后,从token中获取用户信息保存在请求头中
过滤
过滤不需要认证的url ,可以通过yml设置不需要认证的url。
yml配置文件中添加下面的内容
hs:gateway:shouldSkipUrls:- /auth/**- /user/login
创建读取配置文件内容的java类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;import java.util.LinkedHashSet;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 16:36*/
@Data
@ConfigurationProperties(prefix = "hs.gateway")
public class NotAuthUrlProperties {private LinkedHashSet<String> shouldSkipUrls;
}
创建一个全局Filter类
import com.hs.gateway.properties.NotAuthUrlProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;/*** @Description: 验证token* @Author 胡尚* @Date: 2024/7/26 16:41*/
@Slf4j
@Component
@Order(1)
@EnableConfigurationProperties(value = NotAuthUrlProperties.class)
public class AuthenticationFilter implements GlobalFilter, InitializingBean {@Autowiredprivate NotAuthUrlProperties notAuthUrlProperties;@Overridepublic void afterPropertiesSet() throws Exception {// TODO 远程调用授权服务器获取公钥}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String path = exchange.getRequest().getURI().getPath();// 过滤不需要认证的urlif (shouldSkip(path)){log.debug("请求不用认证:{}", path);return chain.filter(exchange);}log.debug("对请求进行校验:{}", path);// TODO 校验tokenreturn chain.filter(exchange);}/*** 过滤掉不需要认证的url* @param requestPath 当前请求* @return true表示不需要认证*/private boolean shouldSkip(String requestPath) {//路径匹配器(简介SpringMvc拦截器的匹配器)//比如/oauth/** 可以匹配/oauth/token /oauth/check_token等AntPathMatcher antPathMatcher = new AntPathMatcher();for (String shouldSkipUrl : notAuthUrlProperties.getShouldSkipUrls()) {if (antPathMatcher.match(shouldSkipUrl, requestPath)){return true;}}return false;}
}
获取token
package com.hs.gateway.filter;import com.hs.gateway.properties.NotAuthUrlProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 16:41*/
@Slf4j
@Component
@Order(1)
@EnableConfigurationProperties(value = NotAuthUrlProperties.class)
public class AuthenticationFilter implements GlobalFilter, InitializingBean {@Autowiredprivate NotAuthUrlProperties notAuthUrlProperties;@Overridepublic void afterPropertiesSet() throws Exception {// TODO 远程调用授权服务器获取公钥}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String path = exchange.getRequest().getURI().getPath();if (shouldSkip(path)){log.info("请求不用认证:{}", path);return chain.filter(exchange);}log.info("对请求进行校验:{}", path);// 获取token// 解析出我们Authorization的请求头 value为: “bearer XXXXXXXXXXXXXX”String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");if (StringUtils.isEmpty(authHeader)){log.warn("不是放行请求,却未携带token:{}", path);// 抛业务自定义异常 我这里就直接随便抛异常了throw new RuntimeException();}return chain.filter(exchange);}private boolean shouldSkip(String requestPath) {AntPathMatcher antPathMatcher = new AntPathMatcher();for (String shouldSkipUrl : notAuthUrlProperties.getShouldSkipUrls()) {if (antPathMatcher.match(shouldSkipUrl, requestPath)){return true;}}return false;}
}
校验token
校验token
拿到token后,通过公钥(需要从授权服务获取公钥)校验,校验失败或超时抛出异常
引入依赖
<!--添加jwt相关的包-->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.10.5</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.10.5</version><scope>runtime</scope>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.10.5</version><scope>runtime</scope>
</dependency>
创建一个JWTUtils工具类
package com.hs.gateway.utils;import com.alibaba.cloud.commons.lang.StringUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.http.*;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 17:14*/
@Slf4j
public class JwtUtils {/*** 认证服务器许可我们的网关的clientId(需要在oauth_client_details表中配置)*/private static final String CLIENT_ID = "hs-gateway";/*** 认证服务器许可我们的网关的client_secret(需要在oauth_client_details表中配置)*/private static final String CLIENT_SECRET = "123123";/*** 认证服务器暴露的获取token_key的地址*/private static final String AUTH_TOKEN_KEY_URL = "http://hs-authcenter-server/oauth/token_key";/*** 请求头中的 token的开始*/private static final String AUTH_HEADER = "Bearer ";/*** 方法实现说明: 通过远程调用获取认证服务器颁发jwt的解析的key** @param restTemplate 远程调用的操作类* @author:smlz* @return: tokenKey 解析jwt的tokenKey* @exception:* @date:2020/1/22 11:31*/private static String getTokenKeyByRemoteCall(RestTemplate restTemplate) throws Exception {//第一步:封装请求头HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);headers.setBasicAuth(CLIENT_ID, CLIENT_SECRET);HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(null, headers);//第二步:远程调用获取token_keytry {ResponseEntity<Map> response = restTemplate.exchange(AUTH_TOKEN_KEY_URL, HttpMethod.GET, entity, Map.class);String tokenKey = response.getBody().get("value").toString();log.info("去认证服务器获取Token_Key:{}", tokenKey);return tokenKey;} catch (Exception e) {log.error("远程调用认证服务器获取Token_Key失败:{}", e.getMessage());// TODO 抛业务自定义异常 我这里就直接随便抛异常了throw new RuntimeException();}}/*** 方法实现说明:生成公钥** @param restTemplate:远程调用操作类* @author:smlz* @return: PublicKey 公钥对象* @exception:* @date:2020/1/22 11:52*/public static PublicKey genPulicKey(RestTemplate restTemplate) throws Exception {String tokenKey = getTokenKeyByRemoteCall(restTemplate);try {//把获取的公钥开头和结尾替换掉String dealTokenKey = tokenKey.replaceAll("\\-*BEGIN PUBLIC KEY\\-*", "").replaceAll("\\-*END PUBLIC KEY\\-*", "").trim();java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(dealTokenKey));KeyFactory keyFactory = KeyFactory.getInstance("RSA");PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);log.info("生成公钥:{}", publicKey);return publicKey;} catch (Exception e) {log.info("生成公钥异常:{}", e.getMessage());// TODO 抛业务自定义异常 我这里就直接随便抛异常了throw new RuntimeException();}}public static Claims validateJwtToken(String authHeader, PublicKey publicKey) {String token = null;try {token = StringUtils.substringAfter(authHeader, AUTH_HEADER);Jwt<JwsHeader, Claims> parseClaimsJwt = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);Claims claims = parseClaimsJwt.getBody();//log.info("claims:{}",claims);return claims;} catch (Exception e) {log.error("校验token异常:{},异常信息:{}", token, e.getMessage());// TODO 抛业务自定义异常 我这里就直接随便抛异常了throw new RuntimeException();}}
}
并对我们的RestTemplate进行增强
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;import java.util.Collections;/*** @Description: 之所以要单独为RestTemplate进行增强的原因是,@LoadBalancer注解是在* 所有非懒加载单例bean创建完成之后通过SmartInitializingSingleton机制在对RestTemplate对象进行增强,* 但是我现在需要在bean初始化的过程中需要发送请求,那么就只能是我们自己对RestTemplate对象进行增强了* @Author 胡尚* @Date: 2024/7/26 17:20*/
@Configuration
public class RibbonConfig {@Autowiredprivate LoadBalancerClient loadBalancer;@Beanpublic RestTemplate restTemplate(){RestTemplate restTemplate = new RestTemplate();restTemplate.setInterceptors(Collections.singletonList(new LoadBalancerInterceptor(loadBalancer)));return restTemplate;}
}
在对全局Filter进行添加
import com.hs.gateway.properties.NotAuthUrlProperties;
import com.hs.gateway.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import java.security.PublicKey;/*** @Description: TODO* @Author 胡尚* @Date: 2024/7/26 16:41*/
@Slf4j
@Component
@Order(1)
@EnableConfigurationProperties(value = NotAuthUrlProperties.class)
public class AuthenticationFilter implements GlobalFilter, InitializingBean {@Autowiredprivate NotAuthUrlProperties notAuthUrlProperties;// 注入我们增强之后的RestTemplate对象@Autowiredprivate RestTemplate restTemplate;private PublicKey publicKey;@Overridepublic void afterPropertiesSet() throws Exception {// 初始化bean过程中向授权服务器发送请求,获取公钥publicKey = JwtUtils.genPulicKey(restTemplate);}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String path = exchange.getRequest().getURI().getPath();if (shouldSkip(path)) {log.info("请求不用认证:{}", path);return chain.filter(exchange);}log.info("对请求进行校验:{}", path);String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");if (StringUtils.isEmpty(authHeader)) {log.warn("不是放行请求,却未携带token:{}", path);throw new RuntimeException();}//3. 校验token// 拿到token后,通过公钥(需要从授权服务获取公钥)校验// 校验失败或超时抛出异常//第三步 校验我们的jwt 若jwt不对或者超时都会抛出异常Claims claims = JwtUtils.validateJwtToken(authHeader, publicKey);return chain.filter(exchange);}private boolean shouldSkip(String requestPath) {AntPathMatcher antPathMatcher = new AntPathMatcher();for (String shouldSkipUrl : notAuthUrlProperties.getShouldSkipUrls()) {if (antPathMatcher.match(shouldSkipUrl, requestPath)) {return true;}}return false;}
}
验证通过后
校验通过后,从token中获取的用户登录信息存储到请求头中
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String path = exchange.getRequest().getURI().getPath();if (shouldSkip(path)) {log.info("请求不用认证:{}", path);return chain.filter(exchange);}log.info("对请求进行校验:{}", path);String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");if (StringUtils.isEmpty(authHeader)) {log.warn("不是放行请求,却未携带token:{}", path);throw new RuntimeException();}Claims claims = JwtUtils.validateJwtToken(authHeader, publicKey);//4. 校验通过后,从token中获取的用户登录信息存储到请求头中//第四步 把从jwt中解析出来的 用户登陆信息存储到请求头中ServerWebExchange webExchange = wrapHeader(exchange,claims);return chain.filter(webExchange);
}private ServerWebExchange wrapHeader(ServerWebExchange serverWebExchange,Claims claims) {String loginUserInfo = JSON.toJSONString(claims);log.info("jwt的用户信息:{}",loginUserInfo);// 这里的数据就和我们在授权服务器对JwtToken增强,往token中保存的信息对应上了Map<String, Object> additionalInfo = claims.get("additionalInfo", Map.class);Integer userId = (Integer) additionalInfo.get("userId");String userName = (String) additionalInfo.get("userName");//向headers中放文件,记得buildServerHttpRequest request = serverWebExchange.getRequest().mutate().header("username",userName).header("userId",userId+"").build();//将现在的request 变成 change对象return serverWebExchange.mutate().request(request).build();
}