尝试让 UserDetailsService 为我设置的 oauth2 资源服务器工作。我能够成功验证 jwt,但我似乎没有做任何事情来让它调用 loadUserByUsername 方法。最初使用 SAML 并且它可以工作,但现在我已经切换到 Oauth2,但我无法让它工作。
@Service
public class OauthUsersDetailsServiceImpl implements UserDetailsService{
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//some user loading junk here - this is never called
}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception
{
//test key for now
SecretKeySpec key = new SecretKeySpec("private key0000000000000000000000000000000".getBytes(), "HMACSHA256");
http
.authorizeRequests()
.antMatchers(/*some endpoints im excluding from auth - this all works*/)
.permitAll().and()
.authorizeRequests()
.anyRequest().authenticated().and()
.oauth2ResourceServer().jwt().decoder(NimbusJwtDecoder.withSecretKey(key).build());
}
}
我通过谷歌发现,我可以将类注册为带有 @service 的 bean,spring 会直接拾取它,但它不起作用。我还尝试通过 AuthenticationManagerBuilder 添加它,但这也不起作用。我的猜测是,jwt 方面有自己的 UserDetailsService,它已实现,并且优先于我的。也就是说,让我的调用的正确方法是什么,或者在身份验证完成后以某种方式手动调用我的用户加载逻辑并覆盖主体对象是否更好?我需要在调用端点之前发生这种情况,以便 PreAuthorize 可以检查 UserDetailsService 加载的角色。
想通了。希望这对遇到同样问题的人有所帮助。我必须在链中添加一个自定义过滤器来调用我的用户详细信息服务并覆盖上下文:
public class Oauth2AuthorizationFilter extends GenericFilterBean {
@Autowired
private OauthUsersDetailsServiceImpl oauthUsersDetailsServiceImpl;
public Oauth2AuthorizationFilter (OauthUsersDetailsServiceImpl userDetailsService) {
this.oauthUsersDetailsServiceImpl = userDetailsService;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.getContext();
if(context.getAuthentication() != null && !(context.getAuthentication().getPrincipal() instanceof Users)) {
UserDetails user = oauthUsersDetailsServiceImpl.loadUserByUsername(((Jwt)context.getAuthentication().getPrincipal()).getClaimAsString("user_name"));
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
context.setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
//test key for now
SecretKeySpec key = new SecretKeySpec("private key0000000000000000000000000000000".getBytes(), "HMACSHA256");
http.authorizeRequests().antMatchers(/*bunch of junk...*/).permitAll().and().authorizeRequests().anyRequest().authenticated().and()
.oauth2ResourceServer().jwt().decoder(NimbusJwtDecoder.withSecretKey(key).build());
http.addFilterAfter(jwtAuthTokenFilterBean(), SwitchUserFilter.class);
}
终于满足了我的需要
问题是
JwtAuthenticationProvider
不会调用 UserDetailService
- 它假设 JWT 具有所有相关的身份验证信息 - 因此无需通过 UserDetailService
来获取权限等。
因此,您要做的就是构建一个 JWT/令牌转换器,从 jwt 中提取用户名并使用
DaoAuthenticationProvider
进行身份验证(这将调用您的 UserDetailsService
)。此外,由于密码将为空,因此您必须使用具有 noop DaoAuthenticationProvider
方法的版本覆盖 additionalAuthenticationChecks
。
这是适合我的代码:
@Configuration
class OAuthSecurityConfiguration() :
WebSecurityConfigurerAdapter() {
/*
Override the default DaoAuthenticationProvider to prevent password validity checks since they will not be set
*/
@Bean
fun daoAuthenticationProvider(userDetailsService: UserDetailsService): DaoAuthenticationProvider {
val daoAuthenticationProvider = object : DaoAuthenticationProvider() {
override fun additionalAuthenticationChecks(
userDetails: UserDetails,
authentication: UsernamePasswordAuthenticationToken
) {
// Do nothing as the password will be set to null
}
}
daoAuthenticationProvider.setUserDetailsService(userDetailsService)
return daoAuthenticationProvider
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.regexMatchers(
"/customers.*",
"/accounts.*",
"/administrators.*"
)
.authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter { jwt ->
convertJwtToUsernamePasswordToken(jwt)
}
}
private fun convertJwtToUsernamePasswordToken(
jwt: Jwt
): AbstractAuthenticationToken {
val username = jwt.getClaimAsString("username") // whichever claim you use to transmit the lookup key in the token
val userPasswordToken = UsernamePasswordAuthenticationToken(username, null)
return authenticationManager().authenticate(userPasswordToken) as AbstractAuthenticationToken
}
}
您需要注册 UserDetailsService 实现,然后由 DaoAuthenticationProvider 使用
// userDetailsService bean
@Autowired
private OauthUsersDetailsServiceImpl oauthUsersDetailsServiceImpl;
//
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(oauthUsersDetailsServiceImpl);
}
之前的帖子是正确的,UserDetailsService 不会被 oauth2ResourceServer.jwt() 调用。
您可以做的一件事是利用 Spring Security 通过
JwtAuthenticationToken
注释返回的 @AuthenticationPrincipal
。您可以使用扩展此注释来自动加载包含所包含信息的用户实体。
首先创建一个方法来执行此操作,如下所示:
public User getUserFromJwt(JwtAuthenticationToken principal) {
try {
User user = ValidateGoogleAuthToken.verifyGoogleAuthToken(principal.getToken().getTokenValue())
.orElseThrow(() -> new RuntimeException("Failed to validate JWT."));
return userRepository.findByUsername(user.getUsername())
.orElseThrow(() -> new UsernameNotFoundException("Unable to get User from JWT"));
} catch (Exception e) {
LOGGER.info("Exception in getUserFromJwt", e);
}
return null;
}
然后创建一个自定义注释,如下所示:
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "@userServiceImpl.getUserFromJwt(#this)")
public @interface CurrentUser {
}
然后您可以将
@CurrentUser
注释添加到控制器以自动加载用户,例如:
public ResponseEntity<String> doSomethingWithAuthenticatedUser(@CurrentUser User user) {
System.out.println("current user: " + user);
return null;
}