OAuth2.0授權(quán)碼模式實戰(zhàn)

OAuth2.0是目前比較流行的一種開源授權(quán)協(xié)議晕城,可以用來授權(quán)第三方應(yīng)用逛薇,允許在不將用戶名和密碼提供給第三方應(yīng)用的情況下獲取一定的用戶資源妹孙,目前很多網(wǎng)站或APP基于微信或QQ的第三方登錄方式都是基于OAuth2實現(xiàn)的诊胞。本文將基于OAuth2中的授權(quán)碼模式益涧,采用數(shù)據(jù)庫配置方式,搭建認證服務(wù)器與資源服務(wù)器院崇,完成授權(quán)與資源的訪問肆氓。

流程分析

在OAuth2中,定義了4種不同的授權(quán)模式底瓣,其中授權(quán)碼模式(authorization code)功能流程相對更加完善谢揪,也被更多的系統(tǒng)采用。首先使用圖解的方式簡單了解一下它的授權(quán)流程:

image
  • 對上面的流程進行一下說明:
    1捐凭、用戶訪問客戶端

    2拨扶、客戶端將用戶導(dǎo)向認證服務(wù)器

    3、用戶登錄茁肠,并對第三方客戶端進行授權(quán)

    4患民、認證服務(wù)器將用戶導(dǎo)向客戶端事先指定的重定向地址,并附上一個授權(quán)碼

    5官套、客戶端使用授權(quán)碼酒奶,向認證服務(wù)器換取令牌

    6蚁孔、認證服務(wù)器對客戶端進行認證以后,發(fā)放令牌

    7惋嚎、客戶端使用令牌杠氢,向資源服務(wù)器申請獲取資源

    8、資源服務(wù)器確認令牌另伍,向客戶端開放資源

在對授權(quán)碼模式的流程有了一定基礎(chǔ)的情況下鼻百,我們開始動手搭建項目。

項目搭建

準(zhǔn)備工作

1摆尝、在Project中創(chuàng)建兩個module温艇,采用認證服務(wù)器和資源服務(wù)器分離的架構(gòu):

image

2、spring-security-oauth2是對Oauth2協(xié)議規(guī)范的一種實現(xiàn)堕汞,這里可以直接使用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讯检、數(shù)據(jù)庫建表琐鲁,OAuth2需要的表結(jié)構(gòu)如下:

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;

  • oauth_access_token:存儲生成的access_token,由類JdbcTokenStore操作
  • oauth_client_details:存儲客戶端的配置信息人灼,由類JdbcClientDetailsService操作
  • oauth_code:存儲服務(wù)端系統(tǒng)生成的code的值围段,由類JdbcAuthorizationCodeServices操作
  • oauth_refresh_token:存儲刷新令牌的refresh_token,如果客戶端的grant_type不支持refresh_token投放,那么不會用到這張表奈泪,同樣由類JdbcTokenStore操作

其余spring security相關(guān)的用戶表、角色表以及權(quán)限表的表結(jié)構(gòu)在這里省略灸芳,可以在文末的Git地址中下載涝桅。

認證服務(wù)器

認證服務(wù)器是服務(wù)提供者專門用來處理認證授權(quán)的服務(wù)器,主要負責(zé)獲取用戶授權(quán)并頒發(fā)token耗绿,以及完成后續(xù)的token認證工作苹支。認證部分功能主要由spring security 負責(zé)砾隅,授權(quán)則由oauth2負責(zé)误阻。

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的方法究反,實現(xiàn)個性化配置。如果使用內(nèi)存配置用戶儒洛,可以重寫其中的configure方法進行配置精耐,由于我們使用數(shù)據(jù)庫中的用戶信息,所以不需要在這里進行配置琅锻。并且采用認證服務(wù)器和資源服務(wù)器分離卦停,也不需要在這里對服務(wù)資源進行權(quán)限的配置向胡。

在類中創(chuàng)建了兩個Bean,分別是用于處理認證請求的認證管理器AuthenticationManager惊完,以及配置全局統(tǒng)一使用的密碼加密方式BCryptPasswordEncoder僵芹,它們會在認證服務(wù)中被使用。

2小槐、開啟并配置認證服務(wù)器

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

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

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

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

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //設(shè)置客戶端的配置從數(shù)據(jù)庫中讀取,存儲在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 驗證端口無權(quán)限訪問
                .checkTokenAccess("isAuthenticated()") // 開啟/oauth/check_token 驗證端口認證權(quán)限訪問
                .allowFormAuthenticationForClients()// 允許表單認證
                .passwordEncoder(passwordEncoder);   // 配置BCrypt加密
    }
}

