Spring Security应用到源码分析

Spring Security应用到源码分析

简单概述

Spring Security最最最重要的两个核心功能就是:"认证" 、"授权"

用户认证(Authentication)

按照业务场景来说,就是用户通过用户名&密码登录系统,系统对该用户的合法性进行验证

用户授权(Authorization)

用户授权是基于用户认证的,在用户认证之后,系统判断该用户是否有权限去操作某些资源

Spring Security的基本原理

  • SpringBoot遵从默认大于配置的原则,只需要开发人员引入SpringBoot与Sucurity整合的包即可实现自动化配置

    • <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
  • Spring Security 本质是一个过滤器链

    • 我们启动一个带有Security的SpringBoot项目

    • 可以从启动日志中得到以下信息

    • 全是过滤器

Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter, org.springframework.security.web.context.SecurityContextPersistenceFilter, org.springframework.security.web.header.HeaderWriterFilter, org.springframework.security.web.csrf.CsrfFilter, org.springframework.security.web.authentication.logout.LogoutFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter, org.springframework.security.web.authentication.www.BasicAuthenticationFilter, org.springframework.security.web.savedrequest.RequestCacheAwareFilter, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter, org.springframework.security.web.authentication.AnonymousAuthenticationFilter, org.springframework.security.web.session.SessionManagementFilter, org.springframework.security.web.access.ExceptionTranslationFilter, org.springframework.security.web.access.intercept.FilterSecurityInterceptor]
  • 重点看三个过滤器即可

FilterSecurityInterceptor

  • FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部

  • super.beforeInvocation(fi);

    • 表示查看之前的Filter是否放行通过

  • fi.getChain().doFilter(fi.getRequest(), fi.getResponse());

    • 表示真正的调用后台的服务

ExceptionTranslationFilter

  • ExceptionTranslationFilter:是一个异常过滤器,用来处理在认证过程中抛出的异常

UsernamePasswordAuthenticationFilter

  • UsernamePasswordAuthenticationFilter:对登录请求的表单做拦截,校验用户名和密码

    • 这里使用的是默认的内存的中密码和默认的账户名user

    • 后期我们会改造使用数据库做验证

  • 可以发现,登录请求必须为post请求

过滤器链如何加载

  • 过滤器链是通过 "DelegatingFilterProxy" 该类加载的,我们跟一下源码

  • 这是SpringBoot自动装配的源码,如果我们使用SpringBoot,就得手写过滤器链的加载过程

 

  • targetBeanName:有一个默认的名字 "FilterChainProxy"

  • 可以看到wac是Spring的应用上下文,从中获取 "FilterChainProxy" 的对象实例

  • 然后执行 "FilterChainProxy"的init方法,会走 "FilterChainProxy" 的doFilter方法

  • 可以看到无论如何都会执行该方法,我们进去其中看看

  • List<Filter> filters = getFilters(fwRequest);

    • 可以跟进去,就是一个迭代器,返回一个过滤器集合

    • 该集合中包含所有的Security过滤器

两大重要接口说明

  • 在我们使用Spring Security的过程,我们自定义开发有两个非常重要的接口,我们详细来学习一下

UserDetailsService接口分析

  • 我们上面的环境,什么也没有配置的情况下,认证的账户和密码都是security生成的,我们在实际项目中,这些隐私数据不可能寄托于内存噻,这时候我们就不能采用他的这种方式,而是要重写默认的认证方法

  • 我们刚刚在上面说到了一个过滤器:"UsernamePasswordAuthenticationFilter" 的doFilter方法

    • 对登录的POSt请求表单做拦截,校验用户名和密码.

  • UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter我们也去看看

 

 

 

  • security的认证流程大致就是

    1. 执行"UsernamePasswordAuthenticationFilter" 的doFilter方法

    2. 如果成功,则调用父类的 "successfulAuthentication"方法

    3. 如果失败,则调用父类的 "unsuccessfulAuthentication"方法

  • 自定义开发的步奏为

    1. 创建一个类实现UserDetailsService接口,重写loadUserByUsername方法

      1. 连接数据库,查询用户信息,封装并返回Security提供的User对象

    2. 创建类继承UsernamePasswordAuthenticationFilter,并重写三个方法

      1. attemptAuthentication():用户认证

      2. successfulAuthentication():认证成功后调用

      3. unsuccessfulAuthentication():认证失败后调用

