Spring boot整合shiro权限管理
Apache Shiro功能框架:
Shiro聚焦与应用程序安全领域的四大基石:认证、授权、会话管理和保密。
#,认证,也叫作登录,用于验证用户是不是他自己所说的那个人;
#,授权,也就是访问控制,比如用于决定“谁”是否有权限访问“什么”;
#,会话管理,管理用户相关的会话,即使在非web和ejb的环境下也支持;
#,保密,使用特性加密算法来保证用户数据的安全性,同时还要保证用起来够简单;
同时Shiro还提供了其他特性来在不同的应用程序环境下使用强化以上的四大基石:
#,Web支持:Shiro的web相关的API简化了web应用安全控制;
#,缓存,在Shiro中,缓存是一等公民,用于保证用户认证和权限控制的性能;
#,测试,支持可测试性,以便用户可以方便的对安全相关代码编写单元测试和集成测试;
#,记住密码,可以跨会话的记住用户的身份信息,以便只有在一些强制性的场合才需要登录。
Subject :实体,代表当前用户,方便交互,实际功能逻辑是由SecurityManager实现
SecurityManager : 安全管理器,负责所有与安全相关的操作,是Shiro的核心,负责与Shiro的其他组件进行交互
Realm : Shiro从Realm获取安全数据(如用户,角色,权限),数据源,需要我们自己实现并提供给框架
Authenticator : 负责身份验证,提供接口,需要我们自己实现并提供给框架
Authorizer :负责权限验证,提供接口,需要我们自己实现并提供给框架
SessionManager 会话管理器,不仅仅可以在Web环境中使用,也可以在普通javaSE中使用
SessionDAO:所有会话的CRUD功能
CacheManager:缓存控制器,来管理用户,角色,权限等的缓存
Cryptography : 密码模块,提供了一些常见的加密组件用于加密和解密
Shiro中做身份认证的是
Step1:应用程序代码在调用Subject.login(token)方法后,传入代表最终用户的身份和凭证构造的AuthenticationToken实例token。
Step2:将Subject实例委托给应用程序的SecurityManager(Shiro的安全管理)通过调用securityManager.login(token)来开始实际的认证工作。这里开始真正的认证工作了。
Step3,4,5:然后SecurityManager就会根据具体的reaml去进行安全认证了。
Shiro授权/访问控制:
主体 :Subject
主体,即访问应用的用户,在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。
资源 :Resource
在应用中用户可以访问的任何东西,比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
权限 :Permission
安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如: 访问用户列表页面、 查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)、 打印文档等等。
如上可以看出,权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许,不反映谁去执行这个操作。所以后续还需要把权限赋予给用户,即定义哪个用户允许在某个资源上做什么操作(权限),Shiro不会去做这件事情,而是由实现人员提供。
Shiro支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的),后续部分介绍。
角色 :Role
角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。
隐式角色:即直接通过角色来验证用户有没有操作权限,如在应用中CTO、技术总监、开发工程师可以使用打印机,假设某天不允许开发工程师使用打印机,此时需要从应用中删除相应代码;再如在应用中CTO、技术总监可以查看用户、查看权限;突然有一天允许技术总监查看用户、查看权限了,需要在相关代码中把技术总监角色从判断逻辑中删除掉;即粒度是以角色为单位进行访问控制的,粒度较粗;如果进行修改可能造成多处代码修改。
显示角色:在程序中通过权限控制谁能访问某个资源,角色聚合一组权限集合;这样假设哪个角色不能访问某个资源,只需要从角色代表的权限集合中移除即可;无须修改多处代码;即粒度是以资源/实例为单位的;粒度较细。
Shiro权限拦截:
Shiro会话管理:
会话:
shiro的session特性
- 基于POJO/J2SE:shiro中session相关的类都是基于接口实现的简单的java对象(POJO),兼容所有java对象的配置方式,扩展也更方便,完全可以定制自己的会话管理功能 。
- 简单灵活的会话存储/持久化:因为shiro中的session对象是基于简单的java对象的,所以你可以将session存储在任何地方,例如,文件,各种数据库,内存中等。
- 容器无关的集群功能:shiro中的session可以很容易的集成第三方的缓存产品完成集群的功能。例如,Ehcache + Terracotta, Coherence, GigaSpaces等。你可以很容易的实现会话集群而无需关注底层的容器实现。
- 异构客户端的访问:可以实现web中的session和非web项目中的session共享。
- 会话事件监听:提供对对session整个生命周期的监听。
- 保存主机地址:在会话开始session会存用户的ip地址和主机名,以此可以判断用户的位置。
- 会话失效/过期的支持:用户长时间处于不活跃状态可以使会话过期,调用touch()方法,可以主动更新最后访问时间,让会话处于活跃状态。
- 透明的Web支持:shiro全面支持Servlet 2.5中的session规范。这意味着你可以将你现有的web程序改为shiro会话,而无需修改代码。
- 单点登录的支持:shiro session基于普通java对象,使得它更容易存储和共享,可以实现跨应用程序共享。可以根据共享的会话,来保证认证状态到另一个程序。从而实现单点登录。
Shiro权限缓存:
shiro实例:
创建项目,创建实体类:
用户类:
package com.mmall.demo2.model; import java.util.HashSet; import java.util.Set; public class User { private Integer uid; private String username; private String password; private Set<Role> roles = new HashSet<>(); public Integer getUid() { return uid; } public void setUid(Integer uid) { this.uid = uid; } 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 Set<Role> getRoles() { return roles; } public void setRoles(Set<Role> roles) { this.roles = roles; } }
权限类:
package com.mmall.demo2.model; public class Permission { private Integer pid; private String name; private String url; public Integer getPid() { return pid; } public void setPid(Integer pid) { this.pid = pid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } }
角色类:
package com.mmall.demo2.model; import java.util.HashSet; import java.util.Set; public class Role { private Integer rid; private String rname; private Set<Permission> permissions = new HashSet<>(); private Set<User> users = new HashSet<>(); public Integer getRid() { return rid; } public void setRid(Integer rid) { this.rid = rid; } public String getRname() { return rname; } public void setRname(String rname) { this.rname = rname; } public Set<Permission> getPermissions() { return permissions; } public void setPermissions(Set<Permission> permissions) { this.permissions = permissions; } public Set<User> getUsers() { return users; } public void setUsers(Set<User> users) { this.users = users; } }
然后创建dao层,service层,创建数据库表结构
shiro认证授权的核心是realm,自定义一个realm继承AuthorizingRealm类:
public class AuthRealm extends AuthorizingRealm { @Autowired private UserService userService; // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { User user = (User) principals.fromRealm(this.getClass().getName()).iterator().next();//因为认证后将对象放到session中,所以先从session中去处对象 List<String> permissionList = new ArrayList<>(); List<String> roleNameList = new ArrayList<>(); Set<Role> roleSet = user.getRoles(); if (CollectionUtils.isNotEmpty(roleSet)) {//如果角色/权限不为空,遍历角色/权限, for(Role role : roleSet) { roleNameList.add(role.getRname()); Set<Permission> permissionSet = role.getPermissions(); if (CollectionUtils.isNotEmpty(permissionSet)) { for (Permission permission : permissionSet) { permissionList.add(permission.getName()); } } } } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermissions(permissionList);//将权限放到AuthorizationInfo info.addRoles(roleNameList); return info; } // 认证登录 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;//将传入的通常肯转换 String username = usernamePasswordToken.getUsername();//取出username User user = userService.findByUsername(username); return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());//转成AuthenticationInfo } }
在认证登录中,密码是直接取出的数据库的密码,如何保证数据库的密码和传入的密码match对上,在 AuthorizingRealm 类中我们可以看到:
允许我们传入CredentialsMatcher
因此自定义密码校验规则要实现这个接口:
public class CredentialMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;//转换tocken String password = new String(usernamePasswordToken.getPassword());//取出password转成字符串 String dbPassword = (String) info.getCredentials();//获取数据库密码 return this.equals(password, dbPassword);// } }
然后我们需要将他们注入到shiro配置中:
@Configuration public class ShiroConfiguration { @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(manager); bean.setLoginUrl("/login");//定义登录的url bean.setSuccessUrl("/index");//定义登陆成功跳转的url bean.setUnauthorizedUrl("/unauthorized");//无权的url LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/index", "authc"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/loginUser", "anon"); filterChainDefinitionMap.put("/admin", "roles[admin]"); filterChainDefinitionMap.put("/edit", "perms[edit]"); filterChainDefinitionMap.put("/druid/**", "anon"); filterChainDefinitionMap.put("/**", "user"); bean.setFilterChainDefinitionMap(filterChainDefinitionMap); return bean; } @Bean("securityManager") public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(authRealm); return manager; } @Bean("authRealm") public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) { AuthRealm authRealm = new AuthRealm(); authRealm.setCacheManager(new MemoryConstrainedCacheManager());//在实例中给出自己的密码比较器 authRealm.setCredentialsMatcher(matcher); return authRealm; } @Bean("credentialMatcher")//密码校验 public CredentialMatcher credentialMatcher() { return new CredentialMatcher(); } //配置shiro和spring的关联 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } }
shiroFilter中调用securityManager,securityManager中调用authRealm,authRealm中调用credentialMatcher
shiro的权限验证:
登录处理:
@RequestMapping("/loginUser") public String loginUser(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) { UsernamePasswordToken token = new UsernamePasswordToken(username, password);//组成UsernamePasswordToken实例 Subject subject = SecurityUtils.getSubject();//拿到Subject try {//认证逻辑 subject.login(token); User user = (User) subject.getPrincipal();//拿到用户 session.setAttribute("user", user);//存入session,在authRealm中会取出并检验 return "index"; } catch (Exception e) { return "login"; } }
访问控制:
在ShiroConfiguration 中,已经设置了不同的url的访问控制,具体的过滤器含义及用法:
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没有参数表示必须存在用户,当登入操作时不做检查
在实际项目中,需要将这些权限控制设置为动态的,存在数据库中。
shiro的优势:
升级:
配置文件:
import org.apache.commons.collections.CollectionUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * @Auther: jiangwenzhang@outlook.com * @Date: 2018/10/8 10:05 * @PackName: * @Description: */ public class AuthRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private PermissionService permissionService; // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { User token = (User) SecurityUtils.getSubject().getPrincipal(); Integer userId = token.getId(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //根据用户ID查询角色(role),放入到Authorization里。 List<Role> roleList = roleService.listByUser(String.valueOf(userId)); Set<String> roleSet = new HashSet<String>(); for(Role role : roleList){ roleSet.add(role.getName()); } info.setRoles(roleSet); //根据用户ID查询权限(permission),放入到Authorization里。 List<Permission> permissionList = permissionService.listByRole(roleList); Set<String> permissionSet = new HashSet<String>(); if(permissionList!=null){ for(Permission Permission : permissionList){ permissionSet.add(Permission.getName()); } } info.setStringPermissions(permissionSet); return info; } // 认证登录 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;//将传入的token转换 String username = usernamePasswordToken.getUsername();//取出username User user = userService.getUserByName(username); return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());//转成AuthenticationInfo } }
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; /** * @Auther: jiangwenzhang@outlook.com * @Date: 2018/10/8 10:08 * @PackName: * @Description: */ public class CredentialMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;//转换tocken String password = new String(usernamePasswordToken.getPassword());//取出password转成字符串 String dbPassword = (String) info.getCredentials();//获取数据库密码 return this.equals(password, dbPassword);// } }
import org.apache.shiro.cache.MemoryConstrainedCacheManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; /** * @Auther: jiangwenzhang@outlook.com * @Date: 2018/10/8 10:09 * @PackName: * @Description: */ @Configuration public class ShiroConfiguration { @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") org.apache.shiro.mgt.SecurityManager manager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(manager); bean.setLoginUrl("/loginUser");//定义登录的url bean.setSuccessUrl("/index");//定义登陆成功跳转的url bean.setUnauthorizedUrl("/unauthorized");//无权的url LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/loginUser", "anon"); // filterChainDefinitionMap.put("/test", "perms[用户管理]"); // filterChainDefinitionMap.put("/sys/**", "roles[admin]"); filterChainDefinitionMap.put("/**","authc"); filterChainDefinitionMap.put("/**/**","authc"); bean.setFilterChainDefinitionMap(filterChainDefinitionMap); return bean; } @Bean("securityManager") public org.apache.shiro.mgt.SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(authRealm); return manager; } @Bean("authRealm") public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) { AuthRealm authRealm = new AuthRealm(); authRealm.setCacheManager(new MemoryConstrainedCacheManager());//在实例中给出自己的密码比较器 authRealm.setCredentialsMatcher(matcher); return authRealm; } @Bean("credentialMatcher")//密码校验 public CredentialMatcher credentialMatcher() { return new CredentialMatcher(); } //配置shiro和spring的关联 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") org.apache.shiro.mgt.SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } }
关于权限拦截方式:
Filter | 解释 |
---|---|
anon | 无参,开放权限,可以理解为匿名用户或游客 |
authc | 无参,需要认证 |
logout | 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url |
authcBasic | 无参,表示 httpBasic 认证 |
user | 无参,表示必须存在用户,当登入操作时不做检查 |
ssl | 无参,表示安全的URL请求,协议为 https |
perms[user] | 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms[“user, admin”],当有多个参数时必须每个参数都通过才算通过 |
roles[admin] | 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles[“admin,user”],当有多个参数时必须每个参数都通过才算通过 |
rest[user] | 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等 |
port[8081] | 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数 |
关于角色获取:
<select id="listByUser" resultType="com.**.**.entity.Role"> select * from role r where r.id in(select ur.rid from user_role ur where ur.uid=#{userId}) </select>
关于权限获取:
<select id="listByRole" resultType="com.**.**.entity.Permission"> select * from permission p where p.id in (select rp.rid from role_permission rp where rp.rid in <foreach collection="roleList" item="role" open="(" close=")" separator=","> #{role.id} </foreach> ) </select>