Spring Security使用单点登录的权限功能


Posted in Java/Android onApril 03, 2022

背景

在配置中心增加权限功能

  • 目前配置中心已经包含了单点登录功能,可以通过统一页面进行登录,登录完会将用户写入用户表
  • RBAC的用户、角色、权限表CRUD、授权等都已经完成
  • 希望不用用户再次登录,就可以使用SpringSecurity的权限控制

Spring Security

Spring Security最主要的两个功能:认证和授权

功能 解决的问题 Spring Security中主要类
认证(Authentication) 你是谁 AuthenticationManager
授权(Authorization) 你可以做什么 AuthorizationManager

实现

在这先简单了解一下Spring Security的架构是怎样的,如何可以认证和授权的

过滤器大家应该都了解,这属于Servlet的范畴,Servlet 过滤器可以动态地拦截请求和响应,以变换或使用包含在请求或响应中的信息

Spring Security使用单点登录的权限功能

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单点登录权限内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Java/Android 相关文章推荐
源码解读Spring-Integration执行过程
Jun 11 Java/Android
Java Kafka 消费积压监控的示例代码
Jul 01 Java/Android
dubbo服务整合zipkin详解
Jul 26 Java/Android
浅谈spring boot使用thymeleaf版本的问题
Aug 04 Java/Android
Java移除无效括号的方法实现
Aug 07 Java/Android
Java中try catch处理异常示例
Dec 06 Java/Android
maven依赖的version声明控制方式
Jan 18 Java/Android
Android Flutter实现图片滑动切换效果
Apr 07 Java/Android
Android学习之BottomSheetDialog组件的使用
Jun 21 Java/Android
Android RecyclerView实现九宫格效果
Jun 28 Java/Android
Spring中bean集合注入的方法详解
Jul 07 Java/Android
Java实现超大Excel文件解析(XSSF,SXSSF,easyExcel)
Jul 15 Java/Android
Spring Boot 底层原理基础深度解析
Java 超详细讲解数据结构中的堆的应用
Java 数据结构七大排序使用分析
Java基础——Map集合
Apr 01 #Java/Android
Android基于Fresco实现圆角和圆形图片
Apr 01 #Java/Android
Android自定义scrollview实现回弹效果
Apr 01 #Java/Android
Android自定义ScrollView实现阻尼回弹
You might like
用PHP+MySql编写聊天室
2006/10/09 PHP
discuz authcode 经典php加密解密函数解析
2020/07/12 PHP
解析strtr函数的效率问题
2013/06/26 PHP
PHP直接修改表内容DataGrid功能实现代码
2015/09/24 PHP
yii2使用GridView实现数据全选及批量删除按钮示例
2017/03/01 PHP
php实现微信企业号支付个人的方法详解
2017/07/26 PHP
Iframe 自适应高度并实时监控高度变化的js代码
2009/10/30 Javascript
javascript获取作用在元素上面的样式属性代码
2012/09/20 Javascript
js字符串日期yyyy-MM-dd转化为date示例代码
2014/03/06 Javascript
javascript实现分栏显示小技巧附图
2014/10/13 Javascript
node爬取微博的数据的简单封装库nodeweibo使用指南
2015/01/02 Javascript
js实现鼠标悬浮给图片加边框的方法
2015/01/30 Javascript
Javascript表单特效之十大常用原理性样例代码大总结
2016/07/12 Javascript
JavaScript中的普通函数和箭头函数的区别和用法详解
2017/03/21 Javascript
vue跨域解决方法
2017/10/15 Javascript
解决v-for中使用v-if或者v-bind:class失效的问题
2018/09/25 Javascript
使用iView Upload 组件实现手动上传图片的示例代码
2018/10/01 Javascript
Vue el-autocomplete远程搜索下拉框并实现自动填充功能(推荐)
2019/10/25 Javascript
解决vue项目刷新后,导航菜单高亮显示的位置不对问题
2019/11/01 Javascript
JS前后端实现身份证号验证代码解析
2020/07/23 Javascript
[38:21]2014 DOTA2国际邀请赛中国区预选赛5.21 TongFu VS LGD-CDEC
2014/05/22 DOTA
[56:42]完美世界DOTA2联赛循环赛 Matador vs Forest 第二场 11.06
2020/11/06 DOTA
Python 3中print函数的使用方法总结
2017/08/08 Python
Python实现的计数排序算法示例
2017/11/29 Python
酷! 程序员用Python带你玩转冲顶大会
2018/01/17 Python
Python实现绘制双柱状图并显示数值功能示例
2018/06/23 Python
使用python接入微信聊天机器人
2020/03/31 Python
Python matplotlib以日期为x轴作图代码实例
2019/11/22 Python
python编写一个会算账的脚本的示例代码
2020/06/02 Python
DRF框架API版本管理实现方法解析
2020/08/21 Python
CSS3制作炫酷带方向感应的鼠标滑过图片3D动画
2016/03/16 HTML / CSS
澳洲网红粉泥面膜:Sand & Sky
2019/08/13 全球购物
食堂采购员岗位职责
2015/04/03 职场文书
SpringCloud Alibaba 基本开发框架搭建过程
2021/06/13 Java/Android
在Windows Server 2012上安装 .NET Framework 3.5 所遇到的问题
2022/04/29 Servers
JS实现页面炫酷的时钟特效示例
2022/08/14 Javascript