Spring Security使用單點(diǎn)登錄的權(quán)限功能
背景
在配置中心增加權(quán)限功能
- 目前配置中心已經(jīng)包含了單點(diǎn)登錄功能,可以通過統(tǒng)一頁面進(jìn)行登錄,登錄完會將用戶寫入用戶表
- RBAC的用戶、角色、權(quán)限表CRUD、授權(quán)等都已經(jīng)完成
- 希望不用用戶再次登錄,就可以使用SpringSecurity的權(quán)限控制
Spring Security
Spring Security最主要的兩個(gè)功能:認(rèn)證和授權(quán)
功能 | 解決的問題 | Spring Security中主要類 |
---|---|---|
認(rèn)證(Authentication) | 你是誰 | AuthenticationManager |
授權(quán)(Authorization) | 你可以做什么 | AuthorizationManager |
實(shí)現(xiàn)
在這先簡單了解一下Spring Security的架構(gòu)是怎樣的,如何可以認(rèn)證和授權(quán)的
過濾器大家應(yīng)該都了解,這屬于Servlet的范疇,Servlet 過濾器可以動態(tài)地?cái)r截請求和響應(yīng),以變換或使用包含在請求或響應(yīng)中的信息
DelegatingFilterProxy是一個(gè)屬于Spring Security的過濾器
通過這個(gè)過濾器,Spring Security就可以從Request中獲取URL來判斷是不是需要認(rèn)證才能訪問,是不是得擁有特定的權(quán)限才能訪問。
已經(jīng)有了單點(diǎn)登錄頁面,Spring Security怎么登錄,不登錄可以拿到權(quán)限嗎
Spring Security官方文檔-授權(quán)架構(gòu)中這樣說,GrantedAuthority(也就是擁有的權(quán)限)被AuthenticationManager寫入Authentication對象,后而被AuthorizationManager用來做權(quán)限認(rèn)證
The GrantedAuthority objects are inserted into the Authentication object by the AuthenticationManager and are later read by either the AuthorizationManager when making authorization decisions.
為了解決我們的問題,即使我只想用權(quán)限認(rèn)證功能,也得造出一個(gè)Authentication,先看下這個(gè)對象:
Authentication
Authentication包含三個(gè)字段:
- principal,代表用戶
- credentials,用戶密碼
- authorities,擁有的權(quán)限
有兩個(gè)作用:
- AuthenticationManager的入?yún)?,僅僅是用來存用戶的信息,準(zhǔn)備去認(rèn)證
- AuthenticationManager的出參,已經(jīng)認(rèn)證的用戶信息,可以從SecurityContext獲取
SecurityContext和SecurityContextHolder用來存儲Authentication, 通常是用了線程全局變量ThreadLocal, 也就是認(rèn)證完成把Authentication放入SecurityContext,后續(xù)在整個(gè)同線程流程中都可以獲取認(rèn)證信息,也方便了認(rèn)證
繼續(xù)分析
看到這可以得到,要實(shí)現(xiàn)不登錄的權(quán)限認(rèn)證,只需要手動造一個(gè)Authentication,然后放入SecurityContext就可以了,先嘗試一下,大概流程是這樣,在每個(gè)請求上
- 獲取sso登錄的用戶
- 讀取用戶、角色、權(quán)限寫入Authentication
- 將Authentication寫入SecurityContext
- 請求完畢時(shí)將SecurityContext清空,因?yàn)槭荰hreadLocal的,不然可能會被別的用戶用到
- 同時(shí)Spring Security的配置中是對所有的url都允許訪問的
加了一個(gè)過濾器,代碼如下:
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,當(dāng)訪問未授權(quán)的接口被拒絕了
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: 不允許訪問
結(jié)論
不登錄是可以使用Spring Security的權(quán)限,從功能上是沒有問題的,但存在一些別的問題
- 性能問題,每個(gè)請求都需要請求用戶角色權(quán)限數(shù)據(jù)庫,當(dāng)然可以利用緩存優(yōu)化
- 我們寫的過濾器其實(shí)也是Spring Security做的事,除此之外,它做了更多的事,比如結(jié)合HttpSession, Remember me這些功能
我們可以采取另外一種做法,對用戶來說只登錄一次就行,我們?nèi)匀皇强梢允謩佑么a再去登錄一次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 ); }
和上面我們直接拿已經(jīng)認(rèn)證過的用戶對比,這段代碼讓Spring Security來執(zhí)行認(rèn)證步驟,不過需要配置額外的AuthenticationManager和UserDetailsServiceImpl,這兩個(gè)配置只是AuthenticationManager的一種實(shí)現(xiàn),和上面的流程區(qū)別不大,目的就是為了拿到用戶的信息和權(quán)限進(jìn)行認(rèn)證
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 ); }
結(jié)論
通過這樣的方式,同樣實(shí)現(xiàn)了權(quán)限認(rèn)證,同時(shí)Spring Security會將用戶信息和權(quán)限緩存到了Session中,這樣就不用每次去數(shù)據(jù)庫獲取
總結(jié)
可以通過兩種方式來實(shí)現(xiàn)不登錄使用SpringSecurity的權(quán)限功能
- 手動組裝認(rèn)證過的Authentication直接寫到SecurityContext,需要我們自己使用過濾器控制寫入和清除
- 手動組裝未認(rèn)證過的Authentication,并交給Spring Security認(rèn)證,并寫入SecurityContext
附
Spring Security是如何配置的,因?yàn)橹皇褂脵?quán)限功能,所有允許所有的路徑訪問(我們的單點(diǎn)登錄會限制接口的訪問)
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; } }
參考
到此這篇關(guān)于Spring Security使用單點(diǎn)登錄的權(quán)限功能的文章就介紹到這了,更多相關(guān)Spring Security單點(diǎn)登錄權(quán)限內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring?boot?security權(quán)限管理集成cas單點(diǎn)登錄功能的實(shí)現(xiàn)
- SpringSecurity OAuth2單點(diǎn)登錄和登出的實(shí)現(xiàn)
- 一個(gè)注解搞定Spring Security基于Oauth2的SSO單點(diǎn)登錄功能
- Spring Security基于JWT實(shí)現(xiàn)SSO單點(diǎn)登錄詳解
- 使用Spring Security OAuth2實(shí)現(xiàn)單點(diǎn)登錄
- Spring Security 單點(diǎn)登錄簡單示例詳解
- SpringSecurity OAtu2+JWT實(shí)現(xiàn)微服務(wù)版本的單點(diǎn)登錄的示例
相關(guān)文章
使用?Spring?Boot?Admin?監(jiān)控應(yīng)用狀態(tài)的詳細(xì)過程
這篇文章主要介紹了使用?Spring?Boot?Admin?監(jiān)控應(yīng)用狀態(tài),該模塊采集應(yīng)用的內(nèi)部信息,并暴露給外部的模塊,支持?HTTP?和?JMX,并可以與一些第三方監(jiān)控系統(tǒng)(如?Prometheus)整合,需要的朋友可以參考下2022-09-09IDEA2020如何打開Run Dashboard的方法步驟
這篇文章主要介紹了IDEA2020如何打開Run Dashboard的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07springboot如何根據(jù)不同的日志級別顯示不同的顏色
這篇文章主要介紹了springboot如何根據(jù)不同的日志級別顯示不同的顏色問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08springboot+mybatis報(bào)錯找不到實(shí)體類的問題
這篇文章主要介紹了springboot+mybatis報(bào)錯找不到實(shí)體類的問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01MyBatis創(chuàng)建存儲過程的實(shí)例代碼_動力節(jié)點(diǎn)Java學(xué)院整理
本節(jié)需要用到的有2部分,第一部分是如何在Derby中創(chuàng)建存儲過程,第二部分是如何在Mybatis中調(diào)用存儲過程,具體實(shí)例代碼大家參考下本文吧2017-09-09Java基礎(chǔ)教程之類數(shù)據(jù)與類方法
這篇文章主要介紹了Java基礎(chǔ)教程之類數(shù)據(jù)與類方法,本文是對類的深入探討,類數(shù)據(jù)指類的一些屬性、參數(shù)等,類方法就是類包含的功能方法,需要的朋友可以參考下2014-08-08springboot?aop里的@Pointcut()的配置方式
這篇文章主要介紹了springboot?aop里的@Pointcut()的配置方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11