前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【读源码】SpringSecurity为什么要这么配置?SpringSecurity判断登录源码解读

【读源码】SpringSecurity为什么要这么配置?SpringSecurity判断登录源码解读

作者头像
小王不头秃
发布2024-06-19 17:44:24
1600
发布2024-06-19 17:44:24
举报

前言

咱们从源码的角度来看看SpringSecurity的处理过程,整个过程比较长,设计的函数调用也比较多,为了在解释详细的同时增加咱们的可读性,我在源码的关键步骤都添加了小标题,大家可以通过小标题来了解大概过程,其他步骤仅以图片形式展示,方便大家一步步跟下来。那一起来看看吧!

SpringSecurity需要配置什么

这里咱们以单体架构为例,使用JWT和SpringSecurity结合之后的样例来讲解。

配置类

代码语言:javascript
复制
@Configuration
//开启注解授权认证的注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
      /**
 	  * 为了方便阅读,这里省略一部分非重要代码,需要全部的代码评论即可
 	  **/

//    把我们自己写好的过滤器添加到过滤器的前面
        http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}

添加自定义的Filter

首先是JwtAuthenticationTokenFilter这个过滤器,这个是咱们整合JWT进行权限验证的关键组成部分,这个过滤器完全由我们自定义,可以实现我们想要做的功能,这里提供一个样例

代码语言:javascript
复制
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    RoleandpermService roleandpermService;
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//获取token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)) {
//            这里放行就是让其他的过滤器帮我们解决未登录
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
//        解析token
        String userid = "";

        try {
            Claims claims = JwtUtils.getClaims(token);
            userid = (String) claims.get("userid");
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("解析token异常");
        }

//        redis中获取信息
        User o = (User) redisTemplate.opsForValue().get("login" + userid);
        if (Objects.isNull(o)) {
            throw new RuntimeException("token异常");
        }
        // 拿到用户的权限,并对权限进行封装
        List<Roleandperm> byUserId = roleandpermService.getByUserId(new Long(o.getUser_id()));
        List<GrantedAuthority> newList=new LinkedList<>();
        List<String> perms=new LinkedList<>();
        byUserId.forEach(p->{
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(p.getPerm());
            newList.add(simpleGrantedAuthority);
            perms.add(p.getPerm());
        });
//        存入SecurityContextgholder,因为后续的过滤器需要在这个东西中找到认证的信息
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(new LoginUser(o,perms), null, newList);
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

大家看上面的注解其实能理解这个过滤器是干啥的,其实就是截取请求中的Headers中的token信息,然后对这个token进行解析,然后根据token中的id信息在Redis中拿到权限信息,并且封装成为UsernamePasswordAuthenticationToken对象,交给后续过滤器进行处理。

在过滤器中我们进行权限验证了吗?

很明显,没有的。我们只是把权限封装起来,交给后续的过滤器使用了。

那他到底是什么时候调用的呢?

开始进入源码状态了,大家准备好!

判断是否登录源码解析

进入过滤器链处理

首先就是FilterChainProxydoFilter方法,

代码语言:javascript
复制
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
           // 判断是否所有过滤器都处理结束了
            if (this.currentPosition == this.size) {
                if (FilterChainProxy.logger.isDebugEnabled()) {
                    FilterChainProxy.logger.debug(LogMessage.of(() -> {
                        return "Secured " + FilterChainProxy.requestLine(this.firewalledRequest);
                    }));
                }

                this.firewalledRequest.reset();
                this.originalChain.doFilter(request, response);
            } else {
            // 指向过滤器的下标
                ++this.currentPosition;
                Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
                if (FilterChainProxy.logger.isTraceEnabled()) {
                    FilterChainProxy.logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(), this.currentPosition, this.size));
                }

                nextFilter.doFilter(request, response, this);
            }
        }
    }

上面的代码简单来说,就是循环遍历additionalFilters中的过滤器,然后依次对请求进行处理,我们看以下additionalFilters到底存了什么

