架构小白到砖家-16-【权限管理问题】-springsecurity处理动态权限验证

权限管理也面临数据存储同样的动态处理问题,默认security都是使用提前配置权限设置,或者使用注解控制方法权限,但是这就造成了硬编码的问题,实际生产中权限是根据业务要求进行动态调整的。

为了解决硬编码问题,就需要动态获取权限信息。
权限信息包含两部分信息,用户拥有权限和资源访问权限。
用户拥有权限,在登录的时候是可以动态进行加载的。
资源访问权限,如果使用security默认的java代码配置方式,肯定就不行了。所以需要将资源访问权限也动态进行获取。
那么咱们的目标就是给security补充,动态资源访问权限的获取和权限判定逻辑。

我们需要先弄清楚security的权限验证流程。
用户授权,就是获取资源访问权限和用户拥有权限,然后权限判定两者是否匹配。
用户拥有权限,已经在用户认证环节存入了会话中,直接就可以获取到。
资源访问权限,需要通过资源权限服务进行获取,我们希望从数据库动态获取。
权限判定,需要根据这两者权限规则来进行判定。相当于钥匙和锁的关系,资源都是有锁的,用户的权限就是钥匙,钥匙和锁匹配才能允许访问。
在这里插入图片描述

那我们怎么来扩展这套动态权限验证逻辑呢?
权限验证的本质,就是多个权限拦截器组成的拦截器链。我们只需要在默认拦截器之前,加入一个咱们自定义的拦截器就可以了。
在这里插入图片描述

那么就开始给security增加一个自定义拦截器吧。
第一步,创建自定义拦截器,MySecurityInterceptor;
第二步,创建自定义资源加载器,MySecurityMetadataSource;
第三步,创建自定义权限决策器,MyAccessDecisionManager;
在这里插入图片描述

创建了自定义拦截器后,需要将拦截器加入到拦截器链中。
MySecurityInterceptor需要注入MySecurityMetadataSource和MyAccessDecisionManager。在咱们的配置类WebSecurityConfig进行这些操作。
为了阅读流畅性,先用截图来说明流程,再后面补充源码。

初始化自定义拦截器;WebSecurityConfig.java
在这里插入图片描述

将自定义拦截器加入拦截器链;WebSecurityConfig.java
在这里插入图片描述

后面的具体security的代码实现,大家就看后面的源码吧,注解都很详细。代码实现不是咱们权限理论的核心,我们主要还是关注权限理论,怎么解决问题的思路才是最重要的,因为每个框架的API都不一样,但是思路都是一样的。

回顾总结
security默认采用的硬编码权限配置方式,为了解决动态配置问题,我们需要自定义拦截器,加入到拦截器链中。用户授权由资源访问权限和用户拥有权限两部分组成。我们只需要将这两部分权限信息放入数据库,使用的时候进行动态加载,就可以实现权限动态验证了。最后就是根据security的框架逻辑,新增自定义的拦截器、资源加载器、权限决策器。

补充security自定义拦截器源码。

MySecurityMetadataSource