PasswordEncoder接口分析

主要用于自定义开发中,查询用户数据封装User时,User属性密码的加密

  • PasswordEncoder接口一共有三个方法

    • String encode(CharSequence rawPassword);

      • 表示吧参数按照默认的解析规则进行解析

    • boolean matches(CharSequence rawPassword, String encodedPassword);

      • 第一个参数:表单提交的密码

      • 第二个参数:数据库的密码

      • 两个密码做匹配,返回匹配结果布尔值

    • default boolean upgradeEncoding(String encodedPassword) {return false;}

      • 如果机械的密码能够在财经系解析且达到更安全的结果返回true

      • 否则返回false,默认返回false

PasswordEncoder的接口实现类:BCryptPasswordEncoder

  • BCryptPasswordEncoder是Spring Security官方推荐使用的密码解析器

  • 通过new 直接创建对象,调用父接口的方法完成密码加密与匹配

Spring Security Web权限方案

用户名和密码的自定义

默认Security的用户名为:"user",密码为项目启动时在日志中打印的密码,这在企业开发中肯定是不行的,下面我们由浅入深的来说说这个账户和密码的几种设置方式,当然最终的账户名和密码都是要落地到数据库中,只是拓宽一下大家的视野,知道有这么个东西

  • 第一种方式:通过配置文件(测试接口时可用)

  • 此时我们访问项目任一接口,都需要使用该用户信息登录后方可访问

  • 第二种方式:通过配置类(测试接口时可用)

  • 第三种方式:自定义编写实现类(企业开发)

    • 首先改造配置文件类如下

  • 然后编写UserDetailsService接口的实现类,重写方法

    • 可见方法的返回值是:UserDetails,我们发现是个接口,查看他的所有实现类,只有User

    • 查看User类的构造方法,创建User对象返回

认证请求相关设置

  • 目前我们的登陆有点过于简陋,且不是自定义的,这肯定不行,那么就引申出一揽子的配置

我先说一下我们会做一个Demo,主要的目的是我们熟悉关于Security的一些配置

  • 这个是我们的Controller

@RestController
@RequestMapping("/test")
public class TestController {
  
    @GetMapping("/hello")
    public String hello(){ return "Hello Security"; }
​
    @PostMapping("/login")
    public String login(){ return "Hello Login"; }
​
    @GetMapping("/test1")
    public String test1(){ return "good bye 欢迎下次光临"; }
​
    @GetMapping("/test2")
    public String test2(){ return "Test 2"; }
}
  • 这是我们自定义的接口实现类

    • 给ninja2账户赋予A1权限 和role1角色

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
​
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //这里可进行数据库查询,封装User对象
        //...
//模拟数据库返回的数据进行User对象封装
        //该用户的权限集合
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("A1,ROLE_role1");
        User user = new User("ninja2", new BCryptPasswordEncoder().encode("ninja2"), auths);
        return user;
    }
}
  • 下面是我们Security的配置类

    • 设置绑定自定义的认证接口

    • 这是一些自定义的页面和接口

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Qualifier("userDetailsServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;
​
    @Bean
    PasswordEncoder initPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    //重写这个方法,将自定义的用户名和密码以及角色set到内存中
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //密码解析器
//        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//        String password = bCryptPasswordEncoder.encode("ninja1");
        //将配置文件配置的信息删了,在此手动将用户名、密码、角色全塞到内存中保存
//        auth.inMemoryAuthentication().withUser("ninja1").password(password).roles("admin");
//指定用户认证业务接口和密码解析器
        auth.userDetailsService(userDetailsService).passwordEncoder(initPasswordEncoder());
    }
​
    //配置认证相关信息
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义自己编写的页面
        http.formLogin()
                .loginPage("/login.html") //配置自定义的登陆页面
                .loginProcessingUrl("/user/login") //设置登陆访问路径url
                .defaultSuccessUrl("/success.html").permitAll(); //登陆成功之后路径跳转
        //认证请求路径相关配置
        http.authorizeRequests()
                .antMatchers("/test/login").permitAll() //请求无责放行
