♻️ Refactoring code. 迁移 Spring Authorization Server

This commit is contained in:
lbw committed 2022-12-11 15:18:48 +08:00
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
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
@@ -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());
}
}
@@ -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();
}
}
@@ -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");
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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,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;
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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;
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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>
@@ -105,4 +105,9 @@ public interface CacheConstants {
*/
String LOGIN_ERROR_TIMES = "login_error_times";
/**
* oauth 缓存前缀
*/
String PROJECT_OAUTH_ACCESS = "token::access_token";
}
@@ -125,4 +125,14 @@ public interface CommonConstants {
*/
String REQUEST_START_TIME = "REQUEST-START-TIME";
/**
* 当前页
*/
String CURRENT = "current";
/**
* size
*/
String SIZE = "size";
}
@@ -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";
}
@@ -72,6 +72,7 @@ public enum LoginTypeEnum {
* 企业微信
*/
WEIXIN_CP("WEIXIN_CP", "企业微信"),
/**
* CAS 登录
*/
@@ -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);
}
}
@@ -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());
@@ -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()));
+11 -2
View File
@@ -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>
@@ -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 {
}
@@ -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)
@@ -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 {
/**
@@ -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, "*"))));
});
}
}
@@ -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);
}
}
}
@@ -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())));
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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);
}
}
@@ -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");
}
}
@@ -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"));
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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 {
}
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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));
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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");
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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));
}
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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