mirror of
https://gitee.com/youlaitech/youlai-mall.git
synced 2024-12-22 12:48:59 +08:00
feat: 完善OAuth2自定义异常处理逻辑
This commit is contained in:
parent
a07a3c8839
commit
4a943ab531
@ -43,11 +43,6 @@
|
||||
</dependency>
|
||||
|
||||
<!-- OAuth2 认证服务器-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-authorization-server</artifactId>
|
||||
|
@ -94,7 +94,7 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider {
|
||||
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
|
||||
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
|
||||
// 用户名密码身份验证,成功后返回带有权限的认证信息
|
||||
// 用户名密码身份验证,成功后返回 带有权限的认证信息
|
||||
Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
|
||||
|
||||
// 访问令牌(Access Token) 构造器
|
||||
@ -119,6 +119,7 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider {
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(usernamePasswordAuthentication.getName())
|
||||
.authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA)
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.authentication.wxminiapp;
|
||||
package com.youlai.auth.authentication.miniapp;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.auth.util.OAuth2EndpointUtils;
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.authentication.wxminiapp;
|
||||
package com.youlai.auth.authentication.miniapp;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
@ -1,4 +1,4 @@
|
||||
package com.youlai.auth.authentication.wxminiapp;
|
||||
package com.youlai.auth.authentication.miniapp;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
@ -2,6 +2,7 @@ package com.youlai.auth.authentication.password;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.auth.util.OAuth2EndpointUtils;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
@ -32,6 +33,7 @@ public class PasswordAuthenticationConverter implements AuthenticationConverter
|
||||
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
||||
// 授权类型 (必需)
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {
|
||||
|
@ -2,13 +2,15 @@ package com.youlai.auth.authentication.password;
|
||||
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.authentication.*;
|
||||
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.UserDetailsChecker;
|
||||
import org.springframework.security.oauth2.core.*;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
@ -35,7 +37,6 @@ import java.util.stream.Collectors;
|
||||
* 密码模式身份验证提供者
|
||||
* <p>
|
||||
* 处理基于用户名和密码的身份验证
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@ -86,7 +87,12 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
|
||||
Map<String, Object> additionalParameters = resourceOwnerPasswordAuthentication.getAdditionalParameters();
|
||||
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
|
||||
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
|
||||
|
||||
// 这种
|
||||
// https://github.com/Basit-Mahmood/spring-authorization-server-password-grant-type-support/blob/master/SpringAuthorizationServer/src/main/java/pk/training/basit/oauth2/authentication/OAuth2ResourceOwnerPasswordAuthenticationProvider.java
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
|
||||
|
||||
|
||||
// 用户名密码身份验证,成功后返回带有权限的认证信息
|
||||
Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
|
||||
|
||||
@ -126,11 +132,15 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
|
||||
|
||||
ReflectUtil.setFieldValue(usernamePasswordAuthentication.getPrincipal(), "perms", null);
|
||||
|
||||
// 持久化令牌发放记录到数据库
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(usernamePasswordAuthentication.getName())
|
||||
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
|
||||
.authorizedScopes(authorizedScopes)
|
||||
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
|
||||
.attribute(Principal.class.getName(), usernamePasswordAuthentication); // attribute 字段
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.token(accessToken, (metadata) ->
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
|
||||
@ -197,4 +207,6 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
|
||||
return PasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
package com.youlai.auth.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
@ -17,14 +19,16 @@ import com.youlai.auth.authentication.password.PasswordAuthenticationProvider;
|
||||
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationConverter;
|
||||
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationProvider;
|
||||
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationToken;
|
||||
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationConverter;
|
||||
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationProvider;
|
||||
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationToken;
|
||||
import com.youlai.auth.authentication.miniapp.WxMiniAppAuthenticationConverter;
|
||||
import com.youlai.auth.authentication.miniapp.WxMiniAppAuthenticationProvider;
|
||||
import com.youlai.auth.authentication.miniapp.WxMiniAppAuthenticationToken;
|
||||
import com.youlai.auth.handler.MyAuthenticationFailureHandler;
|
||||
import com.youlai.auth.handler.MyAuthenticationSuccessHandler;
|
||||
import com.youlai.auth.userdetails.member.MemberDetailsService;
|
||||
import com.youlai.auth.userdetails.user.SysUserDetails;
|
||||
import com.youlai.auth.userdetails.user.jackson.SysUseMixin;
|
||||
import com.youlai.auth.userdetails.user.jackson.SysUserMixin;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@ -34,8 +38,10 @@ import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.support.lob.DefaultLobHandler;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||
@ -98,22 +104,23 @@ public class AuthorizationServerConfig {
|
||||
|
||||
) throws Exception {
|
||||
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer();
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
|
||||
|
||||
authorizationServerConfigurer
|
||||
.tokenEndpoint(tokenEndpoint ->
|
||||
tokenEndpoint
|
||||
.accessTokenRequestConverters(authenticationConverters ->// <1>
|
||||
authenticationConverters.addAll(
|
||||
List.of(
|
||||
new PasswordAuthenticationConverter(),
|
||||
new CaptchaAuthenticationConverter(),
|
||||
new WxMiniAppAuthenticationConverter(),
|
||||
new SmsCodeAuthenticationConverter()
|
||||
.accessTokenRequestConverters(
|
||||
authenticationConverters ->// <1>
|
||||
authenticationConverters.addAll(
|
||||
List.of(
|
||||
new PasswordAuthenticationConverter(),
|
||||
new CaptchaAuthenticationConverter(),
|
||||
new WxMiniAppAuthenticationConverter(),
|
||||
new SmsCodeAuthenticationConverter()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.authenticationProviders(authenticationProviders ->// <2>
|
||||
/*.authenticationProviders(authenticationProviders ->// <2>
|
||||
authenticationProviders.addAll(
|
||||
List.of(
|
||||
new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator),
|
||||
@ -122,25 +129,37 @@ public class AuthorizationServerConfig {
|
||||
new SmsCodeAuthenticationProvider(authorizationService, tokenGenerator, memberDetailsService, redisTemplate)
|
||||
)
|
||||
)
|
||||
)
|
||||
.accessTokenResponseHandler(new MyAuthenticationSuccessHandler())
|
||||
)*/
|
||||
.accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) // 自定义成功响应
|
||||
.errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定义异常响应
|
||||
);
|
||||
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
|
||||
|
||||
http
|
||||
.securityMatcher(endpointsMatcher)
|
||||
.authorizeHttpRequests(authorize ->
|
||||
authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
|
||||
http.securityMatcher(endpointsMatcher)
|
||||
.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.apply(authorizationServerConfigurer);
|
||||
|
||||
|
||||
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
|
||||
authenticationManagerBuilder.parentAuthenticationManager(null);
|
||||
authenticationManagerBuilder.authenticationProvider(new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator));
|
||||
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
|
||||
/* @Bean
|
||||
public AuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
|
||||
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
|
||||
daoAuthenticationProvider.setHideUserNotFoundExceptions(false) ; // 抛出用户不存在异常
|
||||
return daoAuthenticationProvider ;
|
||||
}*/
|
||||
|
||||
|
||||
@Bean // <5>
|
||||
public JWKSource<SecurityContext> jwkSource() {
|
||||
KeyPair keyPair = generateRsaKey();
|
||||
@ -208,7 +227,7 @@ public class AuthorizationServerConfig {
|
||||
objectMapper.registerModules(securityModules);
|
||||
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
|
||||
// You will need to write the Mixin for your class so Jackson can marshall it.
|
||||
objectMapper.addMixIn(SysUserDetails.class, SysUseMixin.class);
|
||||
objectMapper.addMixIn(SysUserDetails.class, SysUserMixin.class);
|
||||
objectMapper.addMixIn(Long.class, Object.class);
|
||||
rowMapper.setObjectMapper(objectMapper);
|
||||
service.setAuthorizationRowMapper(rowMapper);
|
||||
@ -236,11 +255,9 @@ public class AuthorizationServerConfig {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
||||
|
||||
return authenticationConfiguration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
|
@ -6,26 +6,22 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity(debug = true)
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class DefaultSecurityConfig {
|
||||
|
||||
@Setter
|
||||
private List<String> ignoreUrls;
|
||||
|
||||
/**
|
||||
* Spring Security 安全过滤器链配置
|
||||
*
|
||||
@ -33,6 +29,7 @@ public class SecurityConfig {
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
@Order(0)
|
||||
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(requestMatcherRegistry ->
|
||||
@ -45,8 +42,8 @@ public class SecurityConfig {
|
||||
}
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.formLogin(withDefaults())
|
||||
;
|
||||
.formLogin(Customizer.withDefaults());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
@ -17,7 +16,6 @@ import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@ -27,16 +25,17 @@ import java.util.stream.Collectors;
|
||||
* 自定义 JWT 的 Claims(声明)
|
||||
*
|
||||
* @author haoxr
|
||||
* @see <a href="https://github.com/spring-projects/spring-authorization-server/pull/1264">How-to: Authorize an access token containing custom authorities</a>
|
||||
* @since 2023/7/4
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class JwtCustomizerConfig {
|
||||
public class JwtTokenClaimsConfig {
|
||||
|
||||
private final RedisTemplate redisTemplate;
|
||||
|
||||
@Bean
|
||||
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
|
||||
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
|
||||
return context -> {
|
||||
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) && context.getPrincipal() instanceof UsernamePasswordAuthenticationToken) {
|
||||
// Customize headers/claims for access_token
|
||||
@ -59,7 +58,7 @@ public class JwtCustomizerConfig {
|
||||
|
||||
} else if (principal instanceof MemberDetails userDetails) {
|
||||
claims.claim("member_id", String.valueOf(userDetails.getId()));
|
||||
}else{
|
||||
} else {
|
||||
User user = (User) principal;
|
||||
|
||||
// 这里存入角色至JWT,解析JWT的角色用于鉴权的位置: ResourceServerConfig#jwtAuthenticationConverter
|
@ -3,6 +3,8 @@ package com.youlai.auth.controller;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/6/29
|
||||
*/
|
||||
|
@ -0,0 +1,42 @@
|
||||
package com.youlai.auth.handler;
|
||||
|
||||
import com.youlai.common.result.Result;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
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.OAuth2Error;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 认证异常处理器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/7/6
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
private final HttpMessageConverter<Object> accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();
|
||||
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
||||
log.warn(" authentication failure: ", exception);
|
||||
|
||||
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
httpResponse.setStatusCode(HttpStatus.OK);
|
||||
Result result = Result.failed(error.getDescription());
|
||||
accessTokenHttpResponseConverter.write(result, null, httpResponse);
|
||||
}
|
||||
}
|
@ -1,18 +1,18 @@
|
||||
package com.youlai.auth.handler;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.youlai.common.result.Result;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.DefaultOAuth2AccessTokenResponseMapConverter;
|
||||
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.util.CollectionUtils;
|
||||
@ -22,23 +22,24 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 认证成功处理器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/7/3
|
||||
*/
|
||||
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
|
||||
new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
private final HttpMessageConverter<Object> accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();
|
||||
private Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();
|
||||
|
||||
/**
|
||||
*
|
||||
* @see org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter#sendAccessTokenResponse
|
||||
* @param request the request which caused the successful authentication
|
||||
* @param response the response
|
||||
* @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.
|
||||
* the authentication process.
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
* @see org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter#sendAccessTokenResponse
|
||||
*/
|
||||
|
||||
@Override
|
||||
@ -66,10 +67,8 @@ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHand
|
||||
|
||||
Map<String, Object> tokenResponseParameters = this.accessTokenResponseParametersConverter
|
||||
.convert(accessTokenResponse);
|
||||
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.getWriter().write(JSONUtil.toJsonStr(Result.success(tokenResponseParameters)));
|
||||
|
||||
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
|
||||
// 自定义认证成功响应数据结构
|
||||
this.accessTokenHttpResponseConverter.write(Result.success(tokenResponseParameters), null, httpResponse);
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,14 @@ import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.youlai.common.enums.StatusEnum;
|
||||
import com.youlai.system.dto.UserAuthInfo;
|
||||
import lombok.Data;
|
||||
import org.springframework.security.core.CredentialsContainer;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -22,7 +25,7 @@ import java.util.stream.Collectors;
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
public class SysUserDetails implements UserDetails {
|
||||
public class SysUserDetails implements UserDetails, CredentialsContainer {
|
||||
|
||||
/**
|
||||
* 扩展字段:用户ID
|
||||
@ -45,7 +48,13 @@ public class SysUserDetails implements UserDetails {
|
||||
private String username;
|
||||
private String password;
|
||||
private Boolean enabled;
|
||||
private Collection<SimpleGrantedAuthority> authorities;
|
||||
private Collection<GrantedAuthority> authorities;
|
||||
|
||||
private boolean accountNonExpired;
|
||||
|
||||
private boolean accountNonLocked;
|
||||
|
||||
private boolean credentialsNonExpired;
|
||||
|
||||
private Set<String> perms;
|
||||
|
||||
@ -67,6 +76,28 @@ public class SysUserDetails implements UserDetails {
|
||||
this.setPerms(user.getPerms());
|
||||
}
|
||||
|
||||
public SysUserDetails(
|
||||
Long userId,
|
||||
String username,
|
||||
String password,
|
||||
boolean enabled,
|
||||
boolean accountNonExpired,
|
||||
boolean credentialsNonExpired,
|
||||
boolean accountNonLocked,
|
||||
Set<? extends GrantedAuthority> authorities
|
||||
) {
|
||||
Assert.isTrue(username != null && !"".equals(username) && password != null,
|
||||
"Cannot pass null or empty values to constructor");
|
||||
this.userId = userId;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.enabled = enabled;
|
||||
this.accountNonExpired = accountNonExpired;
|
||||
this.credentialsNonExpired = credentialsNonExpired;
|
||||
this.accountNonLocked = accountNonLocked;
|
||||
this.authorities = Collections.unmodifiableSet(authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return this.authorities;
|
||||
@ -101,4 +132,10 @@ public class SysUserDetails implements UserDetails {
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void eraseCredentials() {
|
||||
this.password = null;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,13 @@
|
||||
package com.youlai.auth.userdetails.user;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.common.result.Result;
|
||||
import com.youlai.system.api.UserFeignClient;
|
||||
import com.youlai.system.dto.UserAuthInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
@ -30,26 +27,16 @@ public class SysUserDetailsService implements UserDetailsService {
|
||||
* <p>
|
||||
* 用户名、密码用于后续认证,认证成功之后将权限授予用户
|
||||
*
|
||||
* @param username 前端登录表单的用户名
|
||||
* @param username 用户名
|
||||
* @return {@link SysUserDetails}
|
||||
*/
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) {
|
||||
Result<UserAuthInfo> result = userFeignClient.getUserAuthInfo(username);
|
||||
|
||||
UserAuthInfo userAuthInfo = null;
|
||||
Assert.isTrue(Result.isSuccess(result) && (userAuthInfo = result.getData()) != null,
|
||||
"用户不存在");
|
||||
|
||||
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
|
||||
if (!userDetails.isEnabled()) {
|
||||
throw new DisabledException("该账户已被禁用!");
|
||||
} else if (!userDetails.isAccountNonLocked()) {
|
||||
throw new LockedException("该账号已被锁定!");
|
||||
} else if (!userDetails.isAccountNonExpired()) {
|
||||
throw new AccountExpiredException("该账号已过期!");
|
||||
if (!Result.isSuccess(result) || result.getData() == null) {
|
||||
throw new UsernameNotFoundException(StrUtil.format("用户{}不存在",username));
|
||||
}
|
||||
return userDetails;
|
||||
return new SysUserDetails(result.getData());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,19 +1,3 @@
|
||||
/*
|
||||
* Copyright 2015-2018 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.youlai.auth.userdetails.user.jackson;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
@ -24,6 +8,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.MissingNode;
|
||||
import com.youlai.auth.userdetails.user.SysUserDetails;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
@ -33,51 +18,53 @@ import java.util.Set;
|
||||
|
||||
/**
|
||||
* Custom Deserializer for {@link User} class. This is already registered with
|
||||
* {@link UserMixin}. You can also use it directly with your mixin class.
|
||||
* {@link SysUserMixin}. You can also use it directly with your mixin class.
|
||||
*
|
||||
* @author Jitendra Singh
|
||||
* @see SysUserMixin
|
||||
* @since 4.2
|
||||
* @see UserMixin
|
||||
*/
|
||||
class SysUserDeserializer extends JsonDeserializer<User> {
|
||||
class SysUserDeserializer extends JsonDeserializer<SysUserDetails> {
|
||||
|
||||
private static final TypeReference<Set<SimpleGrantedAuthority>> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference<Set<SimpleGrantedAuthority>>() {
|
||||
};
|
||||
private static final TypeReference<Set<SimpleGrantedAuthority>> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference<Set<SimpleGrantedAuthority>>() {
|
||||
};
|
||||
|
||||
/**
|
||||
* This method will create {@link User} object. It will ensure successful object
|
||||
* creation even if password key is null in serialized json, because credentials may
|
||||
* be removed from the {@link User} by invoking {@link User#eraseCredentials()}. In
|
||||
* that case there won't be any password key in serialized json.
|
||||
* @param jp the JsonParser
|
||||
* @param ctxt the DeserializationContext
|
||||
* @return the user
|
||||
* @throws IOException if a exception during IO occurs
|
||||
* @throws JsonProcessingException if an error during JSON processing occurs
|
||||
*/
|
||||
@Override
|
||||
public User deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
|
||||
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
|
||||
JsonNode jsonNode = mapper.readTree(jp);
|
||||
Set<? extends GrantedAuthority> authorities = mapper.convertValue(jsonNode.get("authorities"),
|
||||
SIMPLE_GRANTED_AUTHORITY_SET);
|
||||
JsonNode passwordNode = readJsonNode(jsonNode, "password");
|
||||
String username = readJsonNode(jsonNode, "username").asText();
|
||||
String password = passwordNode.asText("");
|
||||
boolean enabled = readJsonNode(jsonNode, "enabled").asBoolean();
|
||||
boolean accountNonExpired = readJsonNode(jsonNode, "accountNonExpired").asBoolean();
|
||||
boolean credentialsNonExpired = readJsonNode(jsonNode, "credentialsNonExpired").asBoolean();
|
||||
boolean accountNonLocked = readJsonNode(jsonNode, "accountNonLocked").asBoolean();
|
||||
User result = new User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked,
|
||||
authorities);
|
||||
if (passwordNode.asText(null) == null) {
|
||||
result.eraseCredentials();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* This method will create {@link User} object. It will ensure successful object
|
||||
* creation even if password key is null in serialized json, because credentials may
|
||||
* be removed from the {@link User} by invoking {@link User#eraseCredentials()}. In
|
||||
* that case there won't be any password key in serialized json.
|
||||
*
|
||||
* @param jp the JsonParser
|
||||
* @param ctxt the DeserializationContext
|
||||
* @return the user
|
||||
* @throws IOException if a exception during IO occurs
|
||||
* @throws JsonProcessingException if an error during JSON processing occurs
|
||||
*/
|
||||
@Override
|
||||
public SysUserDetails deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
|
||||
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
|
||||
JsonNode jsonNode = mapper.readTree(jp);
|
||||
Set<? extends GrantedAuthority> authorities = mapper.convertValue(jsonNode.get("authorities"),
|
||||
SIMPLE_GRANTED_AUTHORITY_SET);
|
||||
JsonNode passwordNode = readJsonNode(jsonNode, "password");
|
||||
Long userId = readJsonNode(jsonNode, "userId").asLong();
|
||||
String username = readJsonNode(jsonNode, "username").asText();
|
||||
String password = passwordNode.asText("");
|
||||
boolean enabled = readJsonNode(jsonNode, "enabled").asBoolean();
|
||||
boolean accountNonExpired = readJsonNode(jsonNode, "accountNonExpired").asBoolean();
|
||||
boolean credentialsNonExpired = readJsonNode(jsonNode, "credentialsNonExpired").asBoolean();
|
||||
boolean accountNonLocked = readJsonNode(jsonNode, "accountNonLocked").asBoolean();
|
||||
SysUserDetails result = new SysUserDetails(userId, username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked,
|
||||
authorities);
|
||||
if (passwordNode.asText(null) == null) {
|
||||
result.eraseCredentials();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonNode readJsonNode(JsonNode jsonNode, String field) {
|
||||
return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
|
||||
}
|
||||
private JsonNode readJsonNode(JsonNode jsonNode, String field) {
|
||||
return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,14 +6,17 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/**
|
||||
* @see org.springframework.security.jackson2.UserMixin
|
||||
* SysUserDetails 反序列化注册
|
||||
*
|
||||
* 刷新模式根据 refresh_token 从 oauth2_authorization 表中获取字段 attributes 内容反序列化成
|
||||
*
|
||||
* @author haoxr
|
||||
* @see org.springframework.security.jackson2.UserMixin
|
||||
* @since 2023/7/4
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
|
||||
@JsonDeserialize(using = SysUserDeserializer.class)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class SysUseMixin {
|
||||
public class SysUserMixin {
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
package com.youlai.auth.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.nimbusds.jose.JWSObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.util.Base64;
|
||||
|
||||
@Slf4j
|
||||
public class RequestUtils {
|
||||
|
||||
/**
|
||||
* 获取登录认证的客户端ID
|
||||
* <p>
|
||||
* 兼容两种方式获取OAuth2客户端信息(client_id、client_secret)
|
||||
* 方式一:client_id、client_secret放在请求路径中
|
||||
* 方式二:放在请求头(Request Headers)中的Authorization字段,且经过加密,例如 Basic Y2xpZW50OnNlY3JldA== 明文等于 client:secret
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getClientId() {
|
||||
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
|
||||
// 从请求路径中获取
|
||||
String clientId = request.getParameter("client_id");
|
||||
if (StrUtil.isNotBlank(clientId)) {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
// 从请求头获取
|
||||
String basic = request.getHeader("Authorization");
|
||||
if (StrUtil.isNotBlank(basic) && basic.startsWith("Basic ")) {
|
||||
basic = basic.replace("Basic ", "");
|
||||
String basicPlainText = new String(Base64.getDecoder().decode(basic.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
|
||||
clientId = basicPlainText.split(":")[0]; //client:secret
|
||||
}
|
||||
return clientId;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取JWT Payload
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getJwtPayload() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
String payload = null;
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if (StrUtil.isNotBlank(authorization) && StrUtil.startWithIgnoreCase(authorization, "Bearer ")) {
|
||||
authorization = StrUtil.replaceIgnoreCase(authorization, "Bearer ", "");
|
||||
try {
|
||||
payload = JWSObject.parse(authorization).getPayload().toString();
|
||||
} catch (ParseException e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user