天天看點

學會 OAuth2.0 授權登入,10 張圖就夠了

學會 OAuth2.0 授權登入,10 張圖就夠了

對于身份認證和使用者授權,之前寫過幾篇關于Shiro和Security的文章。今天給大家帶來一種新的授權方式:oauth2。

理論

OAuth是一個關于授權(authorization)的開放網絡标準,用來授權第三方應用擷取使用者資料,是目前最流行的授權機制,它目前的版本是2.0。

應用場景

假如你正在“網站A”上沖浪,看到一篇文章表示非常喜歡,當你情不自禁的想要點贊時,它會提示你進行登入操作。

學會 OAuth2.0 授權登入,10 張圖就夠了

打開登入頁面你會發現,除了最簡單的賬戶密碼登入外,還為我們提供了微網誌、微信、QQ等快捷登入方式。假設選擇了快捷登入,它會提示我們掃碼或者輸入賬号密碼進行登入。

學會 OAuth2.0 授權登入,10 張圖就夠了

登入成功之後便會将QQ/微信的昵稱和頭像等資訊回填到“網站A”中,此時你就可以進行點贊操作了。

名詞定義

在詳細講解oauth2之前,我們先來了解一下它裡邊用到的名詞定義吧:

  • Client:用戶端,它本身不會存儲使用者快捷登入的賬号和密碼,隻是通過資源擁有者的授權去請求資源伺服器的資源,即例子中的網站A;
  • Resource Owner:資源擁有者,通常是使用者,即例子中擁有QQ/微信賬号的使用者;
  • Authorization Server:認證伺服器,可以提供身份認證和使用者授權的伺服器,即給用戶端頒發token和校驗token;
  • Resource Server:資源伺服器,存儲使用者資源的伺服器,即例子中的QQ/微信存儲的使用者資訊;

認證流程

學會 OAuth2.0 授權登入,10 張圖就夠了

如圖是oauth2官網的認證流程圖,我們來分析一下:

  • A用戶端向資源擁有者發送授權申請;
  • B資源擁有者同意用戶端的授權,傳回授權碼;
  • C用戶端使用授權碼向認證伺服器申請令牌token;
  • D認證伺服器對用戶端進行身份校驗,認證通過後發放令牌;
  • E用戶端拿着認證伺服器頒發的令牌去資源伺服器請求資源;
  • F資源伺服器校驗令牌的有效性,傳回給用戶端資源資訊;

為了大家更好的了解,阿Q特地畫了一張圖:

學會 OAuth2.0 授權登入,10 張圖就夠了

到這兒,相信大家對理論知識已經掌握的差不多了,接下來我們就進入實戰訓練吧。

實戰

在正式開始搭建項目之前我們先來做一些準備工作:要想使用oauth2的服務,我們得先建立幾張表。

資料庫

oauth2相關的建表語句可以參考官方初始化sql,也可以檢視阿Q項目中的init.sql檔案,回複“oauth2”擷取源碼。

學會 OAuth2.0 授權登入,10 張圖就夠了

至于表結構,大家可以先大體了解下,其中字段的含義,在init.sql檔案中阿Q已經做了說明。

  • oauth_client_details:存儲用戶端的配置資訊,操作該表的類主要是JdbcClientDetailsService.java;
  • oauth_access_token:存儲生成的令牌資訊,操作該表的類主要是JdbcTokenStore.java;
  • oauth_client_token:在用戶端系統中存儲從服務端擷取的令牌資料,操作該表的類主要是JdbcClientDetailsService.java;
  • oauth_code:存儲授權碼資訊與認證資訊,即隻有grant_type為authorization_code時,該表才會有資料,操作該表的類主要是JdbcAuthorizationCodeServices.java;
  • oauth_approvals:存儲使用者的授權資訊;
  • oauth_refresh_token:存儲重新整理令牌的refresh_token,如果用戶端的grant_type不支援refresh_token,那麼不會用到這張表,操作該表的類主要是JdbcTokenStore;

在oauth_client_details表中添加一條資料

client_id:cheetah_one //用戶端名稱,必須唯一
resource_ids:product_api //用戶端所能通路的資源id集合,多個資源時用逗号(,)分隔
client_secret:$2a$10$h/TmLPvXozJJHXDyJEN22ensJgaciomfpOc9js9OonwWIdAnRQeoi //用戶端的通路密碼
scope:read,write //用戶端申請的權限範圍,可選值包括read,write,trust。若有多個權限範圍用逗号(,)分隔
authorized_grant_types:client_credentials,implicit,authorization_code,refresh_token,password //指定用戶端支援的grant_type,可選值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支援多個grant_type用逗号(,)分隔
web_server_redirect_uri:http://www.baidu.com //用戶端的重定向URI,可為空, 當grant_type為authorization_code或implicit時, 在Oauth的流程中會使用并檢查與注冊時填寫的redirect_uri是否一緻
access_token_validity:43200 //設定用戶端的access_token的有效時間值(機關:秒),可選, 若不設定值則使用預設的有效時間值(60 * 60 * 12, 12小時)
autoapprove:false //設定使用者是否自動Approval操作, 預設值為 'false', 可選值包括 'true','false', 'read','write'
           

