spring-attic / spring-security-oauth

Support for adding OAuth1(a) and OAuth2 features (consumer and provider) for Spring web applications.
http://github.com/spring-projects/spring-security-oauth
Apache License 2.0
4.7k stars 4.04k forks source link

Can JWTToken use RedisTokenStore? #1267

Open douxiaofeng99 opened 6 years ago

douxiaofeng99 commented 6 years ago

My scenery is using two oauth2 servers as sso endpoint.
package com.cmi.oauth2.sso;

import java.security.Principal;

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.filter.OrderedCharacterEncodingFilter; import org.springframework.boot.web.support.SpringBootServletInitializer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; 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.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.stereotype.Controller; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter;

import com.cmi.oauth2.sso.config.ConfidentialClientProperties; import com.cmi.oauth2.sso.config.CustomLogoutSuccessHandler; import com.cmi.oauth2.sso.config.PublicClientProperties; import com.cmi.oauth2.sso.config.TrustedClientProperties; import com.cmi.oauth2.sso.redis.ClusterProperties; import com.cmi.oauth2.sso.redis.SentinelProperties; import com.cmi.oauth2.sso.redis.SingleProperties; import com.cmi.oauth2.sso.user.CustomUserDetailsService;

import redis.clients.jedis.JedisPoolConfig;

@ComponentScan(basePackages = "com.cmi.oauth2.sso") @SpringBootApplication @Controller public class OauthSSOApplication extends SpringBootServletInitializer {

private static final Logger log = LoggerFactory.getLogger(OauthSSOApplication.class);
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/login";
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/**";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/oauth/token";

@RequestMapping("/user")
@ResponseBody
public Principal user(Principal user) {
    return user;
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public OrderedCharacterEncodingFilter filterRegistrationBean() {
    OrderedCharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    filter.setForceEncoding(true);
    filter.setEncoding("UTF-8");
    return filter;
}

public static void main(String[] args) {
    log.info("Begin to starting the sa-sso-server......");
    ApplicationContext context = SpringApplication.run(OauthSSOApplication.class, args);
    log.info("The sa-sso-server started!......" + context.getApplicationName());
}

/**
 * An opinionated WebApplicationInitializer to run a SpringApplication from
 * a traditional WAR deployment. Binds Servlet, Filter and
 * ServletContextInitializer beans from the application context to the
 * servlet container.
 *
 * @link http://docs.spring.io/spring-boot/docs/current/api/index.html?org/
 *       springframework/boot/context/web/SpringBootServletInitializer.html
 */
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    return application.sources(OauthSSOApplication.class);
}

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
protected static class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CustomUserDetailsService customUserDetailsService;

    @Autowired
    CustomLogoutSuccessHandler customLogoutSuccessHandler;

    @Autowired
    OrderedCharacterEncodingFilter  charsetFilter;

    @Override
    @Autowired // <-- This is crucial otherwise Spring Boot creates its own
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // allow options methods
        web.ignoring().antMatchers(HttpMethod.OPTIONS);
    }

    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 不能禁用session
        http
                .addFilterBefore(charsetFilter, CsrfFilter.class)
                // 使用自定义用户服务
                .userDetailsService(customUserDetailsService)
                // 跨域
                .cors()
                // support cross domain
                .and()
                // diable crsf
                .csrf().disable().formLogin()
                .loginPage(FORM_BASED_LOGIN_ENTRY_POINT).permitAll()
                // token
                .and()
                // login请求放过
                .authorizeRequests().antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login

                // 获取token请求放过 // end-point
                .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() 
                .antMatchers("/**/*.js/**").permitAll()
                .antMatchers("/**/*.css/**").permitAll()
                .antMatchers("/**/*.jpg/**","/**/*.png/**").permitAll() 
                .antMatchers("/**/validate/**").permitAll()
                // 新的
                .and()
                // 不允许ajax请求
                .httpBasic().disable()
                // 所有请求都必须认证过
                .authorizeRequests().anyRequest().authenticated()
                // 退出请求
                .and()
                // 退出成功后
                .logout().clearAuthentication(true).invalidateHttpSession(true).logoutSuccessHandler(customLogoutSuccessHandler).permitAll();
    }
}

