OAuth2-0 授權碼模式原理與實戰

OAuth2.0 是目前比較流行的一種開源授權協議,可以用來授權第三方應用,允許在不將用戶名和密碼提供給第三方應用的情況下獲取一定的用戶資源,目前很多網站或 APP 基於微信或 QQ 的第三方登錄方式都是基於 OAuth2 實現的。本文將基於 OAuth2 中的授權碼模式,採用數據庫配置方式,搭建認證服務器與資源服務器,完成授權與資源的訪問。

流程分析

在 OAuth2 中,定義了 4 種不同的授權模式,其中授權碼模式(authorization code)功能流程相對更加完善,也被更多的系統採用。首先使用圖解的方式簡單瞭解一下它的授權流程:

在對授權碼模式的流程有了一定基礎的情況下,我們開始動手搭建項目。

準備工作

1、在 Project 中創建兩個 module,採用認證服務器和資源服務器分離的架構:

2、spring-security-oauth2是對Oauth2協議規範的一種實現,這裏可以直接使用spring-cloud-starter-oauth2,就不需要分別引入spring-securityoauth2了。在父 pom 中引入:

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

3、數據庫建表,OAuth2需要的表結構如下:

DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token`  (
  `token_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `token` blob NULL,
  `authentication_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authentication` blob NULL,
  `refresh_token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`  (
  `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `access_token_validity` int(11) NULL DEFAULT NULL,
  `refresh_token_validity` int(11) NULL DEFAULT NULL,
  `additional_information` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  `autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'false',
  PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code`  (
  `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authentication` blob NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token`  (
  `token_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `token` blob NULL,
  `authentication` blob NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

其餘spring security相關的用戶表、角色表以及權限表的表結構在這裏省略,可以在文末的 Git 地址中下載。

認證服務器

認證服務器是服務提供者專門用來處理認證授權的服務器,主要負責獲取用戶授權並頒發token,以及完成後續的token認證工作。認證部分功能主要由spring security 負責,授權則由oauth2負責。

1、開啓Spring Security配置

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

通過@Configuration@EnableWebSecurity 開啓Spring Security配置,繼承WebSecurityConfigurerAdapter的方法,實現個性化配置。如果使用內存配置用戶,可以重寫其中的configure方法進行配置,由於我們使用數據庫中的用戶信息,所以不需要在這裏進行配置。並且採用認證服務器和資源服務器分離,也不需要在這裏對服務資源進行權限的配置。

在類中創建了兩個Bean,分別是用於處理認證請求的認證管理器AuthenticationManager,以及配置全局統一使用的密碼加密方式BCryptPasswordEncoder,它們會在認證服務中被使用。

2、開啓並配置認證服務器

@Configuration
@EnableAuthorizationServer 
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;    //認證管理器
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;//密碼加密方式
    @Autowired
    private DataSource dataSource;  // 注入數據源
    @Autowired
    private UserDetailsService userDetailsService; //自定義用戶身份認證

    @Bean
    public ClientDetailsService jdbcClientDetailsService(){
        //將client信息存儲在數據庫中
        return new JdbcClientDetailsService(dataSource);
    }

    @Bean
    public TokenStore tokenStore(){
        //對token進行持久化存儲在數據庫中,數據存儲在oauth_access_token和oauth_refresh_token
        return new JdbcTokenStore(dataSource);
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        //加入對授權碼模式的支持
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //設置客戶端的配置從數據庫中讀取,存儲在oauth_client_details表
        clients.withClientDetails(jdbcClientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .tokenStore(tokenStore())//token存儲方式
                .authenticationManager(authenticationManager)// 開啓密碼驗證,來源於 WebSecurityConfigurerAdapter
                .userDetailsService(userDetailsService)// 讀取驗證用戶的信息
                .authorizationCodeServices(authorizationCodeServices())
                .setClientDetailsService(jdbcClientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //  配置Endpoint,允許請求,不被Spring-security攔截
        security.tokenKeyAccess("permitAll()") // 開啓/oauth/token_key 驗證端口無權限訪問
                .checkTokenAccess("isAuthenticated()") // 開啓/oauth/check_token 驗證端口認證權限訪問
                .allowFormAuthenticationForClients()// 允許表單認證
                .passwordEncoder(passwordEncoder);   // 配置BCrypt加密
    }
}

在類中,通過@EnableAuthorizationServer 註解開啓認證服務,通過繼承父類AuthorizationServerConfigurerAdapter,對以下信息進行了配置:

3、採用從數據庫中獲取用戶信息的方式進行身份驗證

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private TbUserService userService;
    @Autowired
    private TbPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        TbUser tbUser = userService.getUserByUserName(userName);
        if (tbUser==null){
            throw new UsernameNotFoundException("username : "+userName+" is not exist");
        }

        List<GrantedAuthority> authorities=new ArrayList<>();
        //獲取用戶權限
        List<TbPermission> permissions = permissionService.getByUserId(tbUser.getId());
        permissions.forEach(permission->{
            authorities.add(new SimpleGrantedAuthority(permission.getEname()));
        });
        return new User(tbUser.getUsername(),tbUser.getPassword(),authorities);
    }
}

創建UserDetailServiceImpl 實現UserDetailsService接口,並實現loadUserByUsername方法,根據用戶名從數據庫查詢用戶信息及權限。

4、啓動服務

首先發起請求獲取授權碼(code),直接訪問下面的url

http://localhost:9004/oauth/authorize?client_id=client1&redirect_uri=http://localhost:8848/nacos&response_type=code&scope=select

看一下各個參數的意義:

client_id:因爲認證服務器要知道是哪一個應用在請求授權,所以client_id就是認證服務器給每個應用分配的id

redirect_uri:重定向地址,會在這個重定向地址後面附加授權碼,讓第三方應用獲取code

response_typecode表明採用授權碼認證模式

scope:需要獲得哪些授權,這個參數的值是由服務提供商定義的,不能隨意填寫

首先會重定向到登錄驗證頁面,因爲之前的url中只明確了第三方應用的身份,這裏要確定第三方應用要請求哪一個用戶的授權。輸入數據庫表tb_user中配置的用戶信息 admin/123456

注意url中請求的參數必須和在數據庫中的表oauth_client中配置的相同,如果不存在或信息不一致都會報錯,在參數填寫錯誤時會產生如下報錯信息:

如果參數完全匹配,會請求用戶向請求資源的客戶端client授權:

點擊Authorize同意授權,會跳轉到redirect_uri定義的重定向地址,並在 url 後面附上授權碼code

這樣,用戶的登錄和授權的操作都在瀏覽器中完成了,接下來我們需要獲取令牌,發送 post 請求到/oauth/token接口,使用授權碼獲取access_token。在發送請求時,需要在請求頭中包含clientIdclientSecret,並且攜帶參數 grant_typecoderedirect_uri,這裏會對redirect_uri做二次驗證:

這樣,就通過/oauth/token端點獲取到了access_token,並一同拿到了它的令牌類型、過期時間、授權範圍信息,這個令牌將在請求資源服務器的資源時被使用。由於這個令牌在一定時間內有效,客戶端可以在有效期內,將令牌保存在本地,避免重複申請。

資源服務器

資源服務器簡單來說就是資源的訪問入口,主要負責處理用戶數據的api調用,資源服務器中存儲了用戶數據,並對外提供http服務,可以將用戶數據返回給經過身份驗證的客戶端。資源服務器和認證服務器可以部署在一起,也可以分離部署,我們這裏採用分開部署的形式。

1、配置資源服務器

@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Primary
    public RemoteTokenServices remoteTokenServices(){
        final RemoteTokenServices tokenServices=new RemoteTokenServices();
        //設置授權服務器check_token Endpoint 完整地址
        tokenServices.setCheckTokenEndpointUrl("http://localhost:9004/oauth/check_token");
        //設置客戶端id與secret,注意:client_secret 值不能使用passwordEncoder加密
        tokenServices.setClientId("client1");
        tokenServices.setClientSecret("client-secret");
        return tokenServices;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        http.authorizeRequests()
                .anyRequest().authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("oauth2").stateless(true);
    }
}

在類中主要實現了以下功能:

2、測試接口,負責提供用戶信息

@RestController
public class TestController {
    @GetMapping("/user/{name}")
    public User user(@PathVariable String name){
        return new User(name, 20);
    }
}

3、啓動服務

不攜帶access_token,直接訪問接口http://127.0.0.1:9005/user/hydra:

使用 Postman,在Authorization中配置使用Bearer Token,並填入從認證服務器獲取的access_token(或在Headers中的Authorization字段直接填寫Bearer 'access_token')。

再次訪問接口,可以正常訪問接口資源,這樣就實現了對資源服務器內資源的訪問,完成了認證服務器與資源服務器的整合。

項目 git 地址:https://github.com/trunks2008/oauth2

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/jofLr2fhYbtSDK2tZ1gEXw