文章目录
认证的具体实现
本文是基于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: nacos
password: nacos
datasource:
url: jdbc:mysql://localhost:3306/oauth-server?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: 1234
druid:
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: druid
login-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 {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private HushangUserDetailsService hushangUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 实现UserDetailsService获取用户信息
auth.userDetailsService(hushangUserDetailsService);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
// oauth2 密码模式需要拿到这个bean
return super.authenticationManagerBean();
}
// SpringSecurity的基础配置,指定/oauth/**请求放行,比如进行授权、获取token等等都是/oauth开头的请求
@Override
protected 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 {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public 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 {
@Autowired
private UserFeignService userFeignService;
@Override
public 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() {
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//返回当前用户的权限
return Arrays.asList(new SimpleGrantedAuthority(user.getRole()));
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public 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
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
}
/**
* 配置客户端属性
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
}
/**
* 配置授权服务器端点的非安全特性:如token store、token
* @param endpoints
* @throws Exception
*/
@Override
public 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 {
@Autowired
private DataSource dataSource;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
}
/**
* 配置客户端属性
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置授权服务器存储第三方客户端的信息 基于DB存储 oauth_client_details
clients.withClientDetails(clientDetails());
}
@Bean
public ClientDetailsService clientDetails(){
// JdbcClientDetailsService就会去操作oauth_client_details数据表
return new JdbcClientDetailsService(dataSource);
}
@Override
public 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 {
@Autowired
private DataSource dataSource;
/**
* 我们自定义的查询用户信息的service类
*/
@Autowired
private HushangUserDetailsService hushangUserDetailsService;
/**
* 在SpringSecurity配置文件中,往Spring容器中添加了一个AuthenticationManager类型的bean
*/
@Autowired
private AuthenticationManager authenticationManagerBean;
/**
* 认证服务器的安全配置
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 第三方客户端校验token需要带入 clientId 和clientSecret来校验
security.checkTokenAccess("isAuthenticated()")
// 来获取我们的tokenKey需要带入clientId,clientSecret
.tokenKeyAccess("isAuthenticated()");
//允许表单认证
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
@Bean
public ClientDetailsService clientDetails(){
return new JdbcClientDetailsService(dataSource);
}
/**
* 配置授权服务器端点的非安全特性:如token store、token
* @param endpoints
* @throws Exception
*/
@Override
public 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";
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
// 往Spring容器中添加一个JwtAccessTokenConverter类型的bean对象
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(signingKey);
return jwtAccessTokenConverter;
}
@Bean
public TokenStore jwtTokenStore(){
// 往Spring容器中添加一个TokenStore类型的Bean对象
// 而 JwtTokenStore 需要用到上面方法中的JwtAccessTokenConverter
return 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.jks
keyPairAlias: jwt
keyPairSecret: 123456
keyPairStoreSecret: 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";
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(signingKey);
return jwtAccessTokenConverter;
}
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
接下来就不使用上面的对称加密方式了,改为使用非对称加密的方式
@Configuration
@EnableConfigurationProperties(value = JwtCAProperties.class)
public class JwtTokenStoreConfig {
/**
* JWT 对称加密密钥key
*/
// private final String signingKey = "123123";
/**
* 注入证书properties配置信息
*/
@Autowired
private JwtCAProperties jwtCAProperties;
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
// 往Spring容器中添加一个JwtAccessTokenConverter类型的bean对象
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
// 使用对称加密方式
// jwtAccessTokenConverter.setSigningKey(signingKey);
//配置JWT使用的秘钥 非对称加密
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
}
@Bean
public TokenStore jwtTokenStore(){
// 往Spring容器中添加一个TokenStore类型的Bean对象
// 而 JwtTokenStore 需要用到上面方法中的JwtAccessTokenConverter
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 根据我们生成的证书,创建一个KeyPair对象
* @return 非对称加密对象
*/
@Bean
public 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 {
@Override
public 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 {
@Autowired
private DataSource dataSource;
@Autowired
private HushangUserDetailsService hushangUserDetailsService;
@Autowired
private AuthenticationManager authenticationManagerBean;
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
* 对jwt生成的token增强,添加等多的用户信息至token中
*/
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("isAuthenticated()")
.tokenKeyAccess("isAuthenticated()");
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
@Bean
public ClientDetailsService clientDetails(){
return new JdbcClientDetailsService(dataSource);
}
@Override
public 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 {
@Autowired
private UserService userService;
@ApiOperation("用户登录")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public 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 用户信息存redis
return CommonResult.success(tokenMap);
}
// 编写一个接口,给授权服务器通过用户名查询用户信息
@ApiOperation("查询用户信息")
@GetMapping( "/queryUser")
@ResponseBody
public 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 {
@Autowired
private RestTemplate restTemplate;
@Override
public 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 clientSecret
HttpHeaders 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: nacos
password: nacos
gateway:
routes:
- id: user_route
uri: lb://hs-user-server
predicates:
- 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 {
@Autowired
private NotAuthUrlProperties notAuthUrlProperties;
@Override
public void afterPropertiesSet() throws Exception {
// TODO 远程调用授权服务器获取公钥
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 过滤不需要认证的url
if (shouldSkip(path)){
log.debug("请求不用认证:{}", path);
return chain.filter(exchange);
}
log.debug("对请求进行校验:{}", path);
// TODO 校验token
return 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 {
@Autowired
private NotAuthUrlProperties notAuthUrlProperties;
@Override
public void afterPropertiesSet() throws Exception {
// TODO 远程调用授权服务器获取公钥
}
@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);
// 获取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_key
try {
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 {
@Autowired
private LoadBalancerClient loadBalancer;
@Bean
public 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 {
@Autowired
private NotAuthUrlProperties notAuthUrlProperties;
// 注入我们增强之后的RestTemplate对象
@Autowired
private RestTemplate restTemplate;
private PublicKey publicKey;
@Override
public void afterPropertiesSet() throws Exception {
// 初始化bean过程中向授权服务器发送请求,获取公钥
publicKey = JwtUtils.genPulicKey(restTemplate);
}
@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();
}
//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中放文件,记得build
ServerHttpRequest request = serverWebExchange.getRequest().mutate()
.header("username",userName)
.header("userId",userId+"")
.build();
//将现在的request 变成 change对象
return serverWebExchange.mutate().request(request).build();
}