编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

Spring Security Reactive多种方式(用户名密码,Email, Ldap)登录

wxchong 2024-08-23 00:01:49 开源技术 16 ℃ 0 评论

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <properties>
        <java.version>11</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <project-name>spring-security-reactive-multi-login</project-name>

        <!--对应关系参考: https://spring.io/projects/spring-cloud/#learn -->
        <spring-boot.version>2.6.15</spring-boot.version>
        <spring-cloud.version>2021.0.9</spring-cloud.version>
    </properties>

    <groupId>org.fiend</groupId>
    <artifactId>${project-name}</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>${project-name}</name>
    <description>Demo project for Spring Security Reactive</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--<dependency>-->
        <!--    <groupId>org.springframework.security</groupId>-->
        <!--    <artifactId>spring-security-oauth2-resource-server</artifactId>-->
        <!--</dependency>-->

        <!-- ============== 应用 ================= -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.25</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.1-jre</version>
        </dependency>
        <!--jwt工具-->
        <!--<dependency>-->
        <!--    <groupId>com.nimbusds</groupId>-->
        <!--    <artifactId>nimbus-jose-jwt</artifactId>-->
        <!--    <version>9.37.3</version>-->
        <!--</dependency>-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.19.4</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>org.fiend.Server</mainClass>
                    <skip>true</skip>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- 解决资源文件的编码问题 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

            <!-- maven打source包 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <!--<phase>verify</phase>-->
                        <goals>
                            <!--jar, jar-no-fork-->
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- spring Boot在编译的时候, 是有默认JDK版本的, 这里自定义指定JDK版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

            <!--拷贝依赖jar到指定的目录-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                            <overWriteReleases>false</overWriteReleases>
                            <overWriteSnapshots>false</overWriteSnapshots>
                            <overWriteIfNewer>true</overWriteIfNewer>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <!-- maven-jar-plugin用于生成META-INF/MANIFEST.MF文件的部分内容, -->
                            <addClasspath>true</addClasspath>
                            <!-- 指定依赖包所在目录。 -->
                            <classpathPrefix>lib/</classpathPrefix>
                            <!-- 指定MANIFEST.MF中的Main-Class, -->
                            <mainClass>org.fiend.SpringBootMiniDemoSrv</mainClass>
                            <useUniqueVersions>false</useUniqueVersions>
                        </manifest>
                    </archive>
                    <excludes>
                        <!--<exclude>*.properties</exclude>-->
                        <!--<exclude>*.yml</exclude>-->
                        <!--<exclude>*.xml</exclude>-->
                        <!--<exclude>org/fiend/controller/HomeController.class</exclude>-->
                    </excludes>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.sonarsource.scanner.maven</groupId>
                <artifactId>sonar-maven-plugin</artifactId>
                <version>3.9.1.2184</version>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

# ========================== springboot变量使用方法 ========================== #
# 端口号变量: ${server.port}
# user-info:
#  name: ${spring.application.name}:${server.port}:${spring.version}
server:
  port: 8600
  max-http-header-size: 8192
  tomcat:
    max-connections: 10000 # 最大连接数, 默认为10000
    accept-count: 500 # 最大连接等待数, 默认100
    threads:
      max: 475  # 最大工作线程数, 默认200
      min-spare: 400 #最小工作线程数, 默认10



spring:
  application:
    name: spring-security-reactive-multi-login
  webflux:
    # 设置 context path 路径
#    base-path: /my-context-path
  security:
    user:
      name: user
      password: 123

# ========================== logging配置 ========================== #
#配置日志
logging:
  #  config: /data/config/logback-spring.xml  # 指定logback-spring.xml文件路径
  #  config: classpath:logback-spring.xml
  level:
    org.springframework.cloud.gateway: TRACE
    root: info
#    # 不同目录下的日志可配置不同级别 info, error
#    com.euler: debug
#    org.springfromework.web: info

Service

CustomMapReactiveUserDetailsService

package org.fiend.a.security.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.*;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 普通用户名密码登录 UserDetailsService
 * @author Administrator 2024-01-16 15:29:59
 */
public class CustomMapReactiveUserDetailsService implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {
    Logger log = LoggerFactory.getLogger(getClass());

    private final Map<String, UserDetails> users;

    public CustomMapReactiveUserDetailsService(UserDetails... users) {
        this(Arrays.asList(users));
    }

