zoukankan      html  css  js  c++  java
  • 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做比较,判断请求是否合法

    .

  • 相关阅读:
    linux下Tomcat配置提示权限不够解决办法
    Linux 生成SSL证书 供 nginx使用
    mysql存储emoji表情报错的处理方法【更改编码为utf8mb4】
    Linux Mysql 备份与还原
    Linux 安装Mysql
    Linux 卸载Mysql
    Linux yum安装java环境
    InMemoryUploadedFile对象复制到磁盘中的临时路径
    在django中使用(配置)celery
    使用ffmpeg以mp4的格式保存视频
  • 原文地址:https://www.cnblogs.com/msi-chen/p/14387227.html
Copyright © 2011-2022 走看看