diff --git a/youlai-auth/pom.xml b/youlai-auth/pom.xml index 3a682aabb..f18cf82ec 100644 --- a/youlai-auth/pom.xml +++ b/youlai-auth/pom.xml @@ -43,11 +43,6 @@ - - org.springframework.boot - spring-boot-starter-security - - org.springframework.security spring-security-oauth2-authorization-server diff --git a/youlai-auth/src/main/java/com/youlai/auth/authentication/captcha/CaptchaAuthenticationProvider.java b/youlai-auth/src/main/java/com/youlai/auth/authentication/captcha/CaptchaAuthenticationProvider.java index b288c595c..3c12391a0 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/authentication/captcha/CaptchaAuthenticationProvider.java +++ b/youlai-auth/src/main/java/com/youlai/auth/authentication/captcha/CaptchaAuthenticationProvider.java @@ -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) diff --git a/youlai-auth/src/main/java/com/youlai/auth/authentication/wxminiapp/WxMiniAppAuthenticationConverter.java b/youlai-auth/src/main/java/com/youlai/auth/authentication/miniapp/WxMiniAppAuthenticationConverter.java similarity index 98% rename from youlai-auth/src/main/java/com/youlai/auth/authentication/wxminiapp/WxMiniAppAuthenticationConverter.java rename to youlai-auth/src/main/java/com/youlai/auth/authentication/miniapp/WxMiniAppAuthenticationConverter.java index 4b81f7864..1c924894e 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/authentication/wxminiapp/WxMiniAppAuthenticationConverter.java +++ b/youlai-auth/src/main/java/com/youlai/auth/authentication/miniapp/WxMiniAppAuthenticationConverter.java @@ -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; diff --git a/youlai-auth/src/main/java/com/youlai/auth/authentication/wxminiapp/WxMiniAppAuthenticationProvider.java b/youlai-auth/src/main/java/com/youlai/auth/authentication/miniapp/WxMiniAppAuthenticationProvider.java similarity index 99% rename from youlai-auth/src/main/java/com/youlai/auth/authentication/wxminiapp/WxMiniAppAuthenticationProvider.java rename to youlai-auth/src/main/java/com/youlai/auth/authentication/miniapp/WxMiniAppAuthenticationProvider.java index 9869ef3a9..f82624234 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/authentication/wxminiapp/WxMiniAppAuthenticationProvider.java +++ b/youlai-auth/src/main/java/com/youlai/auth/authentication/miniapp/WxMiniAppAuthenticationProvider.java @@ -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; diff --git a/youlai-auth/src/main/java/com/youlai/auth/authentication/wxminiapp/WxMiniAppAuthenticationToken.java b/youlai-auth/src/main/java/com/youlai/auth/authentication/miniapp/WxMiniAppAuthenticationToken.java similarity index 97% rename from youlai-auth/src/main/java/com/youlai/auth/authentication/wxminiapp/WxMiniAppAuthenticationToken.java rename to youlai-auth/src/main/java/com/youlai/auth/authentication/miniapp/WxMiniAppAuthenticationToken.java index 6977301c4..edcfd3b2b 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/authentication/wxminiapp/WxMiniAppAuthenticationToken.java +++ b/youlai-auth/src/main/java/com/youlai/auth/authentication/miniapp/WxMiniAppAuthenticationToken.java @@ -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; diff --git a/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationConverter.java b/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationConverter.java index 7acf99857..c555eaa11 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationConverter.java +++ b/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationConverter.java @@ -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)) { diff --git a/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationProvider.java b/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationProvider.java index f1c7f57e7..c7b99700d 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationProvider.java +++ b/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationProvider.java @@ -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; * 密码模式身份验证提供者 *

* 处理基于用户名和密码的身份验证 - * * @author haoxr * @since 3.0.0 */ @@ -86,7 +87,12 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider { Map 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); } + + } diff --git a/youlai-auth/src/main/java/com/youlai/auth/config/AuthorizationServerConfig.java b/youlai-auth/src/main/java/com/youlai/auth/config/AuthorizationServerConfig.java index ee67691f9..f6be85366 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/config/AuthorizationServerConfig.java +++ b/youlai-auth/src/main/java/com/youlai/auth/config/AuthorizationServerConfig.java @@ -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 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(); } diff --git a/youlai-auth/src/main/java/com/youlai/auth/config/SecurityConfig.java b/youlai-auth/src/main/java/com/youlai/auth/config/DefaultSecurityConfig.java similarity index 85% rename from youlai-auth/src/main/java/com/youlai/auth/config/SecurityConfig.java rename to youlai-auth/src/main/java/com/youlai/auth/config/DefaultSecurityConfig.java index 7800bbdf9..c7e34f790 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/config/SecurityConfig.java +++ b/youlai-auth/src/main/java/com/youlai/auth/config/DefaultSecurityConfig.java @@ -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 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(); } diff --git a/youlai-auth/src/main/java/com/youlai/auth/config/JwtCustomizerConfig.java b/youlai-auth/src/main/java/com/youlai/auth/config/JwtTokenClaimsConfig.java similarity index 92% rename from youlai-auth/src/main/java/com/youlai/auth/config/JwtCustomizerConfig.java rename to youlai-auth/src/main/java/com/youlai/auth/config/JwtTokenClaimsConfig.java index bcf85d11a..505325996 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/config/JwtCustomizerConfig.java +++ b/youlai-auth/src/main/java/com/youlai/auth/config/JwtTokenClaimsConfig.java @@ -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 How-to: Authorize an access token containing custom authorities * @since 2023/7/4 */ @Configuration @RequiredArgsConstructor -public class JwtCustomizerConfig { +public class JwtTokenClaimsConfig { private final RedisTemplate redisTemplate; @Bean - public OAuth2TokenCustomizer jwtCustomizer() { + public OAuth2TokenCustomizer 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 diff --git a/youlai-auth/src/main/java/com/youlai/auth/controller/AuthController.java b/youlai-auth/src/main/java/com/youlai/auth/controller/AuthController.java index a6412a5cd..3fc0c37fe 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/controller/AuthController.java +++ b/youlai-auth/src/main/java/com/youlai/auth/controller/AuthController.java @@ -3,6 +3,8 @@ package com.youlai.auth.controller; import org.springframework.web.bind.annotation.RestController; /** + * 认证控制器 + * * @author haoxr * @since 2023/6/29 */ diff --git a/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationFailureHandler.java b/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationFailureHandler.java new file mode 100644 index 000000000..6f772dfcb --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationFailureHandler.java @@ -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 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); + } +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationSuccessHandler.java b/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationSuccessHandler.java index 011249461..6fd60b4d8 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationSuccessHandler.java +++ b/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationSuccessHandler.java @@ -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 accessTokenHttpResponseConverter = - new OAuth2AccessTokenResponseHttpMessageConverter(); + private final HttpMessageConverter accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter(); private Converter> 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 Authentication 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 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); } } diff --git a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/SysUserDetails.java b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/SysUserDetails.java index e0319f6e2..065681812 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/SysUserDetails.java +++ b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/SysUserDetails.java @@ -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 authorities; + private Collection authorities; + + private boolean accountNonExpired; + + private boolean accountNonLocked; + + private boolean credentialsNonExpired; private Set 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 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 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; + } } diff --git a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/SysUserDetailsService.java b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/SysUserDetailsService.java index f7b056a65..350cdd05e 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/SysUserDetailsService.java +++ b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/SysUserDetailsService.java @@ -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 { *

* 用户名、密码用于后续认证,认证成功之后将权限授予用户 * - * @param username 前端登录表单的用户名 + * @param username 用户名 * @return {@link SysUserDetails} */ @Override public UserDetails loadUserByUsername(String username) { Result 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()); } } diff --git a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserDeserializer.java b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserDeserializer.java index 055c95ab7..79ad209b7 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserDeserializer.java +++ b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserDeserializer.java @@ -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 { +class SysUserDeserializer extends JsonDeserializer { - private static final TypeReference> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference>() { - }; + private static final TypeReference> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference>() { + }; - /** - * 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 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 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(); + } } diff --git a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUseMixin.java b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserMixin.java similarity index 80% rename from youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUseMixin.java rename to youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserMixin.java index f87302dff..bb444e757 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUseMixin.java +++ b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserMixin.java @@ -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 { } diff --git a/youlai-auth/src/main/java/com/youlai/auth/util/RequestUtils.java b/youlai-auth/src/main/java/com/youlai/auth/util/RequestUtils.java deleted file mode 100644 index f31ec3980..000000000 --- a/youlai-auth/src/main/java/com/youlai/auth/util/RequestUtils.java +++ /dev/null @@ -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 - *

- * 兼容两种方式获取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; - } - - -}