基于springboot3实现单点登录(二):认证服务端搭建

news2024/9/20 10:50:44

前言

上文我们介绍了oauth2.0的相关理论和流程,本文我们继续实现。

Oauth2协议中有个很重要的概念,叫做”端点“,

以下整理了一些常用的端点及其参考访问路径及使用场景的信息,供参考。

这些端点在oauth2.0协议的整个生命周期中扮演着非常重要的角色。

端点说明参考值使用场景
访问授权端点/oauth2/authorize当客户端需要请求用户的授权时,会将用户重定向到这个端点。用户在这里进行登录验证,并决定是否授权给客户端。一旦用户同意授权,授权服务器会重定向用户回客户端预先注册的重定向URI,携带一个授权码或直接的访问令牌(取决于所使用的授权类型)
获取令牌端点/oauth2/token客户端使用这个端点来交换授权码、用户密码、客户端证书等凭证,以获取访问令牌和刷新令牌。客户端在请求时通常需要进行身份验证,以证明自己有权请求令牌。
用户信息端点/oauth2/userInfo客户端使用访问令牌访问这个端点,以获取授权用户的基本信息,如用户名、电子邮件地址等。这通常用于获取用户个人资料,以便于个性化用户体验或进行进一步的业务逻辑处理。
撤销令牌端点/oauth2/revoke当访问令牌或刷新令牌不再需要或应当被撤销时,客户端或资源所有者可以使用这个端点来撤销令牌。这有助于保护用户数据的安全,防止令牌被滥用。
校验令牌端点/oauth2/introspect资源服务器使用这个端点来检查访问令牌的有效性和属性,以决定是否允许客户端访问受保护资源。这有助于确保只有有效的令牌才能访问资源。
公钥端点/oauth2/jwks如果使用JWT(JSON Web Tokens)作为令牌,客户端可以使用这个端点来获取用于验证JWT签名的公钥。这对于确保令牌未被篡改非常重要。
刷新令牌端点/oauth2/token实际上,刷新令牌的功能通常是在令牌端点中实现的。客户端使用刷新令牌来获取新的访问令牌,当原始的访问令牌过期时,这允许客户端无需重新进行用户授权即可继续访问资源。

实现一个oauth2.0服务器服务就需要提供以上这些端点的实现,本文我们实现一个基于Oauth2.0的认证服务器,并通过验证这些端点来验证其完整性和可用性。

准备

本文所依赖的环境如下:

环境/工具名称版本
Java17
Springboot3.2.2
MybatisPlus3.5.5
Authorization-Server1.3.1
Spring6.3.3
SpringSecurity6.3.3
Maven3.6.5
MySql8.0.33

开始

1. 新建springboot+maven项目

2. 修改pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zjtx.tech.security</groupId>
    <artifactId>springboot3-sso</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.2</version>
    </parent>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
            <version>3.1.0</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>public</id>
            <name>Aliyun Public Repository</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring</id>
            <name>Aliyun Spring Repository</name>
            <url>https://maven.aliyun.com/repository/spring</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>maven</id>
            <name>Maven Central Repository</name>
            <url>https://repo.maven.apache.org/maven2</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>sonatype</id>
            <name>Sonatype Repository</name>
            <url>https://s01.oss.sonatype.org/content/groups/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
            </snapshots>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>public</id>
            <name>aliyun nexus</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

    <build>
        <finalName>sso-server</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.5</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