資料庫中對密碼進行了加密處理,大家可以在此路徑下自行生成

學會 OAuth2.0 授權登入,10 張圖就夠了

使用者角色相關的表也在init.sql檔案中,表結構非常簡單,大家自行查閱。我的初始化資料為

學會 OAuth2.0 授權登入,10 張圖就夠了

依賴引入

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.security</groupId>
 <artifactId>spring-security-jwt</artifactId>
</dependency>
           

至于其它依賴,大家可以根據需要自行引入,不再贅述,回複“oauth2”擷取源碼。

資源服務

配置檔案對服務端口、應用名稱、資料庫、mybatis和日志進行了配置。

寫了一個簡單的控制層代碼,用來模拟資源通路

@RestController
@RequestMapping("/product")
public class ProductController {

    @GetMapping("/findAll")
    public String findAll(){
        return "産品清單查詢成功";
    }
}
           

接着建立配置類繼承ResourceServerConfigurerAdapter并增加@EnableResourceServer注解開啟資源服務,重寫兩個configure方法

/**
 * 指定token的持久化政策
 * InMemoryTokenStore 表示将token存儲在記憶體中
 * RedisTokenStore 表示将token存儲在redis中
 * JdbcTokenStore 表示将token存儲在資料庫中
 * @return
 */
@Bean
public TokenStore jdbcTokenStore(){
    return new JdbcTokenStore(dataSource);
}

/**
 * 指定目前資源的id和token的存儲政策
 * @param resources
 * @throws Exception
 */
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    //此處的id可以寫在配置檔案中,這裡我們先寫死
 resources.resourceId("product_api").tokenStore(jdbcTokenStore());
}


/**
 * 設定請求權限和header處理
 * @param http
 * @throws Exception
 */
@Override
public void configure(HttpSecurity http) throws Exception {
 //固定寫法
 http.authorizeRequests()
   //指定不同請求方式通路資源所需的權限,一般查詢是read,其餘都是write
   .antMatchers(HttpMethod.GET,"/**").access("#oauth2.hasScope('read')")
   .antMatchers(HttpMethod.POST,"/**").access("#oauth2.hasScope('write')")
   .antMatchers(HttpMethod.PATCH,"/**").access("#oauth2.hasScope('write')")
   .antMatchers(HttpMethod.PUT,"/**").access("#oauth2.hasScope('write')")
   .antMatchers(HttpMethod.DELETE,"/**").access("#oauth2.hasScope('write')")
   .and()
   .headers().addHeaderWriter((request,response) -> {
    //域名不同或者子域名不一樣并且是ajax請求就會出現跨域問題
    //允許跨域
    response.addHeader("Access-Control-Allow-Origin","*");
    //跨域中會出現預檢請求,如果不能通過,則真正請求也不會發出
    //如果是跨域的預檢請求,則原封不動向下傳遞請求頭資訊,否則預檢請求會丢失請求頭資訊(主要是token資訊)
    if(request.getMethod().equals("OPTIONS")){
     response.setHeader("Access-Control-Allow-Methods",request.getHeader("Access-Control-Allow-Methods"));
     response.setHeader("Access-Control-Allow-Headers",request.getHeader("Access-Control-Allow-Headers"));
    }
 });
}
           

當然我們也可以配置忽略校驗的url,在上邊的public void configure(HttpSecurity http) throws Exception中進行配置

ExpressionUrlAuthorizationConfigurer<HttpSecurity>
  .ExpressionInterceptUrlRegistry config = http.requestMatchers().anyRequest()
  .and()
  .authorizeRequests();
properties.getUrls().forEach(e -> {
 config.antMatchers(e).permitAll();
});
           

因為我們是需要進行校驗的,是以我把對應的代碼給注釋掉了,大家可以回複“oauth2”下載下傳源碼自行檢視。

然後将實作了UserDetails的SysUser和實作了GrantedAuthority的SysRole放到項目中,當請求發過來時,oauth2會幫我們自行校驗。

認證服務

配置檔案對服務端口、應用名稱、資料庫、mybatis和日志進行了配置。

Security配置

還是和之前Security+JWT組合拳的配置大同小異,不了解的可以先看下該文。

