使用 OAuth2 通过 spring 引导使用 authorization_code 授权和 JWT 登录客户端

Use OAuth2 to sign in to client with spring boot using authorization_code grant and JWT

我正在尝试使用 JWT 和 OAuth2 在 server/client 上获得单一登录。

除了用户名(我打算将其嵌入到 jwt 中)之外,我没有其他资源可以从客户端外部请求。

我的服务器正在运行并且可以很好地处理 jwt 令牌(使用邮递员的 oauth2 令牌身份验证进行测试)。我只是不知道如何处理客户。

我需要让我的客户端成为 ResourceServer 吗?或者我应该直接使用@EnableOAuth2Sso 教程吗?

对于我的客户,我有以下规格:

我认为这是可能的,并且正是单点登录应该做的。

我的第一个想法是使用 @EnableOAuth2Sso 但这不能正常工作:

  1. 我被重定向到服务器上的登录页面
  2. 我正确登录
  3. 我被重定向到:http://localhost:9000/login?code=fmzhJ5&state=bn9077
  4. 我收到此错误:出现意外错误(类型=未授权,状态=401)。 身份验证失败:无法获取访问令牌

更新

我发现以下错误消息:检测到可能的 CSRF - 需要状态参数但找不到状态

我猜 saving/using/converting "state" 参数配置错误。但是我想使用它。

更新2

我找到了 this comment 并为客户端添加了不同的上下文路径。这将错误消息更改为:

org.springframework.security.authentication.BadCredentialsException: 无法获取访问令牌 原因:org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException:请求访问令牌时出错。 原因:org.springframework.web.client.HttpClientErrorException: 401 null

代码:

客户端应用:

@SpringBootApplication
@EnableOAuth2Sso
public class TestClientApplication
{
    public static void main(String[] args) {
        SpringApplication.run(TestClientApplication.class, args);
    }
}

application.yml:

security:
  oauth2:
    client:
      clientId: client
      clientSecret: secret
      accessTokenUri: http://localhost:8080/oauth/token
      userAuthorizationUri: http://localhost:8080/oauth/authorize
      authenticationScheme: query
      clientAuthenticationScheme: form
    resource:
      jwt:
        keyValue:
          -----BEGIN PUBLIC KEY-----
          ...

这是我的服务器实现:

OAuth2Config:

@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter
{

    @Value("${resource.id:spring-boot-application}")
    private String resourceId;

    @Value("${access_token.validity_period:3600}")
    int accessTokenValiditySeconds = 3600;

    //todo
    private static final String JWTSecretKey = "mySecretKey";

    @Autowired
    private AuthenticationManager authenticationManager;

    @Bean
    public TokenEnhancer tokenEnhancer()
    {
        return new JwtTokenEnhancer();
    }

    @Bean
    protected JwtAccessTokenConverter accessTokenConverter()
    {
        //todo use secure cert
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), JWTSecretKey.toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
        return converter;
    }

    @Bean
    public TokenStore tokenStore()
    {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
    {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(
                Arrays.asList(tokenEnhancer(), accessTokenConverter()));

        endpoints
                .tokenStore(tokenStore())
                .tokenEnhancer(tokenEnhancerChain)
                .authenticationManager(this.authenticationManager);      //only needed for password grant, which we should only use in case of native/client side apps

    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception
    {
        //todo add redirect urls
        clients.inMemory()
                .withClient("trusted-app")          //todo for postman testing only, remove in production!
                .secret("secret")
                .authorizedGrantTypes("client_credentials")
                .authorities("ROLE_TRUSTED_CLIENT")
                .scopes("read", "write")
                .resourceIds(resourceId, "myprintforce")
                .autoApprove(true)
                .accessTokenValiditySeconds(accessTokenValiditySeconds)
                .and()
                .withClient("client")
                .secret("secret")
                .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
                .scopes("read", "write")
                .resourceIds("client", resourceId)
                .autoApprove(true)
                .accessTokenValiditySeconds(accessTokenValiditySeconds);
        ;
    }
}

TokenEnhancer

public class JwtTokenEnhancer implements TokenEnhancer
{
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)
    {
        Map<String, Object> additionalInfo = new HashMap<>();
        additionalInfo.put("user", authentication.getName());
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;
    }
}

WebSecurityConfig

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.inMemoryAuthentication().withUser("u").password("p").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .and();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

}

申请

@SpringBootApplication
public class OAuthServerApplication extends WebMvcConfigurerAdapter
{
    public static void main(String[] args)
    {
        SpringApplication.run(OAuthServerApplication.class, args);
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
    }
}

问题是两台服务器使用同一台 (localhost) 机器。当他们有不同的上下文路径时,问题就差不多解决了。

通过更改配置文件中的一些条目,我让它工作了。

我删除了:authenticationScheme: queryclientAuthenticationScheme: form 并添加了 scope=read

现在看起来像这样:

security:
  oauth2:
    client:
      clientId: client
      clientSecret: secret
      accessTokenUri: http://localhost:12000/authentication/oauth/token
      userAuthorizationUri: http://localhost:12000/authentication/oauth/authorize
      scope: read
    resource:
      jwt:
        keyValue:
          -----BEGIN PUBLIC KEY-----