Spring Security 详解以及认证过程


共计 6621 个字符,预计需要花费 17 分钟才能阅读完成。

简介

Spring Security 是基于 Spring 实现的一个安全框架,其中包括非常多的过滤器,主要进行攻击防护、认证授权等功能。

过滤器链

Spring Security 常用的过滤器有15个,如下图所示:

Spring Security 详解以及认证过程

/>

在 FilterChainProxy 类中的 doFilterInternal 方法打断点可以看见。

1.org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

此过滤器用于继承 SecurityContext 到 Spring 异步执行机制中的 WebAsyncManager,和 spring 整合必须的。

2.org.springframework.security.web.context.SecurityContextPersistenceFilter

非常重要的一个过滤器,主要是使用 SecurityContextRepository 在 session 中保存或更新一个 SecurityContext,并将 SecurityContext 给以后的过滤器使用,来为后续 filter 建立所需的上下文,SecurityContext 中存储了当前用户的认证和权限信息。

3.org.springframework.security.web.header.HeaderWriterFilter

向请求的 header 中添加响应的信息,可以在 http 标签内部使用 security:headers 来控制。

4.org.springframework.security.web.csrf.CsrfFilter

Csrf 又称跨域请求伪造,SpringSecurity 会对所有 post 请求验证是否包含系统生成的 csrf 的 token 信息,如果不包含则报错,起到防止 csrf 攻击的效果.

这里需要提到的是,csrf 在前后端分离的项目中一般都会关闭,因为天然不怕 csrf 攻击,毕竟该攻击是基于 session 的,而前后端分离项目一般都是使用 token,将 token 放在请求头。

5.org.springframework.security.web.authentication.logout.LogoutFilter

匹配 URL 为 /logout 的请求,实现用户退出,清除认证信息。

6.org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

认证过滤器,下面将详细讲解该过滤器,默认匹配URL为 /login 且必须为 POST 请求。

7.org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认的认证界面。

8.org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

此过滤器生成一个默认的退出登录页面。

9.org.springframework.security.web.authentication.www.BasicAuthenticationFilter

此过滤器会自动解析HTTP请求中头部名字为 Authentication,且以 Basic 开头的头部信息。

10.org.springframework.security.web.savedrequest.RequestCacheAwareFilter

通过HttpSessionRequestCache内部维护一个RequestCache,用于缓存HttpServletRequest。

11.org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

针对 ServletRequest 进行一次包装,使得 request 具有更加丰富的API。

12.org.springframework.security.web.authentication.AnonymousAuthenticationFilter

当 SecurityContextHolder 中认证信息为空,则会创建一个匿名用户存储到 SecurityContextHolder 中,SpringSecurity 为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。

13.org.springframework.security.web.session.SessionManagementFilter

SecurityContextRepository限制同一个用户开启多个会话的数量。

14.org.springframework.security.web.access.ExceptionTranslationFilter

异常转换过滤器位于整个 SpringSecurityFilterChain 的后方,用来转换整个链路中出现的异常。

15.org.springframework.security.web.access.intercept.FilterSecurityInterceptor

获取所有配置资源的访问授权信息,根据 SecurityContextHolder 中存储的用户信息来决定其是否有权限。

认证过程

认证过程如下:

Spring Security 详解以及认证过程

/>

1.在过滤器链的初始阶段,用户名和密码会被封装成 UsernamePasswordAuthenticationToken,也就是 Authentication 对象,因为继承于它。

2.过滤器会调用 authenticate 方法将 Authentication 对象传递给 AuthenticationManager(认证管理器)。但是它不会进行对用户信息的认证,通过委托多个 AuthenticationProvider 来完成身份验证,只要有一个 AuthenticationProvider 验证成功,就会返回一个已经填充了用户认证信息的 Authentication 对象。

3.在 AuthenticationManager 中,这里是真正对用户信息进行认证的地方。认证管理器会调用 UserDetailsService 接口来获取用户的详细信息。UserDetailsService 是 Spring Security 用于获取用户信息的接口,它需要实现一个 loadUserByUsername()方法,根据用户名查询用户信息。

4.UserDetailsService 返回一个实现了 UserDetails 接口的对象,该对象包含了用户的详细信息,如用户名、密码和权限等。

5.在 AuthenticationProvider 中将 Authentication 对象的密码与用 PasswordEncoder 加密 UserDetails 的密码进行验证是否正确。正确则返回 UserDetails 的权限设置到 Authentication 对象中,并返回。

6.认证成功后,Spring Security 会将认证信息存储在 SecurityContext 中。SecurityContext 是一个线程本地的容器,用于存储当前用户的认证信息。

7.可以配置一个认证成功处理器来处理认证成功的逻辑,例如,更新用户在数据库中的登录失败次数和上次登录时间等等。

自定义认证

需要注意的是,配置文件中需要 HttpSecurity 调用 formLogin 方法,过滤器链才会注入 UsernamePasswordAuthenticationFilter、DefaultLoginPageGeneratingFilter 和 DefaultLogoutPageGeneratingFilter,否则将没有认证过程,那么除了开放的所有请求,其他的请求都将是 403,因为 SecurityContextHolder 里面没有内容。

在实际项目中,登录的接口一般都是自定义,毕竟除了验证账户密码还需要进行一些其他的操作,比如用 jwt 生成 token,放入 redis 并将 token 返回。

而 Spring security 的默认过滤器并不能做到,于是需要在配置文件中手动注入 AuthenticationManager,在自定义的登录接口注入该对象,并调用 authenticate 方法进行认证。所以此时又回到了刚刚所说的认证流程,还是能调用到重写的 loadUserByUsername 方法。

@Service
public class LoginServcieImpl implements ILoginServcie {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        // 这里会去调用 UserDetailsService 的 loadUserByUsername,如果认证失败将抛出异常 BadCredentialsException
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //如果认证没通过,给出对应的提示
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败");
        }
        //如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userid = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userid);
        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);
        //把完整的用户信息存入redis  userid作为key
        redisCache.setCacheObject("login:"+userid,loginUser);
        return new ResponseResult(200,"登录成功",map);
    }
}

登录接口返回了 token,那么接下来调用其他的接口时就需要将该 token 给带上,比如放在请求头。

虽然说认证已经通过了,但是传递 token 后,程序怎么就能判断用户是否有权限呢?答案是自定义过滤器。

自定义过滤器需要做的事情有:

1.判断请求头是否带有 token,如果没有 token 就放行,除了放行的请求不用担心其他请求会顺利通过,因为接下来的权限拦截器 FilterSecurityInterceptor 会校验权限,并返回 403。

2.如果请求头带有 token,需要解析 token 得到用户 id,并通过用户 id 获取 redis 中用户信息。

3.获取到用户信息,将权限信息封装到 Authentication 对象中,再将 Authentication 放入 SecurityContextHolder,这样所有过滤器都能获取到 Authentication 对象,包括其中的权限信息,到后面的权限过滤器根据其权限判断拦截或放行。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

注意配置过滤器后,需要在配置文件中添加该过滤器。

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

    @Resource
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //不通过Session获取SecurityContext
                    .antMatchers("/user/login").permitAll()
                    .anyRequest().authenticated()
                .and().cors();
        //添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

提醒:本文发布于490天前,文中所关联的信息可能已发生改变,请知悉!

Tips:清朝云网络工作室

阅读剩余
THE END