循序渐进学习spring security 第十四篇 自定义验证码认证逻辑,自定义动态权限下URL白名单配置

学习目标

学习完本文您将掌握如下技能

  1. 自定义认证逻辑,如添加验证码校验,增加安全性
  2. 动态权限控制URL的同时,可以配置URL白名单(即哪些URL不需要认证就可以访问)

掌握了这些,无论你是正在开发的岗位上,还是即将炒老板换老板的求职路上,都是非常有用的;开发能更快速实现功能,然后轻松喝茶看其他同事继续敲代码。找工作,能更好的展示自己的技术技能面,大部分面试官都是管理者,虽然他们了解的知识面很广,但是不精,只要你掌握了这些技能,面试的时候,可以加不少分

回顾

前面我们介绍了spring security 登录认证过程和自定义动态权限URL控制和实战项目,如果对之前文章不熟悉的同学可以去看我之前的文章学习下,有助于本文的理解

  1. 面试不要在说不熟悉spring security了,一个demo让你使劲忽悠面试官
  2. 循序渐进学习spring security 第二篇,如何修改默认用户?
  3. 循序渐进学习spring security 第三篇,如何自定义登录页面?登录回调?
  4. 循序渐进雪spring security 第四篇,登录流程是怎样的?登录用户信息保存在哪里?
  5. 循序渐进学习spring security 第五篇,如何处理重定向和服务器跳转?登录如何返回JSON串?
  6. 循序渐进学spring security第六篇,手把手教你如何从数据库读取用户进行登录验证,mybatis集成
  7. 循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了
  8. 循序渐进学spring security 第八篇,如何配置密码加密?是否支持多种加密方案?
  9. 循序渐进学spring security 第九篇,支持多种加密方案源码解读和示例代码
  10. 循序渐进学spring security 第十篇 如何用token登录?JWT闪亮登场
  11. 循序渐进学spring security 第十一篇 如何动态权限控制URL?如何动态给用户添加权限?
  12. 循序渐进学spring security 第十二篇 修改登录密码后如何让修改前的token失效?
  13. 循序渐进学spring security 第十三篇 四种权限控制是什么?项目中如何使用案例

自定义认证逻辑,增加验证码校验

通过前面文章循序渐进雪spring security 第四篇,登录流程是怎样的?登录用户信息保存在哪里? 的学习,我们了解到,登录认证逻辑主要是通过AuthenticationManager完成的,而AuthenticationManager是一个接口,再由其实现者ProviderManager调用AuthenticationProvider 完成校验,而AuthenticationProvider的最终实现者是DaoAuthenticationProvider,也就是DaoAuthenticationProvider完整了登录逻辑的认证校验,如果不熟悉登录过程的,可以先回去看看之前的文章

了解了登录过程后,我们要增加验证码校验,那就容易多了
也就是只需要创建一个类UsernamePasswordProvider (名字自定义)继承UsernamePasswordProvider,然后实现对应的校验方法,然后在配置中替换掉默认的DaoAuthenticationProvider ,就可以了

创建项目:security-verify

在前面文章循序渐进学spring security 第十一篇 如何动态权限控制URL?如何动态给用户添加权限? 的项目上copy一个新项目:security-verify,修改pom.xml的artifactId标签和项目名称 , 如下:

    <groupId>com.harry</groupId>
    <artifactId>security-verify</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security-verify</name>

创建类:UsernamePasswordProvider

创建类UsernamePasswordProvider,继承DaoAuthenticationProvider,重写additionalAuthenticationChecks方法,这个方法是用来校验登录用户密码的,不熟悉的同学可以回去看看循序渐进雪spring security 第四篇,登录流程是怎样的?登录用户信息保存在哪里?

public class UsernamePasswordProvider extends DaoAuthenticationProvider {
    
    
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
    
        HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String code = req.getParameter("code");
        String verify_code = (String) req.getSession().getAttribute("verifyCode");
        if (code == null || verify_code == null || !code.equals(verify_code)) {
    
    
            throw new AuthenticationServiceException("验证码错误");
        }
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

这里,先从request中获取到登录提交的参数code,即验证码,然后从session中取出来的验证码进行比较,如果验证码不一致,就抛出异常,表示验证码错误,退出登录的意思

如果校验通过,则表示验证码正确,继续调用父类进行用户密码校验

编写获取验证码接口

创建类VerifyController,添加一个获取验证码的接口

@RestController
public class VerifyController {
    
    

