feat: 完善OAuth2自定义异常处理逻辑

This commit is contained in:
郝先瑞 2023-07-07 18:21:59 +08:00
parent a07a3c8839
commit 4a943ab531
18 changed files with 231 additions and 219 deletions

View File

@ -43,11 +43,6 @@
</dependency> </dependency>
<!-- OAuth2 认证服务器--> <!-- OAuth2 认证服务器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId> <artifactId>spring-security-oauth2-authorization-server</artifactId>

View File

@ -94,7 +94,7 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider {
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME); String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD); String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 用户名密码身份验证成功后返回带有权限的认证信息 // 用户名密码身份验证成功后返回 带有权限的认证信息
Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
// 访问令牌(Access Token) 构造器 // 访问令牌(Access Token) 构造器
@ -119,6 +119,7 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider {
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName()) .principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) .authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA)

View File

@ -1,4 +1,4 @@
package com.youlai.auth.authentication.wxminiapp; package com.youlai.auth.authentication.miniapp;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.auth.util.OAuth2EndpointUtils; import com.youlai.auth.util.OAuth2EndpointUtils;

View File

@ -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.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;

View File

@ -1,4 +1,4 @@
package com.youlai.auth.authentication.wxminiapp; package com.youlai.auth.authentication.miniapp;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;

View File

@ -2,6 +2,7 @@ package com.youlai.auth.authentication.password;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.auth.util.OAuth2EndpointUtils; import com.youlai.auth.util.OAuth2EndpointUtils;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.AuthorizationGrantType;
@ -32,6 +33,7 @@ public class PasswordAuthenticationConverter implements AuthenticationConverter
@Override @Override
public Authentication convert(HttpServletRequest request) { public Authentication convert(HttpServletRequest request) {
// 授权类型 (必需) // 授权类型 (必需)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {

View File

@ -2,13 +2,15 @@ package com.youlai.auth.authentication.password;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils; import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.*;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; 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.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.OidcIdToken;
@ -35,7 +37,6 @@ import java.util.stream.Collectors;
* 密码模式身份验证提供者 * 密码模式身份验证提供者
* <p> * <p>
* 处理基于用户名和密码的身份验证 * 处理基于用户名和密码的身份验证
*
* @author haoxr * @author haoxr
* @since 3.0.0 * @since 3.0.0
*/ */
@ -86,7 +87,12 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
Map<String, Object> additionalParameters = resourceOwnerPasswordAuthentication.getAdditionalParameters(); Map<String, Object> additionalParameters = resourceOwnerPasswordAuthentication.getAdditionalParameters();
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME); String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD); 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); Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
@ -126,11 +132,15 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
ReflectUtil.setFieldValue(usernamePasswordAuthentication.getPrincipal(), "perms", null);
// 持久化令牌发放记录到数据库
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName()) .principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(AuthorizationGrantType.PASSWORD) .authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizedScopes(authorizedScopes) .authorizedScopes(authorizedScopes)
.attribute(Principal.class.getName(), usernamePasswordAuthentication); .attribute(Principal.class.getName(), usernamePasswordAuthentication); // attribute 字段
if (generatedAccessToken instanceof ClaimAccessor) { if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> authorizationBuilder.token(accessToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims())); metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
@ -197,4 +207,6 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
return PasswordAuthenticationToken.class.isAssignableFrom(authentication); return PasswordAuthenticationToken.class.isAssignableFrom(authentication);
} }
} }

View File

