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

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

No files matched your search

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bsd.pjcloud.auth;
import com.bsd.pjcloud.common.feign.annotation.EnablePjcloudFeignClients;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @author pjcloud
* @date 2018年06月21日 认证授权中心
*/
@EnablePjcloudFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class PjcloudAuthApplication {
public static void main(String[] args) {
SpringApplication.run(PjcloudAuthApplication.class, args);
}
}
@@ -0,0 +1,186 @@
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bsd.pjcloud.auth.config;
import com.bsd.pjcloud.auth.support.CustomeOAuth2AccessTokenGenerator;
import com.bsd.pjcloud.auth.support.core.CustomeOAuth2TokenCustomizer;
import com.bsd.pjcloud.auth.support.core.FormIdentityLoginConfigurer;
import com.bsd.pjcloud.auth.support.core.PjcloudDaoAuthenticationProvider;
import com.bsd.pjcloud.auth.support.core.PjcloudOAuth2RefreshTokenAuthenticationConverter;
import com.bsd.pjcloud.auth.support.filter.PasswordDecoderFilter;
import com.bsd.pjcloud.auth.support.filter.ValidateCodeFilter;
import com.bsd.pjcloud.auth.support.handler.PjcloudAuthenticationFailureEventHandler;
import com.bsd.pjcloud.auth.support.handler.PjcloudAuthenticationSuccessEventHandler;
import com.bsd.pjcloud.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationConverter;
import com.bsd.pjcloud.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationProvider;
import com.bsd.pjcloud.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationConverter;
import com.bsd.pjcloud.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationProvider;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.security.handler.FormAuthenticationFailureHandler;
import com.bsd.pjcloud.common.security.handler.SsoLogoutSuccessHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
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.DelegatingAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
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.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.util.Arrays;
/**
* @author pjcloud
* @date 2022/5/27
* <p>
* 认证服务器配置
*/
@Configuration
@RequiredArgsConstructor
public class AuthorizationServerConfiguration {
private final OAuth2AuthorizationService authorizationService;
private final PasswordDecoderFilter passwordDecoderFilter;
private final ValidateCodeFilter validateCodeFilter;
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnProperty(value = "security.micro", matchIfMissing = true)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
PjcloudAuthenticationSuccessEventHandler successEventHandler,
PjcloudAuthenticationFailureEventHandler failureEventHandler) throws Exception {
// 增加验证码过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
// 增加密码解密过滤器
http.addFilterBefore(passwordDecoderFilter, UsernamePasswordAuthenticationFilter.class);
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
http.with(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)), Customizer.withDefaults());
AntPathRequestMatcher[] requestMatchers = new AntPathRequestMatcher[]{
AntPathRequestMatcher.antMatcher("/token/**"), AntPathRequestMatcher.antMatcher("/actuator/**"),
AntPathRequestMatcher.antMatcher("/code/**"), AntPathRequestMatcher.antMatcher("/css/**"),
AntPathRequestMatcher.antMatcher("/error")};
http.authorizeHttpRequests(authorizeRequests -> {
// 自定义接口、端点暴露
authorizeRequests.requestMatchers(requestMatchers).permitAll();
authorizeRequests.anyRequest().authenticated();
})
.with(authorizationServerConfigurer.authorizationService(authorizationService)// redis存储token的实现
.authorizationServerSettings(
AuthorizationServerSettings.builder().issuer(SecurityConstants.PIGX_LICENSE).build()), Customizer.withDefaults());
http.with(new FormIdentityLoginConfigurer(), Customizer.withDefaults());
DefaultSecurityFilterChain securityFilterChain = http.build();
// 注入自定义授权模式实现
addCustomOAuth2GrantAuthenticationProvider(http);
return securityFilterChain;
}
/**
* 令牌生成规则实现 </br>
* client:username:uuid
*
* @return OAuth2TokenGenerator
*/
@Bean
public OAuth2TokenGenerator oAuth2TokenGenerator() {
CustomeOAuth2AccessTokenGenerator accessTokenGenerator = new CustomeOAuth2AccessTokenGenerator();
// 注入Token 增加关联用户信息
accessTokenGenerator.setAccessTokenCustomizer(new CustomeOAuth2TokenCustomizer());
return new DelegatingOAuth2TokenGenerator(accessTokenGenerator, new OAuth2RefreshTokenGenerator());
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new FormAuthenticationFailureHandler();
}
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new SsoLogoutSuccessHandler();
}
/**
* request -> xToken 注入请求转换器
*
* @return DelegatingAuthenticationConverter
*/
@Bean
public AuthenticationConverter accessTokenRequestConverter() {
return new DelegatingAuthenticationConverter(Arrays.asList(
new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
new OAuth2ResourceOwnerSmsAuthenticationConverter(), new PjcloudOAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
}
/**
* 注入授权模式实现提供方
* <p>
* 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 PjcloudDaoAuthenticationProvider());
// 处理 OAuth2ResourceOwnerPasswordAuthenticationToken
http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider);
// 处理 OAuth2ResourceOwnerSmsAuthenticationToken
http.authenticationProvider(resourceOwnerSmsAuthenticationProvider);
}
}
@@ -0,0 +1,52 @@
package com.bsd.pjcloud.auth.endpoint;
import com.anji.captcha.service.CaptchaCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author pjcloud
* @date 2020/8/27
* <p>
* 验证码 缓存提供支持集群,需要通过SPI
*/
public class CaptchaCacheServiceProvider implements CaptchaCacheService {
private static final String REDIS = "redis";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void set(String key, String value, long expiresInSeconds) {
stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
}
@Override
public boolean exists(String key) {
return stringRedisTemplate.hasKey(key);
}
@Override
public void delete(String key) {
stringRedisTemplate.delete(key);
}
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public String type() {
return REDIS;
}
@Override
public Long increment(String key, long val) {
return stringRedisTemplate.opsForValue().increment(key, val);
}
}
@@ -0,0 +1,89 @@
package com.bsd.pjcloud.auth.endpoint;
import cn.hutool.core.lang.Validator;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import io.springboot.captcha.ArithmeticCaptcha;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* 验证码相关的接口
*
* @author pjcloud
* @date 2022/6/27
*/
@RestController
@RequestMapping("/code")
@RequiredArgsConstructor
public class ImageCodeEndpoint {
private static final Integer DEFAULT_IMAGE_WIDTH = 100;
private static final Integer DEFAULT_IMAGE_HEIGHT = 40;
private final StringRedisTemplate redisTemplate;
/**
* 创建图形验证码
*/
@SneakyThrows
@GetMapping("/image")
public void image(String randomStr, HttpServletResponse response) {
ArithmeticCaptcha captcha = new ArithmeticCaptcha(DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT);
String result = captcha.text();
// 如果是手机号码,不存储下发验证码
if (Validator.isMobile(randomStr)) {
return;
}
redisTemplate.opsForValue()
.set(CacheConstants.DEFAULT_CODE_KEY + randomStr, result, SecurityConstants.CODE_TIME, TimeUnit.SECONDS);
// 转换流信息写出
captcha.out(response.getOutputStream());
}
/**
* 行为 创建滑块图形验证码
*/
@SneakyThrows
@GetMapping("/create")
public R behavior() {
CaptchaVO vo = new CaptchaVO();
vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
return R.ok(captchaService.get(vo));
}
/**
* 行为 创建滑块图形验证码
*/
@SneakyThrows
@PostMapping("/check")
public R check(HttpServletRequest request) {
CaptchaVO vo = new CaptchaVO();
vo.setPointJson(request.getParameter("pointJson"));
vo.setToken(request.getParameter("token"));
vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
return R.ok(captchaService.check(vo));
}
}
@@ -0,0 +1,255 @@
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bsd.pjcloud.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.bsd.pjcloud.admin.api.entity.SysOauthClientDetails;
import com.bsd.pjcloud.admin.api.entity.SysTenant;
import com.bsd.pjcloud.admin.api.feign.RemoteClientDetailsService;
import com.bsd.pjcloud.admin.api.feign.RemoteTenantService;
import com.bsd.pjcloud.admin.api.vo.TokenVO;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.RetOps;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import com.bsd.pjcloud.common.security.annotation.Inner;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.OAuth2ErrorCodesExpand;
import com.bsd.pjcloud.common.security.util.OAuthClientException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.core.Authentication;
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.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author pjcloud
* @date 2019/2/1 删除token端点
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/token")
public class PjcloudTokenEndpoint {
private final OAuth2AuthorizationService authorizationService;
private final RemoteClientDetailsService clientDetailsService;
private final RedisTemplate<String, Object> redisTemplate;
private final RemoteTenantService tenantService;
private final KeyStrResolver tenantKeyStrResolver;
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();
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))
.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 令牌
* @return
*/
@SneakyThrows
@GetMapping("/check_token")
public R<OAuth2AccessToken> checkToken(String token, HttpServletResponse response, HttpServletRequest request) {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
if (StrUtil.isBlank(token)) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
return R.failed(OAuth2ErrorCodesExpand.TOKEN_MISSING);
}
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
// 如果令牌不存在 返回401
if (authorization == null || authorization.getAccessToken() == null) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
return R.failed(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN);
}
// 获取令牌
return R.ok(Objects.requireNonNull(authorization).getAccessToken().getToken());
}
/**
* 令牌管理调用
* @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<TokenVO>> tokenList(@RequestBody Map<String, Object> params) {
// 根据分页参数获取对应数据
String key = String.format("%s::%s::*", tenantKeyStrResolver.key(), 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<TokenVO> 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);
Map<String, Object> attributes = authorization.getAttributes();
Authentication authentication = (Authentication) attributes.get(Principal.class.getName());
if (Objects.isNull(authentication)) {
return tokenVo;
}
PjcloudUser pjcloudUser = (PjcloudUser) authentication.getPrincipal();
tokenVo.setUserId(pjcloudUser.getId());
return tokenVo;
}).filter(tokenVo -> {
// 根据用户名过滤
String username = MapUtil.getStr(params, SecurityConstants.DETAILS_USERNAME);
if (StrUtil.isBlank(username)) {
return true;
}
return tokenVo.getUsername().contains(username);
}).collect(Collectors.toList());
result.setRecords(tokenVoList);
result.setTotal(keys.size());
return R.ok(result);
}
@Inner
@GetMapping("/query-token")
public R queryToken(String token) {
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
return R.ok(authorization);
}
}
@@ -0,0 +1,119 @@
package com.bsd.pjcloud.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 pjcloud
* @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,97 @@
package com.bsd.pjcloud.auth.support.base;
import com.bsd.pjcloud.common.security.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
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 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,332 @@
package com.bsd.pjcloud.auth.support.base;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.security.service.PjcloudRedisOAuth2AuthorizationService;
import com.bsd.pjcloud.common.security.util.OAuth2ErrorCodesExpand;
import com.bsd.pjcloud.common.security.util.ScopeException;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
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 处理自定义授权
*/
@Slf4j
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 {
authorizedScopes = new LinkedHashSet<>();
}
Map<String, Object> reqParameters = resouceOwnerBaseAuthentication.getAdditionalParameters();
try {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);
LOGGER.debug("got usernamePasswordAuthenticationToken=" + usernamePasswordAuthenticationToken);
Authentication usernamePasswordAuthentication = authenticationManager
.authenticate(usernamePasswordAuthenticationToken);
Object onlineQuantity = registeredClient.getClientSettings()
.getSettings()
.get(CommonConstants.ONLINE_QUANTITY);
// 没有设置并发控制走原有逻辑生成 || 设置同时在线为 true
if (Objects.isNull(onlineQuantity) || BooleanUtil.toBooleanObject((String) onlineQuantity)) {
return generatAuthenticationToken(resouceOwnerBaseAuthentication, clientPrincipal, registeredClient,
authorizedScopes, usernamePasswordAuthentication);
}
// 不允许同时在线,删除原有username 关联的所有token
PjcloudRedisOAuth2AuthorizationService redisOAuth2AuthorizationService = (PjcloudRedisOAuth2AuthorizationService) this.authorizationService;
redisOAuth2AuthorizationService.removeByUsername(usernamePasswordAuthentication);
return generatAuthenticationToken(resouceOwnerBaseAuthentication, clientPrincipal, registeredClient,
authorizedScopes, usernamePasswordAuthentication);
} catch (AuthenticationException ex) {
throw oAuth2AuthenticationException(authentication, ex);
} catch (Exception e) {
log.error("登录异常:", e);
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR));
}
}
/**
* 生成新的令牌
*
* @param resouceOwnerBaseAuthentication
* @param clientPrincipal
* @param registeredClient
* @param authorizedScopes
* @param usernamePasswordAuthentication
* @return OAuth2AccessTokenAuthenticationToken
*/
@NotNull
private OAuth2AccessTokenAuthenticationToken generatAuthenticationToken(T resouceOwnerBaseAuthentication,
OAuth2ClientAuthenticationToken clientPrincipal, RegisteredClient registeredClient,
Set<String> authorizedScopes, Authentication usernamePasswordAuthentication) {
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())
.authorizationGrant(resouceOwnerBaseAuthentication);
// @formatter:on
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())
// 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()));
}
/**
* 登录异常转换为oauth2异常
*
* @param authentication 身份验证
* @param authenticationException 身份验证异常
* @return {@link OAuth2AuthenticationException}
*/
private OAuth2AuthenticationException oAuth2AuthenticationException(Authentication authentication,
AuthenticationException authenticationException) {
// OAuth2AuthenticationException 直接返回
if (authenticationException instanceof OAuth2AuthenticationException) {
return (OAuth2AuthenticationException) 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"), ""));
}
log.error(authenticationException.getLocalizedMessage());
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR),
authenticationException.getLocalizedMessage(), authenticationException);
}
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.bsd.pjcloud.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 pjcloud
* @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.bsd.pjcloud.auth.support.base;
@@ -0,0 +1,39 @@
package com.bsd.pjcloud.auth.support.core;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
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 pjcloud
* @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;
}
PjcloudUser pjcloudUser = (PjcloudUser) context.getPrincipal().getPrincipal();
claims.claim(SecurityConstants.DETAILS_USER_ID, pjcloudUser.getId());
claims.claim(SecurityConstants.DETAILS_USERNAME, pjcloudUser.getUsername());
}
}
@@ -0,0 +1,36 @@
package com.bsd.pjcloud.auth.support.core;
import com.bsd.pjcloud.auth.support.handler.FormAuthenticationFailureHandler;
import com.bsd.pjcloud.auth.support.handler.SsoLogoutSuccessHandler;
import com.bsd.pjcloud.auth.support.handler.TenantSavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
/**
* @author pjcloud
* @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();
}
}
@@ -0,0 +1,182 @@
package com.bsd.pjcloud.auth.support.core;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.WebUtils;
import com.bsd.pjcloud.common.security.service.PjcloudUserDetailsService;
import lombok.SneakyThrows;
import org.springframework.core.Ordered;
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.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;
import java.util.Map;
import java.util.Optional;
/**
* @author pjcloud
* @date 2022-06-04
*/
public class PjcloudDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* The plaintext password used to perform PasswordEncoder#matches(CharSequence,
* String)} on when the user is not found to avoid SEC-2056.
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private final static BasicAuthenticationConverter basicConvert = new BasicAuthenticationConverter();
private PasswordEncoder passwordEncoder;
/**
* The password used to perform {@link PasswordEncoder#matches(CharSequence, String)}
* on when the user is not found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the password is not
* in a valid format.
*/
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public PjcloudDaoAuthenticationProvider() {
setMessageSource(SpringUtil.getBean("securityMessageSource"));
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
@Override
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
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
@SneakyThrows
@Override
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);
if (StrUtil.isBlank(clientId)) {
clientId = basicConvert.convert(WebUtils.getRequest()).getName();
}
Map<String, PjcloudUserDetailsService> userDetailsServiceMap = SpringUtil
.getBeansOfType(PjcloudUserDetailsService.class);
String finalClientId = clientId;
Optional<PjcloudUserDetailsService> optional = userDetailsServiceMap.values()
.stream()
.filter(service -> service.support(finalClientId, grantType))
.max(Comparator.comparingInt(Ordered::getOrder));
if (optional.isEmpty()) {
throw new InternalAuthenticationServiceException("UserDetailsService error , not register");
}
try {
UserDetails loadedUser = optional.get().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (Exception ex) {
throw ex;
}
}
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
}
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
/**
* Sets the PasswordEncoder instance to be used to encode and validate passwords. If
* not set, the password will be compared using
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
* @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
* types.
*/
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return this.passwordEncoder;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
@@ -0,0 +1,77 @@
package com.bsd.pjcloud.auth.support.core;
import com.bsd.pjcloud.common.security.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
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.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.*;
/**
* @author pjcloud
* @date 2024/3/5
*
* 覆盖原生的 OAuth2RefreshTokenAuthenticationConverter ,避免无法获取 query params
*/
public class PjcloudOAuth2RefreshTokenAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// grant_type (REQUIRED)
String grantType = parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// refresh_token (REQUIRED)
String refreshToken = parameters.getFirst(OAuth2ParameterNames.REFRESH_TOKEN);
if (!StringUtils.hasText(refreshToken) ||
parameters.get(OAuth2ParameterNames.REFRESH_TOKEN).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.REFRESH_TOKEN,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 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, " ")));
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.REFRESH_TOKEN) &&
!key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0]));
}
});
return new OAuth2RefreshTokenAuthenticationToken(
refreshToken, clientPrincipal, requestedScopes, additionalParameters);
}
}
@@ -0,0 +1,30 @@
package com.bsd.pjcloud.auth.support.filter;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
* @author pjcloud
* @date 2020/10/4
* <p>
* 网关配置文件
*/
@Data
@Component
@RefreshScope
@ConfigurationProperties("security")
public class AuthSecurityConfigProperties {
/**
* 是否是微服务架构
*/
private boolean isMicro;
/**
* 网关解密登录前端密码 秘钥
*/
private String encodeKey;
}
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bsd.pjcloud.auth.support.filter;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.constant.enums.EncFlagTypeEnum;
import com.bsd.pjcloud.common.core.util.RepeatBodyRequestWrapper;
import com.bsd.pjcloud.common.core.util.WebUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.util.Map;
/**
* @author pjcloud
* @date 2019 /2/1 密码解密工具类
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PasswordDecoderFilter extends OncePerRequestFilter {
private final AuthSecurityConfigProperties authSecurityConfigProperties;
private final StringRedisTemplate redisTemplate;
private static final String PASSWORD = "password";
private static final String KEY_ALGORITHM = "AES";
static {
// 关闭hutool 强制关闭Bouncy Castle库的依赖
SecureUtil.disableBouncyCastle();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 不是登录请求,直接向下执行
if (!StrUtil.containsAnyIgnoreCase(request.getRequestURI(), SecurityConstants.OAUTH_TOKEN_URL)) {
chain.doFilter(request, response);
return;
}
// 2. 刷新token类型,直接向下执行
String grantType = request.getParameter("grant_type");
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {
chain.doFilter(request, response);
return;
}
// 3. 判断客户端是否需要解密,明文传输直接向下执行
if (!isEncClient(request)) {
chain.doFilter(request, response);
return; }
// 将请求流转换为可多次读取的请求流
RepeatBodyRequestWrapper requestWrapper = new RepeatBodyRequestWrapper(request);
Map<String, String[]> parameterMap = requestWrapper.getParameterMap();
// 构建前端对应解密AES 因子
AES aes = new AES(Mode.CFB, Padding.NoPadding,
new SecretKeySpec(authSecurityConfigProperties.getEncodeKey().getBytes(), KEY_ALGORITHM),
new IvParameterSpec(authSecurityConfigProperties.getEncodeKey().getBytes()));
parameterMap.forEach((k, v) -> {
String[] values = parameterMap.get(k);
if (!PASSWORD.equals(k) || ArrayUtil.isEmpty(values)) {
return;
}
// 解密密码
String decryptPassword = aes.decryptStr(values[0]);
parameterMap.put(k, new String[]{decryptPassword});
});
chain.doFilter(requestWrapper, response);
}
/**
* 根据请求的clientId 查询客户端配置是否是加密传输
*
* @param request 请求上下文
* @return true 加密传输 、 false 原文传输
*/
private boolean isEncClient(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
String clientId = WebUtils.extractClientId(header).orElse(null);
// 获取租户拼接区分租户的key
String tenantId = request.getHeader(CommonConstants.TENANT_ID);
String key = String.format("%s:%s:%s", StrUtil.isBlank(tenantId) ? CommonConstants.TENANT_ID_1 : tenantId,
CacheConstants.CLIENT_FLAG, clientId);
String val = redisTemplate.opsForValue().get(key);
// 当配置不存在时,默认需要解密
if (val == null) {
return true;
}
JSONObject information = JSONUtil.parseObj(val);
return !StrUtil.equals(EncFlagTypeEnum.NO.getType(), information.getStr(CommonConstants.ENC_FLAG));
}
}
@@ -0,0 +1,179 @@
package com.bsd.pjcloud.auth.support.filter;
/**
* 登录前处理器
*
* @author pjcloud
* @date 2024/4/3
*/
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.constant.enums.CaptchaFlagTypeEnum;
import com.bsd.pjcloud.common.core.exception.ValidateCodeException;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import com.bsd.pjcloud.common.core.util.WebUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* @author lbw
* @date 2024-01-06
* <p>
* 登录前置处理器: 前端密码传输密文解密,验证码处理
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {
private final StringRedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestUrl = request.getServletPath();
// 不是登录URL 请求直接跳过
if (!SecurityConstants.OAUTH_TOKEN_URL.equals(requestUrl)) {
filterChain.doFilter(request, response);
return;
}
// 如果登录URL 但是刷新token的请求,直接向下执行
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (StrUtil.containsAny(grantType, SecurityConstants.REFRESH_TOKEN)) {
filterChain.doFilter(request, response);
return;
}
// mobile模式, 如果请求不包含mobile 参数直接
String mobile = request.getParameter(SecurityConstants.GRANT_MOBILE);
if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && StrUtil.isBlank(mobile)) {
throw new OAuth2AuthenticationException(SecurityConstants.GRANT_MOBILE);
}
// mobile模式, 社交登录模式不校验验证码直接跳过
if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && !StrUtil.contains(mobile, "SMS")) {
filterChain.doFilter(request, response);
return;
}
// 判断客户端是否跳过检验
if (!isCheckCaptchaClient(request)) {
filterChain.doFilter(request, response);
return;
}
// 校验验证码 1. 客户端开启验证码 2. 短信模式
try {
checkCode();
filterChain.doFilter(request, response);
} catch (ValidateCodeException validateCodeException) {
throw new OAuth2AuthenticationException(validateCodeException.getMessage());
}
}
/**
* 校验验证码
*/
private void checkCode() throws ValidateCodeException {
HttpServletRequest request = WebUtils.getRequest();
String code = request.getParameter("code");
if (StrUtil.isBlank(code)) {
throw new ValidateCodeException("验证码不能为空");
}
String randomStr = request.getParameter("randomStr");
// https://gitee.com/log4j/pig/issues/IWA0D
String mobile = request.getParameter("mobile");
if (StrUtil.isNotBlank(mobile)) {
randomStr = mobile;
}
// 若是滑块登录
if (CommonConstants.IMAGE_CODE_TYPE.equalsIgnoreCase(randomStr)) {
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
CaptchaVO vo = new CaptchaVO();
vo.setCaptchaVerification(code);
vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
if (!captchaService.verification(vo).isSuccess()) {
throw new ValidateCodeException("验证码不能为空");
}
return;
}
String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;
if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
throw new ValidateCodeException("验证码不合法");
}
Object codeObj = redisTemplate.opsForValue().get(key);
if (codeObj == null) {
throw new ValidateCodeException("验证码不合法");
}
String saveCode = codeObj.toString();
if (StrUtil.isBlank(saveCode)) {
redisTemplate.delete(key);
throw new ValidateCodeException("验证码不合法");
}
if (!StrUtil.equals(saveCode, code)) {
redisTemplate.delete(key);
throw new ValidateCodeException("验证码不合法");
}
redisTemplate.delete(key);
}
/**
* 是否需要校验客户端,根据client 查询客户端配置
*
* @param request 请求
* @return true 需要校验, false 不需要校验
*/
private boolean isCheckCaptchaClient(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
String clientId = WebUtils.extractClientId(header).orElse(null);
// 获取租户拼接区分租户的key
String tenantId = request.getHeader(CommonConstants.TENANT_ID);
String key = String.format("%s:%s:%s", StrUtil.isBlank(tenantId) ? CommonConstants.TENANT_ID_1 : tenantId,
CacheConstants.CLIENT_FLAG, clientId);
String val = redisTemplate.opsForValue().get(key);
// 当配置不存在时,不用校验
if (val == null) {
return false;
}
JSONObject information = JSONUtil.parseObj(val);
return !StrUtil.equals(CaptchaFlagTypeEnum.OFF.getType(), information.getStr(CommonConstants.CAPTCHA_FLAG));
}
}
@@ -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.bsd.pjcloud.auth.support.handler;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.HttpUtil;
import com.bsd.pjcloud.common.core.util.WebUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* @author pjcloud
* @date 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,172 @@
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bsd.pjcloud.auth.support.handler;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.dto.SysLogDTO;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.core.util.WebUtils;
import com.bsd.pjcloud.common.data.resolver.ParamResolver;
import com.bsd.pjcloud.common.log.event.SysLogEvent;
import com.bsd.pjcloud.common.log.util.LogTypeEnum;
import com.bsd.pjcloud.common.log.util.SysLogUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
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 java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* @author pjcloud
* @date 2022-06-02
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PjcloudAuthenticationFailureEventHandler implements AuthenticationFailureHandler {
private static final MappingJackson2HttpMessageConverter errorHttpResponseConverter = new MappingJackson2HttpMessageConverter();
private final StringRedisTemplate 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);
// 密码错误记录错误次数 (TOC用户不记录)
String header = WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC);
if (exception instanceof OAuth2AuthenticationException
&& !SecurityConstants.HEADER_TOC_YES.equals(header)) {
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);
}
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
String clientId = WebUtils.extractClientId(header).orElse(null);
logVo.setServiceId(clientId);
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", tenantKeyStrResolver.key(), CacheConstants.LOGIN_ERROR_TIMES, 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);
}
}
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();
this.errorHttpResponseConverter.write(
R.failed(authorizationException.getError().getErrorCode(), errorMessage),
MediaType.APPLICATION_JSON, httpResponse);
}
else {
errorMessage = exception.getLocalizedMessage();
this.errorHttpResponseConverter.write(R.failed(errorMessage), MediaType.APPLICATION_JSON, httpResponse);
}
}
}
@@ -0,0 +1,167 @@
/*
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bsd.pjcloud.auth.support.handler;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.dto.SysLogDTO;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
import com.bsd.pjcloud.common.log.event.SysLogEvent;
import com.bsd.pjcloud.common.log.util.LogTypeEnum;
import com.bsd.pjcloud.common.log.util.SysLogUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
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 java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.Map;
/**
* @author pjcloud
* @date 2022-06-02
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PjcloudAuthenticationSuccessEventHandler implements AuthenticationSuccessHandler {
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
private final StringRedisTemplate 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) {
// 写入登录成功的日志
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
Map<String, Object> map = accessTokenAuthentication.getAdditionalParameters();
if (MapUtil.isNotEmpty(map)) {
sendSuccessEventLog(request, accessTokenAuthentication, map);
}
// 清除账号历史锁定次数
clearLoginFailureTimes(map);
// 输出token
sendAccessTokenResponse(response, authentication);
}
/**
* 记录登录成功事件
* @param request HttpServletRequest
* @param accessTokenAuthentication Authentication
* @param map 请求参数
*/
private void sendSuccessEventLog(HttpServletRequest request,
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication, Map<String, Object> map) {
// 发送异步日志事件
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(accessTokenAuthentication);
SecurityContextHolder.setContext(context);
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.setServiceId(accessTokenAuthentication.getRegisteredClient().getClientId());
logVo.setCreateBy(MapUtil.getStr(map, SecurityConstants.DETAILS_USERNAME));
logVo.setTenantId(Long.parseLong(tenantKeyStrResolver.key()));
publisher.publishEvent(new SysLogEvent(logVo));
}
/**
* 清空登录失败的记录
* @param map
*/
private void clearLoginFailureTimes(Map<String, Object> map) {
String key = String.format("%s:%s:%s", tenantKeyStrResolver.key(), CacheConstants.LOGIN_ERROR_TIMES,
MapUtil.getStr(map, SecurityConstants.DETAILS_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.bsd.pjcloud.auth.support.handler;
import com.bsd.pjcloud.admin.api.dto.SysLogDTO;
import com.bsd.pjcloud.common.core.util.WebUtils;
import com.bsd.pjcloud.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 PjcloudLogoutSuccessEventHandler 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.bsd.pjcloud.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 jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author pjcloud
* @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);
}
}
}
@@ -0,0 +1,60 @@
package com.bsd.pjcloud.auth.support.handler;
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author pjcloud
* @date 2021/2/7
*
* 增强成功回调增加租户上下文避免极端情况下丢失问题
* @see SavedRequestAwareAuthenticationSuccessHandler
*/
@Slf4j
public class TenantSavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
public TenantSavedRequestAwareAuthenticationSuccessHandler() {
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
}
if (isAlwaysUseDefaultTargetUrl()) {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
}
else {
this.clearAuthenticationAttributes(request);
// 增加租户信息
assert savedRequest != null;
String targetUrl = savedRequest.getRedirectUrl() + "&TENANT-ID="
+ SpringContextHolder.getBean(KeyStrResolver.class).key();
this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
}
@@ -0,0 +1,63 @@
package com.bsd.pjcloud.auth.support.password;
import com.bsd.pjcloud.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
import com.bsd.pjcloud.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.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
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.bsd.pjcloud.auth.support.password;
import com.bsd.pjcloud.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.bsd.pjcloud.auth.support.password;
import com.bsd.pjcloud.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.bsd.pjcloud.auth.support.password;
@@ -0,0 +1,57 @@
package com.bsd.pjcloud.auth.support.sms;
import com.bsd.pjcloud.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.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 jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Set;
/**
* @author pjcloud
* @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.bsd.pjcloud.auth.support.sms;
import com.bsd.pjcloud.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationProvider;
import com.bsd.pjcloud.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 pjcloud
* @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.bsd.pjcloud.auth.support.sms;
import com.bsd.pjcloud.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 pjcloud
* @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.bsd.pjcloud.auth.support.sms;