    /**
     * Creates a new instance
     * @param users the {@link UserDetails} to use
     */
    public CustomMapReactiveUserDetailsService(Collection<UserDetails> users) {
        Assert.notEmpty(users, "users cannot be null or empty");
        this.users = new ConcurrentHashMap<>();
        for (UserDetails user : users) {
            this.users.put(getKey(user.getUsername()), user);
        }
    }

    @Override
    public Mono<UserDetails> findByUsername(String username) {
        // 如果用户名不符合ldap的命名规则, 则不必再去users中查询,
        // 因为实际情况下去users中查询, 可能涉及到去相关数据库中查询,
        // 资源消耗较大
        if (isIllegalUsername(username)) {
            log.warn("custom username is illegal!");
            return Mono.empty();
        }

        String key = getKey(username);
        UserDetails result = users.get(key);
        return (result != null) ? Mono.just(User.withUserDetails(result).build()) : Mono.empty();
    }

    private boolean isIllegalUsername(String username) {
        if (username == null || username.isEmpty()) {
            return true; // 空字符串或null不合法
        }

        // 只能包含数字大小写字母和“-”, 字符串最小长度为1,最大长度为16
        Pattern usernamePattern = Pattern.compile("^[a-zA-Z0-9-]{1,16}#34;);

        Matcher matcher = usernamePattern.matcher(username);
        return !matcher.matches();
    }

    @Override
    public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {
        // @formatter:off
        return Mono.just(user)
                .map((userDetails) -> withNewPassword(userDetails, newPassword))
                .doOnNext((userDetails) -> {
                    String key = getKey(user.getUsername());
                    this.users.put(key, userDetails);
                });
        // @formatter:on
    }

    private UserDetails withNewPassword(UserDetails userDetails, String newPassword) {
        // @formatter:off
        return User.withUserDetails(userDetails)
                .password(newPassword)
                .build();
        // @formatter:on
    }

    private String getKey(String username) {
        return username.toLowerCase();
    }
}

EmailMapReactiveUserDetailsService

package org.fiend.a.security.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.*;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * email用户名密码登录 UserDetailsService
 * @author Administrator 2024-01-16 15:29:59
 */
public class EmailMapReactiveUserDetailsService implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {
    Logger log = LoggerFactory.getLogger(getClass());

    private final Map<String, UserDetails> users;

    public EmailMapReactiveUserDetailsService(UserDetails... users) {
        this(Arrays.asList(users));
    }

    /**
     * Creates a new instance
     * @param users the {@link UserDetails} to use
     */
    public EmailMapReactiveUserDetailsService(Collection<UserDetails> users) {
        Assert.notEmpty(users, "users cannot be null or empty");
        this.users = new ConcurrentHashMap<>();
        for (UserDetails user : users) {
            this.users.put(getKey(user.getUsername()), user);
        }
    }

    @Override
    public Mono<UserDetails> findByUsername(String username) {
        // 如果用户名不符合ldap的命名规则, 则不必再去users中查询,
        // 因为实际情况下去users中查询, 可能涉及到去相关数据库中查询,
        // 资源消耗较大
        if (isIllegalUsername(username)) {
            log.warn("email username is illegal!");
            return Mono.empty();
        }

        String key = getKey(username);
        UserDetails result = users.get(key);
        return (result != null) ? Mono.just(User.withUserDetails(result).build()) : Mono.empty();
    }

    private boolean isIllegalUsername(String username) {
        if (username == null || username.isEmpty()) {
            return true; // 空字符串或null不合法
        }

        // email地址校验
        Pattern usernamePattern = Pattern.compile("^\\w+((-\\w+)|(\\.\\w+))*\\@[A-Za-z0-9]+((\\.|-)[A-Za-z0-9]+)*\\.[A-Za-z0-9]+#34;);

        Matcher matcher = usernamePattern.matcher(username);
        return !matcher.matches();
    }

    @Override
    public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {
        // @formatter:off
        return Mono.just(user)
                .map((userDetails) -> withNewPassword(userDetails, newPassword))
                .doOnNext((userDetails) -> {
                    String key = getKey(user.getUsername());
                    this.users.put(key, userDetails);
                });
        // @formatter:on
    }

    private UserDetails withNewPassword(UserDetails userDetails, String newPassword) {
        // @formatter:off
        return User.withUserDetails(userDetails)
                .password(newPassword)
                .build();
        // @formatter:on
    }

    private String getKey(String username) {
        return username.toLowerCase();
    }
}

LdapMapReactiveUserDetailsService

package org.fiend.a.security.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.*;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * ldap用户名密码登录 UserDetailsService
 * @author Administrator 2024-01-16 15:29:59
 */
public class LdapMapReactiveUserDetailsService implements ReactiveUserDetailsService, ReactiveUserDetailsPasswordService {
    Logger log = LoggerFactory.getLogger(getClass());

