筆者最近花了點時間研究Oauth2原理,基於spring寫了一些demo用於實踐探索,深深體會到
Spring Security Oauth2
框架的便利,框架幫我們封裝太多了底層實現,所以使用起來非常方便,除了可以爲第三方應用提供授權,也可以作爲應用的登錄框架。
文章基於Spring Security Oauth2.0
授權碼模式實踐,採用SpringBoot
搭建Demo講述基本使用過程。
概述
簡單來說Oauth2是一個授權協議,用於爲第三方訪問服務提供特定的授權流程。
它的最終目的是爲第三方應用頒發一個有時效性的令牌 token。使得第三方應用能夠通過該令牌獲取相關的資源。常見的場景就是:第三方登錄。當你想要登錄某個論壇,但沒有賬號,而這個論壇接入瞭如 QQ、Facebook 等登錄功能,在你使用 QQ 登錄的過程中就使用的OAuth 2.0
協議。
在進入正式實踐章節之前,有必要先了解下Oauth2的關鍵知識點。關於Oauth2協議更多詳細內容,可以參考10 分鐘理解什麼是 OAuth 2.0 協議,是一個比較好的科普文章。
Oauth2.0
協議中的幾個角色
resource owner
資源所有者,能夠允許訪問受保護資源的實體。如果是個人,被稱爲end-user
。resource server
資源服務器,受保護資源的服務器,即訪問該服務器需要獲取授權或許可。client
客戶端,使用資源所有者的授權代表資源所有者發起對受保護資源的請求的應用程序。如:web網站,移動應用等。authorization server
,授權服務器,能夠向客戶端client
頒發令牌。User-Agent
,用戶代理,幫助資源所有者與客戶端溝通的工具,一般爲 web 瀏覽器,移動 APP 等。
幾個角色間的認證步驟
+--------+ +---------------+ | |--(A)- 請求用戶授權許可---- > | Resource | | | | Owner | | |<-(B)-- 授權許可 ----------- | | | | +---------------+ | | | | +---------------+ | |--(C)-- 獲取token ----------> | Authorization | | Client | | Server | | |<-(D)----- 發放token ----------| | | | +---------------+ | | | | +---------------+ | |--(E)----- token訪問資源 ------>| Resource | | | | Server | | |<-(F)--- 返回受保護的資源 ------| | +--------+ +---------------+
Spring Security Oauth2.0
幾個端點的作用
啓動項目時會有這發佈這些端點,用於Oauth2
交互邏輯:
- 根據用戶認證獲得授權碼code
AuthorizationEndpoint
{[/oauth/authorize]}
{[/oauth/authorize],methods=[POST]
- 客戶端根據授權碼code獲取令牌token
TokenEndpoint
{[/oauth/token],methods=[GET]}
{[/oauth/token],methods=[POST]}
- 可以用於遠程解碼令牌
CheckTokenEndpoint
{[/oauth/check_token]}
- 顯示授權服務器的確認頁
WhitelabelApprovalEndpoint
{[/oauth/confirm_access]}
- 顯示授權服務器的錯誤頁
WhitelabelErrorEndpoint
{[/oauth/error]}
Oauth2.0
四種授權模式
- 授權碼模式(authorization code)
最常用,可用於第三方應用授權、SSO登錄實現等。 - 簡化模式(implicit)
一般不用 - 密碼模式(resource owner password credentials)
- 客戶端模式(client credentials)
一般用於後臺服務接口間的認證
實踐
搭建Oauth2
應用,主要分爲4個步驟:搭建認證/授權服務器、搭建資源服務器、配置SpringSercurity、客戶端服務器。
以github
作爲授權服務器舉例說明授權碼模式的流程:
- 用戶訪問第三方應用
A-Server
,未登錄,A-Server
提供github
認證登錄; - 用戶點擊
github
認證,跳轉到github
; - 如用戶在
github
未登錄,跳轉到github
登錄頁,用戶輸入賬戶密碼即可完成登錄,並跳轉到原來的第三方應用A-Server
並得到頒佈的token; - 如用戶已在
github
登錄,則跳轉到github
確認授權頁,用戶點擊授權後即可跳轉到原來的第三方應用A-Server
並得到頒佈的token
項目準備
- 建立項目
oauth-server
,引入依賴
<?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">
<parent>
<artifactId>parent</artifactId>
<groupId>com.oauth.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>oauth-server</artifactId>
<!-- security stater -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.5.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>1.5.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>1.5.10.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- 數據庫 mybatis 連接池等相關配置 -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.3.RELEASE</version>
</dependency>
</dependencies>
</project>
- 項目參數配置
server:
port: 9011
context-path: /auth
session:
cookie:
name: OAUTH2SESSIONID
spring:
application:
name: auth-server
# 數據庫配置
datasource:
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
filters: stat,wall,log4j
initialSize: 5
minIdle: 5
maxActive: 20
maxWaite: 60000
validationQuery: SELECT 1 FROM DUA
url: jdbc:mysql://localhost:3306/oauth?useSSL=false&useUnicode=true&characterEncoding=utf-8
username: root
password: admin
redis:
url: localhost
port: 20806
timeout: 20000
password: test|test
mapper:
mappers: com.oauth2.demo.mapper.BaseMapper
not-empty: true
identity: MYSQL
logging:
level:
root: debug
Oauth2服務端配置
數據庫結構
文中demo基於mysql數據庫存儲Oauth2相關數據。也可以採用內存簡單配置,但實際生產使用中需要用mysql等數據庫進行持久化配置。
CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) NOT NULL,
`client_secret` varchar(256) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`scope` varchar(1024) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(2048) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='oauth2客戶端信息'
-- 添加一個客戶端
insert into `oauth_client_details` (`client_id`, `client_secret`, `resource_ids`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) values('B5CDC04D8D8D419DA406364168F276A2','E2DA5B2DEDD548AEB7ABA689436C2E2C','','userProfile','authorization_code,client_credentials,password','http://localhost:9101/client1/login,http://www.baidu.com','','43200','2592000','','');
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='oauth2訪問令牌'
CREATE TABLE `oauth_code` (
`code` varchar(256) DEFAULT NULL,
`authentication` blob,
`create_ts` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='oauth2授權碼'
CREATE TABLE `oauth_approvals` (
`userId` varchar(256) DEFAULT NULL,
`clientId` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` datetime DEFAULT NULL,
`lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='oauth2已授權客戶端'
授權服務器
用於實現Oauth2認證,關鍵配置@EnableAuthorizationServer
表示開啓Oauth2
認證服務器
@Configuration
@EnableAuthorizationServer
public class AuthServerConfiguration extends AuthorizationServerConfigurerAdapter{
@Autowired
private OauthClientDetailsServiceImpl oauthClientDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailServiceImpl userDetailService;
@Autowired
private DruidDataSource dataSource;
@Autowired
private JedisConnectionFactory jedisConnectionFactory;
/**
* 已授權客戶端存儲記錄
* @return
*/
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
/**
* access_token 採用redis緩存,Redis連接池採用Jedis框架
* 當然也可以採用Jdbc實現用mysql存儲
* @return
*/
@Bean
public RedisTokenStore tokenStore() {
return new RedisTokenStore(jedisConnectionFactory);
}
/**
* 授權碼code的存儲-mysql中
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
JdbcAuthorizationCodeServices service = new JdbcAuthorizationCodeServices(dataSource);
return service;
}
// 配置Oauth2客戶端,存儲到數據庫中
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(oauthClientDetailsService);
}
/**
* 配置oauth2端點信息
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.authenticationManager(authenticationManager)
.approvalStore(approvalStore())
.userDetailsService(userDetailService)
.tokenStore(tokenStore())
.authorizationCodeServices(authorizationCodeServices());
// 自定義確認授權頁面
endpoints.pathMapping("/oauth/confirm_access", "/oauth/confirm_access");
// 自定義錯誤頁
endpoints.pathMapping("/oauth/error", "/oauth/error");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients(); //主要是讓/oauth/token支持client_id以及client_secret作登錄認證
}
}
配置SpingSecurity
用於保護oauth相關的endpoints,主要作用於用戶的登錄(form login,Basic auth),主要是通過一系列過濾鏈實現登錄相關
/**
* Web服務配置類
*/
@Slf4j
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailServiceImpl userDetailsService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/**
* 不定義沒有password grant_type模式
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置用戶賬密的處理類 userDetailsService提供登錄用戶的賬密信息供springsecurity框架校驗
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers() //所有端點配置
.antMatchers("/oauth/**", "/login") // 匹配一個數據 ant路徑格式
.and()
.authorizeRequests() // url權限配置
.antMatchers("/login").permitAll() // 表示登錄表單頁面不攔截
.antMatchers("/oauth/**").authenticated() // 保護url,需要用戶登錄
.and()
.formLogin().permitAll() //沒有自定義loginpage 則不要寫上loginPage("/xxxx") 否則404
.and()
.logout().permitAll()
// /logout退出清除cookie
.addLogoutHandler(new CookieClearingLogoutHandler("token", "remember-me"))
.and()
.csrf().disable()
// 禁用httpBasic
.httpBasic().disable();
}
}
資源服務器
配置受保護的資源,用於保護oauth受限資源,主要作用於client端以及token的認證,security配置WebSecurityConfigurerAdapter
與資源服務配置ResourceServerConfigurerAdapter
比較像,它們的區別會在本文末講到。
@Slf4j
@Configuration
@EnableResourceServer
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/userInfo")
.and()
.authorizeRequests()
.antMatchers("/userInfo").authenticated(); //受保護資源url: /userInfo 需要認證
}
}
用戶信息接口(受保護資源)
@Slf4j
@Controller
public class HomeController {
/**
* 提供通過access_token獲取用戶信息的問題
* @return
*/
@RequestMapping("userInfo")
@ResponseBody
public Object userInfo(Principal principal) {
return principal
}
}
其它配置
通用配置
@Configuration
public class CommonConfiguration {
/**
* 賬戶密碼加密類
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Redis連接池配置:這裏採用的是spring-data-redis自定義配置,不需要依賴
starter
@Configuration
public class JedisPoolConfiguration {
@Value("${redis.url}")
private String redisUrl;
@Value("${redis.port}")
private int redisPort;
@Value("${redis.timeout}")
private int redisTimeout;
@Value("${redis.password}")
private String redisPasswd;
@Bean
public JedisConnectionFactory connectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setPort(redisPort);
jedisConnectionFactory.setHostName(redisUrl);
jedisConnectionFactory.setPassword(redisPasswd);
return jedisConnectionFactory;
}
}
用戶賬密處理類:文章簡單處理,允許所有的賬號用密碼111111登錄。其中 UserInfoEntity 繼承 UserDetails 類。
@Configuration
public class UserDetailServiceImpl implements UserDetailsService{
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
UserInfoEntity userInfoEntity = new UserInfoEntity();
userInfoEntity.setUsername(s);
userInfoEntity.setPassword(passwordEncoder.encode("111111"));
return userInfoEntity;
}
}
Oauth2客戶端業務類:文章採用
mybatis
框架實現ORM
@Slf4j
@Configuration
public class OauthClientDetailsServiceImpl implements ClientDetailsService{
@Autowired
private OauthClientDetailsDao oauthClientDetailsDao;
/**
* 根據id client_id獲取客戶端信息
* @param s
* @return
* @throws ClientRegistrationException
*/
public ClientDetails loadClientByClientId(String s) throws ClientRegistrationException {
// 根據Id查詢
OauthClientDetails oauthClientDetails = oauthClientDetailsDao.selectByPrimaryKey(s);
try {
return translateClient(oauthClientDetails);
} catch (Exception e) {
log.error("===> {}", e);
throw new ClientRegistrationException("無效client");
}
}
public ClientDetails translateClient(OauthClientDetails details) {
BaseClientDetails clientDetails = new BaseClientDetails(details.getClientId(), details.getResourceIds(), details.getScope(),
details.getAuthorizedGrantTypes(), details.getAuthorities(), details.getWebServerRedirectUri());
clientDetails.setClientSecret(details.getClientSecret());
clientDetails.setScope(StringUtils.commaDelimitedListToSet(details.getScope()));
clientDetails.setAutoApproveScopes(new ArrayList<String>());
clientDetails.setRefreshTokenValiditySeconds(details.getRefreshTokenValidity());
clientDetails.setAccessTokenValiditySeconds(details.getAccessTokenValidity());
return clientDetails;
}
}
至此,Oauth2
+ 資源
就配置完成,下面進行測試。
測試
- 獲取code,跳轉到登錄頁
/login
,輸入賬號密碼admin/111111
,登錄後返回code
http://localhost:9011/auth/oauth/authorize?client_id=B5CDC04D8D8D419DA406364168F276A2&response_type=code&redirect_uri=http://localhost:9101/client1/login
登錄成功後獲取到的授權code爲9cB8ks
(生產環境一般是由localhost:9101/client1/login
前端將code傳給後端)
- 通過code獲取令牌token(POST請求)
http://localhost:9011/auth/oauth/token?client_id=B5CDC04D8D8D419DA406364168F276A2&client_secret=E2DA5B2DEDD548AEB7ABA689436C2E2C&grant_type=authorization_code&redirect_uri=http://localhost:9101/client1/login&code=9cB8ks
- 通過access_token訪問資源:獲取用戶信息
這樣就完成了一個基於spring security oauth2
授權碼模式的認證服務。
WebSecurityConfigurerAdapter與ResourceServerConfigurerAdapter
二者都有針對http security的配置,都可用於攔截驗證url甚至有相同的作用效果,但在功能及使用場景上有區別
- 過濾順序
WebSecurityConfigurerAdapter(order=100)
的攔截要低於@EnableResourceServer(order=3)
WebSecurityConfigurerAdapter
用於保護oauth相關的endpoints,同時主要作用於用戶的登錄(form login,Basic auth) - 使用場景
ResourceServerConfigurerAdapter
用於保護oauth要開放的資源,主要作用於client端以及token的認證(Bearer auth) - 兩者分工協作
因此WebSecurityConfigurerAdapter
需要攔截oauth
資源,不攔截保護資源;而被保護資源由ResourceServerConfigurerAdapter
攔截,配置需要acess_token
訪問的url
總結
文章簡單介紹Oauth2
協議的過程,spring sercurity oauth2
框架實現及最常用的授權碼模式的實踐。其它功能或實踐,比如密碼登錄模式、客戶端如何接入使用、sso統一登錄等,筆者後續有空會繼續更新實踐。