今天研究一下 常用的安全框架shiro
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以 快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。 软件名称 Apache Shiro 开发商 Apache 性质 Java安全框架 复制代码
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户 (Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为 是Shiro的“用户”概念。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。 SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来 提供安全管理的各种服务。 Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控 制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲,Realm实质上是一个安全相关的DAO:它 封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和( 或)授权。配置多个Realm是可以的,但是至少需要一个。 Shiro内置了可以连接大量安全数据源(又名目录)的Realm, 如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表 自定义数据源的自己的Realm实现。 复制代码
shiro中核心概念介绍: Filter:
1.AnonymousFilter:通过此filter修饰的url,任何人都可以进行访问,即使没有进行权限认证
2.FormAuthenticationFilter:通过此filter修饰的url,会对请求的url进行验证,如果没有通过,则会重定向返回到loginurl
3.BasicHttpAuthenticationFilter:通过此filter修饰的url,要求用户已经通过认证,如果没有通过,则会要求通过Authorization 信息进行认证
4.LogoutFilter:通过此filter修饰的url,一旦收到url请求,则会立即调用subject进行退出,并重定向到redirectUrl
5.NoSessionCreationFilter:通过此filter修饰的url,不会创建任何会话
6.PermissionAuthorizationFilter:权限拦截器,验证用户是否具有相关权限
7.PortFilter:端口拦截器,不是通过制定端口访问url,将自动将端口重定向到指定端口
8.HttpMethodPermissionFilter:rest风格拦截器,配置rest的访问方式
9.RolesAuthorizationFilter:角色拦截器,未登陆,将跳转到loginurl,未授权,将跳转到unauthorizedUrl
10.SslFilter:HTTPS拦截器,需要以HTTPS的方式进行访问
11.UserFilter:用户拦截器,需要用户已经认证,或已经remember me
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。 authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数 roles:例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。 perms:例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。 rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。 port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。 authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证 ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https user:例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查 注:anon,authcBasic,auchc,user是认证过滤器, perms,roles,ssl,rest,port是授权过滤器 复制代码
上面简单介绍了一下shiro,接下来就进入正题。
加入需要的依赖
<!-- spring-data-jpa --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!--模板引擎,访问静态资源--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- shiro 相关包 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency> <!-- shiro+redis缓存插件 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>2.4.2.1-RELEASE</version> </dependency> <!--shiro与thymelef的集成插件--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency> 复制代码
yml文件在加入一点配置,这里要注意的是jpa 和之前的datasource 处于同一级
#通过jpa 生成数据库表 spring: jpa: hibernate: ddl-auto: update show-sql: true thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html encoding: UTF-8 content-type: text/html mode: HTML5 复制代码
数据库设计 一般的权限管理都会设计到这五张表(用户表、角色表、用户角色中间表、权限表、角色权限中间表)
1、用户表:
@Entity //实体类的注解 @Table(name="sys_user") public class SysUser implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String userName; private String passWord; private int userEnable; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassWord() { return passWord; } public void setPassWord(String passWord) { this.passWord = passWord; } public int getUserEnable() { return userEnable; } public void setUserEnable(int userEnable) { this.userEnable = userEnable; } } 复制代码
2、角色表
@Entity //实体类的注解 @Table(name="sys_role") public class SysRole implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String roleName; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } } 复制代码
3、用户角色中间表
@Entity //实体类的注解 @Table(name="sys_user_role") public class SysUserRole implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private int userId; private int roleId; public int getId() { return id; } public void setId(int id) { this.id = id; } public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public int getRoleId() { return roleId; } public void setRoleId(int roleId) { this.roleId = roleId; } } 复制代码
4、权限表
@Entity //实体类的注解 @Table(name="sys_permission") public class SysPermission implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String userName; private String resUrl; private String userType; private String parentId; private String userSort; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getResUrl() { return resUrl; } public void setResUrl(String resUrl) { this.resUrl = resUrl; } public String getUserType() { return userType; } public void setUserType(String userType) { this.userType = userType; } public String getParentId() { return parentId; } public void setParentId(String parentId) { this.parentId = parentId; } public String getUserSort() { return userSort; } public void setUserSort(String userSort) { this.userSort = userSort; } } 复制代码
5、角色权限中间表
@Entity //实体类的注解 @Table(name="sys_role_permission") public class SysRolePermission implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private int roleId; private int permissionId; public int getId() { return id; } public void setId(int id) { this.id = id; } public int getRoleId() { return roleId; } public void setRoleId(int roleId) { this.roleId = roleId; } public int getPermissionId() { return permissionId; } public void setPermissionId(int permissionId) { this.permissionId = permissionId; } } 复制代码
基本上五个表的结构就是这样,有问题到时候再改,实体类建好了,先建数据库也行都一样,现在先建了实体类就通过spring-data-jpa 生成一下数据库的表结构 这时候在重启项目就可以帮我们在数据库建好表了。
@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 没有登陆的用户只能访问登陆页面 shiroFilterFactoryBean.setLoginUrl("/auth/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/auth/index"); // 未授权界面; ----这个配置了没卵用,具体原因想深入了解的可以自行百度 shiroFilterFactoryBean.setUnauthorizedUrl("/auth/err"); //自定义拦截器 Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>(); shiroFilterFactoryBean.setFilters(filtersMap); // 权限控制map. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/auth/login", "anon"); filterChainDefinitionMap.put("/auth/logout", "logout"); filterChainDefinitionMap.put("/auth/kickout", "anon"); //filterChainDefinitionMap.put("/book/**", "authc,perms[book:list],roles[admin]"); //filterChainDefinitionMap.put("/**", "authc,kickout"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置realm. securityManager.setRealm(myShiroRealm()); // 自定义缓存实现 使用redis securityManager.setCacheManager(cacheManager()); // 自定义session管理 使用redis securityManager.setSessionManager(sessionManager()); return securityManager; } /** * 身份认证realm; (这个需要自己写,账号密码校验;权限等) * * @return */ @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); return myShiroRealm; } /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * * @return */ public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * 配置shiro redisManager * 使用的是shiro-redis开源插件 * * @return */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost("localhost"); redisManager.setPort(6379); redisManager.setExpire(1800);// 配置缓存过期时间 redisManager.setTimeout(0); // redisManager.setPassword(password); return redisManager; } /** * Session Manager * 使用的是shiro-redis开源插件 */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); return sessionManager; } /** * RedisSessionDAO shiro sessionDao层的实现 通过redis * 使用的是shiro-redis开源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } /*** * 授权所用配置 * * @return */ @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } /*** * 使授权注解起作用不如不想配置可以在pom文件中加入 * <dependency> *<groupId>org.springframework.boot</groupId> *<artifactId>spring-boot-starter-aop</artifactId> *</dependency> * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Shiro生命周期处理器 * */ @Bean public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); } } 复制代码
public class MyShiroRealm extends AuthorizingRealm { private static org.slf4j.Logger logger = LoggerFactory.getLogger(MyShiroRealm.class); //如果项目中用到了事物,@Autowired注解会使事物失效,可以自己用get方法获取值 @Autowired private SysRoleService roleService; @Autowired private UserService userService; /** * 认证信息.(身份验证) : Authentication 是用来验证用户身份 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { logger.info("---------------- 执行 Shiro 凭证认证 ----------------------"); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String name = token.getUsername(); String password = String.valueOf(token.getPassword()); SysUser user = new SysUser(); user.setUserName(name); user.setPassWord(password); // 从数据库获取对应用户名密码的用户 SysUser userList = userService.getUser(user); if (userList != null) { // 用户为禁用状态 if (userList.getUserEnable() != 1) { throw new DisabledAccountException(); } logger.info("---------------- Shiro 凭证认证成功 ----------------------"); SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userList, //用户 userList.getPassWord(), //密码 getName() //realm name ); return authenticationInfo; } throw new UnknownAccountException(); } /** * 授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { logger.info("---------------- 执行 Shiro 权限获取 ---------------------"); Object principal = principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); if (principal instanceof SysUser) { SysUser userLogin = (SysUser) principal; Set<String> roles = roleService.findRoleNameByUserId(userLogin.getId()); authorizationInfo.addRoles(roles); Set<String> permissions = userService.findPermissionsByUserId(userLogin.getId()); authorizationInfo.addStringPermissions(permissions); } logger.info("---- 获取到以下权限 ----"); logger.info(authorizationInfo.getStringPermissions().toString()); logger.info("---------------- Shiro 权限获取成功 ----------------------"); return authorizationInfo; } /** * 清除所有用户授权信息缓存. */ public void clearCachedAuthorizationInfo(String principal) { SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName()); clearCachedAuthorizationInfo(principals); } /** * 清除所有用户授权信息缓存. */ public void clearAllCachedAuthorizationInfo() { Cache<Object, AuthorizationInfo> cache = getAuthorizationCache(); if (cache != null) { for (Object key : cache.keys()) { cache.remove(key); } } } /** * @Description: TODO 清楚缓存的授权信息 * @return void 返回类型 */ public void clearAuthz(){ this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals()); } } 复制代码
在数据库里面插入几条数据开始测试。
就拿之前的代码做测试了,先在配置里配置好需要做权限过滤的路径,和权限规则
这样设置应该是必须登录才能访问,浏览器直接访问一下
结果发现跳转到之前配置的登录页了,就是说这个权限起作用了,把它改成anon,重启试一下 ,其实在启动的控制台我们能看到shiroFilter这个过滤器的信息,过滤的是 /*
在访问一下之前的链接,发现可以正常访问到,查到了之前的测试数据
改个角色的权限试试在,之前我们已经给用户设置了 ‘admin’ ‘test’两个角色 , 没有设置‘demo’这个角色,请求应该也会被拦截
果然又跳到了登录页面,把‘demo’ 去掉,发现可以正常请求查到了数据
其实除了在shiro的配置文件配置过滤规则,也可以通过注解的方式在controller上加入权限,效果是一样的
图中框起来的地方可以设置 AND 或者是 OR , 就是设置多个角色的时候, 是全部满足还是满足一个即可
还用刚才的做测试,设置两个权限,我们在数据库设置的权限是 ‘book:*’ , 测试发现没问题可以请求到
其实除了这种配置方式,还可以通过注解的方式,配置方式类似角色的设置
在thymelef 下使用html 进行测试,需要的jar 上面已经导入了,在shiro的config中配置ShiroDialect ,这个上面的配置文件也已经配置好了,剩下的就是在页面头部中引入xmlns
<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> 复制代码
页面里放两个按钮,配置不同的权限,数据库中的权限改为"book:list",看一下效果
<tr> <td colspan="2"> <button shiro:hasPermission="book:list" type="reset"> 重置 </button> <button shiro:hasPermission="book:add" type="button" onclick="submit1()"> 提交 </button> </td> </tr> 复制代码
发现只有拥有权限的按钮才能显示出来,而且查看页面源码发现没有权限的按钮根本就没有生成在页面中