    private final Map<String, UserDetails> users;

    public LdapMapReactiveUserDetailsService(UserDetails... users) {
        this(Arrays.asList(users));
    }

    /**
     * Creates a new instance
     * @param users the {@link UserDetails} to use
     */
    public LdapMapReactiveUserDetailsService(Collection<UserDetails> users) {
        Assert.notEmpty(users, "users cannot be null or empty");
        this.users = new ConcurrentHashMap<>();
        for (UserDetails user : users) {
            this.users.put(getKey(user.getUsername()), user);
        }
    }
    @Override
    public Mono<UserDetails> findByUsername(String username) {
        // 如果用户名不符合ldap的命名规则, 则不必再去users中查询,
        // 因为实际情况下去users中查询, 可能涉及到去相关数据库中查询,
        // 资源消耗较大
        if (isIllegalUsername(username)) {
            log.warn("ldap username is illegal!");
            return Mono.empty();
        }

        String key = getKey(username);
        UserDetails result = users.get(key);
        return (result != null) ? Mono.just(User.withUserDetails(result).build()) : Mono.empty();
    }

    private boolean isIllegalUsername(String username) {
        if (username == null || username.isEmpty()) {
            return true; // 空字符串或null不合法
        }

        // ldap地址校验
        Pattern usernamePattern = Pattern.compile("^dc=[^,]+,dc=[^,]+#34;);

        Matcher matcher = usernamePattern.matcher(username);
        return !matcher.matches();
    }

    @Override
    public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {
        // @formatter:off
        return Mono.just(user)
                .map((userDetails) -> withNewPassword(userDetails, newPassword))
                .doOnNext((userDetails) -> {
                    String key = getKey(user.getUsername());
                    this.users.put(key, userDetails);
                });
        // @formatter:on
    }

    private UserDetails withNewPassword(UserDetails userDetails, String newPassword) {
        // @formatter:off
        return User.withUserDetails(userDetails)
                .password(newPassword)
                .build();
        // @formatter:on
    }

    private String getKey(String username) {
        return username.toLowerCase();
    }
}

Manager

CustomUserReactiveAuthenticationManager

package org.fiend.a.security.manager;

import org.fiend.a.security.service.CustomMapReactiveUserDetailsService;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * @author Administrator 2024-01-03 14:00:35
 */
public class CustomUserReactiveAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
    private Scheduler scheduler = Schedulers.boundedElastic();

    private UserDetailsChecker preAuthenticationChecks = this::defaultPreAuthenticationChecks;

    private UserDetailsChecker postAuthenticationChecks = this::defaultPostAuthenticationChecks;

    private PasswordEncoder passwordEncoder = passwordEncoder();

    private CustomMapReactiveUserDetailsService userDetailsPasswordService = userDetailsService();
    // private MapReactiveUserDetailsService userDetailsPasswordService = userDetailsService2();

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String username = authentication.getName();
        String presentedPassword = (String) authentication.getCredentials();

        // return retrieveUser(username)
        //         // .switchIfEmpty(Mono.just(authentication))  // 空UserDetails, 说明此manager没有此用户
        //         .publishOn(scheduler)
        //         .filter(u -> passwordEncoder().matches(password, passwordEncoder().encode(u.getPassword())))
        //         .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
        //         .map(u-> new UsernamePasswordAuthenticationToken(u, u.getPassword()));

        return retrieveUser(username)
                // 空UserDetails, 说明此manager没有此用户, 交给下一个manager处理,
                // 下一个manager即在myDelegatingAuthenticationManager()中定义的下一个manager
                .switchIfEmpty(Mono.empty())
                // 如果查询结果为空则抛出错误结果返回, 这里使用了myDelegatingAuthenticationManager()因此不执行此行代码
                // .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
                .doOnNext(this.preAuthenticationChecks::check)
                .publishOn(this.scheduler)
                .filter((userDetails) -> this.passwordEncoder.matches(presentedPassword, userDetails.getPassword()))
                .flatMap((userDetails) -> upgradeEncodingIfNecessary(userDetails, presentedPassword))
                .doOnNext(this.postAuthenticationChecks::check)
                .map(this::createUsernamePasswordAuthenticationToken);
    }

