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
登錄成功后嫁艇,它將跳轉(zhuǎn)到授權(quán)訪問頁,如下:
用戶同意或者拒絕對第三方應(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"}