♻️ Refactoring code. 客户端加载处理逻辑

This commit is contained in:
lbw 2022-05-29 16:26:38 +08:00
parent 1a3734d4b4
commit 7f3a008b6d
12 changed files with 324 additions and 75 deletions

View File

@ -24,22 +24,18 @@ import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
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.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.security.web.SecurityFilterChain;
/**
*
* SAS认证服务器配置
*
* @author lengleng
* @date 2022/5/27 认证服务器配置
* @date 2022/5/27
*/
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfiguration {
@ -48,27 +44,9 @@ public class AuthorizationServerConfiguration {
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
// @formatter:off
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient client = RegisteredClient.withId("pig")
.clientId("pig")
.clientSecret("{noop}pig")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantTypes(authorizationGrantTypes -> {
authorizationGrantTypes.add(AuthorizationGrantType.AUTHORIZATION_CODE);
authorizationGrantTypes.add(AuthorizationGrantType.REFRESH_TOKEN);
})
.tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED).build())
.redirectUri("https://pig4cloud.com")
.build();
return new InMemoryRegisteredClientRepository(client);
}
// @formatter:on
@Bean
public OAuth2TokenGenerator tokenGenerator(JWKSource jwkSource) {

View File

@ -20,41 +20,31 @@ import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
/**
* 服务安全相关配置
*
* @author lengleng
* @date 2022/1/12 认证相关配置
* @date 2022/1/12
*/
@EnableWebSecurity
public class WebSecurityConfiguration {
// @formatter:off
/**
* spring security 默认的安全策略
* @param http security注入点
* @return SecurityFilterChain
* @throws Exception
*/
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.antMatchers("/oauth/*").permitAll()
.anyRequest().authenticated()
)
.csrf().disable()
.formLogin(Customizer.withDefaults());
http.authorizeRequests(
// 暴露自定义 password 等端点
authorizeRequests -> authorizeRequests.antMatchers("/oauth/*").permitAll().anyRequest().authenticated())
// 个性化 formLogin
.csrf().disable().formLogin(Customizer.withDefaults());
return http.build();
}
// @formatter:off
@Bean
UserDetailsService users() {
UserDetails user = User.builder()
.username("admin")
.password("{noop}123456")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}

View File

@ -67,6 +67,11 @@ public interface SecurityConstants {
*/
String BCRYPT = "{bcrypt}";
/**
* {noop} 加密的特征码
*/
String NOOP = "{noop}";
/**
* sys_oauth_client_details 表的字段不包括client_idclient_secret
*/

View File

@ -9,9 +9,7 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
@ -29,17 +27,15 @@ public class CustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OAuth2AuthorizationService authorizationService;
private final JwtDecoder jwtDecoder;
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri("http://localhost:8080/auth/oauth2/jwks").build();
Jwt jwt = jwtDecoder.decode(token);
String principalClaimValue = jwt.getClaimAsString(JwtClaimNames.SUB);
OAuth2Authorization oldAuthorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
Map<String, PigUserDetailsService> userDetailsServiceMap = SpringUtil
.getBeansOfType(PigUserDetailsService.class);
Optional<PigUserDetailsService> optional = userDetailsServiceMap.values().stream()
.filter(service -> service.support(oldAuthorization.getRegisteredClientId(),
oldAuthorization.getAuthorizationGrantType().getValue()))
@ -47,7 +43,7 @@ public class CustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
UserDetails userDetails = null;
try {
userDetails = optional.get().loadUserByUsername(principalClaimValue);
userDetails = optional.get().loadUserByUsername(oldAuthorization.getPrincipalName());
}
catch (UsernameNotFoundException notFoundException) {
}

View File

@ -19,42 +19,59 @@ package com.pig4cloud.pig.common.security.component;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.web.client.RestTemplate;
/**
* @author lengleng
* @date 2020-06-23
* @date 2022-05-29
*/
@EnableConfigurationProperties(PermitAllUrlProperties.class)
public class PigResourceServerAutoConfiguration {
/**
* 权限表达式判断的具体实现
* @return @PreAuthorize("@pms.hasPermission('XXX')")
*/
@Bean("pms")
public PermissionService permissionService() {
return new PermissionService();
}
/**
* 解析请求token 的具体实现
* @param urlProperties 直接对外暴露的接口 不判断直接返回NULL
* @return null / token
*/
@Bean
public PigBearerTokenExtractor pigBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
return new PigBearerTokenExtractor(urlProperties);
}
/**
* 资源服务器异常包装
* @param objectMapper jackson
* @return 处理器
*/
@Bean
public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint(ObjectMapper objectMapper) {
return new ResourceAuthExceptionEntryPoint(objectMapper);
}
/**
* 注入 资源服务器 token 处理
* @param authorizationService token存储
* @param restTemplate 远程调用的实现 默认只会在第一次获取 jwk 配置
* @return OpaqueTokenIntrospector
*/
@Bean
public OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2AuthorizationService authorizationService) {
return new CustomOpaqueTokenIntrospector(authorizationService);
}
// @Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri("http://localhost:8080/auth/oauth2/jwks").build();
return jwtDecoder;
public OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2AuthorizationService authorizationService,
RestTemplate restTemplate) {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri("http://pigx-auth/oauth2/jwks")
.restOperations(restTemplate).build();
return new CustomOpaqueTokenIntrospector(authorizationService, jwtDecoder);
}
}

