之前一直只是单纯的知道shiro权限框架,并没有实质性的应用,最近终于对机会接触并且有了实战,当然对其工作过程原理也有了一定的了解。首先想要声明的是其实shiro还是比较复杂的,要想应用、搞懂还是需要花费一定时间的。
一 shiro简介
Apache Shiro是一个功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理。借助Shiro易于理解的API,可以快速轻松地保护任何应用程序 - 从最小的移动应用程序到最大的Web和企业应用程序。
Shiro的架构有三个主要概念 - Subject(主题),SecurityManager(安全管理器)和Realm(领域)。
1.1 Subject:相当于当前用户的概念
Subject currentUser = SecurityUtils.getSubject();
1.2 SecurityManager:Shiro架构的核心,管理所有用户的安全操作。
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
securityManager.setSessionManager(defaultWebSessionManager());
return securityManager;
}
1.3 Realm:充当Shiro与应用程序安全数据之间的“桥接”或“连接器”,执行身份验证(登录)和授权(访问控制)。
通俗来讲就是登录时去获取数据库中对应的用户名密码进行登录验证以及得到当前用户拥有的页面权限等。配置Shiro时,必须至少指定一个Realm用于身份验证和/或授权,这个需要开发者自定义Realm类。
自定义Realm类,需要继承AuthorizingRealm抽象类,重载doGetAuthenticationInfo(身份验证)和doGetAuthorizationInfo(权限认证)
二 实践
实战部分采用的是springboot集成shiro。应用shiro充当权限框架前提是需要先创建相应的表,包括用户表,角色表,权限表,权限角色表,用户角色表。
整个流程大致是这样:A用户通过username与password登录系统,进入相应控制器,控制器通过SercrityUtils.getSubject()创建一个Subject,在认证提交前准备token(令牌),new UsernamePasswordToken(userName, password),然后调用subject.login(token)。这时候就会进入到我们自己定义的Realm类中,将令牌中的用户名密码与数据库中的用户名密码进行比较以及查询出相应的页面权限。
2.1 数据表(最简设计)
1)权限表u_permission:id主键,url地址,url描述
2)角色表u_role:id主键,角色类型,角色名称
3)角色权限表u_role_permission:角色id,权限id
4)用户表u_user:id主键,username用户名,password密码
5)用户角色表u_user_role:用户id,角色id
2.2 主要代码
2.2.1 配置类 ShiroConfig.java
@Configuration
public class ShiroConfig {
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
*
* Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/ajaxLogin", "anon");
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/add", "perms[权限添加]");
// <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
System.out.println("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(shiroRealm());
return securityManager;
}
// 身份认证realm; (这个需要自己写,账号密码校验;权限等)
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
/** 认证算法,必须设置与创建用户密码时的参数(加密算法,加密次数等)保持一致,
创建用户密码加密代码:new SimpleHash("MD5", password, byteSalt, 1024)
因为shiro框架内部会进行库密码与输入密码进行比较 **/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//加密方式
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//加密次数
hashedCredentialsMatcher.setHashIterations(1024);
//存储散列后的密码是否为16进制
//hashedCredentialsMatcher.isStoredCredentialsHexEncoded();
return hashedCredentialsMatcher;
}
}
2.2.2 Realm自定义类
public class ShiroRealm extends AuthorizingRealm {
private final static String hashAlgorithmName = "MD5";
private final static int hashIterations = 1024;
/**
* 获取身份验证信息
* Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
*
* @param authenticationToken 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————身份认证————");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String userName = token.getUsername();
// 从数据库获取对应用户名密码的用户
User user = xxx.findByUserName(token.getUsername());
if (user == null) {
throw new AccountException("用户名不正确或用户不存在!");
}
String password = user.getPassword();
/* if (!password.equals(new String((char[]) token.getCredentials()))) {
throw new AccountException("密码不正确");
}*/
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("user", user);
/* ByteSource.Util.bytes(username),对于第一次接触shiro的人来说应该是最难理解的地方,
这个SimpleAuthenticationInfo会将你Token中的账号密码通过getName()这个方法获取,
与你传入的username及password进行对比,byteSource是盐值,是为了加密时使用的。
这里盐值是在注册用户时设置的,采用的是username */
ByteSource credentialsSalt = ByteSource.Util.bytes(userName);
return new SimpleAuthenticationInfo(userName, password, credentialsSalt,getName());
}
/**
* 获取授权信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("————权限认证————");
String username = (String) SecurityUtils.getSubject().getPrincipal();
User user = xxx.findByUserName(username);
SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(user.getUserId()),SecurityUtils.getSubject().getPrincipals());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//查询user的时候也查询了其角色,and其拥有的权限url
permissions=permissionMapper.getPermissions(user.getUserName());
for(Role role:user.getRoles()){
info.addRole(role.getRoleName());
for(Permission permission:role.getPermissions()){
if(StringUtils.isNotBlank(permission.getUrl())){
info.addStringPermission(permission.getUrl());
}
if(StringUtils.isNotBlank(permission.getUrl())){
info.addStringPermission(permission.getUrl());
}
}
}
return info;
}
}
2.2.3 登录Controller
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
// 执行认证登陆
try{
subject.login(token);
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("userName",userName);
session.setTimeout(1800000);
}catch(IncorrectCredentialsException e){
System.out.println("用户名或密码不正确,请重新输入!");
}
到这边基本的shiro框架已经配置完成,当然还包括缓存及session使用redis管理,remenberme功能。