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>
<!-- 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>

View File

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

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 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.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 org.springframework.security.core.Authentication;

View File

@ -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)) {

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -3,6 +3,8 @@ package com.youlai.auth.controller;
import org.springframework.web.bind.annotation.RestController;
/**
* 认证控制器
*
* @author haoxr
* @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;
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);
}
}

View File

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

View File

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

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

View File

@ -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 {
}

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