3. 添加认证服务器配置文件

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.zjtx.tech.security.demo.provider.MyAuthenticationEntryPoint;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Value("${spring.security.oauth2.authorization-server.issuer}")
    private String issuer;

    @Resource
    private TokenAuthenticationFilter jwtTokenAuthenticationFilter;

    @Resource
    private MyAuthenticationEntryPoint authenticationEntryPoint;

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        // 配置OAuth2授权服务器的默认安全设置
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        // 配置授权端点和OIDC,默认设置
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .authorizationEndpoint(Customizer.withDefaults())
                .oidc(Customizer.withDefaults());
        // 配置异常处理,包括访问被拒绝和未认证的处理方式
        http.exceptionHandling((exceptions) -> exceptions
                        .accessDeniedHandler((request, response, accessDeniedException) ->
                                accessDeniedException.printStackTrace())
                        .authenticationEntryPoint(authenticationEntryPoint))
                // 配置OAuth2资源服务器,使用JWT令牌验证
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()));
        // 构建并返回安全配置
        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .requestMatchers("/css/**", "/js/**", "/images/**",
                        "/webjars/**", "/favicon.ico", "/login.html");
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        // 配置HTTP安全设置,包括授权规则、CORS、会话管理、过滤器、表单登录、CSRF保护和异常处理
        http
                // 设置对特定路径的请求不需要认证,如/auth/**和/oauth2/**的请求
                .authorizeHttpRequests((authorize) ->
                        authorize.requestMatchers(
                                        new AntPathRequestMatcher("/auth/**"),
                                        new AntPathRequestMatcher("/oauth2/**"))
                                .permitAll()
                                // 设置任何其他请求都需要认证
                                .anyRequest().authenticated())
                // 配置CORS,使用默认设置
                .cors(Customizer.withDefaults())
                // 设置总是创建会话,确保每次请求都有会话
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
                // 在UsernamePasswordAuthenticationFilter之前添加自定义的JWT令牌认证过滤器
                .addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                // 配置表单登录,使用默认设置
                .formLogin(Customizer.withDefaults())
                // 禁用CSRF保护
                .csrf(AbstractHttpConfigurer::disable)
                // 配置异常处理,设置未认证的请求跳转到登录页面
                .exceptionHandling((exceptions) -> {
                    exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login.html"));
                });
        // 构建并返回安全配置对象
        return http.build();
    }


    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("oauth2-client")
                .clientSecret(passwordEncoder.encode("123456"))
                // 客户端认证基于请求头
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 配置授权的支持方式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .redirectUri("https://www.baidu.com")
                .scope("user")
                .scope("admin")
                // 客户端设置,设置用户需要确认授权
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false) .build())
                .build();
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());
        if (repositoryByClientId == null) {
            registeredClientRepository.save(registeredClient);
        }
        return registeredClientRepository;
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        // 配置 JWK 源
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    private static KeyPair generateRsaKey() {
        // 生成 RSA 密钥对
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     * 配置 JWT 解码器
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 配置授权服务器设置
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().issuer(issuer).build();
    }

    /**
     * 配置认证管理器
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 配置安全上下文存储库
     */
    @Bean
    public SecurityContextRepository securityContextRepository() {
        return new HttpSessionSecurityContextRepository();
    }

    /**
     * 配置授权服务
     */
    @Bean
    public OAuth2AuthorizationService auth2AuthorizationService(RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 配置授权同意服务
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }
}