①将繼承了UserDetailsService的ISysUserService的實作類SysUserServiceImpl重寫loadUserByUsername方法

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 return this.baseMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
}
           

②繼承WebSecurityConfigurerAdapter類,增加@EnableWebSecurity注解并重寫方法

/**
 * 指定認證對象的來源和加密方式
 * @param auth
 * @throws Exception
 */
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}

/**
 * 安全攔截機制(最重要)
 * @param httpSecurity
 * @throws Exception
 */
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
 httpSecurity
   //CSRF禁用,因為不使用session
   .csrf().disable()
   .authorizeRequests()
   //登入接口和靜态資源不需要認證
   .antMatchers("/login*","/css/*").permitAll()
   //除上面的所有請求全部需要認證通過才能通路
   .anyRequest().authenticated()
   //傳回HttpSecurity以進行進一步的自定義,證明是一次新的配置的開始
   .and()
   .formLogin()
   //如果未指定此頁面,則會跳轉到預設頁面
//                .loginPage("/login.html")
   .loginProcessingUrl("/login")
   .permitAll()
   //認證失敗處理類
   .failureHandler(customAuthenticationFailureHandler);
}

/**
 * AuthenticationManager 對象在OAuth2.0認證服務中要使用,提前放入IOC容器中
 * @return
 * @throws Exception
 */
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
 return super.authenticationManagerBean();
}
           

AuthorizationServer配置

①繼承AuthorizationServerConfigurerAdapter類,增加@EnableAuthorizationServer注解開啟認證服務

②依賴注入,注入7個執行個體Bean對象

/**
 * 資料庫連接配接池對象
 */
private final DataSource dataSource;

/**
 * 認證業務對象
 */
private final ISysUserService userService;

/**
 * 授權碼模式專用對象
 */
private final AuthenticationManager authenticationManager;

/**
 * 用戶端資訊來源
 * @return
 */
@Bean
public JdbcClientDetailsService jdbcClientDetailsService(){
   return new JdbcClientDetailsService(dataSource);
}

/**
 * token儲存政策
 * @return
 */
@Bean
public TokenStore tokenStore(){
 return new JdbcTokenStore(dataSource);
}

/**
 * 授權資訊儲存政策
 * @return
 */
@Bean
public ApprovalStore approvalStore(){
 return new JdbcApprovalStore(dataSource);
}

/**
 * 授權碼模式資料來源
 * @return
 */
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
 return new JdbcAuthorizationCodeServices(dataSource);
}
           

③重寫方法進行配置

/**
 * 用來配置用戶端詳情服務(ClientDetailsService)
 * 用戶端詳情資訊在這裡進行初始化
 * 指定用戶端資訊的資料庫來源
 * @param clients
 * @throws Exception
 */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
 clients.withClientDetails(jdbcClientDetailsService());
}

/**
 * 檢測 token 的政策
 * @param security
 * @throws Exception
 */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
 security
   //允許用戶端以form表單的方式将token傳達給我們
   .allowFormAuthenticationForClients()
   //檢驗token必須需要認證
   .checkTokenAccess("isAuthenticated()");
}


/**
 * OAuth2.0的主配置資訊
 * @param endpoints
 * @throws Exception
 */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
 endpoints
   //重新整理token時會驗證目前使用者是否已經通過認證
   .userDetailsService(userService)
   .approvalStore(approvalStore())
   .authenticationManager(authenticationManager)
   .authorizationCodeServices(authorizationCodeServices())
   .tokenStore(tokenStore());
}
           

其它關于使用者表和權限表的代碼可參考源碼,回複“oauth2”擷取源碼。

模式

授權碼模式

我們前邊所講的内容都是基于授權碼模式,授權碼模式被稱為最安全的一種模式,它擷取令牌的操作是在兩個服務端進行的,極大的減小了令牌洩漏的風險。

啟動兩個服務,當我們再次請求127.0.0.1:9002/product/findAll接口時會提示以下錯誤

{
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource"
}
           

①調用接口擷取授權碼

發送127.0.0.1:9001/oauth/authorize?response_type=code&client_id=cheetah_one請求,前邊的路徑是固定形式的,response_type=code表示擷取授權碼,client_id=cheetah_one表示用戶端的名稱是我們資料庫配置的資料。

學會 OAuth2.0 授權登入,10 張圖就夠了

該頁面是oauth2的預設頁面,輸入使用者的賬戶密碼點選登入會提示我們進行授權,這是資料庫oauth_client_details表我們設定autoapprove為false起到的效果。

學會 OAuth2.0 授權登入,10 張圖就夠了

選擇Approve點選Authorize按鈕,會發現我們設定的回調位址(oauth_client_details表中的web_server_redirect_uri)後邊拼接了code值,該值就是授權碼。