//基于权限的控制
                //[hasAuthority类型]:该接口只有拥有A1权限的用户才可以访问
//                .antMatchers("/test/test1").hasAuthority("A1")
                //[hasAnyAuthority]:改接口只要拥有A1,B1中任一权限的用户可以访问
//                .antMatchers("/test/test1").hasAnyAuthority("A1,B1")
//基于角色的控制
                //可以查查看源码,这个和权限不一样:return "hasRole('ROLE_" + role + "')";
                //这儿我们设置该接口的访问角色为role1,实际上我们的用户的角色标识应该为:ROLE_role1
                //[hasRole]:改接口只有拥有ROLE_role1角色的用户可以访问
//                .antMatchers("/test/test1").hasRole("role1")
                //[hasAnyRole]:改接口只有拥有ROLE_role1、ROLE_role1中任一角色的用户可以访问
                .antMatchers("/test/test1").hasAnyRole("role1,role2")
​
​
                .anyRequest().authenticated(); //其他请求需要认证才放行
        //关闭csrf防护配置
        http.csrf().disable();
        //配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");
        //退出
        http.logout().logoutUrl("/logout")
                .logoutSuccessUrl("/test/test1").permitAll();
    }
}
  • 大部分的代码,注释应该就能解释清楚,唯一需要注意的是

    • 基于权限的两种控制方式的区别

      • hasAuthority

      • hasAnyAuthority

    • 基于角色的两种控制方式的区别

      • hasRole

      • hasAnyRole

  • 我们为ninja2用户配置了A1权限以及role1角色,想调试这些功能,可以更改给ninja2授予的权限和角色进行调试

  • Demo大致功能流程为:

    • 访问任何接口,首页跳转到/login.html

      • 输入自定义账号密码:ninja2 / ninja2 登陆

      • 登陆状态下访问test2接口

      • 登陆成功页,点击退出,再次访问test2接口,会跳转登录页

      • 至于更多的权限校验,以及那四种模式我这里就不多说了,我最后会写一个比较完整的Demo,实现所有的功能

注解的使用

@Secured()

  • 使用之前需要先开启,在启动类上使用注解开启

    • @EnableGlobalMethodSecurity(securedEnabled = true)

  • 该注解用于Controller方法上,用于保护该接口只被具有某角色的用户访问

  • 注意这里匹配的字符串需要添加前缀 "ROLE_"

    • @Secured({"ROLE_role1","ROLE_role2"})

@PreAuthorize()

  • 使用之前需要先开启,在启动类上使用注解开启

    • @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)

  • 该注解用于Controller方法上,用于方法访问前的权限验证

    • 该注解有四个值值得注意一下,这也是之前我们的配置中有说明的部分

    • //    @PreAuthorize("hasRole('Role_role1')")
      //    @PreAuthorize("hasAnyRole('Role_role1','Role_role2')")
      //    @PreAuthorize("hasAuthority('A1)")
      //    @PreAuthorize("hasAnyAuthority('A1,A2')")

@PostAuthorize()

  • 使用之前需要先开启,在启动类上使用注解开启

    • @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)

  • 该注解用于Controller方法上,用于方法访问后的权限验证,使用不多

  • 和@PreAuthorize()直接使用方法一致

@PostFilter()

  • 只要做数据过滤,如下所示

  • 以下代码的中的filterObject是方法返回值List中的遍历对象对象

    • 我们使用 == 做过滤条件,最后返回为true的数据才会被留下来

      • 当然也可以使用其他匹配符或者运算表达式,比如大于 小于 不等于等等...

    • 我们用浏览器测试看看

      •  

         

  • @PostAuthorize("hasAnyAuthority('A1,A2')")
    @PostFilter("filterObject.username == 'zhangsan'")
    @GetMapping("/test2")
    public List test2() {
        List<Ninja> list = new ArrayList<>();
        list.add(new Ninja(1, "zhangsan", "打球"));
        list.add(new Ninja(2, "lisi", "开摩托"));
        return list;
    }