@ -2,6 +2,8 @@
package com.youlai.auth.config; package com.youlai.auth.config;
import cn.binarywang.wx.miniapp.api.WxMaService; 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.Module;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.jwk.JWKSet; 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.SmsCodeAuthenticationConverter;
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationProvider; import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationProvider;
import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationToken; import com.youlai.auth.authentication.smscode.SmsCodeAuthenticationToken;
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationConverter; import com.youlai.auth.authentication.miniapp.WxMiniAppAuthenticationConverter;
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationProvider; import com.youlai.auth.authentication.miniapp.WxMiniAppAuthenticationProvider;
import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationToken; import com.youlai.auth.authentication.miniapp.WxMiniAppAuthenticationToken;
import com.youlai.auth.handler.MyAuthenticationFailureHandler;
import com.youlai.auth.handler.MyAuthenticationSuccessHandler; import com.youlai.auth.handler.MyAuthenticationSuccessHandler;
import com.youlai.auth.userdetails.member.MemberDetailsService; import com.youlai.auth.userdetails.member.MemberDetailsService;
import com.youlai.auth.userdetails.user.SysUserDetails; 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.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.core.JdbcTemplate;
import org.springframework.jdbc.support.lob.DefaultLobHandler; import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.security.authentication.AuthenticationManager; 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.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; 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.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.jackson2.SecurityJackson2Modules; import org.springframework.security.jackson2.SecurityJackson2Modules;
@ -98,22 +104,23 @@ public class AuthorizationServerConfig {
) throws Exception { ) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
new OAuth2AuthorizationServerConfigurer();
authorizationServerConfigurer authorizationServerConfigurer
.tokenEndpoint(tokenEndpoint -> .tokenEndpoint(tokenEndpoint ->
tokenEndpoint tokenEndpoint
.accessTokenRequestConverters(authenticationConverters ->// <1> .accessTokenRequestConverters(
authenticationConverters.addAll( authenticationConverters ->// <1>
List.of( authenticationConverters.addAll(
new PasswordAuthenticationConverter(), List.of(
new CaptchaAuthenticationConverter(), new PasswordAuthenticationConverter(),
new WxMiniAppAuthenticationConverter(), new CaptchaAuthenticationConverter(),
new SmsCodeAuthenticationConverter() new WxMiniAppAuthenticationConverter(),
new SmsCodeAuthenticationConverter()
)
) )
)
) )
.authenticationProviders(authenticationProviders ->// <2> /*.authenticationProviders(authenticationProviders ->// <2>
authenticationProviders.addAll( authenticationProviders.addAll(
List.of( List.of(
new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator), new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator),
@ -122,25 +129,37 @@ public class AuthorizationServerConfig {
new SmsCodeAuthenticationProvider(authorizationService, tokenGenerator, memberDetailsService, redisTemplate) new SmsCodeAuthenticationProvider(authorizationService, tokenGenerator, memberDetailsService, redisTemplate)
) )
) )
) )*/
.accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) .accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) // 自定义成功响应
.errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定义异常响应
); );
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
.securityMatcher(endpointsMatcher) http.securityMatcher(endpointsMatcher)
.authorizeHttpRequests(authorize -> .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
authorize
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer); .apply(authorizationServerConfigurer);
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.parentAuthenticationManager(null);
authenticationManagerBuilder.authenticationProvider(new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator));
return http.build(); return http.build();
} }
/* @Bean
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setHideUserNotFoundExceptions(false) ; // 抛出用户不存在异常
return daoAuthenticationProvider ;
}*/
@Bean // <5> @Bean // <5>
public JWKSource<SecurityContext> jwkSource() { public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey(); KeyPair keyPair = generateRsaKey();
@ -208,7 +227,7 @@ public class AuthorizationServerConfig {
objectMapper.registerModules(securityModules); objectMapper.registerModules(securityModules);
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
// You will need to write the Mixin for your class so Jackson can marshall it. // 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); objectMapper.addMixIn(Long.class, Object.class);
rowMapper.setObjectMapper(objectMapper); rowMapper.setObjectMapper(objectMapper);
service.setAuthorizationRowMapper(rowMapper); service.setAuthorizationRowMapper(rowMapper);
@ -236,11 +255,9 @@ public class AuthorizationServerConfig {
} }
@Bean @Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager(); return authenticationConfiguration.getAuthenticationManager();
} }

View File

@ -6,26 +6,22 @@ import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 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 org.springframework.security.web.SecurityFilterChain;
import java.util.List; import java.util.List;
import static org.springframework.security.config.Customizer.withDefaults; @EnableWebSecurity
@Configuration(proxyBeanMethods = false)
@Configuration public class DefaultSecurityConfig {
@EnableWebSecurity(debug = true)
@RequiredArgsConstructor
public class SecurityConfig {
@Setter @Setter
private List<String> ignoreUrls; private List<String> ignoreUrls;
/** /**
* Spring Security 安全过滤器链配置 * Spring Security 安全过滤器链配置
* *
@ -33,6 +29,7 @@ public class SecurityConfig {
* @return * @return
*/ */
@Bean @Bean
@Order(0)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http http
.authorizeHttpRequests(requestMatcherRegistry -> .authorizeHttpRequests(requestMatcherRegistry ->
@ -45,8 +42,8 @@ public class SecurityConfig {
} }
) )
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.formLogin(withDefaults()) .formLogin(Customizer.withDefaults());
;
return http.build(); return http.build();
} }

View File