针对上述配置,作如下说明:

  1. OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http):应用默认的安全配置。
  2. http.getConfigurer(OAuth2AuthorizationServerConfigurer.class):配置授权端点和OIDC的默认设置, 开启了OIDC后会开启一些节点,如/oauth2/jwks, /.well-known/openid-configuration
  3. http.exceptionHandling((exceptions) -> exceptions:配置异常处理,包括访问被拒绝和未认证的处理方式。
  4. http.oauth2ResourceServer((resourceServer) -> resourceServer:配置OAuth2资源服务器,使用JWT令牌验证。
  5. webSecurityCustomizer():定义了一个Web安全自定义器,用于忽略某些请求路径的安全检查。
  6. defaultSecurityFilterChain(http):配置HTTP安全设置,包括授权规则、CORS、会话管理、过滤器、表单登录、CSRF保护和异常处理。
  7. registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder):配置注册客户端存储库。
  8. jwkSource():配置JWK源,用于生成RSA密钥对。
  9. jwtDecoder(JWKSource jwkSource):配置JWT解码器。
  10. authorizationServerSettings():配置授权服务器设置。
  11. authenticationManager(AuthenticationConfiguration authenticationConfiguration):配置认证管理器。
  12. securityContextRepository():配置安全上下文存储库。
  13. auth2AuthorizationService(RegisteredClientRepository registeredClientRepository):配置授权服务。
  14. authorizationConsentService(RegisteredClientRepository registeredClientRepository):配置授权同意服务。

4. 添加配置文件

server:
  port: 8080

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://ip:port/dbname?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    username: username
    password: password

  security:
    oauth2:
      authorization-server:
        issuer: http://localhost:8080

logging:
  level:
    org.springframework.security: info

数据库脚本文件:

CREATE TABLE oauth2_registered_client (
  id varchar(100) NOT NULL,
  client_id varchar(100) NOT NULL,
  client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
  client_secret varchar(200) DEFAULT NULL,
  client_secret_expires_at timestamp DEFAULT NULL,
  client_name varchar(200) NOT NULL,
  client_authentication_methods varchar(1000) NOT NULL,
  authorization_grant_types varchar(1000) NOT NULL,
  redirect_uris varchar(1000) DEFAULT NULL,
  post_logout_redirect_uris varchar(1000) DEFAULT NULL,
  scopes varchar(1000) NOT NULL,
  client_settings varchar(2000) NOT NULL,
  token_settings varchar(2000) NOT NULL,
  PRIMARY KEY (id)
);

/*
IMPORTANT:
    If using PostgreSQL, update ALL columns defined with 'blob' to 'text',
    as PostgreSQL does not support the 'blob' data type.
*/
CREATE TABLE oauth2_authorization (
    id varchar(100) NOT NULL,
    registered_client_id varchar(100) NOT NULL,
    principal_name varchar(200) NOT NULL,
    authorization_grant_type varchar(100) NOT NULL,
    authorized_scopes varchar(1000) DEFAULT NULL,
    attributes blob DEFAULT NULL,
    state varchar(500) DEFAULT NULL,
    authorization_code_value blob DEFAULT NULL,
    authorization_code_issued_at timestamp DEFAULT NULL,
    authorization_code_expires_at timestamp DEFAULT NULL,
    authorization_code_metadata blob DEFAULT NULL,
    access_token_value blob DEFAULT NULL,
    access_token_issued_at timestamp DEFAULT NULL,
    access_token_expires_at timestamp DEFAULT NULL,
    access_token_metadata blob DEFAULT NULL,
    access_token_type varchar(100) DEFAULT NULL,
    access_token_scopes varchar(1000) DEFAULT NULL,
    oidc_id_token_value blob DEFAULT NULL,
    oidc_id_token_issued_at timestamp DEFAULT NULL,
    oidc_id_token_expires_at timestamp DEFAULT NULL,
    oidc_id_token_metadata blob DEFAULT NULL,
    refresh_token_value blob DEFAULT NULL,
    refresh_token_issued_at timestamp DEFAULT NULL,
    refresh_token_expires_at timestamp DEFAULT NULL,
    refresh_token_metadata blob DEFAULT NULL,
    user_code_value blob DEFAULT NULL,
    user_code_issued_at timestamp DEFAULT NULL,
    user_code_expires_at timestamp DEFAULT NULL,
    user_code_metadata blob DEFAULT NULL,
    device_code_value blob DEFAULT NULL,
    device_code_issued_at timestamp DEFAULT NULL,
    device_code_expires_at timestamp DEFAULT NULL,
    device_code_metadata blob DEFAULT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE oauth2_authorization_consent (
  registered_client_id varchar(100) NOT NULL,
  principal_name varchar(200) NOT NULL,
  authorities varchar(1000) NOT NULL,
  PRIMARY KEY (registered_client_id, principal_name)
);

5. resources下的静态页面

本文目前的配置是前后端不分离场景的实现,也就是说它的登录页面其实是在resources文件夹下的。

如果需要做前后端分离的实现的话需要给前端返回401,由前端重定向到登录页面即可。

这里给出简单示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>统一身份认证-登陆</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta http-equiv="Access-Control-Allow-Origin" content="*">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="format-detection" content="telephone=no">

    <link rel="stylesheet" href="./js/layui/css/layui.css" />
    <link rel="stylesheet" href="./css/layui-blue.css" />
    <script type="text/javascript" src="./js/layui/layui.all.js" ></script>
    <script type="text/javascript" src="./js/common.js" ></script>

    <!--[if lt IE 9]>
    <script src="https://cdn.staticfile.org/html5shiv/r29/html5.min.js"></script>
    <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
</head>
<body>
<div class="main-body">
    <div class="login-main">
        <div class="login-top">
            <span>统一身份认证平台</span>
            <span class="bg1"></span>
            <span class="bg2"></span>
        </div>
        <form class="layui-form login-bottom" id="loginForm">
            <div class="center">
                <div class="item">
                    <span class="icon icon-2"></span>
                    <input type="text" name="username" lay-verify="required"  placeholder="请输入登录账号" maxlength="24"/>
                </div>

                <div class="item">
                    <span class="icon icon-3"></span>
                    <input type="password" name="password" lay-verify="required"  placeholder="请输入密码" maxlength="20">
                    <span class="bind-password icon icon-4"></span>
                </div>
            </div>
            <div class="layui-form-item" style="text-align:center; width:100%;height:100%;margin:0;">
                <button class="login-btn" lay-submit lay-filter="login">立即登录</button>
            </div>
        </form>
    </div>
</div>
<div class="footer">
    这里是版权信息,请自行替换
</div>
<script>
    console.log("lay-ui:", layui)
    // 获取URL中的查询字符串
    const urlParams = new URLSearchParams(window.location.search);
    // 通过参数名获取参数值
    const redirectUri = urlParams.get('redirect_uri');
    console.log('redirectUri: ' + redirectUri);
    layui.use(['form','jquery'], function () {
        var $ = layui.$, form = layui.form,  layer = layui.layer;

        // 登录过期的时候,跳出ifram框架
        if (top.location != self.location) top.location = self.location;

        $('.bind-password').on('click', function () {
            if ($(this).hasClass('icon-5')) {
                $(this).removeClass('icon-5');
                $("input[name='password']").attr('type', 'password');
            } else {
                $(this).addClass('icon-5');
                $("input[name='password']").attr('type', 'text');
            }
        });

        $('.icon-nocheck').on('click', function () {
            if ($(this).hasClass('icon-check')) {
                $(this).removeClass('icon-check');
            } else {
                $(this).addClass('icon-check');
            }
        });

        layui.form.on('submit(login)', function(data){
            $.ajax({
                url: '/auth/usernamePwd',
                type: 'GET',
                // dataType: 'json',
                data: {
                    username: data.field.username,
                    password: data.field.password
                },
                success: function(data) {
                    if(data.code === 200) {
                        layer.msg('登录成功',{time : 1000},function(){
                            console.log("登录成功", data)
                            // location.href='./index.html';
                            if(redirectUri) {
                                location.href = redirectUri;
                            } else {
                                location.href='./test/demo';
                            }

                        })
                    }
                    if(data.code === 6000) {
                        layer.msg(data.msg, {time : 1000})
                    }
                }
            })
            return false;
        });
    });
</script>
</body>
</html>

登录页的效果如下:
登录页面

6. 其他说明

  1. 本文中涉及的一些认证授权、异常处理或者公共类相关代码在之前文章有提到过,这里不再重复。

参考文章: 【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)_springboot3 security token 登录-CSDN博客

【工作记录】基于springboot3+springsecurity实现多种方式登录及鉴权(二)_spring security实现不通过用户名密码鉴权-CSDN博客

  1. 上述配置中客户端的配置是通过Jdbc直接存储到数据库然后再查询出来的,这里还有一些其他的自定义的实现方式,本文仅做示例,不做限制。

测试

期望实现的效果

  1. 通过页面访问时跳转到登录页面,登录成功后可以正常携带code跳转到目标页面
  2. 上文提到的应当有的端点都可以正常访问

开始测试

流程回顾

上篇文章我们提到过oauth2.0的认证的完整流程,认证授权方式有多种,本文我们以authorize_code为例,我们这里再回顾一下:

  1. 授权请求:
    第三方应用(客户端)希望访问资源所有者的资源,但资源存储在资源服务器上。客户端首先构建一个URL,指向授权服务器的/authorize端点,并添加必要的参数,如client_id(客户端标识符)、response_type=code(表示请求授权码)、redirect_uri(授权后重定向的URL)以及scope(请求的权限范围)。

  2. 用户认证与授权:
    用户被重定向到授权服务器的登录页面进行身份验证。
    验证成功后,用户会被呈现一个授权页面,询问是否允许客户端应用访问其资源。
    如果用户同意,授权服务器将用户重定向回客户端提供的redirect_uri,并在URL中附加一个授权码(code)作为查询参数。

  3. 兑换令牌:
    客户端使用接收到的授权码,连同其client_id和client_secret(如果已配置),向授权服务器的/token端点发送一个HTTP POST请求,请求访问令牌(access token)和刷新令牌(refresh token)。
    授权服务器验证请求后,返回一个JSON格式的响应,其中包含access_token、token_type(通常是Bearer)、expires_in(过期时间)以及可选的refresh_token。

  4. 访问受保护资源:
    客户端现在可以使用access_token来访问资源服务器上的受保护资源。在每个请求的头部中,客户端需要包含Authorization: Bearer <access_token>这样的字段。

    当然还有另外一种实现就是第三方通过access_token结合/oauth2/userinfo端点获取用户信息并跟自己的用户体系关联,生成自己跌token并返回给前端,后续使用自己生成的token进行资源访问。

  5. 刷新令牌:
    当access_token即将过期时,客户端可以使用refresh_token来请求一个新的access_token,而无需再次进行完整的授权流程。
    刷新令牌请求同样发送到/token端点,但使用不同的参数。

  6. 令牌撤销:
    如果不再需要访问令牌,客户端可以请求撤销access_token或refresh_token,以防止未来的访问尝试。

授权链接组装

根据oauth2.0协议的规范,我们请求/oauth2/authorize授权端点时需要提供的参数包含: client_idgrant_typescoperedirect_uri,还有个可选的state参数。

正常来讲,应该是有个应用注册的页面的,为了示例,上文中我们手动新建了一个客户端应用,配置如下:

@Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("oauth2-client")
                .clientSecret(passwordEncoder.encode("123456"))
                // 客户端认证基于请求头
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 配置授权的支持方式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .redirectUri("https://www.baidu.com")
                .scope("user")
                .scope("admin")
                // 客户端设置,设置用户需要确认授权
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false) .build())
                .build();
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());
        if (repositoryByClientId == null) {
            registeredClientRepository.save(registeredClient);
        }
        return registeredClientRepository;
    }