學會 OAuth2.0 授權登入,10 張圖就夠了

檢視資料庫發現oauth_approvals和oauth_code表已經存入資料了。

拿着授權碼去擷取token

學會 OAuth2.0 授權登入,10 張圖就夠了

擷取到token之後oauth_access_token和oauth_refresh_token表中會存入資料以用于後邊的認證。而oauth_code表中的資料被清除了,這是因為code值是直接暴漏在網頁連結上的,oauth2為了防止他人拿到code非法請求而特意設定為僅用一次。

拿着擷取到的token去請求資源服務的接口,此時有兩種請求方式

學會 OAuth2.0 授權登入,10 張圖就夠了
學會 OAuth2.0 授權登入,10 張圖就夠了

接下來我們再來看一下oauth2的其它模式。

簡化模式

所謂簡化模式是針對授權碼模式進行的簡化,它将授權碼模式中擷取授權碼的步驟省略了,直接去請求擷取token。

學會 OAuth2.0 授權登入,10 張圖就夠了

流程:發送請求127.0.0.1:9001/oauth/authorize?response_type=token&client_id=cheetah_one跳轉到登入頁進行登入,response_type=token表示擷取token。

輸入賬号密碼登入之後會直接在浏覽器傳回token,我們就可以像授權碼方式一樣攜帶token去請求資源了。

學會 OAuth2.0 授權登入,10 張圖就夠了

該模式的弊端就是token直接暴漏在浏覽器中,非常不安全,不建議使用。

密碼模式

密碼模式下,使用者需要将賬戶和密碼提供給用戶端向認證伺服器申請令牌,是以該種模式需要使用者高度信任用戶端。

學會 OAuth2.0 授權登入,10 張圖就夠了

流程:請求如下

學會 OAuth2.0 授權登入,10 張圖就夠了

擷取成功之後可以去通路資源了。

用戶端模式

用戶端模式已經不太屬于oauth2的範疇了,使用者直接在用戶端進行注冊,然後用戶端去認證伺服器擷取令牌時不需要攜帶使用者資訊,完全脫離了使用者,也就不存在授權問題了。

學會 OAuth2.0 授權登入,10 張圖就夠了

發送請求如下

學會 OAuth2.0 授權登入,10 張圖就夠了

擷取成功之後可以去通路資源了。

重新整理token

學會 OAuth2.0 授權登入,10 張圖就夠了

權限校驗

除了我們在資料庫中為用戶端配置資源服務外,我們還可以動态的給使用者配置設定接口的權限。

①開啟Security内置的動态配置

在開啟資源服務時給ResourceServerConfig類增加注解@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)

②給接口增權重限

@GetMapping("/findAll")
@Secured("ROLE_PRODUCT")
public String findAll(){
    return "産品清單查詢成功";
}
           

③在使用者登入時設定使用者權限

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 SysUser sysUser = this.baseMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
 sysUser.setRoleList(AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_PRODUCT"));
 return sysUser;
}

           

然後測試會發現可以正常通路。

采坑

包名問題

當我在建立項目的時候,給product和server兩個子產品設定了不同的包名,導緻發送請求擷取資源時報錯。

經過分析得知,在登入賬号時會将使用者的資訊存儲到oauth_access_token表的authentication中,在進行token校驗時會根據token_id取出該字段進行反序列化,如果此時發現包名不一緻便會導緻解析token失敗,是以請求資源失敗。

解決思路

  • 兩個項目的包名改為一緻;
  • 可以将使用者和權限的實體抽成單獨的子產品,供其它子產品引用;
  • loadUserByUsername方法中使用的使用者實體類不需要繼承UserDetailsService類,每次傳回時用user類包裝一下即可;

資料庫問題

當我在進行權限校驗測試時,在設定權限時發現少打了一個單詞,導緻請求一直出錯。修改完成之後繼續請求,仍提示權限不足。

于是我将資料庫中oauth_refresh_token和oauth_access_token的資料清除,重新開始測試就可以了。

個人認為是生成token時發現資料庫中token存在,故不重新整理token,但進行校驗時卻用帶有權限辨別的token前去校驗導緻失敗。

至于其它的小坑在這不再贅述,如果遇到問題,建議按照流程對比我的源碼仔細檢查,回複“oauth2”擷取源碼。

小結

本文從原理、應用場景、認證流程出發,對oauth2進行了基本的講解,并且手把手帶大家完成了項目的搭建。大家在對授權碼模式、簡化模式、密碼模式、用戶端模式進行測試的同時要将重點放到授權碼模式上。

以上就是今天的全部内容了,希望對大家有所幫助,我們下期再見

來自公衆号:阿Q說代碼