文章目录
学习目标
学习完本文您将掌握如下技能
- 自定义认证逻辑,如添加验证码校验,增加安全性
- 动态权限控制URL的同时,可以配置URL白名单(即哪些URL不需要认证就可以访问)
掌握了这些,无论你是正在开发的岗位上,还是即将炒老板换老板的求职路上,都是非常有用的;开发能更快速实现功能,然后轻松喝茶看其他同事继续敲代码。找工作,能更好的展示自己的技术技能面,大部分面试官都是管理者,虽然他们了解的知识面很广,但是不精,只要你掌握了这些技能,面试的时候,可以加不少分
回顾
前面我们介绍了spring security 登录认证过程和自定义动态权限URL控制和实战项目,如果对之前文章不熟悉的同学可以去看我之前的文章学习下,有助于本文的理解
- 面试不要在说不熟悉spring security了,一个demo让你使劲忽悠面试官
- 循序渐进学习spring security 第二篇,如何修改默认用户?
- 循序渐进学习spring security 第三篇,如何自定义登录页面?登录回调?
- 循序渐进雪spring security 第四篇,登录流程是怎样的?登录用户信息保存在哪里?
- 循序渐进学习spring security 第五篇,如何处理重定向和服务器跳转?登录如何返回JSON串?
- 循序渐进学spring security第六篇,手把手教你如何从数据库读取用户进行登录验证,mybatis集成
- 循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了
- 循序渐进学spring security 第八篇,如何配置密码加密?是否支持多种加密方案?
- 循序渐进学spring security 第九篇,支持多种加密方案源码解读和示例代码
- 循序渐进学spring security 第十篇 如何用token登录?JWT闪亮登场
- 循序渐进学spring security 第十一篇 如何动态权限控制URL?如何动态给用户添加权限?
- 循序渐进学spring security 第十二篇 修改登录密码后如何让修改前的token失效?
- 循序渐进学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