在项目启动的时候会执行这些代码,简单点说就是向数据库中写入了一条客户端应用记录,如果存在的话就不做处理。应用id和密钥对应的是oauth2-client123456

如上所述,我们可以组装的授权链接格式如下:

http://localhost:8080/oauth2/authorize?client_id=oauth2-client&grant_type=code&scope=user&redirect_uri=https://www.baidu.com

在浏览器访问上述链接,可以看到能正常跳转到登录页面。

登录页面002

输入用户名密码并登录成功后,会跳转到授权页面,

在这里插入图片描述

默认授权界面并不美观,后面我们会研究下如何自定义授权页面。

点击提交后会生成一条授权记录,后续就不再需要授权。

授权成功后会携带code跳转到配置的redirect_uri地址,示例中就是https://www.baidu.com?code=xxxxxxxxxxxxxxxxxxxx

默认授权页面

至此我们的授权和跳转就完成了,接下来就是使用code换取token,再用token获取用户信息了,这部分我们使用接口测试。

获取token接口测试

获取token参数

获取token参数
获取token结果

请求及响应说明

  1. 请求地址: /oauth2/token 对应的过滤器是OAuth2TokenEndpointFilter
  2. 请求头参数中Authorization值为Bearer base64(username:password)
  3. 请求体中参数grant_type为authorization_code,表示授权类型是授权码方式,code即上一步跳转时携带的code参数,redirect_uri需要和应用注册时的uri保持一致。
  4. 请求响应返回了access_token,refresh_token和expires_in这些常用数据,不做解释。
  5. 需要注意code和token的有效期

