首页 > 作文

Spring Security 实现用户名密码登录流程源码详解

更新时间:2023-04-03 22:16:30 阅读: 评论:0

目录
引言探究登录流程校验用户信息保存

引言

你在服务端的安全管理使用了 spring curity,用户登录成功之后,spring curity 帮你把用户信息保存在 ssion 里,但是具体保存在哪里,要是不深究你可能就不知道, 这带来了一个问题,如果用户在前端操作修改了当前用户信息,在不重新登录的情况下,如何获取到最新的用户信息?

探究

无处不在的 authentication

玩过 spring curity 的小伙伴都知道,在 spring curity 中有一个非常重要的对象叫做 authentication,我们可以在任何地方注入 authentication 进而获取到当前登录用户信息,authentication 本身是一个接口,它有很多实现类:

在这众多的实现类中,我们最常用的就是 urnamepasswordauthenticationtoken 了,但是当我们打开这个类的源码后,却发现这个类平平无奇,他只有两个属性、两个构造方法以及若干个 get/t 方法;当然,他还有更多属性在它的父类上。

但是从它仅有的这两个属性中,我们也能大致看出,这个类就保存了我们登录用户的基本信息。那么我们的登录信息是如何存到这两个对象中的?这就要来梳理一下登录流程了。

登录流程

在 spring curity 中,认证与授权的相关校验都是在一系列的过滤器链中完成的,在这一系列的过滤器链中,和认证相关的过滤器就是 urnamepasswordauthenticationfilter::

public class urnamepasswordauthenticationfilter extends abstractauthenticationprocessingfilter {//默认的用户名和密码对应的key    public static final string spring_curity_form_urname_key = "urname";    public static final string spring_curity_form_password_key = "password";//当前过滤器默认拦截的路径        private static final antpathrequestmatcher default_ant_path_request_matcher = new antpathrequestmatcher("/login", "post");    //默认的请求参数名称规定    private string urnameparameter = "urname";    private string passwordparameter = "password";    //默认只能是post请求    private boolean postonly = true;    public urnamepasswordauthenticationfilter() {    //设置默认的拦截路径        super(default_ant_path_request_matcher);    }    public urnamepasswordauthenticationfilter(authenticationmanager authenticationmanager) {       //设置默认的拦截路径,和处理认证的管理器        super(default_ant_path_request_matcher, authenticationmanager);    }    public authentication attemptauthentication(httprvletrequest request, httprvletrespon respon) throws authenticationexception {    //判断请求方式        if (this.postonly && !req到底还能爱多久uest.getmethod().equals("post")) {            throw new authenticationrviceexception("authentication method not supported: " + request.getmethod());        } el {        //从请求参数中获取对应的值            string urname = this.obtainurname(request);            urname = urname != null ? urname : "";            urname = urname.trim();            string password = this.obtainpassword(request);            password = password != null ? password : "";            //构造用户名和密码登录的认证令牌            urnamepasswordauthenticationtoken authrequest = new urnamepasswordauthenticationtoken(urname, password);            //设置details---deltails里面默认存放ssionid和remoteaddr            //authrequest 就是构造好的认证令牌            this.tdetails(request, authrequest);            //校验            //authrequest 就是构造好的认证令牌            return this.getauthenticationmanager().authenticate(authrequest);        }    }    @nullable    protected string obtainpassword(httprvletrequest request) {        return request.getparameter(this.passwordparameter);    }    @nullable    protected string obtainurname(httprvletrequest request) {        return request.getparameter(this.urnameparameter);    }    protected void tdetails(httprvletrequest request, urnamepasswordauthenticationtoken authrequest) {        authrequest.tdetails(this.authenticationdetailssource.builddetails(request));    }    public void turnameparameter(string urnameparameter) {        asrt.hastext(urnameparameter, "urname parameter must not be empty or null");        this.urnameparameter = urnameparameter;    }    public void tpasswordparameter(string passwordparameter) {        asrt.hastext(passwordparameter, "password parameter must not be empty or null");        this.passwordparameter = passwordparameter;    }    public void tpostonly(boolean postonly) {        this.postonly = postonly;    }    public final string geturnameparameter() {        return this.urnameparameter;    }    public final string getpasswordparameter() {        return this.passwordparameter;    }}

根据这段源码我们可以看出:

首先通过 obtainurname 和 obtainpassword 方法提取出请求里边的用户名/密码出来,提取方式就是 request.getparameter ,这也是为什么 spring curity 中默认的表单登录要通过 key/value 的形式传递参数,而不能传递 json 参数,如果像传递 json 参数,修改这里的逻辑即可

获取到请求里传递来的用户名/密码之后,接下来就构造一个 urnamepasswordauthenticationtoken 对象,传入 urname 和 password,urname 对应了 urnamepasswordauthenticationtoken 中的 principal 属性,而 password 则对应了它的 credentials 属性。

public class urnamepasswordauthenticationtoken extends abstractauthenticationtoken {    private static final long rialversionuid = 550l;    private final object principal;    private object credentials;    public urnamepasswordauthenticationtoken(object principal, object credentials) {        super((collection)null);        this.principal = principal;        this.credentials = credentials;        this.tauthenticated(fal);    }    public urnamepasswordauthenticationtoken(object principal, object credentials, collection<? extends grantedauthority> authorities) {        super(authorities);        this.principal = principal;        this.credentials = credentials;        super.tauthenticated(true);    }    public object getcredentials() {        return this.credentials;    }    public object getprincipal() {        return this.principal;    }    public void tauthenticated(boolean isauthenticated) throws illegalargumentexception {        asrt.istrue(!isauthenticated, "cannot t this token to trusted - u constructor which takes a grantedauthority list instead");        super.tauthenticated(fal);    }    public void eracredentials() {        super.eracredentials();        this.credentials = null;    }}

接下来 tdetails 方法给 details 属性赋值,urnamepasswordauthenticationtoken 本身是没有 details 属性的,这个属性在它的父类 abstractauthenticationtoken 中。details 是一个对象,这个对象里边放的是 webauthenticationdetails 实例,该实例主要描述了两个信息,请求的 remoteaddress 以及请求的 ssionid

最后一步,就是调用 authenticate 方法去做校验了。

好了,从这段源码中,大家可以看出来请求的各种信息基本上都找到了自己的位置,找到了位置,这就方便我们未来去获取了。

接下来我们再来看请求的具体校验操作。

校验

在前面的 attemptauthentication 方法中,该方法的最后一步开始做校验,校验操作首先要获取到一个 authenticationmanager,这里拿到的是 providermanager ,所以接下来我们就进入到 providermanagerauthenticate 方法中,当然这个方法也比较长,我这里仅仅摘列出来几个重要的地方:

