Apache Shiro 是一款运用广泛的Java安全框架,特点是使用起来比较简单,Api比较人性化。最近要用到这玩意,提前复习一下相关概念。(下文Shiro的核心组件部分摘自张开涛/著 《跟我学Shiro》)
1、Shiro的核心组件
Subject: 在Shiro中,Subject是对外API的核心,他代表着 当前用户 这一概念,这个用户并不只是一个具体的人,与当前应用交互的,比如爬虫,机器人,接口调用等,是一个抽象的概念,所有的Subject都绑定到了SecurityManager,可以认为Subject是一个门面,SecurityManager才是实际的执行者。
打开Subject的接口方法列表,可以看到里面封装了大量的鉴权操作。
SecurityManager:安全管理器,可以看作是Shiro的核心,所有与安全相关的操作都会与他进行交互;而且他管理着所有Subject,同时负责与后面介绍的其他组件进行交互,可以看出它是Shiro的核心。
Realm Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法,也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
也就是说对于我们而言,最简单的一个 Shiro 应用:
1、 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
2、 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
从以上也可以看出,Shiro不提供维护用户权限,而是通过Realm让开发人员自己注入。
而在Shiro与SpringBoot的整合中,实现一个简单的鉴权,也是实现自定义的Realm,将其注入。
2、与SpringBoot整合
Shiro官方为SpringBoot提供了官方的starter,如下引入:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
我们先来自定义一个Realm。
首先新建一个类,然后继承 AuthorizingRealm。
AuthorizingRealm是一个抽象类,它要求实现两个方法:
public class MyRealm extends AuthorizingRealm {
/**
* 用于角色权限验证
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 用于登录权限验证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
doGetAuthenticationInfo 用于登录权限验证,当用户登录系统时,会调用这个方法,然后返回一个AuthenticationInfo的实现,AuthenticationInfo实际上是对用户登录信息的一个封装,我们来看具体实现:
/**
* 用于登录权限验证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String userName= (String)authenticationToken.getPrincipal();
UserEntity user = userDao.selectOne(new QueryWrapper<UserEntity>().eq("name",userName));
//查询用户的角色
if(user == null){
throw new UnknownAccountException("用户不存在!");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),getName());
return info;
}
然后再看一个简单的通过用户名和密码来登录的接口:
@RequestMapping("/login")
public String login(String name,String password){
UsernamePasswordToken token = new UsernamePasswordToken(name,password);
SecurityUtils.getSubject().login(token);
return "登录成功!";
}
我们可以看到,用户通过用户名密码登录的时候,我们将其封装成了Shiro帮我们实现的UsernamePasswordToken ,然后从SecurityUtils里获取了Subject对象,前面我们说过,Subject对象实际上就是SecurityManager的对外API,在调用login方法的时候,会委托SecurityManager行事,然后我们在自定义的Realm的doGetAuthenticationInfo方法里面,就能接收到这个token,通过getPrincipal()方法就能获取到用户登录时输入的用户名,getCredentials()就能获取密码。
然后我们从数据库里查询出用户的信息,取出密码,然后封装成SimpleAuthenticationInfo,Shiro会帮我们判断密码是否匹配。
值得注意的是,这里传入了一个User,其实只是作为一个唯一凭证,它可以是用户的邮箱,UUID,手机号等任意数据,也可以是Shiro官方定义的接口 PrincipalCollection 的实现。之所以这么做,是因为能标识用户身份的往往有多个,比如手机号和邮箱,以及用户Id,我们可以把它们都封装进PrincipalCollection 里,但必须要指定其中一个作为基础的标识。
在用户登录验证成功之后,访问需要权限判断的接口时,就会通过 doGetAuthorizationInfo来校验用户是否有权限访问。这里给出一个简单的实现:
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
UserEntity user = (UserEntity) SecurityUtils.getSubject().getPrincipal();
List<Long> relRoleList= userRelRoleDao.selectList(new QueryWrapper<UserRelRole>().eq("user_id",user.getId())).stream().map(UserRelRole::getRoleId).collect(Collectors.toList());
List<RoleEntity> roleEntities = roleDao.selectBatchIds(relRoleList);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 角色
Set<String> roles = new HashSet<>();
Set<String> permissions = new HashSet<>();
for(RoleEntity role:roleEntities){
roles.add(role.getRoleName());
permissions.add(role.getPerm());
}
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
具体实现就不用看了,我为了偷懒,直接通过Mybatis-plus的QuerryWapper模式做了个假联表查询。这个方法里主要就是获取用户的权限信息。我们可以看到这里直接从SecurityUtils里获取Subject,getPrincipal,然后直接转换成了User对象,没有一丝犹豫。没错,这就是我们刚刚在登录验证的时候放进去的User对象,所以说不管是什么都行,只要能具体表示到某个用户身上。我们通过它查询到用户所拥有的角色,以及角色拥有的全部权限,把它们统统丢进了HashSet里,返回一个authorizationInfo。
这里每次用户请求权限的时候都会查权限表,实际上是不妥的,权限不是频繁变动的东西,没必要每次都查询,明天再来研究如何对Realm作缓存,Shiro对此也有默认实现。
我们在数据库里配置权限表,设计如下:
用户可以被分配多种角色,而每个角色允许拥有多个权限。在web开发中,这些权限都是对接口的访问权,因此我们用接口路径来作为它的权限标识。
Shiro提供了 @RequiresPermissions 、 @RequiresRoles等注解,可以很方便的对接口的访问权限做限制,但值得一提的是,Shiro的注解是通过AOP实现的,因此要引入Spring-aop,否则注解将不会生效。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然后写一个接口:
@RequiresPermissions("api:user:get")
@RequestMapping("/api/user/get")
public String get(){
return "Lain";
}
这里用了@RequiresPermissions(),我们也可以根据自己的需求写其他注解。其实接口上无脑写请求路径就好,毕竟接口的路径是绝对唯一的,之后只要在角色对应的权限里配上这个perm,用户只要被分配了角色,那么就会必然拥有对于这个接口的权限了。
Shiro里role和permission并没有明确的上下级关系,你也可以只配role。
最后提一下Shiro的配置。
我们使用SpringBoot推荐的代码配置的方式。
创建一个类并打上@Configuration注解,下面直接上代码
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager());
shiroFilterFactoryBean.setLoginUrl("/login");
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/auth");
Map<String,String> map = new HashMap<>();
map.put("/api/user/put","perms[api:user:put]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
@Bean
public MyRealm myRealm(){
return new MyRealm();
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm());
return securityManager;
}
}
注意,我们刚刚自定义的Realm里面是注入了Dao的,但我们并没有给MyRealm加上@Component等注解,因此Spring的包扫描不会将其扫描进容器。然后我们在Configuration里注入了它,
@Bean
public MyRealm myRealm(){
return new MyRealm();
}
这个Bean依旧会被自动装配,被容器托管,其余bean配置里要用到这个Bean的时候,只需要直接调用方法即可注入。
这里我们注入了securityManager和shirFilter,DefaultAdvisorAutoProxyCreator 是为了解决注解模式导致RequestMapping失效的问题。
这里要提一点,如果是使用注解方式,那么我们如果要实现诸如权限不够,跳转到某个页面,或者定制化提示信息,就只能捕获全局权限异常,然后自己去写实现,shiroFilterFactoryBean.setUnauthorizedUrl("/auth")是没用的,它只管通过url方式配置的过滤器,注解的管不着的。