到此获取token就完成了,接下来就是通过token获取用户信息了。

写不动了,留到下一篇吧。

小结

本文在之前文章的基础上实现了sso服务端,并完成了部分测试。

针对以上内容,有任何疑问或者建议欢迎留言。

创作不易,欢迎一键三连~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1995723.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

白嫖不是梦,三分钟搞定一台服务器

在知乎上浏览时&#xff0c;我偶然发现了一个引人入胜的话题。这个话题附带了一张Excel表格的截图&#xff0c;表格设计得非常巧妙。它允许用户输入包括日薪、工作时间、通勤时间、休息时间以及学历等在内的多个变量。通过这些输入&#xff0c;表格能够计算出一个反映工作性价比…

JAVA—正则表达式

关于正则表达式的了解学习&#xff0c;以及用于校验格式&#xff0c;信息查找和替换分割内容。 目录 1.概述 2.书写规则 3.应用案例 4.用于信息查找 5.用于搜索替换&#xff0c;分割内容 1.概述 由一些特定的字符组成&#xff0c;代表一个规则 作用&#xff1a; 1.校验数据…

国外高清风景视频素材无水印哪里找?

哪里可以下载国外高清风景视频素材&#xff1f;有哪些高清海外素材网站&#xff1f;作为一名从业多年的视频剪辑师&#xff0c;今天就来给大家揭秘可以下载国外高清风景视频素材的网站&#xff0c;赶紧进来看看吧&#xff5e; 1. 稻虎网 稻虎网作为国内外知名视频素材网站&a…