    public authentication authenticate(authentication authentication) throws authenticationexception {    //获取到主体(用户名)和凭证(密码)组成的一个令牌对象的class类对象        class<? extends authentication> totest = authentication.getclass();        authenticationexception lastexception = null;        authenticationexception parentexception = null;        authentication result = null;        authentication parentresult = null;        int currentposition = 0;        //获取所有可用来校验令牌对象的provider数量        int size = this.providers.size();        //获取迭代器        iterator var9 = this.getproviders().iterator();         //遍历所有provider        while(var9.hasnext()) {            authenticationprovider provider = (authenticationprovider)var9.next();            //判断当前provider是否支持当前令牌对象的校验            if (provider.supports(totest)) {                if (logger.istraceenabled()) {                    log var10000 = logger;                    string var10002 = provider.getclass().getsimplename();                    ++currentposition;                    var10000.trace(logmessage.format("authenticating request with %s (%d/%d)", var10002, currentposition, size));                }                try {                //如果支持就进行认证校验处理                    result = provider.authenticate(authentication);                    //校验成功返回一个新的authentication                    //将原先的主体由用户名换成了urdetails对象                    if (result != null) {                    //拷贝details到新的令牌对象                        this.copydetails(authentication, result);                        break;                    }                } catch (internalauthenticationrviceexception | accountstatuxception var14) {                    this.prepareexception(var14, authentication);                    throw var14;                } catch (authenticationexception var15) {                    lastexception = var15;                }            }        }//认证失败但是 provider 的 parent不为null        if (result == null && this.parent != null) {            try {            //调用 provider 的 parent进行验证--parent就是providermanager                parentresult = this.parent.authenticate(authentication);                result = parentresult;            } catch (providernotfoundexception var12) {            } catch (authenticationexception var13) {                parentexception = var13;                lastexception = var13;            }        }//认证成功        if (result != null) {         //擦除凭证---密码            if (this.eracredentialsafterauthentication && result instanceof credentialscontainer) {                ((credentialscontainer)result).eracredentials();            }//发布认证成功的结果            if (parentresult == null) {               界限和界线的区别 this.eventpublisher.publishauthenticationsuccess(result);            }//返回新生产的令牌对象            return result;        } el {        //认证失败            if (lastexception == null) {                lastexception = new providernotfoundexception(this.messages.getmessage("providermanager.providernotfound", new object[]{totest.getname()}, "no authenticationprovider found for {0}"));            }            if (parentexception == null) {                this.prepareexception((authenticationexception)lastexception, authentication);            }            throw lastexc情人节的故事eption;        }    }

这个方法就比较魔幻了,因为几乎关于认证的重要逻辑都将在这里完成:

首先获取 authentication 的 class,判断当前 provider天气谚语大全 是否支持该 authentication。

如果支持,则调用 provider 的 authenticate方法开始做校验,校验完成后,会返回一个新的authentication。一会来和大家捋这个方法的具体逻辑

这里的 provider 可能有多个,如果 provider 的 authenticate 方法没能正常返回一个authentication,则调用 provider 的 parent 的 authenticate 方法继续校验。

copydetails 方法则用来把旧的 token 的 details 属性拷贝到新的 token 中来。

接下来会调用 eracredentials 方法擦除凭证信息,也就是你的密码,这个擦除方法比较简单,就是将 token 中的credentials 属性置空

最后通过 publishauthenticationsuccess 方法将登录成功的事件广播出去。

大致的流程,就是上面这样,在 for 循环中,第一次拿到的 provider 是一个 anonymousauthenticationprovider,这个 provider 压根就不支持 urnamepasswordauthenticationtoken,也就是会直接在 provider.supports 方法中返回 fal,结束 for 循环,然后会进入到下一个 if 中,直接调用 parent 的 authenticate 方法进行校验。

parent 就是 providermanager,所以会再次回到这个 authenticate 方法中。再次回到 authenticate 方法中,provider 也变成了 daoauthenticationprovider,这个 provider 是支持 urnamepasswordauthenticationtoken 的,所以会顺利进入到该类的 authenticate 方法去执行,而 daoauthenticationprovider 继承自 abstracturdetailsauthenticationprovider 并且没有重写 authenticate 方法,所以 我们最终来到 abstracturdetailsauthenticationprovider#authenticate 方法中:

public authentication authenticate(authentication authentication)throws authenticationexception {string urname = (authentication.getprincipal() == null) ? "none_provided": authentication.getname();ur = retrieveur(urname,(urnamepasswordauthenticationtoken) authentication);preauthenticationchecks.check(ur);additionalauthenticationchecks(ur,(urnamepasswordauthenticationtoken) authentication);postauthenticationchecks.check(ur);//如果用户没有使用过,将其放进缓存中if (!cachewasud) {            this.urcache.puturincache(ur);        }object principaltoreturn = ur;if (forceprincipalasstring) {principaltoreturn = ur.geturname();}return createsuccessauthentication(principaltoreturn, authentication, ur);}

首先从 authentication 提取出登录用户名。

然后通过拿着 urname 去调用 retrieveur 方法去获取当前用户对象,这一步会调用我们自己在登录时候的写的 loadurbyurname 方法,所以这里返回的 ur 其实就是你的登录对象

接下来调用 preauthenticationchecks.check 方法去检验 ur 中的各个账户状态属性是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等

additionalauthenticationchecks 方法则是做密码比对的,好多小伙伴好奇 spring curity 的密码加密之后,是如何进行比较的,看这里就懂了。

最后在 postauthenticationchecks.check 方法中检查密码是否过期。

判断用户是否在缓存中存在,如果不存在,就放入缓存中

接下来有一个 forceprincipalasstring 属性,这个是是否强制将 authentication 中的 principal 属性设置为字符串,这个属性我们一开始在 urnamepasswordauthenticationfilter 类中其实就是设置为字符串的(即 urname),但是默认情况下,当用户登录成功之后, 这个属性的值就变成当前用户这个对象了。之所以会这样,就是因为 forceprincipalasstring 默认为 fal,不过这块其实不用改,就用 fal,这样在后期获取当前用户信息的时候反而方便很多。

最后,通过 createsuccessauthentication 方法构建一个新的 urnamepasswordauthenticationtoken,此时认证主体就由用户名变为了urdetails对象

好了,那么登录的校验流程现在就基本和大家捋了一遍了。那么接下来还有一个问题,登录的用户信息我们去哪里查找?

用户信息保存

要去找登录的用户信息,我们得先来解决一个问题,就是上面我们说了这么多,这一切是从哪里开始被触发的?

我们来到 urnamepasswordauthenticationfilter 的父类 abstractauthenticationprocessingfilter 中,这个类我们经常会见到,因为很多时候当我们想要在 spring curity 自定义一个登录验证码或者将登录参数改为 json 的时候,我们都需自定义过滤器继承自 abstractauthenticationprocessingfilter ,毫无疑问,urnamepasswordauthenticationfilter#attemptauthentication 方法就是在 abstractauthenticationprocessingfilter 类的 dofilter 方法中被触发的:

 private void dofilter(httprvletrequest request, httprvletrespon respon, filterchain chain) throws ioexception, rvletexception { //不需要认证就直接放行        if (!this.requiresauthentication(request, respon)) {            chain.dofilter(request, respon);        } el {            try {            //获取认证的结果---null或者新生产的令牌对象                authentication authenticationresult = this.attemptauthentication(request, respon);               //认证失败                if (authenticationresult == null) {                    return;                }                                    this.ssionstrategy.onauthentication(authenticationresult, request, respon);                if (this.continuechainbeforesuccessfulauthentication) {                    chain.dofilter(request, respon);                }                this.successfulauthentication(request, respon, chain, authenticationresult);            } catch (internalauthenticationrviceexception var5) {                this.logger.error("an internal error occurred while trying to authenticate the ur.", var5);                this.unsuccessfulauthentication(request, respon, var5);            } catch (authenticationexception var6) {                this.unsuccessfulauthentication(request, respon, var6);            }        }    }

从上面的代码中,我们可以看到,当 attemptauthentication 方法被调用时,实际上就是触发了 urnamepasswordauthenticationfilter#attemptauthentication 方法,当登录抛出异常的时候,unsuccessfulauthentication 方法会被调用,而当登录成功的时候,successfulauthentication 方法则会被调用,那我们就来看一看 successfulauthentication 方法:

protected void successfulauthentication(httprvletrequest request,httprvletrespon respon, filterchain chain, authentication authresult)throws ioexception, rvletexception {//将新生产的令牌对象放入spring curity的上下文环境中curitycontextholder.getcontext().tauthentication(authre小学生作文精选sult);remembermervices.loginsuccess(request, respon, authresult);// fire eventif (this.eventpublisher != null) {eventpublisher.publishevent(new interactiveauthenticationsuccesvent(authresult, this.getclass()));}successhandler.onauthenticationsuccess(request, respon, authresult);}

在这里有一段很重要的代码,就是 curitycontextholder.getcontext().tauthentication(authresult); ,登录成功的用户信息被保存在这里,也就是说,在任何地方,如果我们想获取用户登录信息,都可以从 curitycontextholder.getcontext() 中获取到,想修改,也可以在这里修改。

最后大家还看到有一个 successhandler.onauthenticationsuccess,这就是我们在 curityconfig 中配置登录成功回调方法,就是在这里被触发的

当认证失败时,会调用登录失败处理器,并清空上下文环境中的对象

 protected void unsuccessfulauthentication(httprvletrequest request, httprvletrespon respon, authenticationexception failed) throws ioexception, rvletexception {        curitycontextholder.clearcontext();        this.logger.trace("failed to process authentication request", failed);        this.logger.trace("cleared curitycontextholder");        this.logger.trace("handling authentication failure");        this.remembermervices.loginfail(request, respon);        this.failurehandler.onauthenticationfailure(request, respon, failed);    } 

以上就是spring curity 实现用户名密码登录流程源码详解的详细内容,更多关于spring curity 用户名密码登录的资料请关注www.887551.com其它相关文章!

本文发布于:2023-04-03 22:16:27,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/zuowen/8ea850c8f92c025e6810c3391fbc6d17.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

本文word下载地址:Spring Security 实现用户名密码登录流程源码详解.doc

本文 PDF 下载地址:Spring Security 实现用户名密码登录流程源码详解.pdf

标签:方法   对象   令牌   属性
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图