單點登錄系統使用Spring Security的許可權功能

来源:https://www.cnblogs.com/songjiyang/archive/2022/04/02/16093636.html
-Advertisement-
Play Games

背景 在配置中心增加許可權功能 目前配置中心已經包含了單點登錄功能,可以通過統一頁面進行登錄,登錄完會將用戶寫入用戶表 RBAC的用戶、角色、許可權表CRUD、授權等都已經完成 希望不用用戶再次登錄,就可以使用SpringSecurity的許可權控制 Spring Security Spring Secu ...


背景

在配置中心增加許可權功能

  • 目前配置中心已經包含了單點登錄功能,可以通過統一頁面進行登錄,登錄完會將用戶寫入用戶表
  • RBAC的用戶、角色、許可權表CRUD、授權等都已經完成
  • 希望不用用戶再次登錄,就可以使用SpringSecurity的許可權控制

Spring Security

Spring Security最主要的兩個功能:認證和授權

功能 解決的問題 Spring Security中主要類
認證(Authentication) 你是誰 AuthenticationManager
授權(Authorization) 你可以做什麼 AuthorizationManager

實現

在這先簡單瞭解一下Spring Security的架構是怎樣的,如何可以認證和授權的

過濾器大家應該都瞭解,這屬於Servlet的範疇,Servlet 過濾器可以動態地攔截請求和響應,以變換或使用包含在請求或響應中的信息

image

DelegatingFilterProxy是一個屬於Spring Security的過濾器

通過這個過濾器,Spring Security就可以從Request中獲取URL來判斷是不是需要認證才能訪問,是不是得擁有特定的許可權才能訪問。

已經有了單點登錄頁面,Spring Security怎麼登錄,不登錄可以拿到許可權嗎

Spring Security官方文檔-授權架構中這樣說,GrantedAuthority(也就是擁有的許可權)被AuthenticationManager寫入Authentication對象,後而被AuthorizationManager用來做許可權認證

The GrantedAuthority objects are inserted into the Authentication object by the AuthenticationManager and are later read by either the AuthorizationManager when making authorization decisions.

為瞭解決我們的問題,即使我只想用許可權認證功能,也得造出一個Authentication,先看下這個對象:

Authentication

Authentication包含三個欄位:

  • principal,代表用戶
  • credentials,用戶密碼
  • authorities,擁有的許可權

有兩個作用:

  • AuthenticationManager的入參,僅僅是用來存用戶的信息,準備去認證
  • AuthenticationManager的出參,已經認證的用戶信息,可以從SecurityContext獲取

SecurityContext和SecurityContextHolder用來存儲Authentication, 通常是用了線程全局變數ThreadLocal, 也就是認證完成把Authentication放入SecurityContext,後續在整個同線程流程中都可以獲取認證信息,也方便了認證

繼續分析

看到這可以得到,要實現不登錄的許可權認證,只需要手動造一個Authentication,然後放入SecurityContext就可以了,先嘗試一下,大概流程是這樣,在每個請求上

  1. 獲取sso登錄的用戶
  2. 讀取用戶、角色、許可權寫入Authentication
  3. 將Authentication寫入SecurityContext
  4. 請求完畢時將SecurityContext清空,因為是ThreadLocal的,不然可能會被別的用戶用到
  5. 同時Spring Security的配置中是對所有的url都允許訪問的

加了一個過濾器,代碼如下:

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@WebFilter( urlPatterns = "/*", filterName = "reqResFilter" )
public class ReqResFilter implements Filter{

	@Autowired
	private SSOUtils ssoUtils;
	@Autowired
	private UserManager userManager;
	@Autowired
	private RoleManager roleManager;

	@Override
	public void init( FilterConfig filterConfig ) throws ServletException{

	}