在類中,通過@EnableAuthorizationServer 注解開啟認證服務(wù)控嗜,通過繼承父類AuthorizationServerConfigurerAdapter茧彤,對以下信息進行了配置:

  • ClientDetailsServiceConfigurer:配置客戶端服務(wù),這里我們通過JdbcClientDetailsService從數(shù)據(jù)庫讀取相應(yīng)的客戶端配置信息疆栏,進入源碼可以看到客戶端信息是從表oauth_client_details中拉取棘街。

  • AuthorizationServerEndpointsConfigurer:用來配置授權(quán)(authorization)以及令牌(token)的訪問端點,以及令牌服務(wù)的配置信息承边。該類作為一個裝載類遭殉,裝載了Endpoints所有的相關(guān)配置。

  • AuthorizationServerSecurityConfigurer:配置令牌端點(endpoint)的安全約束博助,OAuth2開放了端點用于檢查令牌险污,/oauth/check_token/oauth/token_key這些端點默認受到保護,在這里配置可被外部調(diào)用富岳。

3蛔糯、采用從數(shù)據(jù)庫中獲取用戶信息的方式進行身份驗證

@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<>();
        //獲取用戶權(quán)限
        List<TbPermission> permissions = permissionService.getByUserId(tbUser.getId());
        permissions.forEach(permission->{
            authorities.add(new SimpleGrantedAuthority(permission.getEname()));
        });
        return new User(tbUser.getUsername(),tbUser.getPassword(),authorities);
    }
}

創(chuàng)建UserDetailServiceImpl 實現(xiàn)UserDetailsService接口,并實現(xiàn)loadUserByUsername方法窖式,根據(jù)用戶名從數(shù)據(jù)庫查詢用戶信息及權(quán)限蚁飒。

4、啟動服務(wù)

首先發(fā)起請求獲取授權(quán)碼(code)萝喘,直接訪問下面的url

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

看一下各個參數(shù)的意義:

client_id:因為認證服務(wù)器要知道是哪一個應(yīng)用在請求授權(quán)淮逻,所以client_id就是認證服務(wù)器給每個應(yīng)用分配的id

redirect_uri:重定向地址,會在這個重定向地址后面附加授權(quán)碼阁簸,讓第三方應(yīng)用獲取code

response_typecode表明采用授權(quán)碼認證模式

scope:需要獲得哪些授權(quán)爬早,這個參數(shù)的值是由服務(wù)提供商定義的,不能隨意填寫

首先會重定向到登錄驗證頁面启妹,因為之前的url中只明確了第三方應(yīng)用的身份筛严,這里要確定第三方應(yīng)用要請求哪一個用戶的授權(quán)。輸入數(shù)據(jù)庫表tb_user中配置的用戶信息 admin/123456

image

注意url中請求的參數(shù)必須和在數(shù)據(jù)庫中的表oauth_client中配置的相同饶米,如果不存在或信息不一致都會報錯桨啃,在參數(shù)填寫錯誤時會產(chǎn)生如下報錯信息:

image

如果參數(shù)完全匹配车胡,會請求用戶向請求資源的客戶端client授權(quán):

image

點擊Authorize同意授權(quán),會跳轉(zhuǎn)到redirect_uri定義的重定向地址照瘾,并在url后面附上授權(quán)碼code

image

這樣吨拍,用戶的登錄和授權(quán)的操作都在瀏覽器中完成了,接下來我們需要獲取令牌网杆,發(fā)送post請求到/oauth/token接口羹饰,使用授權(quán)碼獲取access_token。在發(fā)送請求時碳却,需要在請求頭中包含clientIdclientSecret队秩,并且攜帶參數(shù) grant_typecode昼浦、redirect_uri馍资,這里會對redirect_uri做二次驗證:

image

這樣,就通過/oauth/token端點獲取到了access_token关噪,并一同拿到了它的令牌類型鸟蟹、過期時間、授權(quán)范圍信息使兔,這個令牌將在請求資源服務(wù)器的資源時被使用建钥。

資源服務(wù)器

資源服務(wù)器簡單來說就是資源的訪問入口,主要負責(zé)處理用戶數(shù)據(jù)的api調(diào)用虐沥,資源服務(wù)器中存儲了用戶數(shù)據(jù)坏瞄,并對外提供http服務(wù)冀痕,可以將用戶數(shù)據(jù)返回給經(jīng)過身份驗證的客戶端保礼。資源服務(wù)器和認證服務(wù)器可以部署在一起须喂,也可以分離部署,我們這里采用分開部署的形式天试。

