1.初始化版本,2.引入thirdpart模块,引入canteen模块,初始化菜品分类,spu

This commit is contained in:
zjf committed 2024-12-06 16:54:30 +08:00
commit aff77150d6
1595 files changed
+168833

No files matched your search

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.annotation;
import com.bsd.pjcloud.common.security.component.PjcloudResourceServerAutoConfiguration;
import com.bsd.pjcloud.common.security.component.PjcloudResourceServerConfiguration;
import com.bsd.pjcloud.common.security.feign.PjcloudFeignClientConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author pjcloud
* @date 2022-06-04
* <p>
* 资源服务注解
*/
@Documented
@Inherited
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Import({ PjcloudResourceServerAutoConfiguration.class, PjcloudResourceServerConfiguration.class,
PjcloudFeignClientConfiguration.class })
public @interface EnablePjcloudResourceServer {
}
@@ -0,0 +1,28 @@
package com.bsd.pjcloud.common.security.annotation;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 判断是否有权限
*
* @author pjcloud
* @date 2024/07/15
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@pms.hasPermission('{value}'.split(','))")
public @interface HasPermission {
/**
* 权限字符串
*
* @return {@link String[] }
*/
String[] value();
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.annotation;
import java.lang.annotation.*;
/**
* 服务调用不鉴权注解
*
* @author pjcloud
* @date 2020-06-14
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {
/**
* 是否AOP统一处理
* @return false, true
*/
boolean value() default true;
/**
* 需要特殊判空的字段(预留)
* @return {}
*/
String[] field() default {};
}
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.component;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author pjcloud
* @date 2019/2/1 接口权限判断工具
*/
public class PermissionService {
/**
* 判断接口是否有任意xxx,xxx权限
*
* @param permissions 权限
* @return {boolean}
*/
public boolean hasPermission(String... permissions) {
if (ArrayUtil.isEmpty(permissions)) {
return false;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
return authorities.stream()
.map(GrantedAuthority::getAuthority)
.filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
}
/**
* 客户端模式是否有该接口权限
*
* @param scopes scope 列表
* @return {boolean}
* @PreAuthorize("@pms.hasScope('server')")
*/
public boolean hasScope(String... scopes) {
if (ArrayUtil.isEmpty(scopes)) {
return false;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
if (authentication instanceof BearerTokenAuthentication) {
BearerTokenAuthentication bearerTokenAuthentication = (BearerTokenAuthentication) authentication;
List<String> scopeList = MapUtil.get(bearerTokenAuthentication.getTokenAttributes()
, OAuth2ParameterNames.SCOPE, List.class, new ArrayList<>());
return scopeList.stream().anyMatch(x -> PatternMatchUtils.simpleMatch(scopes, x));
}
return false;
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.component;
import cn.hutool.core.util.ReUtil;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import com.bsd.pjcloud.common.security.annotation.Inner;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.*;
import java.util.regex.Pattern;
/**
* @author pjcloud
* @date 2020-03-11
* <p>
* 资源服务器对外直接暴露URL,如果设置contex-path 要特殊处理
*/
@Slf4j
@ConfigurationProperties(prefix = "security.oauth2.client")
public class PermitAllUrlProperties implements InitializingBean {
private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
private static final String[] DEFAULT_IGNORE_URLS = new String[]{"/actuator/**", "/error", "/v3/api-docs"};
@Getter
@Setter
private List<String> ignoreUrls = new ArrayList<>();
/**
* 跳过对外 URL
* <p>
* 比如某个接口是对外开放的,即使携带 token,也不鉴权
*/
@Getter
@Setter
private boolean skipPublicUrl = false;
@Override
public void afterPropertiesSet() {
ignoreUrls.addAll(Arrays.asList(DEFAULT_IGNORE_URLS));
RequestMappingHandlerMapping mapping = SpringContextHolder.getBean("requestMappingHandlerMapping");
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
map.keySet().forEach(info -> {
HandlerMethod handlerMethod = map.get(info);
// 获取方法上边的注解 替代path variable 为 *
Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
Optional.ofNullable(method)
.ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition())
.getPatternValues()
.forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*"))));
// 获取类上边的注解, 替代path variable 为 *
Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);
Optional.ofNullable(controller)
.ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition())
.getPatternValues()
.forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*"))));
});
}
}
@@ -0,0 +1,118 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.component;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author caiqy
* @date 2020.05.15
*/
public class PjcloudBearerTokenExtractor implements BearerTokenResolver {
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-:._~+/]+=*)$",
Pattern.CASE_INSENSITIVE);
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
private final PathMatcher pathMatcher = new AntPathMatcher();
private final PermitAllUrlProperties urlProperties;
public PjcloudBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
this.urlProperties = urlProperties;
}
/**
* 解析令牌
*
* @param request 请求
* @return {@link String }
*/
@Override
public String resolve(HttpServletRequest request) {
String requestUri = request.getRequestURI();
String relativePath = requestUri.substring(request.getContextPath().length());
if (urlProperties.isSkipPublicUrl()) {
boolean match = urlProperties.getIgnoreUrls().stream().anyMatch(url -> pathMatcher.match(url, relativePath));
if (match) {
return null;
}
}
final String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
if (authorizationHeaderToken != null) {
return authorizationHeaderToken;
}
return resolveFromRequestParameters(request);
}
/**
* 从授权标头解析
*
* @param request 请求
* @return {@link String }
*/
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
String authorization = request.getHeader(this.bearerTokenHeaderName);
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
return null;
}
Matcher matcher = authorizationPattern.matcher(authorization);
if (!matcher.matches()) {
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
throw new OAuth2AuthenticationException(error);
}
return matcher.group("token");
}
/**
* 从请求参数解析
*
* @param request 请求
* @return {@link String }
*/
private static String resolveFromRequestParameters(HttpServletRequest request) {
String[] values = request.getParameterValues("access_token");
if (values == null || values.length == 0) {
return null;
}
if (values.length == 1) {
return values[0];
}
BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
}
@@ -0,0 +1,40 @@
package com.bsd.pjcloud.common.security.component;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import java.util.Collection;
import java.util.Map;
/**
* @author pjcloud
* @date 2022/7/6
*
* credential 支持客户端模式的用户存储
*/
@RequiredArgsConstructor
public class PjcloudClientCredentialsOAuth2AuthenticatedPrincipal implements OAuth2AuthenticatedPrincipal {
private final Map<String, Object> attributes;
private final Collection<GrantedAuthority> authorities;
private final String name;
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getName() {
return this.name;
}
}
@@ -0,0 +1,84 @@
package com.bsd.pjcloud.common.security.component;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.service.PjcloudUserDetailsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import java.security.Principal;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* @author pjcloud
* @date 2022/5/28
*/
@Slf4j
@RequiredArgsConstructor
public class PjcloudCustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OAuth2AuthorizationService authorizationService;
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2Authorization oldAuthorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
if (Objects.isNull(oldAuthorization)) {
throw new InvalidBearerTokenException(token);
}
// 客户端模式默认返回
if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(oldAuthorization.getAuthorizationGrantType())) {
return new PjcloudClientCredentialsOAuth2AuthenticatedPrincipal(
Objects.requireNonNull(oldAuthorization.getAccessToken().getClaims()),
AuthorityUtils.NO_AUTHORITIES, oldAuthorization.getPrincipalName());
}
Map<String, PjcloudUserDetailsService> userDetailsServiceMap = SpringContextHolder
.getBeansOfType(PjcloudUserDetailsService.class);
Optional<PjcloudUserDetailsService> optional = userDetailsServiceMap.values()
.stream()
.filter(service -> service.support(Objects.requireNonNull(oldAuthorization).getRegisteredClientId(),
oldAuthorization.getAuthorizationGrantType().getValue()))
.max(Comparator.comparingInt(Ordered::getOrder));
UserDetails userDetails = null;
try {
Object principal = Objects.requireNonNull(oldAuthorization).getAttributes().get(Principal.class.getName());
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) principal;
Object tokenPrincipal = usernamePasswordAuthenticationToken.getPrincipal();
userDetails = optional.get().loadUserByUser((PjcloudUser) tokenPrincipal);
}
catch (UsernameNotFoundException notFoundException) {
log.warn("用户不不存在 {}", notFoundException.getLocalizedMessage());
throw notFoundException;
}
catch (Exception ex) {
log.error("资源服务器 introspect Token error {}", ex.getLocalizedMessage());
}
// 注入客户端信息,方便上下文中获取
PjcloudUser pjcloudUser = (PjcloudUser) userDetails;
Objects.requireNonNull(pjcloudUser)
.getAttributes()
.put(SecurityConstants.CLIENT_ID, oldAuthorization.getRegisteredClientId());
return pjcloudUser;
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.component;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
/**
* @author pjcloud
* @date 2022-06-02
*/
@RequiredArgsConstructor
@EnableConfigurationProperties(PermitAllUrlProperties.class)
public class PjcloudResourceServerAutoConfiguration {
/**
* 鉴权具体的实现逻辑
* @return (#pms.xxx)
*/
@Bean("pms")
public PermissionService permissionService() {
return new PermissionService();
}
/**
* 请求令牌的抽取逻辑
* @param urlProperties 对外暴露的接口列表
* @return BearerTokenExtractor
*/
@Bean
public PjcloudBearerTokenExtractor pigBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
return new PjcloudBearerTokenExtractor(urlProperties);
}
/**
* 资源服务器异常处理
* @param objectMapper jackson 输出对象
* @param securityMessageSource 自定义国际化处理器
* @return ResourceAuthExceptionEntryPoint
*/
@Bean
public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint(ObjectMapper objectMapper,
MessageSource securityMessageSource) {
return new ResourceAuthExceptionEntryPoint(objectMapper, securityMessageSource);
}
/**
* 资源服务器toke内省处理器
* @param authorizationService token 存储实现
* @return TokenIntrospector
*/
@Bean
public OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2AuthorizationService authorizationService) {
return new PjcloudCustomOpaqueTokenIntrospector(authorizationService);
}
}
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.component;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
* @author pjcloud
* @date 2022-06-04
* <p>
* 资源服务器认证授权配置
*/
@Slf4j
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class PjcloudResourceServerConfiguration {
protected final ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
private final PermitAllUrlProperties permitAllUrl;
private final OpaqueTokenIntrospector customOpaqueTokenIntrospector;
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
AntPathRequestMatcher[] requestMatchers = permitAllUrl.getIgnoreUrls()
.stream()
.map(AntPathRequestMatcher::new)
.toList()
.toArray(new AntPathRequestMatcher[]{});
http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.requestMatchers(requestMatchers)
.permitAll()
.anyRequest()
.authenticated())
.oauth2ResourceServer(
oauth2 -> oauth2.opaqueToken(token -> token.introspector(customOpaqueTokenIntrospector))
.authenticationEntryPoint(resourceAuthExceptionEntryPoint))
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
/**
* 支持自定义权限表达式
*
* @return {@link PrePostTemplateDefaults }
*/
@Bean
PrePostTemplateDefaults prePostTemplateDefaults() {
return new PrePostTemplateDefaults();
}
}
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.component;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.security.annotation.Inner;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.access.AccessDeniedException;
/**
* @author pjcloud
* @date 2022-06-04
*
* 服务间接口不鉴权处理逻辑
*/
@Slf4j
@Aspect
@RequiredArgsConstructor
public class PjcloudSecurityInnerAspect implements Ordered {
private final HttpServletRequest request;
@SneakyThrows
@Before("@within(inner) || @annotation(inner)")
public void around(JoinPoint point, Inner inner) {
String header = request.getHeader(SecurityConstants.FROM);
if (inner == null) {
Class<?> clazz = point.getTarget().getClass();
inner = AnnotationUtils.findAnnotation(clazz, Inner.class);
}
if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
log.warn("访问接口 {} 没有权限", point.getSignature().getName());
throw new AccessDeniedException("Access is denied");
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.component;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Locale;
import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET;
/**
* @author pjcloud
* @date 2022-06-04
* <p>
* 注入自定义错误处理,覆盖 org/springframework/security/messages 内置异常
*/
@ConditionalOnWebApplication(type = SERVLET)
public class PjcloudSecurityMessageSourceConfiguration implements WebMvcConfigurer {
@Bean
public MessageSource securityMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.addBasenames("classpath:errors/messages");
messageSource.setDefaultLocale(Locale.CHINA);
return messageSource;
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.component;
import cn.hutool.http.ContentType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.util.R;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* @author pjcloud
* @date 2019/2/1
*
* 客户端异常处理 AuthenticationException 不同细化异常处理
*/
@RequiredArgsConstructor
public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
private final MessageSource messageSource;
@Override
@SneakyThrows
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) {
response.setCharacterEncoding(CommonConstants.UTF8);
response.setContentType(ContentType.JSON.getValue());
R<String> result = new R<>();
result.setCode(CommonConstants.FAIL);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
if (authException != null) {
result.setMsg("error");
result.setData(authException.getMessage());
}
// 针对令牌过期返回特殊的 424
if (authException instanceof InvalidBearerTokenException
|| authException instanceof InsufficientAuthenticationException) {
response.setStatus(HttpStatus.FAILED_DEPENDENCY.value());
result.setMsg(this.messageSource.getMessage("OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired",
null, LocaleContextHolder.getLocale()));
}
PrintWriter printWriter = response.getWriter();
printWriter.append(objectMapper.writeValueAsString(result));
}
}
@@ -0,0 +1,34 @@
package com.bsd.pjcloud.common.security.feign;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.WebUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
/**
* TOC 客户标识传递
*
* @author pjcloud
* @date 2023/3/17
*/
@Slf4j
public class PjcloudClientToCRequestInterceptor implements RequestInterceptor {
/**
* Called for every request. Add data using methods on the supplied
* {@link RequestTemplate}.
* @param template
*/
public void apply(RequestTemplate template) {
String reqVersion = WebUtils.getRequest() != null
? WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC) : null;
if (StrUtil.isNotBlank(reqVersion)) {
log.debug("feign add header toc :{}", reqVersion);
template.header(SecurityConstants.HEADER_TOC, reqVersion);
}
}
}
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.feign;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
public class PjcloudFeignClientConfiguration {
/**
* 注入 oauth2 feign token 增强
* @param tokenResolver token获取处理器
* @return 拦截器
*/
@Bean
public RequestInterceptor oauthRequestInterceptor(BearerTokenResolver tokenResolver) {
return new PjcloudOAuthRequestInterceptor(tokenResolver);
}
@Bean
public RequestInterceptor clientToCRequestInterceptor() {
return new PjcloudClientToCRequestInterceptor();
}
}
@@ -0,0 +1,69 @@
package com.bsd.pjcloud.common.security.feign;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.WebUtils;
import com.bsd.pjcloud.common.security.util.NonWebTokenContextHolder;
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 jakarta.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* oauth2 feign token传递
* <p>
* 重新 OAuth2FeignRequestInterceptor ,官方实现部分常见不适用
*
* @author pjcloud
* @date 2022/5/29
*/
@Slf4j
@RequiredArgsConstructor
public class PjcloudOAuthRequestInterceptor implements RequestInterceptor {
private final BearerTokenResolver tokenResolver;
/**
* Create a template with the header of provided name and extracted extract </br>
* <p>
* 1. 如果使用 非web 请求,header 区别 </br>
* <p>
* 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 请求直接跳过
String token = "";
if (WebUtils.getRequest() == null) {
token = NonWebTokenContextHolder.getToken();
} else {
HttpServletRequest request = WebUtils.getRequest();
// 避免请求参数的 query token 无法传递
token = tokenResolver.resolve(request);
}
if (StrUtil.isBlank(token)) {
return;
}
template.header(HttpHeaders.AUTHORIZATION,
String.format("%s %s", OAuth2AccessToken.TokenType.BEARER.getValue(), token));
}
}
@@ -0,0 +1,25 @@
package com.bsd.pjcloud.common.security.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* @author pjcloud
* @date 2020/03/25 token 发放失败处理
*/
public interface AuthenticationFailureHandler {
/**
* 业务处理
* @param authenticationException 错误信息
* @param authentication 认证信息
* @param request 请求信息
* @param response 响应信息
*/
void handle(AuthenticationException authenticationException, Authentication authentication,
HttpServletRequest request, HttpServletResponse response);
}
@@ -0,0 +1,22 @@
package com.bsd.pjcloud.common.security.handler;
import org.springframework.security.core.Authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* @author pjcloud
* @date 2021/06/22 退出后置处理
*/
public interface AuthenticationLogoutHandler {
/**
* 业务处理
* @param authentication 认证信息
* @param request 请求信息
* @param response 响应信息
*/
void handle(Authentication authentication, HttpServletRequest request, HttpServletResponse response);
}
@@ -0,0 +1,22 @@
package com.bsd.pjcloud.common.security.handler;
import org.springframework.security.core.Authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* @author pjcloud
* @date 2020/03/25 token 发放成功处理
*/
public interface AuthenticationSuccessHandler {
/**
* 业务处理
* @param authentication 认证信息
* @param request 请求信息
* @param response 响应信息
*/
void handle(Authentication authentication, HttpServletRequest request, HttpServletResponse response);
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.security.handler;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.HttpUtil;
import com.bsd.pjcloud.common.core.util.WebUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* @author pjcloud
* @date 2019-08-20
* <p>
* 表单登录失败处理逻辑
*/
@Slf4j
public class FormAuthenticationFailureHandler implements AuthenticationFailureHandler {
/**
* Called when an authentication attempt fails.
* @param request the request during which the authentication attempt occurred.
* @param response the response.
* @param exception the exception which was thrown to reject the authentication
*/
@Override
@SneakyThrows
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) {
log.debug("表单登录失败:{}", exception.getLocalizedMessage());
String url = HttpUtil.encodeParams(String.format("/token/login?error=%s", exception.getMessage()),
CharsetUtil.CHARSET_UTF_8);
WebUtils.getResponse().sendRedirect(url);
}
}
@@ -0,0 +1,39 @@
package com.bsd.pjcloud.common.security.handler;
import cn.hutool.core.util.StrUtil;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author pjcloud
* @date 2020/10/6
* <p>
* sso 退出功能 ,根据客户端传入跳转
*/
public class SsoLogoutSuccessHandler implements LogoutSuccessHandler {
private static final String REDIRECT_URL = "redirect_url";
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
// 获取请求参数中是否包含 回调地址
String redirectUrl = request.getParameter(REDIRECT_URL);
String referer = request.getHeader(HttpHeaders.REFERER);
if (StrUtil.isNotBlank(redirectUrl)) {
response.sendRedirect(redirectUrl);
}
else if (StrUtil.isNotBlank(referer)) {
// 默认跳转referer 地址
response.sendRedirect(referer);
}
}
}
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.security.service;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.enums.UserTypeEnum;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.RetOps;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.Objects;
import java.util.Optional;
/**
* 用户详细信息 default
*
* @author pjcloud
*/
@Slf4j
@Primary
@RequiredArgsConstructor
public class PjcloudDefaultUserDetailsServiceImpl implements PjcloudUserDetailsService {
private final RemoteUserService remoteUserService;
private final CacheManager cacheManager;
/**
* 用户密码登录
*
* @param username 用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
@SneakyThrows
public UserDetails loadUserByUsername(String username) {
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
if (Objects.nonNull(cache) && Objects.nonNull(cache.get(username))) {
return getUserDetails(Optional.ofNullable(cache.get(username, UserInfo.class)));
}
R<UserInfo> result = remoteUserService.info(username);
Optional<UserInfo> userInfoOptional = RetOps.of(result).getData();
userInfoOptional.ifPresent(userInfo -> Objects.requireNonNull(cache).put(username, userInfo));
return getUserDetails(userInfoOptional);
}
/**
* 通过用户实体查询
*
* @param pjcloudUser user
* @return
*/
@Override
public UserDetails loadUserByUser(PjcloudUser pjcloudUser) {
// 避免 C端用户通过接口调用B端接口的安全问题
if (UserTypeEnum.TOB.getStatus().equals(pjcloudUser.getUserType())) {
return loadUserByUsername(pjcloudUser.getUsername());
}
return null;
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}
@@ -0,0 +1,48 @@
package com.bsd.pjcloud.common.security.service;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.RetOps;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* @author aeizzz
*/
@Slf4j
@RequiredArgsConstructor
public class PjcloudMobileUserDetailServiceImpl implements PjcloudUserDetailsService {
private final UserDetailsService pjcloudDefaultUserDetailsServiceImpl;
private final RemoteUserService remoteUserService;
@Override
@SneakyThrows
public UserDetails loadUserByUsername(String phone) {
R<UserInfo> result = remoteUserService.social(phone);
return getUserDetails(RetOps.of(result).getData());
}
@Override
public UserDetails loadUserByUser(PjcloudUser pjcloudUser) {
return pjcloudDefaultUserDetailsServiceImpl.loadUserByUsername(pjcloudUser.getUsername());
}
/**
* 支持所有的 mobile 类型
* @param clientId 目标客户端
* @param grantType 授权类型
* @return true/false
*/
@Override
public boolean support(String clientId, String grantType) {
return SecurityConstants.GRANT_MOBILE.equals(grantType);
}
}
@@ -0,0 +1,49 @@
package com.bsd.pjcloud.common.security.service;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.util.Assert;
import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class PjcloudRedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
private final RedisTemplate<String, Object> redisTemplate;
private final static Long TIMEOUT = 10L;
@Override
public void save(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
redisTemplate.opsForValue()
.set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT, TimeUnit.MINUTES);
}
@Override
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
redisTemplate.delete(buildKey(authorizationConsent));
}
@Override
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return (OAuth2AuthorizationConsent) redisTemplate.opsForValue()
.get(buildKey(registeredClientId, principalName));
}
private static String buildKey(String registeredClientId, String principalName) {
return "token:consent:" + registeredClientId + ":" + principalName;
}
private static String buildKey(OAuth2AuthorizationConsent authorizationConsent) {
return buildKey(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
}
}
@@ -0,0 +1,189 @@
package com.bsd.pjcloud.common.security.service;
import cn.hutool.core.collection.CollUtil;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.util.Assert;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author pjcloud
* @date 2022/5/27
*/
@RequiredArgsConstructor
public class PjcloudRedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
private final static Long TIMEOUT = 10L;
private static final String AUTHORIZATION = "token";
private final RedisTemplate<String, Object> redisTemplate;
private final KeyStrResolver tenantKeyStrResolver;
@Override
public void save(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
if (isState(authorization)) {
String token = authorization.getAttribute("state");
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.opsForValue()
.set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT, TimeUnit.MINUTES);
}
if (isCode(authorization)) {
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
.getToken(OAuth2AuthorizationCode.class);
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
authorizationCodeToken.getExpiresAt());
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.opsForValue()
.set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()), authorization,
between, TimeUnit.MINUTES);
}
if (isRefreshToken(authorization)) {
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.opsForValue()
.set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()), authorization, between,
TimeUnit.SECONDS);
}
if (isAccessToken(authorization)) {
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.opsForValue()
.set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()), authorization, between,
TimeUnit.SECONDS);
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
String tokenUsername = String.format("%s::%s::%s::%s::%s", tenantKeyStrResolver.key(), AUTHORIZATION,
SecurityConstants.DETAILS_USERNAME, authorization.getPrincipalName(), accessToken.getTokenValue());
redisTemplate.opsForValue().set(tokenUsername, accessToken.getTokenValue(), between, TimeUnit.SECONDS);
}
}
@Override
public void remove(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
List<String> keys = new ArrayList<>();
if (isState(authorization)) {
String token = authorization.getAttribute("state");
keys.add(buildKey(OAuth2ParameterNames.STATE, token));
}
if (isCode(authorization)) {
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
.getToken(OAuth2AuthorizationCode.class);
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
keys.add(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()));
}
if (isRefreshToken(authorization)) {
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
keys.add(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()));
}
if (isAccessToken(authorization)) {
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
String key = String.format("%s::%s::%s::%s::%s", tenantKeyStrResolver.key(), AUTHORIZATION,
SecurityConstants.DETAILS_USERNAME, authorization.getPrincipalName(), accessToken.getTokenValue());
keys.add(key);
}
redisTemplate.delete(keys);
}
@Override
@Nullable
public OAuth2Authorization findById(String id) {
throw new UnsupportedOperationException();
}
@Override
@Nullable
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
Assert.hasText(token, "token cannot be empty");
Assert.notNull(tokenType, "tokenType cannot be empty");
redisTemplate.setValueSerializer(RedisSerializer.java());
return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token));
}
private String buildKey(String type, String id) {
return String.format("%s::%s::%s::%s", tenantKeyStrResolver.key(), AUTHORIZATION, type, id);
}
private static boolean isState(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getAttribute("state"));
}
private static boolean isCode(OAuth2Authorization authorization) {
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
.getToken(OAuth2AuthorizationCode.class);
return Objects.nonNull(authorizationCode);
}
private static boolean isRefreshToken(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getRefreshToken());
}
private static boolean isAccessToken(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getAccessToken());
}
/**
* 扩展方法根据 username 查询是否存在存储的
* @param authentication
* @return
*/
public void removeByUsername(Authentication authentication) {
// 根据 username查询对应access-token
String authenticationName = authentication.getName();
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
String tokenUsernameKey = String.format("%s::%s::%s::%s::*", tenantKeyStrResolver.key(), AUTHORIZATION,
SecurityConstants.DETAILS_USERNAME, authenticationName);
Set<String> keys = redisTemplate.keys(tokenUsernameKey);
if (CollUtil.isEmpty(keys)) {
return;
}
List<Object> tokenList = redisTemplate.opsForValue().multiGet(keys);
for (Object token : tokenList) {
// 根据token 查询存储的 OAuth2Authorization
OAuth2Authorization authorization = this.findByToken((String) token, OAuth2TokenType.ACCESS_TOKEN);
// 根据 OAuth2Authorization 删除相关令牌
this.remove(authorization);
}
}
}
@@ -0,0 +1,162 @@
package com.bsd.pjcloud.common.security.service;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.bsd.pjcloud.admin.api.entity.SysOauthClientDetails;
import com.bsd.pjcloud.admin.api.feign.RemoteClientDetailsService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.MsgUtils;
import com.bsd.pjcloud.common.core.util.RetOps;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
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.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* 查询客户端相关信息实现
*
* @author pjcloud
* @date 2022/5/29
*/
@RequiredArgsConstructor
public class PjcloudRemoteRegisteredClientRepository implements RegisteredClientRepository {
private final static String NOTFOUND_CLIENT_ERROR_CODE = "RegisteredClient.notFound";
/**
* 刷新令牌有效期默认 30 天
*/
private final static int refreshTokenValiditySeconds = 60 * 60 * 24 * 30;
/**
* 请求令牌有效期默认 12 小时
*/
private final static int accessTokenValiditySeconds = 60 * 60 * 12;
private final RemoteClientDetailsService clientDetailsService;
private final CacheManager cacheManager;
/**
* 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) {
}
/**
* 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}
*/
/**
* 按客户端 ID 查找
*
* @param clientId 客户端 ID
* @return {@link RegisteredClient }
*/
@Override
@SneakyThrows
public RegisteredClient findByClientId(String clientId) {
Cache cache = cacheManager.getCache(CacheConstants.CLIENT_DETAILS_KEY);
SysOauthClientDetails clientDetails = null;
if (Objects.nonNull(cache) && Objects.nonNull(cache.get(clientId))) {
clientDetails = cache.get(clientId, SysOauthClientDetails.class);
}
if (Objects.isNull(clientDetails)) {
SysOauthClientDetails fetchedClientDetails = RetOps
.of(clientDetailsService.getClientDetailsById(clientId))
.getData()
.orElseThrow(() -> new OAuth2AuthorizationCodeRequestAuthenticationException(
new OAuth2Error(MsgUtils.getSecurityMessage(NOTFOUND_CLIENT_ERROR_CODE)), null));
clientDetails = fetchedClientDetails;
Optional.ofNullable(cache).ifPresent(c -> c.put(clientId, fetchedClientDetails));
}
final SysOauthClientDetails finalClientDetails = clientDetails;
RegisteredClient.Builder builder = RegisteredClient.withId(finalClientDetails.getClientId())
.clientId(finalClientDetails.getClientId())
.clientSecret(SecurityConstants.NOOP + finalClientDetails.getClientSecret())
.clientAuthenticationMethods(clientAuthenticationMethods -> {
clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
});
// 授权模式
Arrays.stream(finalClientDetails.getAuthorizedGrantTypes())
.forEach(grant -> builder.authorizationGrantType(new AuthorizationGrantType(grant)));
// 回调地址
Optional.ofNullable(finalClientDetails.getWebServerRedirectUri())
.ifPresent(redirectUri -> Arrays.stream(redirectUri.split(StrUtil.COMMA))
.filter(StrUtil::isNotBlank)
.forEach(builder::redirectUri));
// scope
Optional.ofNullable(finalClientDetails.getScope())
.ifPresent(scope -> Arrays.stream(scope.split(StrUtil.COMMA))
.filter(StrUtil::isNotBlank)
.forEach(builder::scope));
// 注入扩展配置
Optional.ofNullable(finalClientDetails.getAdditionalInformation()).ifPresent(ext -> {
Map<String, Object> map = JSONUtil.parseObj(ext).toBean(Map.class);
builder.clientSettings(ClientSettings.withSettings(map)
.requireProofKey(false)
.requireAuthorizationConsent(!BooleanUtil.toBoolean(finalClientDetails.getAutoapprove()))
.build());
});
return builder
.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
.accessTokenTimeToLive(Duration.ofSeconds(
Optional.ofNullable(finalClientDetails.getAccessTokenValidity()).orElse(accessTokenValiditySeconds)))
.refreshTokenTimeToLive(Duration.ofSeconds(Optional.ofNullable(finalClientDetails.getRefreshTokenValidity())
.orElse(refreshTokenValiditySeconds)))
.build())
.build();
}
}
@@ -0,0 +1,143 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.service;
import cn.hutool.core.util.ArrayUtil;
import com.bsd.pjcloud.app.api.dto.AppUserInfo;
import com.bsd.pjcloud.app.api.entity.AppUser;
import com.bsd.pjcloud.app.api.feign.RemoteAppUserService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.constant.enums.UserTypeEnum;
import com.bsd.pjcloud.common.core.util.MsgUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.RetOps;
import com.bsd.pjcloud.common.core.util.WebUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
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.UsernameNotFoundException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import java.util.*;
/**
* 用户详细信息
*
* @author pjcloud hccake
*/
@Slf4j
@RequiredArgsConstructor
public class PjcloudTocDefaultUserDetailsServiceImpl implements PjcloudUserDetailsService {
private final CacheManager cacheManager;
private final Optional<RemoteAppUserService> remoteAppUserServiceOptional;
/**
* 用户密码登录
*
* @param username 用户密码登录
* @return
*/
@Override
@SneakyThrows
public UserDetails loadUserByUsername(String username) {
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS_MINI);
if (Objects.nonNull(cache) && Objects.nonNull(cache.get(username))) {
AppUserInfo appUserInfo = cache.get(username, AppUserInfo.class);
return getUserDetailsAppUser(R.ok(appUserInfo));
}
R<AppUserInfo> result = remoteAppUserServiceOptional.get().info(username);
Optional<AppUserInfo> userInfoOptional = RetOps.of(result).getData();
userInfoOptional.ifPresent(userInfo -> Objects.requireNonNull(cache).put(username, userInfo));
return getUserDetailsAppUser(result);
}
@Override
public UserDetails loadUserByUser(PjcloudUser pjcloudUser) {
return pjcloudUser;
}
UserDetails getUserDetailsAppUser(R<AppUserInfo> result) {
// @formatter:off
return RetOps.of(result)
.assertSuccess(r -> new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE),null)))
.getData()
.map(this::convertUserDetailsAppUser)
.orElseThrow(() -> new UsernameNotFoundException(MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE)));
// @formatter:on
}
/**
* UserInfo 转 UserDetails
*
* @param info
* @return 返回UserDetails对象
*/
UserDetails convertUserDetailsAppUser(AppUserInfo info) {
Set<String> dbAuthsSet = new HashSet<>();
if (ArrayUtil.isNotEmpty(info.getRoles())) {
// 获取角色
Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));
// 获取资源
dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
}
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
AppUser user = info.getAppUser();
// 构造security用户
return new PjcloudUser(user.getUserId(), user.getUsername(), null, user.getPhone(), user.getAvatar(),
user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(),
SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOC.getStatus(), true,
!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
}
@Override
public int getOrder() {
return 10;
}
/**
* 支持所有的 mobile 类型
*
* @param clientId 目标客户端
* @param grantType 授权类型
* @return true/false
*/
@Override
public boolean support(String clientId, String grantType) {
if (Objects.isNull(WebUtils.getRequest())) {
return false;
}
String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC);
return SecurityConstants.HEADER_TOC_YES.equals(header);
}
}
@@ -0,0 +1,135 @@
/*
* Copyright (c) 2020 pig4cloud 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
*
* http://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.bsd.pjcloud.common.security.service;
import cn.hutool.core.util.ArrayUtil;
import com.bsd.pjcloud.app.api.dto.AppUserInfo;
import com.bsd.pjcloud.app.api.entity.AppUser;
import com.bsd.pjcloud.app.api.feign.RemoteAppUserService;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.constant.enums.UserTypeEnum;
import com.bsd.pjcloud.common.core.util.MsgUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.RetOps;
import com.bsd.pjcloud.common.core.util.WebUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
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.UsernameNotFoundException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import java.util.*;
import static com.bsd.pjcloud.common.security.service.PjcloudTocDefaultUserDetailsServiceImpl.NOTFOUND_USER_ERROR_CODE;
/**
* 用户详细信息
*
* @author pjcloud hccake
*/
@Slf4j
@RequiredArgsConstructor
public class PjcloudTocMobileUserDetailsServiceImpl implements PjcloudUserDetailsService {
private final Optional<RemoteAppUserService> remoteAppUserServiceOptional;
/**
* 用户密码登录
*
* @param phone 用户密码登录
* @return
*/
@Override
@SneakyThrows
public UserDetails loadUserByUsername(String phone) {
if (!remoteAppUserServiceOptional.isPresent()) {
throw new UnsupportedOperationException();
}
R<AppUserInfo> info = remoteAppUserServiceOptional.get().social(phone);
return this.getUserDetailsAppUser(info);
}
@Override
public UserDetails loadUserByUser(PjcloudUser pjcloudUser) {
return pjcloudUser;
}
UserDetails getUserDetailsAppUser(R<AppUserInfo> result) {
// @formatter:off
return RetOps.of(result)
.assertSuccess(r -> new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE),null)))
.getData()
.map(this::convertUserDetailsAppUser)
.orElseThrow(() -> new UsernameNotFoundException(MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE)));
// @formatter:on
}
/**
* UserInfo 转 UserDetails
*
* @param info
* @return 返回UserDetails对象
*/
UserDetails convertUserDetailsAppUser(AppUserInfo info) {
Set<String> dbAuthsSet = new HashSet<>();
if (ArrayUtil.isNotEmpty(info.getRoles())) {
// 获取角色
Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));
// 获取资源
dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
}
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
AppUser user = info.getAppUser();
// 构造security用户
return new PjcloudUser(user.getUserId(), user.getUsername(), null, user.getPhone(), user.getAvatar(),
user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(),
SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOC.getStatus(), true,
!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
}
@Override
public int getOrder() {
return 15;
}
/**
* 支持所有的 mobile 类型
*
* @param clientId 目标客户端
* @param grantType 授权类型
* @return true/false
*/
@Override
public boolean support(String clientId, String grantType) {
if (Objects.isNull(WebUtils.getRequest())) {
return false;
}
String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC);
return SecurityConstants.HEADER_TOC_YES.equals(header) && SecurityConstants.GRANT_MOBILE.equals(grantType);
}
}
@@ -0,0 +1,147 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.security.service;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* @author pjcloud
* @date 2020/4/16 扩展用户信息
*/
public class PjcloudUser extends User implements OAuth2AuthenticatedPrincipal {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 扩展属性,方便存放oauth 上下文相关信息
*/
private final Map<String, Object> attributes = new HashMap<>();
/**
* 用户ID
*/
@Getter
private Long id;
/**
* 部门ID
*/
@Getter
private Long deptId;
/**
* 手机号
*/
@Getter
private String phone;
/**
* 头像
*/
@Getter
private String avatar;
/**
* 租户ID
*/
@Getter
private Long tenantId;
/**
* 拓展字段:昵称
*/
@Getter
private String nickname;
/**
* 拓展字段:姓名
*/
@Getter
private String name;
/**
* 拓展字段:邮箱
*/
@Getter
private String email;
@Getter
private String userType;
/**
* Construct the <code>User</code> with the details required by
* {@link DaoAuthenticationProvider}.
* @param id 用户ID
* @param deptId 部门ID
* @param tenantId 租户ID
* @param nickname 昵称
* @param name 姓名
* @param email 邮箱 the username presented to the
* <code>DaoAuthenticationProvider</code>
* @param password the password that should be presented to the
* <code>DaoAuthenticationProvider</code>
* @param enabled set to <code>true</code> if the user is enabled
* @param accountNonExpired set to <code>true</code> if the account has not expired
* @param credentialsNonExpired set to <code>true</code> if the credentials have not
* expired
* @param accountNonLocked set to <code>true</code> if the account is not locked
* @param authorities the authorities that should be granted to the caller if they
* presented the correct username and password and the user is enabled. Not null.
* @throws IllegalArgumentException if a <code>null</code> value was passed either as
* a parameter or as an element in the <code>GrantedAuthority</code> collection
*/
@JsonCreator
public PjcloudUser(@JsonProperty("id") Long id, @JsonProperty("username") String username,
@JsonProperty("deptId") Long deptId, @JsonProperty("phone") String phone,
@JsonProperty("avatar") String avatar, @JsonProperty("nickname") String nickname,
@JsonProperty("name") String name, @JsonProperty("email") String email,
@JsonProperty("tenantId") Long tenantId, @JsonProperty("password") String password,
@JsonProperty("enabled") boolean enabled, @JsonProperty("accountNonExpired") boolean accountNonExpired,
@JsonProperty("userType") String userType,
@JsonProperty("credentialsNonExpired") boolean credentialsNonExpired,
@JsonProperty("accountNonLocked") boolean accountNonLocked,
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.id = id;
this.deptId = deptId;
this.phone = phone;
this.avatar = avatar;
this.tenantId = tenantId;
this.nickname = nickname;
this.name = name;
this.email = email;
this.userType = userType;
}
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}
}
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.security.service;
import cn.hutool.core.util.ArrayUtil;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.constant.enums.UserTypeEnum;
import com.bsd.pjcloud.common.core.util.MsgUtils;
import org.springframework.core.Ordered;
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.*;
/**
* @author pjcloud
* @date 2018/8/15
*/
public interface PjcloudUserDetailsService extends UserDetailsService, Ordered {
/**
* Notfound 用户错误代码
*/
String NOTFOUND_USER_ERROR_CODE = "UserDetailsService.notFound";
/**
* 是否支持此客户端校验
*
* @param clientId 请求客户端
* @param grantType 授权类型
* @return true/false
*/
default boolean support(String clientId, String grantType) {
return true;
}
/**
* 排序值 默认取最大的
*
* @return 排序值
*/
default int getOrder() {
return 0;
}
/**
* 获取用户详细信息
*
* @param userInfoOptional 用户信息:可选
* @return {@link UserDetails }
*/
default UserDetails getUserDetails(Optional<UserInfo> userInfoOptional) {
// @formatter:off
return userInfoOptional
.map(this::convertUserDetails)
.orElseThrow(() -> new UsernameNotFoundException(MsgUtils.getSecurityMessage(NOTFOUND_USER_ERROR_CODE)));
// @formatter:on
}
/**
* UserInfo 转 UserDetails
*
* @param info
* @return 返回UserDetails对象
*/
default UserDetails convertUserDetails(UserInfo info) {
Set<String> dbAuthsSet = new HashSet<>();
if (ArrayUtil.isNotEmpty(info.getRoles())) {
// 获取角色
Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));
// 获取资源
dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
}
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
SysUser user = info.getSysUser();
// 构造security用户
return new PjcloudUser(user.getUserId(), user.getUsername(), user.getDeptId(), user.getPhone(), user.getAvatar(),
user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(),
SecurityConstants.BCRYPT + user.getPassword()
, true, true, UserTypeEnum.TOB.getStatus()
, !CommonConstants.STATUS_LOCK.equals(user.getPasswordExpireFlag()) // 密码过期判断
, !CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
}
/**
* 通过用户实体查询
*
* @param pjcloudUser user
* @return
*/
default UserDetails loadUserByUser(PjcloudUser pjcloudUser) {
return this.loadUserByUsername(pjcloudUser.getUsername());
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.security.util;
import cn.hutool.core.codec.Base64;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import jakarta.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
/**
* @author pjcloud
* @date 2018/5/13 认证授权相关工具类
*/
@Slf4j
@UtilityClass
public class AuthUtils {
private final String BASIC_ = "Basic ";
/**
* 从header 请求中的clientId/clientsecect
* @param header header中的参数
* @throws RuntimeException if the Basic header is not present or is not valid Base64
*/
@SneakyThrows
public String[] extractAndDecodeHeader(String header) {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
}
catch (IllegalArgumentException e) {
throw new RuntimeException("Failed to decode basic authentication token");
}
String token = new String(decoded, StandardCharsets.UTF_8);
int delim = token.indexOf(":");
if (delim == -1) {
throw new RuntimeException("Invalid basic authentication token");
}
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
}
/**
* *从header 请求中的clientId/clientsecect
* @param request
* @return
*/
@SneakyThrows
public String[] extractAndDecodeHeader(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null || !header.startsWith(BASIC_)) {
throw new RuntimeException("请求头中client信息为空");
}
return extractAndDecodeHeader(header);
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.security.util;
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.experimental.UtilityClass;
/**
* @author pjcloud
* @date 2024-04-23
* <p>
* 在非web 调用feign 传递之前手动set token
*/
@UtilityClass
public class NonWebTokenContextHolder {
private final ThreadLocal<String> THREAD_LOCAL_VERSION = new TransmittableThreadLocal<>();
/**
* TTL 设置token<br/>
*
* @param token token
*/
public void setToken(String token) {
THREAD_LOCAL_VERSION.set(token);
}
/**
* 获取TTL中的token
*
* @return
*/
public String getToken() {
return THREAD_LOCAL_VERSION.get();
}
public void clear() {
THREAD_LOCAL_VERSION.remove();
}
}
@@ -0,0 +1,79 @@
package com.bsd.pjcloud.common.security.util;
import cn.hutool.core.map.MapUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.experimental.UtilityClass;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.Map;
/**
* @author jumuning
* @description OAuth2 端点工具
*/
@UtilityClass
public class OAuth2EndpointUtils {
public final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
public MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
for (String value : values) {
parameters.add(key, value);
}
});
return parameters;
}
public boolean matchesPkceTokenRequest(HttpServletRequest request) {
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue()
.equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))
&& request.getParameter(OAuth2ParameterNames.CODE) != null
&& request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
}
public void throwError(String errorCode, String parameterName, String errorUri) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthenticationException(error);
}
/**
* 格式化输出token 信息
* @param authentication 用户认证信息
* @param claims 扩展信息
* @return
* @throws IOException
*/
public OAuth2AccessTokenResponse sendAccessTokenResponse(OAuth2Authorization authentication,
Map<String, Object> claims) {
OAuth2AccessToken accessToken = authentication.getAccessToken().getToken();
OAuth2RefreshToken refreshToken = authentication.getRefreshToken().getToken();
OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes());
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
}
if (refreshToken != null) {
builder.refreshToken(refreshToken.getTokenValue());
}
if (MapUtil.isNotEmpty(claims)) {
builder.additionalParameters(claims);
}
return builder.build();
}
}
@@ -0,0 +1,43 @@
package com.bsd.pjcloud.common.security.util;
/**
* @author jumuning
* @description OAuth2 异常信息
*/
public interface OAuth2ErrorCodesExpand {
/** 用户名未找到 */
String USERNAME_NOT_FOUND = "username_not_found";
/** 错误凭证 */
String BAD_CREDENTIALS = "bad_credentials";
/** 用户被锁 */
String USER_LOCKED = "user_locked";
/** 用户禁用 */
String USER_DISABLE = "user_disable";
/** 用户过期 */
String USER_EXPIRED = "user_expired";
/** 证书过期 */
String CREDENTIALS_EXPIRED = "credentials_expired";
/** scope 为空异常 */
String SCOPE_IS_EMPTY = "scope_is_empty";
/**
* 令牌不存在
*/
String TOKEN_MISSING = "token_missing";
/** 未知的登录异常 */
String UN_KNOW_LOGIN_ERROR = "un_know_login_error";
/**
* 不合法的Token
*/
String INVALID_BEARER_TOKEN = "invalid_bearer_token";
}
@@ -0,0 +1,29 @@
package com.bsd.pjcloud.common.security.util;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
/**
* @author pjcloud
* @description OAuthClientException 异常信息
*/
public class OAuthClientException extends OAuth2AuthenticationException {
/**
* Constructs a <code>ScopeException</code> with the specified message.
* @param msg the detail message.
*/
public OAuthClientException(String msg) {
super(new OAuth2Error(msg), msg);
}
/**
* Constructs a {@code ScopeException} with the specified message and root cause.
* @param msg the detail message.
* @param cause root cause
*/
public OAuthClientException(String msg, Throwable cause) {
super(new OAuth2Error(msg), cause);
}
}
@@ -0,0 +1,29 @@
package com.bsd.pjcloud.common.security.util;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
/**
* @author jumuning
* @description ScopeException 异常信息
*/
public class ScopeException extends OAuth2AuthenticationException {
/**
* Constructs a <code>ScopeException</code> with the specified message.
* @param msg the detail message.
*/
public ScopeException(String msg) {
super(new OAuth2Error(msg), msg);
}
/**
* Constructs a {@code ScopeException} with the specified message and root cause.
* @param msg the detail message.
* @param cause root cause
*/
public ScopeException(String msg, Throwable cause) {
super(new OAuth2Error(msg), cause);
}
}
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2018-2025, pjcloud 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: pjcloud
*/
package com.bsd.pjcloud.common.security.util;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import lombok.experimental.UtilityClass;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 安全工具类
*
* @author L.cm
*/
@UtilityClass
public class SecurityUtils {
/**
* 获取Authentication
*/
public Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
/**
* 获取Authentication Token
*
* @return
*/
public String getToken() {
Authentication authentication = SecurityUtils.getAuthentication();
if (authentication instanceof BearerTokenAuthentication bearerTokenAuthentication) {
return bearerTokenAuthentication.getToken().getTokenValue();
}
return null;
}
/**
* 获取用户
*
* @param authentication
* @return PjcloudUser
* <p>
*/
public PjcloudUser getUser(Authentication authentication) {
if (authentication == null) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal instanceof PjcloudUser) {
return (PjcloudUser) principal;
}
return null;
}
/**
* 获取用户
*/
public PjcloudUser getUser() {
Authentication authentication = getAuthentication();
return getUser(authentication);
}
/**
* 获取用户角色信息
*
* @return 角色集合
*/
public List<Long> getRoleIds() {
Authentication authentication = getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
List<Long> roleIds = new ArrayList<>();
authorities.stream()
.filter(granted -> StrUtil.startWith(granted.getAuthority(), SecurityConstants.ROLE))
.forEach(granted -> {
String id = StrUtil.removePrefix(granted.getAuthority(), SecurityConstants.ROLE);
roleIds.add(Long.parseLong(id));
});
return roleIds;
}
}
@@ -0,0 +1,9 @@
com.bsd.pjcloud.common.security.service.PjcloudDefaultUserDetailsServiceImpl
com.bsd.pjcloud.common.security.service.PjcloudTocDefaultUserDetailsServiceImpl
com.bsd.pjcloud.common.security.service.PjcloudMobileUserDetailServiceImpl
com.bsd.pjcloud.common.security.service.PjcloudTocMobileUserDetailsServiceImpl
com.bsd.pjcloud.common.security.service.PjcloudRedisOAuth2AuthorizationService
com.bsd.pjcloud.common.security.service.PjcloudRedisOAuth2AuthorizationConsentService
com.bsd.pjcloud.common.security.component.PjcloudSecurityInnerAspect
com.bsd.pjcloud.common.security.component.PjcloudSecurityMessageSourceConfiguration
com.bsd.pjcloud.common.security.service.PjcloudRemoteRegisteredClientRepository
@@ -0,0 +1,72 @@
#
# 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)
# --------------------------------------
# PIGX \u4E2D\u7684\u5F02\u5E38\u4FE1\u606F\u4F7F\u7528\u6B64\u6587\u4EF6\u5B9A\u4E49
#
AbstractAccessDecisionManager.accessDenied=\u6743\u9650\u4E0D\u8DB3,\u4E0D\u5141\u8BB8\u8BBF\u95EE{0}
AbstractAccessDecisionManager.expireToken=token \u8FC7\u671F
AbstractLdapAuthenticationProvider.emptyPassword=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
AbstractSecurityInterceptor.authenticationNotFound=\u672A\u5728SecurityContext\u4E2D\u67E5\u627E\u5230\u8BA4\u8BC1\u5BF9\u8C61
AbstractUserDetailsAuthenticationProvider.badCredentials=\u7528\u6237\u540D\u4E0D\u5B58\u5728\u6216\u8005\u5BC6\u7801\u9519\u8BEF
AbstractUserDetailsAuthenticationProvider.badClientCredentials=\u5BA2\u6237\u7AEF\u4FE1\u606F\u9519\u8BEF\uFF0CBasic\u8BA4\u8BC1\u5931\u8D25
AbstractUserDetailsAuthenticationProvider.smsBadCredentials=\u7528\u6237\u4E0D\u5B58\u5728\uFF0C\u767B\u5F55\u5931\u8D25
AbstractUserDetailsAuthenticationProvider.noopBindAccount=\u672A\u7ED1\u5B9A\u767B\u5F55\u8D26\u53F7
AbstractUserDetailsAuthenticationProvider.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
AbstractUserDetailsAuthenticationProvider.disabled=\u7528\u6237\u672A\u6FC0\u6D3B
AbstractUserDetailsAuthenticationProvider.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F
AbstractUserDetailsAuthenticationProvider.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A
AbstractUserDetailsAuthenticationProvider.onlySupports=\u4EC5\u4EC5\u652F\u6301UsernamePasswordAuthenticationToken
AbstractUserDetailsAuthenticationProvider.badTenantId=\u65E0\u6548\u7684\u79DF\u6237\u7F16\u53F7:{0}
AccountStatusUserDetailsChecker.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
AccountStatusUserDetailsChecker.disabled=\u7528\u6237\u672A\u6FC0\u6D3B
AccountStatusUserDetailsChecker.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F
AccountStatusUserDetailsChecker.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A
AclEntryAfterInvocationProvider.noPermission=\u7ED9\u5B9A\u7684Authentication\u5BF9\u8C61({0})\u6839\u672C\u65E0\u6743\u64CD\u63A7\u9886\u57DF\u5BF9\u8C61({1})
AnonymousAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684AnonymousAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
BindAuthenticator.badCredentials=\u5BC6\u7801\u9519\u8BEF
BindAuthenticator.emptyPassword=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
CasAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684CasAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
CasAuthenticationProvider.noServiceTicket=\u672A\u80FD\u591F\u6B63\u786E\u63D0\u4F9B\u5F85\u9A8C\u8BC1\u7684CAS\u670D\u52A1\u7968\u6839
ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=\u5DF2\u7ECF\u8D85\u8FC7\u4E86\u5F53\u524D\u4E3B\u4F53({0})\u88AB\u5141\u8BB8\u7684\u6700\u5927\u4F1A\u8BDD\u6570\u91CF
DigestAuthenticationFilter.incorrectRealm=\u54CD\u5E94\u7ED3\u679C\u4E2D\u7684Realm\u540D\u5B57({0})\u540C\u7CFB\u7EDF\u6307\u5B9A\u7684Realm\u540D\u5B57({1})\u4E0D\u543B\u5408
DigestAuthenticationFilter.incorrectResponse=\u9519\u8BEF\u7684\u54CD\u5E94\u7ED3\u679C
DigestAuthenticationFilter.missingAuth=\u9057\u6F0F\u4E86\u9488\u5BF9'auth' QOP\u7684\u3001\u5FC5\u987B\u7ED9\u5B9A\u7684\u6458\u8981\u53D6\u503C; \u63A5\u6536\u5230\u7684\u5934\u4FE1\u606F\u4E3A{0}
DigestAuthenticationFilter.missingMandatory=\u9057\u6F0F\u4E86\u5FC5\u987B\u7ED9\u5B9A\u7684\u6458\u8981\u53D6\u503C; \u63A5\u6536\u5230\u7684\u5934\u4FE1\u606F\u4E3A{0}
DigestAuthenticationFilter.nonceCompromised=Nonce\u4EE4\u724C\u5DF2\u7ECF\u5B58\u5728\u95EE\u9898\u4E86\uFF0C{0}
DigestAuthenticationFilter.nonceEncoding=Nonce\u672A\u7ECF\u8FC7Base64\u7F16\u7801; \u76F8\u5E94\u7684nonce\u53D6\u503C\u4E3A {0}
DigestAuthenticationFilter.nonceExpired=Nonce\u5DF2\u7ECF\u8FC7\u671F/\u8D85\u65F6
DigestAuthenticationFilter.nonceNotNumeric=Nonce\u4EE4\u724C\u7684\u7B2C1\u90E8\u5206\u5E94\u8BE5\u662F\u6570\u5B57\uFF0C\u4F46\u7ED3\u679C\u5374\u662F{0}
DigestAuthenticationFilter.nonceNotTwoTokens=Nonce\u5E94\u8BE5\u7531\u4E24\u90E8\u5206\u53D6\u503C\u6784\u6210\uFF0C\u4F46\u7ED3\u679C\u5374\u662F{0}
DigestAuthenticationFilter.usernameNotFound=\u7528\u6237\u540D{0}\u672A\u627E\u5230
JdbcDaoImpl.noAuthority=\u6CA1\u6709\u4E3A\u7528\u6237{0}\u6307\u5B9A\u89D2\u8272
JdbcDaoImpl.notFound=\u672A\u627E\u5230\u7528\u6237{0}
UserDetailsService.notFound=\u7528\u6237\u4E0D\u5B58\u5728
RegisteredClient.notFound=\u5BA2\u6237\u7AEF\u4FE1\u606F\u67E5\u8BE2\u5931\u8D25
LdapAuthenticationProvider.badCredentials=\u574F\u7684\u51ED\u8BC1
LdapAuthenticationProvider.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
LdapAuthenticationProvider.disabled=\u7528\u6237\u672A\u6FC0\u6D3B
LdapAuthenticationProvider.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F
LdapAuthenticationProvider.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A
LdapAuthenticationProvider.emptyUsername=\u7528\u6237\u540D\u4E0D\u5141\u8BB8\u4E3A\u7A7A
LdapAuthenticationProvider.onlySupports=\u4EC5\u4EC5\u652F\u6301UsernamePasswordAuthenticationToken
PasswordComparisonAuthenticator.badCredentials=\u574F\u7684\u51ED\u8BC1
ProviderManager.providerNotFound=\u672A\u67E5\u627E\u5230\u9488\u5BF9{0}\u7684AuthenticationProvider
RememberMeAuthenticationProvider.incorrectKey=\u5C55\u793ARememberMeAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
RunAsImplAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684RunAsUserToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
SubjectDnX509PrincipalExtractor.noMatching=\u672A\u5728subjectDN\: {0}\u4E2D\u627E\u5230\u5339\u914D\u7684\u6A21\u5F0F
SwitchUserFilter.noCurrentUser=\u4E0D\u5B58\u5728\u5F53\u524D\u7528\u6237
SwitchUserFilter.noOriginalAuthentication=\u4E0D\u80FD\u591F\u67E5\u627E\u5230\u539F\u5148\u7684\u5DF2\u8BA4\u8BC1\u5BF9\u8C61