@PreFilter()

  • 进入控制器之前对数据进行过滤,和@PostFilter()注解使用方式一致

  • 只会留下匹配结果为true的数据,然后进入到方法里

  • 比如:@PreFilter(value = "filterObject.id % 2 == 0")

    • 我们参数是一组对象集合,经过这个过滤器后

    • 我们的集合中对象的id%2 == 0的对象才会被留下来,进入到方法里面

权限表达式

Security 权限相关文档

security + cookie 实现免登陆(源码分析)

第一次认证请求流程源码分析

  • 首先我们根据上图查看UsernamePasswordAuthenticationFilter相关的源码

    • 我要看的是它的父类 "AbstractAuthenticationProcessingFilter" 的一个"doFilter"方法

    • "doFilter"方法中对于认证结果分别都有自己的处理方法

      • unsuccessfulAuthentication(request, response, failed);

      • successfulAuthentication(request, response, chain, authResult);

    • successfulAuthentication就是认证通过后调用的方法,我们点进去看看

    • 可以看到理由有一个操作方法是:

      • rememberMeServices.loginSuccess(request, response, authResult);
    • 然后我们点击:"rememberMeServices",查看该Service的定义,发现如下

      • private RememberMeServices rememberMeServices = new NullRememberMeServices();
      • RememberMeServices 是一个接口

      • NullRememberMeServices是其实现,但是其实现并没有给出

        • public class NullRememberMeServices implements RememberMeServices {
             // ~ Methods
             // ========================================================================================================
          public Authentication autoLogin(HttpServletRequest request,
                   HttpServletResponse response) {
                return null;
             }
          ​
             public void loginFail(HttpServletRequest request, HttpServletResponse response) {
             }
          ​
             public void loginSuccess(HttpServletRequest request, HttpServletResponse response,
                   Authentication successfulAuthentication) {
             }
          }
      • 然后我们在AbstractAuthenticationProcessingFilter中发现了对RememberMeServices的set方法

        • 猜想,应该是这里替换了初始化的实现类

        • 我们将关注点定位到RememberMeServices 的另一个实现:"AbstractRememberMeServices"

        • 观察他的方法:"loginSuccess"

          • @Override
            public final void loginSuccess(HttpServletRequest request,
                  HttpServletResponse response, Authentication successfulAuthentication) {
            ​
               if (!rememberMeRequested(request, parameter)) {
                  logger.debug("Remember-me login not requested.");
                  return;
               }
               onLoginSuccess(request, response, successfulAuthentication);
            }
        • 发现调用 :"onLoginSuccess()"方法,发现该方法是该抽象类里面的一个接口,我们找其实现

          • 分别是:PersistentTokenBasedRememberMeServices、TokenBasedRememberMeServices

          • 我们首先查看一下:"PersistentTokenBasedRememberMeServices"的实现

            protected void onLoginSuccess(HttpServletRequest request,
                  HttpServletResponse response, Authentication successfulAuthentication) {
               String username = successfulAuthentication.getName();
               logger.debug("Creating new persistent login for user " + username);
               PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
                     username, generateSeriesData(), generateTokenData(), new Date());
               try {
                 //发现图中的tokenRepository在将Token写入数据库
                  tokenRepository.createNewToken(persistentToken);
                 //然后在写入Cookie
                  addCookie(persistentToken, request, response);
               }
               catch (Exception e) {
                  logger.error("Failed to save persistent token ", e);
               }
            }
        • 然后我们查看该 "tokenRepository"的定义

        • private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
        • PersistentTokenRepository是一个接口,默认使用的是基于内存的,我们看看他所有的实现

          • InMemoryTokenRepositoryImpl (默认基于内存)

          • JdbcTokenRepositoryImpl (基于数据库连接,这好像就是我们要找的玩意儿)

        • 我们查看:"JdbcTokenRepositoryImpl ",发现很多默认的sql语句

          • /** Default SQL for creating the database table to store the tokens */
            public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
                  + "token varchar(64) not null, last_used timestamp not null)";
            /** The default SQL used by the <tt>getTokenBySeries</tt> query */
            public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
            /** The default SQL used by <tt>createNewToken</tt> */
            public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
            /** The default SQL used by <tt>updateToken</tt> */
            public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
            /** The default SQL used by <tt>removeUserTokens</tt> */
            public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";

             

          • 这sql写的很明显,就是一个表的定义和增删改查,security都为我们配置好了

            • 或许只需要一个开关,这些热插拔的组件应该就可以使用上

          • 到了这里,上图中的这些步骤都完成了

            • 第一步:请求认证

            • 第二步:认证成功

            • 第三步:向Cookie中写入Token

            • 第三步:使用RemeberMeService 操作 TokenRepository写入Token到数据库