@Service
public class MySecurityMetadataSource implements
		FilterInvocationSecurityMetadataSource {

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

	private Map<String, Collection<ConfigAttribute>> map = null;

	/**
	 * 应用初始化时,会触发一次
	 */
	public Collection<ConfigAttribute> getAllConfigAttributes() {
		return null;
	}

	/**
	 * 每个请求都会触发该方法,获取请求URL所需要的权限列表; 
	 * request参数就是请求对象,通过获取HttpServletRequest对象;
	 * 我们可以获取到请求的URL,用来查询权限列表。 
	 */
	public Collection<ConfigAttribute> getAttributes(Object request)
			throws IllegalArgumentException {
		//这里用Map简化的实现了自定义的URL权限集合,
		//实际应用中可以使用redis等缓存工具
		if (map == null){
			loadResourceDefine();
		}
		// 循环变量map的key,判断request是否有权限限制
		HttpServletRequest req = ((FilterInvocation) request).getHttpRequest();
		for (Iterator<String> iter = map.keySet().iterator(); iter.hasNext();) {
			String resUrl = iter.next();
			//为了满足正则表达式的路径,使用AntPathRequestMatcher来进行匹配请求路径URL
			AntPathRequestMatcher matcher = new AntPathRequestMatcher(resUrl);
			if (matcher.matches(req)) {
				logger.info("获取URL:" + resUrl+"的权限列表"+map.get(resUrl));
				return map.get(resUrl);
			}
		}

		return null;
	}

	public boolean supports(Class<?> arg0) {
		// 支持class一定要设置为true,不然AbstractSecurityInterceptor中类型校验不过。
		return true;
	}

	/**
	 * 初始化自定义的URL权限集合
	 */
	private void loadResourceDefine() {
		map = new HashMap<String, Collection<ConfigAttribute>>();
		//配置“/test/**”URL正则表达式,需要“NORMAL”or “ADMIN"权限;
		//其实权限就是字符串,但是roles创建的权限,需要加“ROLE_”前缀,
		//因为是自定义权限,不是用的roles方法,需要手动添加前缀
		Collection<ConfigAttribute> array = new ArrayList<ConfigAttribute>();
		array.add(new SecurityConfig("ROLE_NORMAL"));
		array.add(new SecurityConfig("ROLE_ADMIN"));
		map.put("/test/**", array);

	}

}

MyAccessDecisionManager

@Service
public class MyAccessDecisionManager implements AccessDecisionManager {

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

	/**
	 * 决定访问请求是否有权限授权。
	 * 
	 * authentication,用户认证通过后,保存的用户权限信息
	 * request,访问请求对象
	 * configAttributes,是MySecurityMetadataSource根据request获取的自定义权限列表
	 * 
	 * 如果MySecurityMetadataSource没有查询到符合的权限列表,该方法就不会别执行;
	 * 所以这里就不需要再从数据库中查询一次该请求URL符合的权限列表。
	 */
	public void decide(Authentication authentication, Object request,Collection<ConfigAttribute> configAttributes) 
			throws AccessDeniedException, InsufficientAuthenticationException {

		HttpServletRequest req = ((FilterInvocation) request).getHttpRequest();
		String url = req.getServletPath();
		
		//遍历请求需要的权限
		for (Iterator<ConfigAttribute> iterator = configAttributes.iterator(); iterator.hasNext();) {
			ConfigAttribute cfg = iterator.next();
			String needAccess = cfg.getAttribute();
			for (GrantedAuthority ga : authentication.getAuthorities()) {
				//用户权限中有一个满足权限要求,就可以放行
				if (needAccess.trim().equals(ga.getAuthority())) {
					logger.info(authentication.getName()+"拥有 url:"+url+"的权限:" + needAccess);
					return;
				}
			}
		}
		
		throw new AccessDeniedException("权限验证失败");
	}

	public boolean supports(ConfigAttribute attribute) {
		return true;
	}

	public boolean supports(Class<?> clazz) {
		return true;
	}

}

MySecurityInterceptor

public class MySecurityInterceptor extends AbstractSecurityInterceptor
		implements Filter {

	//获取自定义的权限列表服务
	private FilterInvocationSecurityMetadataSource securityMetadataSource;
	
	//AbstractSecurityInterceptor,创建的时候必须AccessDecisionManager
	MySecurityInterceptor(AccessDecisionManager myAccessDecisionManager) {
		super.setAccessDecisionManager(myAccessDecisionManager);
	}

	public void init(FilterConfig filterConfig) throws ServletException {

	}

	public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {
		
		//我们还是要使用springsecurity的权限验证机制,
		//所以这里的filter处理逻辑,调用AbstractSecurityInterceptor的方法
		FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
		InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
		try {
			// 执行下一个拦截器
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
		} finally {
			super.afterInvocation(token, null);
		}
	}

	public void destroy() {

	}

	public Class<?> getSecureObjectClass() {
		return FilterInvocation.class;
	}

	//必须实现SecurityMetadataSource对象获取方法
	public SecurityMetadataSource obtainSecurityMetadataSource() {
		return this.getSecurityMetadataSource();
	}

	public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
		return securityMetadataSource;
	}

	public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource securityMetadataSource) {
		this.securityMetadataSource = securityMetadataSource;
	}

}

