1.初始化版本,2.引入thirdpart模块,引入canteen模块,初始化菜品分类,spu
This commit is contained in:
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);
|
||||
}
|
||||
|
||||
}
|
||||
+186
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+52
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+119
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+97
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+332
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -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;
|
||||
+39
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+182
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+77
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -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;
|
||||
|
||||
}
|
||||
+139
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+179
@@ -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));
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.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);
|
||||
}
|
||||
|
||||
}
|
||||
+172
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+167
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.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());
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
+57
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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;
|
||||
Reference in new issue
Block a user