权限管理也面临数据存储同样的动态处理问题,默认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;
}
}
}