WebSecurityConfig

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

	
	/**
	 * 创建自定义用户授权拦截器,MySecurityInterceptor;
	 * 这里不能将自定义拦截器托管个spring,否则会重复拦截;
	 * 只能手动创建对象,并且插入到FilterSecurityInterceptor之前。
	 * .addFilterBefore(getMySecurityInterceptor(), FilterSecurityInterceptor.class);
	 */
	@Autowired
	private MyAccessDecisionManager myAccessDecisionManager;
	@Autowired
	private FilterInvocationSecurityMetadataSource securityMetadataSource;
	
	MySecurityInterceptor getMySecurityInterceptor(){
		MySecurityInterceptor ms = new MySecurityInterceptor(myAccessDecisionManager);
		ms.setSecurityMetadataSource(securityMetadataSource);
		return ms;
	}
	
	
	protected void configure(HttpSecurity http) throws Exception {
	    http
	    		//URL权限控制,优先级从上到下
	    		.authorizeRequests()
		        .antMatchers("/easyui/**","/**/*.js","/**/*.css","/**/*.png").permitAll()      //无需授权的资源     
	            .antMatchers("/test/admin/**").hasRole("ADMIN")		//指定角色授权,直接使用hasRole方法                                    
	            .antMatchers("/test/web/**").access("hasRole('ADMIN') or hasRole('NORMAL')")  //指定角色授权,使用access方法      
	            .anyRequest().authenticated()	//默认所有资源都需要授权
	            
	           //自定义登录逻辑
		        .and()
		        .formLogin()
	            .loginPage("/login.html") 	//自定义登录页面
	            .loginProcessingUrl("/login")  //登录请求,默认url是/login
//	            .successForwardUrl("successForwardUrl")	//登录成功跳转地址
//	            .failureForwardUrl("failureForwardUrl")		//登录失败跳转地址
	            .permitAll()     //允许登录相关的请求,所有人可以访问
		            
	            //自定义注销逻辑
		    	.and()
		    	.logout()
//		        .logoutUrl("/my/logout")     //默认url是/logout                                           
	            .logoutSuccessUrl("/")      //注销成功后,跳转指定页面                                     
	            .invalidateHttpSession(true)    
	            
	            //关闭额外安全机制
	            .and()
	            .csrf().disable()//关闭csrf防跨站请求伪造机制,不然ajax访问无法使用
	           .headers().frameOptions().disable();//关闭iframe防攻击机制,不然iframe无法调用URL
	    
	    //添加自定义的拦截器到security拦截链中
	    http
	    	//自定义用户授权拦截器,插入到FilterSecurityInterceptor之前。
	    	.addFilterBefore(getMySecurityInterceptor(), FilterSecurityInterceptor.class);
	}
	
}

WebUserDetailsService

@Configuration
public class WebUserDetailsService implements UserDetailsService {

	private final static Logger logger = LoggerFactory.getLogger(WebUserDetailsService.class); 
	
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		
		logger.info("用户登录:"+username);	
		
		if (StringUtils.isEmpty(username)) {
			return null;
		}else if("admin".equals(username) ){
//			UserDetails user = User.withUsername(username).password("123456").roles("ADMIN","NORMAL").build();
			//这里使用自定义权限列表的方式初始化权限
			List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority> ();
			grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
			grantedAuthorities.add(new SimpleGrantedAuthority("delete"));
			UserDetails user = new User(username,"123456",grantedAuthorities);
	        return user;
		}else{
//			UserDetails user = User.withUsername(username).password("123").roles("NORMAL").build();
			//这里使用自定义权限列表的方式初始化权限
			List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority> ();
			grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_NORMAL"));
			UserDetails user = new User(username,"123",grantedAuthorities);
	        return user;
		}

	}

}
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值