Skip to content

实现从数据库获取用户信息

AuthorizationServerConfig.java

注释或删除基于内存的用户详情服务

java
/*
 *    Copyright (c) 2023-2024 pigcloud Authors. All Rights Reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.pigcloud.pigx.auth.config;

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.pigcloud.pigx.auth.authentication.DeviceClientAuthenticationConverter;
import com.pigcloud.pigx.auth.authentication.DeviceClientAuthenticationProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
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.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
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.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
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.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

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

/**
 * 授权服务器配置.
 *
 * @author He Yuqiang
 */
@EnableWebSecurity(debug = true)
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {

    /**
     * 自定义同意授权页面.
     */
    private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";

    /**
     * 授权服务器端点配置.
     *
     * @param http                        spring security核心配置类
     * @param registeredClientRepository  存储和检索授权服务器上注册的客户端
     * @param authorizationServerSettings 授权服务器的配置设置
     * @return 过滤器链
     * @throws Exception 抛出
     */
    // @formatter:off
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
            RegisteredClientRepository registeredClientRepository,
            AuthorizationServerSettings authorizationServerSettings)throws Exception {

        // 应用OAuth2授权服务器的默认安全配置。
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        DeviceClientAuthenticationConverter deviceClientAuthenticationConverter = new DeviceClientAuthenticationConverter(
                authorizationServerSettings.getDeviceAuthorizationEndpoint());
        DeviceClientAuthenticationProvider deviceClientAuthenticationProvider = new DeviceClientAuthenticationProvider(
                registeredClientRepository);

        // 配置OAuth2授权服务器的安全过滤链,启用OpenID Connect 1.0。
        // OpenID Connect是OAuth 2.0的一个扩展,用于添加用户身份验证。
        // @formatter:off
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            // 设置设备码用户验证url(自定义用户验证页)
            .deviceAuthorizationEndpoint(
                deviceAuthorizationEndpoint -> deviceAuthorizationEndpoint.verificationUri("/activate")
            )
            // 设置验证设备码用户确认页面
            .deviceVerificationEndpoint(
                deviceVerificationEndpoint -> deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)
            )
            .clientAuthentication(clientAuthentication ->
                // 客户端认证添加设备码的converter和provider
                clientAuthentication
                    .authenticationConverter(deviceClientAuthenticationConverter)
                    .authenticationProvider(deviceClientAuthenticationProvider))
            // 设置自定义用户确认授权页
            .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
            .oidc(Customizer.withDefaults());
        // @formatter:on
        // @formatter:off
        http
            // 配置异常处理,当从授权端点未认证时,重定向到登录页面。
            // 这里定义了一个登录URL认证入口点,并指定它只适用于TEXT_HTML媒体类型。
            .exceptionHandling((exceptions) -> exceptions
                .defaultAuthenticationEntryPointFor(
                    new LoginUrlAuthenticationEntryPoint("/login"),
                    new MediaTypeRequestMatcher(MediaType.TEXT_HTML))
            )
            // 配置OAuth2资源服务器,接受用户信息和/或客户端注册的访问令牌。
            // 这里使用JWT(JSON Web Tokens)作为访问令牌的格式。
            .oauth2ResourceServer((resourceServer) ->
                resourceServer.opaqueToken(Customizer.withDefaults())
            );
        // @formatter:on
        return http.build();
    }

    /**
     * 默认安全过滤器配置.
     *
     * @param http spring security核心配置类
     * @return 过滤器链
     * @throws Exception 抛出
     */
    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            // 配置默认的HTTP请求授权规则,要求所有请求都必须经过身份验证。
            .authorizeHttpRequests((authorize) -> authorize
                // 放行静态资源
                .requestMatchers("favicon.svg", "/assets/**", "/webjars/**", "/error").permitAll()
                .anyRequest().authenticated()
            )
            // 配置表单登录,使用默认设置。
            // 这处理从授权服务器过滤链重定向到登录页面的情况。
            // .formLogin(Customizer.withDefaults())
            .formLogin(formLogin -> formLogin
                    .loginPage("/login")
                    .permitAll()
            )
            .logout(logout -> logout
                    .logoutUrl("/logout")
                    .permitAll()
            );
        // @formatter:on
        return http.build();
    }

    /**
     * 定义一个 WebSecurityCustomizer Bean,用于定制 WebSecurity 的行为.
     *
     * @return 返回 WebSecurityCustomizer 实例
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> {
            // 关闭调试模式,这样在运行时不会输出详细的安全相关信息
            web.debug(false)
                    // 忽略指定的请求路径,不进行安全检查
                    .ignoring()
                    .requestMatchers("/webjars/**", "/images/**", "/css/**", "/assets/**", "/favicon.ico");
        };
    }

//     /**
//     * 先暂时配置一个基于内存的用户,框架在用户认证时会默认调用.
//     * {@link UserDetailsService#loadUserByUsername(String)} 方法根据
//     * 账号查询用户信息,一般是重写该方法实现自己的逻辑
//     * @return UserDetailsService
//     */
//     @Bean
//     public UserDetailsService userDetailsService() {
//         // 创建一个具有默认密码编码器的用户详情对象。
//         // 这里定义了一个用户名为"admin",密码为"password",角色为"ADMIN"的用户。
//         UserDetails userDetails = User.withUsername("admin")
//                 .password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
//                 .roles("ADMIN")
//                 .authorities("message_read", "message_write")
//                 .build();
//
//         // 返回一个基于内存的用户详情服务,它仅包含上面定义的用户。
//         return new InMemoryUserDetailsManager(userDetails);
//     }

    /**
     * 注册密码编码器.
     * @return DelegatingPasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    /**
     * 配置客户端Repository.
     *
     * @param jdbcTemplate db 数据源信息
     * @return 基于数据库的repository
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        // 创建一个JdbcRegisteredClientRepository实例,用于将已注册的客户端保存到数据库中
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        initRegisteredClient(registeredClientRepository);
        return registeredClientRepository;
    }

    /**
     * 初始化OAuth2客户端信息.
     * @param registeredClientRepository 客户端持久化工具
     */
    private static void initRegisteredClient(JdbcRegisteredClientRepository registeredClientRepository) {
        // @formatter:off
        // 创建一个RegisteredClient对象,并为其分配一个唯一的ID,该ID是基于UUID生成的。
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
            // 设置该客户端的ID为"messaging-client"。
            .clientId("messaging-client")
            // 设置客户端的秘密为"{noop}secret",这里的"{noop}"表示不对秘密进行加密处理。
            .clientSecret("{noop}secret")
            // 设置客户端认证方法为基于客户端秘密的基本认证。
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            // 为该客户端配置授权类型,首先配置为授权码模式。
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            // 添加刷新令牌授权类型,允许客户端使用刷新令牌来获取新的访问令牌。
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            // 添加客户端凭证授权类型,允许客户端使用其凭证直接获取令牌。
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            // 设置授权码流程的重定向URI,当授权流程完成后,用户将被重定向到这个URI。
            .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
            // 设置另一个重定向URI,可能用于其他授权流程。
            .redirectUri("http://127.0.0.1:8080/authorized")
            // 设置一个仅用于测试的重定向URI,用于接收授权码。
            .redirectUri("https://pigx.pigcloud.cn")
            // 设置用户注销后的重定向URI。
            .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
            // 添加OpenID Connect的范围,允许客户端访问用户的身份信息。
            .scope(OidcScopes.OPENID)
            // 添加Profile范围,允许客户端访问用户的个人资料信息。
            .scope(OidcScopes.PROFILE)
            // 添加自定义范围"message.read",允许客户端读取消息。
            .scope("message.read")
            // 添加自定义范围"message.write",允许客户端写入消息。
            .scope("message.write")
            // 设置客户端设置,要求每次都需要用户授权同意。
            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
            // 配置令牌设置,设置访问令牌的有效时间为30分钟。
            .tokenSettings(TokenSettings.builder()
                // 访问令牌格式,支持OAuth2TokenFormat.SELF_CONTAINED(自包含的令牌使用受保护的、有时间限制的数据结构,例如JWT);OAuth2TokenFormat.REFERENCE(不透明令牌)
                // .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
                .accessTokenTimeToLive(Duration.ofMinutes(120))
                .build())
            // 构建并返回配置好的RegisteredClient对象。
            .build();
        // @formatter:on

        // @formatter:off
        // 创建另一个RegisteredClient对象,名为deviceClient,用于设备授权
        RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("device-messaging-client")
            // 公共客户端
            .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
            // 设备码授权
            .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            // 自定scope
            .scope("message.read")
            .scope("message.write")
            .build();
        // @formatter:on
        // 初始化客户端
        if (registeredClientRepository.findByClientId(registeredClient.getClientId()) == null) {
            registeredClientRepository.save(registeredClient);
        }

        // 创建另一个RegisteredClient对象,名为deviceClient,用于设备授权
        if (registeredClientRepository.findByClientId(deviceClient.getClientId()) == null) {
            registeredClientRepository.save(deviceClient);
        }
    }

    /**
     * 创建一个OAuth2AuthorizationService的Bean实例,用于处理OAuth2授权相关的服务.
     *
     * @param jdbcTemplate               用于数据库操作的JdbcTemplate实例
     * @param registeredClientRepository 注册客户端的仓库实例
     * @return 返回配置好的OAuth2AuthorizationService实例
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
                                                           RegisteredClientRepository registeredClientRepository) {
        // 创建一个JdbcOAuth2AuthorizationService实例,用于处理与OAuth2授权相关的数据库操作
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 存储新授权同意和查询现有授权同意的核心组件。它主要由实现 OAuth2 授权请求流(例如 authorization_code 授权)的组件使用.
     *
     * @param jdbcTemplate               db数据源信息
     * @param registeredClientRepository 客户端repository
     * @return JdbcOAuth2AuthorizationConsentService
     */
    @Bean
    public JdbcOAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
                                                                             RegisteredClientRepository registeredClientRepository) {
        // Will be used by the ConsentController
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 配置jwk源,使用非对称加密,公开用于检索匹配指定选择器的JWK的方法.
     *
     * @return JWKSource
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        // 生成RSA密钥对。
        KeyPair keyPair = generateRsaKey();
        // 从密钥对中获取公钥。
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        // 从密钥对中获取私钥。
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        // 使用公钥和私钥创建一个RSAKey对象,并为其分配一个随机的UUID作为keyID。
        RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        // 创建一个包含这个RSAKey的JWKSet对象。
        JWKSet jwkSet = new JWKSet(rsaKey);
        // 返回一个不可变的JWKSet对象,这通常是为了保证安全而设计的。
        return new ImmutableJWKSet<>(jwkSet);
    }

    /**
     * 私有方法,用于生成RSA密钥对.
     *
     * @return KeyPair
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            // 实例化一个RSA密钥对生成器。
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            // 初始化密钥对生成器,指定密钥长度为2048位。
            keyPairGenerator.initialize(2048);
            // 生成RSA密钥对。
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            // 如果发生异常,则抛出IllegalStateException。
            throw new IllegalStateException(ex);
        }
        // 返回生成的密钥对。
        return keyPair;
    }

    /**
     * 用于解码签名访问令牌的 JwtDecoder 实例.
     *
     * @param jwkSource jwk源
     * @return JwtDecoder
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        // 使用OAuth2AuthorizationServerConfiguration类中的静态方法jwtDecoder,
        // 根据提供的JWKSource对象创建JwtDecoder。
        // JwtDecoder是用于解码JWT的组件。
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 配置OAuth 2.0授权服务器.
     *
     * @return AuthorizationServerSettings
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        // 创建一个AuthorizationServerSettings对象,并使用默认设置。
        // 这个对象通常用于配置授权服务器的各种设置。
        return AuthorizationServerSettings.builder().build();
    }

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

PigxUser.java

自定义实现UserDetails 为了能够将更多的用户存放到认证信息中。

java
/*
 *    Copyright (c) 2023-2024 pigcloud Authors. All Rights Reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.pigcloud.pigx.common.security.service;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * 扩展其他非安全相关的用户信息(如电子邮件地址、电话号码等).
 * @author heyuqiang
 */