@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Value("${config.oauth2.privateKey}")
    private String privateKey;

    @Value("${config.oauth2.publicKey}")
    private String publicKey;

    @Autowired
    PublicClientProperties publicClientProperties;

    @Autowired
    ConfidentialClientProperties confidentialClientProperties;

    @Autowired
    TrustedClientProperties trustedClientProperties;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Bean
    public JwtAccessTokenConverter tokenEnhancer() {
        log.info("Initializing JWT with public key:\n" + publicKey);
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(privateKey);
        converter.setVerifierKey(publicKey);
        return converter;
    }

    @Value("${spring.redis.mode}")
    private String rediseMode;
    @Value("${spring.redis.password}")
    private String redisPasswd;

    @Autowired
    SingleProperties singleProperties;

    @Autowired
    SentinelProperties sentinelProperties;

    @Autowired
    ClusterProperties clusterProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory redisConnectionFactory = null;
        switch (rediseMode) {
        case "sentinel":
            redisConnectionFactory = new JedisConnectionFactory(
                    new RedisSentinelConfiguration(sentinelProperties.getMaster(), sentinelProperties.getNodes()));
            break;
        case "cluster":
            redisConnectionFactory = new JedisConnectionFactory(
                    new RedisClusterConfiguration(clusterProperties.getNodes()));
            break;

        default:
            redisConnectionFactory = new JedisConnectionFactory();
            redisConnectionFactory.setHostName(singleProperties.getHost());
            redisConnectionFactory.setPort(singleProperties.getPort());
            redisConnectionFactory.setTimeout(singleProperties.getTimeout());
        }
        if(!StringUtils.isEmpty(redisPasswd))
            redisConnectionFactory.setPassword(redisPasswd);
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        redisConnectionFactory.setUsePool(true);
        jedisPoolConfig.setMaxTotal(10000);
        jedisPoolConfig.setMaxIdle(100);
        redisConnectionFactory.setPoolConfig(jedisPoolConfig);

        return redisConnectionFactory;
    }

    @Bean
    public TokenStore tokenStore() {
        //return new JwtTokenStore(tokenEnhancer());
        **return new RedisTokenStore(redisConnectionFactory());**
    }

    /**
     * Defines the security constraints on the token endpoints
     * /oauth/token_key and /oauth/check_token Client credentials are
     * required to access the endpoints
     *
     * @param oauthServer
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("isAnonymous() || hasRole('ROLE_TRUSTED_CLIENT')") // permitAll()
                .checkTokenAccess("hasRole('TRUSTED_CLIENT')"); // isAuthenticated()
    }

    /**
     * Defines the authorization and token endpoints and the token services
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints

                // Which authenticationManager should be used for the
                // password grant
                // If not provided, ResourceOwnerPasswordTokenGranter is not
                // configured
                .authenticationManager(authenticationManager) 
                // Use JwtTokenStore and our jwtAccessTokenConverter
                .tokenStore(tokenStore()).accessTokenConverter(tokenEnhancer());
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()

                // Confidential client where client secret can be kept safe
                // (e.g. server side)
                .withClient(confidentialClientProperties.getClientID()).secret(confidentialClientProperties.getClientSecret())
                .autoApprove(true)
                .authorizedGrantTypes("client_credentials", "authorization_code", "refresh_token")
                .scopes("read", "write").redirectUris(confidentialClientProperties.getRedirectUris())
                .accessTokenValiditySeconds(1500)
                .and()

                // Public client where client secret is vulnerable (e.g.
                // mobile apps, browsers)
                .withClient(publicClientProperties.getClientID()) // No secret!
                .autoApprove(true).authorizedGrantTypes("implicit").scopes("read")
                .redirectUris(publicClientProperties.getRedirectUris())
                .accessTokenValiditySeconds(1500)
                .and()

                // Trusted client: similar to confidential client but also
                // allowed to handle user password
                .withClient(trustedClientProperties.getClientID()).secret(trustedClientProperties.getClientSecret())
                .authorities("ROLE_TRUSTED_CLIENT")
                .autoApprove(true)
                .authorizedGrantTypes("client_credentials", "password", "authorization_code", "refresh_token")
                .scopes("read", "write").redirectUris(trustedClientProperties.getRedirectUris())
                .accessTokenValiditySeconds(1500)
                ;
    }

}

}

I want to use redis to store access_token and refresh_token, also i want to use JWT token to reduce the resource server accessing the sso servers. when i use these two together, the login and the first time to get access_token are ok. The problem is occurred when the access_token is expired, i can use the first time gotten refresh_token to refresh access_token. then i log out, the refresh and access token are renewed, then if i use the new refresh_token to get new access_token, the oauth will throw invalid refresh token. In the redis, the refresh:xxx-xxx do not update ,it is the first refresh_token.

koman-maciej commented 6 years ago

+1

koupeng commented 6 years ago

+1

PaulHorowit commented 6 years ago

It seems that you get the old refresh token. You can ask oauth to give new refresh_token by AuthorizationServerEndpointsConfigurer.reuseRefreshTokens(false).

rezanouri87 commented 5 years ago

@douxiaofeng99 did you manage to make this work?

douxiaofeng99 commented 5 years ago

@ Reza Nouri

No, I have no idea yet.

发件人: Reza Nouri notifications@github.com 答复: spring-projects/spring-security-oauth reply@reply.github.com 日期: 2019年4月17日 星期三 上午11:52 收件人: spring-projects/spring-security-oauth spring-security-oauth@noreply.github.com 抄送: "dou xiaofeng (窦晓峰)" douxf@asiainfo.com, Mention mention@noreply.github.com 主题: Re: [spring-projects/spring-security-oauth] Can JWTToken use RedisTokenStore? (#1267)

@douxiaofeng99 did you manage to make this work?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

jpomykala commented 5 years ago

I also was trying to do this and I think that the only way to make it work is just to implement TokenStore interface by yourself.