Springboot2+SpringSecurity+Oauth2+Mysql數據庫實現持久化客戶端數據


目錄

 (一) 簡介

OAuth是一個關於授權的開放網絡標准,在全世界得到的廣泛的應用,目前是2.0的版本。OAuth2在“客戶端”與“服務提供商”之間,設置了一個授權層(authorization layer)。“客戶端”不能直接登錄“服務提供商”,只能登錄授權層,以此將用戶與客戶端分離。“客戶端”登錄需要OAuth提供的令牌,否則將提示認證失敗而導致客戶端無法訪問服務。OAuth2.0是OAuth協議的延續版本,但不向后兼容OAuth 1.0即完全廢止了OAuth1.0。

OAuth2為我們提供了四種授權方式:

1、授權碼模式(authorization code)
2、簡化模式(implicit)
3、密碼模式(resource owner password credentials)
4、客戶端模式(client credentials)

授權碼模式

授權碼相對其他三種來說是功能比較完整、流程最安全嚴謹的授權方式,通過客戶端的后台服務器與服務提供商的認證服務器交互來完成。流程如下圖所示:

簡化模式

 這種模式不通過服務器端程序來完成,直接由瀏覽器發送請求獲取令牌,令牌是完全暴露在瀏覽器中的,這種模式極力不推崇。流程如下圖所示:

 

密碼模式

 密碼模式也是比較常用到的一種,客戶端向授權服務器提供用戶名、密碼然后得到授權令牌。這種模式不過有種弊端,我們的客戶端需要存儲用戶輸入的密碼,但是對於用戶來說信任度不高的平台是不可能讓他們輸入密碼的。流程如下圖所示:

 

客戶端模式

 客戶端模式是客戶端以自己的名義去授權服務器申請授權令牌,並不是完全意義上的授權。如下圖所示:

上述簡單的介紹了OAuth2內部的四種授權方式,我們下面使用密碼模式來進行測試;我們就來講解下SpringBoot項目中是如何配置使用OAuth2服務器端,並且我們使用數據庫中的用戶數據來做驗證處理,並讓OAuth2整合SpringSecurity來保護我們的REST接口。

(二) 建表,初始化數據

Oauth2相關的5張表:

  • oauth_access_token:訪問令牌
  • oauth_refresh_token:更新令牌
  • oauth_client_details:客戶端信息
  • oauth_code:授權碼
  • oauth_approvals:授權記錄
  • oauth_client_token:  客戶端用來記錄token信息

只以密碼模式來進行測試,不考慮管理功能,只用到了了oauth_client_details,oauth_access_token,oauth_refresh_token 三張表

DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(255) DEFAULT NULL COMMENT '加密的access_token的值',
`token` longblob COMMENT 'OAuth2AccessToken.java對象序列化后的二進制數據',
`authentication_id` varchar(255) DEFAULT NULL COMMENT '加密過的username,client_id,scope',
`user_name` varchar(255) DEFAULT NULL COMMENT '登錄的用戶名',
`client_id` varchar(255) DEFAULT NULL COMMENT '客戶端ID',
`authentication` longblob COMMENT 'OAuth2Authentication.java對象序列化后的二進制數據',
`refresh_token` varchar(255) DEFAULT NULL COMMENT '加密的refresh_token的值'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 
         

DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userId` varchar(255) DEFAULT NULL COMMENT '登錄的用戶名',
`clientId` varchar(255) DEFAULT NULL COMMENT '客戶端ID',
`scope` varchar(255) DEFAULT NULL COMMENT '申請的權限范圍',
`status` varchar(10) DEFAULT NULL COMMENT '狀態(Approve或Deny)',
`expiresAt` datetime DEFAULT NULL COMMENT '過期時間',
`lastModifiedAt` datetime DEFAULT NULL COMMENT '最終修改時間'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 
         

DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL COMMENT '客戶端ID',
`resource_ids` varchar(255) DEFAULT NULL COMMENT '資源ID集合,多個資源時用逗號(,)分隔',
`client_secret` varchar(255) DEFAULT NULL COMMENT '客戶端密匙',
`scope` varchar(255) DEFAULT NULL COMMENT '客戶端申請的權限范圍',
`authorized_grant_types` varchar(255) DEFAULT NULL COMMENT '客戶端支持的grant_type',
`web_server_redirect_uri` varchar(255) DEFAULT NULL COMMENT '重定向URI',
`authorities` varchar(255) DEFAULT NULL COMMENT '客戶端所擁有的Spring Security的權限值,多個用逗號(,)分隔',
`access_token_validity` int(11) DEFAULT NULL COMMENT '訪問令牌有效時間值(單位:秒)',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '更新令牌有效時間值(單位:秒)',
`additional_information` varchar(255) DEFAULT NULL COMMENT '預留字段',
`autoapprove` varchar(255) DEFAULT NULL COMMENT '用戶是否自動Approval操作'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 
         

DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(255) DEFAULT NULL COMMENT '加密的access_token值',
`token` longblob COMMENT 'OAuth2AccessToken.java對象序列化后的二進制數據',
`authentication_id` varchar(255) DEFAULT NULL COMMENT '加密過的username,client_id,scope',
`user_name` varchar(255) DEFAULT NULL COMMENT '登錄的用戶名',
`client_id` varchar(255) DEFAULT NULL COMMENT '客戶端ID'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 
         

DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) DEFAULT NULL COMMENT '授權碼(未加密)',
`authentication` varbinary(255) DEFAULT NULL COMMENT 'AuthorizationRequestHolder.java對象序列化后的二進制數據'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 
         

DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(255) DEFAULT NULL COMMENT '加密過的refresh_token的值',
`token` longblob COMMENT 'OAuth2RefreshToken.java對象序列化后的二進制數據 ',
`authentication` longblob COMMENT 'OAuth2Authentication.java對象序列化后的二進制數據'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 
         

DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL COMMENT '用戶名',
`password` varchar(50) DEFAULT NULL COMMENT '密碼',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶信息表';

憑證(賬號)和權限表3張 

  • authority 權限表
  • credentials  憑證表(相當於用戶賬號表)
  • credentials_authorities 授權表(以上2個表的關聯表)

現實項目中對應的是用戶,角色,權限表

 
         

DROP TABLE IF EXISTS `authority`;
CREATE TABLE `authority` (
`id` bigint(11) NOT NULL COMMENT '權限id',
`authority` varchar(255) DEFAULT NULL COMMENT '權限',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 
         

DROP TABLE IF EXISTS `credentials`;
CREATE TABLE `credentials` (
`id` bigint(11) NOT NULL COMMENT '憑證id',
`enabled` tinyint(1) NOT NULL COMMENT '是否可用',
`name` varchar(255) NOT NULL COMMENT '用戶名',
`password` varchar(255) NOT NULL COMMENT '密碼',
`version` int(11) DEFAULT NULL COMMENT '版本號',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 
         

DROP TABLE IF EXISTS `credentials_authorities`;
CREATE TABLE `credentials_authorities` (
`credentials_id` bigint(20) NOT NULL COMMENT '憑證id',
`authorities_id` bigint(20) NOT NULL COMMENT '權限id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

初始化數據

  • 定義了3個角色3個賬號
  • 以“project_api” 命名resourceServer中的api請求路徑,我們定義一個客戶端叫做:user-client(認證權限類型:read,write)
  • 密碼都是user,數據庫中存的是加密過后的字符串
INSERT INTO authority  VALUES(1,'ROLE_OAUTH_ADMIN');
INSERT INTO authority VALUES(2,'ROLE_RESOURCE_ADMIN');
INSERT INTO authority VALUES(3,'ROLE_PROJECT_ADMIN');
INSERT INTO credentials VALUES(1,b'1','oauth_admin','$2a$10$BurTWIy5NTF9GJJH4magz.9Bd4bBurWYG8tmXxeQh1vs7r/wnCFG2','0');
INSERT INTO credentials VALUES(2,b'1','resource_admin','$2a$10$BurTWIy5NTF9GJJH4magz.9Bd4bBurWYG8tmXxeQh1vs7r/wnCFG2','0');
INSERT INTO credentials  VALUES(3,b'1','project_admin','$2a$10$BurTWIy5NTF9GJJH4magz.9Bd4bBurWYG8tmXxeQh1vs7r/wnCFG2','0');
INSERT INTO credentials_authorities VALUE (1,1);
INSERT INTO credentials_authorities VALUE (2,2);
INSERT INTO credentials_authorities VALUE (3,3);


INSERT INTO oauth_client_details VALUES('user_client','project_api', '$2a$10$BurTWIy5NTF9GJJH4magz.9Bd4bBurWYG8tmXxeQh1vs7r/wnCFG2', 'read,write', 'password,refresh_token', 'http://127.0.0.1', 'ROLE_PROJECT_ADMIN', 7200, 1800, NULL, 'true');

 

(三)工程配置

 創建父工程oauth2,繼續建立兩個子模塊:

  • Authorization Server - 授權服務器
  • Resource Server - 資源服務器

具體配置下載工程源碼查看

 

(四)Authorization Server - Spring Security配置

 創建一個spring security 配置類,在配置類中注入了上面我們自定義的自定義UserDetailsService以及用戶密碼驗證器。

 
         
package com.oauth2.authorization.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* spring security 配置類
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //開啟security注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

/**
* 密碼編碼驗證器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

/**
* 自定義UserDetailsService用來從數據庫中根據用戶名查詢用戶信息以及角色信息
*/
@Autowired
public UserDetailsService userDetailsService;

@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}

/**
* 驗證配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").authenticated()
.and()
.userDetailsService(userDetailsService);
}
} 

 自定義UserDetailsService

 創建一個名叫JdbcUserDetails的類實現UserDetailsService接口,代碼如下:

public class JdbcUserDetails implements UserDetailsService {

    @Autowired
    private CredentialsDao credentialsDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        Credentials credentials = credentialsDao.findByName(username);
        if (credentials == null) {
            throw new UsernameNotFoundException("User '" + username + "' can not be found");
        }

        return new User(credentials.getName(), credentials.getPassword(), credentials.isEnabled(), true, true, true, credentials.getGrantedAuthorities());
    }

}

 

(五)Authorization Server - 授權服務器

 授權服務器負責驗證用戶標識並提供令牌,使用@EnableAuthorizationServer注解啟用授權服務器配置。 

package com.oauth2.authorization.config;

import com.oauth2.authorization.userdetails.JdbcUserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
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.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
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 AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

@Autowired
private DataSource dataSource;

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private AuthenticationManager authenticationManager;

@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}

@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}

@Autowired
public UserDetailsService userDetailsService;

/**
* 配置 token 節點的安全策略
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()"); // 獲取 token 的策略
security.checkTokenAccess("isAuthenticated()");
}

/**
* 配置客戶端信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService()); //設置客戶端的配置從數據庫中讀取,存儲在oauth_client_details表
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager) // 開啟密碼驗證,來源於 WebSecurityConfigurerAdapter
.userDetailsService(userDetailsService) // 讀取驗證用戶的信息
.tokenStore(tokenStore());

}

 

(六)Resource Server - 資源服務器

 資源服務器,受OAuth2令牌保護的資源

package com.oauth2.resources.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;


import javax.sql.DataSource;

/**
* 資源服務器配置
*/

@Configuration
@EnableResourceServer
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourcesServerConfig extends ResourceServerConfigurerAdapter {

@Autowired
private DataSource dataSource;

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Autowired
private LogoutSuccessHandler logoutSuccessHandler;

@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("project_api").stateless(false);
resources.tokenStore(tokenStore());
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutUrl("/logout")//虛擬的登出地址
.logoutSuccessHandler(logoutSuccessHandler)//登出做的操作
.and()
.authorizeRequests()
.antMatchers("/test/hello").permitAll()
.antMatchers("/test/**").authenticated();
}
}

我們這里設置了一個LogoutSuccessHandler,他的作用是請求/logout地址時,清空數據庫中的accessToken,防止被竊取用於訪問,代碼如下:

package com.oauth2.resources.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登出清空accessToken
 */

@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {

    private static String BEARER_AUTHENTICATION = "Bearer";

    private static String HEADER_AUTHENTICATION = "authorization";

    @Autowired
    private TokenStore tokenStore;

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        String auth = httpServletRequest.getHeader(HEADER_AUTHENTICATION);
        String token = httpServletRequest.getParameter("access_token");
        if (auth != null && auth.startsWith(BEARER_AUTHENTICATION)) {
            token = token.split(" ")[0];
        }

        if (token != null) {
            OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
            if (accessToken != null) {
                tokenStore.removeAccessToken(accessToken);
            }
        }
    }

}

 

(七)測試 

資源服務器測試代碼

package com.oauth2.resources.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/test")
public class TestController {

    @GetMapping("/hello")
    public String hello(){
        return "Hello";
    }

    @GetMapping("/meet")
    public String meet(){
        return "I meet you";
    }

    @GetMapping("/welcome")
    public String welcome(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "Welcome " + authentication.getName();
    }

    @GetMapping("/project")
    @PreAuthorize("hasRole('ROLE_PROJECT_ADMIN')")  //具有此角色
    public String project(){
        return "This is my project";
    }


}

 

在ResourceServerConfig配置中,只有/test/hello不需要授權,其他都需要授權。

1. 直接訪問 http://localhost:8081/test/hello ,無需授權,所以正常響應,輸出hello字符串。

 

2.  訪問 http://localhost:8081/test/meet

可以看到正如我們預期一樣,返回了401錯誤以及錯誤信息,下面我們來獲取access_token。

3. Spring Security OAuth2默認提供的四個URL

  • /oauth/authorize : 授權AuthorizationEndpoint
  • /oauth/token : 令牌TokenEndpoint
  • /oauth/check_token : 令牌校驗CheckTokenEndpoint
  • /oauth/confirm_access : 授權頁面WhitelabelApprovalEndpoint
  • /oauth/error : 錯誤頁面WhitelabelErrorEndpoint

在獲取token之前需要在數據庫表oauth_client_details添加對應的數據 ,見上方的初始化數據,初始化了一條客戶端配置信息。 

4.獲得令牌,POST請求 http://localhost:8080/oauth/token?grant_type=password&username=project_admin&password=user    

 

可以看到我們訪問的地址,grant_type使用到了password模式,username和password即credentials表中的name和password字段值。

獲取access_token需要響應頭中添加客戶端的授權信息,通過Postman工具的頭授權信息即可輸出對應的值就可以完成Basic Auth的加密串生成,clientid、secret的值存執表oauth_client_details中對應字段。

成功訪問后oauth2給我們返回了幾個參數:

  • access_token:本地訪問獲取到的access_token,會自動寫入到數據庫中。
  • token_type:獲取到的access_token的授權方式
  • refersh_token:刷新token時所用到的授權
  • tokenexpires_in:有效期(從獲取開始計時,值秒后過期)
  • scope:客戶端的接口操作權限(read:讀,write:寫)

   3.帶上授權服務器返回的access_token發訪問 http://localhost:8081/test/meet?access_token=7afa7ff0-2e17-4388-b8c7-47355de57537

 

成功輸出。

  5. 訪問http://localhost:8081/test/welcome?access_token=7afa7ff0-2e17-4388-b8c7-47355de57537

成功打印出了用戶的賬號。

6. 訪問http://localhost:8081/test/project?access_token=7afa7ff0-2e17-4388-b8c7-47355de57537

成功輸出。

從代碼上到,project方法使用了PreAuthorize注解,要求用戶具有ROLE_PROJECT_ADMIN角色才能訪問,如果使用一個不具有ROLE_PROJECT_ADMIN角色的賬號的access_token訪問,將出現下列403提示access_denied:

7.  訪問 http://localhost:8081/logout?access_token=7afa7ff0-2e17-4388-b8c7-47355de57537 ,token將被從數據庫中刪除

再使用該token將提示invalid_token: 

 

(八)工程下載

 https://download.csdn.net/download/zsg88/11603183


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM