简介

Apache Shiro 是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。

Apache Shiro Features 特性

下图为描述 Shiro 功能的框架图:

Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:

  • **Authentication(认证):**用户身份识别,通常被称为用户“登录”
  • **Authorization(授权):**访问控制。比如某个用户是否具有某个操作的使用权限。
  • **Session Management(会话管理):**特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
  • **Cryptography(加密):**在对数据源使用加密算法加密的同时,保证易于使用。

High-Level Overview 高级概述

在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。

  • Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
  • SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
  • Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。

我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。

引入Maven依赖

使用Shiro的关键依赖如下

1
2
3
4
5
6
7
8
9
10
11
<!--shiro配置权限管理-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>

Shiro 配置

首先要配置的是 ShiroConfig 类,Apache Shiro 核心通过 Filter 来实现,通过 URL 规则来进行过滤和权限校验,所以我们需要定义一系列关于 URL 的规则和访问权限。

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
/**
* Shiro的配置文件
*/
@Configuration
public class ShiroConfig {

@Bean("sessionManager")
public SessionManager sessionManager(RedisShiroSessionDAO redisShiroSessionDAO,
@Value("${db.redis.open}") boolean redisOpen,
@Value("${db.shiro.redis}") boolean shiroRedis){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//设置session过期时间为1小时(单位:毫秒),默认为30分钟
sessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);

//如果开启redis缓存且renren.shiro.redis=true,则shiro session存到redis里
if(redisOpen && shiroRedis){
sessionManager.setSessionDAO(redisShiroSessionDAO);
}
return sessionManager;
}

@Bean("securityManager")
public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
securityManager.setSessionManager(sessionManager);

return securityManager;
}


@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setLoginUrl("/login.html");
shiroFilter.setUnauthorizedUrl("/");

Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/swagger-resources/**", "anon");

filterMap.put("/statics/**", "anon");
filterMap.put("/login.html", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/favicon.ico", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/**", "authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);

return shiroFilter;
}
/* ... */
}

sessionManager 方法管理会话,配置相关参数

securityManager 是管理所有Shiro相关对象的核心方法

shiroFilter 拦截器用来过滤和校验

Filter Chain 定义说明:

  • 1、一个URL可以配置多个 Filter,使用逗号分隔
  • 2、当设置多个过滤器时,全部验证通过,才视为通过
  • 3、部分过滤器可指定参数,如 perms,roles

Shiro 内置的 FilterChain

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
  • anon:所有 url 都都可以匿名访问
  • authc: 需要认证才能进行访问
  • user:配置记住我或认证通过可以访问

Authentication(认证)

在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。因为在 Shiro 中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。通常情况下,在 Realm 中会直接从我们的数据源中获取 Shiro 需要的验证信息。可以说,Realm 是专用于安全框架的 DAO. Shiro 的认证过程最终会交由 Realm 执行,这时会调用 Realm 的getAuthenticationInfo(token)方法。

该方法主要执行以下操作:

  • 1、检查提交的进行认证的令牌信息
  • 2、根据令牌信息从数据源(通常为数据库)中获取用户信息
  • 3、对用户信息进行匹配验证。
  • 4、验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
  • 5、验证失败则抛出AuthenticationException异常信息。

而在我们的应用程序中要做的就是自定义一个 Realm 类,继承AuthorizingRealm 抽象类,重载 doGetAuthenticationInfo(),重写获取用户信息的方法。

doGetAuthenticationInfo 的重写

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
@Component
public class UserRealm extends AuthorizingRealm {

/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authcToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authcToken;

//查询用户信息
SysUserEntity user = new SysUserEntity();
user.setUsername(token.getUsername());
user = sysUserDao.selectOne(user);

//账号不存在
if(user == null) {
throw new UnknownAccountException("账号或密码不正确");
}

//账号锁定
if(user.getStatus() == 0){
throw new LockedAccountException("账号已被锁定,请联系管理员");
}

SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
return info;
}

}

前端

页面代码如下

1
2
3
<div class="col-xs-4">
<button type="button" class="btn btn-primary btn-block btn-flat" @click="login">登录</button>
</div>