@Getter
@JsonDeserialize
// 使用Jackson的@JsonIgnoreProperties注解,指定在反序列化时忽略未知的属性
// ignoreUnknown = true表示忽略JSON中任何在目标类中不存在的属性
@JsonIgnoreProperties(ignoreUnknown = true)
public class PigxUser implements UserDetails {

    // 用户的ID,final表示这是一个不可变的字段,只能在构造方法中初始化
    private final Long id;

    // 用户所在的部门ID
    private final Long deptId;

    // 用户的手机号码
    private final String mobile;

    // 用户的电子邮件地址
    private final String email;

    // 用户的头像URL或路径
    private final String avatar;

    // 用户所属租户ID
    private final Long tenantId;

    // 用户的密码,使用@JsonIgnore注解,表示在序列化对象到JSON时忽略这个字段
    // 通常用于防止密码泄露
    @JsonIgnore
    private final String password;

    // 用户的用户名
    private final String username;

    // 用户所拥有的权限集合
    private final Collection<? extends GrantedAuthority> authorities;

    // 账户是否未过期
    private final boolean accountNonExpired;

    // 账户是否未被锁定
    private final boolean accountNonLocked;

    // 凭证(通常是密码)是否未过期
    private final boolean credentialsNonExpired;

    // 账户是否启用
    private final boolean enabled;

