欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring Security 解析(一) —— 授权过程

程序员文章站 2022-07-01 23:17:24
Spring Security 解析(一) —— 授权过程   在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决定先把Spring Security 、Spring Security Oauth2 等权限、认证相关的内容、原理及设计学 ......

spring security 解析(一) —— 授权过程

  在学习spring cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决定先把spring security 、spring security oauth2 等权限、认证相关的内容、原理及设计学习并整理一遍。本系列文章就是在学习的过程中加强印象和理解所撰写的,如有侵权请告知。

项目环境:

  • jdk1.8
  • spring boot 2.x
  • spring security 5.x

一、 一个简单的security demo

1、 自定义的userdetailsservice实现

  自定义myuserdetailsuserservice类,实现 userdetailsservice 接口的 loaduserbyusername()方法,这里就简单的返回一个spring security 提供的 user 对象。为了后面方便演示spring security 的权限控制,这里使用authorityutils.commaseparatedstringtoauthoritylist("admin") 设置了user账号有一个admin的角色权限信息。实际项目中可以在这里通过访问数据库获取到用户及其角色、权限信息。

@component
public class myuserdetailsuserservice implements userdetailsservice {
    @override
    public userdetails loaduserbyusername(string username) throws usernamenotfoundexception {
        // 不能直接使用 创建 bcryptpasswordencoder 对象来加密, 这种加密方式 没有 {bcrypt}  前缀,
        // 会导致在  matches 时导致获取不到加密的算法出现
        // java.lang.illegalargumentexception: there is no passwordencoder mapped for the id "null"  问题
        // 问题原因是 spring security5 使用 delegatingpasswordencoder(委托)  替代 nooppasswordencoder,
        // 并且 默认使用  bcryptpasswordencoder 加密(注意 delegatingpasswordencoder 委托加密方法bcryptpasswordencoder  加密前  添加了加密类型的前缀)  https://blog.csdn.net/alinyua/article/details/80219500
        return new user("user",  passwordencoderfactories.createdelegatingpasswordencoder().encode("123456"), authorityutils.commaseparatedstringtoauthoritylist("admin"));
    }
}

  注意spring security 5 开始没有使用 nooppasswordencoder作为其默认的密码编码器,而是默认使用 delegatingpasswordencoder 作为其密码编码器,其 encode 方法是通过 密码编码器的名称作为前缀 + 委托各类密码编码器来实现encode的。

public string encode(charsequence rawpassword) {
        return "{" + this.idforencode + "}" + this.passwordencoderforencode.encode(rawpassword);
    }

  这里的 idforencode 就是密码编码器的简略名称,可以通过
passwordencoderfactories.createdelegatingpasswordencoder()
内部实现看到默认是使用的前缀是 bcrypt 也就是 bcryptpasswordencoder

public class passwordencoderfactories {
    public static passwordencoder createdelegatingpasswordencoder() {
        string encodingid = "bcrypt";
        map<string, passwordencoder> encoders = new hashmap();
        encoders.put(encodingid, new bcryptpasswordencoder());
        encoders.put("ldap", new ldapshapasswordencoder());
        encoders.put("md4", new md4passwordencoder());
        encoders.put("md5", new messagedigestpasswordencoder("md5"));
        encoders.put("noop", nooppasswordencoder.getinstance());
        encoders.put("pbkdf2", new pbkdf2passwordencoder());
        encoders.put("scrypt", new scryptpasswordencoder());
        encoders.put("sha-1", new messagedigestpasswordencoder("sha-1"));
        encoders.put("sha-256", new messagedigestpasswordencoder("sha-256"));
        encoders.put("sha256", new standardpasswordencoder());
        return new delegatingpasswordencoder(encodingid, encoders);
    }
}

2、 设置spring security配置

  定义springsecurityconfig 配置类,并继承websecurityconfigureradapter覆盖其configure(httpsecurity http) 方法。