View File

@ -16,13 +16,20 @@
package com.pig4cloud.pig.common.security.feign;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
/**
* @author lengleng
* @date 2019/2/1 feign 拦截器传递 header 中oauth token 使用hystrix 的信号量模式
*/
@ConditionalOnProperty("security.oauth2.client.client-id")
public class PigFeignClientConfiguration {
/**
* 注入 oauth2 feign token 增强
* @param tokenResolver token获取处理器
* @return 拦截器
*/
@Bean
public RequestInterceptor oauthRequestInterceptor(BearerTokenResolver tokenResolver) {
return new PigOAuthRequestInterceptor(tokenResolver);
}
}

View File

@ -0,0 +1,73 @@
package com.pig4cloud.pig.common.security.feign;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.util.WebUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.Enumeration;
/**
* oauth2 feign token传递
*
* 重新 OAuth2FeignRequestInterceptor 官方实现部分常见不适用
*
* @author lengleng
* @date 2022/5/29
*/
@Slf4j
@RequiredArgsConstructor
public class PigOAuthRequestInterceptor implements RequestInterceptor {
private final BearerTokenResolver tokenResolver;
/**
* Create a template with the header of provided name and extracted extract </br>
*
* 1. 如果使用 非web 请求header 区别 </br>
*
* 2. 根据authentication 还原请求token
* @param template
*/
@Override
public void apply(RequestTemplate template) {
Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM);
// 带from 请求直接跳过
if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) {
return;
}
// 非web 请求直接跳过
if (!WebUtils.getRequest().isPresent()) {
return;
}
HttpServletRequest request = WebUtils.getRequest().get();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
template.header(name, values);
}
}
// 避免请求参数的 query token 无法传递
String token = tokenResolver.resolve(request);
if (StrUtil.isBlank(token)) {
return;
}
template.header(HttpHeaders.AUTHORIZATION, String.format("%s %s", OAuth2AccessToken.TokenType.BEARER, token));
}
}

View File

