如何在SpringBoot Security中實現(xiàn)OAuth2授權(quán)

OAuth2已經(jīng)成為了一個授權(quán)的標(biāo)準(zhǔn)協(xié)議,大家在很多的產(chǎn)品中都能看到它的身影,比如我們登錄一個網(wǎng)站蛾找,支持微信,QQ等登錄方式霞幅,這里QQ和微信提供了授權(quán)服務(wù)漠吻。有很多的授權(quán)組件都能提供這種OAuth2認(rèn)證方式,比如keycloak等司恳,但我們也可以在SpringBoot security中實現(xiàn)OAuth2授權(quán)途乃,這篇文章將詳細(xì)講解每一個步驟來演示如何在SpringBoot中實現(xiàn)OAuth2授權(quán)。

OAuth 2 簡介

OAuth 2 是一種授權(quán)協(xié)議扔傅,用于通過 HTTP 協(xié)議提供對受保護(hù)資源的訪問耍共。OAuth2 使第三方應(yīng)用程序能夠獲得對資源的有限訪問。資源的所有者告訴系統(tǒng)猎塞,同意授權(quán)第三方應(yīng)用進(jìn)入系統(tǒng)试读,獲取對這些資源訪問。系統(tǒng)從而產(chǎn)生一個短期的進(jìn)入令牌(token)荠耽,用來代替密碼钩骇,供第三方應(yīng)用使用。
由于授權(quán)的場景眾多铝量,OAuth 2.0 協(xié)議定義了獲取令牌的四種授權(quán)方式倘屹,分別是:

  • 授權(quán)碼模式:授權(quán)碼模式(authorization code)是功能最完整、流程最嚴(yán)密的授權(quán)模式款违。它的特點就是通過客戶端的后臺服務(wù)器唐瀑,與"服務(wù)提供商"的認(rèn)證服務(wù)器進(jìn)行互動。

  • 簡化模式:簡化模式(implicit grant type)不通過第三方應(yīng)用程序的服務(wù)器插爹,直接在瀏覽器中向認(rèn)證服務(wù)器申請令牌,跳過了"授權(quán)碼"這個步驟请梢,因此得名赠尾。所有步驟在瀏覽器中完成,令牌對訪問者是可見的毅弧,且客戶端不需要認(rèn)證气嫁。

  • 密碼模式:密碼模式(Resource Owner Password Credentials Grant)中,用戶向客戶端提供自己的用戶名和密碼够坐〈缦客戶端使用這些信息,向"服務(wù)商提供商"索要授權(quán)元咙。

  • 客戶端模式:客戶端模式(Client Credentials Grant)指客戶端以自己的名義梯影,而不是以用戶的名義,向"服務(wù)提供商"進(jìn)行認(rèn)證庶香。嚴(yán)格地說甲棍,客戶端模式并不屬于OAuth框架所要解決的問題。在這種模式中赶掖,用戶直接向客戶端注冊感猛,客戶端以自己的名義要求"服務(wù)提供商"提供服務(wù)七扰,其實不存在授權(quán)問題。

四種授權(quán)模式分別使用不同的 grant_type 來區(qū)分

角色

OAuth 定義了四個角色

  • 資源所有者——應(yīng)用程序的用戶陪白。
  • 客戶端 – 第三方應(yīng)用程序颈走,需要訪問資源服務(wù)器上的用戶數(shù)據(jù)的應(yīng)用程序。
  • 資源服務(wù)器 - 存儲用戶數(shù)據(jù)和 資源 服務(wù)咱士,這些服務(wù)可以將用戶數(shù)據(jù)返回給經(jīng)過身份驗證的客戶端立由。
  • 授權(quán)服務(wù)器——負(fù)責(zé)驗證用戶身份并提供授權(quán)令牌。此令牌被資源服務(wù)器接受并驗證你的身份司致。


    image.png

訪問令牌(access_token)與刷新令牌(refresh_token)

  • 訪問令牌是一個字符串拆吆,表示頒發(fā)給客戶端的授權(quán)。 令牌代表特定的訪問范圍和持續(xù)時間脂矫,由資源所有者授予枣耀,并由資源服務(wù)器和授權(quán)服務(wù)器強(qiáng)制執(zhí)行。

  • 刷新令牌(與訪問令牌一起)由授權(quán)服務(wù)器頒發(fā)給客戶端庭再,用于在當(dāng)前訪問令牌失效或過期時獲取新的訪問令牌。 刷新令牌還用于獲取具有相同或更窄范圍的其他訪問令牌(訪問令牌可能具有比資源所有者授權(quán)的更短的生命周期和更少的權(quán)限)拄轻。 頒發(fā)刷新令牌是可選的颅围,由授權(quán)服務(wù)器決定院促。