@configuration
@enablewebsecurity //1
public class springsecurityconfig extends websecurityconfigureradapter {

    @override
    protected void configure(httpsecurity http) throws exception {
        http.formlogin()  //2
            .and()
                .authorizerequests() //3
                .antmatchers("/index","/").permitall() //4
                .anyrequest().authenticated(); //6
    }
}

配置解析:

  • @enablewebsecurity 查看其注解源码,主要是引用websecurityconfiguration.class 和 加入了@enableglobalauthentication 注解 ,这里就不介绍了,我们只要明白添加 @enablewebsecurity 注解将开启 security 功能。
  • formlogin() 使用表单登录(默认请求地址为 /login),在spring security 5 里其实已经将旧版本默认的 httpbasic() 更换成 formlogin() 了,这里为了表明表单登录还是配置了一次。
  • authorizerequests() 开始请求权限配置
  • antmatchers() 使用ant风格的路径匹配,这里配置匹配 / 和 /index
  • permitall() 用户可任意访问
  • anyrequest() 匹配所有路径
  • authenticated() 用户登录后可访问

3、 配置html 和测试接口

   在 resources/static 目录下新建 index.html , 其内部定义一个访问测试接口的按钮

<!doctype html>
<html lang="en" >
<head>
    <meta charset="utf-8">
    <title>欢迎</title>
</head>
<body>
        spring security 欢迎你!
        <p> <a href="/get_user/test">测试验证security 权限控制</a></p>
</body>
</html>

  创建 rest 风格的获取用户信息接口

@restcontroller
public class testcontroller {

    @getmapping("/get_user/{username}")
    public string getuser(@pathvariable  string username){
        return username;
    }
}

4、 启动项目测试

1、访问 localhost:8080 无任何阻拦直接成功

Spring Security 解析(一) —— 授权过程

2、点击测试验证权限控制按钮 被重定向到了 security默认的登录页面
Spring Security 解析(一) —— 授权过程

3、使用 myuserdetailsuserservice定义的默认账户 user : 123456 进行登录后成功跳转到 /get_user 接口

Spring Security 解析(一) —— 授权过程


二、 @enablewebsecurity 配置解析

   还记得之前讲过 @enablewebsecurity 引用了 websecurityconfiguration 配置类 和 @enableglobalauthentication 注解吗? 其中 websecurityconfiguration 就是与授权相关的配置,@enableglobalauthentication 配置了 认证相关的我们下节再细讨。

   首先我们查看 websecurityconfiguration 源码,可以很清楚的发现 springsecurityfilterchain() 方法。

    @bean(name = abstractsecuritywebapplicationinitializer.default_filter_name)
    public filter springsecurityfilterchain() throws exception {
        boolean hasconfigurers = websecurityconfigurers != null
                && !websecurityconfigurers.isempty();
        if (!hasconfigurers) {
            websecurityconfigureradapter adapter = objectobjectpostprocessor
                    .postprocess(new websecurityconfigureradapter() {
                    });
            websecurity.apply(adapter);
        }
        return websecurity.build(); //1
    }

  这个方法首先会判断 websecurityconfigurers 是否为空,为空加载一个默认的 websecurityconfigureradapter对象,由于自定义的 springsecurityconfig 本身是继承 websecurityconfigureradapter对象 的,所以我们自定义的 security 配置肯定会被加载进来的(如果想要了解如何加载进来可以看下websecurityconfiguration.setfilterchainproxysecurityconfigurer() 方法)。

   我们看下 websecurity.build() 方法实现 实际调用的是 abstractconfiguredsecuritybuilder.dobuild() 方法,其方法内部实现如下:

@override
    protected final o dobuild() throws exception {
        synchronized (configurers) {
            buildstate = buildstate.initializing;

            beforeinit();
            init();

            buildstate = buildstate.configuring;

            beforeconfigure();
            configure();

            buildstate = buildstate.building;

            o result = performbuild(); // 1 创建 defaultsecurityfilterchain (security filter 责任链 ) 

            buildstate = buildstate.built;

            return result;
        }
    }

   我们把关注点放到 performbuild() 方法,看其实现子类 httpsecurity.performbuild() 方法,其内部排序 filters 并创建了 defaultsecurityfilterchain 对象。

    @override
    protected defaultsecurityfilterchain performbuild() throws exception {
        collections.sort(filters, comparator);
        return new defaultsecurityfilterchain(requestmatcher, filters);
    }

   查看defaultsecurityfilterchain 的构造方法,我们可以看到有记录日志。

public defaultsecurityfilterchain(requestmatcher requestmatcher, list<filter> filters) {
        logger.info("creating filter chain: " + requestmatcher + ", " + filters); // 按照正常情况,我们可以看到控制台输出 这条日志 
        this.requestmatcher = requestmatcher;
        this.filters = new arraylist<>(filters);
    }

   我们可以回头看下项目启动日志。可以看到下图明显打印了 这条日志,并且把所有 filter名都打印出来了。==(请注意这里打印的 filter 链,接下来我们的所有授权过程都是依靠这条filter 链展开 )==
Spring Security 解析(一) —— 授权过程

  那么还有个疑问: httpsecurity.performbuild() 方法中的 filters 是怎么加载的呢? 这个时候需要查看 websecurityconfigureradapter.init() 方法,这个方法内部 调用 gethttp() 方法返回 httpsecurity 对象(看到这里我们应该能想到 filters 就是这个方法中添加好了数据),具体如何加载的也就不介绍了。

public void init(final websecurity web) throws exception {
        final httpsecurity http = gethttp(); // 1 
        web.addsecurityfilterchainbuilder(http).postbuildaction(new runnable() {
            public void run() {
                filtersecurityinterceptor securityinterceptor = http
                        .getsharedobject(filtersecurityinterceptor.class);
                web.securityinterceptor(securityinterceptor);
            }
        });
    }

   用了这么长时间解析 @enablewebsecurity ,其实最关键的一点就是创建了 defaultsecurityfilterchain 也就是我们常 security filter 责任链,接下来我们围绕这个 defaultsecurityfilterchain 中 的 filters 进行授权过程的解析。

三、 授权过程解析

  security的授权过程可以理解成各种 filter 处理最终完成一个授权。那么我们再看下之前 打印的filter 链,这里为了方便,再次贴出图片
Spring Security 解析(一) —— 授权过程

  这里我们只关注以下几个重要的 filter :

  • securitycontextpersistencefilter
  • usernamepasswordauthenticationfilter (abstractauthenticationprocessingfilter)
  • basicauthenticationfilter
  • anonymousauthenticationfilter
  • exceptiontranslationfilter
  • filtersecurityinterceptor

1、securitycontextpersistencefilter

  securitycontextpersistencefilter 这个filter的主要负责以下几件事:

  • 通过 (securitycontextrepository)repo.loadcontext() 方法从请求session中获取 securitycontext(security 上下文 ,类似 applicaitoncontext ) 对象,如果请求session中没有默认创建一个 authentication(认证的关键对象,由于本节只讲授权,暂不介绍) 属性为 null 的 securitycontext 对象
  • securitycontextholder.setcontext() 将 securitycontext 对象放入 securitycontextholder进行管理(securitycontextholder默认使用threadlocal 策略来存储认证信息)
  • 由于在 finally 里实现 会在最后通过 securitycontextholder.clearcontext() 将 securitycontext 对象 从 securitycontextholder中清除
  • 由于在 finally 里实现 会在最后通过 repo.savecontext() 将 securitycontext 对象 放入session中
