Bootstrap

OAuth2 + Gateway统一认证一步步实现(公司项目能直接使用),密码模式&授权码模式

认证的具体实现

本文是基于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();
}
;