♻️ Refactoring code. 迁移 Spring Authorization Server
This commit is contained in:
1 parent
0735bba5e2
commit
0513be8644
119 files changed
+3210
-4754
No files matched your search
@@ -1,20 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Copyright (c) 2018-2025, lengleng 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
|
||||
*
|
||||
* 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)
|
||||
* 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.pig4cloud.pigx.auth;
|
||||
@@ -26,7 +23,7 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020-02-08 认证授权中心
|
||||
* @date 2018年06月21日 认证授权中心
|
||||
*/
|
||||
@EnablePigxFeignClients
|
||||
@EnableDiscoveryClient
|
||||
|
||||
+126
-90
@@ -1,123 +1,159 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Copyright (c) 2018-2025, lengleng 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
|
||||
*
|
||||
* 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)
|
||||
* 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.pig4cloud.pigx.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxCommenceAuthExceptionEntryPoint;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxWebResponseExceptionTranslator;
|
||||
import com.pig4cloud.pigx.common.security.grant.ResourceOwnerCustomeAppTokenGranter;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxCustomTokenServices;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import com.pig4cloud.pigx.auth.support.CustomeOAuth2AccessTokenGenerator;
|
||||
import com.pig4cloud.pigx.auth.support.core.CustomeOAuth2TokenCustomizer;
|
||||
import com.pig4cloud.pigx.auth.support.core.FormIdentityLoginConfigurer;
|
||||
import com.pig4cloud.pigx.auth.support.core.PigDaoAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.auth.support.handler.PigAuthenticationFailureEventHandler;
|
||||
import com.pig4cloud.pigx.auth.support.handler.PigAuthenticationSuccessEventHandler;
|
||||
import com.pig4cloud.pigx.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationConverter;
|
||||
import com.pig4cloud.pigx.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationConverter;
|
||||
import com.pig4cloud.pigx.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.handler.FormAuthenticationFailureHandler;
|
||||
import com.pig4cloud.pigx.common.security.handler.SsoLogoutSuccessHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
|
||||
import org.springframework.security.oauth2.provider.ClientDetailsService;
|
||||
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
|
||||
import org.springframework.security.oauth2.provider.TokenGranter;
|
||||
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
|
||||
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
|
||||
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.*;
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/6/22 认证服务器配置
|
||||
* @date 2022/5/27
|
||||
*
|
||||
* 认证服务器配置
|
||||
*/
|
||||
@Configuration
|
||||
@AllArgsConstructor
|
||||
@EnableAuthorizationServer
|
||||
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
|
||||
@RequiredArgsConstructor
|
||||
public class AuthorizationServerConfiguration {
|
||||
|
||||
private final ClientDetailsService pigxClientDetailsServiceImpl;
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
private final AuthenticationManager authenticationManagerBean;
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
|
||||
PigAuthenticationSuccessEventHandler successEventHandler,
|
||||
PigAuthenticationFailureEventHandler failureEventHandler) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
|
||||
|
||||
private final AuthorizationCodeServices authorizationCodeServices;
|
||||
http.apply(authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> {// 个性化认证授权端点
|
||||
tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter
|
||||
.accessTokenResponseHandler(successEventHandler) // 登录成功处理器
|
||||
.errorResponseHandler(failureEventHandler);// 登录失败处理器
|
||||
}).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证
|
||||
oAuth2ClientAuthenticationConfigurer.errorResponseHandler(failureEventHandler))// 处理客户端认证异常
|
||||
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面
|
||||
.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI)));
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
DefaultSecurityFilterChain securityFilterChain = http.authorizeHttpRequests(authorizeRequests -> {
|
||||
// 自定义接口、端点暴露
|
||||
authorizeRequests.antMatchers("/token/**", "/actuator/**", "/css/**", "/error").permitAll();
|
||||
authorizeRequests.anyRequest().authenticated();
|
||||
}).apply(authorizationServerConfigurer.authorizationService(authorizationService)// redis存储token的实现
|
||||
.authorizationServerSettings(
|
||||
AuthorizationServerSettings.builder().issuer(SecurityConstants.PIGX_LICENSE).build()))
|
||||
// 授权码登录的登录页个性化
|
||||
.and().apply(new FormIdentityLoginConfigurer()).and().build();
|
||||
|
||||
private final TokenStore redisTokenStore;
|
||||
|
||||
private final TokenEnhancer tokenEnhancer;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void configure(ClientDetailsServiceConfigurer clients) {
|
||||
clients.withClientDetails(pigxClientDetailsServiceImpl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
|
||||
oauthServer.allowFormAuthenticationForClients()
|
||||
.authenticationEntryPoint(new PigxCommenceAuthExceptionEntryPoint(objectMapper))
|
||||
.checkTokenAccess("isAuthenticated()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
|
||||
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenServices(tokenServices())
|
||||
.tokenStore(redisTokenStore).tokenEnhancer(tokenEnhancer)
|
||||
.authorizationCodeServices(authorizationCodeServices).authenticationManager(authenticationManagerBean)
|
||||
.reuseRefreshTokens(false).pathMapping("/oauth/confirm_access", "/token/confirm_access")
|
||||
.exceptionTranslator(new PigxWebResponseExceptionTranslator());
|
||||
|
||||
// 注入自定义认证类型
|
||||
setTokenGranter(endpoints);
|
||||
// 注入自定义授权模式实现
|
||||
addCustomOAuth2GrantAuthenticationProvider(http);
|
||||
return securityFilterChain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 APP 认证类型
|
||||
* @param endpoints AuthorizationServerEndpointsConfigurer
|
||||
* 令牌生成规则实现 </br>
|
||||
* client:username:uuid
|
||||
* @return OAuth2TokenGenerator
|
||||
*/
|
||||
private void setTokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
|
||||
// 获取默认授权类型
|
||||
TokenGranter tokenGranter = endpoints.getTokenGranter();
|
||||
ArrayList<TokenGranter> tokenGranters = new ArrayList<>(Arrays.asList(tokenGranter));
|
||||
ResourceOwnerCustomeAppTokenGranter resourceOwnerCustomeAppTokenGranter = new ResourceOwnerCustomeAppTokenGranter(
|
||||
authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(),
|
||||
endpoints.getOAuth2RequestFactory());
|
||||
tokenGranters.add(resourceOwnerCustomeAppTokenGranter);
|
||||
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(tokenGranters);
|
||||
endpoints.tokenGranter(compositeTokenGranter);
|
||||
@Bean
|
||||
public OAuth2TokenGenerator oAuth2TokenGenerator() {
|
||||
CustomeOAuth2AccessTokenGenerator accessTokenGenerator = new CustomeOAuth2AccessTokenGenerator();
|
||||
// 注入Token 增加关联用户信息
|
||||
accessTokenGenerator.setAccessTokenCustomizer(new CustomeOAuth2TokenCustomizer());
|
||||
return new DelegatingOAuth2TokenGenerator(accessTokenGenerator, new OAuth2RefreshTokenGenerator());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PigxCustomTokenServices tokenServices() {
|
||||
PigxCustomTokenServices tokenServices = new PigxCustomTokenServices();
|
||||
tokenServices.setTokenStore(redisTokenStore);
|
||||
tokenServices.setSupportRefreshToken(true);
|
||||
tokenServices.setReuseRefreshToken(false);
|
||||
tokenServices.setClientDetailsService(pigxClientDetailsServiceImpl);
|
||||
tokenServices.setTokenEnhancer(tokenEnhancer);
|
||||
return tokenServices;
|
||||
public AuthenticationFailureHandler authenticationFailureHandler() {
|
||||
return new FormAuthenticationFailureHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogoutSuccessHandler logoutSuccessHandler() {
|
||||
return new SsoLogoutSuccessHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* request -> xToken 注入请求转换器
|
||||
* @return DelegatingAuthenticationConverter
|
||||
*/
|
||||
private AuthenticationConverter accessTokenRequestConverter() {
|
||||
return new DelegatingAuthenticationConverter(Arrays.asList(
|
||||
new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
|
||||
new OAuth2ResourceOwnerSmsAuthenticationConverter(), new OAuth2RefreshTokenAuthenticationConverter(),
|
||||
new OAuth2ClientCredentialsAuthenticationConverter(),
|
||||
new OAuth2AuthorizationCodeAuthenticationConverter(),
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入授权模式实现提供方
|
||||
*
|
||||
* 1. 密码模式 </br>
|
||||
* 2. 短信登录 </br>
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http) {
|
||||
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
|
||||
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
|
||||
|
||||
OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider(
|
||||
authenticationManager, authorizationService, oAuth2TokenGenerator());
|
||||
|
||||
OAuth2ResourceOwnerSmsAuthenticationProvider resourceOwnerSmsAuthenticationProvider = new OAuth2ResourceOwnerSmsAuthenticationProvider(
|
||||
authenticationManager, authorizationService, oAuth2TokenGenerator());
|
||||
|
||||
// 处理 UsernamePasswordAuthenticationToken
|
||||
http.authenticationProvider(new PigDaoAuthenticationProvider());
|
||||
// 处理 OAuth2ResourceOwnerPasswordAuthenticationToken
|
||||
http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider);
|
||||
// 处理 OAuth2ResourceOwnerSmsAuthenticationToken
|
||||
http.authenticationProvider(resourceOwnerSmsAuthenticationProvider);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.config;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxDaoAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.common.security.grant.CustomAppAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.common.security.handler.FormAuthenticationFailureHandler;
|
||||
import com.pig4cloud.pigx.common.security.handler.MobileLoginSuccessHandler;
|
||||
import com.pig4cloud.pigx.common.security.handler.SsoLogoutSuccessHandler;
|
||||
import com.pig4cloud.pigx.common.security.handler.TenantSavedRequestAwareAuthenticationSuccessHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/6/22 认证相关配置
|
||||
*/
|
||||
@Primary
|
||||
@Order(90)
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
protected void configure(HttpSecurity http) {
|
||||
http.formLogin().loginPage("/token/login").loginProcessingUrl("/token/form")
|
||||
.successHandler(tenantSavedRequestAwareAuthenticationSuccessHandler())
|
||||
.failureHandler(authenticationFailureHandler()).and().logout()
|
||||
.logoutSuccessHandler(logoutSuccessHandler()).deleteCookies("JSESSIONID").invalidateHttpSession(true)
|
||||
.and().authorizeRequests().antMatchers("/token/**", "/actuator/**", "/mobile/**").permitAll()
|
||||
.anyRequest().authenticated().and().csrf().disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 provider 列表注入
|
||||
* @param auth AuthenticationManagerBuilder
|
||||
*/
|
||||
@Override
|
||||
protected void configure(AuthenticationManagerBuilder auth) {
|
||||
PigxDaoAuthenticationProvider daoAuthenticationProvider = new PigxDaoAuthenticationProvider();
|
||||
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
|
||||
// 注入自定义异常国际化文件处理器
|
||||
MessageSource messageSource = SpringContextHolder.getBean("securityMessageSource");
|
||||
daoAuthenticationProvider.setMessageSource(messageSource);
|
||||
|
||||
// 处理默认的密码模式认证
|
||||
auth.authenticationProvider(daoAuthenticationProvider);
|
||||
// 自定义的认证模式
|
||||
CustomAppAuthenticationProvider customAppAuthenticationProvider = new CustomAppAuthenticationProvider();
|
||||
customAppAuthenticationProvider.setMessageSource(messageSource);
|
||||
auth.authenticationProvider(customAppAuthenticationProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* 不拦截静态资源
|
||||
* @param web
|
||||
*/
|
||||
@Override
|
||||
public void configure(WebSecurity web) {
|
||||
web.ignoring().antMatchers("/favicon.ico", "/css/**", "/error");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public AuthenticationManager authenticationManagerBean() {
|
||||
return super.authenticationManagerBean();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationFailureHandler authenticationFailureHandler() {
|
||||
return new FormAuthenticationFailureHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogoutSuccessHandler logoutSuccessHandler() {
|
||||
return new SsoLogoutSuccessHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationSuccessHandler mobileLoginSuccessHandler() {
|
||||
return new MobileLoginSuccessHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 具备租户传递能力
|
||||
* @return AuthenticationSuccessHandler
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationSuccessHandler tenantSavedRequestAwareAuthenticationSuccessHandler() {
|
||||
return new TenantSavedRequestAwareAuthenticationSuccessHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码处理器
|
||||
* @return 动态密码处理器 {类型}密文
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.auth.endpoint;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.TemporalAccessorUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysOauthClientDetails;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysTenant;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteClientDetailsService;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteTenantService;
|
||||
import com.pig4cloud.pigx.admin.api.vo.TokenVo;
|
||||
import com.pig4cloud.pigx.auth.support.handler.PigAuthenticationFailureEventHandler;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.core.util.RetOps;
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import com.pig4cloud.pigx.common.security.annotation.Inner;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2ErrorCodesExpand;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuthClientException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authentication.event.LogoutSuccessEvent;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
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.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019/2/1 删除token端点
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/token")
|
||||
public class PigTokenEndpoint {
|
||||
|
||||
private final PigAuthenticationFailureEventHandler authenticationFailureHandler;
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
private final RemoteClientDetailsService clientDetailsService;
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final RemoteTenantService tenantService;
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
/**
|
||||
* 认证页面
|
||||
* @param modelAndView
|
||||
* @param error 表单登录失败处理回调的错误信息
|
||||
* @return ModelAndView
|
||||
*/
|
||||
@GetMapping("/login")
|
||||
public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
|
||||
modelAndView.setViewName("ftl/login");
|
||||
modelAndView.addObject("error", error);
|
||||
|
||||
R<List<SysTenant>> tenantList = tenantService.list(SecurityConstants.FROM_IN);
|
||||
modelAndView.addObject("tenantList", tenantList.getData());
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/confirm_access")
|
||||
public ModelAndView confirm(Principal principal, ModelAndView modelAndView,
|
||||
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
|
||||
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
|
||||
@RequestParam(OAuth2ParameterNames.STATE) String state) {
|
||||
SysOauthClientDetails clientDetails = RetOps
|
||||
.of(clientDetailsService.getClientDetailsById(clientId, SecurityConstants.FROM_IN)).getData()
|
||||
.orElseThrow(() -> new OAuthClientException("clientId 不合法"));
|
||||
|
||||
Set<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
|
||||
modelAndView.addObject("clientId", clientId);
|
||||
modelAndView.addObject("state", state);
|
||||
modelAndView.addObject("scopeList", authorizedScopes);
|
||||
modelAndView.addObject("principalName", principal.getName());
|
||||
modelAndView.setViewName("ftl/confirm");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出并删除token
|
||||
* @param authHeader Authorization
|
||||
*/
|
||||
@DeleteMapping("/logout")
|
||||
public R<Boolean> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
|
||||
if (StrUtil.isBlank(authHeader)) {
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), StrUtil.EMPTY).trim();
|
||||
return removeToken(tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验token
|
||||
* @param token 令牌
|
||||
*/
|
||||
@SneakyThrows
|
||||
@GetMapping("/check_token")
|
||||
public void checkToken(String token, HttpServletResponse response, HttpServletRequest request) {
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
|
||||
if (StrUtil.isBlank(token)) {
|
||||
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
|
||||
new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING));
|
||||
return;
|
||||
}
|
||||
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
||||
|
||||
// 如果令牌不存在 返回401
|
||||
if (authorization == null || authorization.getAccessToken() == null) {
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
|
||||
new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌管理调用
|
||||
* @param token token
|
||||
*/
|
||||
@Inner
|
||||
@DeleteMapping("/{token}")
|
||||
public R<Boolean> removeToken(@PathVariable("token") String token) {
|
||||
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
||||
if (authorization == null) {
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
|
||||
if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
|
||||
return R.ok();
|
||||
}
|
||||
// 清空用户信息
|
||||
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(authorization.getPrincipalName());
|
||||
// 清空access token
|
||||
authorizationService.remove(authorization);
|
||||
// 处理自定义退出事件,保存相关日志
|
||||
SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken(
|
||||
authorization.getPrincipalName(), authorization.getRegisteredClientId())));
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询token
|
||||
* @param params 分页参数
|
||||
* @return
|
||||
*/
|
||||
@Inner
|
||||
@PostMapping("/page")
|
||||
public R<Page> tokenList(@RequestBody Map<String, Object> params) {
|
||||
// 根据分页参数获取对应数据
|
||||
String key = String.format("%s::*", CacheConstants.PROJECT_OAUTH_ACCESS);
|
||||
int current = MapUtil.getInt(params, CommonConstants.CURRENT);
|
||||
int size = MapUtil.getInt(params, CommonConstants.SIZE);
|
||||
Set<String> keys = redisTemplate.keys(key);
|
||||
List<String> pages = keys.stream().skip((current - 1) * size).limit(size).collect(Collectors.toList());
|
||||
Page result = new Page(current, size);
|
||||
|
||||
List<TokenVo> tokenVoList = redisTemplate.opsForValue().multiGet(pages).stream().map(obj -> {
|
||||
OAuth2Authorization authorization = (OAuth2Authorization) obj;
|
||||
TokenVo tokenVo = new TokenVo();
|
||||
tokenVo.setClientId(authorization.getRegisteredClientId());
|
||||
tokenVo.setId(authorization.getId());
|
||||
tokenVo.setUsername(authorization.getPrincipalName());
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
|
||||
tokenVo.setAccessToken(accessToken.getToken().getTokenValue());
|
||||
|
||||
String expiresAt = TemporalAccessorUtil.format(accessToken.getToken().getExpiresAt(),
|
||||
DatePattern.NORM_DATETIME_PATTERN);
|
||||
tokenVo.setExpiresAt(expiresAt);
|
||||
|
||||
String issuedAt = TemporalAccessorUtil.format(accessToken.getToken().getIssuedAt(),
|
||||
DatePattern.NORM_DATETIME_PATTERN);
|
||||
tokenVo.setIssuedAt(issuedAt);
|
||||
return tokenVo;
|
||||
}).collect(Collectors.toList());
|
||||
result.setRecords(tokenVoList);
|
||||
result.setTotal(keys.size());
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.endpoint;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysTenant;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteTenantService;
|
||||
import com.pig4cloud.pigx.auth.service.PigxTokenDealServiceImpl;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.security.annotation.Inner;
|
||||
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.provider.AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.provider.ClientDetails;
|
||||
import org.springframework.security.oauth2.provider.ClientDetailsService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/6/24 删除token端点
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/token")
|
||||
public class PigxTokenEndpoint {
|
||||
|
||||
private final ClientDetailsService clientDetailsService;
|
||||
|
||||
private final PigxTokenDealServiceImpl dealService;
|
||||
|
||||
private final RemoteTenantService tenantService;
|
||||
|
||||
/**
|
||||
* 认证页面
|
||||
* @param modelAndView
|
||||
* @param error 表单登录失败处理回调的错误信息
|
||||
* @return ModelAndView
|
||||
*/
|
||||
@GetMapping("/login")
|
||||
public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
|
||||
R<List<SysTenant>> tenantList = tenantService.list(SecurityConstants.FROM_IN);
|
||||
modelAndView.setViewName("ftl/login");
|
||||
modelAndView.addObject("error", error);
|
||||
modelAndView.addObject("tenantList", tenantList.getData());
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认授权页面
|
||||
* @param request
|
||||
* @param session
|
||||
* @param modelAndView
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/confirm_access")
|
||||
public ModelAndView confirm(HttpServletRequest request, HttpSession session, ModelAndView modelAndView) {
|
||||
Map<String, Object> scopeList = (Map<String, Object>) request.getAttribute("scopes");
|
||||
modelAndView.addObject("scopeList", scopeList.keySet());
|
||||
|
||||
Object auth = session.getAttribute("authorizationRequest");
|
||||
if (auth != null) {
|
||||
AuthorizationRequest authorizationRequest = (AuthorizationRequest) auth;
|
||||
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId());
|
||||
modelAndView.addObject("app", clientDetails.getAdditionalInformation());
|
||||
modelAndView.addObject("user", SecurityUtils.getUser());
|
||||
}
|
||||
|
||||
modelAndView.setViewName("ftl/confirm");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出token
|
||||
* @param authHeader Authorization
|
||||
*/
|
||||
@DeleteMapping("/logout")
|
||||
public R logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
|
||||
if (StrUtil.isBlank(authHeader)) {
|
||||
return R.ok(Boolean.FALSE, "退出失败,token 为空");
|
||||
}
|
||||
|
||||
String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, StrUtil.EMPTY).trim();
|
||||
return delToken(tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌管理调用
|
||||
* @param token token
|
||||
* @return
|
||||
*/
|
||||
@Inner
|
||||
@DeleteMapping("/{token}")
|
||||
public R<Boolean> delToken(@PathVariable("token") String token) {
|
||||
return dealService.removeToken(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询token
|
||||
* @param page 分页参数
|
||||
* @param username 用户名
|
||||
* @return
|
||||
*/
|
||||
@Inner
|
||||
@GetMapping("/page")
|
||||
public R<Page> tokenList(Page page, String username) {
|
||||
// 根据username 查询 token 列表
|
||||
if (StrUtil.isNotBlank(username)) {
|
||||
return dealService.queryTokenByUsername(page, username);
|
||||
}
|
||||
|
||||
return dealService.queryToken(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询token
|
||||
* @param token
|
||||
* @return
|
||||
*/
|
||||
@Inner
|
||||
@GetMapping("/query-token")
|
||||
public R queryToken(String token) {
|
||||
return dealService.queryTokenInfo(token);
|
||||
}
|
||||
|
||||
}
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.handler;
|
||||
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteUserService;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.data.resolver.ParamResolver;
|
||||
import com.pig4cloud.pigx.common.security.handler.AuthenticationFailureHandler;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-04-11
|
||||
*
|
||||
* 登录操作次数锁定功能
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PigxAuthenticationFailureLockEventHandler implements AuthenticationFailureHandler {
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private final KeyStrResolver tenantKeyStrResolver;
|
||||
|
||||
private final RemoteUserService userService;
|
||||
|
||||
/**
|
||||
* 密码错误超过 5 此自动锁定
|
||||
* <p>
|
||||
* @param authenticationException 登录的authentication 对象
|
||||
* @param authentication 登录的authenticationException 对象
|
||||
* @param request 请求
|
||||
* @param response 响应
|
||||
*/
|
||||
@Async
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void handle(AuthenticationException authenticationException, Authentication authentication,
|
||||
HttpServletRequest request, HttpServletResponse response) {
|
||||
// 只处理账号密码错误异常
|
||||
if (!(authenticationException instanceof BadCredentialsException)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = authentication.getName();
|
||||
String key = String.format("%s:%s:%s", CacheConstants.LOGIN_ERROR_TIMES, tenantKeyStrResolver.key(), username);
|
||||
Long deltaTimes = ParamResolver.getLong("LOGIN_ERROR_TIMES", 5L);
|
||||
Long times = redisTemplate.opsForValue().increment(key);
|
||||
|
||||
// 自动过期时间
|
||||
Long deltaTime = ParamResolver.getLong("DELTA_TIME", 1L);
|
||||
redisTemplate.expire(key, deltaTime, TimeUnit.HOURS);
|
||||
|
||||
if (deltaTimes <= times) {
|
||||
userService.lockUser(username, SecurityConstants.FROM_IN);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteLogService;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import com.pig4cloud.pigx.common.log.util.LogTypeEnum;
|
||||
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
|
||||
import com.pig4cloud.pigx.common.security.handler.AuthenticationFailureHandler;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/10/8
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PigxAuthenticationFailureLogEventHandler implements AuthenticationFailureHandler {
|
||||
|
||||
private final RemoteLogService logService;
|
||||
|
||||
private final KeyStrResolver tenantKeyStrResolver;
|
||||
|
||||
/**
|
||||
* 异步处理,登录失败方法
|
||||
* <p>
|
||||
* @param authenticationException 登录的authentication 对象
|
||||
* @param authentication 登录的authenticationException 对象
|
||||
* @param request 请求
|
||||
* @param response 响应
|
||||
*/
|
||||
@Async
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void handle(AuthenticationException authenticationException, Authentication authentication,
|
||||
HttpServletRequest request, HttpServletResponse response) {
|
||||
String username = authentication.getName();
|
||||
SysLogDTO sysLog = SysLogUtils.getSysLog(request, username);
|
||||
|
||||
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
|
||||
if (StrUtil.isNotBlank(startTimeStr)) {
|
||||
Long startTime = Long.parseLong(startTimeStr);
|
||||
Long endTime = System.currentTimeMillis();
|
||||
sysLog.setTime(endTime - startTime);
|
||||
}
|
||||
sysLog.setTitle(username + "用户登录");
|
||||
sysLog.setLogType(LogTypeEnum.ERROR.getType());
|
||||
sysLog.setParams(username);
|
||||
sysLog.setException(authenticationException.getLocalizedMessage());
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
sysLog.setServiceId(WebUtils.extractClientId(header).orElse("N/A"));
|
||||
sysLog.setTenantId(Long.parseLong(tenantKeyStrResolver.key()));
|
||||
|
||||
logService.saveLog(sysLog, SecurityConstants.FROM_IN);
|
||||
|
||||
log.info("用户:{} 登录失败,异常:{}", username, authenticationException.getLocalizedMessage());
|
||||
}
|
||||
|
||||
}
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteLogService;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
|
||||
import com.pig4cloud.pigx.common.security.handler.AuthenticationLogoutHandler;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 退出事件处理
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2021/06/22
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PigxAuthenticationLogoutEventHandler implements AuthenticationLogoutHandler {
|
||||
|
||||
private final RemoteLogService logService;
|
||||
|
||||
/**
|
||||
* 处理登录成功方法
|
||||
* <p>
|
||||
* 获取到登录的authentication 对象
|
||||
* @param authentication 登录对象
|
||||
* @param request 请求
|
||||
* @param response 返回
|
||||
*/
|
||||
@Async
|
||||
@Override
|
||||
public void handle(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
|
||||
|
||||
PigxUser pigxUser = (PigxUser) authentication.getPrincipal();
|
||||
SysLogDTO sysLog = SysLogUtils.getSysLog(request, pigxUser.getUsername());
|
||||
sysLog.setTitle(pigxUser.getUsername() + "用户退出登录");
|
||||
sysLog.setParams(pigxUser.getUsername());
|
||||
|
||||
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
|
||||
if (StrUtil.isNotBlank(startTimeStr)) {
|
||||
Long startTime = Long.parseLong(startTimeStr);
|
||||
Long endTime = System.currentTimeMillis();
|
||||
sysLog.setTime(endTime - startTime);
|
||||
}
|
||||
|
||||
// 获取clientId 信息
|
||||
OAuth2Authentication auth2Authentication = (OAuth2Authentication) authentication;
|
||||
sysLog.setServiceId(auth2Authentication.getOAuth2Request().getClientId());
|
||||
sysLog.setTenantId(pigxUser.getTenantId());
|
||||
// 保存退出的token
|
||||
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
sysLog.setParams(token);
|
||||
|
||||
logService.saveLog(sysLog, SecurityConstants.FROM_IN);
|
||||
log.info("用户:{} 退出成功, token:{} 已注销", pigxUser.getTenantId(), token);
|
||||
}
|
||||
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.handler;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.security.handler.AuthenticationSuccessHandler;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-04-11
|
||||
*
|
||||
* 登录操作次数锁定清楚功能
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PigxAuthenticationSuccessLockEventHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private final KeyStrResolver tenantKeyStrResolver;
|
||||
|
||||
/**
|
||||
* 处理登录成功方法
|
||||
* <p>
|
||||
* 获取到登录的authentication 对象
|
||||
* @param authentication 登录对象
|
||||
* @param request 请求
|
||||
* @param response 返回
|
||||
*/
|
||||
@Async
|
||||
@Override
|
||||
public void handle(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
|
||||
String username = authentication.getName();
|
||||
String key = String.format("%s:%s:%s", CacheConstants.LOGIN_ERROR_TIMES, tenantKeyStrResolver.key(), username);
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
}
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteLogService;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
|
||||
import com.pig4cloud.pigx.common.security.handler.AuthenticationSuccessHandler;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-04-11
|
||||
*
|
||||
* 登录成功日志记录
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PigxAuthenticationSuccessLogEventHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
private final RemoteLogService logService;
|
||||
|
||||
private final KeyStrResolver tenantKeyStrResolver;
|
||||
|
||||
/**
|
||||
* 处理登录成功方法
|
||||
* <p>
|
||||
* 获取到登录的authentication 对象
|
||||
* @param authentication 登录对象
|
||||
* @param request 请求
|
||||
* @param response 返回
|
||||
*/
|
||||
@Async
|
||||
@Override
|
||||
public void handle(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
|
||||
String username = authentication.getName();
|
||||
SysLogDTO sysLog = SysLogUtils.getSysLog(request, username);
|
||||
sysLog.setTitle(username + "用户登录");
|
||||
sysLog.setParams(username);
|
||||
|
||||
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
|
||||
if (StrUtil.isNotBlank(startTimeStr)) {
|
||||
Long startTime = Long.parseLong(startTimeStr);
|
||||
Long endTime = System.currentTimeMillis();
|
||||
sysLog.setTime(endTime - startTime);
|
||||
}
|
||||
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
sysLog.setServiceId(WebUtils.extractClientId(header).orElse("N/A"));
|
||||
sysLog.setTenantId(Long.parseLong(tenantKeyStrResolver.key()));
|
||||
|
||||
logService.saveLog(sysLog, SecurityConstants.FROM_IN);
|
||||
log.info("用户:{} 登录成功", username);
|
||||
}
|
||||
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
package com.pig4cloud.pigx.auth.service;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import lombok.Cleanup;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisStringCommands;
|
||||
import org.springframework.data.redis.core.types.Expiration;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
|
||||
import org.springframework.security.oauth2.provider.token.store.redis.JdkSerializationStrategy;
|
||||
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/9/3
|
||||
* <p>
|
||||
* 授权码模式支持集群
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PigxAuthorizationCodeServicesImpl extends RandomValueAuthorizationCodeServices {
|
||||
|
||||
/**
|
||||
* 授权码模式时: 验证码有效期设置, 默认有效期为5分钟, 单位秒
|
||||
*/
|
||||
@Value("${pigx.authorizationCode.expirationTime:300}")
|
||||
private Long expirationTime;
|
||||
|
||||
private final RedisConnectionFactory connectionFactory;
|
||||
|
||||
@Setter
|
||||
private String prefix = SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_CODE_PREFIX;
|
||||
|
||||
@Setter
|
||||
private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
|
||||
|
||||
/**
|
||||
* 保存 code 和 认证信息
|
||||
* @param code 授权码模式: code
|
||||
* @param authentication 认证信息
|
||||
*/
|
||||
@Override
|
||||
protected void store(String code, OAuth2Authentication authentication) {
|
||||
@Cleanup
|
||||
RedisConnection connection = connectionFactory.getConnection();
|
||||
connection.set(serializationStrategy.serialize(prefix + code), serializationStrategy.serialize(authentication),
|
||||
Expiration.seconds(expirationTime), RedisStringCommands.SetOption.UPSERT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除code 并返回认证信息
|
||||
* @param code 授权码模式: code
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected OAuth2Authentication remove(String code) {
|
||||
@Cleanup
|
||||
RedisConnection connection = connectionFactory.getConnection();
|
||||
|
||||
byte[] key = serializationStrategy.serialize(prefix + code);
|
||||
byte[] value = connection.get(key);
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
OAuth2Authentication oAuth2Authentication = serializationStrategy.deserialize(value,
|
||||
OAuth2Authentication.class);
|
||||
|
||||
connection.del(key);
|
||||
return oAuth2Authentication;
|
||||
}
|
||||
|
||||
}
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
package com.pig4cloud.pigx.auth.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysOauthClientDetails;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteClientDetailsService;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.core.util.RetOps;
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.data.redis.core.ConvertingCursor;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.security.authentication.event.LogoutSuccessEvent;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/9/3
|
||||
* <p>
|
||||
* token 端点相关的业务处理
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PigxTokenDealServiceImpl {
|
||||
|
||||
private final RedisTemplate redisTemplate;
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
private final TokenStore tokenStore;
|
||||
|
||||
private final KeyStrResolver keyStrResolver;
|
||||
|
||||
private final RemoteClientDetailsService clientDetailsService;
|
||||
|
||||
/**
|
||||
* 删除 请求令牌 和 刷新令牌
|
||||
* @param token 请求令牌
|
||||
* @return R
|
||||
*/
|
||||
public R<Boolean> removeToken(String token) {
|
||||
|
||||
OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
|
||||
if (accessToken == null || StrUtil.isBlank(accessToken.getValue())) {
|
||||
return R.ok(Boolean.TRUE, "退出失败,token 无效");
|
||||
}
|
||||
|
||||
OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);
|
||||
// 清空用户信息
|
||||
cacheManager.getCache(CacheConstants.USER_DETAILS).evict(auth2Authentication.getName());
|
||||
|
||||
// 清空access token
|
||||
tokenStore.removeAccessToken(accessToken);
|
||||
|
||||
// 清空 refresh token
|
||||
OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
|
||||
tokenStore.removeRefreshToken(refreshToken);
|
||||
|
||||
// 发送退出成功事件
|
||||
SpringContextHolder.publishEvent(new LogoutSuccessEvent(auth2Authentication));
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查询 令牌相关信息
|
||||
* @param page 分页信息
|
||||
* @param username 用户名
|
||||
* @return R
|
||||
*/
|
||||
public R queryTokenByUsername(Page page, String username) {
|
||||
// @formatter:off
|
||||
List<OAuth2AccessToken> oAuth2AccessTokenList =
|
||||
RetOps.of(clientDetailsService.listClientDetails(SecurityConstants.FROM_IN))
|
||||
.getData()
|
||||
.orElseGet(Collections::emptyList)
|
||||
.stream().map(SysOauthClientDetails::getClientId)
|
||||
.flatMap(clientId -> tokenStore.findTokensByClientIdAndUserName(clientId, username).stream()).distinct()
|
||||
.collect(Collectors.toList());
|
||||
// @formatter:on
|
||||
|
||||
page.setRecords(oAuth2AccessTokenList);
|
||||
page.setTotal(oAuth2AccessTokenList.size());
|
||||
return R.ok(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询token 列表
|
||||
* @param page page
|
||||
* @return
|
||||
*/
|
||||
public R<Page> queryToken(Page page) {
|
||||
// 根据分页参数获取对应数据
|
||||
String key = keyStrResolver.extract(SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX + "access:*",
|
||||
StrUtil.COLON);
|
||||
List<String> pages = findKeysForPage(key, page.getCurrent(), page.getSize());
|
||||
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
|
||||
page.setRecords(redisTemplate.opsForValue().multiGet(pages));
|
||||
page.setTotal(redisTemplate.keys(key).size());
|
||||
return R.ok(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用游标 分页查询key
|
||||
* @param patternKey key 通配符
|
||||
* @param pageNum 当前页
|
||||
* @param pageSize 每页大小
|
||||
* @return 符合的 value列表
|
||||
*/
|
||||
private List<String> findKeysForPage(String patternKey, long pageNum, long pageSize) {
|
||||
ScanOptions options = ScanOptions.scanOptions().count(1000L).match(patternKey).build();
|
||||
RedisSerializer<String> redisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
|
||||
Cursor cursor = (Cursor) redisTemplate.executeWithStickyConnection(
|
||||
redisConnection -> new ConvertingCursor<>(redisConnection.scan(options), redisSerializer::deserialize));
|
||||
List<String> result = new ArrayList<>();
|
||||
int tmpIndex = 0;
|
||||
long startIndex = (pageNum - 1) * pageSize;
|
||||
long end = pageNum * pageSize;
|
||||
|
||||
assert cursor != null;
|
||||
while (cursor.hasNext()) {
|
||||
if (tmpIndex >= startIndex && tmpIndex < end) {
|
||||
result.add(cursor.next().toString());
|
||||
tmpIndex++;
|
||||
continue;
|
||||
}
|
||||
if (tmpIndex >= end) {
|
||||
break;
|
||||
}
|
||||
tmpIndex++;
|
||||
cursor.next();
|
||||
}
|
||||
|
||||
try {
|
||||
cursor.close();
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("关闭cursor 失败");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public R queryTokenInfo(String token) {
|
||||
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);
|
||||
return R.ok(oAuth2AccessToken);
|
||||
}
|
||||
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package com.pig4cloud.pigx.auth.support;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.server.authorization.token.*;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022/5/29
|
||||
*/
|
||||
public class CustomeOAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {
|
||||
|
||||
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2AccessToken generate(OAuth2TokenContext context) {
|
||||
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) || !OAuth2TokenFormat.REFERENCE
|
||||
.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String issuer = null;
|
||||
if (context.getAuthorizationServerContext() != null) {
|
||||
issuer = context.getAuthorizationServerContext().getIssuer();
|
||||
}
|
||||
RegisteredClient registeredClient = context.getRegisteredClient();
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
|
||||
|
||||
// @formatter:off
|
||||
OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
|
||||
if (StringUtils.hasText(issuer)) {
|
||||
claimsBuilder.issuer(issuer);
|
||||
}
|
||||
claimsBuilder
|
||||
.subject(context.getPrincipal().getName())
|
||||
.audience(Collections.singletonList(registeredClient.getClientId()))
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.notBefore(issuedAt)
|
||||
.id(UUID.randomUUID().toString());
|
||||
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
|
||||
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
if (this.accessTokenCustomizer != null) {
|
||||
// @formatter:off
|
||||
OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
|
||||
.registeredClient(context.getRegisteredClient())
|
||||
.principal(context.getPrincipal())
|
||||
.authorizationServerContext(context.getAuthorizationServerContext())
|
||||
.authorizedScopes(context.getAuthorizedScopes())
|
||||
.tokenType(context.getTokenType())
|
||||
.authorizationGrantType(context.getAuthorizationGrantType());
|
||||
if (context.getAuthorization() != null) {
|
||||
accessTokenContextBuilder.authorization(context.getAuthorization());
|
||||
}
|
||||
if (context.getAuthorizationGrant() != null) {
|
||||
accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
|
||||
this.accessTokenCustomizer.customize(accessTokenContext);
|
||||
}
|
||||
|
||||
OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
|
||||
return new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER, UUID.randomUUID().toString(),
|
||||
accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(), context.getAuthorizedScopes(),
|
||||
accessTokenClaimsSet.getClaims());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenCustomizer} that customizes the
|
||||
* {@link OAuth2TokenClaimsContext#getClaims() claims} for the
|
||||
* {@link OAuth2AccessToken}.
|
||||
* @param accessTokenCustomizer the {@link OAuth2TokenCustomizer} that customizes the
|
||||
* claims for the {@code OAuth2AccessToken}
|
||||
*/
|
||||
public void setAccessTokenCustomizer(OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer) {
|
||||
Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
|
||||
this.accessTokenCustomizer = accessTokenCustomizer;
|
||||
}
|
||||
|
||||
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
|
||||
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
|
||||
Set<String> scopes, Map<String, Object> claims) {
|
||||
super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.pig4cloud.pigx.auth.support.base;
|
||||
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2EndpointUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author jumuning
|
||||
* @date 2022-06-02
|
||||
*
|
||||
* 自定义模式认证转换器
|
||||
*/
|
||||
public abstract class OAuth2ResourceOwnerBaseAuthenticationConverter<T extends OAuth2ResourceOwnerBaseAuthenticationToken>
|
||||
implements AuthenticationConverter {
|
||||
|
||||
/**
|
||||
* 是否支持此convert
|
||||
* @param grantType 授权类型
|
||||
* @return
|
||||
*/
|
||||
public abstract boolean support(String grantType);
|
||||
|
||||
/**
|
||||
* 校验参数
|
||||
* @param request 请求
|
||||
*/
|
||||
public void checkParams(HttpServletRequest request) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建具体类型的token
|
||||
* @param clientPrincipal
|
||||
* @param requestedScopes
|
||||
* @param additionalParameters
|
||||
* @return
|
||||
*/
|
||||
public abstract T buildToken(Authentication clientPrincipal, Set<String> requestedScopes,
|
||||
Map<String, Object> additionalParameters);
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
||||
// grant_type (REQUIRED)
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (!support(grantType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
// scope (OPTIONAL)
|
||||
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
||||
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
|
||||
Set<String> requestedScopes = null;
|
||||
if (StringUtils.hasText(scope)) {
|
||||
requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
||||
}
|
||||
|
||||
// 校验个性化参数
|
||||
checkParams(request);
|
||||
|
||||
// 获取当前已经认证的客户端信息
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (clientPrincipal == null) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
|
||||
// 扩展信息
|
||||
Map<String, Object> additionalParameters = parameters.entrySet().stream()
|
||||
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE)
|
||||
&& !e.getKey().equals(OAuth2ParameterNames.SCOPE))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
||||
|
||||
// 创建token
|
||||
return buildToken(clientPrincipal, requestedScopes, additionalParameters);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
package com.pig4cloud.pigx.auth.support.base;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2ErrorCodesExpand;
|
||||
import com.pig4cloud.pigx.common.security.util.ScopeException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.security.authentication.*;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.oauth2.core.*;
|
||||
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.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* @author jumuning
|
||||
* @description
|
||||
*
|
||||
* 处理自定义授权
|
||||
*/
|
||||
public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider<T extends OAuth2ResourceOwnerBaseAuthenticationToken>
|
||||
implements AuthenticationProvider {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerBaseAuthenticationProvider.class);
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
private final MessageSourceAccessor messages;
|
||||
|
||||
@Deprecated
|
||||
private Supplier<String> refreshTokenGenerator;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the
|
||||
* provided parameters.
|
||||
* @param authorizationService the authorization service
|
||||
* @param tokenGenerator the token generator
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2ResourceOwnerBaseAuthenticationProvider(AuthenticationManager authenticationManager,
|
||||
OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
|
||||
// 国际化配置
|
||||
this.messages = new MessageSourceAccessor(SpringUtil.getBean("securityMessageSource"), Locale.CHINA);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setRefreshTokenGenerator(Supplier<String> refreshTokenGenerator) {
|
||||
Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
|
||||
this.refreshTokenGenerator = refreshTokenGenerator;
|
||||
}
|
||||
|
||||
public abstract UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters);
|
||||
|
||||
/**
|
||||
* 当前provider是否支持此令牌类型
|
||||
* @param authentication
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public abstract boolean supports(Class<?> authentication);
|
||||
|
||||
/**
|
||||
* 当前的请求客户端是否支持此模式
|
||||
* @param registeredClient
|
||||
*/
|
||||
public abstract void checkClient(RegisteredClient registeredClient);
|
||||
|
||||
/**
|
||||
* Performs authentication with the same contract as
|
||||
* {@link AuthenticationManager#authenticate(Authentication)} .
|
||||
* @param authentication the authentication request object.
|
||||
* @return a fully authenticated object including credentials. May return
|
||||
* <code>null</code> if the <code>AuthenticationProvider</code> is unable to support
|
||||
* authentication of the passed <code>Authentication</code> object. In such a case,
|
||||
* the next <code>AuthenticationProvider</code> that supports the presented
|
||||
* <code>Authentication</code> class will be tried.
|
||||
* @throws AuthenticationException if authentication fails.
|
||||
*/
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
|
||||
T resouceOwnerBaseAuthentication = (T) authentication;
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(
|
||||
resouceOwnerBaseAuthentication);
|
||||
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
checkClient(registeredClient);
|
||||
|
||||
Set<String> authorizedScopes;
|
||||
// Default to configured scopes
|
||||
if (!CollectionUtils.isEmpty(resouceOwnerBaseAuthentication.getScopes())) {
|
||||
for (String requestedScope : resouceOwnerBaseAuthentication.getScopes()) {
|
||||
if (!registeredClient.getScopes().contains(requestedScope)) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
|
||||
}
|
||||
}
|
||||
authorizedScopes = new LinkedHashSet<>(resouceOwnerBaseAuthentication.getScopes());
|
||||
}
|
||||
else {
|
||||
throw new ScopeException(OAuth2ErrorCodesExpand.SCOPE_IS_EMPTY);
|
||||
}
|
||||
|
||||
Map<String, Object> reqParameters = resouceOwnerBaseAuthentication.getAdditionalParameters();
|
||||
try {
|
||||
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);
|
||||
|
||||
LOGGER.debug("got usernamePasswordAuthenticationToken=" + usernamePasswordAuthenticationToken);
|
||||
|
||||
Authentication usernamePasswordAuthentication = authenticationManager
|
||||
.authenticate(usernamePasswordAuthenticationToken);
|
||||
|
||||
// @formatter:off
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(usernamePasswordAuthentication)
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
.authorizationGrant(resouceOwnerBaseAuthentication);
|
||||
// @formatter:on
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
|
||||
.withRegisteredClient(registeredClient).principalName(usernamePasswordAuthentication.getName())
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
// 0.4.0 新增的方法
|
||||
.authorizedScopes(authorizedScopes);
|
||||
|
||||
// ----- Access token -----
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the access token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.id(accessToken.getTokenValue())
|
||||
.token(accessToken,
|
||||
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
|
||||
((ClaimAccessor) generatedAccessToken).getClaims()))
|
||||
// 0.4.0 新增的方法
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
|
||||
}
|
||||
else {
|
||||
authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken);
|
||||
}
|
||||
|
||||
// ----- Refresh token -----
|
||||
OAuth2RefreshToken refreshToken = null;
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||
// Do not issue refresh token to public client
|
||||
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||
|
||||
if (this.refreshTokenGenerator != null) {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());
|
||||
refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
|
||||
}
|
||||
else {
|
||||
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
|
||||
"The token generator failed to generate the refresh token.", ERROR_URI);
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
}
|
||||
authorizationBuilder.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken");
|
||||
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken,
|
||||
refreshToken, Objects.requireNonNull(authorization.getAccessToken().getClaims()));
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw oAuth2AuthenticationException(authentication, (AuthenticationException) ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录异常转换为oauth2异常
|
||||
* @param authentication 身份验证
|
||||
* @param authenticationException 身份验证异常
|
||||
* @return {@link OAuth2AuthenticationException}
|
||||
*/
|
||||
private OAuth2AuthenticationException oAuth2AuthenticationException(Authentication authentication,
|
||||
AuthenticationException authenticationException) {
|
||||
if (authenticationException instanceof UsernameNotFoundException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USERNAME_NOT_FOUND,
|
||||
this.messages.getMessage("JdbcDaoImpl.notFound", new Object[] { authentication.getName() },
|
||||
"Username {0} not found"),
|
||||
""));
|
||||
}
|
||||
if (authenticationException instanceof BadCredentialsException) {
|
||||
return new OAuth2AuthenticationException(
|
||||
new OAuth2Error(OAuth2ErrorCodesExpand.BAD_CREDENTIALS, this.messages.getMessage(
|
||||
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), ""));
|
||||
}
|
||||
if (authenticationException instanceof LockedException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_LOCKED, this.messages
|
||||
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"), ""));
|
||||
}
|
||||
if (authenticationException instanceof DisabledException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_DISABLE,
|
||||
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"),
|
||||
""));
|
||||
}
|
||||
if (authenticationException instanceof AccountExpiredException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_EXPIRED, this.messages
|
||||
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"), ""));
|
||||
}
|
||||
if (authenticationException instanceof CredentialsExpiredException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.CREDENTIALS_EXPIRED,
|
||||
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
|
||||
"User credentials have expired"),
|
||||
""));
|
||||
}
|
||||
if (authenticationException instanceof ScopeException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
|
||||
this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "invalid_scope"), ""));
|
||||
}
|
||||
return new OAuth2AuthenticationException(OAuth2ErrorCodesExpand.UN_KNOW_LOGIN_ERROR);
|
||||
}
|
||||
|
||||
private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(
|
||||
Authentication authentication) {
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||
}
|
||||
|
||||
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||
return clientPrincipal;
|
||||
}
|
||||
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.pig4cloud.pigx.auth.support.base;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022/6/2
|
||||
*
|
||||
* 自定义授权模式抽象
|
||||
*/
|
||||
public abstract class OAuth2ResourceOwnerBaseAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
@Getter
|
||||
private final AuthorizationGrantType authorizationGrantType;
|
||||
|
||||
@Getter
|
||||
private final Authentication clientPrincipal;
|
||||
|
||||
@Getter
|
||||
private final Set<String> scopes;
|
||||
|
||||
@Getter
|
||||
private final Map<String, Object> additionalParameters;
|
||||
|
||||
public OAuth2ResourceOwnerBaseAuthenticationToken(AuthorizationGrantType authorizationGrantType,
|
||||
Authentication clientPrincipal, @Nullable Set<String> scopes,
|
||||
@Nullable Map<String, Object> additionalParameters) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
this.authorizationGrantType = authorizationGrantType;
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
|
||||
this.additionalParameters = Collections.unmodifiableMap(
|
||||
additionalParameters != null ? new HashMap<>(additionalParameters) : Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展模式一般不需要密码
|
||||
*/
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户名
|
||||
*/
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.clientPrincipal;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 自定义认证模式接入的抽象实现
|
||||
*/
|
||||
package com.pig4cloud.pigx.auth.support.base;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.pig4cloud.pigx.auth.support.core;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
||||
|
||||
/**
|
||||
* token 输出增强
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2022/6/3
|
||||
*/
|
||||
public class CustomeOAuth2TokenCustomizer implements OAuth2TokenCustomizer<OAuth2TokenClaimsContext> {
|
||||
|
||||
/**
|
||||
* Customize the OAuth 2.0 Token attributes.
|
||||
* @param context the context containing the OAuth 2.0 Token attributes
|
||||
*/
|
||||
@Override
|
||||
public void customize(OAuth2TokenClaimsContext context) {
|
||||
OAuth2TokenClaimsSet.Builder claims = context.getClaims();
|
||||
claims.claim(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PIGX_LICENSE);
|
||||
String clientId = context.getAuthorizationGrant().getName();
|
||||
claims.claim(SecurityConstants.CLIENT_ID, clientId);
|
||||
claims.claim(SecurityConstants.ACTIVE, Boolean.TRUE);
|
||||
|
||||
// 客户端模式不返回具体用户信息
|
||||
if (SecurityConstants.CLIENT_CREDENTIALS.equals(context.getAuthorizationGrantType().getValue())) {
|
||||
return;
|
||||
}
|
||||
|
||||
PigxUser pigxUser = (PigxUser) context.getPrincipal().getPrincipal();
|
||||
claims.claim(SecurityConstants.DETAILS_USER_ID, pigxUser.getId());
|
||||
claims.claim(SecurityConstants.DETAILS_USERNAME, pigxUser.getUsername());
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.pig4cloud.pigx.auth.support.core;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.handler.FormAuthenticationFailureHandler;
|
||||
import com.pig4cloud.pigx.auth.support.handler.SsoLogoutSuccessHandler;
|
||||
import com.pig4cloud.pigx.auth.support.handler.TenantSavedRequestAwareAuthenticationSuccessHandler;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @data 2022-06-04
|
||||
*
|
||||
* 基于授权码模式 统一认证登录 spring security & sas 都可以使用 所以抽取成 HttpConfigurer
|
||||
*/
|
||||
public final class FormIdentityLoginConfigurer
|
||||
extends AbstractHttpConfigurer<FormIdentityLoginConfigurer, HttpSecurity> {
|
||||
|
||||
@Override
|
||||
public void init(HttpSecurity http) throws Exception {
|
||||
http.formLogin(formLogin -> {
|
||||
formLogin.loginPage("/token/login");
|
||||
formLogin.loginProcessingUrl("/token/form");
|
||||
formLogin.failureHandler(new FormAuthenticationFailureHandler());
|
||||
formLogin.successHandler(new TenantSavedRequestAwareAuthenticationSuccessHandler());
|
||||
|
||||
}).logout() // SSO登出成功处理
|
||||
.logoutSuccessHandler(new SsoLogoutSuccessHandler()).deleteCookies("JSESSIONID")
|
||||
.invalidateHttpSession(true).and().csrf().disable();
|
||||
}
|
||||
|
||||
}
|
||||
+31
-20
@@ -1,8 +1,11 @@
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
package com.pig4cloud.pigx.auth.support.core;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
@@ -10,13 +13,14 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Comparator;
|
||||
@@ -25,9 +29,9 @@ import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022/1/15
|
||||
* @date 2022-06-04
|
||||
*/
|
||||
public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
|
||||
public class PigDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
|
||||
|
||||
/**
|
||||
* The plaintext password used to perform PasswordEncoder#matches(CharSequence,
|
||||
@@ -35,6 +39,8 @@ public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthentica
|
||||
*/
|
||||
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
|
||||
|
||||
private final static BasicAuthenticationConverter basicConvert = new BasicAuthenticationConverter();
|
||||
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
/**
|
||||
@@ -49,7 +55,8 @@ public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthentica
|
||||
|
||||
private UserDetailsPasswordService userDetailsPasswordService;
|
||||
|
||||
public PigxDaoAuthenticationProvider() {
|
||||
public PigDaoAuthenticationProvider() {
|
||||
setMessageSource(SpringUtil.getBean("securityMessageSource"));
|
||||
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
|
||||
}
|
||||
|
||||
@@ -57,6 +64,13 @@ public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthentica
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void additionalAuthenticationChecks(UserDetails userDetails,
|
||||
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
|
||||
|
||||
// app 模式不用校验密码
|
||||
String grantType = WebUtils.getRequest().getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (StrUtil.equals(SecurityConstants.APP, grantType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (authentication.getCredentials() == null) {
|
||||
this.logger.debug("Failed to authenticate since no credentials provided");
|
||||
throw new BadCredentialsException(this.messages
|
||||
@@ -70,27 +84,24 @@ public class PigxDaoAuthenticationProvider extends AbstractUserDetailsAuthentica
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
|
||||
throws AuthenticationException {
|
||||
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {
|
||||
prepareTimingAttackProtection();
|
||||
String grantType = WebUtils.getRequest().getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
String clientId = WebUtils.getRequest().getParameter(OAuth2ParameterNames.CLIENT_ID);
|
||||
|
||||
// 此处已获得 客户端认证 获取对应 userDetailsService
|
||||
Authentication clientAuthentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
// SSO NPE 处理
|
||||
String clientId;
|
||||
if (clientAuthentication == null) {
|
||||
clientId = WebUtils.getRequest().getParameter("clientId");
|
||||
}
|
||||
else {
|
||||
clientId = clientAuthentication.getName();
|
||||
if (StrUtil.isBlank(clientId)) {
|
||||
clientId = basicConvert.convert(WebUtils.getRequest()).getName();
|
||||
}
|
||||
|
||||
Map<String, PigxUserDetailsService> userDetailsServiceMap = SpringContextHolder
|
||||
Map<String, PigxUserDetailsService> userDetailsServiceMap = SpringUtil
|
||||
.getBeansOfType(PigxUserDetailsService.class);
|
||||
|
||||
String finalClientId = clientId;
|
||||
Optional<PigxUserDetailsService> optional = userDetailsServiceMap.values().stream()
|
||||
.filter(service -> service.support(clientId, null)).max(Comparator.comparingInt(Ordered::getOrder));
|
||||
.filter(service -> service.support(finalClientId, grantType))
|
||||
.max(Comparator.comparingInt(Ordered::getOrder));
|
||||
|
||||
if (!optional.isPresent()) {
|
||||
throw new InternalAuthenticationServiceException("UserDetailsService error , not register");
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.pig4cloud.pigx.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 javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-02
|
||||
* <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);
|
||||
}
|
||||
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteUserService;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.core.util.MsgUtils;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.data.resolver.ParamResolver;
|
||||
import com.pig4cloud.pigx.common.log.event.SysLogEvent;
|
||||
import com.pig4cloud.pigx.common.log.util.LogTypeEnum;
|
||||
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-02
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PigAuthenticationFailureEventHandler implements AuthenticationFailureHandler {
|
||||
|
||||
private static final MappingJackson2HttpMessageConverter errorHttpResponseConverter = new MappingJackson2HttpMessageConverter();
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
private final KeyStrResolver tenantKeyStrResolver;
|
||||
|
||||
private final RemoteUserService userService;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* request.
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) {
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
log.info("登录失败,异常:{}", exception.getLocalizedMessage());
|
||||
|
||||
// 密码模式记录错误信息
|
||||
if (OAuth2ParameterNames.PASSWORD.equals(grantType)) {
|
||||
String username = request.getParameter(OAuth2ParameterNames.USERNAME);
|
||||
|
||||
// 密码错误记录错误次数
|
||||
if (exception instanceof OAuth2AuthenticationException) {
|
||||
recordLoginFailureTimes(username);
|
||||
}
|
||||
|
||||
// 记录登录失败错误信息
|
||||
sendFailureEventLog(request, exception, username);
|
||||
}
|
||||
|
||||
// 写出错误信息
|
||||
sendErrorResponse(request, response, exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录失败日志
|
||||
* @param request HttpServletRequest
|
||||
* @param exception 错误日志
|
||||
* @param username 用户名
|
||||
*/
|
||||
private void sendFailureEventLog(HttpServletRequest request, AuthenticationException exception, String username) {
|
||||
SysLogDTO logVo = SysLogUtils.getSysLog();
|
||||
logVo.setTitle("登录失败");
|
||||
logVo.setLogType(LogTypeEnum.ERROR.getType());
|
||||
logVo.setException(exception.getLocalizedMessage());
|
||||
// 发送异步日志事件
|
||||
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
|
||||
if (StrUtil.isNotBlank(startTimeStr)) {
|
||||
Long startTime = Long.parseLong(startTimeStr);
|
||||
Long endTime = System.currentTimeMillis();
|
||||
logVo.setTime(endTime - startTime);
|
||||
}
|
||||
logVo.setCreateBy(username);
|
||||
logVo.setTenantId(Long.parseLong(tenantKeyStrResolver.key()));
|
||||
publisher.publishEvent(new SysLogEvent(logVo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录失败此处,如果操作阈值 调用接口锁定用户
|
||||
* @param username username
|
||||
*/
|
||||
private void recordLoginFailureTimes(String username) {
|
||||
String key = String.format("%s:%s:%s", CacheConstants.LOGIN_ERROR_TIMES, tenantKeyStrResolver.key(), username);
|
||||
Long deltaTimes = ParamResolver.getLong("LOGIN_ERROR_TIMES", 5L);
|
||||
Long times = redisTemplate.opsForValue().increment(key);
|
||||
|
||||
// 自动过期时间
|
||||
Long deltaTime = ParamResolver.getLong("DELTA_TIME", 1L);
|
||||
redisTemplate.expire(key, deltaTime, TimeUnit.HOURS);
|
||||
|
||||
if (deltaTimes <= times) {
|
||||
userService.lockUser(username, SecurityConstants.FROM_IN);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) throws IOException {
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
String errorMessage;
|
||||
|
||||
if (exception instanceof OAuth2AuthenticationException) {
|
||||
OAuth2AuthenticationException authorizationException = (OAuth2AuthenticationException) exception;
|
||||
errorMessage = StrUtil.isBlank(authorizationException.getError().getDescription())
|
||||
? authorizationException.getError().getErrorCode()
|
||||
: authorizationException.getError().getDescription();
|
||||
}
|
||||
else {
|
||||
errorMessage = exception.getLocalizedMessage();
|
||||
}
|
||||
|
||||
// 手机号登录
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (SecurityConstants.APP.equals(grantType)) {
|
||||
errorMessage = MsgUtils.getSecurityMessage("AbstractUserDetailsAuthenticationProvider.smsBadCredentials");
|
||||
}
|
||||
|
||||
this.errorHttpResponseConverter.write(R.failed(errorMessage), MediaType.APPLICATION_JSON, httpResponse);
|
||||
}
|
||||
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.log.event.SysLogEvent;
|
||||
import com.pig4cloud.pigx.common.log.util.LogTypeEnum;
|
||||
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-02
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PigAuthenticationSuccessEventHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
private final KeyStrResolver tenantKeyStrResolver;
|
||||
|
||||
/**
|
||||
* Called when a user has been successfully authenticated.
|
||||
* @param request the request which caused the successful authentication
|
||||
* @param response the response
|
||||
* @param authentication the <tt>Authentication</tt> object which was created during
|
||||
* the authentication process.
|
||||
*/
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) {
|
||||
|
||||
// 清除账号历史锁定次数
|
||||
clearLoginFailureTimes(authentication);
|
||||
|
||||
// 写入登录成功的日志
|
||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
|
||||
Map<String, Object> map = accessTokenAuthentication.getAdditionalParameters();
|
||||
if (MapUtil.isNotEmpty(map)) {
|
||||
sendSuccessEventLog(request, accessTokenAuthentication, map);
|
||||
}
|
||||
|
||||
// 输出token
|
||||
sendAccessTokenResponse(response, authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录成功事件
|
||||
* @param request HttpServletRequest
|
||||
* @param accessTokenAuthentication Authentication
|
||||
* @param map 请求参数
|
||||
*/
|
||||
private void sendSuccessEventLog(HttpServletRequest request,
|
||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication, Map<String, Object> map) {
|
||||
// 发送异步日志事件
|
||||
SecurityContextHolder.getContext().setAuthentication(accessTokenAuthentication);
|
||||
SysLogDTO logVo = SysLogUtils.getSysLog();
|
||||
logVo.setTitle("登录成功");
|
||||
logVo.setLogType(LogTypeEnum.NORMAL.getType());
|
||||
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
|
||||
if (StrUtil.isNotBlank(startTimeStr)) {
|
||||
Long startTime = Long.parseLong(startTimeStr);
|
||||
Long endTime = System.currentTimeMillis();
|
||||
logVo.setTime(endTime - startTime);
|
||||
}
|
||||
logVo.setCreateBy(MapUtil.getStr(map, SecurityConstants.DETAILS_USERNAME));
|
||||
logVo.setTenantId(Long.parseLong(tenantKeyStrResolver.key()));
|
||||
|
||||
publisher.publishEvent(new SysLogEvent(logVo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空登录失败的记录
|
||||
* @param authentication
|
||||
*/
|
||||
private void clearLoginFailureTimes(Authentication authentication) {
|
||||
String username = authentication.getName();
|
||||
String key = String.format("%s:%s:%s", CacheConstants.LOGIN_ERROR_TIMES, tenantKeyStrResolver.key(), username);
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
private void sendAccessTokenResponse(HttpServletResponse response, Authentication authentication)
|
||||
throws IOException {
|
||||
|
||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
|
||||
|
||||
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
|
||||
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
|
||||
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
|
||||
|
||||
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 (!CollectionUtils.isEmpty(additionalParameters)) {
|
||||
builder.additionalParameters(additionalParameters);
|
||||
}
|
||||
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
|
||||
// 无状态 注意删除 context 上下文的信息
|
||||
SecurityContextHolder.clearContext();
|
||||
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
|
||||
}
|
||||
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.event.LogoutSuccessEvent;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author zhangran
|
||||
* @date 2022-06-02
|
||||
*
|
||||
* 事件机制处理退出相关
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PigLogoutSuccessEventHandler implements ApplicationListener<LogoutSuccessEvent> {
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(LogoutSuccessEvent event) {
|
||||
Authentication authentication = (Authentication) event.getSource();
|
||||
if (authentication instanceof PreAuthenticatedAuthenticationToken) {
|
||||
handle(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退出成功方法
|
||||
* <p>
|
||||
* 获取到登录的authentication 对象
|
||||
* @param authentication 登录对象
|
||||
*/
|
||||
public void handle(Authentication authentication) {
|
||||
log.info("用户:{} 退出成功", authentication.getPrincipal());
|
||||
SysLogDTO logVo = SysLogUtils.getSysLog();
|
||||
logVo.setTitle("退出成功");
|
||||
// 发送异步日志事件
|
||||
Long startTime = System.currentTimeMillis();
|
||||
Long endTime = System.currentTimeMillis();
|
||||
logVo.setTime(endTime - startTime);
|
||||
|
||||
// 设置对应的token
|
||||
logVo.setParams(WebUtils.getRequest().getHeader(HttpHeaders.AUTHORIZATION));
|
||||
|
||||
// 这边设置ServiceId
|
||||
if (authentication instanceof PreAuthenticatedAuthenticationToken) {
|
||||
logVo.setServiceId(authentication.getCredentials().toString());
|
||||
}
|
||||
logVo.setCreateBy(authentication.getName());
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.pig4cloud.pigx.auth.support.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 javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-02
|
||||
* <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 {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取请求参数中是否包含 回调地址
|
||||
String redirectUrl = request.getParameter(REDIRECT_URL);
|
||||
if (StrUtil.isNotBlank(redirectUrl)) {
|
||||
response.sendRedirect(redirectUrl);
|
||||
}
|
||||
else if (StrUtil.isNotBlank(request.getHeader(HttpHeaders.REFERER))) {
|
||||
// 默认跳转referer 地址
|
||||
String referer = request.getHeader(HttpHeaders.REFERER);
|
||||
response.sendRedirect(referer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.pig4cloud.pigx.common.security.handler;
|
||||
package com.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.pig4cloud.pigx.auth.support.password;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2EndpointUtils;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author jumuning
|
||||
* @date 2022-06-02
|
||||
*
|
||||
* 密码认证转换器
|
||||
*/
|
||||
public class OAuth2ResourceOwnerPasswordAuthenticationConverter
|
||||
extends OAuth2ResourceOwnerBaseAuthenticationConverter<OAuth2ResourceOwnerPasswordAuthenticationToken> {
|
||||
|
||||
/**
|
||||
* 支持密码模式
|
||||
* @param grantType 授权类型
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String grantType) {
|
||||
return AuthorizationGrantType.PASSWORD.getValue().equals(grantType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2ResourceOwnerPasswordAuthenticationToken buildToken(Authentication clientPrincipal,
|
||||
Set requestedScopes, Map additionalParameters) {
|
||||
return new OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType.PASSWORD, clientPrincipal,
|
||||
requestedScopes, additionalParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验扩展参数 密码模式密码必须不为空
|
||||
* @param request 参数列表
|
||||
*/
|
||||
@Override
|
||||
public void checkParams(HttpServletRequest request) {
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
// username (REQUIRED)
|
||||
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||
if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
|
||||
// password (REQUIRED)
|
||||
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||
if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.pig4cloud.pigx.auth.support.password;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationProvider;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author jumuning
|
||||
* @description 处理用户名密码授权
|
||||
*/
|
||||
public class OAuth2ResourceOwnerPasswordAuthenticationProvider
|
||||
extends OAuth2ResourceOwnerBaseAuthenticationProvider<OAuth2ResourceOwnerPasswordAuthenticationToken> {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerPasswordAuthenticationProvider.class);
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the
|
||||
* provided parameters.
|
||||
* @param authenticationManager
|
||||
* @param authorizationService the authorization service
|
||||
* @param tokenGenerator the token generator
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2ResourceOwnerPasswordAuthenticationProvider(AuthenticationManager authenticationManager,
|
||||
OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
super(authenticationManager, authorizationService, tokenGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
|
||||
String username = (String) reqParameters.get(OAuth2ParameterNames.USERNAME);
|
||||
String password = (String) reqParameters.get(OAuth2ParameterNames.PASSWORD);
|
||||
return new UsernamePasswordAuthenticationToken(username, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
boolean supports = OAuth2ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
LOGGER.debug("supports authentication=" + authentication + " returning " + supports);
|
||||
return supports;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClient(RegisteredClient registeredClient) {
|
||||
assert registeredClient != null;
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.pig4cloud.pigx.auth.support.password;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author jumuning
|
||||
* @description 密码授权token信息
|
||||
*/
|
||||
public class OAuth2ResourceOwnerPasswordAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {
|
||||
|
||||
public OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType authorizationGrantType,
|
||||
Authentication clientPrincipal, Set<String> scopes, Map<String, Object> additionalParameters) {
|
||||
super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 密码模式
|
||||
*/
|
||||
package com.pig4cloud.pigx.auth.support.password;
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.pig4cloud.pigx.auth.support.sms;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2EndpointUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-05-31
|
||||
*
|
||||
* 短信登录转换器
|
||||
*/
|
||||
public class OAuth2ResourceOwnerSmsAuthenticationConverter
|
||||
extends OAuth2ResourceOwnerBaseAuthenticationConverter<OAuth2ResourceOwnerSmsAuthenticationToken> {
|
||||
|
||||
/**
|
||||
* 是否支持此convert
|
||||
* @param grantType 授权类型
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String grantType) {
|
||||
return SecurityConstants.APP.equals(grantType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2ResourceOwnerSmsAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes,
|
||||
Map additionalParameters) {
|
||||
return new OAuth2ResourceOwnerSmsAuthenticationToken(new AuthorizationGrantType(SecurityConstants.APP),
|
||||
clientPrincipal, requestedScopes, additionalParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验扩展参数 密码模式密码必须不为空
|
||||
* @param request 参数列表
|
||||
*/
|
||||
@Override
|
||||
public void checkParams(HttpServletRequest request) {
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
// PHONE (REQUIRED)
|
||||
String phone = parameters.getFirst(SecurityConstants.SMS_PARAMETER_NAME);
|
||||
if (!StringUtils.hasText(phone) || parameters.get(SecurityConstants.SMS_PARAMETER_NAME).size() != 1) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, SecurityConstants.SMS_PARAMETER_NAME,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.pig4cloud.pigx.auth.support.sms;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date date
|
||||
*
|
||||
* 短信登录的核心处理
|
||||
*/
|
||||
public class OAuth2ResourceOwnerSmsAuthenticationProvider
|
||||
extends OAuth2ResourceOwnerBaseAuthenticationProvider<OAuth2ResourceOwnerSmsAuthenticationToken> {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerSmsAuthenticationProvider.class);
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the
|
||||
* provided parameters.
|
||||
* @param authenticationManager
|
||||
* @param authorizationService the authorization service
|
||||
* @param tokenGenerator the token generator
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2ResourceOwnerSmsAuthenticationProvider(AuthenticationManager authenticationManager,
|
||||
OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
super(authenticationManager, authorizationService, tokenGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
boolean supports = OAuth2ResourceOwnerSmsAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
LOGGER.debug("supports authentication=" + authentication + " returning " + supports);
|
||||
return supports;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClient(RegisteredClient registeredClient) {
|
||||
assert registeredClient != null;
|
||||
if (!registeredClient.getAuthorizationGrantTypes()
|
||||
.contains(new AuthorizationGrantType(SecurityConstants.APP))) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
|
||||
String phone = (String) reqParameters.get(SecurityConstants.SMS_PARAMETER_NAME);
|
||||
return new UsernamePasswordAuthenticationToken(phone, null);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.pig4cloud.pigx.auth.support.sms;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @description 短信登录token信息
|
||||
*/
|
||||
public class OAuth2ResourceOwnerSmsAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {
|
||||
|
||||
public OAuth2ResourceOwnerSmsAuthenticationToken(AuthorizationGrantType authorizationGrantType,
|
||||
Authentication clientPrincipal, Set<String> scopes, Map<String, Object> additionalParameters) {
|
||||
super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 短信模式
|
||||
*/
|
||||
package com.pig4cloud.pigx.auth.support.sms;
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport"
|
||||
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
|
||||
<title>PigX第三方授权</title>
|
||||
<title>PigX 第三方授权</title>
|
||||
<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/css/signin.css"/>
|
||||
</head>
|
||||
@@ -21,21 +21,26 @@
|
||||
<a target="_blank" href="https://pig4cloud.com">技术支持</a>
|
||||
</p>
|
||||
<p class="navbar-text navbar-right">
|
||||
<a target="_blank" href="https://pig4cloud.com">${user.username}</a>
|
||||
<#if principalName=="anonymousUser">
|
||||
未登录
|
||||
<#else>
|
||||
<a target="_blank" href="https://pig4cloud.com">${principalName}</a>
|
||||
</#if>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div style="padding-top: 80px;width: 300px; color: #555; margin:0px auto;">
|
||||
<form id='confirmationForm' name='confirmationForm' action="/oauth/authorize" method='post'>
|
||||
<input name='user_oauth_approval' value='true' type='hidden'/>
|
||||
<form id='confirmationForm' name='confirmationForm' action="/oauth2/authorize" method='post'>
|
||||
<input type="hidden" name="client_id" value="${clientId}">
|
||||
<input type="hidden" name="state" value="${state}">
|
||||
|
||||
<p>
|
||||
<a href="${app.website!''}" target="_blank">${app.appName!'未定义应用名称'}</a> 将获得以下权限:</p>
|
||||
将获得以下权限:</p>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item"> <span>
|
||||
<#list scopeList as scope>
|
||||
<input type="hidden" name="${scope}" value="true"/>
|
||||
<input type="checkbox" disabled checked="checked"/><label>${scope}</label>
|
||||
<input type="checkbox" checked="checked" name="scope" value="${scope}"/><label>${scope}</label>
|
||||
</#list>
|
||||
</ul>
|
||||
<p class="help-block">授权后表明你已同意 <a>服务协议</a></p>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</select>
|
||||
</#if>
|
||||
|
||||
<input type="hidden" name="clientId" class="form-control" value="pig" placeholder="所属客户端" >
|
||||
<input type="hidden" name="client_id" class="form-control" value="pig" placeholder="所属客户端" >
|
||||
<input type="text" name="username" class="form-control" placeholder="账号" required autofocus>
|
||||
<input type="password" name="password" class="form-control" placeholder="密码" required>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">sign in</button>
|
||||
|
||||
+5
@@ -105,4 +105,9 @@ public interface CacheConstants {
|
||||
*/
|
||||
String LOGIN_ERROR_TIMES = "login_error_times";
|
||||
|
||||
/**
|
||||
* oauth 缓存前缀
|
||||
*/
|
||||
String PROJECT_OAUTH_ACCESS = "token::access_token";
|
||||
|
||||
}
|
||||
+10
@@ -125,4 +125,14 @@ public interface CommonConstants {
|
||||
*/
|
||||
String REQUEST_START_TIME = "REQUEST-START-TIME";
|
||||
|
||||
/**
|
||||
* 当前页
|
||||
*/
|
||||
String CURRENT = "current";
|
||||
|
||||
/**
|
||||
* size
|
||||
*/
|
||||
String SIZE = "size";
|
||||
|
||||
}
|
||||
+34
-4
@@ -73,7 +73,7 @@ public interface SecurityConstants {
|
||||
/**
|
||||
* 项目的license
|
||||
*/
|
||||
String PIGX_LICENSE = "made by pigx";
|
||||
String PIGX_LICENSE = "https://pig4cloud.com";
|
||||
|
||||
/**
|
||||
* 内部
|
||||
@@ -88,7 +88,7 @@ public interface SecurityConstants {
|
||||
/**
|
||||
* OAUTH URL
|
||||
*/
|
||||
String OAUTH_TOKEN_URL = "/oauth/token";
|
||||
String OAUTH_TOKEN_URL = "/oauth2/token";
|
||||
|
||||
/**
|
||||
* 移动端授权
|
||||
@@ -96,9 +96,14 @@ public interface SecurityConstants {
|
||||
String GRANT_MOBILE = "mobile";
|
||||
|
||||
/**
|
||||
* 小程序授权
|
||||
* TOC 客户端
|
||||
*/
|
||||
String CLIENT_MINI = "mini";
|
||||
String HEADER_TOC = "CLIENT_TOC";
|
||||
|
||||
/**
|
||||
* TOC 客户端
|
||||
*/
|
||||
String HEADER_TOC_YES = "Y";
|
||||
|
||||
/**
|
||||
* QQ获取token
|
||||
@@ -256,4 +261,29 @@ public interface SecurityConstants {
|
||||
*/
|
||||
String AES = "aes";
|
||||
|
||||
/**
|
||||
* 授权码模式confirm
|
||||
*/
|
||||
String CUSTOM_CONSENT_PAGE_URI = "/token/confirm_access";
|
||||
|
||||
/**
|
||||
* {noop} 加密的特征码
|
||||
*/
|
||||
String NOOP = "{noop}";
|
||||
|
||||
/**
|
||||
* 短信登录 参数名称
|
||||
*/
|
||||
String SMS_PARAMETER_NAME = "mobile";
|
||||
|
||||
/**
|
||||
* 手机号登录
|
||||
*/
|
||||
String APP = "mobile";
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
String DETAILS_USER = "user_info";
|
||||
|
||||
}
|
||||
+1
@@ -72,6 +72,7 @@ public enum LoginTypeEnum {
|
||||
* 企业微信
|
||||
*/
|
||||
WEIXIN_CP("WEIXIN_CP", "企业微信"),
|
||||
|
||||
/**
|
||||
* CAS 登录
|
||||
*/
|
||||
|
||||
+10
@@ -34,4 +34,14 @@ public class MsgUtils {
|
||||
return messageSource.getMessage(code, objects, Locale.CHINA);
|
||||
}
|
||||
|
||||
/**
|
||||
* security 通过code 和参数获取中文错误信息
|
||||
* @param code
|
||||
* @return
|
||||
*/
|
||||
public String getSecurityMessage(String code, Object... objects) {
|
||||
MessageSource messageSource = SpringContextHolder.getBean("securityMessageSource");
|
||||
return messageSource.getMessage(code, objects, Locale.CHINA);
|
||||
}
|
||||
|
||||
}
|
||||
+2
@@ -55,9 +55,11 @@ public class PigxDefaultDatascopeHandle implements DataScopeHandle {
|
||||
@Override
|
||||
public Boolean calcScope(DataScope dataScope) {
|
||||
PigxUser user = SecurityUtils.getUser();
|
||||
// toc 客户端不进行数据权限
|
||||
if (UserTypeEnum.TOC.getStatus().equals(user.getUserType())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> roleIdList = user.getAuthorities().stream().map(GrantedAuthority::getAuthority)
|
||||
.filter(authority -> authority.startsWith(SecurityConstants.ROLE))
|
||||
.map(authority -> authority.split(StrUtil.UNDERLINE)[1]).collect(Collectors.toList());
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ public class SysLogUtils {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) Objects
|
||||
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
|
||||
SysLogDTO sysLog = new SysLogDTO();
|
||||
sysLog.setCreateBy(Objects.requireNonNull(getUsername()));
|
||||
sysLog.setCreateBy(getUsername());
|
||||
sysLog.setLogType(LogTypeEnum.NORMAL.getType());
|
||||
sysLog.setRemoteAddr(ServletUtil.getClientIP(request));
|
||||
sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
|
||||
|
||||
@@ -45,8 +45,13 @@
|
||||
</dependency>
|
||||
<!--安全模块-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.oauth.boot</groupId>
|
||||
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-authorization-server</artifactId>
|
||||
<version>${spring.authorization.version}</version>
|
||||
</dependency>
|
||||
<!--feign-->
|
||||
<dependency>
|
||||
@@ -73,5 +78,9 @@
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-app-server-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+19
-20
@@ -1,43 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 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
|
||||
*
|
||||
* 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)
|
||||
* 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.pig4cloud.pigx.common.security.annotation;
|
||||
|
||||
import com.pig4cloud.pigx.common.security.component.PigxResourceServerAutoConfiguration;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxSecurityBeanDefinitionRegistrar;
|
||||
import com.pig4cloud.pigx.common.security.component.PigResourceServerAutoConfiguration;
|
||||
import com.pig4cloud.pigx.common.security.component.PigResourceServerConfiguration;
|
||||
import com.pig4cloud.pigx.common.security.feign.PigFeignClientConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/11/10
|
||||
* @date 2022-06-04
|
||||
* <p>
|
||||
* 资源服务注解
|
||||
*/
|
||||
@Documented
|
||||
@Inherited
|
||||
@EnableResourceServer
|
||||
@EnableMethodSecurity
|
||||
@Target({ ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
@Import({ PigxResourceServerAutoConfiguration.class, PigxSecurityBeanDefinitionRegistrar.class })
|
||||
@Import({ PigResourceServerAutoConfiguration.class, PigResourceServerConfiguration.class,
|
||||
PigFeignClientConfiguration.class })
|
||||
public @interface EnablePigxResourceServer {
|
||||
|
||||
}
|
||||
+14
-15
@@ -1,18 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 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
|
||||
*
|
||||
* 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)
|
||||
* 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.pig4cloud.pigx.common.security.annotation;
|
||||
@@ -20,10 +19,10 @@ package com.pig4cloud.pigx.common.security.annotation;
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 服务调用不鉴权注解
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2019/4/13
|
||||
* <p>
|
||||
* 服务调用鉴权注解
|
||||
* @date 2020-06-14
|
||||
*/
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
|
||||
+13
-17
@@ -1,28 +1,26 @@
|
||||
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* 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
|
||||
*
|
||||
* 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)
|
||||
* 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.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -30,10 +28,8 @@ import java.util.Collection;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/6/22 接口权限判断工具
|
||||
* @date 2019/2/1 接口权限判断工具
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("pms")
|
||||
public class PermissionService {
|
||||
|
||||
/**
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.core.util.ReUtil;
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import com.pig4cloud.pigx.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 lengleng
|
||||
* @date 2020-03-11
|
||||
* <p>
|
||||
* 资源服务器对外直接暴露URL,如果设置contex-path 要特殊处理
|
||||
*/
|
||||
@Slf4j
|
||||
@ConfigurationProperties(prefix = "security.oauth2.ignore")
|
||||
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> urls = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
urls.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 -> urls.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 -> urls.add(ReUtil.replaceAll(url, PATTERN, "*"))));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ReUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.annotation.Inner;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
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.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019/4/13
|
||||
* <p>
|
||||
* 资源暴露处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnExpression("!'${security.oauth2.client.ignore-urls}'.isEmpty()")
|
||||
@ConfigurationProperties(prefix = "security.oauth2.client")
|
||||
public class PermitAllUrlResolver implements InitializingBean {
|
||||
|
||||
private static final PathMatcher PATHMATCHER = new AntPathMatcher();
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
|
||||
|
||||
private static final String[] DEFAULT_IGNORE_URLS = new String[] { "/error", "/v2/api-docs" };
|
||||
|
||||
private final WebApplicationContext applicationContext;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private List<String> ignoreUrls = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
RequestMappingHandlerMapping mapping = applicationContext.getBean("requestMappingHandlerMapping",
|
||||
RequestMappingHandlerMapping.class);
|
||||
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
|
||||
|
||||
for (RequestMappingInfo info : map.keySet()) {
|
||||
HandlerMethod handlerMethod = map.get(info);
|
||||
|
||||
// 1. 首先获取类上边 @Inner 注解
|
||||
Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);
|
||||
|
||||
// 2. 当类上不包含 @Inner 注解则获取该方法的注解
|
||||
if (controller == null) {
|
||||
Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
|
||||
Optional.ofNullable(method)
|
||||
.ifPresent(inner -> Objects.requireNonNull(info.getPatternsCondition().getPatterns())
|
||||
.forEach(url -> this.filterPath(url, info, map)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 当类上包含 @Inner 注解 判断handlerMethod 是否包含在 inner 类中
|
||||
Class<?> beanType = handlerMethod.getBeanType();
|
||||
Method[] methods = beanType.getDeclaredMethods();
|
||||
Method method = handlerMethod.getMethod();
|
||||
if (ArrayUtil.contains(methods, method)) {
|
||||
Objects.requireNonNull(info.getPatternsCondition().getPatterns())
|
||||
.forEach(url -> filterPath(url, info, map));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤 Inner 设置
|
||||
* <p>
|
||||
* 0. 暴露安全检查 1. 路径转换: 如果为restful(/xx/{xx}) --> /xx/* ant 表达式 2.
|
||||
* 构建表达式:允许暴露的接口|允许暴露的方法类型,允许暴露的方法类型 URL|GET,POST,DELETE,PUT
|
||||
* </p>
|
||||
* @param url mapping路径
|
||||
* @param info 请求犯法
|
||||
* @param map 路由映射信息
|
||||
*/
|
||||
private void filterPath(String url, RequestMappingInfo info, Map<RequestMappingInfo, HandlerMethod> map) {
|
||||
// 安全检查
|
||||
if (SecurityConstants.INNER_CHECK) {
|
||||
security(url, info, map);
|
||||
}
|
||||
|
||||
List<String> methodList = info.getMethodsCondition().getMethods().stream().map(RequestMethod::name)
|
||||
.collect(Collectors.toList());
|
||||
String resultUrl = ReUtil.replaceAll(url, PATTERN, "*");
|
||||
if (CollUtil.isEmpty(methodList)) {
|
||||
ignoreUrls.add(resultUrl);
|
||||
}
|
||||
else {
|
||||
ignoreUrls.add(String.format("%s|%s", resultUrl, CollUtil.join(methodList, StrUtil.COMMA)));
|
||||
}
|
||||
|
||||
// 增加默认暴露规则
|
||||
for (String ignoreUrl : DEFAULT_IGNORE_URLS) {
|
||||
ignoreUrls.add(ignoreUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对Pathvariable 请求安全检查。增加启动好使影响启动效率 请注意
|
||||
* @param url 接口路径
|
||||
* @param rq 当前请求的元信息
|
||||
* @param map springmvc 接口列表
|
||||
*/
|
||||
private void security(String url, RequestMappingInfo rq, Map<RequestMappingInfo, HandlerMethod> map) {
|
||||
// 判断 URL 是否是 rest path 形式
|
||||
if (!StrUtil.containsAny(url, StrUtil.DELIM_START, StrUtil.DELIM_END)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (RequestMappingInfo info : map.keySet()) {
|
||||
Set<RequestMethod> methods = info.getMethodsCondition().getMethods();
|
||||
// 如果请求方法不匹配跳过
|
||||
if (!CollUtil.containsAny(methods, rq.getMethodsCondition().getMethods())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果请求方法路径匹配
|
||||
|
||||
Set<String> patterns = Objects.requireNonNull(info.getPatternsCondition().getPatterns());
|
||||
for (String pattern : patterns) {
|
||||
// 跳过自身
|
||||
if (StrUtil.equals(url, pattern)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (PATHMATCHER.match(url, pattern)) {
|
||||
HandlerMethod rqMethod = map.get(rq);
|
||||
HandlerMethod infoMethod = map.get(info);
|
||||
log.error("@Inner 标记接口 ==> {}.{} 使用不当,会额外暴露接口 ==> {}.{} 请知悉", rqMethod.getBeanType().getName(),
|
||||
rqMethod.getMethod().getName(), infoMethod.getBeanType().getName(),
|
||||
infoMethod.getMethod().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对外暴露的URL,注册到 spring security
|
||||
* @param registry spring security context
|
||||
*/
|
||||
public void registry(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
|
||||
for (String url : getIgnoreUrls()) {
|
||||
List<String> strings = StrUtil.split(url, "|");
|
||||
|
||||
// 仅配置对外暴露的URL ,则注册到 spring security的为全部方法
|
||||
if (strings.size() == 1) {
|
||||
registry.antMatchers(strings.get(0)).permitAll();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 当配置对外的URL|GET,POST 这种形式,则获取方法列表 并注册到 spring security
|
||||
if (strings.size() == 2) {
|
||||
for (String method : StrUtil.split(strings.get(1), StrUtil.COMMA)) {
|
||||
registry.antMatchers(HttpMethod.valueOf(method), strings.get(0)).permitAll();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
log.warn("{} 配置无效,无法配置对外暴露", url);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
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 javax.servlet.http.HttpServletRequest;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author caiqy
|
||||
* @date 2020.05.15
|
||||
*/
|
||||
public class PigBearerTokenExtractor implements BearerTokenResolver {
|
||||
|
||||
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-:._~+/]+=*)$",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private boolean allowFormEncodedBodyParameter = false;
|
||||
|
||||
private boolean allowUriQueryParameter = true;
|
||||
|
||||
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
|
||||
|
||||
private final PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
private final PermitAllUrlProperties urlProperties;
|
||||
|
||||
public PigBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
|
||||
this.urlProperties = urlProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolve(HttpServletRequest request) {
|
||||
boolean match = urlProperties.getUrls().stream()
|
||||
.anyMatch(url -> pathMatcher.match(url, request.getRequestURI()));
|
||||
|
||||
if (match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
|
||||
final String parameterToken = isParameterTokenSupportedForRequest(request)
|
||||
? resolveFromRequestParameters(request) : null;
|
||||
if (authorizationHeaderToken != null) {
|
||||
if (parameterToken != null) {
|
||||
final BearerTokenError error = BearerTokenErrors
|
||||
.invalidRequest("Found multiple bearer tokens in the request");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
return authorizationHeaderToken;
|
||||
}
|
||||
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
|
||||
return parameterToken;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
|
||||
return (("POST".equals(request.getMethod())
|
||||
&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
|
||||
|| "GET".equals(request.getMethod()));
|
||||
}
|
||||
|
||||
private boolean isParameterTokenEnabledForRequest(final HttpServletRequest request) {
|
||||
return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod())
|
||||
&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
|
||||
|| (this.allowUriQueryParameter && "GET".equals(request.getMethod())));
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.pig4cloud.pigx.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 lengleng
|
||||
* @date 2022/7/6
|
||||
*
|
||||
* credential 支持客户端模式的用户存储
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class PigClientCredentialsOAuth2AuthenticatedPrincipal 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;
|
||||
}
|
||||
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService;
|
||||
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 lengleng
|
||||
* @date 2022/5/28
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PigCustomOpaqueTokenIntrospector 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 PigClientCredentialsOAuth2AuthenticatedPrincipal(oldAuthorization.getAttributes(),
|
||||
AuthorityUtils.NO_AUTHORITIES, oldAuthorization.getPrincipalName());
|
||||
}
|
||||
|
||||
Map<String, PigxUserDetailsService> userDetailsServiceMap = SpringContextHolder
|
||||
.getBeansOfType(PigxUserDetailsService.class);
|
||||
|
||||
Optional<PigxUserDetailsService> 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((PigxUser) tokenPrincipal);
|
||||
}
|
||||
catch (UsernameNotFoundException notFoundException) {
|
||||
log.warn("用户不不存在 {}", notFoundException.getLocalizedMessage());
|
||||
throw notFoundException;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
log.error("资源服务器 introspect Token error {}", ex.getLocalizedMessage());
|
||||
}
|
||||
return (PigxUser) userDetails;
|
||||
}
|
||||
|
||||
}
|
||||
+76
@@ -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.pig4cloud.pigx.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 lengleng
|
||||
* @date 2022-06-02
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@EnableConfigurationProperties(PermitAllUrlProperties.class)
|
||||
public class PigResourceServerAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 鉴权具体的实现逻辑
|
||||
* @return (#pms.xxx)
|
||||
*/
|
||||
@Bean("pms")
|
||||
public PermissionService permissionService() {
|
||||
return new PermissionService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求令牌的抽取逻辑
|
||||
* @param urlProperties 对外暴露的接口列表
|
||||
* @return BearerTokenExtractor
|
||||
*/
|
||||
@Bean
|
||||
public PigBearerTokenExtractor pigBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
|
||||
return new PigBearerTokenExtractor(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 PigCustomOpaqueTokenIntrospector(authorizationService);
|
||||
}
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
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.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-04
|
||||
*
|
||||
* 资源服务器认证授权配置
|
||||
*/
|
||||
@Slf4j
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class PigResourceServerConfiguration {
|
||||
|
||||
protected final ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
|
||||
|
||||
private final PermitAllUrlProperties permitAllUrl;
|
||||
|
||||
private final PigBearerTokenExtractor pigBearerTokenExtractor;
|
||||
|
||||
private final OpaqueTokenIntrospector customOpaqueTokenIntrospector;
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
http.authorizeHttpRequests(authorizeRequests -> authorizeRequests
|
||||
.antMatchers(ArrayUtil.toArray(permitAllUrl.getUrls(), String.class)).permitAll().anyRequest()
|
||||
.authenticated())
|
||||
.oauth2ResourceServer(
|
||||
oauth2 -> oauth2.opaqueToken(token -> token.introspector(customOpaqueTokenIntrospector))
|
||||
.authenticationEntryPoint(resourceAuthExceptionEntryPoint)
|
||||
.bearerTokenResolver(pigBearerTokenExtractor))
|
||||
.headers().frameOptions().disable().and().csrf().disable();
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.annotation.Inner;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-04
|
||||
*
|
||||
* 服务间接口不鉴权处理逻辑
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@RequiredArgsConstructor
|
||||
public class PigSecurityInnerAspect implements Ordered {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
@SneakyThrows
|
||||
@Around("@within(inner) || @annotation(inner)")
|
||||
public Object around(ProceedingJoinPoint point, Inner inner) {
|
||||
// 实际注入的inner实体由表达式后一个注解决定,即是方法上的@Inner注解实体,若方法上无@Inner注解,则获取类上的
|
||||
if (inner == null) {
|
||||
Class<?> clazz = point.getTarget().getClass();
|
||||
inner = AnnotationUtils.findAnnotation(clazz, Inner.class);
|
||||
}
|
||||
String header = request.getHeader(SecurityConstants.FROM);
|
||||
if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
|
||||
log.warn("访问接口 {} 没有权限", point.getSignature().getName());
|
||||
throw new AccessDeniedException("Access is denied");
|
||||
}
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE + 1;
|
||||
}
|
||||
|
||||
}
|
||||
+6
-3
@@ -22,21 +22,24 @@ 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 lengleng
|
||||
* @date 2022年01月06日
|
||||
* @date 2022-06-04
|
||||
* <p>
|
||||
* 注入自定义错误处理
|
||||
* 注入自定义错误处理,覆盖 org/springframework/security/messages 内置异常
|
||||
*/
|
||||
@ConditionalOnWebApplication(type = SERVLET)
|
||||
public class PigxSecurityMessageSourceConfiguration implements WebMvcConfigurer {
|
||||
public class PigSecurityMessageSourceConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public MessageSource securityMessageSource() {
|
||||
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
|
||||
messageSource.addBasenames("classpath:errors/messages");
|
||||
messageSource.setDefaultLocale(Locale.CHINA);
|
||||
return messageSource;
|
||||
}
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.security.exception.PigxAuth2Exception;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/11/16
|
||||
* <p>
|
||||
* OAuth2 异常格式化
|
||||
*/
|
||||
public class PigxAuth2ExceptionSerializer extends StdSerializer<PigxAuth2Exception> {
|
||||
|
||||
public PigxAuth2ExceptionSerializer() {
|
||||
super(PigxAuth2Exception.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void serialize(PigxAuth2Exception value, JsonGenerator gen, SerializerProvider provider) {
|
||||
gen.writeStartObject();
|
||||
gen.writeObjectField("code", CommonConstants.FAIL);
|
||||
gen.writeStringField("msg", value.getMessage());
|
||||
gen.writeStringField("data", value.getErrorCode());
|
||||
gen.writeEndObject();
|
||||
}
|
||||
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright (c) 2019-2020, 冷冷 (wangiegie@gmail.com).
|
||||
* * <p>
|
||||
* * Licensed under the GNU Lesser General Public License 3.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* * <p>
|
||||
* * https://www.gnu.org/licenses/lgpl.html
|
||||
* * <p>
|
||||
* * 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.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 改造 {@link BearerTokenExtractor} 对公开权限的请求不进行校验
|
||||
*
|
||||
* @author caiqy
|
||||
* @date 2020.05.15
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PigxBearerTokenExtractor extends BearerTokenExtractor {
|
||||
|
||||
private final PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
private final PermitAllUrlResolver permitAllUrlResolver;
|
||||
|
||||
@Override
|
||||
public Authentication extract(HttpServletRequest request) {
|
||||
|
||||
// 2. 判断请求方法是否匹配
|
||||
boolean result = permitAllUrlResolver.getIgnoreUrls().stream().anyMatch(url -> {
|
||||
List<String> strings = StrUtil.split(url, "|");
|
||||
// 1. 判断路径是否匹配
|
||||
boolean match = pathMatcher.match(strings.get(0), request.getRequestURI());
|
||||
// 2. 判断方法是否匹配
|
||||
if (strings.size() == 2) {
|
||||
List<String> methods = StrUtil.split(strings.get(1), StrUtil.COMMA);
|
||||
return CollUtil.contains(methods, request.getMethod()) && match;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
return result ? null : super.extract(request);
|
||||
}
|
||||
|
||||
}
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.security.util.PigxSecurityMessageSourceUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.CredentialsExpiredException;
|
||||
import org.springframework.security.authentication.InsufficientAuthenticationException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* 针对资源服务器的异常处理 {@link OAuth2AuthenticationProcessingFilter}不同细化异常处理
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2020-06-14
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PigxCommenceAuthExceptionEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) {
|
||||
response.setCharacterEncoding(CommonConstants.UTF8);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
R<String> result = new R<>();
|
||||
result.setMsg(authException.getMessage());
|
||||
result.setData(authException.getMessage());
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
result.setCode(CommonConstants.FAIL);
|
||||
|
||||
if (authException instanceof CredentialsExpiredException) {
|
||||
String msg = PigxSecurityMessageSourceUtil.getAccessor().getMessage(
|
||||
"AbstractUserDetailsAuthenticationProvider.credentialsExpired", authException.getMessage());
|
||||
result.setMsg(msg);
|
||||
}
|
||||
|
||||
if (authException instanceof UsernameNotFoundException) {
|
||||
String msg = PigxSecurityMessageSourceUtil.getAccessor().getMessage(
|
||||
"AbstractUserDetailsAuthenticationProvider.noopBindAccount", authException.getMessage());
|
||||
result.setMsg(msg);
|
||||
}
|
||||
|
||||
if (authException instanceof BadCredentialsException) {
|
||||
String msg = PigxSecurityMessageSourceUtil.getAccessor().getMessage(
|
||||
"AbstractUserDetailsAuthenticationProvider.badClientCredentials", authException.getMessage());
|
||||
result.setMsg(msg);
|
||||
}
|
||||
|
||||
if (authException instanceof InsufficientAuthenticationException) {
|
||||
String msg = PigxSecurityMessageSourceUtil.getAccessor()
|
||||
.getMessage("AbstractAccessDecisionManager.expireToken", authException.getMessage());
|
||||
response.setStatus(HttpStatus.FAILED_DEPENDENCY.value());
|
||||
result.setMsg(msg);
|
||||
}
|
||||
|
||||
PrintWriter printWriter = response.getWriter();
|
||||
printWriter.append(objectMapper.writeValueAsString(result));
|
||||
}
|
||||
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
|
||||
import org.springframework.security.oauth2.provider.authentication.TokenExtractor;
|
||||
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/9/30
|
||||
* <p>
|
||||
* 支持本地模式 [不进过认证中心 CheckToken]的的资源服务器配置
|
||||
*/
|
||||
@Slf4j
|
||||
public class PigxLocalResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {
|
||||
|
||||
@Autowired
|
||||
protected AuthenticationEntryPoint resourceAuthExceptionEntryPoint;
|
||||
|
||||
@Autowired
|
||||
private PermitAllUrlResolver permitAllUrlResolver;
|
||||
|
||||
@Autowired
|
||||
private TokenExtractor tokenExtractor;
|
||||
|
||||
@Autowired
|
||||
private ResourceServerTokenServices resourceServerTokenServices;
|
||||
|
||||
/**
|
||||
* 默认的配置,对外暴露
|
||||
* @param httpSecurity
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void configure(HttpSecurity httpSecurity) {
|
||||
// 允许使用iframe 嵌套,避免swagger-ui 不被加载的问题
|
||||
httpSecurity.headers().frameOptions().disable();
|
||||
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
|
||||
.authorizeRequests();
|
||||
// 配置对外暴露接口
|
||||
permitAllUrlResolver.registry(registry);
|
||||
registry.anyRequest().authenticated().and().csrf().disable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(ResourceServerSecurityConfigurer resources) {
|
||||
resources.authenticationEntryPoint(resourceAuthExceptionEntryPoint).tokenExtractor(tokenExtractor)
|
||||
.tokenServices(resourceServerTokenServices);
|
||||
}
|
||||
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import com.pig4cloud.pigx.common.security.exception.UnauthorizedException;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService;
|
||||
import com.pig4cloud.pigx.common.security.util.PigxSecurityMessageSourceUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.security.authentication.AccountStatusException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Request;
|
||||
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
|
||||
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/9/29
|
||||
*/
|
||||
@Primary
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PigxLocalResourceServerTokenServices implements ResourceServerTokenServices {
|
||||
|
||||
private final TokenStore tokenStore;
|
||||
|
||||
private final UserDetailsChecker preAuthenticationChecks;
|
||||
|
||||
@Override
|
||||
public OAuth2Authentication loadAuthentication(String accessToken)
|
||||
throws AuthenticationException, InvalidTokenException {
|
||||
OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);
|
||||
if (oAuth2Authentication == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
|
||||
if (!(oAuth2Authentication.getPrincipal() instanceof PigxUser)) {
|
||||
return oAuth2Authentication;
|
||||
}
|
||||
|
||||
String clientId = oAuth2Request.getClientId();
|
||||
|
||||
Map<String, PigxUserDetailsService> userDetailsServiceMap = SpringContextHolder
|
||||
.getBeansOfType(PigxUserDetailsService.class);
|
||||
|
||||
Optional<PigxUserDetailsService> optional = userDetailsServiceMap.values().stream()
|
||||
.filter(service -> service.support(clientId, oAuth2Request.getGrantType()))
|
||||
.max(Comparator.comparingInt(Ordered::getOrder));
|
||||
|
||||
if (!optional.isPresent()) {
|
||||
throw new InternalAuthenticationServiceException("UserDetailsService error , not register");
|
||||
}
|
||||
|
||||
// 根据 username 查询 spring cache 最新的值 并返回
|
||||
PigxUser pigUser = (PigxUser) oAuth2Authentication.getPrincipal();
|
||||
|
||||
UserDetails userDetails;
|
||||
try {
|
||||
userDetails = optional.get().loadUserByUser(pigUser);
|
||||
|
||||
preAuthenticationChecks.check(userDetails);
|
||||
}
|
||||
catch (UsernameNotFoundException notFoundException) {
|
||||
throw new UnauthorizedException(String.format("%s username not found", pigUser.getUsername()),
|
||||
notFoundException);
|
||||
}
|
||||
catch (AccountStatusException accountStatusException) {
|
||||
throw new UnauthorizedException(
|
||||
PigxSecurityMessageSourceUtil.getAccessor().getMessage(
|
||||
"AbstractUserDetailsAuthenticationProvider.locked", accountStatusException.getMessage()),
|
||||
accountStatusException);
|
||||
}
|
||||
Authentication userAuthentication = new UsernamePasswordAuthenticationToken(userDetails, "N/A",
|
||||
userDetails.getAuthorities());
|
||||
OAuth2Authentication authentication = new OAuth2Authentication(oAuth2Request, userAuthentication);
|
||||
authentication.setAuthenticated(true);
|
||||
return authentication;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessToken readAccessToken(String accessToken) {
|
||||
throw new UnsupportedOperationException("Not supported: read access token");
|
||||
}
|
||||
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.pig4cloud.pigx.common.security.util.PigxSecurityMessageSourceUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019-01-02
|
||||
*/
|
||||
@Slf4j
|
||||
public class PigxPreAuthenticationChecks implements UserDetailsChecker {
|
||||
|
||||
private MessageSourceAccessor messages = PigxSecurityMessageSourceUtil.getAccessor();
|
||||
|
||||
@Override
|
||||
public void check(UserDetails user) {
|
||||
if (!user.isAccountNonLocked()) {
|
||||
log.debug("User account is locked");
|
||||
|
||||
throw new LockedException(
|
||||
messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
|
||||
}
|
||||
|
||||
if (!user.isEnabled()) {
|
||||
log.debug("User account is disabled");
|
||||
|
||||
throw new DisabledException(
|
||||
messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
|
||||
}
|
||||
|
||||
if (!user.isAccountNonExpired()) {
|
||||
log.debug("User account is expired");
|
||||
|
||||
throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired",
|
||||
"User account has expired"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-483
@@ -1,483 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisZSetCommands;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
|
||||
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
|
||||
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||
import org.springframework.security.oauth2.provider.token.store.redis.JdkSerializationStrategy;
|
||||
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
|
||||
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author efenderbosch
|
||||
* @date 2020/9/30
|
||||
* <p>
|
||||
* @link https://github.com/spring-projects/spring-security-oauth/pull/1660
|
||||
* 重写RedisTokenStore ,主要解决 #1814 oauth2中client_id_to_access数据膨胀问题
|
||||
*/
|
||||
public class PigxRedisTokenStore implements TokenStore {
|
||||
|
||||
private static final String ACCESS = "access:";
|
||||
|
||||
private static final String AUTH_TO_ACCESS = "auth_to_access:";
|
||||
|
||||
private static final String AUTH = "auth:";
|
||||
|
||||
private static final String REFRESH_AUTH = "refresh_auth:";
|
||||
|
||||
private static final String REFRESH = "refresh:";
|
||||
|
||||
private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
|
||||
|
||||
private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access_z:";
|
||||
|
||||
private static final String UNAME_TO_ACCESS = "uname_to_access_z:";
|
||||
|
||||
private static final boolean springDataRedis_2_0 = ClassUtils.isPresent(
|
||||
"org.springframework.data.redis.connection.RedisStandaloneConfiguration",
|
||||
RedisTokenStore.class.getClassLoader());
|
||||
|
||||
private final RedisConnectionFactory connectionFactory;
|
||||
|
||||
private final KeyStrResolver keyStrResolver;
|
||||
|
||||
private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
|
||||
|
||||
private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
|
||||
|
||||
private String prefix = "";
|
||||
|
||||
private Method redisConnectionSet_2_0;
|
||||
|
||||
public PigxRedisTokenStore(RedisConnectionFactory connectionFactory, KeyStrResolver resolver) {
|
||||
this.keyStrResolver = resolver;
|
||||
this.connectionFactory = connectionFactory;
|
||||
if (springDataRedis_2_0) {
|
||||
this.loadRedisConnectionMethods_2_0();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
|
||||
this.authenticationKeyGenerator = authenticationKeyGenerator;
|
||||
}
|
||||
|
||||
public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) {
|
||||
this.serializationStrategy = serializationStrategy;
|
||||
}
|
||||
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
private void loadRedisConnectionMethods_2_0() {
|
||||
this.redisConnectionSet_2_0 = ReflectionUtils.findMethod(RedisConnection.class, "set", byte[].class,
|
||||
byte[].class);
|
||||
}
|
||||
|
||||
private RedisConnection getConnection() {
|
||||
return connectionFactory.getConnection();
|
||||
}
|
||||
|
||||
private byte[] serialize(Object object) {
|
||||
return serializationStrategy.serialize(object);
|
||||
}
|
||||
|
||||
private byte[] serializeKey(String object) {
|
||||
return serialize(keyStrResolver.extract((prefix + object), StrUtil.COLON));
|
||||
}
|
||||
|
||||
private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
|
||||
return serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
|
||||
}
|
||||
|
||||
private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
|
||||
return serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
|
||||
}
|
||||
|
||||
private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
|
||||
return serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
|
||||
}
|
||||
|
||||
private byte[] serialize(String string) {
|
||||
return serializationStrategy.serialize(string);
|
||||
}
|
||||
|
||||
private String deserializeString(byte[] bytes) {
|
||||
return serializationStrategy.deserializeString(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
|
||||
String key = authenticationKeyGenerator.extractKey(authentication);
|
||||
byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key);
|
||||
byte[] bytes;
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
bytes = conn.get(serializedKey);
|
||||
}
|
||||
OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
|
||||
if (accessToken != null) {
|
||||
OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue());
|
||||
if ((storedAuthentication == null
|
||||
|| !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) {
|
||||
// Keep the stores consistent (maybe the same user is
|
||||
// represented by this authentication but the details have
|
||||
// changed)
|
||||
storeAccessToken(accessToken, authentication);
|
||||
}
|
||||
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
|
||||
return readAuthentication(token.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2Authentication readAuthentication(String token) {
|
||||
byte[] bytes;
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
bytes = conn.get(serializeKey(AUTH + token));
|
||||
}
|
||||
return deserializeAuthentication(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
|
||||
return readAuthenticationForRefreshToken(token.getValue());
|
||||
}
|
||||
|
||||
public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
byte[] bytes = conn.get(serializeKey(REFRESH_AUTH + token));
|
||||
return deserializeAuthentication(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
|
||||
byte[] serializedAccessToken = serialize(token);
|
||||
byte[] serializedAuth = serialize(authentication);
|
||||
byte[] accessKey = serializeKey(ACCESS + token.getValue());
|
||||
byte[] authKey = serializeKey(AUTH + token.getValue());
|
||||
byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));
|
||||
byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
|
||||
byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
|
||||
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
conn.openPipeline();
|
||||
if (springDataRedis_2_0) {
|
||||
try {
|
||||
this.redisConnectionSet_2_0.invoke(conn, accessKey, serializedAccessToken);
|
||||
this.redisConnectionSet_2_0.invoke(conn, authKey, serializedAuth);
|
||||
this.redisConnectionSet_2_0.invoke(conn, authToAccessKey, serializedAccessToken);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
conn.set(accessKey, serializedAccessToken);
|
||||
conn.set(authKey, serializedAuth);
|
||||
conn.set(authToAccessKey, serializedAccessToken);
|
||||
}
|
||||
|
||||
if (token.getExpiration() != null) {
|
||||
int seconds = token.getExpiresIn();
|
||||
long expirationTime = token.getExpiration().getTime();
|
||||
|
||||
if (!authentication.isClientOnly()) {
|
||||
conn.zAdd(approvalKey, expirationTime, serializedAccessToken);
|
||||
}
|
||||
conn.zAdd(clientId, expirationTime, serializedAccessToken);
|
||||
|
||||
conn.expire(accessKey, seconds);
|
||||
conn.expire(authKey, seconds);
|
||||
conn.expire(authToAccessKey, seconds);
|
||||
conn.expire(clientId, seconds);
|
||||
conn.expire(approvalKey, seconds);
|
||||
}
|
||||
else {
|
||||
conn.zAdd(clientId, -1, serializedAccessToken);
|
||||
if (!authentication.isClientOnly()) {
|
||||
conn.zAdd(approvalKey, -1, serializedAccessToken);
|
||||
}
|
||||
}
|
||||
OAuth2RefreshToken refreshToken = token.getRefreshToken();
|
||||
if (refreshToken != null && refreshToken.getValue() != null) {
|
||||
byte[] auth = serialize(token.getValue());
|
||||
byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue());
|
||||
if (springDataRedis_2_0) {
|
||||
try {
|
||||
this.redisConnectionSet_2_0.invoke(conn, refreshToAccessKey, auth);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
conn.set(refreshToAccessKey, auth);
|
||||
}
|
||||
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
|
||||
ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
|
||||
Date expiration = expiringRefreshToken.getExpiration();
|
||||
if (expiration != null) {
|
||||
int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
|
||||
.intValue();
|
||||
conn.expire(refreshToAccessKey, seconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
conn.closePipeline();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getApprovalKey(OAuth2Authentication authentication) {
|
||||
String userName = authentication.getUserAuthentication() == null ? ""
|
||||
: authentication.getUserAuthentication().getName();
|
||||
return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
|
||||
}
|
||||
|
||||
private static String getApprovalKey(String clientId, String userName) {
|
||||
return clientId + (userName == null ? "" : ":" + userName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAccessToken(OAuth2AccessToken accessToken) {
|
||||
removeAccessToken(accessToken.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessToken readAccessToken(String tokenValue) {
|
||||
byte[] key = serializeKey(ACCESS + tokenValue);
|
||||
byte[] bytes;
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
bytes = conn.get(key);
|
||||
}
|
||||
return deserializeAccessToken(bytes);
|
||||
}
|
||||
|
||||
public void removeAccessToken(String tokenValue) {
|
||||
byte[] accessKey = serializeKey(ACCESS + tokenValue);
|
||||
byte[] authKey = serializeKey(AUTH + tokenValue);
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
conn.openPipeline();
|
||||
conn.get(accessKey);
|
||||
conn.get(authKey);
|
||||
conn.del(accessKey);
|
||||
// Don't remove the refresh token - it's up to the caller to do that
|
||||
conn.del(authKey);
|
||||
List<Object> results = conn.closePipeline();
|
||||
byte[] access = (byte[]) results.get(0);
|
||||
byte[] auth = (byte[]) results.get(1);
|
||||
|
||||
OAuth2Authentication authentication = deserializeAuthentication(auth);
|
||||
if (authentication != null) {
|
||||
String key = authenticationKeyGenerator.extractKey(authentication);
|
||||
byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
|
||||
byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
|
||||
byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
|
||||
conn.openPipeline();
|
||||
conn.del(authToAccessKey);
|
||||
conn.zRem(unameKey, access);
|
||||
conn.zRem(clientId, access);
|
||||
conn.del(serialize(ACCESS + key));
|
||||
conn.closePipeline();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
|
||||
byte[] refreshKey = serializeKey(REFRESH + refreshToken.getValue());
|
||||
byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + refreshToken.getValue());
|
||||
byte[] serializedRefreshToken = serialize(refreshToken);
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
conn.openPipeline();
|
||||
if (springDataRedis_2_0) {
|
||||
try {
|
||||
this.redisConnectionSet_2_0.invoke(conn, refreshKey, serializedRefreshToken);
|
||||
this.redisConnectionSet_2_0.invoke(conn, refreshAuthKey, serialize(authentication));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
conn.set(refreshKey, serializedRefreshToken);
|
||||
conn.set(refreshAuthKey, serialize(authentication));
|
||||
}
|
||||
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
|
||||
ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
|
||||
Date expiration = expiringRefreshToken.getExpiration();
|
||||
if (expiration != null) {
|
||||
int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L).intValue();
|
||||
conn.expire(refreshKey, seconds);
|
||||
conn.expire(refreshAuthKey, seconds);
|
||||
}
|
||||
}
|
||||
conn.closePipeline();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2RefreshToken readRefreshToken(String tokenValue) {
|
||||
byte[] key = serializeKey(REFRESH + tokenValue);
|
||||
byte[] bytes;
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
bytes = conn.get(key);
|
||||
}
|
||||
return deserializeRefreshToken(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
|
||||
removeRefreshToken(refreshToken.getValue());
|
||||
}
|
||||
|
||||
public void removeRefreshToken(String tokenValue) {
|
||||
byte[] refreshKey = serializeKey(REFRESH + tokenValue);
|
||||
byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + tokenValue);
|
||||
byte[] refresh2AccessKey = serializeKey(REFRESH_TO_ACCESS + tokenValue);
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
conn.openPipeline();
|
||||
conn.del(refreshKey);
|
||||
conn.del(refreshAuthKey);
|
||||
conn.del(refresh2AccessKey);
|
||||
conn.closePipeline();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
|
||||
removeAccessTokenUsingRefreshToken(refreshToken.getValue());
|
||||
}
|
||||
|
||||
private void removeAccessTokenUsingRefreshToken(String refreshToken) {
|
||||
byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken);
|
||||
List<Object> results;
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
conn.openPipeline();
|
||||
conn.get(key);
|
||||
conn.del(key);
|
||||
results = conn.closePipeline();
|
||||
}
|
||||
byte[] bytes = (byte[]) results.get(0);
|
||||
String accessToken = deserializeString(bytes);
|
||||
if (accessToken != null) {
|
||||
removeAccessToken(accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
private List<byte[]> getZByteLists(byte[] key, RedisConnection conn) {
|
||||
// Sorted Set expiration maintenance
|
||||
long currentTime = System.currentTimeMillis();
|
||||
conn.zRemRangeByScore(key, 0, currentTime);
|
||||
|
||||
List<byte[]> byteList;
|
||||
Long size = conn.zCard(key);
|
||||
assert size != null;
|
||||
byteList = new ArrayList<>(size.intValue());
|
||||
Cursor<RedisZSetCommands.Tuple> cursor = conn.zScan(key, ScanOptions.NONE);
|
||||
|
||||
while (cursor.hasNext()) {
|
||||
RedisZSetCommands.Tuple t = cursor.next();
|
||||
|
||||
// Probably not necessary because of the maintenance at the beginning but why
|
||||
// not...
|
||||
if (t.getScore() == -1 || t.getScore() > currentTime) {
|
||||
byteList.add(t.getValue());
|
||||
}
|
||||
}
|
||||
return byteList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a maintenance of the RedisTokenStore.
|
||||
* <p>
|
||||
* SortedSets UNAME_TO_ACCESS and CLIENT_ID_TO_ACCESS contains access tokens that can
|
||||
* expire. This expiration is set as a score of the Redis SortedSet data structure.
|
||||
* Redis does not support expiration of items in a container data structure. It
|
||||
* supports only expiration of whole key. In case there is still new access tokens
|
||||
* being stored into the RedisTokenStore before whole key gets expired, the expiration
|
||||
* is prolonged and the key is not effectively deleted. To do "garbage collection"
|
||||
* this method should be called once upon a time.
|
||||
* @return how many items were removed
|
||||
*/
|
||||
public long doMaintenance() {
|
||||
long removed = 0;
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
// client_id_to_acccess maintenance
|
||||
Cursor<byte[]> clientToAccessKeys = conn.scan(ScanOptions.scanOptions()
|
||||
.match(keyStrResolver.extract(prefix + CLIENT_ID_TO_ACCESS + "*", StrUtil.COLON)).build());
|
||||
while (clientToAccessKeys.hasNext()) {
|
||||
byte[] clientIdToAccessKey = clientToAccessKeys.next();
|
||||
|
||||
removed += conn.zRemRangeByScore(clientIdToAccessKey, 0, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// uname_to_access maintenance
|
||||
Cursor<byte[]> unameToAccessKeys = conn.scan(ScanOptions.scanOptions()
|
||||
.match(keyStrResolver.extract(prefix + UNAME_TO_ACCESS + "*", StrUtil.COLON)).build());
|
||||
while (unameToAccessKeys.hasNext()) {
|
||||
byte[] unameToAccessKey = unameToAccessKeys.next();
|
||||
|
||||
removed += conn.zRemRangeByScore(unameToAccessKey, 0, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
|
||||
byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName));
|
||||
List<byte[]> byteList;
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
byteList = getZByteLists(approvalKey, conn);
|
||||
}
|
||||
if (byteList.size() == 0) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
List<OAuth2AccessToken> accessTokens = new ArrayList<>(byteList.size());
|
||||
for (byte[] bytes : byteList) {
|
||||
OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
|
||||
accessTokens.add(accessToken);
|
||||
}
|
||||
return Collections.unmodifiableCollection(accessTokens);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
|
||||
byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId);
|
||||
List<byte[]> byteList;
|
||||
try (RedisConnection conn = getConnection()) {
|
||||
byteList = getZByteLists(key, conn);
|
||||
}
|
||||
if (byteList.size() == 0) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
List<OAuth2AccessToken> accessTokens = new ArrayList<>(byteList.size());
|
||||
for (byte[] bytes : byteList) {
|
||||
OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
|
||||
accessTokens.add(accessToken);
|
||||
}
|
||||
return Collections.unmodifiableCollection(accessTokens);
|
||||
}
|
||||
|
||||
}
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.pig4cloud.pigx.common.security.handler.RestResponseErrorHandler;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/11/10
|
||||
*/
|
||||
@ComponentScan("com.pig4cloud.pigx.common.security")
|
||||
public class PigxResourceServerAutoConfiguration implements ApplicationContextAware {
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@LoadBalanced
|
||||
public RestTemplate lbRestTemplate() {
|
||||
// 获取上下文配置的ClientHttpRequestInterceptor 实现
|
||||
Map<String, ClientHttpRequestInterceptor> beansOfType = applicationContext
|
||||
.getBeansOfType(ClientHttpRequestInterceptor.class);
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setInterceptors(new ArrayList<>(beansOfType.values()));
|
||||
restTemplate.setErrorHandler(new RestResponseErrorHandler());
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持 #oauth2.xxx 校验注解
|
||||
* @return OAuth2MethodSecurityExpressionHandler
|
||||
*/
|
||||
@Bean
|
||||
public OAuth2MethodSecurityExpressionHandler expressionHandler() {
|
||||
OAuth2MethodSecurityExpressionHandler oAuth2MethodSecurityExpressionHandler = new OAuth2MethodSecurityExpressionHandler();
|
||||
oAuth2MethodSecurityExpressionHandler.setApplicationContext(applicationContext);
|
||||
return oAuth2MethodSecurityExpressionHandler;
|
||||
}
|
||||
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration;
|
||||
import org.springframework.cloud.commons.security.AccessTokenContextRelay;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019/4/14 注入AccessTokenContextRelay 解决feign 传递token 为空问题
|
||||
*/
|
||||
@Configuration
|
||||
@AutoConfigureAfter(OAuth2AutoConfiguration.class)
|
||||
@ConditionalOnWebApplication
|
||||
@ConditionalOnProperty("security.oauth2.client.client-id")
|
||||
public class PigxResourceServerTokenRelayAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public AccessTokenContextRelay accessTokenContextRelay(OAuth2ClientContext context) {
|
||||
return new AccessTokenContextRelay(context);
|
||||
}
|
||||
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Conditional(OAuth2OnClientInResourceServerCondition.class)
|
||||
@interface ConditionalOnOAuth2ClientInResourceServer {
|
||||
|
||||
}
|
||||
|
||||
private static class OAuth2OnClientInResourceServerCondition extends AllNestedConditions {
|
||||
|
||||
public OAuth2OnClientInResourceServerCondition() {
|
||||
super(ConfigurationPhase.REGISTER_BEAN);
|
||||
}
|
||||
|
||||
@ConditionalOnBean(ResourceServerConfiguration.class)
|
||||
static class Server {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnBean(OAuth2ClientConfiguration.class)
|
||||
static class Client {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018-11-24
|
||||
*/
|
||||
@Slf4j
|
||||
public class PigxSecurityBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
/**
|
||||
* 根据注解值动态注入资源服务器的相关属性
|
||||
* @param metadata 注解信息
|
||||
* @param registry 注册器
|
||||
*/
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
|
||||
if (registry.isBeanNameInUse(SecurityConstants.RESOURCE_SERVER_CONFIGURER)) {
|
||||
log.warn("本地存在资源服务器配置,覆盖默认配置:" + SecurityConstants.RESOURCE_SERVER_CONFIGURER);
|
||||
return;
|
||||
}
|
||||
|
||||
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
|
||||
beanDefinition.setBeanClass(PigxLocalResourceServerConfigurerAdapter.class);
|
||||
registry.registerBeanDefinition(SecurityConstants.RESOURCE_SERVER_CONFIGURER, beanDefinition);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
|
||||
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-04-28
|
||||
* <p>
|
||||
* 资源服务器 认证服务器 公用配置
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class PigxSecurityCommonAutoConfiguration {
|
||||
|
||||
private final KeyStrResolver resolver;
|
||||
|
||||
private final RedisConnectionFactory connectionFactory;
|
||||
|
||||
@Bean
|
||||
public TokenStore tokenStore() {
|
||||
PigxRedisTokenStore tokenStore = new PigxRedisTokenStore(connectionFactory, resolver);
|
||||
tokenStore.setPrefix(SecurityConstants.PIGX_PREFIX + SecurityConstants.OAUTH_PREFIX);
|
||||
tokenStore.setAuthenticationKeyGenerator(new DefaultAuthenticationKeyGenerator() {
|
||||
@Override
|
||||
public String extractKey(OAuth2Authentication authentication) {
|
||||
// 增加租户隔离部分 租户ID:原生计算值
|
||||
return resolver.extract(super.extractKey(authentication), StrUtil.COLON);
|
||||
}
|
||||
});
|
||||
return tokenStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证状态检查
|
||||
* @return UserDetailsChecker
|
||||
*/
|
||||
@Bean
|
||||
public UserDetailsChecker preAuthenticationChecks() {
|
||||
return new PigxPreAuthenticationChecks();
|
||||
}
|
||||
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.annotation.Inner;
|
||||
import com.pig4cloud.pigx.common.security.util.PigxSecurityMessageSourceUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/11/26
|
||||
* <p>
|
||||
* 服务间接口不鉴权处理逻辑
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@AllArgsConstructor
|
||||
public class PigxSecurityInnerAspect {
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
@SneakyThrows
|
||||
@Around("@within(inner) || @annotation(inner)")
|
||||
public Object around(ProceedingJoinPoint point, Inner inner) {
|
||||
// 先判断 inner 是否为空, 为空则获取类上注解
|
||||
if (inner == null) {
|
||||
Class<?> aClass = point.getTarget().getClass();
|
||||
inner = AnnotationUtils.findAnnotation(aClass, Inner.class);
|
||||
}
|
||||
|
||||
String header = request.getHeader(SecurityConstants.FROM);
|
||||
if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
|
||||
log.warn("访问接口 {} 没有权限", inner.value());
|
||||
throw new AccessDeniedException(PigxSecurityMessageSourceUtil.getAccessor().getMessage(
|
||||
"AbstractAccessDecisionManager.accessDenied", new Object[] { inner.value() }, "access denied"));
|
||||
}
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019-09-17
|
||||
* <p>
|
||||
* token增强,客户端模式不增强。
|
||||
*/
|
||||
public class PigxTokenEnhancer implements TokenEnhancer {
|
||||
|
||||
/**
|
||||
* Provides an opportunity for customization of an access token (e.g. through its
|
||||
* additional information map) during the process of creating a new token for use by a
|
||||
* client.
|
||||
* @param accessToken the current access token with its expiration and refresh token
|
||||
* @param authentication the current authentication including client and user details
|
||||
* @return a new token enhanced with additional information
|
||||
*/
|
||||
@Override
|
||||
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
|
||||
|
||||
final Map<String, Object> additionalInfo = new HashMap<>(8);
|
||||
String clientId = authentication.getOAuth2Request().getClientId();
|
||||
additionalInfo.put(SecurityConstants.CLIENT_ID, clientId);
|
||||
additionalInfo.put(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PIGX_LICENSE);
|
||||
additionalInfo.put(SecurityConstants.ACTIVE, Boolean.TRUE);
|
||||
|
||||
// 客户端模式不返回具体用户信息
|
||||
if (SecurityConstants.CLIENT_CREDENTIALS.equals(authentication.getOAuth2Request().getGrantType())) {
|
||||
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
PigxUser pigxUser = (PigxUser) authentication.getUserAuthentication().getPrincipal();
|
||||
additionalInfo.put(SecurityConstants.DETAILS_USER_ID, pigxUser.getId());
|
||||
additionalInfo.put(SecurityConstants.DETAILS_USERNAME, pigxUser.getUsername());
|
||||
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2021/10/3
|
||||
*/
|
||||
@Slf4j
|
||||
@EnableScheduling
|
||||
@ConditionalOnBean(AuthorizationServerConfigurerAdapter.class)
|
||||
public class PigxTokenStoreCleanSchedule {
|
||||
|
||||
@Scheduled(cron = "@hourly")
|
||||
public void doMaintenance() {
|
||||
PigxRedisTokenStore tokenStore = SpringContextHolder.getBean(PigxRedisTokenStore.class);
|
||||
long maintenance = tokenStore.doMaintenance();
|
||||
log.debug("清理Redis ZADD 过期 token 数量: {}", maintenance);
|
||||
}
|
||||
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import com.pig4cloud.pigx.common.security.exception.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;
|
||||
import org.springframework.security.oauth2.common.exceptions.ClientAuthenticationException;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
|
||||
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
|
||||
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
|
||||
import org.springframework.security.web.util.ThrowableAnalyzer;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020-06-20 OAutH Server 异常处理,重写oauth 默认实现
|
||||
*/
|
||||
@Slf4j
|
||||
public class PigxWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
|
||||
|
||||
private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
|
||||
|
||||
@Override
|
||||
public ResponseEntity<OAuth2Exception> translate(Exception e) {
|
||||
|
||||
// Try to extract a SpringSecurityException from the stacktrace
|
||||
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);
|
||||
|
||||
Exception ase = (AuthenticationException) throwableAnalyzer
|
||||
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
|
||||
if (ase != null) {
|
||||
return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e));
|
||||
}
|
||||
|
||||
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class,
|
||||
causeChain);
|
||||
if (ase != null) {
|
||||
return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase));
|
||||
}
|
||||
|
||||
ase = (InvalidGrantException) throwableAnalyzer.getFirstThrowableOfType(InvalidGrantException.class,
|
||||
causeChain);
|
||||
if (ase != null) {
|
||||
return handleOAuth2Exception(new InvalidException(ase.getLocalizedMessage(), ase));
|
||||
}
|
||||
|
||||
ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer
|
||||
.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain);
|
||||
if (ase != null) {
|
||||
return handleOAuth2Exception(new MethodNotAllowedException(ase.getMessage(), ase));
|
||||
}
|
||||
|
||||
// 处理不合法的令牌错误 424 返回
|
||||
ase = (InvalidTokenException) throwableAnalyzer.getFirstThrowableOfType(InvalidTokenException.class,
|
||||
causeChain);
|
||||
if (ase != null) {
|
||||
return handleOAuth2Exception(new TokenInvalidException(ase.getMessage(), ase));
|
||||
}
|
||||
|
||||
ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
|
||||
|
||||
if (ase != null) {
|
||||
return handleOAuth2Exception((OAuth2Exception) ase);
|
||||
}
|
||||
|
||||
return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
|
||||
|
||||
}
|
||||
|
||||
private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) {
|
||||
int status = e.getHttpErrorCode();
|
||||
// 客户端异常直接返回客户端,不然无法解析
|
||||
if (e instanceof ClientAuthenticationException) {
|
||||
return new ResponseEntity<>(e, HttpStatus.valueOf(status));
|
||||
}
|
||||
return new ResponseEntity<>(new PigxAuth2Exception(e.getMessage(), e.getOAuth2ErrorCode()),
|
||||
HttpStatus.valueOf(status));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+75
@@ -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.pig4cloud.pigx.common.security.component;
|
||||
|
||||
import cn.hutool.http.ContentType;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.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 javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @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));
|
||||
}
|
||||
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.exception;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxAuth2ExceptionSerializer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/7/8
|
||||
*/
|
||||
@JsonSerialize(using = PigxAuth2ExceptionSerializer.class)
|
||||
public class ForbiddenException extends PigxAuth2Exception {
|
||||
|
||||
public ForbiddenException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public ForbiddenException(String msg, Throwable t) {
|
||||
super(msg, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOAuth2ErrorCode() {
|
||||
return "access_denied";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpErrorCode() {
|
||||
return HttpStatus.FORBIDDEN.value();
|
||||
}
|
||||
|
||||
}
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.exception;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxAuth2ExceptionSerializer;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/7/8
|
||||
*/
|
||||
@JsonSerialize(using = PigxAuth2ExceptionSerializer.class)
|
||||
public class InvalidException extends PigxAuth2Exception {
|
||||
|
||||
public InvalidException(String msg, Throwable t) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOAuth2ErrorCode() {
|
||||
return "invalid_exception";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpErrorCode() {
|
||||
return 426;
|
||||
}
|
||||
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.exception;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxAuth2ExceptionSerializer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/7/8
|
||||
*/
|
||||
@JsonSerialize(using = PigxAuth2ExceptionSerializer.class)
|
||||
public class MethodNotAllowedException extends PigxAuth2Exception {
|
||||
|
||||
public MethodNotAllowedException(String msg, Throwable t) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOAuth2ErrorCode() {
|
||||
return "method_not_allowed";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpErrorCode() {
|
||||
return HttpStatus.METHOD_NOT_ALLOWED.value();
|
||||
}
|
||||
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.exception;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxAuth2ExceptionSerializer;
|
||||
import lombok.Getter;
|
||||
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/7/8 自定义OAuth2Exception
|
||||
*/
|
||||
@JsonSerialize(using = PigxAuth2ExceptionSerializer.class)
|
||||
public class PigxAuth2Exception extends OAuth2Exception {
|
||||
|
||||
@Getter
|
||||
private String errorCode;
|
||||
|
||||
public PigxAuth2Exception(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public PigxAuth2Exception(String msg, Throwable t) {
|
||||
super(msg, t);
|
||||
}
|
||||
|
||||
public PigxAuth2Exception(String msg, String errorCode) {
|
||||
super(msg);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.exception;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxAuth2ExceptionSerializer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/7/8
|
||||
*/
|
||||
@JsonSerialize(using = PigxAuth2ExceptionSerializer.class)
|
||||
public class ServerErrorException extends PigxAuth2Exception {
|
||||
|
||||
public ServerErrorException(String msg, Throwable t) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOAuth2ErrorCode() {
|
||||
return "server_error";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpErrorCode() {
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR.value();
|
||||
}
|
||||
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.exception;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxAuth2ExceptionSerializer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2021-08-05
|
||||
* <p>
|
||||
* 令牌不合法
|
||||
*/
|
||||
@JsonSerialize(using = PigxAuth2ExceptionSerializer.class)
|
||||
public class TokenInvalidException extends PigxAuth2Exception {
|
||||
|
||||
public TokenInvalidException(String msg, Throwable t) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOAuth2ErrorCode() {
|
||||
return "invalid_token";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpErrorCode() {
|
||||
return HttpStatus.FAILED_DEPENDENCY.value();
|
||||
}
|
||||
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.exception;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.pig4cloud.pigx.common.security.component.PigxAuth2ExceptionSerializer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/7/8
|
||||
*/
|
||||
@JsonSerialize(using = PigxAuth2ExceptionSerializer.class)
|
||||
public class UnauthorizedException extends PigxAuth2Exception {
|
||||
|
||||
public UnauthorizedException(String msg, Throwable t) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOAuth2ErrorCode() {
|
||||
return "unauthorized";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHttpErrorCode() {
|
||||
return HttpStatus.UNAUTHORIZED.value();
|
||||
}
|
||||
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.pig4cloud.pigx.common.security.feign;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
|
||||
public class PigFeignClientConfiguration {
|
||||
|
||||
/**
|
||||
* 注入 oauth2 feign token 增强
|
||||
* @param tokenResolver token获取处理器
|
||||
* @return 拦截器
|
||||
*/
|
||||
@Bean
|
||||
public RequestInterceptor oauthRequestInterceptor(BearerTokenResolver tokenResolver) {
|
||||
return new PigOAuthRequestInterceptor(tokenResolver);
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.pig4cloud.pigx.common.security.feign;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.RequestTemplate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* oauth2 feign token传递
|
||||
*
|
||||
* 重新 OAuth2FeignRequestInterceptor ,官方实现部分常见不适用
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2022/5/29
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PigOAuthRequestInterceptor implements RequestInterceptor {
|
||||
|
||||
private final BearerTokenResolver tokenResolver;
|
||||
|
||||
/**
|
||||
* Create a template with the header of provided name and extracted extract </br>
|
||||
*
|
||||
* 1. 如果使用 非web 请求,header 区别 </br>
|
||||
*
|
||||
* 2. 根据authentication 还原请求token
|
||||
* @param template
|
||||
*/
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM);
|
||||
// 带from 请求直接跳过
|
||||
if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 非web 请求直接跳过
|
||||
if (WebUtils.getRequest() == null) {
|
||||
return;
|
||||
}
|
||||
HttpServletRequest request = WebUtils.getRequest();
|
||||
// 避免请求参数的 query token 无法传递
|
||||
String token = tokenResolver.resolve(request);
|
||||
if (StrUtil.isBlank(token)) {
|
||||
return;
|
||||
}
|
||||
template.header(HttpHeaders.AUTHORIZATION,
|
||||
String.format("%s %s", OAuth2AccessToken.TokenType.BEARER.getValue(), token));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.grant;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsChecker;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @author hzq
|
||||
* @since 2021-09-14
|
||||
*/
|
||||
@Slf4j
|
||||
public class CustomAppAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
|
||||
|
||||
/**
|
||||
* user 属性校验
|
||||
*/
|
||||
@Setter
|
||||
private UserDetailsChecker preAuthenticationChecks = new AccountStatusUserDetailsChecker();
|
||||
|
||||
/**
|
||||
* 校验 请求信息userDetails
|
||||
* @param userDetails 用户信息
|
||||
* @param authentication 认证信息
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
@Override
|
||||
protected void additionalAuthenticationChecks(UserDetails userDetails,
|
||||
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
|
||||
|
||||
if (authentication.getCredentials() == null) {
|
||||
this.logger.debug("Failed to authenticate since no credentials provided");
|
||||
throw new BadCredentialsException(this.messages
|
||||
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
|
||||
if (authentication.getCredentials() == null) {
|
||||
log.debug("Failed to authenticate since no credentials provided");
|
||||
throw new BadCredentialsException("Bad credentials");
|
||||
}
|
||||
|
||||
CustomAppAuthenticationToken requestToken = (CustomAppAuthenticationToken) authentication;
|
||||
// 此处已获得 客户端认证 获取对应 userDetailsService
|
||||
Authentication clientAuthentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String clientId = clientAuthentication.getName();
|
||||
|
||||
Map<String, PigxUserDetailsService> userDetailsServiceMap = SpringContextHolder
|
||||
.getBeansOfType(PigxUserDetailsService.class);
|
||||
Optional<PigxUserDetailsService> optional = userDetailsServiceMap.values().stream()
|
||||
.filter(service -> service.support(clientId, requestToken.getGrantType()))
|
||||
.max(Comparator.comparingInt(Ordered::getOrder));
|
||||
|
||||
if (!optional.isPresent()) {
|
||||
throw new InternalAuthenticationServiceException("UserDetailsService error , not register");
|
||||
}
|
||||
|
||||
// 手机号
|
||||
String phone = authentication.getName();
|
||||
UserDetails userDetails = optional.get().loadUserByUsername(phone);
|
||||
|
||||
// userDeails 校验
|
||||
preAuthenticationChecks.check(userDetails);
|
||||
|
||||
CustomAppAuthenticationToken token = new CustomAppAuthenticationToken(userDetails);
|
||||
token.setDetails(authentication.getDetails());
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UserDetails retrieveUser(String phone, UsernamePasswordAuthenticationToken authentication)
|
||||
throws AuthenticationException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return authentication.isAssignableFrom(CustomAppAuthenticationToken.class);
|
||||
}
|
||||
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.grant;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* @author hzq
|
||||
* @since 2021-09-14
|
||||
*/
|
||||
public class CustomAppAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private final Object principal;
|
||||
|
||||
/**
|
||||
* 验证码/密码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 授权类型
|
||||
*/
|
||||
@Getter
|
||||
private String grantType;
|
||||
|
||||
public CustomAppAuthenticationToken(String phone, String code, String grantType) {
|
||||
super(AuthorityUtils.NO_AUTHORITIES);
|
||||
this.principal = phone;
|
||||
this.code = code;
|
||||
this.grantType = grantType;
|
||||
}
|
||||
|
||||
public CustomAppAuthenticationToken(UserDetails sysUser) {
|
||||
super(sysUser.getAuthorities());
|
||||
this.principal = sysUser;
|
||||
super.setAuthenticated(true); // 设置认证成功 必须
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
}
|
||||
-78
@@ -1,78 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.grant;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AccountStatusException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
|
||||
import org.springframework.security.oauth2.provider.*;
|
||||
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
|
||||
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 资源所有者电话令牌授予者
|
||||
*
|
||||
* @author hzq
|
||||
* @since 2021-09-14
|
||||
*/
|
||||
public class ResourceOwnerCustomeAppTokenGranter extends AbstractTokenGranter {
|
||||
|
||||
private static final String GRANT_TYPE = "mobile";
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
|
||||
public ResourceOwnerCustomeAppTokenGranter(AuthenticationManager authenticationManager,
|
||||
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
|
||||
OAuth2RequestFactory requestFactory) {
|
||||
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
|
||||
}
|
||||
|
||||
protected ResourceOwnerCustomeAppTokenGranter(AuthenticationManager authenticationManager,
|
||||
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
|
||||
OAuth2RequestFactory requestFactory, String grantType) {
|
||||
super(tokenServices, clientDetailsService, requestFactory, grantType);
|
||||
this.authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
|
||||
|
||||
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
|
||||
|
||||
// 手机号
|
||||
String mobile = parameters.get("mobile");
|
||||
// 验证码/密码
|
||||
String code = parameters.get("code");
|
||||
|
||||
if (StrUtil.isBlank(mobile) || StrUtil.isBlank(code)) {
|
||||
throw new InvalidGrantException("Bad credentials [ params must be has phone with code ]");
|
||||
}
|
||||
|
||||
// Protect from downstream leaks of code
|
||||
parameters.remove("code");
|
||||
|
||||
Authentication userAuth = new CustomAppAuthenticationToken(mobile, code, tokenRequest.getGrantType());
|
||||
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
|
||||
try {
|
||||
userAuth = authenticationManager.authenticate(userAuth);
|
||||
}
|
||||
catch (AccountStatusException | BadCredentialsException ase) {
|
||||
// covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
|
||||
throw new InvalidGrantException(ase.getMessage());
|
||||
}
|
||||
// If the phone/code are wrong the spec says we should send 400/invalid grant
|
||||
|
||||
if (userAuth == null || !userAuth.isAuthenticated()) {
|
||||
throw new InvalidGrantException("Could not authenticate user: " + mobile);
|
||||
}
|
||||
|
||||
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
|
||||
return new OAuth2Authentication(storedOAuth2Request, userAuth);
|
||||
}
|
||||
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
package com.pig4cloud.pigx.common.security.handler;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxCustomTokenServices;
|
||||
import com.pig4cloud.pigx.common.security.util.AuthUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
|
||||
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
|
||||
import org.springframework.security.oauth2.provider.*;
|
||||
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/8/5 手机号登录成功,返回oauth token
|
||||
*/
|
||||
@Slf4j
|
||||
public class MobileLoginSuccessHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
private static final String BASIC_ = "Basic ";
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private ClientDetailsService clientDetailsService;
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
private PigxCustomTokenServices tokenServices;
|
||||
|
||||
/**
|
||||
* Called when a user has been successfully authenticated. 调用spring security oauth API
|
||||
* 生成 oAuth2AccessToken
|
||||
* @param request the request which caused the successful authentication
|
||||
* @param response the response
|
||||
* @param authentication the <tt>Authentication</tt> object which was created during
|
||||
*/
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
if (header == null || !header.startsWith(BASIC_)) {
|
||||
throw new UnapprovedClientAuthenticationException("请求头中client信息为空");
|
||||
}
|
||||
|
||||
try {
|
||||
String[] tokens = AuthUtils.extractAndDecodeHeader(header);
|
||||
assert tokens.length == 2;
|
||||
String clientId = tokens[0];
|
||||
|
||||
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
|
||||
|
||||
// 校验secret
|
||||
if (!passwordEncoder.matches(tokens[1], clientDetails.getClientSecret())) {
|
||||
throw new InvalidClientException("Given client ID does not match authenticated client");
|
||||
|
||||
}
|
||||
|
||||
TokenRequest tokenRequest = new TokenRequest(MapUtil.newHashMap(), clientId, clientDetails.getScope(),
|
||||
"mobile");
|
||||
|
||||
// 校验scope
|
||||
new DefaultOAuth2RequestValidator().validateScope(tokenRequest, clientDetails);
|
||||
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
|
||||
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
|
||||
OAuth2AccessToken oAuth2AccessToken = tokenServices.createAccessToken(oAuth2Authentication);
|
||||
log.info("获取token 成功:{}", oAuth2AccessToken.getValue());
|
||||
|
||||
response.setCharacterEncoding(CharsetUtil.UTF_8);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
PrintWriter printWriter = response.getWriter();
|
||||
printWriter.append(objectMapper.writeValueAsString(oAuth2AccessToken));
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new BadCredentialsException("Failed to decode basic authentication token");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.handler;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.web.client.DefaultResponseErrorHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/4/29
|
||||
* <p>
|
||||
* 重写默认的 响应失败处理器,400 不作为异常
|
||||
*/
|
||||
public class RestResponseErrorHandler extends DefaultResponseErrorHandler {
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
if (response.getRawStatusCode() != HttpStatus.BAD_REQUEST.value()) {
|
||||
super.handleError(response);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.interceptor;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/4/29
|
||||
* <p>
|
||||
* 传递accpet 请求头,避免 请求返回 XML
|
||||
*/
|
||||
@Component
|
||||
public class AccpetRequestInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
|
||||
throws IOException {
|
||||
request.getHeaders().set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
|
||||
return execution.execute(request, body);
|
||||
}
|
||||
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.interceptor;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import feign.RequestTemplate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cloud.commons.security.AccessTokenContextRelay;
|
||||
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
|
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018/8/13 扩展OAuth2FeignRequestInterceptor
|
||||
*/
|
||||
@Slf4j
|
||||
public class PigxFeignClientInterceptor extends OAuth2FeignRequestInterceptor {
|
||||
|
||||
private final OAuth2ClientContext oAuth2ClientContext;
|
||||
|
||||
private final AccessTokenContextRelay accessTokenContextRelay;
|
||||
|
||||
/**
|
||||
* Default constructor which uses the provided OAuth2ClientContext and Bearer tokens
|
||||
* within Authorization header
|
||||
* @param oAuth2ClientContext provided context
|
||||
* @param resource type of resource to be accessed
|
||||
* @param accessTokenContextRelay
|
||||
*/
|
||||
public PigxFeignClientInterceptor(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails resource,
|
||||
AccessTokenContextRelay accessTokenContextRelay) {
|
||||
super(oAuth2ClientContext, resource);
|
||||
this.oAuth2ClientContext = oAuth2ClientContext;
|
||||
this.accessTokenContextRelay = accessTokenContextRelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a template with the header of provided name and extracted extract 1. 如果使用
|
||||
* 非web 请求,header 区别 2. 根据authentication 还原请求token
|
||||
* @param template
|
||||
*/
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM);
|
||||
if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
accessTokenContextRelay.copyToken();
|
||||
if (oAuth2ClientContext != null && oAuth2ClientContext.getAccessToken() != null) {
|
||||
super.apply(template);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.interceptor;
|
||||
|
||||
import feign.Feign;
|
||||
import feign.RequestInterceptor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.commons.security.AccessTokenContextRelay;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
|
||||
|
||||
/**
|
||||
* fegin 配置增强
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(Feign.class)
|
||||
public class PigxFeignConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty("security.oauth2.client.client-id")
|
||||
public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
|
||||
OAuth2ProtectedResourceDetails resource, AccessTokenContextRelay accessTokenContextRelay) {
|
||||
return new PigxFeignClientInterceptor(oAuth2ClientContext, resource, accessTokenContextRelay);
|
||||
}
|
||||
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.listener;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.pig4cloud.pigx.common.security.handler.AuthenticationFailureHandler;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
|
||||
import org.springframework.security.authentication.event.AuthenticationFailureProviderNotFoundEvent;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/03/25 认证失败事件监听器
|
||||
*/
|
||||
public class AuthenticationFailureEventListener implements ApplicationListener<AbstractAuthenticationFailureEvent> {
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<AuthenticationFailureHandler> failureHandlerList;
|
||||
|
||||
/**
|
||||
* Handle an application event.
|
||||
* @param event the event to respond to
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(AbstractAuthenticationFailureEvent event) {
|
||||
// 此类型事件不传递处理 #2386
|
||||
if (event instanceof AuthenticationFailureProviderNotFoundEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes();
|
||||
HttpServletRequest request = requestAttributes.getRequest();
|
||||
HttpServletResponse response = requestAttributes.getResponse();
|
||||
|
||||
AuthenticationException authenticationException = event.getException();
|
||||
Authentication authentication = (Authentication) event.getSource();
|
||||
|
||||
// 调用自定义业务链实现
|
||||
if (CollUtil.isNotEmpty(failureHandlerList)) {
|
||||
failureHandlerList.forEach(failureHandler -> failureHandler.handle(authenticationException, authentication,
|
||||
request, response));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.listener;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.pig4cloud.pigx.common.security.handler.AuthenticationLogoutHandler;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.security.authentication.event.LogoutSuccessEvent;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2021/06/22 退出事件监听器
|
||||
*/
|
||||
public class AuthenticationLogoutSuccessEventListener implements ApplicationListener<LogoutSuccessEvent> {
|
||||
|
||||
@Autowired(required = false)
|
||||
private AuthenticationLogoutHandler logoutHandler;
|
||||
|
||||
/**
|
||||
* Handle an application event.
|
||||
* @param event the event to respond to
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(LogoutSuccessEvent event) {
|
||||
// 健壮性判断
|
||||
if (!(event.getSource() instanceof OAuth2Authentication)) {
|
||||
return;
|
||||
}
|
||||
|
||||
OAuth2Authentication authentication = (OAuth2Authentication) event.getSource();
|
||||
if (logoutHandler != null && isUserAuthentication(authentication)) {
|
||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes();
|
||||
HttpServletRequest request = requestAttributes.getRequest();
|
||||
HttpServletResponse response = requestAttributes.getResponse();
|
||||
|
||||
logoutHandler.handle(authentication, request, response);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isUserAuthentication(Authentication authentication) {
|
||||
return authentication.getPrincipal() instanceof PigxUser
|
||||
|| CollUtil.isNotEmpty(authentication.getAuthorities());
|
||||
}
|
||||
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.common.security.listener;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.pig4cloud.pigx.common.security.handler.AuthenticationSuccessHandler;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/03/25 认证成功事件监听器
|
||||
*/
|
||||
public class AuthenticationSuccessEventListener implements ApplicationListener<AuthenticationSuccessEvent> {
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<AuthenticationSuccessHandler> successHandlerList;
|
||||
|
||||
/**
|
||||
* Handle an application event.
|
||||
* @param event the event to respond to
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(AuthenticationSuccessEvent event) {
|
||||
Authentication authentication = (Authentication) event.getSource();
|
||||
if (CollUtil.isNotEmpty(successHandlerList) && isUserAuthentication(authentication)) {
|
||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes();
|
||||
HttpServletRequest request = requestAttributes.getRequest();
|
||||
HttpServletResponse response = requestAttributes.getResponse();
|
||||
successHandlerList.forEach(successHandler -> successHandler.handle(authentication, request, response));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isUserAuthentication(Authentication authentication) {
|
||||
return authentication.getPrincipal() instanceof PigxUser
|
||||
|| CollUtil.isNotEmpty(authentication.getAuthorities());
|
||||
}
|
||||
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.pig4cloud.pigx.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 PigRedisOAuth2AuthorizationConsentService 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());
|
||||
}
|
||||
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package com.pig4cloud.pigx.common.security.service;
|
||||
|
||||
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.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.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022/5/27
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class PigRedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
|
||||
|
||||
private final static Long TIMEOUT = 10L;
|
||||
|
||||
private static final String AUTHORIZATION = "token";
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@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()));
|
||||
}
|
||||
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", 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());
|
||||
}
|
||||
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package com.pig4cloud.pigx.common.security.service;
|
||||
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysOauthClientDetails;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteClientDetailsService;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.RetOps;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
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.Optional;
|
||||
|
||||
/**
|
||||
* 查询客户端相关信息实现
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2022/5/29
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class PigRemoteRegisteredClientRepository implements RegisteredClientRepository {
|
||||
|
||||
/**
|
||||
* 刷新令牌有效期默认 30 天
|
||||
*/
|
||||
private final static int refreshTokenValiditySeconds = 60 * 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* 请求令牌有效期默认 12 小时
|
||||
*/
|
||||
private final static int accessTokenValiditySeconds = 60 * 60 * 12;
|
||||
|
||||
private final RemoteClientDetailsService clientDetailsService;
|
||||
|
||||
/**
|
||||
* Saves the registered client.
|
||||
*
|
||||
* <p>
|
||||
* IMPORTANT: Sensitive information should be encoded externally from the
|
||||
* implementation, e.g. {@link RegisteredClient#getClientSecret()}
|
||||
* @param registeredClient the {@link RegisteredClient}
|
||||
*/
|
||||
@Override
|
||||
public void save(RegisteredClient registeredClient) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the registered client identified by the provided {@code id}, or
|
||||
* {@code null} if not found.
|
||||
* @param id the registration identifier
|
||||
* @return the {@link RegisteredClient} if found, otherwise {@code null}
|
||||
*/
|
||||
@Override
|
||||
public RegisteredClient findById(String id) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the registered client identified by the provided {@code clientId}, or
|
||||
* {@code null} if not found.
|
||||
* @param clientId the client identifier
|
||||
* @return the {@link RegisteredClient} if found, otherwise {@code null}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 重写原生方法支持redis缓存
|
||||
* @param clientId
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
// @Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless =
|
||||
// "#result == null")
|
||||
public RegisteredClient findByClientId(String clientId) {
|
||||
|
||||
SysOauthClientDetails clientDetails = RetOps
|
||||
.of(clientDetailsService.getClientDetailsById(clientId, SecurityConstants.FROM_IN)).getData()
|
||||
.orElseThrow(() -> new OAuth2AuthorizationCodeRequestAuthenticationException(
|
||||
new OAuth2Error("客户端查询异常,请检查数据库链接"), null));
|
||||
|
||||
RegisteredClient.Builder builder = RegisteredClient.withId(clientDetails.getClientId())
|
||||
.clientId(clientDetails.getClientId())
|
||||
.clientSecret(SecurityConstants.NOOP + clientDetails.getClientSecret())
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
|
||||
// 授权模式
|
||||
Arrays.stream(clientDetails.getAuthorizedGrantTypes())
|
||||
.forEach(grant -> builder.authorizationGrantType(new AuthorizationGrantType(grant)));
|
||||
|
||||
// 回调地址
|
||||
Optional.ofNullable(clientDetails.getWebServerRedirectUri()).ifPresent(redirectUri -> Arrays
|
||||
.stream(redirectUri.split(StrUtil.COMMA)).filter(StrUtil::isNotBlank).forEach(builder::redirectUri));
|
||||
|
||||
// scope
|
||||
Optional.ofNullable(clientDetails.getScope()).ifPresent(
|
||||
scope -> Arrays.stream(scope.split(StrUtil.COMMA)).filter(StrUtil::isNotBlank).forEach(builder::scope));
|
||||
|
||||
return builder
|
||||
.tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE)
|
||||
.accessTokenTimeToLive(Duration.ofSeconds(Optional
|
||||
.ofNullable(clientDetails.getAccessTokenValidity()).orElse(accessTokenValiditySeconds)))
|
||||
.refreshTokenTimeToLive(
|
||||
Duration.ofSeconds(Optional.ofNullable(clientDetails.getRefreshTokenValidity())
|
||||
.orElse(refreshTokenValiditySeconds)))
|
||||
.build())
|
||||
.clientSettings(ClientSettings.builder()
|
||||
.requireAuthorizationConsent(!BooleanUtil.toBoolean(clientDetails.getAutoapprove())).build())
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.service;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysOauthClientDetails;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteClientDetailsService;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.RetOps;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
|
||||
import org.springframework.security.oauth2.provider.ClientDetails;
|
||||
import org.springframework.security.oauth2.provider.ClientDetailsService;
|
||||
import org.springframework.security.oauth2.provider.NoSuchClientException;
|
||||
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2020/03/25
|
||||
* <p>
|
||||
* 扩展 JdbcClientDetailsService 支持多租户
|
||||
*/
|
||||
@Slf4j
|
||||
@Primary
|
||||
@RequiredArgsConstructor
|
||||
public class PigxClientDetailsServiceImpl implements ClientDetailsService {
|
||||
|
||||
private final RemoteClientDetailsService clientDetailsService;
|
||||
|
||||
/**
|
||||
* 重写原生方法支持redis缓存
|
||||
* @param clientId
|
||||
* @return ClientDetails
|
||||
* @throws InvalidClientException
|
||||
*/
|
||||
@Override
|
||||
@Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null")
|
||||
public ClientDetails loadClientByClientId(String clientId) {
|
||||
// 适配成oauth2内置类型
|
||||
return RetOps.of(clientDetailsService.getClientDetailsById(clientId, SecurityConstants.FROM_IN)).getData()
|
||||
.map(this::clientDetailsWrapper)
|
||||
.orElseThrow(() -> new NoSuchClientException("No client with requested id: " + clientId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端类型转化 参考
|
||||
* {@link org.springframework.security.oauth2.provider.client.JdbcClientDetailsService}
|
||||
* @param origin 数据库保存参数
|
||||
* @return target oauth 类型客户端参数
|
||||
*/
|
||||
private ClientDetails clientDetailsWrapper(SysOauthClientDetails origin) {
|
||||
BaseClientDetails target = new BaseClientDetails();
|
||||
// 必选项
|
||||
target.setClientId(origin.getClientId());
|
||||
target.setClientSecret(String.format("{noop}%s", origin.getClientSecret()));
|
||||
|
||||
if (ArrayUtil.isNotEmpty(origin.getAuthorizedGrantTypes())) {
|
||||
target.setAuthorizedGrantTypes(CollUtil.newArrayList(origin.getAuthorizedGrantTypes()));
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(origin.getAuthorities())) {
|
||||
target.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(origin.getAuthorities()));
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(origin.getResourceIds())) {
|
||||
target.setResourceIds(StringUtils.commaDelimitedListToSet(origin.getResourceIds()));
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(origin.getWebServerRedirectUri())) {
|
||||
target.setRegisteredRedirectUri(StringUtils.commaDelimitedListToSet(origin.getWebServerRedirectUri()));
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(origin.getScope())) {
|
||||
target.setScope(StringUtils.commaDelimitedListToSet(origin.getScope()));
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(origin.getAutoapprove())) {
|
||||
target.setAutoApproveScopes(StringUtils.commaDelimitedListToSet(origin.getAutoapprove()));
|
||||
}
|
||||
|
||||
if (origin.getAccessTokenValidity() != null) {
|
||||
target.setAccessTokenValiditySeconds(origin.getAccessTokenValidity());
|
||||
}
|
||||
|
||||
if (origin.getRefreshTokenValidity() != null) {
|
||||
target.setRefreshTokenValiditySeconds(origin.getRefreshTokenValidity());
|
||||
}
|
||||
|
||||
String json = origin.getAdditionalInformation();
|
||||
if (StrUtil.isNotBlank(json)) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> additionalInformation = JSONUtil.toBean(json, Map.class);
|
||||
target.setAdditionalInformation(additionalInformation);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("Could not decode JSON for additional information: " + json, e);
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
}
|
||||
-391
@@ -1,391 +0,0 @@
|
||||
package com.pig4cloud.pigx.common.security.service;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.common.*;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidScopeException;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
|
||||
import org.springframework.security.oauth2.provider.*;
|
||||
import org.springframework.security.oauth2.provider.token.*;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 自定义 token 放发处理逻辑
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2021/10/15
|
||||
*/
|
||||
@Slf4j
|
||||
public class PigxCustomTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
|
||||
ConsumerTokenServices, InitializingBean {
|
||||
|
||||
private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.
|
||||
|
||||
private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.
|
||||
|
||||
private int defaultOnlineQuantity = 0; // 客户端默认同时在线数量
|
||||
|
||||
private boolean supportRefreshToken = false;
|
||||
|
||||
private boolean reuseRefreshToken = true;
|
||||
|
||||
private TokenStore tokenStore;
|
||||
|
||||
private ClientDetailsService clientDetailsService;
|
||||
|
||||
private TokenEnhancer accessTokenEnhancer;
|
||||
|
||||
/**
|
||||
* Initialize these token services. If no random generator is set, one will be
|
||||
* created.
|
||||
*/
|
||||
public void afterPropertiesSet() {
|
||||
Assert.notNull(tokenStore, "tokenStore must be set");
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
|
||||
OAuth2RefreshToken refreshToken = null;
|
||||
// 若已产生token , 过期时删除相关token,执行下边的重新生成逻辑
|
||||
if (existingAccessToken != null) {
|
||||
int onlineQuantity = getOnlineQuantity(authentication.getOAuth2Request());
|
||||
// (0)默认情况下支持同时在线
|
||||
if (onlineQuantity == defaultOnlineQuantity) {
|
||||
if (existingAccessToken.isExpired()) {
|
||||
if (existingAccessToken.getRefreshToken() != null) {
|
||||
refreshToken = existingAccessToken.getRefreshToken();
|
||||
tokenStore.removeRefreshToken(refreshToken);
|
||||
}
|
||||
tokenStore.removeAccessToken(existingAccessToken);
|
||||
}
|
||||
else {
|
||||
// Re-store the access token in case the authentication has changed
|
||||
tokenStore.storeAccessToken(existingAccessToken, authentication);
|
||||
return existingAccessToken;
|
||||
}
|
||||
}
|
||||
|
||||
// 增加允许同时在线数量逻辑
|
||||
Collection<OAuth2AccessToken> currentTokenList = tokenStore.findTokensByClientIdAndUserName(
|
||||
authentication.getOAuth2Request().getClientId(), authentication.getName());
|
||||
if (currentTokenList.size() >= getOnlineQuantity(authentication.getOAuth2Request())) {
|
||||
Optional<OAuth2AccessToken> oldestTokenOpt = currentTokenList.stream()
|
||||
.min(Comparator.comparing(OAuth2AccessToken::getExpiration));
|
||||
if (oldestTokenOpt.isPresent()) {
|
||||
OAuth2AccessToken oldestToken = oldestTokenOpt.get();
|
||||
log.info("online quantity reaches the upper limit");
|
||||
tokenStore.removeAccessToken(oldestToken);
|
||||
if (oldestToken.getRefreshToken() != null) {
|
||||
refreshToken = oldestToken.getRefreshToken();
|
||||
tokenStore.removeRefreshToken(refreshToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshToken = createRefreshToken(authentication);
|
||||
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
|
||||
tokenStore.storeAccessToken(accessToken, authentication);
|
||||
|
||||
refreshToken = accessToken.getRefreshToken();
|
||||
if (refreshToken != null) {
|
||||
tokenStore.storeRefreshToken(refreshToken, authentication);
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
@Transactional(noRollbackFor = { InvalidTokenException.class, InvalidGrantException.class })
|
||||
public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
|
||||
throws AuthenticationException {
|
||||
|
||||
if (!supportRefreshToken) {
|
||||
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
|
||||
}
|
||||
|
||||
OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
|
||||
if (refreshToken == null) {
|
||||
throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
|
||||
}
|
||||
|
||||
OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
|
||||
String clientId = authentication.getOAuth2Request().getClientId();
|
||||
if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {
|
||||
throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
|
||||
}
|
||||
|
||||
// clear out any access tokens already associated with the refresh
|
||||
// token.
|
||||
tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
|
||||
|
||||
if (isExpired(refreshToken)) {
|
||||
tokenStore.removeRefreshToken(refreshToken);
|
||||
throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
|
||||
}
|
||||
|
||||
authentication = createRefreshedAuthentication(authentication, tokenRequest);
|
||||
|
||||
if (!reuseRefreshToken) {
|
||||
tokenStore.removeRefreshToken(refreshToken);
|
||||
refreshToken = createRefreshToken(authentication);
|
||||
}
|
||||
|
||||
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
|
||||
tokenStore.storeAccessToken(accessToken, authentication);
|
||||
if (!reuseRefreshToken) {
|
||||
tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
|
||||
return tokenStore.getAccessToken(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a refreshed authentication.
|
||||
* @param authentication The authentication.
|
||||
* @param request The scope for the refreshed token.
|
||||
* @return The refreshed authentication.
|
||||
* @throws InvalidScopeException If the scope requested is invalid or wider than the
|
||||
* original scope.
|
||||
*/
|
||||
private OAuth2Authentication createRefreshedAuthentication(OAuth2Authentication authentication,
|
||||
TokenRequest request) {
|
||||
OAuth2Authentication narrowed = authentication;
|
||||
Set<String> scope = request.getScope();
|
||||
OAuth2Request clientAuth = authentication.getOAuth2Request().refresh(request);
|
||||
if (scope != null && !scope.isEmpty()) {
|
||||
Set<String> originalScope = clientAuth.getScope();
|
||||
if (originalScope == null || !originalScope.containsAll(scope)) {
|
||||
throw new InvalidScopeException(
|
||||
"Unable to narrow the scope of the client authentication to " + scope + ".", originalScope);
|
||||
}
|
||||
else {
|
||||
clientAuth = clientAuth.narrowScope(scope);
|
||||
}
|
||||
}
|
||||
narrowed = new OAuth2Authentication(clientAuth, authentication.getUserAuthentication());
|
||||
return narrowed;
|
||||
}
|
||||
|
||||
protected boolean isExpired(OAuth2RefreshToken refreshToken) {
|
||||
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
|
||||
ExpiringOAuth2RefreshToken expiringToken = (ExpiringOAuth2RefreshToken) refreshToken;
|
||||
return expiringToken.getExpiration() == null
|
||||
|| System.currentTimeMillis() > expiringToken.getExpiration().getTime();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public OAuth2AccessToken readAccessToken(String accessToken) {
|
||||
return tokenStore.readAccessToken(accessToken);
|
||||
}
|
||||
|
||||
public OAuth2Authentication loadAuthentication(String accessTokenValue)
|
||||
throws AuthenticationException, InvalidTokenException {
|
||||
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
|
||||
if (accessToken == null) {
|
||||
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
|
||||
}
|
||||
else if (accessToken.isExpired()) {
|
||||
tokenStore.removeAccessToken(accessToken);
|
||||
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
|
||||
}
|
||||
|
||||
OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
|
||||
if (result == null) {
|
||||
// in case of race condition
|
||||
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
|
||||
}
|
||||
if (clientDetailsService != null) {
|
||||
String clientId = result.getOAuth2Request().getClientId();
|
||||
try {
|
||||
clientDetailsService.loadClientByClientId(clientId);
|
||||
}
|
||||
catch (ClientRegistrationException e) {
|
||||
throw new InvalidTokenException("Client not valid: " + clientId, e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getClientId(String tokenValue) {
|
||||
OAuth2Authentication authentication = tokenStore.readAuthentication(tokenValue);
|
||||
if (authentication == null) {
|
||||
throw new InvalidTokenException("Invalid access token: " + tokenValue);
|
||||
}
|
||||
OAuth2Request clientAuth = authentication.getOAuth2Request();
|
||||
if (clientAuth == null) {
|
||||
throw new InvalidTokenException("Invalid access token (no client id): " + tokenValue);
|
||||
}
|
||||
return clientAuth.getClientId();
|
||||
}
|
||||
|
||||
public boolean revokeToken(String tokenValue) {
|
||||
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
|
||||
if (accessToken == null) {
|
||||
return false;
|
||||
}
|
||||
if (accessToken.getRefreshToken() != null) {
|
||||
tokenStore.removeRefreshToken(accessToken.getRefreshToken());
|
||||
}
|
||||
tokenStore.removeAccessToken(accessToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
|
||||
if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
|
||||
return null;
|
||||
}
|
||||
int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
|
||||
String value = UUID.randomUUID().toString();
|
||||
if (validitySeconds > 0) {
|
||||
return new DefaultExpiringOAuth2RefreshToken(value,
|
||||
new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
|
||||
}
|
||||
return new DefaultOAuth2RefreshToken(value);
|
||||
}
|
||||
|
||||
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
|
||||
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
|
||||
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
|
||||
if (validitySeconds > 0) {
|
||||
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
|
||||
}
|
||||
token.setRefreshToken(refreshToken);
|
||||
token.setScope(authentication.getOAuth2Request().getScope());
|
||||
|
||||
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
|
||||
}
|
||||
|
||||
/**
|
||||
* The access token validity period in seconds
|
||||
* @param clientAuth the current authorization request
|
||||
* @return the access token validity period in seconds
|
||||
*/
|
||||
protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) {
|
||||
if (clientDetailsService != null) {
|
||||
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
|
||||
Integer validity = client.getAccessTokenValiditySeconds();
|
||||
if (validity != null && validity > 0) {
|
||||
return validity;
|
||||
}
|
||||
}
|
||||
return accessTokenValiditySeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* The refresh token validity period in seconds
|
||||
* @param clientAuth the current authorization request
|
||||
* @return the refresh token validity period in seconds
|
||||
*/
|
||||
protected int getRefreshTokenValiditySeconds(OAuth2Request clientAuth) {
|
||||
if (clientDetailsService != null) {
|
||||
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
|
||||
Integer validity = client.getRefreshTokenValiditySeconds();
|
||||
if (validity != null && validity > 0) {
|
||||
return validity;
|
||||
}
|
||||
}
|
||||
return refreshTokenValiditySeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* the client online quantity
|
||||
* @param clientAuth the current authorization request
|
||||
* @return the client online quantity
|
||||
*/
|
||||
protected int getOnlineQuantity(OAuth2Request clientAuth) {
|
||||
if (clientDetailsService != null) {
|
||||
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
|
||||
return MapUtil.getInt(client.getAdditionalInformation(), CommonConstants.ONLINE_QUANTITY, -1);
|
||||
}
|
||||
return defaultOnlineQuantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a refresh token supported for this client (or the global setting if
|
||||
* {@link #setClientDetailsService(ClientDetailsService) clientDetailsService} is not
|
||||
* set.
|
||||
* @param clientAuth the current authorization request
|
||||
* @return boolean to indicate if refresh token is supported
|
||||
*/
|
||||
protected boolean isSupportRefreshToken(OAuth2Request clientAuth) {
|
||||
if (clientDetailsService != null) {
|
||||
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
|
||||
return client.getAuthorizedGrantTypes().contains("refresh_token");
|
||||
}
|
||||
return this.supportRefreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* An access token enhancer that will be applied to a new token before it is saved in
|
||||
* the token store.
|
||||
* @param accessTokenEnhancer the access token enhancer to set
|
||||
*/
|
||||
public void setTokenEnhancer(TokenEnhancer accessTokenEnhancer) {
|
||||
this.accessTokenEnhancer = accessTokenEnhancer;
|
||||
}
|
||||
|
||||
/**
|
||||
* The validity (in seconds) of the refresh token. If less than or equal to zero then
|
||||
* the tokens will be non-expiring.
|
||||
* @param refreshTokenValiditySeconds The validity (in seconds) of the refresh token.
|
||||
*/
|
||||
public void setRefreshTokenValiditySeconds(int refreshTokenValiditySeconds) {
|
||||
this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default validity (in seconds) of the access token. Zero or negative for
|
||||
* non-expiring tokens. If a client details service is set the validity period will be
|
||||
* read from the client, defaulting to this value if not defined by the client.
|
||||
* @param accessTokenValiditySeconds The validity (in seconds) of the access token.
|
||||
*/
|
||||
public void setAccessTokenValiditySeconds(int accessTokenValiditySeconds) {
|
||||
this.accessTokenValiditySeconds = accessTokenValiditySeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to support the refresh token.
|
||||
* @param supportRefreshToken Whether to support the refresh token.
|
||||
*/
|
||||
public void setSupportRefreshToken(boolean supportRefreshToken) {
|
||||
this.supportRefreshToken = supportRefreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to reuse refresh tokens (until expired).
|
||||
* @param reuseRefreshToken Whether to reuse refresh tokens (until expired).
|
||||
*/
|
||||
public void setReuseRefreshToken(boolean reuseRefreshToken) {
|
||||
this.reuseRefreshToken = reuseRefreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* The persistence strategy for token storage.
|
||||
* @param tokenStore the store for access and refresh tokens.
|
||||
*/
|
||||
public void setTokenStore(TokenStore tokenStore) {
|
||||
this.tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* The client details service to use for looking up clients (if necessary). Optional
|
||||
* if the access token expiry is set globally via
|
||||
* {@link #setAccessTokenValiditySeconds(int)}.
|
||||
* @param clientDetailsService the client details service
|
||||
*/
|
||||
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
|
||||
this.clientDetailsService = clientDetailsService;
|
||||
}
|
||||
|
||||
}
|
||||
+4
-5
@@ -16,10 +16,7 @@
|
||||
|
||||
package com.pig4cloud.pigx.common.security.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.pig4cloud.pigx.admin.api.dto.UserInfo;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysUser;
|
||||
import com.pig4cloud.pigx.app.api.dto.AppUserInfo;
|
||||
import com.pig4cloud.pigx.app.api.entity.AppUser;
|
||||
import com.pig4cloud.pigx.app.api.feign.RemoteAppUserService;
|
||||
@@ -29,6 +26,7 @@ import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.enums.UserTypeEnum;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.core.util.RetOps;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -51,7 +49,7 @@ import java.util.Set;
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PigxAppUserDetailsServiceImpl implements PigxUserDetailsService {
|
||||
public class PigxTocDefaultUserDetailsServiceImpl implements PigxUserDetailsService {
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
@@ -129,7 +127,8 @@ public class PigxAppUserDetailsServiceImpl implements PigxUserDetailsService {
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String clientId, String grantType) {
|
||||
return SecurityConstants.CLIENT_MINI.equals(clientId);
|
||||
String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC);
|
||||
return SecurityConstants.HEADER_TOC_YES.equals(header);
|
||||
}
|
||||
|
||||
}
|
||||
+4
-5
@@ -26,6 +26,7 @@ import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.enums.UserTypeEnum;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.core.util.RetOps;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -34,7 +35,6 @@ import org.springframework.cache.CacheManager;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -49,9 +49,7 @@ import java.util.Set;
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PigxMobildAppUserDetailsServiceImpl implements PigxUserDetailsService {
|
||||
|
||||
private final UserDetailsService pigxAppUserDetailsServiceImpl;
|
||||
public class PigxTocMobileUserDetailsServiceImpl implements PigxUserDetailsService {
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
@@ -126,7 +124,8 @@ public class PigxMobildAppUserDetailsServiceImpl implements PigxUserDetailsServi
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String clientId, String grantType) {
|
||||
return SecurityConstants.CLIENT_MINI.equals(clientId) && SecurityConstants.GRANT_MOBILE.equals(grantType);
|
||||
String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC);
|
||||
return SecurityConstants.HEADER_TOC_YES.equals(header) && SecurityConstants.GRANT_MOBILE.equals(grantType);
|
||||
}
|
||||
|
||||
}
|
||||
+9
-1
@@ -24,14 +24,17 @@ 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 lengleng
|
||||
* @date 2020/4/16 扩展用户信息
|
||||
*/
|
||||
public class PigxUser extends User {
|
||||
public class PigxUser extends User implements OAuth2AuthenticatedPrincipal {
|
||||
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
|
||||
@@ -131,4 +134,9 @@ public class PigxUser extends User {
|
||||
this.userType = userType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
}
|
||||
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user