httprequestresponseholder holder = new httprequestresponseholder(request,
                response);
        //从session中获取securitycontxt 对象,如果session中没有则创建一个 authtication 属性为 null 的securitycontext对象
        securitycontext contextbeforechainexecution = repo.loadcontext(holder); 

        try {
            // 将 securitycontext 对象放入 securitycontextholder进行管理 (securitycontextholder默认使用threadlocal 策略来存储认证信息)
             securitycontextholder.setcontext(contextbeforechainexecution);

             chain.dofilter(holder.getrequest(), holder.getresponse());

        }
        finally {
            securitycontext contextafterchainexecution = securitycontextholder
                    .getcontext();
            
            // 将 securitycontext 对象 从 securitycontextholder中清除
            securitycontextholder.clearcontext();
            // 将 securitycontext 对象 放入session中
            repo.savecontext(contextafterchainexecution, holder.getrequest(),
                    holder.getresponse());
            request.removeattribute(filter_applied);

            if (debug) {
                logger.debug("securitycontextholder now cleared, as request processing completed");
            }
        }

  我们在 securitycontextpersistencefilter 中打上断点,启动项目,访问 localhost:8080 , 来debug看下实现:

Spring Security 解析(一) —— 授权过程
   我们可以清楚的看到创建了一个authtication 为null 的 securitycontext对象,并且可以看到请求调用的filter链具体有哪些。接下来看下 finally 内部处理

Spring Security 解析(一) —— 授权过程

   你会发现这里的securitycontxt中的 authtication 是一个名为 anonymoususer (匿名用户)的认证信息,这是因为 请求调用到了 anonymousauthenticationfilter , security默认创建了一个匿名用户访问。

2、usernamepasswordauthenticationfilter (abstractauthenticationprocessingfilter)

  看filter字面意思就知道这是一个通过获取请求中的账户密码来进行授权的filter,按照惯例,整理了这个filter的职责:

  • 通过 requiresauthentication()判断 是否以post 方式请求 /login
  • 调用 attemptauthentication() 方法进行认证,内部创建了 authenticated 属性为 false(即未授权)的usernamepasswordauthenticationtoken 对象, 并传递给 authenticationmanager().authenticate() 方法进行认证,认证成功后 返回一个 authenticated = true (即授权成功的)usernamepasswordauthenticationtoken 对象
  • 通过 sessionstrategy.onauthentication() 将 authentication 放入session中
  • 通过 successfulauthentication() 调用 authenticationsuccesshandler 的 onauthenticationsuccess 接口 进行成功处理( 可以 通过 继承 authenticationsuccesshandler 自行编写成功处理逻辑 )successfulauthentication(request, response, chain, authresult);
  • 通过 unsuccessfulauthentication() 调用authenticationfailurehandler 的 onauthenticationfailure 接口 进行失败处理(可以通过继承authenticationfailurehandler 自行编写失败处理逻辑 )

  我们再看下官方源码的处理逻辑:

