Apache Shiro是Java的一个安全框架,主要完成:认证、授权、加密、会话管理、缓存等功能,而且API简单,越来越多人使用该框架。
Shiro介绍
基本功能如下:
- Authentications:身份认证/登录,验证用户是否有对应的身份。
- Authorization:授权,判断用户是否拥有某个权限,比如当前用户是否拥有后台管理员管理的权限等。
- SessionManager:会话管理,用户登录后的一次会话,在还没退出之前,都是通过会话来识别用户。
- Cryptography:加密,保证数据的安全性。
- Web Support:便于集成到Web环境中。
- Cacheing:缓存,保存用户登录、权限等信息。
Shrio api核心是Subject。每个Subject表示一个主体,可以是一个用户,也可以是一个请求。所有的Subject都会绑定到SecurityManager中,通过SecurityManager.getSubject()方法,可以获取到当前用户的subject。
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager 交互;
且它管理着所有Subject。而SecurityManager是如何判断用户的身份和权限呢?这时就要引入Realm,域的概念,Shiro从Realm中获取安全数据(如权限等),提供给SecurityManager来判断用户的权限。
Shiro使用
通过上述的简介,可以得知,如果我们要对用户分不同的权限,如管理员、普通用户;要对会话进行管理,就需要用到Shiro这个安全框架。Shiro在SpringBoot中的使用就需要创建一个配置类,同时要配置Realm。
一、Shiro配置类
Shiro配置类中,需要配置生命周期处理器、SecurityManager,在该安全管理器中配置Realm、ShiroFilter过滤器、aop注解支持等。
/**
* spring-boot集成shiro配置类
*/
@Configuration
public class ShiroConfiguration {
/**
* 实例化lifecycleBeanPostProcessor
* shiro生命周期处理器
*
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 实例化DefaultAdvisorAutoProxyCreator
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
daap.setProxyTargetClass(true);
return daap;
}
/**
* 实例化自定义realm
*
* @return
*/
@Bean
public Realm getRealm() {
return new UserRealm();
}
/**
* 实例化securityManager
*
* @return
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getRealm());
return securityManager;
}
/**
* 实例化shiroFilter
*
* @return
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(getDefaultWebSecurityManager());
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面;
shiroFilterFactoryBean.setUnauthorizedUrl("/admin/403");
return shiroFilterFactoryBean;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
二、Realm
一个应用中可以有一个或者多个Realm,可以通过JDBC、内存来实现。在该系列文章中Server-2 MyBatis逆向生成中,观察仔细的朋友会发现,我创建了permission、role、role-permission三张数据表,分别表示了权限类型、角色类型、角色所拥有的权限这三种含义。这三张表与Realm之间有什么关系呢?在项目中创建了UserRealm用户认证、授权接口。具体代码如下:
public class UserRealm extends AuthorizingRealm {
private final String realmName = "UserRealm.class";
@Autowired
private UserMapper userMapper;
@Autowired
private PermissionService permissionService;
@Autowired
private RoleMapper roleMapper;
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Object principal = principals.getPrimaryPrincipal();
try {
if (principal instanceof String) {
UserExample example = new UserExample();
UserExample.Criteria criteria = example.createCriteria();
criteria.andAccountEqualTo((String) principal);
Integer roleId = userMapper.selectByExample(example).get(0).getRoleId();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Role role = roleMapper.selectByPrimaryKey(roleId);
info.addRole(role.getName());
Set permissions = permissionService.getPermissionsBy(roleId);
for (PermissionVO permission : permissions) {
info.addStringPermission(permission.getName());
}
return info;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
Object principal = token.getPrincipal();
if (principal instanceof String) {
String phone = (String) principal;
UserExample example = new UserExample();
UserExample.Criteria criteria = example.createCriteria();
criteria.andAccountEqualTo(phone);
List users = userMapper.selectByExample(example);
if (null != users && users.size() > 0) {
User user = users.get(0);
return new SimpleAccount(user.getAccount(), user.getPassword(), realmName);
}
}
return null;
}
}
在UserRealm中,可以看出,其实认证和授权都是通过数据库的数据来进行判断,比如认证,其实就是通过MyBatis来查找数据库中是否存在该用户;授权方法,则是传入用户的账号,查看该用户的角色,再查找该角色所拥有的权限,将账号、角色、权限信息存储到AuthorizationInfo中,即完成用户授权。
三、用户认证
下面几行简单的代码就完成了用户的认证。
Subject subject = SecurityUtils.getSubject(); //获取Subject
AuthenticationToken token = new UsernamePasswordToken(account, MD5Util.encrypt(password)); //根据用户账号、密码获取令牌。
try{
subject.login(token);
}catch( ..){
...
}
首先,第一行代码,getSubject(),这里有个巨坑,我怀疑getSubject()这个方式是根据请求中是否带有cookie,来判断是新的subject还是已经存在的subject,因为如果没有带cookie的请求,每次获取的subject都不一致。如果Subject不唯一,那么会导致Session会话也不唯一,那么session保存的键值对(通常是一些状态、参数)就无法使用,导致功能的异常。
如果登录(用户认证)失败,那么就会抛出不同的异常,比如UnknownAccountException(账号不存在)、IncorrectCredentialsException(账号密码不匹配)、LockedAccountException(不允许登录)等。