    @Override
    protected Mono<UserDetails> retrieveUser(String username) {
        return this.userDetailsPasswordService.findByUsername(username);
    }

    public CustomMapReactiveUserDetailsService userDetailsService() {
        // 返回自定义的ReactiveUserDetailsService实现
        UserDetails user = User.withUsername("admin")
                .password("123")
                .roles("USER")
                .build();
        return new CustomMapReactiveUserDetailsService(user);
    }

    public MapReactiveUserDetailsService userDetailsService2() {
        // 返回自定义的ReactiveUserDetailsService实现
        UserDetails user = User.withUsername("admin")
                .password("123")
                .roles("USER")
                .build();
        return new MapReactiveUserDetailsService(user);
    }

    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    private Mono<UserDetails> upgradeEncodingIfNecessary(UserDetails userDetails, String presentedPassword) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null
                && this.passwordEncoder.upgradeEncoding(userDetails.getPassword());
        if (upgradeEncoding) {
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            return this.userDetailsPasswordService.updatePassword(userDetails, newPassword);
        }
        return Mono.just(userDetails);
    }

    private UsernamePasswordAuthenticationToken createUsernamePasswordAuthenticationToken(UserDetails userDetails) {
        return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(),
                userDetails.getAuthorities());
    }

    private void defaultPreAuthenticationChecks(UserDetails user) {
        if (!user.isAccountNonLocked()) {
            this.logger.debug("User account is locked");
            throw new LockedException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
                    "User account is locked"));
        }
        if (!user.isEnabled()) {
            this.logger.debug("User account is disabled");
            throw new DisabledException(
                    this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
        }
        if (!user.isAccountNonExpired()) {
            this.logger.debug("User account is expired");
            throw new AccountExpiredException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
        }
    }

    private void defaultPostAuthenticationChecks(UserDetails user) {
        if (!user.isCredentialsNonExpired()) {
            this.logger.debug("User account credentials have expired");
            throw new CredentialsExpiredException(this.messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
        }
    }
}

EmailUserReactiveAuthenticationManager

package org.fiend.a.security.manager;

import org.fiend.a.security.service.EmailMapReactiveUserDetailsService;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * @author Administrator 2024-01-03 14:00:35
 */