// 1 abstractauthenticationprocessingfilter 的 dofilter 方法
public void dofilter(servletrequest req, servletresponse res, filterchain chain)
            throws ioexception, servletexception {

        httpservletrequest request = (httpservletrequest) req;
        httpservletresponse response = (httpservletresponse) res;

        // 2 判断请求地址是否是  /login 和 请求方式为 post  (usernamepasswordauthenticationfilter 构造方法 确定的)
        if (!requiresauthentication(request, response)) {
            chain.dofilter(request, response);
            return;
        }
        authentication authresult;
        try {
            
            // 3 调用 子类  usernamepasswordauthenticationfilter 的 attemptauthentication 方法
            // attemptauthentication 方法内部创建了 authenticated 属性为 false (即未授权)的 usernamepasswordauthenticationtoken 对象, 并传递给 authenticationmanager().authenticate() 方法进行认证,
            //认证成功后 返回一个 authenticated = true (即授权成功的) usernamepasswordauthenticationtoken 对象 
            authresult = attemptauthentication(request, response);
            if (authresult == null) {
                return;
            }
            // 4 将认证成功的 authentication 存入session中
            sessionstrategy.onauthentication(authresult, request, response);
        }
        catch (internalauthenticationserviceexception failed) {
             // 5 认证失败后 调用 authenticationfailurehandler 的 onauthenticationfailure 接口 进行失败处理( 可以 通过 继承 authenticationfailurehandler 自行编写失败处理逻辑 )
            unsuccessfulauthentication(request, response, failed);
            return;
        }
        catch (authenticationexception failed) {
            // 5 认证失败后 调用 authenticationfailurehandler 的 onauthenticationfailure 接口 进行失败处理( 可以 通过 继承 authenticationfailurehandler 自行编写失败处理逻辑 )
            unsuccessfulauthentication(request, response, failed);
            return;
        }
        
        ......
         // 6 认证成功后 调用 authenticationsuccesshandler 的 onauthenticationsuccess 接口 进行失败处理( 可以 通过 继承 authenticationsuccesshandler 自行编写成功处理逻辑 )
        successfulauthentication(request, response, chain, authresult);
    }

  从源码上看,整个流程其实是很清晰的:从判断是否处理,到认证,最后判断认证结果分别作出认证成功和认证失败的处理。

  debug 调试下看 结果,这次我们请求 localhast:8080/get_user/test , 由于没权限会直接跳转到登录界面,我们先输入错误的账号密码,看下认证失败是否与我们总结的一致。

Spring Security 解析(一) —— 授权过程

  结果与预想时一致的,也许你会奇怪这里的提示为啥时中文,这就不得不说security 5 开始支持 中文,说明咋中国程序员在世界上越来越有地位了!!!

   这次输入正确的密码, 看下返回的authtication 对象信息:

Spring Security 解析(一) —— 授权过程

   可以看到这次成功返回一个 authticated = ture ,没有密码的 user账户信息,而且还包含我们定义的一个admin权限信息。放开断点,由于security默认的成功处理器是simpleurlauthenticationsuccesshandler ,这个处理器会重定向到之前访问的地址,也就是 localhast:8080/get_user/test。 至此整个流程结束。不,我们还差一个,session,我们从浏览器cookie中看到 session:

Spring Security 解析(一) —— 授权过程

3、basicauthenticationfilter

  basicauthenticationfilter 与usernameauthticationfilter类似,不过区别还是很明显,basicauthenticationfilter 主要是从header 中获取 authorization 参数信息,然后调用认证,认证成功后最后直接访问接口,不像usernameauthticationfilter过程一样通过authenticationsuccesshandler 进行跳转。这里就不在贴代码了,想了解的同学可以直接看源码。不过有一点要注意的是,basicauthenticationfilter 的 onsuccessfulauthentication() 成功处理方法是一个空方法。

   为了试验basicauthenticationfilter, 我们需要将 springsecurityconfig 中的formlogin()更换成httpbasic()以支持basicauthenticationfilter,重启项目,同样访问
localhast:8080/get_user/test,这时由于没权限访问这个接口地址,页面上会弹出一个登陆框,熟悉security4的同学一定很眼熟吧,同样,我们输入账户密码后,看下debug数据:

Spring Security 解析(一) —— 授权过程

   这时,我们就能够获取到 authorization 参数,进而解析获取到其中的账户和密码信息,进行认证,我们查看认证成功后返回的authtication对象信息其实是和usernamepasswordauthticationfilter中的一致,最后再次调用下一个filter,由于已经认证成功了会直接进入filtersecurityinterceptor 进行权限验证。

4、anonymousauthenticationfilter

  这里为什么要提下 anonymousauthenticationfilter呢,主要是因为在security中不存在没有账户这一说法(这里可能描述不是很清楚,但大致意思是这样的),针对这个security官方专门指定了这个anonymousauthenticationfilter ,用于前面所有filter都认证失败的情况下,自动创建一个默认的匿名用户,拥有匿名访问权限。还记得 在讲解 securitycontextpersistencefilter 时我们看到得匿名 autication信息么?如果不记得还得回头看下哦,这里就不再叙述了。

