import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.*; import java.util.stream.Collectors; @Component public class UserRealm extends AbstractUserRealm { @Autowired private SysUserRoleMapper sysUserRoleMapper; @Autowired private SysRoleMapper sysRoleMapper; @Override public UserRolesAndPermissions doGetRoleAuthorizationInfo(User userInfo) { Set<String> userRoles = new HashSet<>(); Set<String> userPermissions = new HashSet<>(); //获取当前用户下拥有的所有角色列表,及权限 Map param = new HashMap<>(); param.put("userId", userInfo.getId()); List<SysUserRole> userRoleList = sysUserRoleMapper.selectListSelective(param); List<String> roleIdList = Optional.ofNullable(userRoleList).orElse(new ArrayList<>()).stream().map(temp -> { return temp.getRoleId().toString(); }).collect(Collectors.toList()); if (!CollectionUtils.isEmpty(roleIdList)) { List<String> rolePermissionList = sysUserRoleMapper.selectUserRolePermissionList(roleIdList); if (!CollectionUtils.isEmpty(rolePermissionList)) { for (String rolePermission : rolePermissionList) { userPermissions.add(rolePermission); } } } param.clear(); param.put("idList",roleIdList); List<SysRole> roleList = sysRoleMapper.selectListSelective(param); List<String> roleNameList = Optional.ofNullable(roleList).orElse(new ArrayList<>()).stream().map(temp -> { return temp.getRole().toString(); }).collect(Collectors.toList()); userRoles.addAll(roleNameList); return new UserRolesAndPermissions(userRoles, userPermissions); } }
Shiro工具类
import org.apache.shiro.SecurityUtils; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.SessionKey; import org.apache.shiro.subject.support.DefaultSubjectContext; import org.apache.shiro.web.session.mgt.WebSessionKey; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class SecurityUtil { public static User getUser(){ User user = (User)SecurityUtils.getSubject().getPrincipal(); return user; } public static Session getSession(){ return SecurityUtils.getSubject().getSession(); } }
密码加密
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; public class CustomCredentialsMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info){ UsernamePasswordToken token = (UsernamePasswordToken) authcToken; Object accountCredentials = getCredentials(info); try { return PasswordHash.validatePassword(token.getPassword(), accountCredentials.toString()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (InvalidKeySpecException e) { e.printStackTrace(); } return false; } }
import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.math.BigInteger; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; public class PasswordHash { public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1"; // The following constants may be changed without breaking existing hashes. public static final int SALT_BYTE_SIZE = 24; public static final int HASH_BYTE_SIZE = 24; public static final int PBKDF2_ITERATIONS = 1000; public static final int ITERATION_INDEX = 0; public static final int SALT_INDEX = 1; public static final int PBKDF2_INDEX = 2; /** * Returns a salted PBKDF2 hash of the password. * * @param password the password to hash * @return a salted PBKDF2 hash of the password */ public static String createHash(String password) throws NoSuchAlgorithmException, InvalidKeySpecException { return createHash(password.toCharArray()); } /** * Returns a salted PBKDF2 hash of the password. * * @param password the password to hash * @return a salted PBKDF2 hash of the password */ public static String createHash(char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException { // Generate a random salt SecureRandom random = new SecureRandom(); byte[] salt = new byte[SALT_BYTE_SIZE]; random.nextBytes(salt); // Hash the password byte[] hash = pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE); // format iterations:salt:hash return PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" + toHex(hash); } /** * Validates a password using a hash. * * @param password the password to check * @param correctHash the hash of the valid password * @return true if the password is correct, false if not */ public static boolean validatePassword(String password, String correctHash) throws NoSuchAlgorithmException, InvalidKeySpecException { return validatePassword(password.toCharArray(), correctHash); } /** * Validates a password using a hash. * * @param password the password to check * @param correctHash the hash of the valid password * @return true if the password is correct, false if not */ public static boolean validatePassword(char[] password, String correctHash) throws NoSuchAlgorithmException, InvalidKeySpecException { // Decode the hash into its parameters String[] params = correctHash.split(":"); int iterations = Integer.parseInt(params[ITERATION_INDEX]); byte[] salt = fromHex(params[SALT_INDEX]); byte[] hash = fromHex(params[PBKDF2_INDEX]); // Compute the hash of the provided password, using the same salt, // iteration count, and hash length byte[] testHash = pbkdf2(password, salt, iterations, hash.length); // Compare the hashes in constant time. The password is correct if // both hashes match. return slowEquals(hash, testHash); } /** * Compares two byte arrays in length-constant time. This comparison method * is used so that password hashes cannot be extracted from an on-line * system using a timing attack and then attacked off-line. * * @param a the first byte array * @param b the second byte array * @return true if both byte arrays are the same, false if not */ private static boolean slowEquals(byte[] a, byte[] b) { int diff = a.length ^ b.length; for(int i = 0; i < a.length && i < b.length; i++) diff |= a[i] ^ b[i]; return diff == 0; } /** * Computes the PBKDF2 hash of a password. * * @param password the password to hash. * @param salt the salt * @param iterations the iteration count (slowness factor) * @param bytes the length of the hash to compute in bytes * @return the PBDKF2 hash of the password */ private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes) throws NoSuchAlgorithmException, InvalidKeySpecException { PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8); SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); return skf.generateSecret(spec).getEncoded(); } /** * Converts a string of hexadecimal characters into a byte array. * * @param hex the hex string * @return the hex string decoded into a byte array */ private static byte[] fromHex(String hex) { byte[] binary = new byte[hex.length() / 2]; for(int i = 0; i < binary.length; i++) { binary[i] = (byte)Integer.parseInt(hex.substring(2*i, 2*i+2), 16); } return binary; } /** * Converts a byte array into a hexadecimal string. * * @param array the byte array to convert * @return a length*2 character string encoding the byte array */ private static String toHex(byte[] array) { BigInteger bi = new BigInteger(1, array); String hex = bi.toString(16); int paddingLength = (array.length * 2) - hex.length(); if(paddingLength > 0) return String.format("%0" + paddingLength + "d", 0) + hex; else return hex; } }
涉及的主要表结构
CREATE TABLE `user` ( `id` int(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', `status` tinyint(4) DEFAULT '1' COMMENT '1:正常 2:停用 3锁定 -1:已删除', `password` varchar(255) NOT NULL DEFAULT '' COMMENT '登录密码', `user_name` varchar(64) DEFAULT NULL COMMENT '用户姓名', `mobile` varchar(20) DEFAULT NULL COMMENT '手机号', `latest_login_time` datetime DEFAULT NULL COMMENT '最后一次登陆时间', `salt` varchar(50) DEFAULT NULL COMMENT '盐', `create_name` varchar(50) DEFAULT NULL COMMENT '创建人', `update_name` varchar(50) DEFAULT NULL COMMENT '更新人', PRIMARY KEY (`id`), KEY `idx_mobile_name` (`mobile`,`user_name`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='用户表'; CREATE TABLE `sys_role` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `role` varchar(50) DEFAULT NULL COMMENT '角色', `description` varchar(100) DEFAULT NULL COMMENT '角色描述', `available` int(11) DEFAULT '1' COMMENT '是否可用, 0-不可用,1-可用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `modify_time` datetime DEFAULT CURRENT_TIMESTAMP, `oper_name` varchar(32) DEFAULT NULL COMMENT '操作人', PRIMARY KEY (`id`), UNIQUE KEY `idx_sys_role_role` (`role`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='角色表'; CREATE TABLE `sys_user_role` ( `user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID', `role_id` int(11) NOT NULL DEFAULT '0' COMMENT '角色ID', PRIMARY KEY (`user_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户-角色关联表'; CREATE TABLE `role_menu` ( `role_id` int(11) NOT NULL DEFAULT '0' COMMENT '角色ID', `menu_id` int(11) NOT NULL DEFAULT '0' COMMENT '菜单ID', PRIMARY KEY (`role_id`,`menu_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色-菜单关联表'; CREATE TABLE `menu` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单 ID', `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父类id', `menu_name` varchar(20) DEFAULT NULL COMMENT '菜单名称', `url` varchar(100) DEFAULT NULL COMMENT '菜单 URL', `perms` varchar(50) DEFAULT NULL COMMENT '权限标识符', `order_num` int(11) DEFAULT NULL COMMENT '排序', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `modify_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `icon` varchar(32) DEFAULT NULL COMMENT '图标', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='菜单表';
上述两个关键方法的调用时机参考
https://blog.csdn.net/zhangjianming2018/article/details/80973548
角色权限的控制
@RequiresRoles("admin") @ApiOperation(value = "角色列表接口") @ApiImplicitParams({ @ApiImplicitParam(paramType = "header", name = "token", required = true, value = "token", dataType = "String") }) @PostMapping(value = "/roleList") public BasePageVo roleList(BasePageDto basePageDto) { BasePageVo basePageVo = roleService.roleList(basePageDto); return basePageVo; }
直接通过注解的方式和上述写在ShiroFilterFactoryBean里的拦截规则效果是一致的
Shiro的其他配置
记住我
@Bean(name = "rememberMeCookie") public SimpleCookie rememberMeCookie() { log.info("ShiroConfiguration.rememberMeCookie()"); // 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); // 记住我cookie生效时间30天 ,单位秒; simpleCookie.setMaxAge(259200); return simpleCookie; } /** * cookie管理对象; * * @return */ @Bean(name = "rememberMeManager") public CookieRememberMeManager rememberMeManager() { log.info("ShiroConfiguration.rememberMeManager()"); CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); return cookieRememberMeManager; }
基于Redis实现分布式Sesssion
/** * SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件 * MemorySessionDAO 直接在内存中进行会话维护 * EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。 * * @return */ @Bean public SessionDAO sessionDAO(RedisConfig redisConfig) { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager(redisConfig)); //session在redis中的保存时间,最好大于session会话超时时间 redisSessionDAO.setExpire(12000); return redisSessionDAO; } /** * 配置保存sessionId的cookie * 注意:这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理 也需要自己的cookie * 默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid * * @return */ @Bean("sessionIdCookie") public SimpleCookie sessionIdCookie() { //这个参数是cookie的名称 SimpleCookie simpleCookie = new SimpleCookie("sid"); //setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点: //setcookie()的第七个参数 //设为true后,只能通过http访问,javascript无法访问 //防止xss读取cookie simpleCookie.setHttpOnly(true); simpleCookie.setPath("/"); //maxAge=-1表示浏览器关闭时失效此Cookie simpleCookie.setMaxAge(-1); return simpleCookie; } /** * 配置会话管理器,设定会话超时及保存 * * @return */ @Bean("sessionManager") public SessionManager sessionManager(RedisConfig redisConfig) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionIdCookie(sessionIdCookie()); sessionManager.setSessionDAO(sessionDAO(redisConfig)); sessionManager.setCacheManager(cacheManager(redisConfig)); //全局会话超时时间(单位毫秒),默认30分钟 暂时设置为10秒钟 用来测试 sessionManager.setGlobalSessionTimeout(1800000); //是否开启删除无效的session对象 默认为true sessionManager.setDeleteInvalidSessions(true); //是否开启定时调度器进行检测过期session 默认为true sessionManager.setSessionValidationSchedulerEnabled(true); //设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时 //设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler //暂时设置为 5秒 用来测试 sessionManager.setSessionValidationInterval(3600000); //取消url 后面的 JSESSIONID sessionManager.setSessionIdUrlRewritingEnabled(false); return sessionManager; } /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * * @return */ public RedisCacheManager cacheManager(RedisConfig redisConfig) { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager(redisConfig)); return redisCacheManager; } @Bean public RedisManager redisManager(RedisConfig redisConfig) { RedisManager redisManager = new RedisManager(); redisManager.setHost(redisConfig.getRedisHost() + ":" + redisConfig.getRedisPort()); redisManager.setPassword(redisConfig.getRedisPassword()); redisManager.setDatabase(redisConfig.getRedisDatabase()); redisManager.setTimeout(redisConfig.getRedisTimeOut()); return redisManager; } /** * 注入 securityManager */ @Bean public SecurityManager securityManager(UserRealm userRealm, RedisConfig redisConfig) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置自定义 realm securityManager.setRealm(userRealm); //注入缓存管理器 securityManager.setCacheManager(cacheManager(redisConfig)); //如果我们需要让shiro 的 session 在集群中共享,就需要替换这个默认的 sessionManager securityManager.setSessionManager(sessionManager(redisConfig)); //注入记住我管理器; securityManager.setRememberMeManager(rememberMeManager()); SecurityUtils.setSecurityManager(securityManager); return securityManager; }
防止重复登入
SessionsSecurityManager securityManager = (SessionsSecurityManager) SecurityUtils.getSecurityManager(); DefaultSessionManager sessionManager = (DefaultSessionManager) securityManager.getSessionManager(); Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();//获取当前已登录的用户session列表 for (Session session : sessions) { //清除该用户以前登录时保存的session //如果和当前session是同一个session,则不剔除 if (SecurityUtils.getSubject().getSession().getId().equals(session.getId())) break; User user = (User) (session.getAttribute("user")); if (user != null) { String mobile = user.getMobile(); if (token.getUsername().equals(mobile)) { log.info(mobile + "已登录,剔除中..."); sessionManager.getSessionDAO().delete(session); } } }