    // 使用Jackson的@JsonCreator注解,指定构造函数用于创建对象实例
    // 当Jackson反序列化JSON到对象时,会使用这个构造函数
    @JsonCreator
    public PigxUser(
            // 使用@JsonProperty注解,指定JSON属性名与Java字段之间的映射关系
            @JsonProperty("id") Long id,
            @JsonProperty("deptId") Long deptId,
            @JsonProperty("mobile") String mobile,
            @JsonProperty("email") String email,
            @JsonProperty("avatar") String avatar,
            @JsonProperty("tenantId") Long tenantId,
            @JsonProperty("username") String username,
            @JsonProperty("password") String password,
            @JsonProperty("enabled") boolean enabled,
            @JsonProperty("accountNonExpired") boolean accountNonExpired,
            @JsonProperty("credentialsNonExpired") boolean credentialsNonExpired,
            @JsonProperty("accountNonLocked") boolean accountNonLocked,
            @JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities
    ) {
        // 在构造函数中初始化所有字段
        this.id = id;
        this.deptId = deptId;
        this.mobile = mobile;
        this.email = email;
        this.avatar = avatar;
        this.tenantId = tenantId;
        this.password = password;
        this.username = username;
        this.authorities = authorities;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        this.enabled = enabled;
    }
}