public class EmailUserReactiveAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
    private Scheduler scheduler = Schedulers.boundedElastic();

    private UserDetailsChecker preAuthenticationChecks = this::defaultPreAuthenticationChecks;

    private UserDetailsChecker postAuthenticationChecks = this::defaultPostAuthenticationChecks;

    private PasswordEncoder passwordEncoder = emailPasswordEncoder();

    private EmailMapReactiveUserDetailsService emailMapReactiveUserDetailsService = userDetailsService();

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String username = authentication.getName();
        String presentedPassword = String.valueOf(authentication.getCredentials());

        // return retrieveUser(username)
        //         // .switchIfEmpty(Mono.empty())  // 空UserDetails, 说明此manager没有此用户
        //         .doOnNext(this.preAuthenticationChecks::check)
        //         .publishOn(scheduler)
        //         .filter(u -> passwordEncoder.matches(password, emailPasswordEncoder().encode(u.getPassword())))
        //         .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
        //         .map(u-> new UsernamePasswordAuthenticationToken(u, u.getPassword()));
        return retrieveUser(username)
                // 空UserDetails, 说明此manager没有此用户, 交给下一个manager处理,
                // 下一个manager即在myDelegatingAuthenticationManager()中定义的下一个manager
                .switchIfEmpty(Mono.empty())
                // 如果查询结果为空则抛出错误结果返回, 这里使用了myDelegatingAuthenticationManager()因此不执行此行代码
                // .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))

                // .doOnNext(this.preAuthenticationChecks::check)
                .publishOn(scheduler)
                // .filter(u -> passwordEncoder.matches(password, passwordEncoder.encode(u.getPassword())))
                .filter(u -> passwordEncoder.matches(presentedPassword, u.getPassword()))
                // .flatMap((userDetails) -> upgradeEncodingIfNecessary(userDetails, presentedPassword))
                // .doOnNext(this.postAuthenticationChecks::check)
                .map(this::createUsernamePasswordAuthenticationToken);
    }

    @Override
    protected Mono<UserDetails> retrieveUser(String username) {
        return userDetailsService().findByUsername(username);
    }

    private EmailMapReactiveUserDetailsService userDetailsService() {
        // 返回自定义的ReactiveUserDetailsService实现
        UserDetails user = User.withUsername("test@163.com")
                .password("123")
                .roles("USER")
                .build();
        return new EmailMapReactiveUserDetailsService(user);
    }

    public PasswordEncoder emailPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    private Mono<UserDetails> upgradeEncodingIfNecessary(UserDetails userDetails, String presentedPassword) {
        boolean upgradeEncoding = this.emailMapReactiveUserDetailsService != null
                && this.passwordEncoder.upgradeEncoding(userDetails.getPassword());
        if (upgradeEncoding) {
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            return this.emailMapReactiveUserDetailsService.updatePassword(userDetails, newPassword);
        }
        return Mono.just(userDetails);
    }

    private UsernamePasswordAuthenticationToken createUsernamePasswordAuthenticationToken(UserDetails userDetails) {
        return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(),
                userDetails.getAuthorities());
    }

    private void defaultPreAuthenticationChecks(UserDetails user) {
        if (!user.isAccountNonLocked()) {
            this.logger.debug("User account is locked");
            throw new LockedException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
                    "User account is locked"));
        }
        if (!user.isEnabled()) {
            this.logger.debug("User account is disabled");
            throw new DisabledException(
                    this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
        }
        if (!user.isAccountNonExpired()) {
            this.logger.debug("User account is expired");
            throw new AccountExpiredException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
        }
    }

    private void defaultPostAuthenticationChecks(UserDetails user) {
        if (!user.isCredentialsNonExpired()) {
            this.logger.debug("User account credentials have expired");
            throw new CredentialsExpiredException(this.messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
        }
    }
}

LdapUserReactiveAuthenticationManager

package org.fiend.a.security.manager;

import org.fiend.a.security.service.LdapMapReactiveUserDetailsService;
import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * @author Administrator 2024-01-03 14:00:35
 */
public class LdapUserReactiveAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
    private Scheduler scheduler = Schedulers.boundedElastic();

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());
        return retrieveUser(username)
                // 在delegatingAuthenticationManager()中最后一个manager,
                // 说明没有此用户, 需要报异常, 因此该行需要注释掉
                // .switchIfEmpty(Mono.empty())

                // 如果查询结果为空则抛出错误结果返回, 该manager是myDelegatingAuthenticationManager()最后一个manager,
                // 因此查询结果为空, 需要抛出错误
                .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))

                .publishOn(scheduler)
                .filter(u -> emailPasswordEncoder().matches(password, emailPasswordEncoder().encode(u.getPassword())))
                .map(this::createUsernamePasswordAuthenticationToken);
    }

    @Override
    protected Mono<UserDetails> retrieveUser(String username) {
        return userDetailsService().findByUsername(username);
    }

    private LdapMapReactiveUserDetailsService userDetailsService() {
        // 返回自定义的 ReactiveUserDetailsService 实现
        UserDetails user = User.withUsername("dc=example,dc=org")
                .password("123")
                .roles("USER")
                .build();
        return new LdapMapReactiveUserDetailsService(user);
    }

    public PasswordEncoder emailPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    private UsernamePasswordAuthenticationToken createUsernamePasswordAuthenticationToken(UserDetails userDetails) {
        return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(),
                userDetails.getAuthorities());
    }
}

SecurityConfig

package org.fiend.a.security;

import org.fiend.a.security.manager.CustomUserReactiveAuthenticationManager;
import org.fiend.a.security.manager.EmailUserReactiveAuthenticationManager;
import org.fiend.a.security.manager.LdapUserReactiveAuthenticationManager;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;

/**
 * 实现多种登录方式 -- 普通用户名密码登录, 邮箱登录, Ldap登录等
 * @author Administrator 2024-01-16 9:48:50
 */