    @GetMapping("/getVerifyCode")
    public String getVerifyCode(HttpSession session){
    
    
        String verifyCode=getVerifyCode();
        //将生成的验证码存放到session中,待登录时取出进行校验
        session.setAttribute("verifyCode",verifyCode);
        return verifyCode;
    }
	//随机获取4位验证码
    private String getVerifyCode() {
    
    
        StringBuilder sb=new StringBuilder();
        Random random=new Random();
        for (int x=0;x<4;x++){
    
    
            int i = random.nextInt(10);
            sb.append(i);
        }
        return sb.toString();
    }
}

配置URL白名单

        http.authorizeRequests()
                .antMatchers("/getVerifyCode").permitAll()
                .anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    
    
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
    
    
                        object.setSecurityMetadataSource(menuFilterInvocationSecurityMetadataSource); //动态获取url权限配置
                        object.setAccessDecisionManager(menuAccessDecisionManager); //权限判断
                        return object;
                    }
                })

其中, .antMatchers("/getVerifyCode").permitAll() 就是配置/getVerifyCode 接口URL白名单

测试

启动项目,先调用接口/getVerifyCode 获取验证码,发现调不通,报错了,说明我们配置的URL白名单是有问题的,这是为什么呢?这就是因为我们在上篇文章循序渐进学spring security 第十一篇 如何动态权限控制URL?如何动态给用户添加权限? 中自定义了动态权限引起的,那么如何解决?

如何添加URL白名单

前面我们通过文章 循序渐进学spring security 第十一篇 如何动态权限控制URL?如何动态给用户添加权限? 介绍了如何给用户添加动态权限,如何动态控制URL,但是还有一个问题,不知道大家是否有留意到,当你按照文章介绍的方法去配置时,想配置URL白名单,代码如下,发现配置的URL白名单都无效了

        http.authorizeRequests()
                .antMatchers("/getVerifyCode").permitAll()
                .anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    
    
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
    
    
                        object.setSecurityMetadataSource(menuFilterInvocationSecurityMetadataSource); //动态获取url权限配置
                        object.setAccessDecisionManager(menuAccessDecisionManager); //权限判断
                        return object;
                    }
                })

这个代码中, .antMatchers("/getVerifyCode").permitAll() 已经指定了URL白名单,
此时如果没有登录就去访问,发现访问不到的

** 这是为什么?这句代码不是配置的URL白名单吗?为什么会访问不到呢?**

这是因为,我们重写了接口FilterInvocationSecurityMetadataSource ,重新通过withObjectPostProcessor方法配置了该过滤器,这样,对于原来默认的FilterInvocationSecurityMetadataSource 实现类DefaultFilterInvocationSecurityMetadataSource 将不起作用

那怎么办?怎么去改?

DefaultFilterInvocationSecurityMetadataSource 源码剖析

FilterInvocationSecurityMetadataSource 默认实现类DefaultFilterInvocationSecurityMetadataSource ,也就是如果我们没有自定义类MenuFilterInvocationSecurityMetadataSource 实现FilterInvocationSecurityMetadataSource ,并通过如下代码配置的话,默认是会走DefaultFilterInvocationSecurityMetadataSource 过滤器的

.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    
    
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
    
    
                        object.setSecurityMetadataSource(menuFilterInvocationSecurityMetadataSource); //动态获取url权限配置
                        object.setAccessDecisionManager(menuAccessDecisionManager); //权限判断
                        return object;
                    }
                })

既然自定义了动态权限之后,对于URL白名单就不生效,那答案肯定是在DefaultFilterInvocationSecurityMetadataSource 里面了,我们跟进DefaultFilterInvocationSecurityMetadataSource 源码,

public class DefaultFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    
    
	protected final Log logger = LogFactory.getLog(getClass());
	private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
	public DefaultFilterInvocationSecurityMetadataSource(
			LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap) {
    
    
		this.requestMap = requestMap;
	}
	@Override
	public Collection<ConfigAttribute> getAllConfigAttributes() {
    
    
		Set<ConfigAttribute> allAttributes = new HashSet<>();
		this.requestMap.values().forEach(allAttributes::addAll);
		return allAttributes;
	}
	@Override
	public Collection<ConfigAttribute> getAttributes(Object object) {
    
    
		final HttpServletRequest request = ((FilterInvocation) object).getRequest();
		int count = 0;
		for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : this.requestMap.entrySet()) {
    
    
			if (entry.getKey().matches(request)) {
    
    
				return entry.getValue();
			}
			else {
    
    
				if (this.logger.isTraceEnabled()) {
    
    
					this.logger.trace(LogMessage.format("Did not match request to %s - %s (%d/%d)", entry.getKey(),
							entry.getValue(), ++count, this.requestMap.size()));
				}
			}
		}
		return null;
	}
	@Override
	public boolean supports(Class<?> clazz) {
    
    
		return FilterInvocation.class.isAssignableFrom(clazz);
	}
}