@ -0,0 +1,115 @@
package com.pig4cloud.pig.common.security.service;
import cn.hutool.core.util.BooleanUtil;
import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails;
import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService;
import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.util.R;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2TokenFormat;
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.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.Optional;
/**
* 查询客户端相关信息实现
*
* @author lengleng
* @date 2022/5/29
*/
@RequiredArgsConstructor
public class PigRemoteRegisteredClientRepository implements RegisteredClientRepository {
/**
* 刷新令牌有效期默认 30
*/
private final static int refreshTokenValiditySeconds = 60 * 60 * 24 * 30;
/**
* 请求令牌有效期默认 12 小时
*/
private final static int accessTokenValiditySeconds = 60 * 60 * 12;
private final RemoteClientDetailsService clientDetailsService;
/**
* Saves the registered client.
*
* <p>
* IMPORTANT: Sensitive information should be encoded externally from the
* implementation, e.g. {@link RegisteredClient#getClientSecret()}
* @param registeredClient the {@link RegisteredClient}
*/
@Override
public void save(RegisteredClient registeredClient) {
throw new UnsupportedOperationException();
}
/**
* Returns the registered client identified by the provided {@code id}, or
* {@code null} if not found.
* @param id the registration identifier
* @return the {@link RegisteredClient} if found, otherwise {@code null}
*/
@Override
public RegisteredClient findById(String id) {
throw new UnsupportedOperationException();
}
/**
* Returns the registered client identified by the provided {@code clientId}, or
* {@code null} if not found.
* @param clientId the client identifier
* @return the {@link RegisteredClient} if found, otherwise {@code null}
*/
/**
* 重写原生方法支持redis缓存
* @param clientId
* @return
*/
@Override
@SneakyThrows
@Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null")
public RegisteredClient findByClientId(String clientId) {
R<SysOauthClientDetails> detailsR = clientDetailsService.getClientDetailsById(clientId,
SecurityConstants.FROM_IN);
SysOauthClientDetails clientDetails = detailsR.getData();
RegisteredClient.Builder builder = RegisteredClient.withId(clientDetails.getClientId())
.clientId(clientDetails.getClientSecret())
.clientSecret(SecurityConstants.NOOP + clientDetails.getClientSecret())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
// 授权模式
Optional.ofNullable(clientDetails.getAuthorizedGrantTypes())
.ifPresent(grants -> StringUtils.commaDelimitedListToSet(grants)
.forEach(s -> builder.authorizationGrantType(new AuthorizationGrantType(s))));
// 回调地址
Optional.ofNullable(clientDetails.getWebServerRedirectUri()).ifPresent(builder::redirectUri);
return builder
.tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.accessTokenTimeToLive(Duration.ofSeconds(Optional
.ofNullable(clientDetails.getAccessTokenValidity()).orElse(accessTokenValiditySeconds)))
.refreshTokenTimeToLive(
Duration.ofSeconds(Optional.ofNullable(clientDetails.getRefreshTokenValidity())
.orElse(refreshTokenValiditySeconds)))
.build())
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(BooleanUtil.toBoolean(clientDetails.getAutoapprove())).build())
.build();
}
}

View File

@ -3,3 +3,4 @@ com.pig4cloud.pig.common.security.service.PigAppUserDetailsServiceImpl
com.pig4cloud.pig.common.security.component.PigSecurityInnerAspect
com.pig4cloud.pig.common.security.component.PigTokenStoreAutoConfiguration
com.pig4cloud.pig.common.security.component.PigSecurityMessageSourceConfiguration
com.pig4cloud.pig.common.security.service.PigRemoteRegisteredClientRepository

View File

@ -0,0 +1,58 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pig.admin.api.feign;
import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails;
import com.pig4cloud.pig.common.core.constant.SecurityConstants;
import com.pig4cloud.pig.common.core.constant.ServiceNameConstants;
import com.pig4cloud.pig.common.core.util.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import java.util.List;
/**
* @author lengleng
* @date 2020/12/05
*/
@FeignClient(contextId = "remoteClientDetailsService", value = ServiceNameConstants.UMPS_SERVICE)
public interface RemoteClientDetailsService {
/**
* 通过clientId 查询客户端信息
* @param clientId 用户名
* @param from 调用标志
* @return R
*/
@GetMapping("/client/getClientDetailsById/{clientId}")
R<SysOauthClientDetails> getClientDetailsById(@PathVariable("clientId") String clientId,
@RequestHeader(SecurityConstants.FROM) String from);
/**
* 查询全部客户端
* @param from 调用标识
* @return R
*/
@GetMapping("/client/list")
R<List<SysOauthClientDetails>> listClientDetails(@RequestHeader(SecurityConstants.FROM) String from);
}

View File

@ -1,3 +1,4 @@
com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService
com.pig4cloud.pig.admin.api.feign.RemoteDictService
com.pig4cloud.pig.admin.api.feign.RemoteDeptService
com.pig4cloud.pig.admin.api.feign.RemoteLogService

View File

@ -23,6 +23,7 @@ import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails;
import com.pig4cloud.pig.admin.service.SysOauthClientDetailsService;
import com.pig4cloud.pig.common.core.util.R;
import com.pig4cloud.pig.common.log.annotation.SysLog;
import com.pig4cloud.pig.common.security.annotation.Inner;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@ -117,4 +118,11 @@ public class OauthClientDetailsController {
return R.ok();
}
@Inner(false)
@GetMapping("/getClientDetailsById/{clientId}")
public R getClientDetailsById(@PathVariable String clientId) {
return R.ok(sysOauthClientDetailsService.getOne(
Wrappers.<SysOauthClientDetails>lambdaQuery().eq(SysOauthClientDetails::getClientId, clientId), false));
}
}