js逻辑如下

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
<script type="text/javascript">
var vm = new Vue({
el:'#rrapp',
data:{
username: '',
password: '',
captcha: '',
error: false,
errorMsg: '',
src: 'captcha.jpg'
},
beforeCreate: function(){
if(self != top){
top.location.href = self.location.href;
}
},
methods: {
refreshCode: function(){
this.src = "captcha.jpg?t=" + $.now();
},
login: function (event) {
var data = "username="+vm.username+"&password="+vm.password+"&captcha="+vm.captcha;
$.ajax({
type: "POST",
url: "sys/login",
data: data,
dataType: "json",
success: function(result){
if(result.code == 0){//登录成功
parent.location.href ='index.html';
}else{
vm.error = true;
vm.errorMsg = result.msg;

vm.refreshCode();
}
}
});
}
}
});
</script>

后端

Controller层配置登录验证的方法

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
/**
* 登录
*/
@ResponseBody
@RequestMapping(value = "/sys/login", method = RequestMethod.POST)
public R login(String username, String password, String captcha) {
String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY);
if(!captcha.equalsIgnoreCase(kaptcha)){
return R.error("验证码不正确");
}

try{
Subject subject = ShiroUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token);
}catch (UnknownAccountException e) {
return R.error(e.getMessage());
}catch (IncorrectCredentialsException e) {
return R.error("账号或密码不正确");
}catch (LockedAccountException e) {
return R.error("账号已被锁定,请联系管理员");
}catch (AuthenticationException e) {
return R.error("账户验证失败");
}

return R.ok();
}

小结

登陆验证流程:

  • 若用户未登录,Shiro配置的拦截器会生效,跳转到登录界面
  • 页面中点击登录按钮,按照js逻辑进行登录验证请求,此处即 /sys/login
  • 后端Controller层调用相应方法处理登录验证请求
  • Controller层方法最终会调用Realm 中的doGetAuthenticationInfo方法

Authorization(授权)

Shiro 的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();当访问到页面的时候,链接配置了相应的权限或者 Shiro 标签才会执行此方法否则不会执行,所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回 null 即可。在这个方法中主要是使用类:SimpleAuthorizationInfo进行角色的添加和权限的添加。

doGetAuthorizationInfo 的重写

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
@Component
public class UserRealm extends AuthorizingRealm {

/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
Long userId = user.getUserId();

List<String> permsList;

//系统管理员,拥有最高权限
if(userId == Constant.SUPER_ADMIN){
List<SysMenuEntity> menuList = sysMenuDao.selectList(null);
permsList = new ArrayList<>(menuList.size());
for(SysMenuEntity menu : menuList){
permsList.add(menu.getPerms());
}
}else{
permsList = sysUserDao.queryAllPerms(userId);
}

//用户权限列表
Set<String> permsSet = new HashSet<>();
for(String perms : permsList){
if(StringUtils.isBlank(perms)){
continue;
}
permsSet.addAll(Arrays.asList(perms.trim().split(",")));
}

SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}

}

@RequiresPermissions() 注释的使用

在Controller层对应方法上添加注释,例如

1
2
3
4
5
6
7
8
9
10
/**
* 所有用户列表
*/
@RequestMapping("/list")
@RequiresPermissions("sys:user:list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = sysUserService.queryPage(params);

return R.ok().put("page", page);
}

数据库样例

部分权限数据如下

访问接口的时候,会验证其拥有的权限是否包含接口上的权限

Session Management(会话管理)

会话管理的配置在ShiroConfig类中。

Cryptography(加密)

加密配置 可以通过设置Shiro的bean,传入自定义密码加密器,参见SpringBoot Shiro 配置自定义密码加密器;也可以重写Realm的setCredentialsMatcher方法

setCredentialsMatcher 的重写

传入的参数包括加密方式和迭代次数

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class UserRealm extends AuthorizingRealm {

@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
shaCredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName);
shaCredentialsMatcher.setHashIterations(ShiroUtils.hashIterations);
super.setCredentialsMatcher(shaCredentialsMatcher);
}

}

参考资料

Apache Shiro 官方网站

Spring Boot 整合 Shiro-登录认证和权限管理