PigxDefaultUserDetailsServiceImpl.java

实现 UserDetailsService 接口的 loadUserByUsername 方法,完成从数据库查询加载用户信息。

java
/*
 *    Copyright (c) 2023-2024 pigcloud Authors. All Rights Reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.pigcloud.pigx.common.security.service;

import cn.hutool.core.util.ArrayUtil;
import com.pigcloud.pigx.common.core.constants.CacheConstants;
import com.pigcloud.pigx.common.core.constants.SecurityConstants;
import com.pigcloud.pigx.common.core.http.R;
import com.pigcloud.pigx.system.dto.UserInfo;
import com.pigcloud.pigx.system.entity.SysUser;
import com.pigcloud.pigx.system.feign.RemoteUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
 * 负责从特定的地方(通常是数据库)加载用户信息.
 *
 * @author He Yuqiang
 */
@Slf4j
@RequiredArgsConstructor
public class PigxDefaultUserDetailsServiceImpl implements UserDetailsService {

    private final RemoteUserService remoteUserService;

    private final CacheManager cacheManager;

    /**
     * 根据用户名查询用户信息.
     * @param username 用户名
     * @return UserDetails
     * @throws UsernameNotFoundException Exception
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 获取用户缓存
        Cache cache = this.cacheManager.getCache(CacheConstants.USER_DETAILS);
        if (cache != null && cache.get(username) != null) {
            // 返回缓存用户信息
            return (PigxUser) Objects.requireNonNull(cache.get(username)).get();
        }
        R<UserInfo> result = this.remoteUserService.loadUserByUsername(username, SecurityConstants.FROM_IN);
        UserDetails userDetails = buildUserDetails(result);
        assert cache != null;
        // 缓存用户信息
        cache.put(username, userDetails);
        return userDetails;
    }

    /**
     * 根据用户信息构建 UserDetails.
     * @param result 用户信息
     * @return UserDetails
     */
    private UserDetails buildUserDetails(R<UserInfo> result) {

        if (result == null || result.getData() == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        UserInfo info = result.getData();
        // 权限集合
        Set<String> dbAuthsSet = new HashSet<>();
        if (ArrayUtil.isNotEmpty(info.getRoles())) {
            // 获取角色
            Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE_PREFIX + roleId));
            // 获取权限
            dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
        }

        Collection<? extends GrantedAuthority> authorities = AuthorityUtils
            .createAuthorityList(dbAuthsSet.toArray(new String[0]));

        SysUser user = info.getSysUser();

        return new PigxUser(user.getUserId(), user.getDeptId(), user.getMobile(), user.getEmail(), user.getAvatar(),
                user.getTenantId(), user.getUsername(), user.getPassword(), true, true, true, true, authorities);
    }

}

RedisTemplateConfiguration

java
/*
 *    Copyright (c) 2023-2024 pigcloud Authors. All Rights Reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.pigcloud.pigx.common.data.cache;

import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

/**
 * 自定义 RedisTemplate 配置.
 *
 * @author he yuqiang
 */
// 启用Spring的缓存功能
@EnableCaching
@Configuration
// 覆盖默认的Redis配置
@AutoConfigureBefore(name = { "org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration" })
public class RedisTemplateConfiguration {

