实现从数据库获取用户信息
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>