1槐壳、配置資源服務(wù)器

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

    @Bean
    @Primary
    public RemoteTokenServices remoteTokenServices(){
        final RemoteTokenServices tokenServices=new RemoteTokenServices();
        //設(shè)置授權(quán)服務(wù)器check_token Endpoint 完整地址
        tokenServices.setCheckTokenEndpointUrl("http://localhost:9004/oauth/check_token");
        //設(shè)置客戶端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);
    }
}

在類中主要實現(xiàn)了以下功能:

  • @EnableResourceServer注解表明開啟OAuth2資源服務(wù)器喜每,在請求資源服務(wù)器的請求前务唐,需要通過認證服務(wù)器獲取access_token令牌,然后在訪問資源服務(wù)器中的資源時需要攜帶令牌才能正常進行請求

  • 通過RemoteTokenServices實現(xiàn)自定義認證服務(wù)器灼卢,這里配置了我們之前創(chuàng)建的認證服務(wù)器

  • 重寫configure(HttpSecurity http)方法绍哎,開啟所有請求需要授權(quán)才可以訪問

  • 配置資源相關(guān)設(shè)置configure(ResourceServerSecurityConfigurer resources)来农,這里只設(shè)置resourceId鞋真,作為該服務(wù)資源的唯一標(biāo)識

2、測試接口沃于,負責(zé)提供用戶信息

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

3涩咖、啟動服務(wù)

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

image

使用Postman,在Authorization中配置使用Bearer Token檩互,并填入從認證服務(wù)器獲取的access_token(或在Headers中的Authorization字段直接填寫Bearer 'access_token'),再次訪問接口特幔,可以正常訪問接口資源:

image

如果文章對您有所幫助,歡迎關(guān)注公眾號 碼農(nóng)參上闸昨。項目Git地址:

https://github.com/trunks2008/oauth2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚯斯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子饵较,更是在濱河造成了極大的恐慌拍嵌,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,946評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件循诉,死亡現(xiàn)場離奇詭異横辆,居然都是意外死亡,警方通過查閱死者的電腦和手機茄猫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評論 3 399
  • 文/潘曉璐 我一進店門狈蚤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人划纽,你說我怎么就攤上這事脆侮。” “怎么了勇劣?”我有些...
    開封第一講書人閱讀 169,716評論 0 364
  • 文/不壞的土叔 我叫張陵他嚷,是天一觀的道長。 經(jīng)常有香客問我芭毙,道長筋蓖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,222評論 1 300
  • 正文 為了忘掉前任退敦,我火速辦了婚禮粘咖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘侈百。我一直安慰自己瓮下,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 69,223評論 6 398
  • 文/花漫 我一把揭開白布钝域。 她就那樣靜靜地躺著讽坏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪例证。 梳的紋絲不亂的頭發(fā)上路呜,一...
    開封第一講書人閱讀 52,807評論 1 314
  • 那天,我揣著相機與錄音,去河邊找鬼胀葱。 笑死漠秋,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的抵屿。 我是一名探鬼主播庆锦,決...
    沈念sama閱讀 41,235評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼轧葛!你這毒婦竟也來了搂抒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,189評論 0 277
  • 序言:老撾萬榮一對情侶失蹤尿扯,失蹤者是張志新(化名)和其女友劉穎燕耿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姜胖,經(jīng)...
    沈念sama閱讀 46,712評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡誉帅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,775評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了右莱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚜锨。...
    茶點故事閱讀 40,926評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖慢蜓,靈堂內(nèi)的尸體忽然破棺而出亚再,到底是詐尸還是另有隱情,我是刑警寧澤晨抡,帶...
    沈念sama閱讀 36,580評論 5 351
  • 正文 年R本政府宣布氛悬,位于F島的核電站,受9級特大地震影響耘柱,放射性物質(zhì)發(fā)生泄漏如捅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,259評論 3 336
  • 文/蒙蒙 一调煎、第九天 我趴在偏房一處隱蔽的房頂上張望镜遣。 院中可真熱鬧,春花似錦士袄、人聲如沸悲关。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寓辱。三九已至,卻和暖如春赤拒,著一層夾襖步出監(jiān)牢的瞬間秫筏,已是汗流浹背诱鞠。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留跳昼,地道東北人般甲。 一個月前我還...
    沈念sama閱讀 49,368評論 3 379
  • 正文 我出身青樓肋乍,卻偏偏與公主長得像鹅颊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子墓造,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,930評論 2 361

推薦閱讀更多精彩內(nèi)容