web-dev-qa-db-ja.com

Spring Security:カスタムUserDetailsS​​erviceが呼び出されない(Auth0認証を使用)

私は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セキュリティプリンシパルはjwtトークンからのuserId(sub)に設定されています:auth0|5b2b...。ただし、userIdだけでなく、データベースから一致するユーザーに設定する必要があります。私の質問はその方法です。

私が試したこと

このチュートリアル からコピーした、データベースに基づくカスタムのUserDetailsS​​erviceを実装してみました。ただし、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のanyのドキュメントが見つからないのは奇妙に思えます。

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の回答に基づいて、彼のCustomUserDetailsS​​erviceをコピーして、以下を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 );
}

残念ながら、これらの変更により、CustomUserDetailsS​​erviceはまだ呼び出されていません。

編集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
10
Joakim

私は最終的にこれについてAuth0のサポートを求めましたが、彼らは現在、ライブラリソースを変更せずにプリンシパルを変更することはできないと言っています。

ただし、Spring Security API SDKの代わりにJWT検証ライブラリー(例: https://github.com/auth0/Java-jwt )を使用するという代替アプローチを提供します。

私の解決策は、トークンだけをプリンシパルとして機能するようにコードを変更することです。

2
Joakim

たぶんこれはスプリングブートコンテキストの初期化の問題です。つまり、Configurationクラスの初期化中に_@Autowired_アノテーションを解決できません。

Configurationクラスの上に@ComponentScan()アノテーションを試し、MyUserDetailsServiceを明示的にロードすることができます。 (参照: https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-configuration-classes.html#using-boot-importing-configuration ) 。これを行ったら、Configurationクラスで次のことをお勧めします。

_@Autowired
private MyUserDetailsService userService;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService);
}
_

これがあなたを助けることを願っています。

2
git-flo

アダプターコードを見て、configure自体でJWTトークンを生成しています。何がapiAudience、発行者かわかりませんが、JWTのサブを生成したと思います。問題は、データベースごとにJWTサブを変更することです。

私は最近、Spring Boot ApplicationにJWTセキュリティを実装しました。

そして、私はデータベースからフェッチした後にユーザー名を設定しています。

わかりやすくするために、pkg infoのコードを追加しました。

// My adapter class私がFilterを追加したことを除いて、あなたのものと同じです。このフィルターでは、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 

Name loadUserByUsernameのメソッドを実装していないため。このログを取得しています。

編集1:

トークンを検証し、トークンから抽出されるセキュリティコンテキストにユーザーを追加するためだけにフィルターチェーンを使用しています...

JWTを使用していて、AuthOを使用しています。実装のみが異なります。完全なワークフローの完全な実装を追加しました。

UserServiceを使用するには、authenticationManagerBeanおよびconfigureGlobalWebSecurityConfigクラスの実装に焦点を当てます。

およびTokenBasedAuthenticationクラスの実装。

スキップできるその他のこと。

2
MyTwoCents

ユーザーをJwtAuthenticationProviderオブジェクトに入れるオーバーライドされたauthenticateメソッドでAuthenticationを拡張できます。

  • Springboot 2.1.7.RELEASEの使用
  • Auth0 deps:_com.auth0:auth0:1.14.2_、_com.auth0:auth0-spring-security-api:1.2.5_、_com.auth0:jwks-rsa:0.8.3_

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

メソッドJwtWebSecurityConfigurer.forRS256(String audience, String issuer)で通常使用されるデフォルトJwtAuthenticationProviderを拡張します

_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();
_
1
Aivaras