【机器学习】 Sigmoid函数:机器学习中的关键激活函数

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 Sigmoid函数&#xff1a;机器学习中的关键激活函数1. 引言2. Sigmoid函数定义3.…

C++day04

1】思维导图 2】完成关系运算符重载&#xff0c;实现成员函数和全局函数的版本。 #include <iostream>using namespace std;class Stu { friend bool operator<(const Stu &L,const Stu &R); private:int age;int id; public:Stu(){}Stu(int age,int id):age…

OD工具-18个实操工具共创会

一、什么是共创会1.1 什么是共创共创&#xff0c;共同创造词组的缩写&#xff0c;字面上来看就是团队为了多个个体一起把以前没有的事物给产生出或者造出来&#xff0c;这明显的是一种典型的人类自主行为。 托德老师有一段描述:共创是一种工作状态&#xff0c;让团队中的每个个…

【时时三省】(C语言基础)操作符3

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 &取地址操作符 示例: 每个内存单元都有自己的编号 编号就成为内存单元的地址&a就是找出a的地址 后面可以加一个int *pa&#xff1d;&a 是可以用来存放地址 pa是用来存放地址的…

SQL进阶技巧:有序数据合并问题之如何按照指定的规则对数据进行合并?【详解collect_list函数有序性保证】

目录 0 问题描述【小红书面试题】 1 数据准备 2 问题分析 3 小结 0 问题描述【小红书】 有如下需求,需要将左边的表变换成右边的表,注意字段内容的顺序及对应内容的一致性。 第一个字段为name,第二个字段为subject,第三个字段为score,变换后要求subject按照语文、数学…

电脑管家软件搬运导致edge、chrome浏览器不可用

最新版本的腾讯电脑管家可以直接搬运软件到其他路径&#xff0c;但是搬运浏览器会造成软件问题&#xff0c;不建议搬运。 浏览器恢复到原路径&#xff0c;可以解决浏览器不可用的问题&#xff1a; 首先到达你的搬运路径下 可以看到软件文件夹&#xff0c;比如Microsoft Edge或…

C++11——包装器和绑定

1.包装器 C中有一些可调用对象&#xff0c;比如 函数指针&#xff0c;仿函数对象 lambda等等&#xff0c;但是他们又有缺点 因此就引出了包装器的概念 包装器 function的头文件是<functional> 我们可以用包装器包装函数指针&#xff0c;仿函数&#xff0c;lambda int …

关于LLC知识5