我来解析一下,主要两点

  • DefaultFilterInvocationSecurityMetadataSource 在实例化时,通过有参构造方法传入了requestMap,这个requestMap 实际上就是我们在securityConfig里面配置的URL信息,包括登录的URL,这个可以通过注释掉withObjectPostProcessor配置,然后启动项目时在构造方法处打个断点了解得到
    在这里插入图片描述
  • Collection getAttributes(Object object) 方法中,会遍历map集合requestMap,进行当前访问URL接口的校验,如果校验通过就返回对应URL的权限集合

了解了这一点,就好办了

将过滤器 MenuFilterInvocationSecurityMetadataSource 改为继承DefaultFilterInvocationSecurityMetadataSource

这样,需要实现DefaultFilterInvocationSecurityMetadataSource 的构造方法,定义自己的requestMap ,通过构造方法传入参数初始化requestMap,重写getAttributes(Object object)方法,将父类中的实现复制下来,先校验白名单,然后再校验动态权限,这样就可以将自定义URL白名单通过构造方法传入,在请求中校验开放了

@Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
    
    
        final HttpServletRequest request = ((FilterInvocation) object).getRequest();
        int count = 0;
        for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : this.requestMap.entrySet()) {
    
    
            if (entry.getKey().matches(request)) {
    
    
                return entry.getValue();
            }
            else {
    
    
                if (this.logger.isTraceEnabled()) {
    
    
                    this.logger.trace(LogMessage.format("Did not match request to %s - %s (%d/%d)", entry.getKey(),
                            entry.getValue(), ++count, this.requestMap.size()));
                }
            }
        }
        Set<ConfigAttribute> set = new HashSet<>();
        // 获取请求地址
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        log.info("requestUrl >> {}", requestUrl);
        List<Menu> allMenus = menuMapper.findAllMenus();
        if (!CollectionUtils.isEmpty(allMenus)) {
    
    
            List<String> urlList = allMenus.stream().filter(f->f.getUrl().endsWith("**")?requestUrl.startsWith(f.getUrl().substring(0,f.getUrl().lastIndexOf("/"))):requestUrl.equals(f.getUrl())).map(menu -> menu.getUrl()).collect(Collectors.toList());
            for (String url:urlList){
    
    
                List<Role> roles = roleMapper.findRolesByUrl(url); //当前请求需要的权限
                if(!CollectionUtils.isEmpty(roles)){
    
    
                    roles.forEach(role -> {
    
    
                        SecurityConfig securityConfig = new SecurityConfig(role.getAuthority());
                        set.add(securityConfig);
                    });
                }
            }

        }
        if (ObjectUtils.isEmpty(set)) {
    
    
            return SecurityConfig.createList("ROLE_LOGIN");
        }
        return set;
    }

配置URL白名单

好了,现在注释掉原来的URL白名单配置

        http.authorizeRequests()
//                .antMatchers("/getVerifyCode")
//                .permitAll()
                .anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    
    
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
    
    
                        object.setSecurityMetadataSource(menuFilterInvocationSecurityMetadataSource); //动态获取url权限配置
                        object.setAccessDecisionManager(menuAccessDecisionManager); //权限判断
                        return object;
                    }
                })

改用新的配置

    @Bean
    public LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> getAnyRequest() {
    
    
        LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>anyRequst=new LinkedHashMap<>();
        anyRequst.put(new AntPathRequestMatcher("/getVerifyCode", null),null);
        return anyRequst;
    }

定义MenuFilterInvocationSecurityMetadataSource 构造方法参数的bean,因为参数是一个LinkedHashMap,我们这里通过new出来,然后将白名单添加后,返回,然后注册到spring bean中,这样项目启动时,就会自动注入到spring容器中了

测试

启动项目,现在去访问/getVerifyCode接口
在这里插入图片描述
正常获取到验证码 7880

此时,去访问其他接口
在这里插入图片描述
因为该接口不是白名单,需要登录才能访问,但当前还没有登录

登录,没有输入验证码直接登录
在这里插入图片描述

输入验证码登录,登录成功
在这里插入图片描述

这样,我们就已经完成了自定义认证逻辑和URL白名单的配置了

源码下载:security-verify

猜你喜欢

转载自blog.csdn.net/huangxuanheng/article/details/119514042