本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

2024-11(1)

Spring Security深入剖析

发布于2021-05-29 21:56     阅读(1360)     评论(0)     点赞(4)     收藏(3)


关键源码

io.undertow.servlet.core.ManagedFilter:42

io.undertow.servlet.handlers.FilterHandler:58

org.springframework.web.filter.OncePerRequestFilter:42

org.apache.catalina.core.ApplicationFilterChain

org.springframework.security.web.savedrequest.HttpSessionRequestCache

问题分析

问题1: 过滤器选择

org.springframework.web.filter.OncePerRequestFilter:请求只会执行一次过滤链,请求转发会跳过,重定向可以被拦截。

GenericFilterBean:请求转发还是会被过滤器拦截,重定向也会被拦截

问题2:springsecurity怎么进行拦截资源鉴权无权限跳转登录页面的?

org.springframework.security.web.access.ExceptionTranslationFilter继承GenericFilterBean

这里会拦截到acess deny异常

image-20210513160020493

不同的exception进行不同的处理,大致可以分成两种Exception。

AuthenticationException

AccessDeniedException

image-20210513160628121

将封装后的请求进行封装,存到session里面key为(SPRING_SECURITY_SAVED_REQUEST)

image-20210513172308507

根据请求匹配入口类org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint

image-20210513172859438

image-20210513172942012

image-20210513175219421

问题3:loginUrl怎么赋值的?

org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint

image-20210513174234337

image-20210513174342296

问题4:springsercurity怎么鉴权的?

org.springframework.security.web.access.intercept.FilterSecurityInterceptor拦截到请求,获取SecurityContextHolder.getContext().getAuthentication(),然后进行投票器AffirmativeBased进行投票管理。鉴权,鉴权成功则放行,鉴权不通过则返回AccessDeniedException,然后跳转到登录页面。

image-20210516175528602

image-20210516175646780

image-20210516180204610

问题5:投票器怎么鉴权的?

三个决策器的区别如下:

AffirmativeBased:有一个投票器同意了,就通过。

ConsensusBased:多数投票器同意就通过,平局的话,则看 allowIfEqualGrantedDeniedDecisions 参数的取值。

UnanimousBased 所有投票器都同意,请求才通过。

org.springframework.security.access.intercept.AbstractSecurityInterceptor从org.springframework.security.web.access.intercept.FilterSecurityInterceptor中获取securityMetadataSource

image-20210516194234157

WebExpressionVoter进行鉴权。

image-20210516194711747

org.springframework.security.access.expression.SecurityExpressionRoot

image-20210516194917618

问题6: Sercurity怎么传递session的。登录成功后再次访问不再进入登录页面。

org.springframework.security.web.context.SecurityContextPersistenceFilter每次访问时会从session获取key为SPRING_SECURITY_CONTEXT的登录信息,如果能够获取到,则保存到SecurityContextHolder

image-20210516214128057

image-20210516214237939

问题7:session、Cookie、JSESSIONID关系

所谓session可以这样理解:当与服务端进行会话时,比如说登陆成功后,服务端会为用户开壁一块内存区间,用以存放用户这次会话的一些内容,比如说用户名之类的。那么就需要一个东西来标志这个内存区间是你的而不是别人的,这个东西就是session id(jsessionid只是tomcat中对session id的叫法,在其它容器里面,不一定就是叫jsessionid了。),而这个内存区间你可以理解为session。
然后,服务器会将这个session id发回给你的浏览器,放入你的浏览器的cookies中(这个cookies是内存cookies,跟一般的不一样,它会随着浏览器的关闭而消失)。
之后,只有你浏览器没有关闭,你每向服务器发请求,服务器就会从你发送过来的cookies中拿出这个session id,然后根据这个session id到相应的内存中取你之前存放的数据。
但是,如果你退出登陆了,服务器会清掉属于你的内存区域,所以你再登的话,会产生一个新的session了。

这是一个保险措施 因为Session默认是需要Cookie支持的,但有些客户浏览器是关闭Cookie的【而jsessionid是存储在Cookie中的,

如果禁用Cookie的话,也就是说服务器那边得不到jsessionid,这样也就没法根据jsessionid获得对应的session了,获得不了session就

得不到session中存储的数据了。】这个时候就需要在URL中指定服务器上的session标识,也

就是类似于“jsessionid=5F4771183629C9834F8382E23BE13C4C” 这种格式。用一个方法(忘了方法的名字)处理URL串就可以得到

这个东西,这个方法会判断你的浏览器是否开启了Cookie,如果他认为应该加他就会加上去。