你会发现我们自定义的JwtAuthenticationTokenFilter 也在里面,这说明在遍历过程中,我们自定义的Filter也会对请求进行处理。

JwtAuthenticationTokenFilter干了啥

现在咱们回到JwtAuthenticationTokenFilter中,你会发现我们把用户和权限信息放在SecurityContextHolder.getContext里面,这是干啥用的呢,咱们接着往下看。

代码语言:javascript
复制
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
FilterSecurityInterceptor判断是否登录

前面咱们提到了在SecurityContextHolder.getContext()里把usernamePasswordAuthenticationToken放进去了,那放进去是为了干什么呢?

一个请求过来,我们是不是要判断登陆状态,那我们是不是可以基于usernamePasswordAuthenticationToken来判断是否登录呢?

答案是可以的

那他是怎么判断的呢

由于篇幅原因咱们直接来到FilterSecurityInterceptor的处理过程。

然后一步步点进去

获取JwtAuthenticationTokenFilter中存储的用户认证信息

注意这里,authenticated是不是有点熟悉,我们刚才怎么放用户信息来着,是不是

代码语言:javascript
复制
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken)

我们点进去看看,他获取了什么

是不是发现了我们的老朋友SecurityContextHolder.getContext(),通过getAuthentication就获取到了我们的用户信息和权限信息

接下来返回到AbstractSecurityInterceptor,接着往下走,一路点进去

投票器判断是否用户是否登录

就来到了上图的地方,这里很有意思哈。我们看一下他要做什么,SpringSecurity在判断用户状态时用的是一种叫做投票器的机制。可以理解为有一个投票通过那么该请求就可以继续执行,如果一票都没通过,那就直接抛出异常就可以了。

voter就可以认为是投票者,点进去

判断Authentication是否为AnonymousAuthentication

关键来了,兄弟们,这里的!isAnonymous是什么意思呢?

着我们就要回溯以下之前的过滤器AnonymousAuthenticationFilter

注意哈,咱们的JwtAuthenticationTokenFilter执行顺序是在这个过滤器之前的

如果说你没有传递token,那我们就不会往SecurityContextHolder.getContext()放认证对象,那么AnonymousAuthenticationFilter就会帮我们放一个Anonymous的认证对象进去,这样我们在FilterSecurityInterceptor就可以判断请求的用户是否已经登陆。

接下来一路返回到咱们投票器的部分

这里的result为1,也就直接返回,就可以进行后续的操作了,并不会抛出异常,否则会deny++。

当没有直接返回并且deny>0,就会抛出异常也就是用户授权未通过,那就直接返回403了。

到这里为止,我们实现了什么功能?

就是一个判断用户是否登录的过程,那验权是不是还没做,别着急,咱们接着看。

不传递token会怎么处理未登录请求

我们看下不传token会怎样,直接看Voter这边

你会发现deny直接++

然后接下来就开始抛出异常了

然后交给ExceptionTranslationFilter进行处理,注意哈这也是咱们之前调用的过滤器链中的一个过滤器,然后就可以返回403了。

总结

到现在我们大概了解了SpringSecurity怎么判断请求是否登录的。简单来说就是我们可以通过自定过滤器的方式,将用户信息和权限信息封装起来存储到的SecurityContextHolder中,然后再由后续的FilterSecurityInterceptor判断是否登录,未登陆的话SecurityContextHolder存储的是匿名用户,这样就可以判断是否登录了。今天就到这了,下期咱们讲讲SpringSecurity怎么做的权限验证!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • SpringSecurity需要配置什么
    • 配置类
      • 添加自定义的Filter
        • 判断是否登录源码解析
          • 进入过滤器链处理
          • JwtAuthenticationTokenFilter干了啥
          • FilterSecurityInterceptor判断是否登录
          • 获取JwtAuthenticationTokenFilter中存储的用户认证信息
          • 投票器判断是否用户是否登录
          • 判断Authentication是否为AnonymousAuthentication
          • 不传递token会怎么处理未登录请求
      • 总结
      相关产品与服务
      云数据库 Redis
      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档