Spring Security OAuth2
一. Oauth2.0
? 是開放授權(quán)的一個(gè)標(biāo)準(zhǔn)汽抚,旨在讓用戶允許第三方應(yīng)用去訪問用戶在某服務(wù)器中的特定私有資源殊橙,而可以不提供其在某服務(wù)器的賬號(hào)密碼給到第三方應(yīng)用膨蛮。通俗的話可以這樣去理解季研,假如你們公司正在開發(fā)一個(gè) 第三方應(yīng)用XXX,該應(yīng)用會(huì)需要在微信中分享出來一個(gè)活動(dòng)頁(yè)惹谐,該活動(dòng)需要讓微信用戶去參與驼卖,你們的應(yīng)用需要收集到用戶的姓名,頭像怎囚,地域等信息桥胞,那么問題來了?你的應(yīng)用如何才能拿到所有參與活動(dòng)的微信用戶的基本信息呢?
? 根據(jù)如上的描述催烘,我們可以將OAuth2分為四個(gè)角色:
- Resource Owner:資源所有者 即上述中的微信用戶
- Resource Server:資源服務(wù)器 即上述中的微信服務(wù)器,提供微信用戶基本信息給到第三方應(yīng)用
- Client:第三方應(yīng)用客戶端 即上述中你公司正在開發(fā)的第三方應(yīng)用
- Authorication Server:授權(quán)服務(wù)器 該角色可以理解為管理其余三者關(guān)系的中間層
其具體的執(zhí)行流程如下圖所示:
[圖片上傳失敗...(image-cd3d38-1589169864698)]
1.1 OAuth2的四種授權(quán)方式
1.1.1 授權(quán)碼(authorization-code)
? 這種方式是最常用的流程伊群,安全性也最高在岂,它適用于那些有后端的 Web 應(yīng)用蛮寂。授權(quán)碼通過前端傳送,令牌則是儲(chǔ)存在后端及老,而且所有與資源服務(wù)器的通信都在后端完成范抓。這樣的前后端分離,可以避免令牌泄漏匕垫。
<font color="red">第一步</font>,A 網(wǎng)站提供一個(gè)鏈接寞秃,用戶點(diǎn)擊后就會(huì)跳轉(zhuǎn)到 B 網(wǎng)站,授權(quán)用戶數(shù)據(jù)給 A 網(wǎng)站使用朗涩。下面就是 A 網(wǎng)站跳轉(zhuǎn) B 網(wǎng)站的一個(gè)示意鏈接绑改。
https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL
? 上面 URL 中,response_type
參數(shù)表示要求返回授權(quán)碼(code
)识腿,client_id
參數(shù)讓 B 知道是誰(shuí)在請(qǐng)求造壮,redirect_uri
參數(shù)是 B 接受或拒絕請(qǐng)求后的跳轉(zhuǎn)網(wǎng)址,scope
參數(shù)表示要求的授權(quán)范圍(這里是只讀)硝全。
<font color="red">第二步</font>,用戶跳轉(zhuǎn)后伟众,B 網(wǎng)站會(huì)要求用戶登錄召廷,然后詢問是否同意給予 A 網(wǎng)站授權(quán)。用戶表示同意先紫,這時(shí) B 網(wǎng)站就會(huì)跳回redirect_uri
參數(shù)指定的網(wǎng)址遮精。跳轉(zhuǎn)時(shí),會(huì)傳回一個(gè)授權(quán)碼本冲,就像下面這樣劫扒。
https://a.com/callback?code=AUTHORIZATION_CODE
<font color="red">第三步</font>,A 網(wǎng)站拿到授權(quán)碼以后添怔,就可以在后端,向 B 網(wǎng)站請(qǐng)求令牌广料。
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL
? 上面 URL 中,client_id
參數(shù)和client_secret
參數(shù)用來讓 B 確認(rèn) A 的身份(client_secret
參數(shù)是保密的拦止,因此只能在后端發(fā)請(qǐng)求)糜颠,grant_type
參數(shù)的值是AUTHORIZATION_CODE
萧求,表示采用的授權(quán)方式是授權(quán)碼夸政,code
參數(shù)是上一步拿到的授權(quán)碼,redirect_uri
參數(shù)是令牌頒發(fā)后的回調(diào)網(wǎng)址守问。
<font color="red">第四步</font>,B 網(wǎng)站收到請(qǐng)求以后穆端,就會(huì)頒發(fā)令牌仿便。具體做法是向redirect_uri
指定的網(wǎng)址,發(fā)送一段 JSON 數(shù)據(jù)荒勇。
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101,
"info":{...}
}
上面 JSON 數(shù)據(jù)中闻坚,access_token
字段就是令牌,A 網(wǎng)站在后端拿到了仅偎。
1.1.2 隱藏式(implicit)
? 有些 Web 應(yīng)用是純前端應(yīng)用哨颂,沒有后端相种。這時(shí)就不能用上面的方式了品姓,必須將令牌儲(chǔ)存在前端章贞。
<font color="red">第一步</font>弛槐,A 網(wǎng)站提供一個(gè)鏈接粟按,要求用戶跳轉(zhuǎn)到 B 網(wǎng)站翔烁,授權(quán)用戶數(shù)據(jù)給 A 網(wǎng)站使用友驮。
https://b.com/oauth/authorize?
response_type=token&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL
? 上面 URL 中驾锰,response_type
參數(shù)為token
,表示要求直接返回令牌耻瑟。
<font color="red">第二步</font>,用戶跳轉(zhuǎn)到 B 網(wǎng)站喳整,登錄后同意給予 A 網(wǎng)站授權(quán)裸扶。這時(shí),B 網(wǎng)站就會(huì)跳回redirect_uri
參數(shù)指定的跳轉(zhuǎn)網(wǎng)址瞬项,并且把令牌作為 URL 參數(shù)何荚,傳給 A 網(wǎng)站。
https://a.com/callback?token=ACCESS_TOKEN
? 這種方式把令牌直接傳給前端妥衣,是很不安全的戒傻。因此,只能用于一些安全要求不高的場(chǎng)景芦倒,并且令牌的有效期必須非常短兵扬,通常就是會(huì)話期間(session)有效,瀏覽器關(guān)掉器钟,令牌就失效了。
1.1.3 密碼式(password)
? 如果你高度信任某個(gè)應(yīng)用傲霸,RFC 6749 也允許用戶把用戶名和密碼昙啄,直接告訴該應(yīng)用。該應(yīng)用就使用你的密碼孵睬,申請(qǐng)令牌伶跷,這種方式稱為"密碼式"(password)秘狞。
<font color="red">第一步</font>,A 網(wǎng)站要求用戶提供 B 網(wǎng)站的用戶名和密碼雇初。拿到以后减响,A 就直接向 B 請(qǐng)求令牌支示。
ttps://oauth.b.com/token?
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
? 上面 URL 中,grant_type
參數(shù)是授權(quán)方式颂鸿,這里的password
表示"密碼式",username
和password
是 B 的用戶名和密碼败晴。
<font color="red">第二步</font>栽渴,B 網(wǎng)站驗(yàn)證身份通過后,直接給出令牌慢味。注意僚祷,這時(shí)不需要跳轉(zhuǎn)辙谜,而是把令牌放在 JSON 數(shù)據(jù)里面,作為 HTTP 回應(yīng)装哆,A 因此拿到令牌定嗓。
這種方式需要用戶給出自己的用戶名/密碼,顯然風(fēng)險(xiǎn)很大宵溅,因此只適用于其他授權(quán)方式都無法采用的情況,而且必須是用戶高度信任的應(yīng)用恃逻。
1.1.4 客戶端憑證(client credentials)
? 最后一種方式是憑證式(client credentials)雏搂,適用于沒有前端的命令行應(yīng)用,即在命令行下請(qǐng)求令牌寇损。
<font color="red">第一步</font>凸郑,A 應(yīng)用在命令行向 B 發(fā)出請(qǐng)求。
https://oauth.b.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
? 上面 URL 中矛市,grant_type
參數(shù)等于client_credentials
表示采用憑證式芙沥,client_id
和client_secret
用來讓 B 確認(rèn) A 的身份浊吏。
<font color="red">第二步</font>,B 網(wǎng)站驗(yàn)證通過以后歌憨,直接返回令牌躺孝。
這種方式給出的令牌植袍,是針對(duì)第三方應(yīng)用的于个,而不是針對(duì)用戶的厅篓,即有可能多個(gè)用戶共享同一個(gè)令牌。
二. 授權(quán)服務(wù)器的搭建
2.1 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
2.2 配置
spring:
datasource:
url: jdbc:mysql://mysql:3306/oauth2?useSSL=false&serverTimezone=UTC
username: root
password:
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 20
max-active: 50
min-idle: 15
validation-query: 'select 1'
test-on-borrow: false
test-on-return: false
test-while-idle: true
# psCache, 緩存preparedStatement, 對(duì)支持游標(biāo)的數(shù)據(jù)庫(kù)性能有巨大的提升,oracle開啟档押,mysql建議關(guān)閉
pool-prepared-statements: false
# psCache開啟的時(shí)候有效
max-open-prepared-statements: 100
# 一個(gè)連接在被驅(qū)逐出連接池的時(shí)候令宿,在連接池中最小的空閑時(shí)間粒没,單位為毫秒
min-evictable-idle-time-millis: 30000
# 距離上次釋放空閑連接的時(shí)間間隔
time-between-eviction-runs-millis: 30000
2.3 數(shù)據(jù)庫(kù)表的創(chuàng)建
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
create table oauth_client_token (
token_id VARCHAR(256),
token blob,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
create table oauth_access_token (
token_id VARCHAR(256),
token blob,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication blob,
refresh_token VARCHAR(256)
);
create table oauth_refresh_token (
token_id VARCHAR(256),
token blob,
authentication blob
);
create table oauth_code (
code VARCHAR(256), authentication blob
);
create table oauth_approvals (
userId VARCHAR(256),
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
說明:數(shù)據(jù)庫(kù)表是依據(jù)spring-security的官網(wǎng)爽撒,地址為:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql匆浙,但是在創(chuàng)建的時(shí)候?qū)⑺械淖侄晤愋蚅ONGVARBINARY改為BLOB類型。
數(shù)據(jù)庫(kù)的說明參考:http://andaily.com/spring-oauth-server/db_table_description.html
2.4 用戶登錄認(rèn)證
@Component
public class UserSecurityService implements UserDetailsService {
private static Logger logger = LoggerFactory.getLogger(UserSecurityService.class);
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("用戶名:" + username);
return new User(username, "$2a$10$TWf8wOKvyAeuJiL/gj8AfeWOrW9vr6g4Q6kJ.PZ1bt53ISRXTTcga",
Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")));
}
}
2.5 web安全配置
@Configuration
public class WebAuthorizationConfig extends WebSecurityConfigurerAdapter {
// 密碼的加解密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests()
.antMatchers("/login.html").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
}
2.6 授權(quán)服務(wù)器配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private LoginAuthencation loginAuthencation;
@Autowired
private PasswordEncoder passwordEncoder;
@Resource
private DataSource dataSource;
// 根據(jù)用戶的client_id查詢用戶的授權(quán)信息
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
//用于將token信息存放在數(shù)據(jù)庫(kù)中
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
// authentication_code放入到數(shù)據(jù)中
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 采用數(shù)據(jù)庫(kù)的方式查詢用戶的授權(quán)信息
clients.withClientDetails(clientDetails());
/** 供學(xué)習(xí)使用
clients.inMemory()
// client_id
.withClient("client")
// client_secret
.secret("secret")
// 該client允許的授權(quán)類型言秸,不同的類型举畸,則獲得token的方式不一樣抄沮。
.authorizedGrantTypes("authorization_code")
.scopes("all")
//回調(diào)uri叛买,在authorization_code與implicit授權(quán)方式時(shí)率挣,用以接收服務(wù)器的返回信息
.redirectUris("http://localhost:9090/login");
*/
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 存數(shù)據(jù)庫(kù)
endpoints.tokenStore(tokenStore())
.authorizationCodeServices(authorizationCodeServices())
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
// 配置tokenServices參數(shù)
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(false);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// token的過期時(shí)間為1天
tokenServices.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1));
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
/**
* 作用是使用client_id和client_secret來做登錄認(rèn)證椒功,如果是在瀏覽器的情況下动漾,會(huì)讓用戶
* 輸入用戶名和密碼
*/
oauthServer.allowFormAuthenticationForClients();
oauthServer.checkTokenAccess("isAuthenticated()");
oauthServer.passwordEncoder(passwordEncoder);
}
}
2.7 更改默認(rèn)授權(quán)頁(yè)面
@Controller
@SessionAttributes("authorizationRequest") // 必須配置
public class AuthController {
/**
* org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint
默認(rèn)的授權(quán)頁(yè)面
*/
@RequestMapping("/oauth/confirm_access")
public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
System.out.println(authorizationRequest.getScope());
return "/oauth.html";
}
}
2.8 獲取code
? 獲取授權(quán)碼的地址:http://127.0.0.1:8080/oauth/authorize?client_id=other_client&response_type=code&redirect_uri=http://localhost:9090/login
? [圖片上傳失敗...(image-4ddc11-1589169864698)]
? 在瀏覽器地址欄的重定向地址上可以看到 code值旱眯。
2.9 獲取access_token
? 根據(jù)上一步獲取到的code值獲取acccess_token, 請(qǐng)求的地址為:
? 其中code的值為上一步請(qǐng)求獲取到的code的數(shù)據(jù)键思,返回內(nèi)容如下:
[圖片上傳失敗...(image-d0a6d4-1589169864698)]
2.10 獲取額外的信息
? Oauth2在獲取用戶額外信息的時(shí)候吼鳞,內(nèi)部實(shí)現(xiàn)上并沒有去做赔桌,所以需要我們自己去實(shí)現(xiàn),實(shí)現(xiàn)的方式就是去重寫其代碼音诫,思路是從數(shù)據(jù)庫(kù)查詢到的信息封裝到 ClientDetails中竭钝,但是內(nèi)部卻沒有開放出來香罐,所以需要去找到是在何處查詢數(shù)據(jù)庫(kù)时肿,根據(jù)源代碼的追蹤螃成,發(fā)現(xiàn)查詢數(shù)據(jù)庫(kù)的操作是在ApprovalStoreUserApprovalHandler這個(gè)類中寸宏,所以我們需要手動(dòng)的去修改其源代碼击吱,修改的內(nèi)容如下:
[圖片上傳失敗...(image-444a2f-1589169864698)]
三. 資源服務(wù)器的搭建
? 資源服務(wù)器就是用戶想要真正獲取資源的服務(wù)器,我們必須要通過2.9節(jié)中獲取到的access_token來獲取炭臭。
3.1依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
</dependencies>
3.2 配置
security:
oauth2:
resource:
# access_token的驗(yàn)證地址
token-info-uri: http://localhost:8080/oauth/check_token
client:
client-id: resources_client
client-secret: 1
3.3 資源服務(wù)賬號(hào)
? 我們需要在授權(quán)服務(wù)器上創(chuàng)建client_id和client_secret鞋仍,當(dāng)資源服務(wù)器拿到第三方的access_token后需要到授權(quán)服務(wù)器上驗(yàn)證access_token的來源是否合法威创。
3.4 資源服務(wù)器安全配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //
.antMatchers("/user").access("#oauth2.hasAnyScope('all','read')");
}
}
3.5 獲取資源
通過調(diào)用如下接口去獲取資源服務(wù)器的資源:
http://localhost:8081/user?access_token=92de29ea-df7d-4d35-b585-c740322f9028
四. JWT(Json Web Token)
4.1 傳統(tǒng)跨域登錄流程
- 用戶向服務(wù)器發(fā)送用戶名和密碼肚豺。
- 驗(yàn)證服務(wù)器后吸申,相關(guān)數(shù)據(jù)(如用戶角色截碴,登錄時(shí)間等)將保存在當(dāng)前會(huì)話中日丹。
- 服務(wù)器向用戶返回session_id蚯嫌,session信息都會(huì)寫入到用戶的Cookie齐帚。
- 用戶的每個(gè)后續(xù)請(qǐng)求都將通過在Cookie中取出session_id傳給服務(wù)器对妄。
- 服務(wù)器收到session_id并對(duì)比之前保存的數(shù)據(jù)剪菱,確認(rèn)用戶的身份。
[圖片上傳失敗...(image-a616f0-1589169864698)]
? 這種模式最大的問題是孝常,沒有分布式架構(gòu)构灸,無法支持橫向擴(kuò)展喜颁。如果使用一個(gè)服務(wù)器,該模式完全沒有問題隔披。但是奢米,如果它是服務(wù)器群集或面向服務(wù)的跨域體系結(jié)構(gòu)的話鬓长,則需要一個(gè)統(tǒng)一的session數(shù)據(jù)庫(kù)庫(kù)來保存會(huì)話數(shù)據(jù)實(shí)現(xiàn)共享尝江,這樣負(fù)載均衡下的每個(gè)服務(wù)器才可以正確的驗(yàn)證用戶身份。
? 但是在實(shí)際中常見的單點(diǎn)登陸的需求:站點(diǎn)A和站點(diǎn)B提供統(tǒng)一公司的相關(guān)服務(wù)〉□澹現(xiàn)在要求用戶只需要登錄其中一個(gè)網(wǎng)站少态,然后它就會(huì)自動(dòng)登錄到另一個(gè)網(wǎng)站彼妻。怎么做侨歉?
? 一種解決方案是聽過持久化session數(shù)據(jù)幽邓,寫入數(shù)據(jù)庫(kù)或文件持久層等牵舵。收到請(qǐng)求后,驗(yàn)證服務(wù)從持久層請(qǐng)求數(shù)據(jù)担巩。該解決方案的優(yōu)點(diǎn)在于架構(gòu)清晰涛癌,而缺點(diǎn)是架構(gòu)修改比較費(fèi)勁先匪,整個(gè)服務(wù)的驗(yàn)證邏輯層都需要重寫,工作量相對(duì)較大假颇。而且由于依賴于持久層的數(shù)據(jù)庫(kù)或者問題系統(tǒng)笨鸡,會(huì)有單點(diǎn)風(fēng)險(xiǎn)形耗,如果持久層失敗激涤,整個(gè)認(rèn)證體系都會(huì)掛掉倦踢。
4.2 Jwt
? 針對(duì)如上的問題辱挥,另外一種解決方案就是JWT(Json Web Token)晤碘,其原則是在服務(wù)器驗(yàn)證之后园爷,將生產(chǎn)的一個(gè)Json對(duì)象返回給用戶童社,格式如下:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzEyNDE2NTksInVzZXJfbmFtZSI6ImFhIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJlZTczOTI4OC0zNDMwLTQxMjUtODBhOC1lMDU0Njg5OWQ2ODIiLCJjbGllbnRfaWQiOiJteV9jbGllbnQiLCJzY29wZSI6WyJhbGwiXX0.Ji4xYQJuJRrZeCTTMOb1e2GiOESAyiI9NzbWffKzcJ0",
"token_type": "bearer",
"expires_in": 43198,
"scope": "all",
"jti": "ee739288-3430-4125-80a8-e0546899d682"
}
4.2.1 Jwt數(shù)據(jù)結(jié)構(gòu)
? 如上代碼返回的access_token為一個(gè)字符串甘改,之間用 .
分隔為為三段,其中第一段為 header(頭)腾节,第二段為 payload (負(fù)載)案腺,第三段為signature(簽名)劈榨,如下圖所示:
[圖片上傳失敗...(image-588c21-1589169864698)]
? 我們可以在 <https://jwt.io/> 在線解析這個(gè)JWT token, 如下圖所示
[圖片上傳失敗...(image-f2bd36-1589169864698)]
header
字段名 | 描述 |
---|---|
alg | 算法 |
typ | 令牌類型 |
payload
字段名 | 描述 |
---|---|
exp | 超時(shí)時(shí)間 |
jti | JWT ID |
signature
? 簽名拷姿,為了驗(yàn)證發(fā)送過來的access_token是否有效响巢,通過如下算法得到:
[圖片上傳失敗...(image-280921-1589169864698)]
4.2.2 用法與問題
? 客戶端接收服務(wù)器返回的JWT棒妨,將其存儲(chǔ)在Cookie或localStorage中券腔。此后纷纫,客戶端將在與服務(wù)器交互中都會(huì)帶JWT涛酗。如果將它存儲(chǔ)在Cookie中商叹,就可以自動(dòng)發(fā)送,因此一般是將它放入HTTP請(qǐng)求的Header Authorization字段中卵洗。當(dāng)跨域時(shí)过蹂,也可以將JWT被放置于POST請(qǐng)求的數(shù)據(jù)主體中酷勺。
? 但是JWT也面臨著諸多的問題脆诉,如下所示:
- JWT默認(rèn)不加密,但可以加密亏狰。生成原始令牌后,可以使用改令牌再次對(duì)其進(jìn)行加密辰斋。
- 當(dāng)JWT未加密方法是亡呵,一些私密數(shù)據(jù)無法通過JWT傳輸锰什。
- JWT不僅可用于認(rèn)證汁胆,還可用于信息交換嫩码。善用JWT有助于減少服務(wù)器請(qǐng)求數(shù)據(jù)庫(kù)的次數(shù)铸题。
- JWT的最大缺點(diǎn)是服務(wù)器不保存會(huì)話狀態(tài),所以在使用期間不可能取消令牌或更改令牌的權(quán)限探熔。也就是說诀艰,一旦JWT簽發(fā),在有效期內(nèi)將會(huì)一直有效绿满。
- JWT本身包含認(rèn)證信息棒口,因此一旦信息泄露,任何人都可以獲得令牌的所有權(quán)限厂抖。為了減少盜用七蜘,JWT的有效期不宜設(shè)置太長(zhǎng)橡卤。對(duì)于某些重要操作碧库,用戶在使用時(shí)應(yīng)該每次都進(jìn)行進(jìn)行身份驗(yàn)證嵌灰。
- 為了減少盜用和竊取沽瞭,JWT不建議使用HTTP協(xié)議來傳輸代碼驹溃,而是使用加密的HTTPS協(xié)議進(jìn)行傳輸。
4.3 jwt授權(quán)服務(wù)器搭建
依賴:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
</dependencies>
jwt授權(quán)服務(wù)器
@Configuration
@EnableAuthorizationServer
public class OauthAuthenticationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("123"); //設(shè)置簽名
return jwtAccessTokenConverter;
}
// 基于內(nèi)存的授權(quán)碼
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() //
.withClient("my_client") //
.secret("$2a$10$TWf8wOKvyAeuJiL/gj8AfeWOrW9vr6g4Q6kJ.PZ1bt53ISRXTTcga") //
.scopes("all") //
.authorizedGrantTypes("authorization_code")
.redirectUris("http://localhost:9090/login");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter())
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.passwordEncoder(passwordEncoder);
}
}
4.4 jwt資源服務(wù)器
@Configuration
@EnableResourceServer
public class ReourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public DefaultTokenServices defaultTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(jwtTokenStore());
return defaultTokenServices;
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("123");
return jwtAccessTokenConverter;
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //
.anyRequest() //
.authenticated() //
.and() //
.csrf().disable();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(defaultTokenServices());
}
}
五. 單點(diǎn)登錄
? 單點(diǎn)登錄(Singal Sign On)是很多企業(yè)經(jīng)常使用的一種登錄方式拐辽,那么何為單點(diǎn)登錄呢俱诸?為了解決什么樣的問題呢睁搭?舉個(gè)例子,在淘寶公司內(nèi)部,有天貓锌唾、淘寶晌涕、阿里云重窟、聚劃算等眾多的產(chǎn)品線巡扇,這些產(chǎn)品線是不同的服務(wù)器來支撐運(yùn)行霎迫,但是作為用戶的我們卻可以使用同一套賬戶名和密碼進(jìn)行登錄他幾乎所有的產(chǎn)品,登錄服務(wù)器只有一個(gè)涩赢,根據(jù)登錄服務(wù)器返回的特定的信息筒扒,可以去訪問它所有的產(chǎn)品線花墩。著就是所謂的單點(diǎn)登錄。
? 在具體的實(shí)現(xiàn)的時(shí)候祠肥,我們使用JWT的方式來實(shí)現(xiàn)單點(diǎn)登錄仇箱。他的處理流程如下:
5.1 認(rèn)證服務(wù)器
依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
</dependencies>
授權(quán)服務(wù)
@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationConfigServer extends AuthorizationServerConfigurerAdapter {
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("abcxyz");
return jwtAccessTokenConverter;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() //
.withClient("client-a") //
.secret("$2a$10$TWf8wOKvyAeuJiL/gj8AfeWOrW9vr6g4Q6kJ.PZ1bt53ISRXTTcga") //
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris("http://localhost:8081/clientA/login")
.autoApprove(true)
.and()
.withClient("client-b") //
.secret("$2a$10$TWf8wOKvyAeuJiL/gj8AfeWOrW9vr6g4Q6kJ.PZ1bt53ISRXTTcga") //
.authorizedGrantTypes("authorization_code")
.scopes("all")
.redirectUris("http://localhost:8082/clientB/login")
.autoApprove(true);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints//.tokenStore(jwtTokenStore())
.accessTokenConverter(jwtAccessTokenConverter());
}
}
5.2 子系統(tǒng)
依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
</dependencies>
配置
server:
port: 8081
servlet:
context-path: /clientA
security:
oauth2:
client:
client-id: client-a
client-secret: 1
access-token-uri: http://localhost:7070/auth/oauth/token
user-authorization-uri: http://localhost:7070/auth/oauth/authorize
resource:
jwt:
key-value: abcxyz
啟動(dòng)類配置
@SpringBootApplication
@EnableOAuth2Sso
public class SsoClientApplicationA {
public static void main( String[] args ) {
SpringApplication.run(SsoClientApplicationA.class, args);
}
}