@ -8,7 +8,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; 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.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -27,16 +25,17 @@ import java.util.stream.Collectors;
* 自定义 JWT Claims(声明) * 自定义 JWT Claims(声明)
* *
* @author haoxr * @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 * @since 2023/7/4
*/ */
@Configuration @Configuration
@RequiredArgsConstructor @RequiredArgsConstructor
public class JwtCustomizerConfig { public class JwtTokenClaimsConfig {
private final RedisTemplate redisTemplate; private final RedisTemplate redisTemplate;
@Bean @Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() { public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
return context -> { return context -> {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) && context.getPrincipal() instanceof UsernamePasswordAuthenticationToken) { if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) && context.getPrincipal() instanceof UsernamePasswordAuthenticationToken) {
// Customize headers/claims for access_token // Customize headers/claims for access_token
@ -59,7 +58,7 @@ public class JwtCustomizerConfig {
} else if (principal instanceof MemberDetails userDetails) { } else if (principal instanceof MemberDetails userDetails) {
claims.claim("member_id", String.valueOf(userDetails.getId())); claims.claim("member_id", String.valueOf(userDetails.getId()));
}else{ } else {
User user = (User) principal; User user = (User) principal;
// 这里存入角色至JWT解析JWT的角色用于鉴权的位置: ResourceServerConfig#jwtAuthenticationConverter // 这里存入角色至JWT解析JWT的角色用于鉴权的位置: ResourceServerConfig#jwtAuthenticationConverter

View File

@ -3,6 +3,8 @@ package com.youlai.auth.controller;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** /**
* 认证控制器
*
* @author haoxr * @author haoxr
* @since 2023/6/29 * @since 2023/6/29
*/ */

View File

@ -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);
}
}

View File

@ -1,18 +1,18 @@
package com.youlai.auth.handler; package com.youlai.auth.handler;
import cn.hutool.json.JSONUtil;
import com.youlai.common.result.Result; import com.youlai.common.result.Result;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.http.converter.HttpMessageConverter; 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.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.DefaultOAuth2AccessTokenResponseMapConverter; import org.springframework.security.oauth2.core.endpoint.DefaultOAuth2AccessTokenResponseMapConverter;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; 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.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
@ -22,23 +22,24 @@ import java.time.temporal.ChronoUnit;
import java.util.Map; import java.util.Map;
/** /**
* 认证成功处理器
*
* @author haoxr * @author haoxr
* @since 2023/7/3 * @since 2023/7/3
*/ */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = private final HttpMessageConverter<Object> accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();
new OAuth2AccessTokenResponseHttpMessageConverter();
private Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter(); private Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();
/** /**
* * @param request the request which caused the successful authentication
* @see org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter#sendAccessTokenResponse * @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 * @param authentication the <tt>Authentication</tt> object which was created during
* the authentication process. * the authentication process.
* @throws IOException * @throws IOException
* @throws ServletException * @throws ServletException
* @see org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter#sendAccessTokenResponse
*/ */
@Override @Override
@ -66,10 +67,8 @@ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHand
Map<String, Object> tokenResponseParameters = this.accessTokenResponseParametersConverter Map<String, Object> tokenResponseParameters = this.accessTokenResponseParametersConverter
.convert(accessTokenResponse); .convert(accessTokenResponse);
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
response.setCharacterEncoding("UTF-8"); // 自定义认证成功响应数据结构
response.getWriter().write(JSONUtil.toJsonStr(Result.success(tokenResponseParameters))); this.accessTokenHttpResponseConverter.write(Result.success(tokenResponseParameters), null, httpResponse);
} }
} }

View File

@ -4,11 +4,14 @@ import cn.hutool.core.collection.CollectionUtil;
import com.youlai.common.enums.StatusEnum; import com.youlai.common.enums.StatusEnum;
import com.youlai.system.dto.UserAuthInfo; import com.youlai.system.dto.UserAuthInfo;
import lombok.Data; import lombok.Data;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -22,7 +25,7 @@ import java.util.stream.Collectors;
* @since 3.0.0 * @since 3.0.0
*/ */
@Data @Data
public class SysUserDetails implements UserDetails { public class SysUserDetails implements UserDetails, CredentialsContainer {
/** /**
* 扩展字段用户ID * 扩展字段用户ID
@ -45,7 +48,13 @@ public class SysUserDetails implements UserDetails {
private String username; private String username;
private String password; private String password;
private Boolean enabled; private Boolean enabled;
private Collection<SimpleGrantedAuthority> authorities; private Collection<GrantedAuthority> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private Set<String> perms; private Set<String> perms;
@ -67,6 +76,28 @@ public class SysUserDetails implements UserDetails {
this.setPerms(user.getPerms()); 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 @Override
public Collection<? extends GrantedAuthority> getAuthorities() { public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities; return this.authorities;
@ -101,4 +132,10 @@ public class SysUserDetails implements UserDetails {
public boolean isEnabled() { public boolean isEnabled() {
return this.enabled; return this.enabled;
} }
@Override
public void eraseCredentials() {
this.password = null;
}
} }

View File

@ -1,16 +1,13 @@
package com.youlai.auth.userdetails.user; 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.common.result.Result;
import com.youlai.system.api.UserFeignClient; import com.youlai.system.api.UserFeignClient;
import com.youlai.system.dto.UserAuthInfo; import com.youlai.system.dto.UserAuthInfo;
import lombok.RequiredArgsConstructor; 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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
@ -30,26 +27,16 @@ public class SysUserDetailsService implements UserDetailsService {
* <p> * <p>
* 用户名密码用于后续认证认证成功之后将权限授予用户 * 用户名密码用于后续认证认证成功之后将权限授予用户
* *
* @param username 前端登录表单的用户名 * @param username 用户名
* @return {@link SysUserDetails} * @return {@link SysUserDetails}
*/ */
@Override @Override
public UserDetails loadUserByUsername(String username) { public UserDetails loadUserByUsername(String username) {
Result<UserAuthInfo> result = userFeignClient.getUserAuthInfo(username); Result<UserAuthInfo> result = userFeignClient.getUserAuthInfo(username);
if (!Result.isSuccess(result) || result.getData() == null) {
UserAuthInfo userAuthInfo = null; throw new UsernameNotFoundException(StrUtil.format("用户{}不存在",username));
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("该账号已过期!");
} }
return userDetails; return new SysUserDetails(result.getData());
} }
} }

