feat: initial iShare project code
This commit is contained in:
18
pigx-auth/Dockerfile
Normal file
18
pigx-auth/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM pig4cloud/java:8-jre
|
||||
|
||||
MAINTAINER wangiegie@gmail.com
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
|
||||
|
||||
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN mkdir -p /pigx-auth
|
||||
|
||||
WORKDIR /pigx-auth
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ADD ./target/pigx-auth.jar ./
|
||||
|
||||
CMD sleep 120;java $JAVA_OPTS -jar pigx-auth.jar
|
||||
97
pigx-auth/pom.xml
Normal file
97
pigx-auth/pom.xml
Normal file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx</artifactId>
|
||||
<version>5.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>pigx-auth</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<description>pigx 认证授权中心,基于 spring security oAuth2</description>
|
||||
|
||||
<dependencies>
|
||||
<!--注册中心客户端-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
<!--配置中心客户端-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
<!--log-->
|
||||
<dependency>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-common-log</artifactId>
|
||||
</dependency>
|
||||
<!--security-->
|
||||
<dependency>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-common-security</artifactId>
|
||||
</dependency>
|
||||
<!--feign 依赖-->
|
||||
<dependency>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-common-feign</artifactId>
|
||||
</dependency>
|
||||
<!--缓存操作-->
|
||||
<dependency>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-common-data</artifactId>
|
||||
</dependency>
|
||||
<!--sentinel 依赖-->
|
||||
<dependency>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-common-sentinel</artifactId>
|
||||
</dependency>
|
||||
<!--路由控制-->
|
||||
<dependency>
|
||||
<groupId>com.pig4cloud</groupId>
|
||||
<artifactId>pigx-common-gray</artifactId>
|
||||
</dependency>
|
||||
<!--freemarker-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-freemarker</artifactId>
|
||||
</dependency>
|
||||
<!--web 模块-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!--undertow容器-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.fabric8</groupId>
|
||||
<artifactId>docker-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -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.pig4cloud.pigx.auth;
|
||||
|
||||
import com.pig4cloud.pigx.common.feign.annotation.EnablePigxFeignClients;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2018年06月21日 认证授权中心
|
||||
*/
|
||||
@EnablePigxFeignClients
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication
|
||||
public class PigxAuthApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(PigxAuthApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.config;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.CustomeOAuth2AccessTokenGenerator;
|
||||
import com.pig4cloud.pigx.auth.support.core.CustomeOAuth2TokenCustomizer;
|
||||
import com.pig4cloud.pigx.auth.support.core.FormIdentityLoginConfigurer;
|
||||
import com.pig4cloud.pigx.auth.support.core.PigxDaoAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.auth.support.handler.PigxAuthenticationFailureEventHandler;
|
||||
import com.pig4cloud.pigx.auth.support.handler.PigxAuthenticationSuccessEventHandler;
|
||||
import com.pig4cloud.pigx.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationConverter;
|
||||
import com.pig4cloud.pigx.auth.support.password.OAuth2ResourceOwnerPasswordAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationConverter;
|
||||
import com.pig4cloud.pigx.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.handler.FormAuthenticationFailureHandler;
|
||||
import com.pig4cloud.pigx.common.security.handler.SsoLogoutSuccessHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.*;
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022/5/27
|
||||
*
|
||||
* 认证服务器配置
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class AuthorizationServerConfiguration {
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
|
||||
PigxAuthenticationSuccessEventHandler successEventHandler,
|
||||
PigxAuthenticationFailureEventHandler failureEventHandler) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
|
||||
|
||||
http.apply(authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> {// 个性化认证授权端点
|
||||
tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter
|
||||
.accessTokenResponseHandler(successEventHandler) // 登录成功处理器
|
||||
.errorResponseHandler(failureEventHandler);// 登录失败处理器
|
||||
}).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证
|
||||
oAuth2ClientAuthenticationConfigurer.errorResponseHandler(failureEventHandler))// 处理客户端认证异常
|
||||
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面
|
||||
.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI)));
|
||||
|
||||
DefaultSecurityFilterChain securityFilterChain = http.authorizeHttpRequests(authorizeRequests -> {
|
||||
// 自定义接口、端点暴露
|
||||
authorizeRequests.antMatchers("/token/**", "/actuator/**", "/css/**", "/error").permitAll();
|
||||
authorizeRequests.anyRequest().authenticated();
|
||||
}).apply(authorizationServerConfigurer.authorizationService(authorizationService)// redis存储token的实现
|
||||
.authorizationServerSettings(
|
||||
AuthorizationServerSettings.builder().issuer(SecurityConstants.PIGX_LICENSE).build()))
|
||||
// 授权码登录的登录页个性化
|
||||
.and().apply(new FormIdentityLoginConfigurer()).and().build();
|
||||
|
||||
// 注入自定义授权模式实现
|
||||
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
|
||||
*/
|
||||
private AuthenticationConverter accessTokenRequestConverter() {
|
||||
return new DelegatingAuthenticationConverter(Arrays.asList(
|
||||
new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
|
||||
new OAuth2ResourceOwnerSmsAuthenticationConverter(), new OAuth2RefreshTokenAuthenticationConverter(),
|
||||
new OAuth2ClientCredentialsAuthenticationConverter(),
|
||||
new OAuth2AuthorizationCodeAuthenticationConverter(),
|
||||
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入授权模式实现提供方
|
||||
*
|
||||
* 1. 密码模式 </br>
|
||||
* 2. 短信登录 </br>
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http) {
|
||||
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
|
||||
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
|
||||
|
||||
OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider(
|
||||
authenticationManager, authorizationService, oAuth2TokenGenerator());
|
||||
|
||||
OAuth2ResourceOwnerSmsAuthenticationProvider resourceOwnerSmsAuthenticationProvider = new OAuth2ResourceOwnerSmsAuthenticationProvider(
|
||||
authenticationManager, authorizationService, oAuth2TokenGenerator());
|
||||
|
||||
// 处理 UsernamePasswordAuthenticationToken
|
||||
http.authenticationProvider(new PigxDaoAuthenticationProvider());
|
||||
// 处理 OAuth2ResourceOwnerPasswordAuthenticationToken
|
||||
http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider);
|
||||
// 处理 OAuth2ResourceOwnerSmsAuthenticationToken
|
||||
http.authenticationProvider(resourceOwnerSmsAuthenticationProvider);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.endpoint;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.TemporalAccessorUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysOauthClientDetails;
|
||||
import com.pig4cloud.pigx.admin.api.entity.SysTenant;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteClientDetailsService;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteTenantService;
|
||||
import com.pig4cloud.pigx.admin.api.vo.TokenVo;
|
||||
import com.pig4cloud.pigx.auth.support.handler.PigxAuthenticationFailureEventHandler;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.core.util.RetOps;
|
||||
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
|
||||
import com.pig4cloud.pigx.common.security.annotation.Inner;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2ErrorCodesExpand;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuthClientException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.authentication.event.LogoutSuccessEvent;
|
||||
import org.springframework.security.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.oauth2.server.resource.InvalidBearerTokenException;
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019/2/1 删除token端点
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/token")
|
||||
public class PigxTokenEndpoint {
|
||||
|
||||
private final PigxAuthenticationFailureEventHandler authenticationFailureHandler;
|
||||
|
||||
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(SecurityConstants.FROM_IN);
|
||||
modelAndView.addObject("tenantList", tenantList.getData());
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/confirm_access")
|
||||
public ModelAndView confirm(Principal principal, ModelAndView modelAndView,
|
||||
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
|
||||
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
|
||||
@RequestParam(OAuth2ParameterNames.STATE) String state) {
|
||||
SysOauthClientDetails clientDetails = RetOps
|
||||
.of(clientDetailsService.getClientDetailsById(clientId, SecurityConstants.FROM_IN)).getData()
|
||||
.orElseThrow(() -> new OAuthClientException("clientId 不合法"));
|
||||
|
||||
Set<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
|
||||
modelAndView.addObject("clientId", clientId);
|
||||
modelAndView.addObject("state", state);
|
||||
modelAndView.addObject("scopeList", authorizedScopes);
|
||||
modelAndView.addObject("principalName", principal.getName());
|
||||
modelAndView.setViewName("ftl/confirm");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出并删除token
|
||||
* @param authHeader Authorization
|
||||
*/
|
||||
@DeleteMapping("/logout")
|
||||
public R<Boolean> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
|
||||
if (StrUtil.isBlank(authHeader)) {
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), StrUtil.EMPTY).trim();
|
||||
return removeToken(tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验token
|
||||
* @param token 令牌
|
||||
* @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);
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
|
||||
new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING));
|
||||
return R.failed();
|
||||
}
|
||||
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
||||
|
||||
// 如果令牌不存在 返回401
|
||||
if (authorization == null || authorization.getAccessToken() == null) {
|
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
|
||||
new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN));
|
||||
}
|
||||
|
||||
// 获取令牌
|
||||
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;
|
||||
}
|
||||
|
||||
PigxUser pigxUser = (PigxUser) authentication.getPrincipal();
|
||||
tokenVo.setUserId(pigxUser.getId());
|
||||
return tokenVo;
|
||||
}).filter(tokenVo -> {
|
||||
// 根据用户名过滤
|
||||
String username = MapUtil.getStr(params, SecurityConstants.DETAILS_USERNAME);
|
||||
if (StrUtil.isBlank(username)) {
|
||||
return true;
|
||||
}
|
||||
return tokenVo.getUsername().contains(username);
|
||||
}).collect(Collectors.toList());
|
||||
result.setRecords(tokenVoList);
|
||||
result.setTotal(keys.size());
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@Inner
|
||||
@GetMapping("/query-token")
|
||||
public R queryToken(String token) {
|
||||
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
||||
return R.ok(authorization);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.pig4cloud.pigx.auth.support;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.oauth2.core.ClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.server.authorization.token.*;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022/5/29
|
||||
*/
|
||||
public class CustomeOAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {
|
||||
|
||||
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public OAuth2AccessToken generate(OAuth2TokenContext context) {
|
||||
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) || !OAuth2TokenFormat.REFERENCE
|
||||
.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String issuer = null;
|
||||
if (context.getAuthorizationServerContext() != null) {
|
||||
issuer = context.getAuthorizationServerContext().getIssuer();
|
||||
}
|
||||
RegisteredClient registeredClient = context.getRegisteredClient();
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
|
||||
|
||||
// @formatter:off
|
||||
OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
|
||||
if (StringUtils.hasText(issuer)) {
|
||||
claimsBuilder.issuer(issuer);
|
||||
}
|
||||
claimsBuilder
|
||||
.subject(context.getPrincipal().getName())
|
||||
.audience(Collections.singletonList(registeredClient.getClientId()))
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.notBefore(issuedAt)
|
||||
.id(UUID.randomUUID().toString());
|
||||
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
|
||||
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
if (this.accessTokenCustomizer != null) {
|
||||
// @formatter:off
|
||||
OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
|
||||
.registeredClient(context.getRegisteredClient())
|
||||
.principal(context.getPrincipal())
|
||||
.authorizationServerContext(context.getAuthorizationServerContext())
|
||||
.authorizedScopes(context.getAuthorizedScopes())
|
||||
.tokenType(context.getTokenType())
|
||||
.authorizationGrantType(context.getAuthorizationGrantType());
|
||||
if (context.getAuthorization() != null) {
|
||||
accessTokenContextBuilder.authorization(context.getAuthorization());
|
||||
}
|
||||
if (context.getAuthorizationGrant() != null) {
|
||||
accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
|
||||
this.accessTokenCustomizer.customize(accessTokenContext);
|
||||
}
|
||||
|
||||
OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
|
||||
return new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER, UUID.randomUUID().toString(),
|
||||
accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(), context.getAuthorizedScopes(),
|
||||
accessTokenClaimsSet.getClaims());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link OAuth2TokenCustomizer} that customizes the
|
||||
* {@link OAuth2TokenClaimsContext#getClaims() claims} for the
|
||||
* {@link OAuth2AccessToken}.
|
||||
* @param accessTokenCustomizer the {@link OAuth2TokenCustomizer} that customizes the
|
||||
* claims for the {@code OAuth2AccessToken}
|
||||
*/
|
||||
public void setAccessTokenCustomizer(OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer) {
|
||||
Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
|
||||
this.accessTokenCustomizer = accessTokenCustomizer;
|
||||
}
|
||||
|
||||
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
|
||||
|
||||
private final Map<String, Object> claims;
|
||||
|
||||
private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
|
||||
Set<String> scopes, Map<String, Object> claims) {
|
||||
super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getClaims() {
|
||||
return this.claims;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.pig4cloud.pigx.auth.support.base;
|
||||
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2EndpointUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author jumuning
|
||||
* @date 2022-06-02
|
||||
*
|
||||
* 自定义模式认证转换器
|
||||
*/
|
||||
public abstract class OAuth2ResourceOwnerBaseAuthenticationConverter<T extends OAuth2ResourceOwnerBaseAuthenticationToken>
|
||||
implements AuthenticationConverter {
|
||||
|
||||
/**
|
||||
* 是否支持此convert
|
||||
* @param grantType 授权类型
|
||||
* @return
|
||||
*/
|
||||
public abstract boolean support(String grantType);
|
||||
|
||||
/**
|
||||
* 校验参数
|
||||
* @param request 请求
|
||||
*/
|
||||
public void checkParams(HttpServletRequest request) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建具体类型的token
|
||||
* @param clientPrincipal
|
||||
* @param requestedScopes
|
||||
* @param additionalParameters
|
||||
* @return
|
||||
*/
|
||||
public abstract T buildToken(Authentication clientPrincipal, Set<String> requestedScopes,
|
||||
Map<String, Object> additionalParameters);
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
||||
// grant_type (REQUIRED)
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (!support(grantType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
// scope (OPTIONAL)
|
||||
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
|
||||
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
|
||||
Set<String> requestedScopes = null;
|
||||
if (StringUtils.hasText(scope)) {
|
||||
requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
|
||||
}
|
||||
|
||||
// 校验个性化参数
|
||||
checkParams(request);
|
||||
|
||||
// 获取当前已经认证的客户端信息
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (clientPrincipal == null) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
|
||||
// 扩展信息
|
||||
Map<String, Object> additionalParameters = parameters.entrySet().stream()
|
||||
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE)
|
||||
&& !e.getKey().equals(OAuth2ParameterNames.SCOPE))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
|
||||
|
||||
// 创建token
|
||||
return buildToken(clientPrincipal, requestedScopes, additionalParameters);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package com.pig4cloud.pigx.auth.support.base;
|
||||
|
||||
import cn.hutool.core.util.BooleanUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxRedisOAuth2AuthorizationService;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2ErrorCodesExpand;
|
||||
import com.pig4cloud.pigx.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 {
|
||||
throw new ScopeException(OAuth2ErrorCodesExpand.SCOPE_IS_EMPTY);
|
||||
}
|
||||
|
||||
Map<String, Object> reqParameters = resouceOwnerBaseAuthentication.getAdditionalParameters();
|
||||
try {
|
||||
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);
|
||||
|
||||
LOGGER.debug("got usernamePasswordAuthenticationToken=" + usernamePasswordAuthenticationToken);
|
||||
|
||||
Authentication usernamePasswordAuthentication = authenticationManager
|
||||
.authenticate(usernamePasswordAuthenticationToken);
|
||||
|
||||
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
|
||||
PigxRedisOAuth2AuthorizationService redisOAuth2AuthorizationService = (PigxRedisOAuth2AuthorizationService) this.authorizationService;
|
||||
redisOAuth2AuthorizationService.removeByUsername(usernamePasswordAuthentication);
|
||||
|
||||
return generatAuthenticationToken(resouceOwnerBaseAuthentication, clientPrincipal, registeredClient,
|
||||
authorizedScopes, usernamePasswordAuthentication);
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw oAuth2AuthenticationException(authentication, (AuthenticationException) ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新的令牌
|
||||
* @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) {
|
||||
if (authenticationException instanceof UsernameNotFoundException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USERNAME_NOT_FOUND,
|
||||
this.messages.getMessage("JdbcDaoImpl.notFound", new Object[] { authentication.getName() },
|
||||
"Username {0} not found"),
|
||||
""));
|
||||
}
|
||||
if (authenticationException instanceof BadCredentialsException) {
|
||||
return new OAuth2AuthenticationException(
|
||||
new OAuth2Error(OAuth2ErrorCodesExpand.BAD_CREDENTIALS, this.messages.getMessage(
|
||||
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), ""));
|
||||
}
|
||||
if (authenticationException instanceof LockedException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_LOCKED, this.messages
|
||||
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"), ""));
|
||||
}
|
||||
if (authenticationException instanceof DisabledException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_DISABLE,
|
||||
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"),
|
||||
""));
|
||||
}
|
||||
if (authenticationException instanceof AccountExpiredException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_EXPIRED, this.messages
|
||||
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"), ""));
|
||||
}
|
||||
if (authenticationException instanceof CredentialsExpiredException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.CREDENTIALS_EXPIRED,
|
||||
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
|
||||
"User credentials have expired"),
|
||||
""));
|
||||
}
|
||||
if (authenticationException instanceof ScopeException) {
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
|
||||
this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "invalid_scope"), ""));
|
||||
}
|
||||
|
||||
log.error(authenticationException.getLocalizedMessage());
|
||||
return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR),
|
||||
authenticationException.getLocalizedMessage(), authenticationException);
|
||||
}
|
||||
|
||||
private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(
|
||||
Authentication authentication) {
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||
}
|
||||
|
||||
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||
return clientPrincipal;
|
||||
}
|
||||
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.pig4cloud.pigx.auth.support.base;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022/6/2
|
||||
*
|
||||
* 自定义授权模式抽象
|
||||
*/
|
||||
public abstract class OAuth2ResourceOwnerBaseAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
@Getter
|
||||
private final AuthorizationGrantType authorizationGrantType;
|
||||
|
||||
@Getter
|
||||
private final Authentication clientPrincipal;
|
||||
|
||||
@Getter
|
||||
private final Set<String> scopes;
|
||||
|
||||
@Getter
|
||||
private final Map<String, Object> additionalParameters;
|
||||
|
||||
public OAuth2ResourceOwnerBaseAuthenticationToken(AuthorizationGrantType authorizationGrantType,
|
||||
Authentication clientPrincipal, @Nullable Set<String> scopes,
|
||||
@Nullable Map<String, Object> additionalParameters) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
this.authorizationGrantType = authorizationGrantType;
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
|
||||
this.additionalParameters = Collections.unmodifiableMap(
|
||||
additionalParameters != null ? new HashMap<>(additionalParameters) : Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展模式一般不需要密码
|
||||
*/
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户名
|
||||
*/
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.clientPrincipal;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 自定义认证模式接入的抽象实现
|
||||
*/
|
||||
package com.pig4cloud.pigx.auth.support.base;
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.pig4cloud.pigx.auth.support.core;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUser;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsSet;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
||||
|
||||
/**
|
||||
* token 输出增强
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2022/6/3
|
||||
*/
|
||||
public class CustomeOAuth2TokenCustomizer implements OAuth2TokenCustomizer<OAuth2TokenClaimsContext> {
|
||||
|
||||
/**
|
||||
* Customize the OAuth 2.0 Token attributes.
|
||||
* @param context the context containing the OAuth 2.0 Token attributes
|
||||
*/
|
||||
@Override
|
||||
public void customize(OAuth2TokenClaimsContext context) {
|
||||
OAuth2TokenClaimsSet.Builder claims = context.getClaims();
|
||||
claims.claim(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PIGX_LICENSE);
|
||||
String clientId = context.getAuthorizationGrant().getName();
|
||||
claims.claim(SecurityConstants.CLIENT_ID, clientId);
|
||||
claims.claim(SecurityConstants.ACTIVE, Boolean.TRUE);
|
||||
|
||||
// 客户端模式不返回具体用户信息
|
||||
if (SecurityConstants.CLIENT_CREDENTIALS.equals(context.getAuthorizationGrantType().getValue())) {
|
||||
return;
|
||||
}
|
||||
|
||||
PigxUser pigxUser = (PigxUser) context.getPrincipal().getPrincipal();
|
||||
claims.claim(SecurityConstants.DETAILS_USER_ID, pigxUser.getId());
|
||||
claims.claim(SecurityConstants.DETAILS_USERNAME, pigxUser.getUsername());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.pig4cloud.pigx.auth.support.core;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.handler.FormAuthenticationFailureHandler;
|
||||
import com.pig4cloud.pigx.auth.support.handler.SsoLogoutSuccessHandler;
|
||||
import com.pig4cloud.pigx.auth.support.handler.TenantSavedRequestAwareAuthenticationSuccessHandler;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @data 2022-06-04
|
||||
*
|
||||
* 基于授权码模式 统一认证登录 spring security & sas 都可以使用 所以抽取成 HttpConfigurer
|
||||
*/
|
||||
public final class FormIdentityLoginConfigurer
|
||||
extends AbstractHttpConfigurer<FormIdentityLoginConfigurer, HttpSecurity> {
|
||||
|
||||
@Override
|
||||
public void init(HttpSecurity http) throws Exception {
|
||||
http.formLogin(formLogin -> {
|
||||
formLogin.loginPage("/token/login");
|
||||
formLogin.loginProcessingUrl("/token/form");
|
||||
formLogin.failureHandler(new FormAuthenticationFailureHandler());
|
||||
formLogin.successHandler(new TenantSavedRequestAwareAuthenticationSuccessHandler());
|
||||
|
||||
}).logout() // SSO登出成功处理
|
||||
.logoutSuccessHandler(new SsoLogoutSuccessHandler()).deleteCookies("JSESSIONID")
|
||||
.invalidateHttpSession(true).and().csrf().disable();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.pig4cloud.pigx.auth.support.core;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import com.pig4cloud.pigx.common.security.service.PigxUserDetailsService;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
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 lengleng
|
||||
* @date 2022-06-04
|
||||
*/
|
||||
public class PigxDaoAuthenticationProvider 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 PigxDaoAuthenticationProvider() {
|
||||
setMessageSource(SpringUtil.getBean("securityMessageSource"));
|
||||
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void additionalAuthenticationChecks(UserDetails userDetails,
|
||||
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
|
||||
|
||||
// app 模式不用校验密码
|
||||
String grantType = WebUtils.getRequest().getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (StrUtil.equals(SecurityConstants.APP, grantType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (authentication.getCredentials() == null) {
|
||||
this.logger.debug("Failed to authenticate since no credentials provided");
|
||||
throw new BadCredentialsException(this.messages
|
||||
.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, PigxUserDetailsService> userDetailsServiceMap = SpringUtil
|
||||
.getBeansOfType(PigxUserDetailsService.class);
|
||||
|
||||
String finalClientId = clientId;
|
||||
Optional<PigxUserDetailsService> optional = userDetailsServiceMap.values().stream()
|
||||
.filter(service -> service.support(finalClientId, grantType))
|
||||
.max(Comparator.comparingInt(Ordered::getOrder));
|
||||
|
||||
if (!optional.isPresent()) {
|
||||
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 (InternalAuthenticationServiceException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
|
||||
UserDetails user) {
|
||||
boolean upgradeEncoding = this.userDetailsPasswordService != null
|
||||
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
|
||||
if (upgradeEncoding) {
|
||||
String presentedPassword = authentication.getCredentials().toString();
|
||||
String newPassword = this.passwordEncoder.encode(presentedPassword);
|
||||
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
|
||||
}
|
||||
return super.createSuccessAuthentication(principal, authentication, user);
|
||||
}
|
||||
|
||||
private void prepareTimingAttackProtection() {
|
||||
if (this.userNotFoundEncodedPassword == null) {
|
||||
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
|
||||
if (authentication.getCredentials() != null) {
|
||||
String presentedPassword = authentication.getCredentials().toString();
|
||||
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PasswordEncoder instance to be used to encode and validate passwords. If
|
||||
* not set, the password will be compared using
|
||||
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
|
||||
* @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
|
||||
* types.
|
||||
*/
|
||||
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
|
||||
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.userNotFoundEncodedPassword = null;
|
||||
}
|
||||
|
||||
protected PasswordEncoder getPasswordEncoder() {
|
||||
return this.passwordEncoder;
|
||||
}
|
||||
|
||||
public void setUserDetailsService(UserDetailsService userDetailsService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
protected UserDetailsService getUserDetailsService() {
|
||||
return this.userDetailsService;
|
||||
}
|
||||
|
||||
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
|
||||
this.userDetailsPasswordService = userDetailsPasswordService;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-02
|
||||
* <p>
|
||||
* 表单登录失败处理逻辑
|
||||
*/
|
||||
@Slf4j
|
||||
public class FormAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
/**
|
||||
* Called when an authentication attempt fails.
|
||||
* @param request the request during which the authentication attempt occurred.
|
||||
* @param response the response.
|
||||
* @param exception the exception which was thrown to reject the authentication
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) {
|
||||
log.debug("表单登录失败:{}", exception.getLocalizedMessage());
|
||||
String url = HttpUtil.encodeParams(String.format("/token/login?error=%s", exception.getMessage()),
|
||||
CharsetUtil.CHARSET_UTF_8);
|
||||
WebUtils.getResponse().sendRedirect(url);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
|
||||
import com.pig4cloud.pigx.admin.api.feign.RemoteUserService;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.core.util.MsgUtils;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import com.pig4cloud.pigx.common.data.resolver.ParamResolver;
|
||||
import com.pig4cloud.pigx.common.log.event.SysLogEvent;
|
||||
import com.pig4cloud.pigx.common.log.util.LogTypeEnum;
|
||||
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.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 javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-02
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PigxAuthenticationFailureEventHandler implements AuthenticationFailureHandler {
|
||||
|
||||
private static final MappingJackson2HttpMessageConverter errorHttpResponseConverter = new MappingJackson2HttpMessageConverter();
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
private final KeyStrResolver tenantKeyStrResolver;
|
||||
|
||||
private final RemoteUserService userService;
|
||||
|
||||
/**
|
||||
* Called when an authentication attempt fails.
|
||||
* @param request the request during which the authentication attempt occurred.
|
||||
* @param response the response.
|
||||
* @param exception the exception which was thrown to reject the authentication
|
||||
* request.
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) {
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
log.info("登录失败,异常:{}", exception.getLocalizedMessage());
|
||||
|
||||
// 密码模式记录错误信息
|
||||
if (OAuth2ParameterNames.PASSWORD.equals(grantType)) {
|
||||
String username = request.getParameter(OAuth2ParameterNames.USERNAME);
|
||||
|
||||
// 密码错误记录错误次数 (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, SecurityConstants.FROM_IN);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception) throws IOException {
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
String errorMessage;
|
||||
|
||||
if (exception instanceof OAuth2AuthenticationException) {
|
||||
OAuth2AuthenticationException authorizationException = (OAuth2AuthenticationException) exception;
|
||||
errorMessage = StrUtil.isBlank(authorizationException.getError().getDescription())
|
||||
? authorizationException.getError().getErrorCode()
|
||||
: authorizationException.getError().getDescription();
|
||||
}
|
||||
else {
|
||||
errorMessage = exception.getLocalizedMessage();
|
||||
}
|
||||
|
||||
// 手机号登录
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (SecurityConstants.APP.equals(grantType)) {
|
||||
errorMessage = MsgUtils.getSecurityMessage("AbstractUserDetailsAuthenticationProvider.smsBadCredentials");
|
||||
}
|
||||
|
||||
this.errorHttpResponseConverter.write(R.failed(errorMessage), MediaType.APPLICATION_JSON, httpResponse);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.common.log.event.SysLogEvent;
|
||||
import com.pig4cloud.pigx.common.log.util.LogTypeEnum;
|
||||
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.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 javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-02
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PigxAuthenticationSuccessEventHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
private final KeyStrResolver tenantKeyStrResolver;
|
||||
|
||||
/**
|
||||
* Called when a user has been successfully authenticated.
|
||||
* @param request the request which caused the successful authentication
|
||||
* @param response the response
|
||||
* @param authentication the <tt>Authentication</tt> object which was created during
|
||||
* the authentication process.
|
||||
*/
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) {
|
||||
|
||||
// 写入登录成功的日志
|
||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
|
||||
Map<String, Object> map = accessTokenAuthentication.getAdditionalParameters();
|
||||
if (MapUtil.isNotEmpty(map)) {
|
||||
sendSuccessEventLog(request, accessTokenAuthentication, map);
|
||||
}
|
||||
|
||||
// 清除账号历史锁定次数
|
||||
clearLoginFailureTimes(map);
|
||||
|
||||
// 输出token
|
||||
sendAccessTokenResponse(response, authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录成功事件
|
||||
* @param request HttpServletRequest
|
||||
* @param accessTokenAuthentication Authentication
|
||||
* @param map 请求参数
|
||||
*/
|
||||
private void sendSuccessEventLog(HttpServletRequest request,
|
||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication, Map<String, Object> map) {
|
||||
// 发送异步日志事件
|
||||
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(accessTokenAuthentication);
|
||||
SecurityContextHolder.setContext(context);
|
||||
|
||||
SysLogDTO logVo = SysLogUtils.getSysLog();
|
||||
logVo.setTitle("登录成功");
|
||||
logVo.setLogType(LogTypeEnum.NORMAL.getType());
|
||||
String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);
|
||||
if (StrUtil.isNotBlank(startTimeStr)) {
|
||||
Long startTime = Long.parseLong(startTimeStr);
|
||||
Long endTime = System.currentTimeMillis();
|
||||
logVo.setTime(endTime - startTime);
|
||||
}
|
||||
|
||||
logVo.setServiceId(accessTokenAuthentication.getRegisteredClient().getClientId());
|
||||
logVo.setCreateBy(MapUtil.getStr(map, SecurityConstants.DETAILS_USERNAME));
|
||||
logVo.setTenantId(Long.parseLong(tenantKeyStrResolver.key()));
|
||||
|
||||
publisher.publishEvent(new SysLogEvent(logVo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空登录失败的记录
|
||||
* @param map
|
||||
*/
|
||||
private void clearLoginFailureTimes(Map<String, Object> map) {
|
||||
String key = String.format("%s:%s:%s", tenantKeyStrResolver.key(), CacheConstants.LOGIN_ERROR_TIMES,
|
||||
MapUtil.getStr(map, SecurityConstants.DETAILS_USERNAME));
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
private void sendAccessTokenResponse(HttpServletResponse response, Authentication authentication)
|
||||
throws IOException {
|
||||
|
||||
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
|
||||
|
||||
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
|
||||
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
|
||||
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
|
||||
|
||||
OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
|
||||
.tokenType(accessToken.getTokenType()).scopes(accessToken.getScopes());
|
||||
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
|
||||
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
|
||||
}
|
||||
if (refreshToken != null) {
|
||||
builder.refreshToken(refreshToken.getTokenValue());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(additionalParameters)) {
|
||||
builder.additionalParameters(additionalParameters);
|
||||
}
|
||||
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
|
||||
// 无状态 注意删除 context 上下文的信息
|
||||
SecurityContextHolder.clearContext();
|
||||
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
|
||||
import com.pig4cloud.pigx.common.core.util.WebUtils;
|
||||
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.event.LogoutSuccessEvent;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author zhangran
|
||||
* @date 2022-06-02
|
||||
*
|
||||
* 事件机制处理退出相关
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PigxLogoutSuccessEventHandler implements ApplicationListener<LogoutSuccessEvent> {
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(LogoutSuccessEvent event) {
|
||||
Authentication authentication = (Authentication) event.getSource();
|
||||
if (authentication instanceof PreAuthenticatedAuthenticationToken) {
|
||||
handle(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退出成功方法
|
||||
* <p>
|
||||
* 获取到登录的authentication 对象
|
||||
* @param authentication 登录对象
|
||||
*/
|
||||
public void handle(Authentication authentication) {
|
||||
log.info("用户:{} 退出成功", authentication.getPrincipal());
|
||||
SysLogDTO logVo = SysLogUtils.getSysLog();
|
||||
logVo.setTitle("退出成功");
|
||||
// 发送异步日志事件
|
||||
Long startTime = System.currentTimeMillis();
|
||||
Long endTime = System.currentTimeMillis();
|
||||
logVo.setTime(endTime - startTime);
|
||||
|
||||
// 设置对应的token
|
||||
logVo.setParams(WebUtils.getRequest().getHeader(HttpHeaders.AUTHORIZATION));
|
||||
|
||||
// 这边设置ServiceId
|
||||
if (authentication instanceof PreAuthenticatedAuthenticationToken) {
|
||||
logVo.setServiceId(authentication.getCredentials().toString());
|
||||
}
|
||||
logVo.setCreateBy(authentication.getName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-06-02
|
||||
* <p>
|
||||
* sso 退出功能 ,根据客户端传入跳转
|
||||
*/
|
||||
public class SsoLogoutSuccessHandler implements LogoutSuccessHandler {
|
||||
|
||||
private static final String REDIRECT_URL = "redirect_url";
|
||||
|
||||
@Override
|
||||
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws IOException {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取请求参数中是否包含 回调地址
|
||||
String redirectUrl = request.getParameter(REDIRECT_URL);
|
||||
if (StrUtil.isNotBlank(redirectUrl)) {
|
||||
response.sendRedirect(redirectUrl);
|
||||
}
|
||||
else if (StrUtil.isNotBlank(request.getHeader(HttpHeaders.REFERER))) {
|
||||
// 默认跳转referer 地址
|
||||
String referer = request.getHeader(HttpHeaders.REFERER);
|
||||
response.sendRedirect(referer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.pig4cloud.pigx.auth.support.handler;
|
||||
|
||||
import com.pig4cloud.pigx.common.core.util.KeyStrResolver;
|
||||
import com.pig4cloud.pigx.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 javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2021/2/7
|
||||
*
|
||||
* 增强成功回调增加租户上下文避免极端情况下丢失问题
|
||||
* @see SavedRequestAwareAuthenticationSuccessHandler
|
||||
*/
|
||||
@Slf4j
|
||||
public class TenantSavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
private RequestCache requestCache = new HttpSessionRequestCache();
|
||||
|
||||
public TenantSavedRequestAwareAuthenticationSuccessHandler() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) throws ServletException, IOException {
|
||||
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
|
||||
if (savedRequest == null) {
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
|
||||
if (isAlwaysUseDefaultTargetUrl()) {
|
||||
this.requestCache.removeRequest(request, response);
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
else {
|
||||
this.clearAuthenticationAttributes(request);
|
||||
// 增加租户信息
|
||||
assert savedRequest != null;
|
||||
String targetUrl = savedRequest.getRedirectUrl() + "&TENANT-ID="
|
||||
+ SpringContextHolder.getBean(KeyStrResolver.class).key();
|
||||
|
||||
this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.pig4cloud.pigx.auth.support.password;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2EndpointUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author jumuning
|
||||
* @date 2022-06-02
|
||||
*
|
||||
* 密码认证转换器
|
||||
*/
|
||||
public class OAuth2ResourceOwnerPasswordAuthenticationConverter
|
||||
extends OAuth2ResourceOwnerBaseAuthenticationConverter<OAuth2ResourceOwnerPasswordAuthenticationToken> {
|
||||
|
||||
/**
|
||||
* 支持密码模式
|
||||
* @param grantType 授权类型
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String grantType) {
|
||||
return AuthorizationGrantType.PASSWORD.getValue().equals(grantType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2ResourceOwnerPasswordAuthenticationToken buildToken(Authentication clientPrincipal,
|
||||
Set requestedScopes, Map additionalParameters) {
|
||||
return new OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType.PASSWORD, clientPrincipal,
|
||||
requestedScopes, additionalParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验扩展参数 密码模式密码必须不为空
|
||||
* @param request 参数列表
|
||||
*/
|
||||
@Override
|
||||
public void checkParams(HttpServletRequest request) {
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
// username (REQUIRED)
|
||||
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||
if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
|
||||
// password (REQUIRED)
|
||||
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||
if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.pig4cloud.pigx.auth.support.password;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationProvider;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author jumuning
|
||||
* @description 处理用户名密码授权
|
||||
*/
|
||||
public class OAuth2ResourceOwnerPasswordAuthenticationProvider
|
||||
extends OAuth2ResourceOwnerBaseAuthenticationProvider<OAuth2ResourceOwnerPasswordAuthenticationToken> {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerPasswordAuthenticationProvider.class);
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the
|
||||
* provided parameters.
|
||||
* @param authenticationManager
|
||||
* @param authorizationService the authorization service
|
||||
* @param tokenGenerator the token generator
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2ResourceOwnerPasswordAuthenticationProvider(AuthenticationManager authenticationManager,
|
||||
OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
super(authenticationManager, authorizationService, tokenGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
|
||||
String username = (String) reqParameters.get(OAuth2ParameterNames.USERNAME);
|
||||
String password = (String) reqParameters.get(OAuth2ParameterNames.PASSWORD);
|
||||
return new UsernamePasswordAuthenticationToken(username, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
boolean supports = OAuth2ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
LOGGER.debug("supports authentication=" + authentication + " returning " + supports);
|
||||
return supports;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClient(RegisteredClient registeredClient) {
|
||||
assert registeredClient != null;
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.pig4cloud.pigx.auth.support.password;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author jumuning
|
||||
* @description 密码授权token信息
|
||||
*/
|
||||
public class OAuth2ResourceOwnerPasswordAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {
|
||||
|
||||
public OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType authorizationGrantType,
|
||||
Authentication clientPrincipal, Set<String> scopes, Map<String, Object> additionalParameters) {
|
||||
super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 密码模式
|
||||
*/
|
||||
package com.pig4cloud.pigx.auth.support.password;
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.pig4cloud.pigx.auth.support.sms;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationConverter;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.security.util.OAuth2EndpointUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2022-05-31
|
||||
*
|
||||
* 短信登录转换器
|
||||
*/
|
||||
public class OAuth2ResourceOwnerSmsAuthenticationConverter
|
||||
extends OAuth2ResourceOwnerBaseAuthenticationConverter<OAuth2ResourceOwnerSmsAuthenticationToken> {
|
||||
|
||||
/**
|
||||
* 是否支持此convert
|
||||
* @param grantType 授权类型
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean support(String grantType) {
|
||||
return SecurityConstants.APP.equals(grantType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2ResourceOwnerSmsAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes,
|
||||
Map additionalParameters) {
|
||||
return new OAuth2ResourceOwnerSmsAuthenticationToken(new AuthorizationGrantType(SecurityConstants.APP),
|
||||
clientPrincipal, requestedScopes, additionalParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验扩展参数 密码模式密码必须不为空
|
||||
* @param request 参数列表
|
||||
*/
|
||||
@Override
|
||||
public void checkParams(HttpServletRequest request) {
|
||||
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
|
||||
// PHONE (REQUIRED)
|
||||
String phone = parameters.getFirst(SecurityConstants.SMS_PARAMETER_NAME);
|
||||
if (!StringUtils.hasText(phone) || parameters.get(SecurityConstants.SMS_PARAMETER_NAME).size() != 1) {
|
||||
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, SecurityConstants.SMS_PARAMETER_NAME,
|
||||
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.pig4cloud.pigx.auth.support.sms;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationProvider;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date date
|
||||
*
|
||||
* 短信登录的核心处理
|
||||
*/
|
||||
public class OAuth2ResourceOwnerSmsAuthenticationProvider
|
||||
extends OAuth2ResourceOwnerBaseAuthenticationProvider<OAuth2ResourceOwnerSmsAuthenticationToken> {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerSmsAuthenticationProvider.class);
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the
|
||||
* provided parameters.
|
||||
* @param authenticationManager
|
||||
* @param authorizationService the authorization service
|
||||
* @param tokenGenerator the token generator
|
||||
* @since 0.2.3
|
||||
*/
|
||||
public OAuth2ResourceOwnerSmsAuthenticationProvider(AuthenticationManager authenticationManager,
|
||||
OAuth2AuthorizationService authorizationService,
|
||||
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
super(authenticationManager, authorizationService, tokenGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
boolean supports = OAuth2ResourceOwnerSmsAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
LOGGER.debug("supports authentication=" + authentication + " returning " + supports);
|
||||
return supports;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClient(RegisteredClient registeredClient) {
|
||||
assert registeredClient != null;
|
||||
if (!registeredClient.getAuthorizationGrantTypes()
|
||||
.contains(new AuthorizationGrantType(SecurityConstants.APP))) {
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
|
||||
String phone = (String) reqParameters.get(SecurityConstants.SMS_PARAMETER_NAME);
|
||||
return new UsernamePasswordAuthenticationToken(phone, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.pig4cloud.pigx.auth.support.sms;
|
||||
|
||||
import com.pig4cloud.pigx.auth.support.base.OAuth2ResourceOwnerBaseAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @description 短信登录token信息
|
||||
*/
|
||||
public class OAuth2ResourceOwnerSmsAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {
|
||||
|
||||
public OAuth2ResourceOwnerSmsAuthenticationToken(AuthorizationGrantType authorizationGrantType,
|
||||
Authentication clientPrincipal, Set<String> scopes, Map<String, Object> additionalParameters) {
|
||||
super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 短信模式
|
||||
*/
|
||||
package com.pig4cloud.pigx.auth.support.sms;
|
||||
18
pigx-auth/src/main/resources/application.yml
Normal file
18
pigx-auth/src/main/resources/application.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
server:
|
||||
port: 3000
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: @artifactId@
|
||||
cloud:
|
||||
nacos:
|
||||
username: @nacos.username@
|
||||
password: @nacos.password@
|
||||
discovery:
|
||||
server-addr: ${NACOS_HOST:pigx-register}:${NACOS_PORT:8848}
|
||||
config:
|
||||
server-addr: ${spring.cloud.nacos.discovery.server-addr}
|
||||
config:
|
||||
import:
|
||||
- optional:nacos:application-@profiles.active@.yml
|
||||
- optional:nacos:${spring.application.name}-@profiles.active@.yml
|
||||
88
pigx-auth/src/main/resources/logback-spring.xml
Normal file
88
pigx-auth/src/main/resources/logback-spring.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
~
|
||||
~ Redistributions of source code must retain the above copyright notice,
|
||||
~ this list of conditions and the following disclaimer.
|
||||
~ Redistributions in binary form must reproduce the above copyright
|
||||
~ notice, this list of conditions and the following disclaimer in the
|
||||
~ documentation and/or other materials provided with the distribution.
|
||||
~ Neither the name of the pig4cloud.com developer nor the names of its
|
||||
~ contributors may be used to endorse or promote products derived from
|
||||
~ this software without specific prior written permission.
|
||||
~ Author: lengleng (wangiegie@gmail.com)
|
||||
-->
|
||||
|
||||
<!--
|
||||
小技巧: 在根pom里面设置统一存放路径,统一管理方便维护
|
||||
<properties>
|
||||
<log-path>/Users/lengleng</log-path>
|
||||
</properties>
|
||||
1. 其他模块加日志输出,直接copy本文件放在resources 目录即可
|
||||
2. 注意修改 <property name="${log-path}/log.path" value=""/> 的value模块
|
||||
-->
|
||||
<configuration debug="false" scan="false">
|
||||
<property name="log.path" value="logs/${project.artifactId}"/>
|
||||
<!-- 彩色日志格式 -->
|
||||
<property name="CONSOLE_LOG_PATTERN"
|
||||
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
|
||||
<!-- 彩色日志依赖的渲染类 -->
|
||||
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
|
||||
<conversionRule conversionWord="wex"
|
||||
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
|
||||
<conversionRule conversionWord="wEx"
|
||||
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
|
||||
<!-- Console log output -->
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- Log file debug output -->
|
||||
<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/debug.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<maxFileSize>50MB</maxFileSize>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- Log file error output -->
|
||||
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/error.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<maxFileSize>50MB</maxFileSize>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>ERROR</level>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<logger name="org.activiti.engine.impl.db" level="DEBUG">
|
||||
<appender-ref ref="debug"/>
|
||||
</logger>
|
||||
|
||||
<!--nacos 心跳 INFO 屏蔽-->
|
||||
<logger name="com.alibaba.nacos" level="OFF">
|
||||
<appender-ref ref="error"/>
|
||||
</logger>
|
||||
<!-- Level: FATAL 0 ERROR 3 WARN 4 INFO 6 DEBUG 7 -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="console"/>
|
||||
<appender-ref ref="debug"/>
|
||||
<appender-ref ref="error"/>
|
||||
</root>
|
||||
</configuration>
|
||||
6
pigx-auth/src/main/resources/static/css/bootstrap.min.css
vendored
Normal file
6
pigx-auth/src/main/resources/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
67
pigx-auth/src/main/resources/static/css/signin.css
Normal file
67
pigx-auth/src/main/resources/static/css/signin.css
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
.sign_body {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.form-signin {
|
||||
max-width: 330px;
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.form-margin-top {
|
||||
margin-top: 50px;
|
||||
}
|
||||
.form-signin .form-signin-heading,
|
||||
.form-signin .checkbox {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.form-signin .checkbox {
|
||||
font-weight: normal;
|
||||
}
|
||||
.form-signin .form-control {
|
||||
position: relative;
|
||||
height: auto;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.form-signin .form-control:focus {
|
||||
z-index: 2;
|
||||
}
|
||||
.form-signin input[type="email"] {
|
||||
margin-bottom: -1px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.form-signin input[type="password"] {
|
||||
margin-bottom: 10px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
footer{
|
||||
text-align: center;
|
||||
position:absolute;
|
||||
bottom:0;
|
||||
width:100%;
|
||||
height:100px;
|
||||
}
|
||||
56
pigx-auth/src/main/resources/templates/ftl/confirm.ftl
Normal file
56
pigx-auth/src/main/resources/templates/ftl/confirm.ftl
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport"
|
||||
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
|
||||
<title>PigX 第三方授权</title>
|
||||
<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/css/signin.css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-default container-fluid">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="#">开放平台</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-5">
|
||||
<p class="navbar-text navbar-right">
|
||||
<a target="_blank" href="https://pig4cloud.com">技术支持</a>
|
||||
</p>
|
||||
<p class="navbar-text navbar-right">
|
||||
<#if principalName=="anonymousUser">
|
||||
未登录
|
||||
<#else>
|
||||
<a target="_blank" href="https://pig4cloud.com">${principalName}</a>
|
||||
</#if>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div style="padding-top: 80px;width: 300px; color: #555; margin:0px auto;">
|
||||
<form id='confirmationForm' name='confirmationForm' action="/oauth2/authorize" method='post'>
|
||||
<input type="hidden" name="client_id" value="${clientId}">
|
||||
<input type="hidden" name="state" value="${state}">
|
||||
|
||||
<p>
|
||||
将获得以下权限:</p>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item"> <span>
|
||||
<#list scopeList as scope>
|
||||
<input type="checkbox" checked="checked" name="scope" value="${scope}"/><label>${scope}</label>
|
||||
</#list>
|
||||
</ul>
|
||||
<p class="help-block">授权后表明你已同意 <a>服务协议</a></p>
|
||||
<button class="btn btn-success pull-right" type="submit" id="write-email-btn">授权</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
<footer>
|
||||
<p>support by: pig4cloud.com</p>
|
||||
<p>email: <a href="mailto:wangiegie@gmail.com">wangiegie@gmail.com</a>.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
44
pigx-auth/src/main/resources/templates/ftl/login.ftl
Normal file
44
pigx-auth/src/main/resources/templates/ftl/login.ftl
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>PigX微服务统一认证</title>
|
||||
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/css/signin.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="sign_body">
|
||||
<div class="container form-margin-top">
|
||||
<form class="form-signin" action="/token/form" method="post">
|
||||
<h2 class="form-signin-heading" align="center">统一认证系统</h2>
|
||||
|
||||
<#if tenantList??>
|
||||
<select class="form-control form-margin-top" placeholder="所属租户" name="TENANT-ID">
|
||||
<#list tenantList as tenant>
|
||||
<option value="${tenant.id}">${tenant.name}</option>
|
||||
</#list>
|
||||
</select>
|
||||
</#if>
|
||||
|
||||
<input type="hidden" name="client_id" class="form-control" value="pig" placeholder="所属客户端" >
|
||||
<input type="text" name="username" class="form-control" placeholder="账号" required autofocus>
|
||||
<input type="password" name="password" class="form-control" placeholder="密码" required>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">sign in</button>
|
||||
<#if error??>
|
||||
<span style="color: red; ">${error?html}</span>
|
||||
</#if>
|
||||
</form>
|
||||
</div>
|
||||
<footer>
|
||||
<p>support by: pig4cloud</p>
|
||||
<p>email: <a href="mailto:support@mail.pigx.vip">support@mail.pigx.vip</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user