5、exceptiontranslationfilter

  exceptiontranslationfilter 其实没有做任何过滤处理,但别小看它得作用,它最大也最牛叉之处就在于它捕获authenticationexception 和accessdeniedexception,如果发生的异常是这2个异常 会调用 handlespringsecurityexception()方法进行处理。 我们模拟下 accessdeniedexception(无权限,禁止访问异常)情况,首先我们需要修改下 /get_user 接口:

  • 在controller 上添加
    @enableglobalmethodsecurity(prepostenabled =true) 启用security 方法级别得权限控制
  • 在 接口上添加 @preauthorize("hasrole('user')") 只允许有user角色得账户访问(还记得我们默认得user 账户时admin角色么?)
@restcontroller
@enableglobalmethodsecurity(prepostenabled =true)  // 开启方法级别的权限控制
public class testcontroller {

    @preauthorize("hasrole('user')") //只允许user角色访问
    @getmapping("/get_user/{username}")
    public string getuser(@pathvariable  string username){
        return username;
    }
}

  重启项目,重新访问 /get_user 接口,输入正确的账户密码,发现返回一个 403 状态的错误页面,这与我们之前将的流程时一致的。debug,看下处理:

Spring Security 解析(一) —— 授权过程

  可以明显的看到异常对象是 accessdeniedexception ,异常信息是不允许访问,我们再看下 accessdeniedexception 异常后的处理方法accessdeniedhandler.handle(),进入到了 accessdeniedhandlerimpl 的handle()方法,这个方法会先判断系统是否配置了 errorpage (错误页面),没有的话直接往 response 中设置403 状态码。

Spring Security 解析(一) —— 授权过程

6、filtersecurityinterceptor

  filtersecurityinterceptor 是整个security filter链中的最后一个,也是最重要的一个,它的主要功能就是判断认证成功的用户是否有权限访问接口,其最主要的处理方法就是 调用父类(abstractsecurityinterceptor)的 super.beforeinvocation(fi),我们来梳理下这个方法的处理流程:

  • 通过 obtainsecuritymetadatasource().getattributes() 获取 当前访问地址所需权限信息
  • 通过 authenticateifrequired() 获取当前访问用户的权限信息
  • 通过 accessdecisionmanager.decide() 使用 投票机制判权,判权失败直接抛出 accessdeniedexception 异常
protected interceptorstatustoken beforeinvocation(object object) {
           
        ......
        
        // 1 获取访问地址的权限信息 
        collection<configattribute> attributes = this.obtainsecuritymetadatasource()
                .getattributes(object);

        if (attributes == null || attributes.isempty()) {
        
            ......
            
            return null;
        }

        ......

        // 2 获取当前访问用户权限信息
        authentication authenticated = authenticateifrequired();

    
        try {
            // 3  默认调用affirmativebased.decide() 方法, 其内部 使用 accessdecisionvoter 对象 进行投票机制判权,判权失败直接抛出 accessdeniedexception 异常 
            this.accessdecisionmanager.decide(authenticated, object, attributes);
        }
        catch (accessdeniedexception accessdeniedexception) {
            publishevent(new authorizationfailureevent(object, attributes, authenticated,
                    accessdeniedexception));

            throw accessdeniedexception;
        }

        ......
        return new interceptorstatustoken(securitycontextholder.getcontext(), false,
                    attributes, object);
    }

   整个流程其实看起来不复杂,主要就分3个部分,首选获取访问地址的权限信息,其次获取当前访问用户的权限信息,最后通过投票机制判断出是否有权。

三、 个人总结

  整个授权流程核心的就在于这几次核心filter的处理,这里我用序列图来概况下这个授权流程

Spring Security 解析(一) —— 授权过程(ps: 如果图片展示不清楚,可访问项目的 github 地址)

   本文介绍授权过程的代码可以访问代码仓库中的 security 模块 ,项目的github 地址 : https://github.com/bug9/spring-security

         如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!