jsessionid是服务器那边生成的,因为cookie是服务器那边送到客户端的信息。不管能不能修改jsessionid,都不应该修改,如果你修改了,这就失去了jessionid的自身意义了,你修改的话,你让服务器那边如何找到对应的session?找不到的话,你存放在那个session中的数据不是取不到了吗?

问题8:登录成功,不退出,关闭浏览器后重新访问

image-20210520171546758

org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter:从cookie中读取remember-me的cookie,读取用户信息。

org.springframework.security.web.session.SessionManagementFilter:存储用户信息到session中。

问题9:SpringSecurity的过滤器链

1.怎么创建的?

通过FilterChainProxy进行添加。

在启动时添加SpringSecurity的额外过滤链。

image-20210526141836909

2.如何查看过滤链有哪些过滤器?

image-20210526210258623

启动查看org.springframework.security.config.annotation.web.builders.HttpSecurity

image-20210526133354653

或者运行时任意请求可以查看FilterChainProxy

image-20210526141240167

实践

1.需求:根据url直接跳过登录

例:http://localhost:8088/?userName=admin

直接以admin的角色进入系统

首先自定义过滤器

package com.codermy.myspringsecurityplus.security.filter;

import cn.hutool.core.util.StrUtil;
import com.codermy.myspringsecurityplus.security.UserDetailsServiceImpl;
import com.codermy.myspringsecurityplus.security.dto.JwtUserDto;
import com.codermy.myspringsecurityplus.security.utils.com.pigic.hzeropigic.utils.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author: 潘顾昌
 * @Date: 2021/5/26 20:16
 */
@Slf4j
public class MyPermitLoginFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 获取授权信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication!=null){
            // 如果已经授权则跳过
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return ;
        }
        try {
            // 如果未授权查询是否有请求参数userName
            String userName = httpServletRequest.getParameter("userName");
            if (StrUtil.isNotEmpty(userName)){
                // 如果userName不为空,获取用户信息
                UserDetailsServiceImpl userDetailsService = SpringUtils.getBean(UserDetailsServiceImpl.class);
                JwtUserDto jwtUserDto = userDetailsService.loadUserByUsername(userName);
                // 设置授权信息到SecurityContext
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwtUserDto, null, jwtUserDto.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }else {
                log.info("未获取到用户信息!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }
    }
}

设置过滤器的位置在UsernamePasswordAuthenticationFilter之后

/**
 * anyRequest          |   匹配所有请求路径
 * access              |   SpringEl表达式结果为true时可以访问
 * anonymous           |   匿名可以访问
 * denyAll             |   用户不能访问
 * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
 * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
 * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
 * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
 * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
 * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
 * permitAll           |   用户可以任意访问
 * rememberMe          |   允许通过remember-me登录的用户访问
 * authenticated       |   用户登录后可访问
 */
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
    http.addFilterAfter(new MyPermitLoginFilter(), UsernamePasswordAuthenticationFilter.class);
    //关闭csrf
    http.csrf().disable()
            // .sessionManagement()// 基于token,所以不需要session
            // .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            // .and()
            //未登陆时返回 JSON 格式的数据给前端
            .httpBasic().authenticationEntryPoint(restAuthenticationEntryPoint)
            .and()
            .authorizeRequests()
            //任何人都能访问这个请求
            .antMatchers("/captcha").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            //登录页面 不设限访问
            .loginPage("/login.html")
            //拦截的请求
            .loginProcessingUrl("/login")
            // 登录成功
            .successHandler(authenticationSuccessHandler)
            // 登录失败
            .failureHandler(authenticationFailureHandler)
            .permitAll()
            .and()
            .rememberMe().rememberMeParameter("rememberme")
            // 防止iframe 造成跨域
            .and()
            .headers()
            .frameOptions()
            .disable()
            .and();

    // 禁用缓存
    http.headers().cacheControl();



    // 无权访问 JSON 格式的数据
    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}
成跨域
            .and()
            .headers()
            .frameOptions()
            .disable()
            .and();

    // 禁用缓存
    http.headers().cacheControl();



    // 无权访问 JSON 格式的数据
    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
}

原文链接:https://blog.csdn.net/qq_37442469/article/details/117306474



所属网站分类: 技术文章 > 博客

作者:我长得真不赖

链接:http://www.javaheidong.com/blog/article/207412/20c9351608ee5b467a5d/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

4 0
收藏该文
已收藏

评论内容:(最多支持255个字符)