需求背景
本篇文章講解如何通過Springboot2集成驗證服務Token,以及資源服務的用法(更多官方關於 Oauth2)
概要
主要使用Spring Boot2和SpringSecurity5
OAuth2術語
- 資源擁有者 Resource Owner
- 用戶授權哪些應用程序,能夠去訪問資源信息等,訪問受限於作用域
- 資源服務 Resource Server
- 一個在客戶端擁有令牌後,處理驗證它請求的服務
- 客戶端 Client
- 一個代表資源擁有者的應用程序,可以訪問受保護的資源
- 授權服務 Authorization Server
- 一個在成功驗證了客戶端和資源擁有者,以及驗證請求後,分發令牌的授權服務
- 令牌 Access Token
- 一個用於訪問受限資源的唯一token
- 作用域範圍 Scope
- 一個許可權限
- 授權類型 Grant type
- 授權是指獲得令牌的方法(更多官方支持的Grant Type)
Oauth2 密碼授權流程
在oauth2協議中,一個應用會有自己的clientId和clientSecret(從認證方申請),由認證方下發clientId和secret
代碼演示
授權服務 Authorization Server
構建Authorization Server,這裏我使用了SpringSecurity5 和 SpringBoot 2.0.6版本
1. 項目目錄結構
2. pom.xml依賴組件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib-ext-spring</artifactId>
</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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
3. 這裏爲演示,使用了H2數據庫
以下是Spring Security需要用到的OAuth2 SQL
CREATE TABLE IF NOT EXISTS oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256) NOT NULL,
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(4000),
autoapprove VARCHAR(256)
);
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS oauth_access_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256),
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication BLOB,
refresh_token VARCHAR(256)
);
CREATE TABLE IF NOT EXISTS oauth_refresh_token (
token_id VARCHAR(256),
token BLOB,
authentication BLOB
);
CREATE TABLE IF NOT EXISTS oauth_code (
code VARCHAR(256), authentication BLOB
);
使用下面這句:
INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, authorities, access_token_validity)
VALUES ('clientId', '{bcrypt}$2a$10$vCXMWCn7fDZWOcLnIEhmK.74dvK1Eh8ae2WrWlhr2ETPLoxQctN4.', 'read,write', 'password,refresh_token,client_credentials', 'ROLE_CLIENT', 300);
特別要注意:這裏是下發clientId和secret,值分別爲clientId和secret(這兩個值後面要用到)
這裏的client_secret列值,由
bcrypt生成
而{bcrypt}前綴寫法,這是
Spring Security 5 DelegatingPasswordEncoder的新用法
access_token_validity列:表示token有效時間,單位秒,即300秒有效
同時,以下是User和Authority表(被org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl使用到)
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(256) NOT NULL,
password VARCHAR(256) NOT NULL,
enabled TINYINT(1),
UNIQUE KEY unique_username(username)
);
CREATE TABLE IF NOT EXISTS authorities (
username VARCHAR(256) NOT NULL,
authority VARCHAR(256) NOT NULL,
PRIMARY KEY(username, authority)
);
同樣,使用下面的sql,初始化user和authority
INSERT INTO users (id, username, password, enabled) VALUES (1, 'user', '{bcrypt}$2a$10$cyf5NfobcruKQ8XGjUJkEegr9ZWFqaea6vjpXWEaSqTa2xL9wjgQC', 1);
INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');
注意:用戶名是user,password的值是:pass,後面會用到
Spring Security Configuration 部分
4. WebSecurityConfiguration.java文件
package com.md.demo.oauth.opaque.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.sql.DataSource;
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final DataSource dataSource;
private PasswordEncoder passwordEncoder;
private UserDetailsService userDetailsService;
public WebSecurityConfiguration(final DataSource dataSource) {
this.dataSource = dataSource;
}
// 排除路徑驗證
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello");
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
if (passwordEncoder == null) {
passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
return passwordEncoder;
}
@Bean
@Override
public UserDetailsService userDetailsService() {
if (userDetailsService == null) {
userDetailsService = new JdbcDaoImpl();
((JdbcDaoImpl) userDetailsService).setDataSource(dataSource);
}
return userDetailsService;
}
}
一起使用@EnableWebSecurity註解和WebSecurityConfigurerAdapter,可以提供web基礎安全機制
5. 附加說明
如果你使用了springboot DataSource數據源對象,它會自動裝配,你不需要再自定義,只需要注入即可。它需要注入到由Spring Security提供的UserDetailsService中,結合使用JdbcDaoImpl。如果需要,你也能替換它去自定義實現。
6. 驗證服務配置
驗證服務驗證客戶端和用戶憑證,同時生成token
package com.md.demo.oauth.opaque.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final DataSource dataSource;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private TokenStore tokenStore;
public AuthorizationServerConfiguration(final DataSource dataSource, final PasswordEncoder passwordEncoder,
final AuthenticationManager authenticationManager) {
this.dataSource = dataSource;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
}
@Bean
public TokenStore tokenStore() {
if (tokenStore == null) {
tokenStore = new JdbcTokenStore(dataSource);
}
return tokenStore;
}
@Bean
public DefaultTokenServices tokenServices(final ClientDetailsService clientDetailsService) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore());
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setAuthenticationManager(authenticationManager);
return tokenServices;
}
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore());
}
@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.passwordEncoder(passwordEncoder)
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
}
7. 測試接口訪問
package com.md.demo.oauth.rest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@RequestMapping("/profile")
public class ProfileController {
//http://localhost:9001/profile/me?access_token=xxxxxx
@GetMapping("/me")
public ResponseEntity<Principal> get(final Principal principal) {
return ResponseEntity.ok(principal);
}
/*
* 通過以下接口Post請求獲得access_token:
* http://localhost:9001/oauth/token?grant_type=password&username=user&password=pass
*
* 注意:Authorization中使用BasicAuth,Username和Password分別爲:clientId,secret
*/
}
資源服務 Resource Server
1. 項目目錄結構
2. pom.xml依賴組件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib-ext-spring</artifactId>
</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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
3. 接口訪問(受保護)
package com.md.demo.oauth.rest;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@RequestMapping("/me")
public class UserController {
//http://localhost:9101/me?access_token=xxxxxx
@GetMapping
@PreAuthorize("hasRole('ROLE_USER')")
public ResponseEntity<Principal> get(final Principal principal) {
return ResponseEntity.ok(principal);
}
/*
* 通過以下接口Post請求獲得access_token:
* http://localhost:9001/oauth/token?grant_type=password&username=user&password=pass
*
* 注意:Authorization中使用BasicAuth,Username和Password分別爲:clientId,secret
*/
}
@PreAuthorize註解驗證用戶的權限去是否執行代碼,同時需要啓用prePost註解
4. Web安全配置
package com.md.demo.oauth.opaque.ds.config;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration {
}
prePostEnabled默認是false,使@PreAuthorize生效,則設爲true
5. 資源服務配置
package com.md.demo.oauth.opaque.ds.config;
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 ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private static final String ROOT_PATTERN = "/**";
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 排除路徑驗證
.antMatchers("/hello")
.permitAll()
.antMatchers(ROOT_PATTERN)
.authenticated();
}
}
6. application.yml配置
server:
port: 9101
# 設置url,把token轉換成authentication對象
security:
oauth2:
resource:
user-info-uri: http://localhost:9001/profile/me
spring:
jackson:
serialization:
INDENT_OUTPUT: true
測試訪問
1. 訪問受保護接口:http://localhost:9101/me,默認失敗
2. 獲得token接口:http://localhost:9001/oauth/token?grant_type=password&username=user&password=pass
3. 再訪問受保護接口:http://localhost:9101/me?access_token=8df31c53-e616-47d1-802e-9b00adf64266,即可訪問成功
完整源碼下載
- OAuth2 Server端對應的Github源碼地址
- OAuth2 Resource端對應的Github源碼地址
下一章教程
SpringBoot從入門到精通教程(二十三)- Oauth2+JWT集成/SpringSecurity
該系列教程
至此,全部介紹就結束了
------------------------------------------------------
------------------------------------------------------
關於我(個人域名)
期望和大家一起學習,一起成長,共勉,O(∩_∩)O謝謝
歡迎交流問題,可加個人QQ 469580884,
或者,加我的羣號 751925591,一起探討交流問題
不講虛的,只做實幹家
Talk is cheap,show me the code