Spring Security架构以及源码详析
前言
现在流行的通用授权框架有apache的shiro和spring家族的spring security,在涉及今天的微服务鉴权时,需要利用我们的授权框架搭建自己的鉴权服务,今天总理了spring security。
spring security 主要实现了authentication(认证,解决who are you? ) 和 access control(访问控制,也就是what are you allowed to do?,也称为authorization)。spring security在架构上将认证与授权分离,并提供了扩展点。
核心对象
主要代码在spring-security-core包下面。要了解spring security,需要先关注里面的核心对象。
securitycontextholder, securitycontext 和 authentication
securitycontextholder 是 securitycontext的存放容器,默认使用threadlocal 存储,意味securitycontext在相同线程中的方法都可用。
securitycontext主要是存储应用的principal信息,在spring security中用authentication 来表示。
获取principal:
object principal = securitycontextholder.getcontext().getauthentication().getprincipal(); if (principal instanceof userdetails) { string username = ((userdetails)principal).getusername(); } else { string username = principal.tostring(); }
在spring security中,可以看一下authentication定义:
public interface authentication extends principal, serializable { collection<? extends grantedauthority> getauthorities(); /** * 通常是密码 */ object getcredentials(); /** * stores additional details about the authentication request. these might be an ip * address, certificate serial number etc. */ object getdetails(); /** * 用来标识是否已认证,如果使用用户名和密码登录,通常是用户名 */ object getprincipal(); /** * 是否已认证 */ boolean isauthenticated(); void setauthenticated(boolean isauthenticated) throws illegalargumentexception; }
在实际应用中,通常使用usernamepasswordauthenticationtoken:
public abstract class abstractauthenticationtoken implements authentication, credentialscontainer { } public class usernamepasswordauthenticationtoken extends abstractauthenticationtoken { }
一个常见的认证过程通常是这样的,创建一个usernamepasswordauthenticationtoken,然后交给authenticationmanager认证(后面详细说明),认证通过则通过securitycontextholder存放authentication信息。
usernamepasswordauthenticationtoken authenticationtoken = new usernamepasswordauthenticationtoken(loginvm.getusername(), loginvm.getpassword()); authentication authentication = this.authenticationmanager.authenticate(authenticationtoken); securitycontextholder.getcontext().setauthentication(authentication);
userdetails与userdetailsservice
userdetails 是spring security里的一个关键接口,他用来表示一个principal。
public interface userdetails extends serializable { /** * 用户的授权信息,可以理解为角色 */ collection<? extends grantedauthority> getauthorities(); /** * 用户密码 * * @return the password */ string getpassword(); /** * 用户名 * */ string getusername(); boolean isaccountnonexpired(); boolean isaccountnonlocked(); boolean iscredentialsnonexpired(); boolean isenabled(); }
userdetails提供了认证所需的必要信息,在实际使用里,可以自己实现userdetails,并增加额外的信息,比如email、mobile等信息。
在authentication中的principal通常是用户名,我们可以通过userdetailsservice来通过principal获取userdetails:
public interface userdetailsservice { userdetails loaduserbyusername(string username) throws usernamenotfoundexception; }
grantedauthority
在userdetails里说了,grantedauthority可以理解为角色,例如 role_administrator or role_hr_supervisor。
小结
- securitycontextholder, 用来访问 securitycontext.
- securitycontext, 用来存储authentication .
- authentication, 代表凭证.
- grantedauthority, 代表权限.
- userdetails, 用户信息.
- userdetailsservice,获取用户信息.
authentication认证
authenticationmanager
实现认证主要是通过authenticationmanager接口,它只包含了一个方法:
public interface authenticationmanager { authentication authenticate(authentication authentication) throws authenticationexception; }
authenticate()方法主要做三件事:
- 如果验证通过,返回authentication(通常带上authenticated=true)。
- 认证失败抛出authenticationexception
- 如果无法确定,则返回null
authenticationexception是运行时异常,它通常由应用程序按通用方式处理,用户代码通常不用特意被捕获和处理这个异常。
authenticationmanager的默认实现是providermanager,它委托一组authenticationprovider实例来实现认证。
authenticationprovider和authenticationmanager类似,都包含authenticate,但它有一个额外的方法supports,以允许查询调用方是否支持给定authentication类型:
public interface authenticationprovider { authentication authenticate(authentication authentication) throws authenticationexception; boolean supports(class<?> authentication); }
providermanager包含一组authenticationprovider,执行authenticate时,遍历providers,然后调用supports,如果支持,则执行遍历当前provider的authenticate方法,如果一个provider认证成功,则break。
public authentication authenticate(authentication authentication) throws authenticationexception { class<? extends authentication> totest = authentication.getclass(); authenticationexception lastexception = null; authentication result = null; boolean debug = logger.isdebugenabled(); for (authenticationprovider provider : getproviders()) { if (!provider.supports(totest)) { continue; } if (debug) { logger.debug("authentication attempt using " + provider.getclass().getname()); } try { result = provider.authenticate(authentication); if (result != null) { copydetails(authentication, result); break; } } catch (accountstatusexception e) { prepareexception(e, authentication); // sec-546: avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (internalauthenticationserviceexception e) { prepareexception(e, authentication); throw e; } catch (authenticationexception e) { lastexception = e; } } if (result == null && parent != null) { // allow the parent to try. try { result = parent.authenticate(authentication); } catch (providernotfoundexception e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw providernotfound even though a provider in the child already // handled the request } catch (authenticationexception e) { lastexception = e; } } if (result != null) { if (erasecredentialsafterauthentication && (result instanceof credentialscontainer)) { // authentication is complete. remove credentials and other secret data // from authentication ((credentialscontainer) result).erasecredentials(); } eventpublisher.publishauthenticationsuccess(result); return result; } // parent was null, or didn't authenticate (or throw an exception). if (lastexception == null) { lastexception = new providernotfoundexception(messages.getmessage( "providermanager.providernotfound", new object[] { totest.getname() }, "no authenticationprovider found for {0}")); } prepareexception(lastexception, authentication); throw lastexception; }
从上面的代码可以看出, providermanager有一个可选parent,如果parent不为空,则调用parent.authenticate(authentication)
authenticationprovider
authenticationprovider有多种实现,大家最关注的通常是daoauthenticationprovider,继承于abstractuserdetailsauthenticationprovider,核心是通过userdetails来实现认证,daoauthenticationprovider默认会自动加载,不用手动配。
先来看abstractuserdetailsauthenticationprovider,看最核心的authenticate:
public authentication authenticate(authentication authentication) throws authenticationexception { // 必须是usernamepasswordauthenticationtoken assert.isinstanceof(usernamepasswordauthenticationtoken.class, authentication, messages.getmessage( "abstractuserdetailsauthenticationprovider.onlysupports", "only usernamepasswordauthenticationtoken is supported")); // 获取用户名 string username = (authentication.getprincipal() == null) ? "none_provided" : authentication.getname(); boolean cachewasused = true; // 从缓存获取 userdetails user = this.usercache.getuserfromcache(username); if (user == null) { cachewasused = false; try { // retrieveuser 抽象方法,获取用户 user = retrieveuser(username, (usernamepasswordauthenticationtoken) authentication); } catch (usernamenotfoundexception notfound) { logger.debug("user '" + username + "' not found"); if (hideusernotfoundexceptions) { throw new badcredentialsexception(messages.getmessage( "abstractuserdetailsauthenticationprovider.badcredentials", "bad credentials")); } else { throw notfound; } } assert.notnull(user, "retrieveuser returned null - a violation of the interface contract"); } try { // 预先检查,defaultpreauthenticationchecks,检查用户是否被lock或者账号是否可用 preauthenticationchecks.check(user); // 抽象方法,自定义检验 additionalauthenticationchecks(user, (usernamepasswordauthenticationtoken) authentication); } catch (authenticationexception exception) { if (cachewasused) { // there was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cachewasused = false; user = retrieveuser(username, (usernamepasswordauthenticationtoken) authentication); preauthenticationchecks.check(user); additionalauthenticationchecks(user, (usernamepasswordauthenticationtoken) authentication); } else { throw exception; } } // 后置检查 defaultpostauthenticationchecks,检查iscredentialsnonexpired postauthenticationchecks.check(user); if (!cachewasused) { this.usercache.putuserincache(user); } object principaltoreturn = user; if (forceprincipalasstring) { principaltoreturn = user.getusername(); } return createsuccessauthentication(principaltoreturn, authentication, user); }
上面的检验主要基于userdetails实现,其中获取用户和检验逻辑由具体的类去实现,默认实现是daoauthenticationprovider,这个类的核心是让开发者提供userdetailsservice来获取userdetails以及 passwordencoder来检验密码是否有效:
private userdetailsservice userdetailsservice; private passwordencoder passwordencoder;
看具体的实现,retrieveuser,直接调用userdetailsservice获取用户:
protected final userdetails retrieveuser(string username, usernamepasswordauthenticationtoken authentication) throws authenticationexception { userdetails loadeduser; try { loadeduser = this.getuserdetailsservice().loaduserbyusername(username); } catch (usernamenotfoundexception notfound) { if (authentication.getcredentials() != null) { string presentedpassword = authentication.getcredentials().tostring(); passwordencoder.ispasswordvalid(usernotfoundencodedpassword, presentedpassword, null); } throw notfound; } catch (exception repositoryproblem) { throw new internalauthenticationserviceexception( repositoryproblem.getmessage(), repositoryproblem); } if (loadeduser == null) { throw new internalauthenticationserviceexception( "userdetailsservice returned null, which is an interface contract violation"); } return loadeduser; }
再来看验证:
protected void additionalauthenticationchecks(userdetails userdetails, usernamepasswordauthenticationtoken authentication) throws authenticationexception { object salt = null; if (this.saltsource != null) { salt = this.saltsource.getsalt(userdetails); } if (authentication.getcredentials() == null) { logger.debug("authentication failed: no credentials provided"); throw new badcredentialsexception(messages.getmessage( "abstractuserdetailsauthenticationprovider.badcredentials", "bad credentials")); } // 获取用户密码 string presentedpassword = authentication.getcredentials().tostring(); // 比较passwordencoder后的密码是否和userdetails的密码一致 if (!passwordencoder.ispasswordvalid(userdetails.getpassword(), presentedpassword, salt)) { logger.debug("authentication failed: password does not match stored value"); throw new badcredentialsexception(messages.getmessage( "abstractuserdetailsauthenticationprovider.badcredentials", "bad credentials")); } }
小结:要自定义认证,使用daoauthenticationprovider,只需要为其提供passwordencoder和userdetailsservice就可以了。
定制 authentication managers
spring security提供了一个builder类authenticationmanagerbuilder,借助它可以快速实现自定义认证。
看官方源码说明:
securitybuilder used to create an authenticationmanager . allows for easily building in memory authentication, ldap authentication, jdbc based authentication, adding userdetailsservice , and adding authenticationprovider's.
authenticationmanagerbuilder可以用来build一个authenticationmanager,可以创建基于内存的认证、ldap认证、 jdbc认证,以及添加userdetailsservice和authenticationprovider。
简单使用:
@configuration @enablewebsecurity @enableglobalmethodsecurity(prepostenabled = true, securedenabled = true) public class applicationsecurity extends websecurityconfigureradapter { public securityconfiguration(authenticationmanagerbuilder authenticationmanagerbuilder, userdetailsservice userdetailsservice,tokenprovider tokenprovider,corsfilter corsfilter, securityproblemsupport problemsupport) { this.authenticationmanagerbuilder = authenticationmanagerbuilder; this.userdetailsservice = userdetailsservice; this.tokenprovider = tokenprovider; this.corsfilter = corsfilter; this.problemsupport = problemsupport; } @postconstruct public void init() { try { authenticationmanagerbuilder .userdetailsservice(userdetailsservice) .passwordencoder(passwordencoder()); } catch (exception e) { throw new beaninitializationexception("security configuration failed", e); } } @override protected void configure(httpsecurity http) throws exception { http .addfilterbefore(corsfilter, usernamepasswordauthenticationfilter.class) .exceptionhandling() .authenticationentrypoint(problemsupport) .accessdeniedhandler(problemsupport) .and() .csrf() .disable() .headers() .frameoptions() .disable() .and() .sessionmanagement() .sessioncreationpolicy(sessioncreationpolicy.stateless) .and() .authorizerequests() .antmatchers("/api/register").permitall() .antmatchers("/api/activate").permitall() .antmatchers("/api/authenticate").permitall() .antmatchers("/api/account/reset-password/init").permitall() .antmatchers("/api/account/reset-password/finish").permitall() .antmatchers("/api/profile-info").permitall() .antmatchers("/api/**").authenticated() .antmatchers("/management/health").permitall() .antmatchers("/management/**").hasauthority(authoritiesconstants.admin) .antmatchers("/v2/api-docs/**").permitall() .antmatchers("/swagger-resources/configuration/ui").permitall() .antmatchers("/swagger-ui/index.html").hasauthority(authoritiesconstants.admin) .and() .apply(securityconfigureradapter()); } }
授权与访问控制
一旦认证成功,我们可以继续进行授权,授权是通过accessdecisionmanager来实现的。框架有三种实现,默认是affirmativebased,通过accessdecisionvoter决策,有点像providermanager委托给authenticationproviders来认证。
public void decide(authentication authentication, object object, collection<configattribute> configattributes) throws accessdeniedexception { int deny = 0; // 遍历decisionvoter for (accessdecisionvoter voter : getdecisionvoters()) { // 投票 int result = voter.vote(authentication, object, configattributes); if (logger.isdebugenabled()) { logger.debug("voter: " + voter + ", returned: " + result); } switch (result) { case accessdecisionvoter.access_granted: return; case accessdecisionvoter.access_denied: deny++; break; default: break; } } // 一票否决 if (deny > 0) { throw new accessdeniedexception(messages.getmessage( "abstractaccessdecisionmanager.accessdenied", "access is denied")); } // to get this far, every accessdecisionvoter abstained checkallowifallabstaindecisions(); }
来看accessdecisionvoter:
boolean supports(configattribute attribute); boolean supports(class<?> clazz); int vote(authentication authentication, s object, collection<configattribute> attributes);
object是用户要访问的资源,configattribute则是访问object要满足的条件,通常payload是字符串,比如role_admin 。所以我们来看下rolevoter的实现,其核心就是从authentication提取出grantedauthority,然后和configattribute比较是否满足条件。
public boolean supports(configattribute attribute) { if ((attribute.getattribute() != null) && attribute.getattribute().startswith(getroleprefix())) { return true; } else { return false; } } public boolean supports(class<?> clazz) { return true; } public int vote(authentication authentication, object object, collection<configattribute> attributes) { if(authentication == null) { return access_denied; } int result = access_abstain; // 获取grantedauthority信息 collection<? extends grantedauthority> authorities = extractauthorities(authentication); for (configattribute attribute : attributes) { if (this.supports(attribute)) { // 默认拒绝访问 result = access_denied; // attempt to find a matching granted authority for (grantedauthority authority : authorities) { // 判断是否有匹配的 authority if (attribute.getattribute().equals(authority.getauthority())) { // 可访问 return access_granted; } } } } return result; }
这里要疑问,configattribute哪来的?其实就是上面applicationsecurity的configure里的。
web security 如何实现
web层中的spring security(用于ui和http后端)基于servlet filters,下图显示了单个http请求的处理程序的典型分层。
spring security通过filterchainproxy作为单一的filter注册到web层,proxy内部的filter。
filterchainproxy相当于一个filter的容器,通过virtualfilterchain来依次调用各个内部filter
public void dofilter(servletrequest request, servletresponse response, filterchain chain) throws ioexception, servletexception { boolean clearcontext = request.getattribute(filter_applied) == null; if (clearcontext) { try { request.setattribute(filter_applied, boolean.true); dofilterinternal(request, response, chain); } finally { securitycontextholder.clearcontext(); request.removeattribute(filter_applied); } } else { dofilterinternal(request, response, chain); } } private void dofilterinternal(servletrequest request, servletresponse response, filterchain chain) throws ioexception, servletexception { firewalledrequest fwrequest = firewall .getfirewalledrequest((httpservletrequest) request); httpservletresponse fwresponse = firewall .getfirewalledresponse((httpservletresponse) response); list<filter> filters = getfilters(fwrequest); if (filters == null || filters.size() == 0) { if (logger.isdebugenabled()) { logger.debug(urlutils.buildrequesturl(fwrequest) + (filters == null ? " has no matching filters" : " has an empty filter list")); } fwrequest.reset(); chain.dofilter(fwrequest, fwresponse); return; } virtualfilterchain vfc = new virtualfilterchain(fwrequest, chain, filters); vfc.dofilter(fwrequest, fwresponse); } private static class virtualfilterchain implements filterchain { private final filterchain originalchain; private final list<filter> additionalfilters; private final firewalledrequest firewalledrequest; private final int size; private int currentposition = 0; private virtualfilterchain(firewalledrequest firewalledrequest, filterchain chain, list<filter> additionalfilters) { this.originalchain = chain; this.additionalfilters = additionalfilters; this.size = additionalfilters.size(); this.firewalledrequest = firewalledrequest; } public void dofilter(servletrequest request, servletresponse response) throws ioexception, servletexception { if (currentposition == size) { if (logger.isdebugenabled()) { logger.debug(urlutils.buildrequesturl(firewalledrequest) + " reached end of additional filter chain; proceeding with original chain"); } // deactivate path stripping as we exit the security filter chain this.firewalledrequest.reset(); originalchain.dofilter(request, response); } else { currentposition++; filter nextfilter = additionalfilters.get(currentposition - 1); if (logger.isdebugenabled()) { logger.debug(urlutils.buildrequesturl(firewalledrequest) + " at position " + currentposition + " of " + size + " in additional filter chain; firing filter: '" + nextfilter.getclass().getsimplename() + "'"); } nextfilter.dofilter(request, response, this); } } }
参考
https://spring.io/guides/topicals/spring-security-architecture/
https://docs.spring.io/spring-security/site/docs/5.0.5.release/reference/htmlsingle/#overall-architecture
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。