    /**
     * 定义一个名为redisTemplate的Bean,类型为RedisTemplate<String, Object>.
     * 使用@Primary注解表示当存在多个同类型的Bean时,优先使用这个Bean。
     * 该方法接收一个RedisConnectionFactory类型的参数,用于创建RedisTemplate实例。
     *
     * @param redisConnectionFactory Redis连接工厂,用于创建Redis连接。
     * @return 返回一个配置好的RedisTemplate实例。
     */
    @Bean
    @Primary
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 创建一个新的RedisTemplate实例
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // 设置key的序列化器为字符串序列化器
        redisTemplate.setKeySerializer(RedisSerializer.string());

        // 设置hash key的序列化器为字符串序列化器
        redisTemplate.setHashKeySerializer(RedisSerializer.string());

        // 设置value的序列化器为Java对象序列化器
        // 注意:Java对象序列化器可能会导致序列化和反序列化时的问题,特别是当对象结构变化时
        redisTemplate.setValueSerializer(RedisSerializer.java());

        // 设置hash value的序列化器为Java对象序列化器
        redisTemplate.setHashValueSerializer(RedisSerializer.java());

        // 设置RedisTemplate的连接工厂,用于创建与Redis服务器的连接
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 返回配置好的RedisTemplate实例
        return redisTemplate;
    }
}

LoginController.java

添加一个系统首页的控制器,为了测试获取当前登录的用户信息

java
/*
 *    Copyright (c) 2023-2024 pigcloud Authors. All Rights Reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.pigcloud.pigx.auth.web;

import com.pigcloud.pigx.common.security.service.PigxUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * 自定义登录表单.
 *
 * @author He Yuqiang
 */
@Slf4j
@Controller
public class LoginController {

    /**
     * 系统首页.
     * @param model 参数绑定
     * @param pigxUser 当前登录用户
     * @return index.html页面
     */
    @GetMapping("/")
    public String index(Model model, @AuthenticationPrincipal PigxUser pigxUser) {
        log.debug(pigxUser.getUsername());
        model.addAttribute("message", "Hello, Thymeleaf");
        model.addAttribute("pigxUser", pigxUser);
        return "index";
    }

    /**
     * 自定义登录页面.
     * @return login页面
     */
    @GetMapping("/login")
    public String loginPage() {
        // custom logic before showing login page...
        return "login";
    }

}

index.html

html
<!--
  ~    Copyright (c) 2023-2024 pigcloud Authors. All Rights Reserved.
  ~
  ~    Licensed under the Apache License, Version 2.0 (the "License");
  ~    you may not use this file except in compliance with the License.
  ~    You may obtain a copy of the License at
  ~
  ~        https://www.apache.org/licenses/LICENSE-2.0
  ~
  ~    Unless required by applicable law or agreed to in writing, software
  ~    distributed under the License is distributed on an "AS IS" BASIS,
  ~    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~    See the License for the specific language governing permissions and
  ~    limitations under the License.
  -->

<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="utf-8"/>
    <link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>首页 | PIGX 微服务开发平台</title>
    <link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.css"
          th:href="@{/webjars/bootstrap/css/bootstrap.css}"/>
</head>
<body>
<div class="container mt-5">
    <h1 th:text="${message}">Hello World!</h1>
    <div th:text="${#authentication.name}">
        The value of the "name" property of the authentication object should appear here.
    </div>
    <div th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}">
        This will only be displayed if authenticated user has role ROLE_ADMIN.
    </div>
    <div sec:authentication="name">
        The value of the "name" property of the authentication object should appear here.
    </div>
    <div sec:authorize="hasRole('ROLE_ADMIN')">
        This will only be displayed if authenticated user has role ROLE_ADMIN.
    </div>
    <div sec:authorize="${hasRole(#vars.expectedRole)}">
        This will only be displayed if authenticated user has a role computed by the controller.
    </div>
    <div sec:authorize-url="/admin">
        This will only be displayed if authenticated user can call the "/admin" URL.
    </div>
    <div sec:authorize="isAuthenticated()">
        Text visible only to authenticated users.
    </div>
    <div sec:authentication="name"></div>
    <div sec:authentication="principal.authorities"></div>
    <div sec:authentication="principal.avatar"></div>
    <div th:text="${pigxUser.email}"></div>
    <div>
        <form method="post" th:action="@{/logout}">
            <button type="submit" class="btn btn-primary">退 出</button>
        </form>
    </div>
</div>
</body>
</html>

基于 MIT 许可发布