	@Override
	public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain )
			throws IOException, ServletException{
		setAuthentication(servletRequest);
		filterChain.doFilter( servletRequest, servletResponse );
		clearAuthentication();
	}

	@Override
	public void destroy(){

	}

	private void setAuthentication( ServletRequest request ){

		Map<String, String> data;
		try{
			data = ssoUtils.getLoginData( ( HttpServletRequest )request );
		}
		catch( Exception e ){
			data = new HashMap<>();
			data.put( "name", "visitor" );
		}
		String username = data.get( "name" );
		if( username != null ){
			userManager.findAndInsert( username );
		}
		List<Role> userRole = userManager.findUserRole( username );
		List<Long> roleIds = userRole.stream().map( Role::getId ).collect( Collectors.toList() );
		List<Permission> rolePermission = roleManager.findRolePermission( roleIds );
		List<SimpleGrantedAuthority> authorities = rolePermission.stream().map( one -> new SimpleGrantedAuthority( one.getName() ) ).collect(
				Collectors.toList() );

		UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( username, "", authorities );
		SecurityContextHolder.getContext().setAuthentication( authenticationToken );
	}

	private void clearAuthentication(){

		SecurityContextHolder.clearContext();
	}
}

從日誌可以看出,Principal: visitor,當訪問未授權的介面被拒絕了

16:04:07.429 [http-nio-8081-exec-9] DEBUG org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@cc4c6ea0: Principal: visitor; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: CHANGE_USER_ROLE, CHANGE_ROLE_PERMISSION, ROLE_ADD
...
org.springframework.security.access.AccessDeniedException: 不允許訪問

結論

不登錄是可以使用Spring Security的許可權,從功能上是沒有問題的,但存在一些別的問題

  • 性能問題,每個請求都需要請求用戶角色許可權資料庫,當然可以利用緩存優化
  • 我們寫的過濾器其實也是Spring Security做的事,除此之外,它做了更多的事,比如結合HttpSession, Remember me這些功能

我們可以採取另外一種做法,對用戶來說只登錄一次就行,我們仍然是可以手動用代碼再去登錄一次Spring Security的

如何手動登錄Spring Security

How to login user from java code in Spring Security? 從這篇文章從可以看到,只要通過以下代碼即可

	private void loginInSpringSecurity( String username, String password ){

		UsernamePasswordAuthenticationToken loginToken = new UsernamePasswordAuthenticationToken( username, password );
		Authentication authenticatedUser = authenticationManager.authenticate( loginToken );
		SecurityContextHolder.getContext().setAuthentication( authenticatedUser );
	}

和上面我們直接拿已經認證過的用戶對比,這段代碼讓Spring Security來執行認證步驟,不過需要配置額外的AuthenticationManager和UserDetailsServiceImpl,這兩個配置只是AuthenticationManager的一種實現,和上面的流程區別不大,目的就是為了拿到用戶的信息和許可權進行認證

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class UserDetailsServiceImpl implements UserDetailsService{

	private static final Logger logger = LoggerFactory.getLogger( UserDetailsServiceImpl.class );

	@Autowired
	private UserManager userManager;

	@Autowired
	private RoleManager roleManager;

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

		User user = userManager.findByName( username );
		if( user == null ){
			logger.info( "登錄用戶[{}]沒註冊!", username );
			throw new UsernameNotFoundException( "登錄用戶[" + username + "]沒註冊!" );
		}
		return new org.springframework.security.core.userdetails.User( user.getUsername(), "", getAuthority( username ) );
	}

	private List<? extends GrantedAuthority> getAuthority( String username ){

		List<Role> userRole = userManager.findUserRole( username );
		List<Long> roleIds = userRole.stream().map( Role::getId ).collect( Collectors.toList() );
		List<Permission> rolePermission = roleManager.findRolePermission( roleIds );
		return rolePermission.stream().map( one -> new SimpleGrantedAuthority( one.getName() ) ).collect( Collectors.toList() );
	}
}
	@Override
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception{

		DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
		daoAuthenticationProvider.setUserDetailsService( userDetailsService );
		daoAuthenticationProvider.setPasswordEncoder( NoOpPasswordEncoder.getInstance() );
		return new ProviderManager( daoAuthenticationProvider );
	}

結論

通過這樣的方式,同樣實現了許可權認證,同時Spring Security會將用戶信息和許可權緩存到了Session中,這樣就不用每次去資料庫獲取

總結

可以通過兩種方式來實現不登錄使用SpringSecurity的許可權功能

  1. 手動組裝認證過的Authentication直接寫到SecurityContext,需要我們自己使用過濾器控制寫入和清除
  2. 手動組裝未認證過的Authentication,並交給Spring Security認證,並寫入SecurityContext

