Spring Security:未调用自定义 UserDetailsService(使用 Auth0 身份验证)
Posted
技术标签:
【中文标题】Spring Security:未调用自定义 UserDetailsService(使用 Auth0 身份验证)【英文标题】:Spring Security: Custom UserDetailsService not being called (using Auth0 authentication) 【发布时间】:2018-12-06 08:27:51 【问题描述】:我是 Spring 框架的新手,所以对于我的理解中的任何漏洞,我提前道歉。
我正在使用 Auth0 来保护我的 API,它运行良好。我的设置和配置与 Auth0 文档中的 suggested setup 相同:
// SecurityConfig.java
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
// auth0 config vars here
@Override
protected void configure(HttpSecurity http)
JwtWebSecurityConfigurer
.forRS256(apiAudience, issuer)
.configure(http)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/public").permitAll()
.antMatchers(HttpMethod.GET, "/api/private").authenticated();
通过此设置,spring 安全主体被设置为来自 jwt 令牌的 userId (sub
):auth0|5b2b...
。但是,我希望将其设置为匹配的用户(来自我的数据库),而不仅仅是 userId。我的问题是如何做到这一点。
我尝试过的
我已经尝试实现从this tutorial 复制的自定义数据库支持的 UserDetailsService。 但是,无论我如何尝试将它添加到我的 conf 中,它都不会被调用。我尝试了几种不同的方式添加它但没有效果:
// SecurityConfig.java (changes only)
// My custom userDetailsService, overriding the loadUserByUsername
// method from Spring Framework's UserDetailsService.
@Autowired
private MyUserDetailsService userDetailsService;
protected void configure(HttpSecurity http)
http.userDetailsService(userDetailsService); // Option 1
http.authenticationProvider(authenticationProvider()); // Option 2
JwtWebSecurityConfigurer
[...] // The rest unchanged from above
@Override // Option 3 & 4: Override the following method
protected void configure(AuthenticationManagerBuilder auth)
auth.authenticationProvider(authenticationProvider()); // Option 3
auth.userDetailsService(userDetailsService); // Option 4
@Bean // Needed for Options 2 or 4
public DaoAuthenticationProvider authenticationProvider()
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
return authProvider;
不幸的是,由于我需要将其与 Auth0 身份验证结合使用,因此没有一个类似的“未调用 userDetails”问题对我有帮助。
我不确定我是否走在正确的道路上。在这个极其常见的用例中,我无法从 Auth0 中找到 任何 文档,这对我来说似乎很奇怪,所以我可能遗漏了一些明显的东西。
PS:不确定是否相关,但在初始化期间始终会记录以下内容。
Jun 27, 2018 11:25:22 AM com.test.UserRepository initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.
编辑 1:
根据 Ashish451 的回答,我尝试复制他的 CustomUserDetailsService,并将以下内容添加到我的 SecurityConfig:
@Autowired
private CustomUserDetailsService userService;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
@Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception
auth.userDetailsService( userService );
很遗憾,由于这些更改,CustomUserDetailsService 仍未被调用。
编辑 2:
添加@Norberto Ritzmann 建议的日志记录方法时的输出:
Jul 04, 2018 3:49:22 PM com.test.repositories.UserRepositoryImpl initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.
Jul 04, 2018 3:49:22 PM com.test.runner.JettyRunner testUserDetailsImpl
INFO: UserDetailsService implementation: com.test.services.CustomUserDetailsService
【问题讨论】:
只有实现自定义数据库支持的 UserDetailsService 才能工作。我的猜测是您需要将@ComponentScan 添加到Spring 中找出您的包中的@Service 类。 我确实尝试过(参考@git-flo的回答),但不幸的是没有效果。由于 auth0 处理身份验证并设置安全主体,我相信 auth0 可能会覆盖我自定义 userDetails 的尝试。不过,我不确定如何检验该理论。 好的,这是一个很好的电话。试着把这个方法放在你的主类上,看看它记录了什么:@Autowired public void testUserDetailsImpl(UserDetailsService service) log.info("UserDetailsService implementation: " + service.getClass().getName()); 我在问题的编辑中添加了输出。所以看起来它正在被设置。我可能还应该提到正在调用服务的构造函数。只是没有一种方法。我的 auth0 实现是否仍有可能覆盖它?或者因为我没有实现自定义过滤器链,也许它只是从未被 Spring Security 使用过?我当然没有显式调用任何服务方法,所以我有点依赖 Spring Security 来决定它需要一个 UserDetailsService 和其中的方法。 Auth0 可能覆盖了默认的安全行为并且没有使用 UserDetailsService。 【参考方案1】:查看您的适配器代码,您正在配置本身中生成 JWT 令牌。 我不确定 apiAudience 是什么,发行者,但我猜它生成了 JWT 的子集。 您的问题是您想根据您的数据库更改 JWT 子。
我最近在 Spring Boot 应用程序中实现了 JWT 安全性。
我在从数据库中获取用户名后设置它。
为了清楚起见,我添加了带有 pkg 信息的代码。
// 我的适配器类。除了 我添加了一个过滤器之外,它和你的一样。在这个过滤器中,我正在验证 JWT 令牌。每次触发 Secured Rest URL 时都会调用此过滤器。
import java.nio.charset.StandardCharsets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import com.dev.myapp.jwt.model.CustomUserDetailsService;
import com.dev.myapp.security.RestAuthenticationEntryPoint;
import com.dev.myapp.security.TokenAuthenticationFilter;
import com.dev.myapp.security.TokenHelper;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
@Bean
public PasswordEncoder passwordEncoder()
return new BCryptPasswordEncoder();
@Autowired
private CustomUserDetailsService jwtUserDetailsService; // Get UserDetail bu UserName
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint; // Handle any exception during Authentication
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
// Binds User service for User and Password Query from Database with Password Encryption
@Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception
auth.userDetailsService( jwtUserDetailsService )
.passwordEncoder( passwordEncoder() );
@Autowired
TokenHelper tokenHelper; // Contains method for JWT key Generation, Validation and many more...
@Override
protected void configure(HttpSecurity http) throws Exception
http
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
.exceptionHandling().authenticationEntryPoint( restAuthenticationEntryPoint ).and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(new TokenAuthenticationFilter(tokenHelper, jwtUserDetailsService), BasicAuthenticationFilter.class);
http.csrf().disable();
// Patterns to ignore from JWT security check
@Override
public void configure(WebSecurity web) throws Exception
// TokenAuthenticationFilter will ignore below paths
web.ignoring().antMatchers(
HttpMethod.POST,
"/auth/login"
);
web.ignoring().antMatchers(
HttpMethod.GET,
"/",
"/assets/**",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
);
//获取用户详细信息的用户服务
@Transactional
@Repository
public class CustomUserDetailsService implements UserDetailsService
protected final Log LOGGER = LogFactory.getLog(getClass());
@Autowired
private UserRepo userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
User uu = userRepository.findByUsername(username);
if (user == null)
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
else
return user;
// 未经授权的访问处理程序
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException
// This is invoked when user tries to access a secured REST resource without supplying any credentials
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
// 验证 JWT 令牌的过滤器链
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;
public class TokenAuthenticationFilter extends OncePerRequestFilter
protected final Log logger = LogFactory.getLog(getClass());
private TokenHelper tokenHelper;
private UserDetailsService userDetailsService;
public TokenAuthenticationFilter(TokenHelper tokenHelper, UserDetailsService userDetailsService)
this.tokenHelper = tokenHelper;
this.userDetailsService = userDetailsService;
@Override
public void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws IOException, ServletException
String username;
String authToken = tokenHelper.getToken(request);
logger.info("AuthToken: "+authToken);
if (authToken != null)
// get username from token
username = tokenHelper.getUsernameFromToken(authToken);
logger.info("UserName: "+username);
if (username != null)
// get user
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenHelper.validateToken(authToken, userDetails))
// create authentication
TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
authentication.setToken(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication); // Adding Token in Security COntext
else
logger.error("Something is wrong with Token.");
chain.doFilter(request, response);
// TokenBasedAuthentication 类
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
public class TokenBasedAuthentication extends AbstractAuthenticationToken
private static final long serialVersionUID = -8448265604081678951L;
private String token;
private final UserDetails principle;
public TokenBasedAuthentication( UserDetails principle )
super( principle.getAuthorities() );
this.principle = principle;
public String getToken()
return token;
public void setToken( String token )
this.token = token;
@Override
public boolean isAuthenticated()
return true;
@Override
public Object getCredentials()
return token;
@Override
public UserDetails getPrincipal()
return principle;
// JWT 生成和验证逻辑的辅助类
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.dev.myapp.common.TimeProvider;
import com.dev.myapp.entity.User;
@Component
public class TokenHelper
protected final Log LOGGER = LogFactory.getLog(getClass());
@Value("$app.name") // reading details from property file added in Class path
private String APP_NAME;
@Value("$jwt.secret")
public String SECRET;
@Value("$jwt.licenseSecret")
public String LICENSE_SECRET;
@Value("$jwt.expires_in")
private int EXPIRES_IN;
@Value("$jwt.mobile_expires_in")
private int MOBILE_EXPIRES_IN;
@Value("$jwt.header")
private String AUTH_HEADER;
@Autowired
TimeProvider timeProvider; // return current time. Basically Deployment time.
private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;
// Generate Token based on UserName. You can Customize this
public String generateToken(String username)
String audience = generateAudience();
return Jwts.builder()
.setIssuer( APP_NAME )
.setSubject(username)
.setAudience(audience)
.setIssuedAt(timeProvider.now())
.setExpiration(generateExpirationDate())
.signWith( SIGNATURE_ALGORITHM, SECRET )
.compact();
public Boolean validateToken(String token, UserDetails userDetails)
User user = (User) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getIssuedAtDateFromToken(token);
return (
username != null &&
username.equals(userDetails.getUsername())
);
// If Token is valid will extract all claim else throw appropriate error
private Claims getAllClaimsFromToken(String token)
Claims claims;
try
claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
catch (Exception e)
LOGGER.error("Could not get all claims Token from passed token");
claims = null;
return claims;
private Date generateExpirationDate()
long expiresIn = EXPIRES_IN;
return new Date(timeProvider.now().getTime() + expiresIn * 1000);
对于这个日志
No authentication manager set. Reauthentication of users when changing passwords
由于您尚未实现名称为 loadUserByUsername 的方法。您正在获取此日志。
编辑 1:
我使用过滤器链只是为了验证令牌并在安全上下文中添加用户,这将从令牌中提取......
我正在使用 JWT,而您正在使用 AuthO,只是实现方式不同。为完整的工作流程添加了完整的实现。
您专注于从 WebSecurityConfig 类实现 authenticationManagerBean 和 configureGlobal 以使用 UserService。
和 TokenBasedAuthentication 类实现。
您可以跳过的其他内容。
【讨论】:
感谢您的回答。如果我的理解是正确的,那么您正在实现一个完整的过滤器链,它允许您在 TokenAuthenticationFilter 类中设置自定义 userDetails。不幸的是,我在遵循您的示例时遇到了一些麻烦,因为我正在使用 Auth0 来处理令牌身份验证。我没有看到任何方法可以在不放弃 Auth0 身份验证以支持自定义过滤器链的情况下实施您的解决方案。您对此有什么建议吗? 刚刚添加了更多细节 感谢您的更新。我实施了您建议的大部分更改(显示在我的问题的编辑中)。但是,我看到您仅在 TokenAuthenticationFilter 中使用 TokenBasedAuthentication 类。鉴于我没有 TokenAuthenticationFilter 类,我应该如何实现 TokenBasedAuthentication?【参考方案2】:可能这是一个spring-boot上下文初始化问题,意味着@Autowired
注解在Configuration类的初始化过程中无法解析。
您可以尝试在 Configuration 类顶部添加 @ComponentScan()
注释并显式加载您的 MyUserDetailsService
。 (见:https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-configuration-classes.html#using-boot-importing-configuration)。完成此操作后,我会在您的配置类中推荐以下内容:
@Autowired
private MyUserDetailsService userService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
auth.userDetailsService(userService);
希望对您有所帮助。
【讨论】:
还是什么都没有。未调用自定义 userService。由于我尝试过的解决方案数量众多,我目前的想法是 Auth0 实现会覆盖我的自定义 userService。毕竟,它确实设置了 spring 安全主体。【参考方案3】:我最终询问了 Auth0 支持,他们说目前无法在不修改库源的情况下修改主体。
不过,他们提供了一种替代方法,即使用 JWT 验证库(例如 https://github.com/auth0/java-jwt)而不是他们的 Spring Security API SDK。
我的解决方案是修改我的代码以仅使用令牌作为主体。
【讨论】:
实际上我遇到了类似的问题。虽然我没有使用 Auth0。请参阅我发布的这个问题和它下面的 cmets:***.com/questions/57155789/…【参考方案4】:您可以使用覆盖的authenticate
方法扩展JwtAuthenticationProvider
,这会将您的用户放入Authentication
对象:
com.auth0:auth0:1.14.2
、com.auth0:auth0-spring-security-api:1.2.5
、com.auth0:jwks-rsa:0.8.3
注意:以下代码 sn-ps 中可能存在一些错误,因为我已将 kotlin 代码手动转换为 java
照常配置SecurityConfig
,但通过修改后的身份验证提供程序:
@Autowired UserService userService;
...
@Override
protected void configure(HttpSecurity http)
// same thing used in usual method `JwtWebSecurityConfigurer.forRS256(String audience, String issuer)`
JwkProvider jwkProvider = JwkProviderBuilder(issuer).build()
// provider deduced from existing default one
Auth0UserAuthenticationProvider authenticationProvider = new Auth0UserAuthenticationProvider(userService, jwkProvider, issuer, audience)
JwtWebSecurityConfigurer
.forRS256(apiAudience, issuer, authenticationProvider)
.configure(http)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/public").permitAll()
.antMatchers(HttpMethod.GET, "/api/private").authenticated();
扩展默认JwtAuthenticationProvider
,通常用于方法JwtWebSecurityConfigurer.forRS256(String audience, String issuer)
public class Auth0UserAuthenticationProvider extends JwtAuthenticationProvider
private final UserService userService;
public (UserService userService, JwkProvider jwkProvider, String issuer, String audience)
super(jwkProvider, issuer, audience);
this.userService = userService;
/**
* Intercept Authentication object before it is set in context
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
Authentication jwtAuth = super.authenticate(authentication);
// Use your service and get user details here
User user = userService.getDetailsOrWhatever(jwtAuth.getPrincipal().toString());
// TODO implement following class which merges Auth0 provided details with your user
return new MyAuthentication(jwtAuth, user);
实现您自己的 MyAuthentication.class
,它将覆盖 getDetails()
并返回实际用户,而不是 Auth0 库提供的解码令牌。
之后用户将在
中可用SecurityContextHolder.getContext().getAuthentication().getDetails();
【讨论】:
这种方法的唯一问题是 JwtAuthenticationProvider 是一个最终类,你不能扩展它。 它不是final
中的com.auth0:auth0-spring-security-api:1.2.5
,如源代码JwtAuthenticationProvider.java 中所见
JwtAuthenticationProvider 的包名是什么?您可能正在谈论来自 auth0 库的一个。我说的是 org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider
它是来自 auth0 的那个,如之前的评论中所述。我也试图在答案中说清楚。【参考方案5】:
如果您只想坚持使用 Spring 库,我最终会这样做。您可以更改 Auth0UserDetailsService 以从任何来源查询用户详细信息。
public class Auth0JwtAuthenticationProvider implements AuthenticationProvider
@Autowired
ApplicationContext context;
@Autowired
@Qualifier("auth0UserDetailsService")
UserDetailsService auth0UserDetailsService;
private JwtAuthenticationProvider jwtAuthenticationProvider;
public Auth0JwtAuthenticationProvider()
@PostConstruct
public void postConstruct()
jwtAuthenticationProvider = new JwtAuthenticationProvider(context.getBean(JwtDecoder.class));
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
Authentication token = jwtAuthenticationProvider.authenticate(authentication);
((AbstractAuthenticationToken) token).setDetails(auth0UserDetailsService.loadUserByUsername(authentication.getName()));
return token;
@Override
public boolean supports(Class<?> authentication)
return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
对于 UserDetailsService,我正在使用服务获取用户详细信息。
public class Auth0UserDetailsService implements UserDetailsService
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
RestTemplate restTemplate = new RestTemplate();
.....
ResponseEntity<Auth0UserDetails> responseEntity = restTemplate.exchange(url, HttpMethod.GET, entity, Auth0UserDetails.class);
return responseEntity.getBody();
.....
UserDetails 将包含您需要的所有详细信息。
public class Auth0UserDetails implements UserDetails
@JsonProperty("name")
private String userName;
@JsonProperty("created_at")
private LocalDateTime createdAt;
@JsonProperty("email")
private String email;
@JsonProperty("email_verified")
private boolean isEmailVerified;
@JsonProperty("nickname")
private String nickName;
@JsonProperty("picture")
private String pictureURL;
@JsonProperty("updated_at")
private LocalDateTime updatedAt;
@JsonProperty("user_id")
private String userID;
... //Getter 和 Setter 以及被覆盖的方法。
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter
@Bean
public Auth0JwtAuthenticationProvider auth0JwtAuthenticationProvider()
return new Auth0JwtAuthenticationProvider();
@Override
public void configure(HttpSecurity httpSecurity) throws Exception
httpSecurity.authenticationProvider(auth0JwtAuthenticationProvider()).authorizeRequests().
....
.oauth2ResourceServer().jwt();
【讨论】:
以上是关于Spring Security:未调用自定义 UserDetailsService(使用 Auth0 身份验证)的主要内容,如果未能解决你的问题,请参考以下文章
Spring security - 未调用自定义 userDetailsService 实现
Spring Security:未调用自定义 UserDetailsService(使用 Auth0 身份验证)
未调用 Spring Security j_spring_security_check
Spring安全性j_spring_security_check调用给出404未找到错误[关闭]