RLC的增益曲线不知一条 频率升高&#xff0c;增益会越来越低 无论在容性区还是感性区&#xff0c;当负载加重的时候&#xff0c;R阻值会变小&#xff0c;所以R的分压也会变小&#xff0c;导致增益会变低 当负载突然加重&#xff0c;输出电压会变低&#xff0c;增益曲线由红色变…

MySQL数据分析进阶(十二)设计数据库——PART3

※食用指南&#xff1a;文章内容为‘CodeWithMosh’SQL进阶教程系列学习笔记&#xff0c;笔记整理比较粗糙&#xff0c;主要目的自存为主&#xff0c;记录完整的学习过程。&#xff08;图片超级多&#xff0c;慎看&#xff01;&#xff09; 【中字】SQL进阶教程 | 史上最易懂S…

c++ - c++11(1)

文章目录 前言一、统一的列表初始化1、使用{ }初始化2、 std::initializer_list 二、声明1、auto2、decltype3、nullptr 三、范围for循环四、右值引用1、左值引用和右值引用2、左值引用和右值引用的比较3、左值引用的使用场景4、右值引用的使用场景5、完美转发 前言 一、统一的…

在百度飞浆中搭建pytorch环境

文章目录 1 先检查创建的环境2 创建虚拟环境3 最终结果 1 先检查创建的环境 选择GPU版本 检查python版本 2 创建虚拟环境 虚拟环境的创建 python3 -m venv env_name # (python3 -m 路径 环境名)激活虚拟环境 source env_name/bin/activate这里注意&#xff0c;同名文件…

MySQL的InnoDB的页里面存了些什么

文章目录 创建新表页的信息新增一条数据根据页号找数据信息脚本代码py_innodb_page_info根据地址计算页号根据页号计算起始地址 主要介绍数据页里面有哪些内容&#xff0c;一行数据在文件里面是怎么组织的 创建新表页的信息 CREATE TABLE test8 (id bigint(20) NOT NULL AUTO…

Android开发-使用FFmpeg-Android进行音视频处理

使用 FFmpeg-Android 进行音视频处理 1. 前言2. FFmpeg-Android 简介3. 功能介绍及实现3.1. 视频字幕添加Kotlin 代码示例&#xff1a;3.2. 尺寸剪切Kotlin 代码示例&#xff1a;3.3. 添加或去除水印Kotlin 代码示例&#xff1a;3.4. 时长截取Kotlin 代码示例&#xff1a;3.5. …

【十万个为什么】强磁场环境用什么编码器好?磁编为什么不怕强磁场?磁编与光编哪一个抗干扰强?

系列文章目录 1.元件基础 2.电路设计 3.PCB设计 4.元件焊接 5.板子调试 6.程序设计 7.算法学习 8.编写exe 9.检测标准 10.项目举例 11.职业规划 文章目录 前言一、案例场景1&#xff1a;场景2&#xff1a; 二、为什么磁编可以在磁场中工作三、磁编为什么不怕强磁场&#xf…

LVS实战项目

LVS简介 LVS:Linux Virtual Server&#xff0c;负载调度器&#xff0c;内核集成&#xff0c;章文嵩&#xff0c;阿里的四层SLB(Server LoadBalance)是基于LVSkeepalived实现。 LVS集群的类型 lvs-nat &#xff1a; 修改请求报文的目标IP, 多目标 IP 的 DNAT lvs-dr &#xff…

vpp编译安装(Ubuntu 16.04)

1、编译 git clone -b stable/1801 https://github.com/FDio/vpp.git cd vpp ./extras/vagrant/build.sh && make 报错 解决&#xff1a; 操作系统是Ubuntu 18.04.5 换成ubuntu 16即可

step11:打包qml程序

文章目录 0.文章介绍1.增加环境变量2.复制RDU.exe3.找到Qt安装路径4.操作qt 6.4.3&#xff08;minGW&#xff09;命令框 0.文章介绍 1.增加环境变量 电脑里安装了两个版本的QT&#xff0c;最好把现在打包的QT版本环境变量移到最前面 添加完成环境变量之后&#xff0c;再在wi…