免登陆认证请求流程源码分析

security中有一个拦截器是专门为此而生的:"RememberMeAuthenticationFilter"

  • 我们查看其"doFilter"方法,发现有这么一个代码片段

    • Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
            response);
  • 我们查看:"autoLogin"的实现,也是有两个,根据上面的经验,我们直接走AbstractRememberMeServices类的实现

    • 其他的一些判断我们浏览即可,我们只看核心代码

    • try {
         String[] cookieTokens = decodeCookie(rememberMeCookie);
        //检查cookie的有效性和操作tokenRepository查询数据库的Token是否一致
         user = processAutoLoginCookie(cookieTokens, request, response);
        //该check只做判断,如果不符合会抛出异常
         userDetailsChecker.check(user);
         logger.debug("Remember-me cookie accepted");
         return createSuccessfulAuthentication(request, user);

操作Demo实现免登陆

  • 第一步:操刀security配置类

    • 注入数据源

    • 容器注入PersistentTokenRepository 对象

    • 配置基于数据库的免登陆认证

@Autowired
private DataSource dataSource;
​
@Bean
public PersistentTokenRepository persistentTokenRepository(){
    JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
    tokenRepository.setDataSource(dataSource); // 设置数据源
    //自动建表,就是我们看到那写默认的sql中的建表语句
    //如果不自动建表可以将那些sql拿出来,手动在数据库创建表
    tokenRepository.setCreateTableOnStartup(true);
    return tokenRepository;
}
​
//配置认证相关信息
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      //配置基于数据库的免登陆认证
        http.rememberMe().tokenRepository(persistentTokenRepository()) //组件热插拔
                .tokenValiditySeconds(50) //设置有效期
                .userDetailsService(userDetailsService); //组件热插拔
        //自定义自己编写的页面
        http.formLogin()
                .loginPage("/login.html") //配置自定义的登陆页面
                .loginProcessingUrl("/user/login") //设置登陆访问路径url
                .defaultSuccessUrl("/success.html").permitAll(); //登陆成功之后跳转哪个路径
        //认证请求路径相关配置
        http.authorizeRequests()
                .antMatchers("/test/login").permitAll() //请求无责放行
                .anyRequest().authenticated(); //其他请求需要认证才放行
        //关闭csrf防护配置
        http.csrf().disable();
        //配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");
        //退出
        http.logout().logoutUrl("/logout")
                .logoutSuccessUrl("/test/test1").permitAll();
    }
  • 第二步:页面修改

  • 页面新增一个checkbox的输入框

  • name 必须为remeber-me

CSRF & XSRF

  • CSRF利用的是网站对用户网页浏览器的信任,伪造权限认证数据,骗取服务器的放行。获取服务器资源的一种攻击手段

  • Spring Security自 4.0版本开始,默认情况下就开启CSRF保护

    • CsrfFilter过滤器就是为此而生

    • 但是只针对 PATCH、POST、UPDATE、DELETE类型的请求

  • 原理就是

  • 在登陆的表单中新增一个隐藏的输入框

    • name必须为:"_csrf"

    • value,在登陆后的值为Security自动授予

  • 服务器端无需任何处理,自动开启Csrf,你只要不去关闭即可

  • 用户使用账号/密码登陆,Security会生成一个Token,并将其返回给页面,并在Sessin中记录

  • 用户下次 访问时,那个隐藏的标签给我吧名为_csrf的标签的值带上

  • 服务端CsrfFilter过滤器会对请求中的Token和服务端的Token做比较,判断请求是否合法

.

posted @ 2021-02-07 23:49  鞋破露脚尖儿  阅读(384)  评论(0编辑  收藏  举报