首頁 > 軟體

Spring Security使用單點登入的許可權功能

2022-04-03 13:00:50

背景

在設定中心增加許可權功能

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

Spring Security

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

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

實現

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

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

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就可以了,先嚐試一下,大概流程是這樣,在每個請求上

  • 獲取sso登入的使用者
  • 讀取使用者、角色、許可權寫入Authentication
  • 將Authentication寫入SecurityContext
  • 請求完畢時將SecurityContext清空,因為是ThreadLocal的,不然可能會被別的使用者用到
  • 同時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的許可權功能

  • 手動組裝認證過的Authentication直接寫到SecurityContext,需要我們自己使用過濾器控制寫入和清除
  • 手動組裝未認證過的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;
	}

}

參考

 到此這篇關於Spring Security使用單點登入的許可權功能的文章就介紹到這了,更多相關Spring Security單點登入許可權內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com