前言
现在流行的通用授权框架有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:
1
2
3
4
5
6
7
|
object principal = securitycontextholder.getcontext().getauthentication().getprincipal();
if (principal instanceof userdetails) {
string username = ((userdetails)principal).getusername();
} else {
string username = principal.tostring();
}
|
在spring security中,可以看一下authentication定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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:
1
2
3
4
5
|
public abstract class abstractauthenticationtoken implements authentication,
credentialscontainer {
}
public class usernamepasswordauthenticationtoken extends abstractauthenticationtoken {
}
|
一个常见的认证过程通常是这样的,创建一个usernamepasswordauthenticationtoken,然后交给authenticationmanager认证(后面详细说明),认证通过则通过securitycontextholder存放authentication信息。
1
2
3
4
5
|
usernamepasswordauthenticationtoken authenticationtoken =
new usernamepasswordauthenticationtoken(loginvm.getusername(), loginvm.getpassword());
authentication authentication = this .authenticationmanager.authenticate(authenticationtoken);
securitycontextholder.getcontext().setauthentication(authentication);
|
userdetails与userdetailsservice
userdetails 是spring security里的一个关键接口,他用来表示一个principal。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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:
1
2
3
|
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接口,它只包含了一个方法:
1
2
3
4
|
public interface authenticationmanager {
authentication authenticate(authentication authentication)
throws authenticationexception;
}
|
authenticate()方法主要做三件事:
- 如果验证通过,返回authentication(通常带上authenticated=true)。
- 认证失败抛出authenticationexception
- 如果无法确定,则返回null
authenticationexception是运行时异常,它通常由应用程序按通用方式处理,用户代码通常不用特意被捕获和处理这个异常。
authenticationmanager的默认实现是providermanager,它委托一组authenticationprovider实例来实现认证。
authenticationprovider和authenticationmanager类似,都包含authenticate,但它有一个额外的方法supports,以允许查询调用方是否支持给定authentication类型:
1
2
3
4
5
|
public interface authenticationprovider {
authentication authenticate(authentication authentication)
throws authenticationexception;
boolean supports( class <?> authentication);
}
|
providermanager包含一组authenticationprovider,执行authenticate时,遍历providers,然后调用supports,如果支持,则执行遍历当前provider的authenticate方法,如果一个provider认证成功,则break。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
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来检验密码是否有效:
1
2
|
private userdetailsservice userdetailsservice;
private passwordencoder passwordencoder;
|
看具体的实现,retrieveuser,直接调用userdetailsservice获取用户:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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;
}
|
再来看验证:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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。
简单使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
@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来认证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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:
1
2
3
4
|
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比较是否满足条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
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
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。
原文链接:http://www.cnblogs.com/xiaoqi/p/spring-security.html