Spring Security: пользовательский UserDetailsService не вызывается (с использованием аутентификации Auth0)
Я новичок в Spring framework, поэтому заранее приношу извинения за любые зияющие дыры в моем понимании.
Я использую Auth0 для защиты моего API, который отлично работает. Моя настройка и конфигурация такая же, как предложили настройки в документации Auth0:
// 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 устанавливается в userId (sub
) из токена jwt:auth0|5b2b...
. Однако вместо просто userId я хочу, чтобы он был установлен на соответствие пользователь (из моей базы данных). Мой вопрос в том, как это сделать.
что я пробовал
Я попытался реализовать пользовательскую базу данных с поддержкой UserDetailsService, которую я скопировал из в этом уроке. Однако он не вызывается независимо от того, как я пытаюсь добавить его в свою конф. Я попытался добавить его несколькими разными способами без эффекта:
// 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;
}
к сожалению, ни один из подобных "userDetails не вызывается" вопросы помогли мне из-за меня необходимо объединить его с аутентификацией Auth0.
Я не уверен, что я на правильном пути. Мне кажется странным, что я не могу найти любой документация от Auth0 по этому чрезвычайно распространенному случаю использования, поэтому, возможно, я упускаю что-то очевидное.
PS: Не уверен, что это актуально, но во время init всегда регистрируется следующее.
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.
EDIT 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 по-прежнему не вызывается.
EDIT 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
3 ответов
глядя на код адаптера, вы генерируете токен JWT в configure. Не уверен, что apiAudience, эмитент, но он сгенерировал sub JWT, я думаю. Ваша проблема заключается в том, что вы хотите изменить JWT sub в соответствии с вашей базой данных.
недавно я реализовал JWT security в приложении Spring Boot.
и я устанавливаю имя пользователя после его извлечения из базы данных.
я добавил код с информацией pkg для ясности.
// мой адаптер класс!--17-->. Его же, как Ваш, за исключением одной вещи, что Я добавил фильтр к нему. В этом фильтре я проверку подлинности токенов JWT. Этот фильтр будет вызываться каждый раз при запуске защищенного URL-адреса Rest.
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"
);
}
}
/ / User Service для получения сведений о пользователе
@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. Ты получишь этот журнал.
Edit 1:
Я использую цепочку фильтров только для проверки токена и добавления пользователя в контексте безопасности, который будет извлечен из Token....
Я использую JWT, и вы используете AuthO, только реализация отличается. Am добавлена полная реализация для полного рабочего потока.
вы сосредоточены на реализации authenticationManagerBean и configureGlobal С WebSecurityConfig класс для использования UserService.
и TokenBasedAuthentication реализация класса.
другие вещи, которые вы можете пропустить.
возможно, это проблема инициализации контекста spring-boot, что означает @Autowired
аннотация не может быть разрешена во время инициализации класса конфигурации.
вы можете попробовать @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);
}
надеюсь, это поможет вам.
Я в конечном итоге попросил поддержку Auth0 об этом, и они говорят, что в настоящее время невозможно изменить принципала без изменения источника библиотеки.
они предоставляют альтернативный подход, однако, который должен использовать библиотеку проверки JWT (например,https://github.com/auth0/java-jwt) вместо их Spring Security API SDK.
моим решением будет изменить мой код для работы только с токеном в качестве участника.