Я новичок в среде Spring, поэтому заранее прошу прощения за любые пробелы в моем понимании.
Я использую 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 с поддержкой базы данных, которую я скопировал из этот учебник. Однако он не вызывается, независимо от того, как я пытаюсь добавить его в свой 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;
}
К сожалению, ни один из подобных вопросов типа «userDetails not be called» мне не помог, так как мне пришлось комбинировать его с аутентификацией Auth0.
Я не уверен, что нахожусь на правильном пути. Мне кажется странным, что я не могу найти документацию любой от 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.
Основываясь на ответе 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 все еще не вызывается.
Вывод при добавлении метода ведения журнала, предложенного @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
Я действительно пробовал это (см. Ответ @ git-flo), но, к сожалению, безрезультатно. Поскольку auth0 обрабатывает аутентификацию и устанавливает участника безопасности, я считаю, что auth0 может перезаписывать мои попытки настройки userDetails. Однако я не знаю, как проверить эту теорию.
Хорошо, это хороший звонок. Попробуйте поместить этот метод в свой основной класс и посмотрите, что он регистрирует: @Autowired public void testUserDetailsImpl (служба UserDetailsService) {log.info ("реализация UserDetailsService:" + service.getClass (). GetName ()); }
Я добавил вывод в свой вопрос. Так что, похоже, его устанавливают. Я, наверное, также должен был упомянуть, что вызывается конструктор сервиса. Просто ни одним из методов. Возможно ли, что моя реализация auth0 перезапишет его? Или, поскольку я не реализую настраиваемый filterChain, может быть, он просто никогда не используется Spring Security? Я, конечно, не вызываю какие-либо методы службы явно, поэтому я полагаюсь на Spring Security, решив, что ему нужен UserDetailsService и методы в нем.
Auth0, вероятно, переопределяет поведение безопасности по умолчанию и не использует UserDetailsService.




Глядя на код вашего адаптера, вы генерируете токен JWT в самой конфигурации. Я не уверен, что такое apiAudience, эмитент, но я думаю, он сгенерировал под JWT. Ваша проблема в том, что вы хотите изменить подпрограмму JWT в соответствии с вашей базой данных.
Недавно я реализовал безопасность JWT в приложении Spring Boot Application.
И я устанавливаю UserName после получения его из базы данных.
Я добавил код с информацией о пакете для ясности.
// Мой класс адаптера. Он такой же, как и ваш, за исключением одного, что к нему относится Я добавил фильтр. В этом фильтре я аутентифицирую токен JWT. Этот фильтр будет вызываться каждый раз, когда активируется 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, отличается только реализация. Я добавил полную реализацию для полного рабочего процесса.
Вы сосредотачиваетесь на реализации authenticationManagerBean и configureGlobal из класса WebSecurityConfig для использования UserService.
и реализация класса TokenBasedAuthentication.
Другие вещи вы можете пропустить.
Спасибо за ваш ответ. Если я правильно понимаю, вы реализуете полную цепочку фильтров, которая позволяет вам устанавливать настраиваемые userDetails в вашем классе TokenAuthenticationFilter. К сожалению, у меня возникли проблемы с вашим примером, потому что я использую Auth0 для обработки аутентификации токена. Я не вижу способа реализовать ваше решение без отказа от аутентификации Auth0 в пользу настраиваемой цепочки фильтров. Есть ли у вас какие-либо предложения по этому поводу?
просто добавил больше деталей
Спасибо за обновление. Я реализовал большинство предложенных вами изменений (показано в правке к моему вопросу). Однако я вижу, что вы используете только класс TokenBasedAuthentication в своем TokenAuthenticationFilter. Как мне приступить к реализации TokenBasedAuthentication, учитывая, что у меня нет класса TokenAuthenticationFilter?
Возможно, это проблема инициализации контекста весенней загрузки, что означает, что аннотация @Autowired не может быть разрешена во время инициализации класса Configuration.
Вы можете попробовать аннотацию @ComponentScan() поверх своего класса Configuration и явно загрузить свой 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.
В конце концов я попросил поддержку Auth0 об этом, и они сказали, что в настоящее время невозможно изменить принципала без изменения источника библиотеки.
Однако они предоставляют альтернативный подход, который заключается в использовании библиотеки проверки JWT (например, https://github.com/auth0/java-jwt) вместо их SDK Spring Security API.
Мое решение состоит в том, чтобы изменить мой код, чтобы он работал только с токеном в качестве принципала.
На самом деле я сталкиваюсь с аналогичной проблемой. Хотя я не использую Auth0. См. Этот вопрос, который я разместил, и комментарии под ним: stackoverflow.com/questions/57155789/…
Вы можете расширить JwtAuthenticationProvider с помощью переопределенного метода authenticate, который поместит вашего пользователя в объект Authentication:
com.auth0:auth0:1.14.2, com.auth0:auth0-spring-security-api:1.2.5, com.auth0:jwks-rsa:0.8.3Note: some errors in the following code snippets might exist as I've transformed kotlin code by hand into 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.authenti cation.JwtAuthenticationProvider
Это тот, что от auth0, как упоминалось в предыдущем комментарии. Я тоже постарался прояснить это в ответе.
Это то, чем я закончил, если вы хотите просто использовать библиотеки 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;
... // Геттеры, сеттеры и переопределенные методы.
}
@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();
}}
Должна работать только реализация настраиваемой службы UserDetailsService, поддерживаемой базой данных. Я предполагаю, что вам нужно добавить @ ComponentScan в Spring, чтобы узнать ваш класс @ Service в вашем пакете.