Apache Shiro的拦截器
FormAuthenticationFilter
FormAuthenticationFilter
是shiro 包自带的一个拦截器,继承了 AuthenticatingFilter
这个抽象类,再上一级就是 AuthenticationFilter
。
在进入拦截器之前,会进入
onPreHandle
方法中:1
2
3
4
5//只要isAccessAllowed或者onAccessDenied有一个为真,就返回true,继续执行
判断这两个方法需不需要执行,按顺序
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}isAccessAllowed
方法验证请求是否认证
如果认证结果为false
,根据上面的流程控制,则进入onAccessDenied
方法中进行认证。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/**如果满足(1).当前的subject是被认证过的。
(2).用户请求的不是登录页面,但是在定义该过滤器时,使用了PERMISSIVE=”permissive”参数。
只要满足两个条件的一个即可允许操作
*/
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||(!isLoginRequest(request, response) && isPermissive(mappedValue));
}
//其实就是判断当前的subject是不是被认证过的
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
//判断当前的请求是不是登陆请求
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
return pathsMatch(getLoginUrl(), request);
}
//判断当前的拦截器是不是配置了PERMISSIVE=”permissive”参数,如果配置了就可以通过
protected boolean isPermissive(Object mappedValue) {
if(mappedValue != null) {
String[] values = (String[]) mappedValue;
return Arrays.binarySearch(values, PERMISSIVE) >= 0;
}
return false;
}onAccessDenied
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
54protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
//判断当前的其实是不是一个HTTP的POST请求
protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
}
//经过前面的判断是不是POST请求后,程序就为我们创建一个token,但是我们并没有传入userName和password在创建的时候就会抛出异常了
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
/**saveRequest就是把一个request保存在session中,redirectToLogin这里就是返回到设置的登录页面,在开头自定义的过滤器中就是重载了这个函数,在实际项目中,一般都会重载这个函数,比方说返回到指定的页面*/
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
saveRequest(request);
redirectToLogin(request, response);
}
可以看到如果前面 isAccessAllowed
检查是否认证过的请求,则进入 onAccessDenied
进行认证的操作。
自定义认证拦截器
继承 FormAuthenticationFilter
或者 AuthenticatingFilter
,并改写核心认证逻辑即可,这个需要阅读下 FormAuthenticationFilter的源码
的源码,这个时候思路就很清晰了。
认证
AuthorizingRealm 抽象类
Shiro的认证过程最终会交由Realm执行,这时会调用Realm的 getAuthenticationInfo(token)
方法。
该方法主要执行以下操作:
- 检查提交的进行认证的令牌信息
- 根据令牌信息从数据源(通常为数据库)中获取用户信息
- 对用户信息进行匹配验证。
- 验证通过将返回一个封装了用户信息的
AuthenticationInfo
实例。 - 验证失败则抛出
AuthenticationException
异常信息。拓展认证
需要自己定义一个 realm 类,来继承AuthorizingRealm
抽象类,然后重写相关需要修改的方法即可。
一般重写这两个方法即可:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* 获取授权信息
PrincipalCollection是一个身份集合,因为我们现在就一个Realm,所以直接调用getPrimaryPrincipal得到之前传入的用户名即可;然后根据用户名调用UserService接口获取角色及权限信息。
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
/**
* 获取身份验证信息
* 认证回调函数,登录时调用
* 首先根据传入的用户名获取User信息;然后如果user为空,那么抛出没找到帐号异常UnknownAccountException;
* 如果user找到但锁定了抛出锁定异常LockedAccountException;最后生成AuthenticationInfo信息,
* 交给间接父类AuthenticatingRealm使用CredentialsMatcher进行判断密码是否匹配,
* 如果不匹配将抛出密码错误异常IncorrectCredentialsException;
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
UserRealm父类AuthorizingRealm将获取Subject相关信息分成两步:获取身份验证信息(doGetAuthenticationInfo)及授权信息(doGetAuthorizationInfo);
- doGetAuthenticationInfo获取身份验证相关信息:首先根据传入的用户名获取User信息;然后如果user为空,那么抛出没找到帐号异常UnknownAccountException;如果user找到但锁定了抛出锁定异常LockedAccountException;最后生成AuthenticationInfo信息,交给间接父类AuthenticatingRealm使用CredentialsMatcher进行判断密码是否匹配,如果不匹配将抛出密码错误异常IncorrectCredentialsException;另外如果密码重试此处太多将抛出超出重试次数异常ExcessiveAttemptsException;在组装SimpleAuthenticationInfo信息时,需要传入:身份信息(用户名)、凭据(密文密码)、盐(username+salt),CredentialsMatcher使用盐加密传入的明文密码和此处的密文密码进行匹配。
- doGetAuthorizationInfo获取授权信息:PrincipalCollection是一个身份集合,因为我们现在就一个Realm,所以直接调用getPrimaryPrincipal得到之前传入的用户名即可;然后根据用户名调用UserService接口获取角色及权限信息。
AuthenticationToken
AuthenticationToken用于收集用户提交的身份(如用户名)及凭据(如密码):
1 | public interface AuthenticationToken extends Serializable { |
- 扩展接口
RememberMeAuthenticationToken
:提供了boolean isRememberMe()
现“记住我”的功能; - 扩展接口是
HostAuthenticationToken
:提供了String getHost()
方法用于获取用户“主机”的功能。
Shiro提供了一个直接拿来用的UsernamePasswordToken,用于实现用户名/密码Token组,另外其实现了RememberMeAuthenticationToken和HostAuthenticationToken,可以实现记住我及主机验证的支持。
自己实现 AuthenticationToken
主要实现能够实现获取用户的身份信息的 token :
1 | public class OAuth2Token implements AuthenticationToken { |
AuthenticationInfo
AuthenticationInfo有两个作用:
- 如果Realm是AuthenticatingRealm子类,则提供给AuthenticatingRealm内部使用的CredentialsMatcher进行凭据验证;(如果没有继承它需要在自己的Realm中自己实现验证);
- 提供给SecurityManager来创建Subject(提供身份信息);
PrincipalCollection
因为我们可以在Shiro中同时配置多个Realm,所以呢身份信息可能就有多个;因此其提供了PrincipalCollection用于聚合这些身份信息:1
2
3
4
5
6
7
8
9
10public interface PrincipalCollection extends Iterable, Serializable {
Object getPrimaryPrincipal(); //得到主要的身份
<T> T oneByType(Class<T> type); //根据身份类型获取第一个
<T> Collection<T> byType(Class<T> type); //根据身份类型获取一组
List asList(); //转换为List
Set asSet(); //转换为Set
Collection fromRealm(String realmName); //根据Realm名字获取
Set<String> getRealmNames(); //获取所有身份验证通过的Realm名字
boolean isEmpty(); //判断是否为空
}
因为PrincipalCollection聚合了多个,此处最需要注意的是getPrimaryPrincipal,如果只有一个Principal那么直接返回即可,如果有多个Principal,则返回第一个(因为内部使用Map存储,所以可以认为是返回任意一个);oneByType / byType根据凭据的类型返回相应的Principal;fromRealm根据Realm名字(每个Principal都与一个Realm关联)获取相应的Principal。
如果我们还需要实现别的类型的多个认证,具体参考文章第二篇,我先不研究了。
AuthorizationInfo
AuthorizationInfo用于聚合授权信息的:
1 | public interface AuthorizationInfo extends Serializable { |
当我们使用AuthorizingRealm时,如果身份验证成功,在进行授权时就通过 doGetAuthorizationInfo
方法获取角色/权限信息用于授权验证。
Shiro提供了一个实现 SimpleAuthorizationInfo
,大多数时候使用这个即可。
Subject
Subject是Shiro的核心对象,基本所有身份验证、授权都是通过Subject完成。
对于Subject的构建一般没必要我们去创建;一般通过 SecurityUtils.getSubject()
获取;
对于Subject我们一般这么使用:
- 身份验证(login)
- 授权(
hasRole*/isPermitted*
或checkRole*/checkPermission*
) - 将相应的数据存储到会话(Session)
- 切换身份(RunAs)/多线程身份传播
- 退出 logout