訪問令牌的職責(zé)是在數(shù)據(jù)過期之前訪問數(shù)據(jù)辉浦。
刷新令牌的職責(zé)是在現(xiàn)有訪問令牌過期時請求新的訪問令牌宪郊。

OAuth2-創(chuàng)建授權(quán)服務(wù)器

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.springexamples.demo</groupId>
        <artifactId>SpringExamples</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.howtodoinjava</groupId>
    <artifactId>oauth2</artifactId>
    <name>oauth2</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

要使用 spring security oauth2 模塊創(chuàng)建授權(quán)服務(wù)器,我們需要使用注解@EnableAuthorizationServer 并擴(kuò)展類 AuthorizationServerConfigurerAdapter艰争。

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

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

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
            .inMemory()
            .withClient("clientapp")
            .secret(passwordEncoder.encode("123456"))
            .authorizedGrantTypes("password", "authorization_code", "refresh_token")
            .authorities("READ_ONLY_CLIENT")
            .scopes("read_profile_info")
            .resourceIds("oauth2-resource")
            .redirectUris("http://localhost:8081/login")
            .accessTokenValiditySeconds(5000)
            .refreshTokenValiditySeconds(50000);
    }
}

Spring security oauth 公開了兩個用于檢查令牌的端點(/oauth/check_token 和 /oauth/token_key)坏瞄,默認(rèn)它們在 denyAll() 之后受保護(hù)。 tokenKeyAccess() 和 checkTokenAccess() 方法打開這些端點以供使用甩卓。
ClientDetailsServiceConfigurer 用于定義客戶端詳細(xì)信息,可以基于內(nèi)存或 JDBC 實現(xiàn)鸠匀。 為了演示的簡單,例子中使用內(nèi)存實現(xiàn)逾柿。 它具有以下重要屬性:
clientId –(必需)客戶端 ID缀棍。
secret(密鑰) -(受信任的客戶端需要)客戶端密鑰,如果有的話机错。
scope – 客戶端受限的范圍爬范。 如果范圍未定義或為空(默認(rèn)),則客戶端不受范圍限制弱匪。
authorizedGrantTypes – 授權(quán)客戶端使用的授權(quán)類型青瀑。 默認(rèn)值為空。
權(quán)限 - 授予客戶端的權(quán)限(常規(guī) Spring Security 權(quán)限)萧诫。
redirectUris – 將用戶代理重定向到客戶端的重定向url斥难。 它必須是絕對 URL。

OAuth2-創(chuàng)建資源服務(wù)器

