【问题标题】:OAuth2 Authorization Code flow without sharing client secretOAuth2 授权代码流,不共享客户端密码
【发布时间】:2018-03-13 07:51:30
【问题描述】:

我已经使用带有 Angular 2 客户端的 Spring Security Cloud 对 OAuth2 的授权代码流程做了一个小演示。

一切正常,我正在从服务器获取访问令牌响应。

但是根据 Aaron perecki 的博客 https://aaronparecki.com/oauth-2-simplified/

单页应用程序(或基于浏览器的应用程序)在从网页加载源代码后完全在浏览器中运行。由于浏览器可以使用整个源代码,因此他们无法维护其客户端机密的机密性,因此在这种情况下不使用机密。流程与上面的授权码流程完全相同,但在最后一步,将授权码交换为访问令牌,而不使用客户端密码。

所以我不想使用客户端密码从 auth-server 获取访问令牌。

但是,如果不将客户端密码共享给身份验证服务器,我将无法继续。

以下是我的 Angular 2 检索令牌的逻辑

import {Injectable} from '@angular/core';
import {IUser} from './user';
import {Router} from '@angular/router';
import {Http, RequestOptions, Headers, URLSearchParams} from '@angular/http';

@Injectable()
export class AuthService {
  currentUser: IUser;
  redirectUrl: string;
  state: string;
  tokenObj: any;

  constructor(private router: Router, private http: Http) {
    this.state = '43a5';
  }

  isLoggedIn(): boolean {
    return !!this.currentUser;
  }

  loginAttempt(username: string, password: string): void {
    const credentials: IUser = {
      username: username,
      password: password
    };

    const params = new URLSearchParams();
    params.append('client_id', 'webapp');
    params.append('redirect_uri', 'http://localhost:9090/callback');
    params.append('scope', 'read');
    params.append('grant_type', 'authorization_code');
    params.append('state', this.state);
    params.append('response_type', 'code');

    const headers = new Headers({
      'Authorization': 'Basic ' + btoa(username + ':' + password)
    });

    const options = new RequestOptions({headers: headers});

    this.http.post('http://localhost:9090/oauth/authorize', params, options)
      .subscribe(
        data => {
          const authresponse = data.json();
          this.tokenObj = this.getTokens(authresponse.code).json();
        },
        err => console.log(err)
      );
  }

  getTokens(code: string): any {
    const params = new URLSearchParams();
    params.append('grant_type', 'authorization_code');
    params.append('code', code);
    params.append('redirect_uri', 'http://localhost:9090/callback');

    const headers = new Headers({
      'Authorization': 'Basic ' + btoa('webapp:websecret')
    });

    const options = new RequestOptions({headers: headers});

    this.http.post('http://localhost:9090/oauth/token', params, options)
      .subscribe(
        data => {
          return data.json();
        },
        err => console.log(err)
      );
  }

  logout(): void {
    this.currentUser = null;
  }
}

这是我的AuthorizationServerConfig类源代码

@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authManager;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(accessTokenConverter()).authenticationManager(authManager);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("permitAll()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource());
    }

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

    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/oauth2?createDatabaseIfNotExist=true");
        dataSource.setUsername("root");
        dataSource.setPassword("chandra");
        return dataSource;
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");
        return converter;
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }
}

WebConfig 类的源代码

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user1").password("password1").roles("USER")
            .and().withUser("admin1").password("password1").roles("ADMIN");
        auth.eraseCredentials(false);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/oauth/**").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable()
            .httpBasic();
    }
}

SpringBootApplication 类

@SpringBootApplication
@EnableAuthorizationServer
@RestController
public class SpringMicroservicesOauthServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringMicroservicesOauthServerApplication.class, args);
    }

    @RequestMapping("callback")
    public AuthCodeResponse test(@RequestParam("code") String code, @RequestParam("state") String state) {
        return new AuthCodeResponse(code,state);
    }
}

AuthCodeResponse POJO

public class AuthCodeResponse {

    private String code;

    private String state;

    public AuthCodeResponse() {
    }

    public AuthCodeResponse(String code, String state) {
        this.code = code;
        this.state = state;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}

【问题讨论】:

    标签: angular spring-security oauth-2.0 spring-security-oauth2 spring-cloud-security


    【解决方案1】:

    如果您的应用程序的 client type公开

    但是,即使您的应用程序的客户端类型是公开的,您的授权服务器也需要一对 API 密钥和 API 机密。为什么?这是因为WebSecurityConfig 正在保护/oauth/**。即使/oauth/** 不是 OAuth 端点,也会执行保护。

    (a) 通过客户端 ID 和客户端密码进行保护和 (b) 通过通用方式进行保护(在这种情况下,通过 WebSecurityConfig 进行保护)是不同的东西。

    【讨论】:

      【解决方案2】:

      我还没有看到一个实际的问题,但是如果您只想隐藏客户端密码,为什么不创建自己的 API,这就是您处理所有 OAuth2 东西的地方,从而让您保持客户的秘密,很好的秘密。

      那是你从 Angular 前端调用的那个。

      您完全不用在 JavaScript 中使用令牌,从而避免了完全暴露令牌的需要。

      是的,这是一个额外的步骤,但如果您想确保任何东西的安全,这是完全值得的。

      【讨论】:

      • 是的,也许我们可以创建一个请求过滤器来验证请求是否包含 clientid,如果找到,那么它会在请求中添加一个客户端密码并将其发送到身份验证服务器以进行进一步处理,听起来像好主意!感谢您的建议。
      • 这很好,但必须确保 API 和 Angular 前端之间的通信安全。
      • 所以我的组织做了类似的事情。角度应用程序被“包装”在 Spring Boot 应用程序中。这个 Spring Boot 应用程序有一个 API 端点来获取 Angular 应用程序调用的令牌。该端点使用 Spring Security 和 Oauth2 身份验证代码进行保护。因此,包装器 Spring 应用程序的工作是重定向到身份验证服务器进行登录(也是一个 Spring 应用程序),将令牌发送回 Spring 包装器应用程序,然后将令牌安全地返回到 Angular 应用程序。我不觉得这很理想,因为我不知道如何处理刷新令牌....
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-01-31
      • 2020-09-03
      • 1970-01-01
      • 1970-01-01
      • 2017-03-04
      • 2015-06-25
      • 2019-01-09
      相关资源
      最近更新 更多