View File

@ -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; package com.youlai.auth.userdetails.user.jackson;
import com.fasterxml.jackson.core.JsonParser; 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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.MissingNode; 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.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User; 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 * 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 * @author Jitendra Singh
* @see SysUserMixin
* @since 4.2 * @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 * 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 * 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 * be removed from the {@link User} by invoking {@link User#eraseCredentials()}. In
* that case there won't be any password key in serialized json. * that case there won't be any password key in serialized json.
* @param jp the JsonParser *
* @param ctxt the DeserializationContext * @param jp the JsonParser
* @return the user * @param ctxt the DeserializationContext
* @throws IOException if a exception during IO occurs * @return the user
* @throws JsonProcessingException if an error during JSON processing occurs * @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 { @Override
ObjectMapper mapper = (ObjectMapper) jp.getCodec(); public SysUserDetails deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode jsonNode = mapper.readTree(jp); ObjectMapper mapper = (ObjectMapper) jp.getCodec();
Set<? extends GrantedAuthority> authorities = mapper.convertValue(jsonNode.get("authorities"), JsonNode jsonNode = mapper.readTree(jp);
SIMPLE_GRANTED_AUTHORITY_SET); Set<? extends GrantedAuthority> authorities = mapper.convertValue(jsonNode.get("authorities"),
JsonNode passwordNode = readJsonNode(jsonNode, "password"); SIMPLE_GRANTED_AUTHORITY_SET);
String username = readJsonNode(jsonNode, "username").asText(); JsonNode passwordNode = readJsonNode(jsonNode, "password");
String password = passwordNode.asText(""); Long userId = readJsonNode(jsonNode, "userId").asLong();
boolean enabled = readJsonNode(jsonNode, "enabled").asBoolean(); String username = readJsonNode(jsonNode, "username").asText();
boolean accountNonExpired = readJsonNode(jsonNode, "accountNonExpired").asBoolean(); String password = passwordNode.asText("");
boolean credentialsNonExpired = readJsonNode(jsonNode, "credentialsNonExpired").asBoolean(); boolean enabled = readJsonNode(jsonNode, "enabled").asBoolean();
boolean accountNonLocked = readJsonNode(jsonNode, "accountNonLocked").asBoolean(); boolean accountNonExpired = readJsonNode(jsonNode, "accountNonExpired").asBoolean();
User result = new User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, boolean credentialsNonExpired = readJsonNode(jsonNode, "credentialsNonExpired").asBoolean();
authorities); boolean accountNonLocked = readJsonNode(jsonNode, "accountNonLocked").asBoolean();
if (passwordNode.asText(null) == null) { SysUserDetails result = new SysUserDetails(userId, username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked,
result.eraseCredentials(); authorities);
} if (passwordNode.asText(null) == null) {
return result; result.eraseCredentials();
} }
return result;
}
private JsonNode readJsonNode(JsonNode jsonNode, String field) { private JsonNode readJsonNode(JsonNode jsonNode, String field) {
return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
} }
} }

View File

@ -6,14 +6,17 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
/** /**
* @see org.springframework.security.jackson2.UserMixin * SysUserDetails 反序列化注册
*
* 刷新模式根据 refresh_token oauth2_authorization 表中获取字段 attributes 内容反序列化成
* *
* @author haoxr * @author haoxr
* @see org.springframework.security.jackson2.UserMixin
* @since 2023/7/4 * @since 2023/7/4
*/ */
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonDeserialize(using = SysUserDeserializer.class) @JsonDeserialize(using = SysUserDeserializer.class)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE) @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class SysUseMixin { public class SysUserMixin {
} }

View File

@ -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_idclient_secret
* 方式一client_idclient_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;
}
}