Spring Security是如何配置的,因為只使用許可權功能,所有允許所有的路徑訪問(我們的單點登錄會限制介面的訪問)

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.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.password.NoOpPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.Collections;



@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

	@Autowired
	private UserDetailsService userDetailsService;

	@Override
	protected void configure( HttpSecurity http ) throws Exception{

		http
				.cors()
				.and()
				.csrf()
				.disable()
				.sessionManagement()
				.and()
				.authorizeRequests()
				.anyRequest()
				.permitAll()
				.and()
				.exceptionHandling()
				.accessDeniedHandler( new SimpleAccessDeniedHandler() );
	}


	@Override
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception{

		DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
		daoAuthenticationProvider.setUserDetailsService( userDetailsService );
		daoAuthenticationProvider.setPasswordEncoder( NoOpPasswordEncoder.getInstance() );
		return new ProviderManager( daoAuthenticationProvider );
	}

	@Bean
	public CorsConfigurationSource corsConfigurationSource(){

		CorsConfiguration configuration = new CorsConfiguration();
		configuration.setAllowedOrigins( Collections.singletonList( "*" ) );
		configuration.setAllowedMethods( Arrays.asList( "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS" ) );
		configuration.setAllowCredentials( true );
		configuration.setAllowedHeaders( Collections.singletonList( "*" ) );
		configuration.setMaxAge( 3600L );
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration( "/**", configuration );
		return source;
	}

}

參考


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 註意如果你的mac是M1處理器 那抱歉當前文章可能不支持了,因為當前模擬器不支持。 3步完成mac uniapp 模擬器配置 1.下載網易mumu模擬器 https://mumu.163.com/mac/index.html 2.安裝 設置 下載完成後安裝運行就是這樣的 選擇屏幕旋轉 手機模式 3. ...
  • 具體示例 //代碼 console.log(JSON.stringify({ x: 5, y: 6 },null,2)); //輸出結果 { "x": 5, "y": 6 } JSON.stringify() 介紹 JSON.stringify()方法將一個JavaScript對象或值轉換為JSON ...
  • 原型模式不是通過new生成新的對象,而使通過複製進行生成; 原型模式適用於相同類型的多個對象的生成; 原型模式分為兩種:淺克隆/淺表副本(Shallow Clone)和深克隆/深表副本(Deep Clone); 淺克隆:Shallow Clone,只複製值類型變數,不複製引用類型變數的克隆;(只複製 ...
  • 相比於工廠模式,抽象工廠模式的每個工廠可以創建產品系列,而不是一個產品; 抽象工廠用到的技術:介面、多態、配置文件、反射; 抽象工廠模式的設計原則: 實現客戶端創建產品和使用產品的分離,客戶端無須瞭解創建的細節,符合迪米特法則; 客戶端面向介面定義產品,符合依賴倒置原則; 客戶端面向介面定義工廠,而 ...
  • 外觀(Facade)模式,又叫做門面模式,是一種通過為多個複雜的子系統提供一個一致的介面,使這些子系統更加容易被訪問的模式。比如說我們日常生活中醫院的分診台,就是實現統一訪問介面的特性: 一、外觀模式介紹 外觀模式提供一個統一介面,用來訪問子系統的一系列介面,從而讓子系統更容易使用。這個子系統可以有 ...
  • 本篇是根據 GopherCon SG 2019 “Understanding Allocations” 演講的學習筆記。 Understanding Allocations: the Stack and the Heap - GopherCon SG 2019 - YouTube 理解分配:棧和堆 ...
  • 內容概要 request 對象和 response 對象 GenericAPIView 介紹 基於 GenericAPIView 的 5個視圖擴展類 GenericAPIView 的9個視圖子類 視圖集 ModelViewSet 的使用 ViewSetMixin 源碼分析 內容詳細 request ...
  • (一)Java簡介 Java 是由 Sun Microsystems 公司於 1995 年 5 月推出的 Java 面向對象程式設計語言和 Java 平臺的總稱。由 James Gosling和同事們共同研發,併在 1995 年正式推出。 後來 Sun 公司被 Oracle (甲骨文)公司收購,Ja ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...