小編給大家分享一下Spring Cloud OAuth2如何實(shí)現(xiàn)用戶認(rèn)證及單點(diǎn)登錄,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
鹽城網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián),鹽城網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為鹽城上1000家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\外貿(mào)網(wǎng)站建設(shè)要多少錢(qián),請(qǐng)找那個(gè)售后服務(wù)好的鹽城做網(wǎng)站的公司定做!
OAuth 2 有四種授權(quán)模式,分別是授權(quán)碼模式(authorization code)、簡(jiǎn)化模式(implicit)、密碼模式(resource owner password credentials)、客戶端模式(client credentials),具體 OAuth3 是什么,可以參考這篇文章。(http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)
本文我們將使用授權(quán)碼模式和密碼模式兩種方式來(lái)實(shí)現(xiàn)用戶認(rèn)證和授權(quán)管理。
OAuth3 其實(shí)是一個(gè)關(guān)于授權(quán)的網(wǎng)絡(luò)標(biāo)準(zhǔn),它制定了設(shè)計(jì)思路和運(yùn)行流程,利用這個(gè)標(biāo)準(zhǔn)我們其實(shí)是可以自己實(shí)現(xiàn) OAuth3 的認(rèn)證過(guò)程的。今天要介紹的 spring-cloud-starter-oauth3 ,其實(shí)是 Spring Cloud 按照 OAuth3 的標(biāo)準(zhǔn)并結(jié)合 spring-security 封裝好的一個(gè)具體實(shí)現(xiàn)。
什么情況下需要用 OAuth3
首先大家最熟悉的就是幾乎每個(gè)人都用過(guò)的,比如用微信登錄、用 QQ 登錄、用微博登錄、用 Google 賬號(hào)登錄、用 github 授權(quán)登錄等等,這些都是典型的 OAuth3 使用場(chǎng)景。假設(shè)我們做了一個(gè)自己的服務(wù)平臺(tái),如果不使用 OAuth3 登錄方式,那么我們需要用戶先完成注冊(cè),然后用注冊(cè)號(hào)的賬號(hào)密碼或者用手機(jī)驗(yàn)證碼登錄。而使用了 OAuth3 之后,相信很多人使用過(guò)、甚至開(kāi)發(fā)過(guò)公眾號(hào)網(wǎng)頁(yè)服務(wù)、小程序,當(dāng)我們進(jìn)入網(wǎng)頁(yè)、小程序界面,第一次使用就無(wú)需注冊(cè),直接使用微信授權(quán)登錄即可,大大提高了使用效率。因?yàn)槊總€(gè)人都有微信號(hào),有了微信就可以馬上使用第三方服務(wù),這體驗(yàn)不要太好了。而對(duì)于我們的服務(wù)來(lái)說(shuō),我們也不需要存儲(chǔ)用戶的密碼,只要存儲(chǔ)認(rèn)證平臺(tái)返回的唯一ID 和用戶信息即可。
以上是使用了 OAuth3 的授權(quán)碼模式,利用第三方的權(quán)威平臺(tái)實(shí)現(xiàn)用戶身份的認(rèn)證。當(dāng)然了,如果你的公司內(nèi)部有很多個(gè)服務(wù),可以專(zhuān)門(mén)提取出一個(gè)認(rèn)證中心,這個(gè)認(rèn)證中心就充當(dāng)上面所說(shuō)的權(quán)威認(rèn)證平臺(tái)的角色,所有的服務(wù)都要到這個(gè)認(rèn)證中心做認(rèn)證。
這樣一說(shuō),發(fā)現(xiàn)沒(méi),這其實(shí)就是個(gè)單點(diǎn)登錄的功能。這就是另外一種使用場(chǎng)景,對(duì)于多服務(wù)的平臺(tái),可以使用 OAuth3 實(shí)現(xiàn)服務(wù)的單點(diǎn)登錄,只做一次登錄,就可以在多個(gè)服務(wù)中自由穿行,當(dāng)然僅限于授權(quán)范圍內(nèi)的服務(wù)和接口。
實(shí)現(xiàn)統(tǒng)一認(rèn)證功能
本篇先介紹密碼模式實(shí)現(xiàn)的單點(diǎn)登錄,下一篇再繼續(xù)說(shuō)授權(quán)碼模式。
在微服務(wù)橫行的今天,誰(shuí)敢說(shuō)自己手上沒(méi)幾個(gè)微服務(wù)。微服務(wù)減少了服務(wù)間的耦合,同時(shí)也在某些方面增加了系統(tǒng)的復(fù)雜度,比如說(shuō)用戶認(rèn)證。假設(shè)我們這里實(shí)現(xiàn)了一個(gè)電商平臺(tái),用戶看到的就是一個(gè) APP 或者一個(gè) web 站點(diǎn),實(shí)際上背后是由多個(gè)獨(dú)立的服務(wù)構(gòu)成的,比如用戶服務(wù)、訂單服務(wù)、產(chǎn)品服務(wù)等。用戶只要第一次輸入用戶名、密碼完成登錄后,一段時(shí)間內(nèi),都可以任意訪問(wèn)各個(gè)頁(yè)面,比如產(chǎn)品列表頁(yè)面、我的訂單頁(yè)面、我的關(guān)注等頁(yè)面。
我們可以想象一下,自然能夠想到,在請(qǐng)求各個(gè)服務(wù)、各個(gè)接口的時(shí)候,一定攜帶著什么憑證,然后各個(gè)服務(wù)才知道請(qǐng)求接口的用戶是哪個(gè),不然肯定有問(wèn)題,那其實(shí)這里面的憑證簡(jiǎn)單來(lái)說(shuō)就是一個(gè) Token,標(biāo)識(shí)用戶身份的 Token。
系統(tǒng)架構(gòu)說(shuō)明
認(rèn)證中心:oauth3-auth-server,OAuth3 主要實(shí)現(xiàn)端,Token 的生成、刷新、驗(yàn)證都在認(rèn)證中心完成。
訂單服務(wù):oauth3-client-order-server,微服務(wù)之一,接收到請(qǐng)求后會(huì)到認(rèn)證中心驗(yàn)證。
用戶服務(wù):oauth3-client-user-server,微服務(wù)之二,接收到請(qǐng)求后會(huì)到認(rèn)證中心驗(yàn)證。
客戶端:例如 APP 端、web 端 等終端
上圖描述了使用了 OAuth3 的客戶端與微服務(wù)間的請(qǐng)求過(guò)程。大致的過(guò)程就是客戶端用用戶名和密碼到認(rèn)證服務(wù)端換取 token,返回給客戶端,客戶端拿著 token 去各個(gè)微服務(wù)請(qǐng)求數(shù)據(jù)接口,一般這個(gè) token 是放到 header 中的。當(dāng)微服務(wù)接到請(qǐng)求后,先要拿著 token 去認(rèn)證服務(wù)端檢查 token 的合法性,如果合法,再根據(jù)用戶所屬的角色及具有的權(quán)限動(dòng)態(tài)的返回?cái)?shù)據(jù)。
創(chuàng)建并配置認(rèn)證服務(wù)端
配置最多的就是認(rèn)證服務(wù)端,驗(yàn)證賬號(hào)、密碼,存儲(chǔ) token,檢查 token ,刷新 token 等都是認(rèn)證服務(wù)端的工作。
1、引入需要的 maven 包
org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-oauth3 org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-actuator
spring-cloud-starter-oauth3
包含了 spring-cloud-starter-security
,所以不用再單獨(dú)引入了。之所以引入 redis 包,是因?yàn)橄旅鏁?huì)介紹一種用 redis 存儲(chǔ) token 的方式。
2、配置好 application.yml
將項(xiàng)目基本配置設(shè)置好,并加入有關(guān) redis 的配置,稍后會(huì)用到。
spring: application: name: auth-server redis: database: 2 host: localhost port: 32768 password: 1qaz@WSX jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 100ms server: port: 6001 management: endpoint: health: enabled: true
3、spring security 基礎(chǔ)配置
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 允許匿名訪問(wèn)所有接口 主要是 oauth 接口 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/**").permitAll(); } }
使用@EnableWebSecurity
注解修飾,并繼承自WebSecurityConfigurerAdapter
類(lèi)。
這個(gè)類(lèi)的重點(diǎn)就是聲明 PasswordEncoder
和 AuthenticationManager
兩個(gè) Bean。稍后會(huì)用到。其中 BCryptPasswordEncoder
是一個(gè)密碼加密工具類(lèi),它可以實(shí)現(xiàn)不可逆的加密,AuthenticationManager
是為了實(shí)現(xiàn) OAuth3 的 password 模式必須要指定的授權(quán)管理 Bean。
4、實(shí)現(xiàn) UserDetailsService
如果你之前用過(guò) Security 的話,那肯定對(duì)這個(gè)類(lèi)很熟悉,它是實(shí)現(xiàn)用戶身份驗(yàn)證的一種方式,也是最簡(jiǎn)單方便的一種。另外還有結(jié)合 AuthenticationProvider
的方式,有機(jī)會(huì)講 Security 的時(shí)候再展開(kāi)來(lái)講吧。
UserDetailsService
的核心就是 loadUserByUsername
方法,它要接收一個(gè)字符串參數(shù),也就是傳過(guò)來(lái)的用戶名,返回一個(gè) UserDetails
對(duì)象。
@Slf4j @Component(value = "kiteUserDetailsService") public class KiteUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { log.info("usernameis:" + username); // 查詢數(shù)據(jù)庫(kù)操作 if(!username.equals("admin")){ throw new UsernameNotFoundException("the user is not found"); }else{ // 用戶角色也應(yīng)在數(shù)據(jù)庫(kù)中獲取 String role = "ROLE_ADMIN"; Listauthorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(role)); // 線上環(huán)境應(yīng)該通過(guò)用戶名查詢數(shù)據(jù)庫(kù)獲取加密后的密碼 String password = passwordEncoder.encode("123456"); return new org.springframework.security.core.userdetails.User(username,password, authorities); } } }
這里為了做演示,把用戶名、密碼和所屬角色都寫(xiě)在代碼里了,正式環(huán)境中,這里應(yīng)該是從數(shù)據(jù)庫(kù)或者其他地方根據(jù)用戶名將加密后的密碼及所屬角色查出來(lái)的。賬號(hào) admin ,密碼 123456,稍后在換取 token 的時(shí)候會(huì)用到。并且給這個(gè)用戶設(shè)置 "ROLE_ADMIN" 角色。
5、OAuth3 配置文件
創(chuàng)建一個(gè)配置文件繼承自 AuthorizationServerConfigurerAdapter
.
@Configuration @EnableAuthorizationServer public class OAuth3Config extends AuthorizationServerConfigurerAdapter { @Autowired public PasswordEncoder passwordEncoder; @Autowired public UserDetailsService kiteUserDetailsService; @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenStore redisTokenStore; @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception { /** * redis token 方式 */ endpoints.authenticationManager(authenticationManager) .userDetailsService(kiteUserDetailsService) .tokenStore(redisTokenStore); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("order-client") .secret(passwordEncoder.encode("order-secret-8888")) .authorizedGrantTypes("refresh_token", "authorization_code", "password") .accessTokenValiditySeconds(3600) .scopes("all") .and() .withClient("user-client") .secret(passwordEncoder.encode("user-secret-8888")) .authorizedGrantTypes("refresh_token", "authorization_code", "password") .accessTokenValiditySeconds(3600) .scopes("all"); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); security.checkTokenAccess("isAuthenticated()"); security.tokenKeyAccess("isAuthenticated()"); } }
有三個(gè) configure 方法的重寫(xiě)。
AuthorizationServerEndpointsConfigurer
參數(shù)的重寫(xiě)
endpoints.authenticationManager(authenticationManager) .userDetailsService(kiteUserDetailsService) .tokenStore(redisTokenStore);
authenticationManage()
調(diào)用此方法才能支持 password 模式。
userDetailsService()
設(shè)置用戶驗(yàn)證服務(wù)。
tokenStore()
指定 token 的存儲(chǔ)方式。
redisTokenStore Bean 的定義如下:
@Configuration public class RedisTokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore (){ return new RedisTokenStore(redisConnectionFactory); } }
ClientDetailsServiceConfigurer
參數(shù)的重寫(xiě),在這里定義各個(gè)端的約束條件。包括
ClientId、Client-Secret:這兩個(gè)參數(shù)對(duì)應(yīng)請(qǐng)求端定義的 cleint-id 和 client-secret
authorizedGrantTypes 可以包括如下幾種設(shè)置中的一種或多種:
authorization_code:授權(quán)碼類(lèi)型。
implicit:隱式授權(quán)類(lèi)型。
password:資源所有者(即用戶)密碼類(lèi)型。
client_credentials:客戶端憑據(jù)(客戶端ID以及Key)類(lèi)型。
refresh_token:通過(guò)以上授權(quán)獲得的刷新令牌來(lái)獲取新的令牌。
accessTokenValiditySeconds:token 的有效期
scopes:用來(lái)限制客戶端訪問(wèn)的權(quán)限,在換取的 token 的時(shí)候會(huì)帶上 scope 參數(shù),只有在 scopes 定義內(nèi)的,才可以正常換取 token。
上面代碼中是使用 inMemory 方式存儲(chǔ)的,將配置保存到內(nèi)存中,相當(dāng)于硬編碼了。正式環(huán)境下的做法是持久化到數(shù)據(jù)庫(kù)中,比如 MySQL 中。
具體的做法如下:
在數(shù)據(jù)庫(kù)中增加表,并插入數(shù)據(jù)
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) ); INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('user-client', '$2a$10$o2l5kA7z.Caekp72h6kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true); INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
注意: client_secret 字段不能直接是 secret 的原始值,需要經(jīng)過(guò)加密。因?yàn)槭怯玫?BCryptPasswordEncoder
,所以最終插入的值應(yīng)該是經(jīng)過(guò) BCryptPasswordEncoder.encode()
之后的值。
然后在配置文件 application.yml 中添加關(guān)于數(shù)據(jù)庫(kù)的配置
spring: datasource: url: jdbc:mysql://localhost:3306/spring_cloud?characterEncoding=UTF-8&useSSL=false username: root password: password hikari: connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 maximum-pool-size: 9
Spring Boot 2.0 之后默認(rèn)使用 hikari 作為數(shù)據(jù)庫(kù)連接池。如果使用其他連接池需要引入相關(guān)包,然后對(duì)應(yīng)的增加配置。
在 OAuth3 配置類(lèi)(OAuth3Config)中增加 DataSource 的注入
@Autowired private DataSource dataSource;
將 public void configure(ClientDetailsServiceConfigurer clients)
重寫(xiě)方法修改為如下:
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource); jcsb.passwordEncoder(passwordEncoder); }
還有一個(gè)重寫(xiě)的方法 public void configure(AuthorizationServerSecurityConfigurer security)
,這個(gè)方法限制客戶端訪問(wèn)認(rèn)證接口的權(quán)限。
security.allowFormAuthenticationForClients(); security.checkTokenAccess("isAuthenticated()"); security.tokenKeyAccess("isAuthenticated()");
第一行代碼是允許客戶端訪問(wèn) OAuth3 授權(quán)接口,否則請(qǐng)求 token 會(huì)返回 401。
第二行和第三行分別是允許已授權(quán)用戶訪問(wèn) checkToken 接口和獲取 token 接口。
完成之后,啟動(dòng)項(xiàng)目,如果你用的是 IDEA 會(huì)在下方的 Mapping 窗口中看到 oauth3 相關(guān)的 RESTful 接口。
主要有如下幾個(gè):
POST /oauth/authorize 授權(quán)碼模式認(rèn)證授權(quán)接口
GET/POST /oauth/token 獲取 token 的接口
POST /oauth/check_token 檢查 token 合法性接口
上面創(chuàng)建完成了認(rèn)證服務(wù)端,下面開(kāi)始創(chuàng)建一個(gè)客戶端,對(duì)應(yīng)到我們系統(tǒng)中的業(yè)務(wù)相關(guān)的微服務(wù)。我們假設(shè)這個(gè)微服務(wù)項(xiàng)目是管理用戶相關(guān)數(shù)據(jù)的,所以叫做用戶客戶端。
1、引用相關(guān)的 maven 包
org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-oauth3 org.springframework.boot spring-boot-starter-data-redis
2、application.yml 配置文件
spring: application: name: client-user redis: database: 2 host: localhost port: 32768 password: 1qaz@WSX jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 timeout: 100ms server: port: 6101 servlet: context-path: /client-user security: oauth3: client: client-id: user-client client-secret: user-secret-8888 user-authorization-uri: http://localhost:6001/oauth/authorize access-token-uri: http://localhost:6001/oauth/token resource: id: user-client user-info-uri: user-info authorization: check-token-access: http://localhost:6001/oauth/check_token
上面是常規(guī)配置信息以及 redis 配置,重點(diǎn)是下面的 security 的配置,這里的配置稍有不注意就會(huì)出現(xiàn) 401 或者其他問(wèn)題。
client-id、client-secret 要和認(rèn)證服務(wù)中的配置一致,如果是使用 inMemory 還是 jdbc 方式。
user-authorization-uri 是授權(quán)碼認(rèn)證方式需要的,下一篇文章再說(shuō)。
access-token-uri 是密碼模式需要用到的獲取 token 的接口。
authorization.check-token-access 也是關(guān)鍵信息,當(dāng)此服務(wù)端接收到來(lái)自客戶端端的請(qǐng)求后,需要拿著請(qǐng)求中的 token 到認(rèn)證服務(wù)端做 token 驗(yàn)證,就是請(qǐng)求的這個(gè)接口
3、資源配置文件
在 OAuth3 的概念里,所有的接口都被稱(chēng)為資源,接口的權(quán)限也就是資源的權(quán)限,所以 Spring Security OAuth3 中提供了關(guān)于資源的注解 @EnableResourceServer
,和 @EnableWebSecurity
的作用類(lèi)似。
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Value("${security.oauth3.client.client-id}") private String clientId; @Value("${security.oauth3.client.client-secret}") private String secret; @Value("${security.oauth3.authorization.check-token-access}") private String checkTokenEndpointUrl; @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore (){ return new RedisTokenStore(redisConnectionFactory); } @Bean public RemoteTokenServices tokenService() { RemoteTokenServices tokenService = new RemoteTokenServices(); tokenService.setClientId(clientId); tokenService.setClientSecret(secret); tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl); return tokenService; } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenServices(tokenService()); } }
因?yàn)槭褂玫氖?redis 作為 token 的存儲(chǔ),所以需要特殊配置一下叫做 tokenService 的 Bean,通過(guò)這個(gè) Bean 才能實(shí)現(xiàn) token 的驗(yàn)證。
4、最后,添加一個(gè) RESTful 接口
@Slf4j @RestController public class UserController { @GetMapping(value = "get") //@PreAuthorize("hasAuthority('ROLE_ADMIN')") @PreAuthorize("hasAnyRole('ROLE_ADMIN')") public Object get(Authentication authentication){ //Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); authentication.getCredentials(); OAuth3AuthenticationDetails details = (OAuth3AuthenticationDetails)authentication.getDetails(); String token = details.getTokenValue(); return token; } }
一個(gè) RESTful 方法,只有當(dāng)訪問(wèn)用戶具有 ROLE_ADMIN 權(quán)限時(shí)才能訪問(wèn),否則返回 401 未授權(quán)。
通過(guò) Authentication 參數(shù)或者 SecurityContextHolder.getContext().getAuthentication()
可以拿到授權(quán)信息進(jìn)行查看。
測(cè)試認(rèn)證功能
1、啟動(dòng)認(rèn)證服務(wù)端,啟動(dòng)端口為 6001
2、啟動(dòng)用戶服務(wù)客戶端,啟動(dòng)端口為6101
3、請(qǐng)求認(rèn)證服務(wù)端獲取 token
我是用 REST Client 來(lái)做訪問(wèn)請(qǐng)求的,請(qǐng)求格式如下:
POST http://localhost:6001/oauth/token?grant_type=password&username=admin&password=123456&scope=all Accept: */* Cache-Control: no-cache Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
假設(shè)咱們?cè)谝粋€(gè) web 端使用,grant_type 是 password,表明這是使用 OAuth3 的密碼模式。
username=admin 和 password=123456 就相當(dāng)于在 web 端登錄界面輸入的用戶名和密碼,我們?cè)谡J(rèn)證服務(wù)端配置中固定了用戶名是 admin 、密碼是 123456,而線上環(huán)境中則應(yīng)該通過(guò)查詢數(shù)據(jù)庫(kù)獲取。
scope=all 是權(quán)限有關(guān)的,在認(rèn)證服務(wù)的 OAuthConfig 中指定了 scope 為 all 。
Authorization 要加在請(qǐng)求頭中,格式為 Basic 空格 base64(clientId:clientSecret),這個(gè)微服務(wù)客戶端的 client-id 是 user-client,client-secret 是 user-secret-8888,將這兩個(gè)值通過(guò)冒號(hào)連接,并使用 base64 編碼(user-client:user-secret-8888)之后的值為 dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==,可以通過(guò) https://www.sojson.com/base64.html 在線編碼獲取。
運(yùn)行請(qǐng)求后,如果參數(shù)都正確的話,獲取到的返回內(nèi)容如下,是一段 json 格式
{ "access_token": "9f958300-5005-46ea-9061-323c9e6c7a4d", "token_type": "bearer", "refresh_token": "0f5871f5-98f1-405e-848e-80f641bab72e", "expires_in": 3599, "scope": "all" }
access_token : 就是之后請(qǐng)求需要帶上的 token,也是本次請(qǐng)求的主要目的
token_type:為 bearer,這是 access token 最常用的一種形式
refresh_token:之后可以用這個(gè)值來(lái)?yè)Q取新的 token,而不用輸入賬號(hào)密碼
expires_in:token 的過(guò)期時(shí)間(秒)
4、用獲取到的 token 請(qǐng)求資源接口
我們?cè)谟脩艨蛻舳酥卸x了一個(gè)接口 http://localhost:6101/client-user/get,現(xiàn)在就拿著上一步獲取的 token 來(lái)請(qǐng)求這個(gè)接口。
GET http://localhost:6101/client-user/get Accept: */* Cache-Control: no-cache Authorization: bearer ce334918-e666-455a-8ecd-8bd680415d84
同樣需要請(qǐng)求頭 Authorization,格式為 bearer + 空格 + token,正常情況下根據(jù)接口的邏輯,會(huì)把 token 原樣返回。
5、token 過(guò)期后,用 refresh_token 換取 access_token
一般都會(huì)設(shè)置 access_token 的過(guò)期時(shí)間小于 refresh_token 的過(guò)期時(shí)間,以便在 access_token 過(guò)期后,不用用戶再次登錄的情況下,獲取新的 access_token。
### 換取 access_token POST http://localhost:6001/oauth/token?grant_type=refresh_token&refresh_token=706dac10-d48e-4795-8379-efe8307a2282 Accept: */* Cache-Control: no-cache Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
grant_type 設(shè)置為 refresh_token。
refresh_token 設(shè)置為請(qǐng)求 token 時(shí)返回的 refresh_token 的值。
請(qǐng)求頭加入 Authorization,格式依然是 Basic + 空格 + base64(client-id:client-secret)
請(qǐng)求成功后會(huì)返回和請(qǐng)求 token 同樣的數(shù)據(jù)格式。
用 JWT 替換 redisToken
上面 token 的存儲(chǔ)用的是 redis 的方案,Spring Security OAuth3 還提供了 jdbc 和 jwt 的支持,jdbc 的暫不考慮,現(xiàn)在來(lái)介紹用 JWT 的方式來(lái)實(shí)現(xiàn) token 的存儲(chǔ)。
用 JWT 的方式就不用把 token 再存儲(chǔ)到服務(wù)端了,JWT 有自己特殊的加密方式,可以有效的防止數(shù)據(jù)被篡改,只要不把用戶密碼等關(guān)鍵信息放到 JWT 里就可以保證安全性。
認(rèn)證服務(wù)端改造
先把有關(guān) redis 的配置去掉。
添加 JwtConfig 配置類(lèi)
@Configuration public class JwtTokenConfig { @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev"); return accessTokenConverter; } }
JwtAccessTokenConverter
是為了做 JWT 數(shù)據(jù)轉(zhuǎn)換,這樣做是因?yàn)?JWT 有自身獨(dú)特的數(shù)據(jù)格式。如果沒(méi)有了解過(guò) JWT ,可以搜索一下先了解一下。
更改 OAuthConfig 配置類(lèi)
@Autowired private TokenStore jwtTokenStore; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception { /** * 普通 jwt 模式 */ endpoints.tokenStore(jwtTokenStore) .accessTokenConverter(jwtAccessTokenConverter) .userDetailsService(kiteUserDetailsService) /** * 支持 password 模式 */ .authenticationManager(authenticationManager); }
注入 JWT 相關(guān)的 Bean,然后修改 configure(final AuthorizationServerEndpointsConfigurer endpoints)
方法為 JWT 存儲(chǔ)模式。
改造用戶客戶端
修改 application.yml 配置文件
security: oauth3: client: client-id: user-client client-secret: user-secret-8888 user-authorization-uri: http://localhost:6001/oauth/authorize access-token-uri: http://localhost:6001/oauth/token resource: jwt: key-uri: http://localhost:6001/oauth/token_key key-value: dev
注意認(rèn)證服務(wù)端 JwtAccessTokenConverter
設(shè)置的 SigningKey 要和配置文件中的 key-value 相同,不然會(huì)導(dǎo)致無(wú)法正常解碼 JWT ,導(dǎo)致驗(yàn)證不通過(guò)。
ResourceServerConfig 類(lèi)的配置
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev"); accessTokenConverter.setVerifierKey("dev"); return accessTokenConverter; } @Autowired private TokenStore jwtTokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenStore(jwtTokenStore); } }
運(yùn)行請(qǐng)求 token 接口的請(qǐng)求
POST http://localhost:6001/oauth/token?grant_type=password&username=admin&password=123456&scope=all Accept: */* Cache-Control: no-cache Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
返回結(jié)果如下:
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzE3NDM0OTQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.0Ik3UwB1xjX2le5luEdtVAI_MEyu_OloRRYtPOvtvwM", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJleHAiOjE1NzE3NzU4OTQsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiZjdkMjg4NDUtMmU2ZC00ZmRjLTg1OGYtMWNiY2RlNzI1ZmMyIiwiY2xpZW50X2lkIjoidXNlci1jbGllbnQifQ.vk_msYtbrAr93h6sK4wy6EC2_wRD_cD_UBS8O6eRziw", "expires_in": 3599, "scope": "all", "jti": "8cca29af-ea77-4fe6-9fe1-327415dcd21d" }
我們已經(jīng)看到返回的 token 是 JWT 格式了,到 JWT 在線解碼網(wǎng)站 https://jwt.io/ 或者 http://jwt.calebb.net/將 token 解碼看一下
看到了沒(méi),user_name、client_id 等信息都在其中。
拿著返回的 token 請(qǐng)求用戶客戶端接口
GET http://localhost:6101/client-user/get Accept: */* Cache-Control: no-cache Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzE3NDM0OTQsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI4Y2NhMjlhZi1lYTc3LTRmZTYtOWZlMS0zMjc0MTVkY2QyMWQiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.0Ik3UwB1xjX2le5luEdtVAI_MEyu_OloRRYtPOvtvwM
增強(qiáng) JWT
如果我想在 JWT 中加入額外的字段(比方說(shuō)用戶的其他信息)怎么辦呢,當(dāng)然可以。spring security oauth3 提供了 TokenEnhancer
增強(qiáng)器。其實(shí)不光 JWT ,RedisToken 的方式同樣可以。
聲明一個(gè)增強(qiáng)器
public class JWTokenEnhancer implements TokenEnhancer { @Override public OAuth3AccessToken enhance(OAuth3AccessToken oAuth3AccessToken, OAuth3Authentication oAuth3Authentication) { Mapinfo = new HashMap<>(); info.put("jwt-ext", "JWT 擴(kuò)展信息"); ((DefaultOAuth3AccessToken) oAuth3AccessToken).setAdditionalInformation(info); return oAuth3AccessToken; } }
通過(guò) oAuth3Authentication 可以拿到用戶名等信息,通過(guò)這些我們可以在這里查詢數(shù)據(jù)庫(kù)或者緩存獲取更多的信息,而這些信息都可以作為 JWT 擴(kuò)展信息加入其中。
OAuthConfig 配置類(lèi)修改
注入增強(qiáng)器
@Autowired private TokenEnhancer jwtTokenEnhancer; @Bean public TokenEnhancer jwtTokenEnhancer(){ return new JWTokenEnhancer(); }
修改 configure(final AuthorizationServerEndpointsConfigurer endpoints)
方法
@Override public void configure( final AuthorizationServerEndpointsConfigurer endpoints ) throws Exception{ /** * jwt 增強(qiáng)模式 */ TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); ListenhancerList = new ArrayList<>(); enhancerList.add( jwtTokenEnhancer ); enhancerList.add( jwtAccessTokenConverter ); enhancerChain.setTokenEnhancers( enhancerList ); endpoints.tokenStore( jwtTokenStore ) .userDetailsService( kiteUserDetailsService ) /** * 支持 password 模式 */ .authenticationManager( authenticationManager ) .tokenEnhancer( enhancerChain ) .accessTokenConverter( jwtAccessTokenConverter ); }
再次請(qǐng)求 token ,返回內(nèi)容中多了個(gè)剛剛加入的 jwt-ext 字段
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h6oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h6oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImE0NTUxZDllLWI3ZWQtNDc1OS1iMmYxLWYwYjliMjFjYTQyYyIsImV4cCI6MTU3MTc3NzU3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJmNTI3ODJlOS0wOGRjLTQ2NGUtYmJhYy03OTMwNzYwYmZiZjciLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.UQMf140CG8U0eWh08nGlctpIye9iJ7p2i6NYHkGAwhY", "expires_in": 3599, "scope": "all", "jwt-ext": "JWT 擴(kuò)展信息", "jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c" }
用戶客戶端解析 JWT 數(shù)據(jù)
我們?nèi)绻?JWT 中加入了額外信息,這些信息我們可能會(huì)用到,而在接收到 JWT 格式的 token 之后,用戶客戶端要把 JWT 解析出來(lái)。
引入 JWT 包
io.jsonwebtoken jjwt 0.9.1
加一個(gè) RESTful 接口,在其中解析 JWT
@GetMapping(value = "jwt") @PreAuthorize("hasAnyRole('ROLE_ADMIN')") public Object jwtParser(Authentication authentication){ authentication.getCredentials(); OAuth3AuthenticationDetails details = (OAuth3AuthenticationDetails)authentication.getDetails(); String jwtToken = details.getTokenValue(); Claims claims = Jwts.parser() .setSigningKey("dev".getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(jwtToken) .getBody(); return claims; }
同樣注意其中簽名的設(shè)置要與認(rèn)證服務(wù)端相同。
用上一步的 token 請(qǐng)求上面的接口
### 解析 jwt GET http://localhost:6101/client-user/jwt Accept: */* Cache-Control: no-cache Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h6oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MTc0NTE3OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhNDU1MWQ5ZS1iN2VkLTQ3NTktYjJmMS1mMGI5YjIxY2E0MmMiLCJjbGllbnRfaWQiOiJ1c2VyLWNsaWVudCJ9.5j4hNsVpktG2iKxNqR-q1rfcnhlyV3M6HUBx5cd6PiQ
返回內(nèi)容如下:
{ "user_name": "admin", "jwt-ext": "JWT 擴(kuò)展信息", "scope": [ "all" ], "exp": 1571745178, "authorities": [ "ROLE_ADMIN" ], "jti": "a4551d9e-b7ed-4759-b2f1-f0b9b21ca42c", "client_id": "user-client" }
以上是“Spring Cloud OAuth2如何實(shí)現(xiàn)用戶認(rèn)證及單點(diǎn)登錄”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道!