構建安全的移動應用API
最近和小伙伴鼓搗一個APP, 沒想到一開始在登陸注冊這塊就卡住了, 卡住的原因在于 如何對接口進行訪問控制 , 大家都知道, 在傳統的web開發中由于有session/cookie的存在,請求可以保持狀態, 但一般來講,APP用到的API都是被設計成無狀態的, 那應該如何解決問題呢?
解決思路
-
對于平臺類API來說,其目標用戶一般是開發者, 諸如餓了么OpenApi或者 Pusher.com 這類服務,每次調用都是獨立的, 無需保存狀態信息, 數據權限和功能權限可以通過 AppId 這類唯一標識符來進行區分。安全上通過 auth_signature 的方式來進行校驗。具體算法可以參見上面提到的兩個文檔。
</li> -
如果目標對象是那些APP, 怎么辦呢? , 剛工作那會解決這種需求的方法十分暴力:把用戶名密碼保存在app本地,調用接口的時候把用戶名密碼傳過去做校驗, 沒有優雅性可言。目前來講,在寫Mobile API時, 直接使用 Oauth2 來處理權限問題是一種比較常用的方法。Oauth2 看起來略復雜,但其最終目的是獲取一個 訪問令牌 , 獲取令牌的模式一共有四種.
</li> </ul>- 授權碼: 例子有微博第三方登陸,流程為: 第三方網站 -> 跳轉到微博讓用戶選擇是否授權 -> 用戶授權并通過回調返回第三方一個授權碼 -> 第三方根據授權碼向微博申請訪問令牌 -> 微博返回訪問令牌
- 隱式授權: 流程為: 跳轉到授權頁面 -> 授權成功之后回調返回訪問令牌
- 密碼模式: 流程為: 發送一個帶用戶名密碼參數的請求(并附帶Http Basic Authorization) -> 返回一個訪問令牌
- 客戶端模式: 這個方式很有意思,在這種模式下, 是以客戶端的名義而不是以用戶的名義進行令牌申請, 權限上并沒有區分,也就不存在授權問題了, 流程為: 向認證服務器發起請求 -> 以某種方式驗證客戶端的方式(比如根據appId,appSecret) -> 返回訪問令牌 </ol> </blockquote>
- 配置spring-security </ul>
- 配置oauth2 </ul>
如果是編寫Mobile API, 密碼模式是一種比較簡單的選擇: 這樣,登錄過程就變成了獲取令牌的過程,登錄成功之后把令牌存到本地,之后的API調用帶上令牌即可。
工程實踐
對于NodeJs開發者來說, 由于有 passport.js及一眾package的存在, 編寫一個 受不記名訪問令牌保護的API 十分的簡單, 可以參考 這篇教程 搭建基礎環境。 下面的內容是在java環境中使用spring-security-oauth2+springmvc的工程實踐。
不得不說,采用 Annotation 方式配置spring是一種非常好的實踐, 可讀性上比XML強太多, 詳細配置請參考 示例項目
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER").and() .withUser("stackbox").password("123456").roles("ADMIN"); }
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable(); } /** * 這個Bean用于oauth2的密碼授權模式的配置 */ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
}</pre>
一般來講spring-security還要加個過濾器,通過加入下面這個類,就能夠不配置web.xml來加入過濾器了。public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer{
}</pre>
項目文檔 里講了幾個核心接口,參照例子, 我們同樣采用注解的方式進行配置。在代碼里可以通過@EnableResourceServer來配置資源服務器, 資源服務器的配置和spring-security的權限配置十分類似,@EnableAuthorizationServer來配置認證服務器。注意在文檔中有這么一句話。
The grant types supported by the AuthorizationEndpoint can be configured via the AuthorizationServerEndpointsConfigurer. By default all grant types are supported except password (see below for details of how to switch it on). The following properties affect grant types:
</blockquote>也就是說,如果要用密碼授權方式的話,需要注入一個authenticationManagerBean, 它就是在上面spring-security配置中的那個bean。
@Configuration public class Oauth2ServerConfig {
protected static final String RESOURCE_ID = "STACKBOX"; @Configuration @EnableResourceServer protected static class ResourceServer extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .requestMatchers().antMatchers("/admin/**").and() .authorizeRequests() .anyRequest().access("#oauth2.hasScope('read')"); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId(RESOURCE_ID); } } @Configuration @EnableAuthorizationServer protected static class AuthorizationServer extends AuthorizationServerConfigurerAdapter { private TokenStore tokenStore = new InMemoryTokenStore(); @Autowired @Qualifier("authenticationManagerBean") private AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { /** * allow表示允許在認證的時候把參數放到url之中傳過去 * @see org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter */ oauthServer.allowFormAuthenticationForClients(); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager); endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("client") .authorizedGrantTypes("password","refresh_token") .authorities("ROLE_USER") .scopes("read") .resourceIds(RESOURCE_ID) .secret("secret").accessTokenValiditySeconds(3600); } }
}</pre>
其他策略