要創(chuàng)建資源服務(wù)器組件帘饶,需要使用 @EnableResourceServer 注釋并擴(kuò)展 ResourceServerConfigurerAdapter 類哑诊。

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/api/v1/**").authenticated();
    }
}

上面的配置在 /api 開始的所有端點上啟用保護(hù)。 所有其他端點都可以自由訪問及刻。
資源服務(wù)器還提供了一種機(jī)制來驗證用戶本身镀裤。 在大多數(shù)情況下,它將是基于表單的登錄缴饭。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .antMatchers("/**").permitAll().and().formLogin();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("jack")
            .password(passwordEncoder().encode("123456"))
            .roles("USER");
    }
     
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){ 
        return new BCryptPasswordEncoder(); 
    }
}

上面的 WebSecurityConfigurerAdapter 類設(shè)置了一個基于表單的登錄頁面淹禾,并使用 permitAll() 打開授權(quán) URL。

Oauth2保護(hù)Rest資源

為了簡化的目的茴扁,僅提供了一個簡單的獲取用戶profile的服務(wù),如下:

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class RestResource {
    @RequestMapping("/api/users/me")
    public ResponseEntity<UserProfile> profile() 
    {
        //Build some dummy data to return for testing
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String email = user.getUsername() + "@hotmail.com";

        UserProfile profile = new UserProfile();
        profile.setName(user.getUsername());
        profile.setEmail(email);

        return ResponseEntity.ok(profile);
    }
}

public class UserProfile {
    private String name;
    private String email;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "UserProfile [name=" + name + ", email=" + email + "]";
    }
}

授權(quán)過程

瀏覽器訪問

我們在瀏覽器中訪問 http://localhost:8080/api/users/me汪疮,會跳轉(zhuǎn)到SpringBoot security提供的登錄頁面峭火,輸入用戶名jack,密碼123456就可以訪問該api,但作為第三方應(yīng)用程序智嚷,沒有用戶名和密碼卖丸,只能通過OAuth2令牌來訪問

第三方應(yīng)用訪問

第一步,獲取用戶授權(quán)碼

如上面的流程圖所示盏道,第一步是從 URL 獲取資源所有者的授權(quán)稍浆,如下的url

http://localhost:8080/oauth/authorize?client_id=clientapp&response_type=code&scope=read_profile_info

它將跳轉(zhuǎn)到Springboot security提供的login頁面,用戶提供用戶名和密碼,我們的例子中提供用戶名jack衅枫,密碼123456

image.png

登錄成功后嫁艇,它將跳轉(zhuǎn)到授權(quán)訪問頁,如下:
image.png

用戶同意或者拒絕對第三方應(yīng)用的授權(quán)弦撩,同意后它將跳轉(zhuǎn)到http://localhost:8081/login?code=CUuyz2,其中http://localhost:8081/login是在代碼的redirectUris中提供步咪,這個網(wǎng)址可以不存在,第三方應(yīng)用需要在代碼中截取code部分來獲取授權(quán)碼益楼,本例中是CUuyz2

第二步猾漫,從授權(quán)服務(wù)器中獲取access token

上一步獲取到了授權(quán)碼,下一步第三方應(yīng)用程序?qū)⑹褂檬跈?quán)碼來獲取訪問令牌感凤。 可以使用curl命令來獲取access token悯周。

curl -X POST "http://localhost:8080/oauth/token" -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -H "authorization: Basic Y2xpZW50YXBwOjEyMzQ1Ng==" -d "grant_type=authorization_code&code=CUuyz2&redirect_uri=http://localhost:8081/login"

authorization中需要使用basic認(rèn)證,提供用戶名clientapp陪竿,密碼123456禽翼,可以使用下面的網(wǎng)站來獲取加密后的字符串

https://www.blitter.se/utils/basic-authentication-header-generator/

授權(quán)服務(wù)將返回如下的信息,包括access_token,refresh_token和token_type等萨惑。

{"access_token":"cca36355-be99-4e13-a9db-db31fac8c28c","token_type":"bearer","refresh_token":"3b472d86-e371-422a-b0a7-fe23a7c7e8c8","expires_in":4999,"scope":"read_profile_info"}
第三步捐康,從資源服務(wù)器訪問API

獲得訪問令牌后,我們可以前往資源服務(wù)器獲取受保護(hù)的API.使用curl命令來訪問API,authorization是上一步獲取到的access_token庸蔼。

curl -X GET http://localhost:8080/api/users/me 
     -H "authorization: Bearer cca36355-be99-4e13-a9db-db31fac8c28c"

獲取信息如下:

{"name":"jack","email":"jack@hotmail.com"}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末解总,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子姐仅,更是在濱河造成了極大的恐慌花枫,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掏膏,死亡現(xiàn)場離奇詭異劳翰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)馒疹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門佳簸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颖变,你說我怎么就攤上這事生均。” “怎么了腥刹?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵马胧,是天一觀的道長。 經(jīng)常有香客問我衔峰,道長佩脊,這世上最難降的妖魔是什么蛙粘? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮威彰,結(jié)果婚禮上出牧,老公的妹妹穿的比我還像新娘。我一直安慰自己抱冷,他們只是感情好崔列,可當(dāng)我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著旺遮,像睡著了一般赵讯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上耿眉,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天边翼,我揣著相機(jī)與錄音,去河邊找鬼鸣剪。 笑死组底,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的筐骇。 我是一名探鬼主播债鸡,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼铛纬!你這毒婦竟也來了厌均?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤告唆,失蹤者是張志新(化名)和其女友劉穎棺弊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體擒悬,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡模她,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了懂牧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片侈净。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖僧凤,靈堂內(nèi)的尸體忽然破棺而出用狱,到底是詐尸還是另有隱情,我是刑警寧澤拼弃,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站摇展,受9級特大地震影響吻氧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一盯孙、第九天 我趴在偏房一處隱蔽的房頂上張望鲁森。 院中可真熱鬧,春花似錦振惰、人聲如沸歌溉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痛垛。三九已至,卻和暖如春桶蛔,著一層夾襖步出監(jiān)牢的瞬間匙头,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工仔雷, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留蹂析,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓碟婆,卻偏偏與公主長得像电抚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子竖共,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,507評論 2 359

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