@EnableWebFluxSecurity
public class SecurityConfig {
    @Bean
    public SecurityWebFilterChain mySpringWebFilterChain(ServerHttpSecurity http) {
        return http
                .authorizeExchange()
                .anyExchange().authenticated()
                .and()
                // 不能同时设置多个manager,否则, 只会最后一个生效
                .authenticationManager(myDelegatingAuthenticationManager())
                // .authenticationManager(customAuthenticationManager())
                // .authenticationManager(new CustomUserReactiveAuthenticationManager())
                // .authenticationManager(new EmailUserReactiveAuthenticationManager())
                // .authenticationManager(new LdapUserReactiveAuthenticationManager())
                .formLogin()
                    // .authenticationManager(myDelegatingAuthenticationManager())
                .and()
                .httpBasic()
                .and()
                .build();
    }

    /* ====================== 组合多个认证提供者 =========================== */
    /**
     * 主要是通过 DelegatingReactiveAuthenticationManager 的
     *     List<ReactiveAuthenticationManager>
     *     来添加不同的ReactiveAuthenticationManager实现不同方式的登录
     *     (用户名密码登录, 邮箱登录, Ldap登录等)
     */
    public DelegatingReactiveAuthenticationManager myDelegatingAuthenticationManager() {
        return new DelegatingReactiveAuthenticationManager(
                new CustomUserReactiveAuthenticationManager(),
                new EmailUserReactiveAuthenticationManager(),
                new LdapUserReactiveAuthenticationManager());
    }

    /* ====================== 组合多个认证提供者 =========================== */
    /**
     * 主要是通过 DelegatingReactiveAuthenticationManager 的
     *     List<ReactiveAuthenticationManager>
     *     来添加不同的ReactiveAuthenticationManager实现不同方式的登录
     *     (用户名密码登录, 邮箱登录, Ldap登录等)
     */
    public DelegatingReactiveAuthenticationManager delegatingAuthenticationManager() {
        return new DelegatingReactiveAuthenticationManager(
                customAuthenticationManager(),
                emailAuthenticationManager(),
                ldapAuthenticationManager());
    }

    /* =========================== 普通用户名密码登录 =========================== */
    public ReactiveAuthenticationManager customAuthenticationManager() {
        UserDetailsRepositoryReactiveAuthenticationManager manager =
                new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService());
        // 可能需要设置密码编码器
        manager.setPasswordEncoder(passwordEncoder());

        return manager;
    }

    public ReactiveUserDetailsService userDetailsService() {
        // 返回自定义的ReactiveUserDetailsService实现
        UserDetails user = User.withUsername("admin")
                .password("123")
                .roles("USER")
                .build();
        return new MapReactiveUserDetailsService(user);
    }

    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /* =========================== 邮箱登录 =========================== */
    public ReactiveAuthenticationManager emailAuthenticationManager() {
        UserDetailsRepositoryReactiveAuthenticationManager manager =
                new UserDetailsRepositoryReactiveAuthenticationManager(emailUserDetailsService());
        // 可能需要设置密码编码器
        manager.setPasswordEncoder(emailPasswordEncoder());

        return manager;
    }

    public ReactiveUserDetailsService emailUserDetailsService() {
        // 返回自定义的ReactiveUserDetailsService实现
        UserDetails user = User.withUsername("email")
                .password("123")
                .roles("USER")
                .build();
        return new MapReactiveUserDetailsService(user);
    }

    public PasswordEncoder emailPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /* =========================== LDAP登录 =========================== */
    public ReactiveAuthenticationManager ldapAuthenticationManager() {
        UserDetailsRepositoryReactiveAuthenticationManager manager =
                new UserDetailsRepositoryReactiveAuthenticationManager(ldapUserDetailsService());
        // 可能需要设置密码编码器
        manager.setPasswordEncoder(ldapPasswordEncoder());

        return manager;
    }

    public ReactiveUserDetailsService ldapUserDetailsService() {
        // 返回自定义的ReactiveUserDetailsService实现
        UserDetails user = User.withUsername("email")
                .password("123")
                .roles("USER")
                .build();
        return new MapReactiveUserDetailsService(user);
    }

    public PasswordEncoder ldapPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

LoginController

package org.fiend.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

/**
 * @author 86133 2023-05-11 9:12:44
 */
@RestController
public class LoginController {
    Logger log = LoggerFactory.getLogger(getClass());

    /**
     * @return
     */
    @RequestMapping("login")
    public Mono<String> login() {
        return Mono.just("login success!");
    }

    /**
     * localhost:8600/hello
     * @return
     */
    @RequestMapping("/hello")
    public Mono<String> hello() {
        return Mono.just("Flux");
    }

}

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表