最近花了三天时间跟着燕青老师学习了基于shiro框架的权限管理的设计与实现,现在总结,写一篇博客供大家共同学习和交流,联系穆荣斌。
权限管理的基本知识;
概念:只要用户参与的系统一般都有权限管理模块,权限管理主要实现对用户访问系统的控制,并且控制用户访问的资源和菜单'
分类:认证和授权
***认证:验证用户是否合法,常用:用户名密码,指纹机,基于某种证书验证
主体在进行身份认证时需要提供身份信息和凭证信息。
***授权:验证用户是否具有访问系统某些资源的权限,注意:这是基于认证通过的用户主体
授权可以理解为:who对what/which进行how的操作
这里注意:what资源包括:资源类型和资源实例
资源类型指某些模块菜单,用户删除增加等;
资源实例指针对id为1的用户的增删查看,针对陕西省管理人员只能查看陕西省的资源而不能查看其它省的资源
权限管理的解决方案
谈到解决方案涉及到权限管理中的粗粒度权限和细粒度权限:
粗粒度权限:例如资源类型的控制,例如某个模块菜单和button按钮
解决方案:将比较容易的权限管理代码抽取出来使用struts或者springmvc拦截器来判断验证
细粒度权限:例如资源类型的实例,就是对数据级别的权限管理例如id为1的产品信息增删差等
解决方案:针对细粒度资源实例,因为业务数据的不确定性,在业务开发时就要在service层业务代码中进行判断和拦截。
*例如:部门经理只能查询本部门的员工信息,可以在controller层调用service等方法时传一个部门id给servcie层的方法,然后在servcie层进行按本部门id查询员工的信息。
权限管理的模型图
根据实际分析可以将权限模型概括如下:
主体:账号和密码
资源:资源名称和访问地址
权限:权限名称和资源id
角色:角色名称和id
权限与角色:权限id和角色id
用户与角色:用户账号和角色id
一般过程中:将资源和权限合并为:权限:权限id、资源名称和资源url
基本会建立以上五张表,角色是其中的润滑剂和过度器
基于url地址的权限管理方案
思路如下:
所有的资源和全下url都要配置在数据库中或者配置文件中
流程如下:
主体登录,先认证
拦截器拦截判断是否有session,如果有登录,没有进行认证验证用户名密码是否正确,提示错误信息
授权:使用授权拦截器实现
实现思路如下:将用户对应角色的权限菜单和权限url查询出来放在session中,然后在拦截器进行拦截时获取并判断被拦截的url是否为session存在,如果存在即放行,不存在拦截提示无权限访问。
//在执行handler之前来执行的
//用于用户认证校验、用户权限校验
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//得到请求的url
String url = request.getRequestURI();
//判断是否是公开 地址
//实际开发中需要公开 地址配置在配置文件中
//从配置中取逆名访问url
List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL");
//遍历公开 地址,如果是公开 地址则放行
for(String open_url:open_urls){
if(url.indexOf(open_url)>=0){
//如果是公开 地址则放行
return true;
}
}
//从配置文件中获取公共访问地址
List<String> common_urls = ResourcesUtil.gekeyList("commonURL");
//遍历公用 地址,如果是公用 地址则放行
for(String common_url:common_urls){
if(url.indexOf(common_url)>=0){
//如果是公开 地址则放行
return true;
}
}
//获取session
HttpSession session = request.getSession();
ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
//从session中取权限范围的url
List<SysPermission> permissions = activeUser.getPermissions();
for(SysPermission sysPermission:permissions){
//权限的url
String permission_url = sysPermission.getUrl();
if(url.indexOf(permission_url)>=0){
//如果是权限的url 地址则放行
return true;
}
}
//执行到这里拦截,跳转到无权访问的提示页面
request.getRequestDispatcher("/WEB-INF/jsp/refuse.jsp").forward(request, response);
//如果返回false表示拦截不继续执行handler,如果返回true表示放行
return false;
}
基于shiro框架的权限管理方案
目前市面上流行Apache的shiro框架和spring的security,由于后者对spring依赖过于强烈,所以多用于shiro
shiro架构
其中不管是认证器还是授权器都是要操作realm来判断实现的shiro实现流程
认证流程如下:
用户或者某个程序登录,在spring中配置或者搭建SecurityManager环境,然后用户登录,然后SecurityManager通过调用Authenticator来执行shiro框架默认的Realm或者自定义realm来根据身份信息验证用户是否正确来进行登录认证,
同理授权也一样,所有的判断都会通过SecurityManager安全管理器来调用Authorizor来执行realm判断该主体用户是否拥有被拦截地址的权限来进行权限判断自定义realm
public class CustomRealm extends AuthorizingRealm {
@Autowired
private SystemService systemService;
//设置realm名字
@Override
public void setName(String name) {
super.setName("customRealm");
}
//认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//第一步:从token中获取用户凭证信息******类似基于url权限管理中心的activtyuser记录用户信息
String userCode = (String) authenticationToken.getPrincipal();
//第二步:拿着用户名从数据库中查密码,如果查不到返回null
SysUser sysUser = null;
try {
sysUser = systemService.findSysUser(userCode);
} catch (Exception e) {
e.printStackTrace();
}
if (sysUser == null) {
return null;
}
//如果查询到密码
String password = sysUser.getPassword();
//查询盐,用于验证密码是否正确
String salt = sysUser.getSalt();
//查询用户菜单和url
List<SysPermission> menus = systemService.findPermissionMenue(userCode);
List<SysPermission> urls = systemService.findPermissionUrl(userCode);
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid(userCode);
activeUser.setUsercode(userCode);
activeUser.setUsername(activeUser.getUsername());
activeUser.setMenues(menus);
activeUser.setPermissionUrls(urls);
//然后获得认证信息并返回
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(activeUser, password, ByteSource.Util.bytes(salt), this.getName());
return simpleAuthenticationInfo;
}
//授权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//第一步:从principalCollection中获取用户信息,这是已经封装成一个对象中
ActiveUser activeUser = (ActiveUser) principalCollection.getPrimaryPrincipal();
//第二步:拿着用户名从数据库查询正确权限信息
List<SysPermission> permissions=systemService.findPermissionUrl(activeUser.getUsercode());
List<SysPermission> permissionMenu= systemService.findPermissionMenue(activeUser.getUsercode());
List<String> permissionList=new ArrayList<String>();
if (permissions!=null){
for (int i=0;i<permissions.size();i++){
permissionList.add(permissions.get(i).getPercode());
}
}
if (permissionMenu!=null){
for (int i=0;i<permissionMenu.size();i++){
permissionList.add(permissionMenu.get(i).getPercode());
}
}
//获取权限信息并返回
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//将权限信息设置到simpleAuthorizationInfo
simpleAuthorizationInfo.addStringPermissions(permissionList);
return simpleAuthorizationInfo;
}
//清除缓存,在修改权限时sevice层修改
public void clearCached() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
}
spring整合shiro配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
<!--开启shiro注解-->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!-- Shiro 的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 -->
<property name="loginUrl" value="/login.action" />
<property name="successUrl" value="/first.action"/>
<!--无权限跳转页面-->
<property name="unauthorizedUrl" value="/refuse.jsp" />
<!--注入自定认证过滤器-->
<property name="filters">
<map>
<entry key="authc" value-ref="customFormAuthenticationFilter"/>
</map>
</property>
<!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 左边是url,右边是shiro拦截器简称-->
<property name="filterChainDefinitions">
<value>
<!-- 退出拦截,请求logout.action执行退出操作,shiro会自动清除session -->
/logout.action = logout
<!-- 无权访问页面 -->
/refuse.jsp = anon
<!-- roles[XX]表示有XX角色才可访问-->
<!--/item/list.action = roles[item],authc-->
<!-- /items/queryItems.action=perms[user:query]-->
/js/** anon
/images/** anon
/styles/** anon
/validatecode.jsp anon
<!--配置记住我或者认证成功后拦截-->
/index.jsp=user
/first.action= user
/welcome.jsp=user
<!--/item/* authc-->
<!-- user表示身份认证通过或通过记住我认证通过的可以访问 -->
/** = authc
</value>
</property>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm" />
<!--配置session管理器-->
<property name="sessionManager" ref="sessionManager"/>
<!--注入缓存bean-->
<property name="cacheManager" ref="cacheManager"/>
<!--记住我-->
<property name="rememberMeManager" ref="rememberMeManage"/>
</bean>
<!--配置自定义认证过滤器,用于验证码判断-->
<bean id="customFormAuthenticationFilter" class="com.cad.ssm_web.util.filter.CustomFormAuthenticationFilter">
<property name="usernameParam" value="username"/>
<property name="passwordParam" value="password"/>
</bean>
<!-- 自定义 realm -->
<bean id="customRealm" class="com.cad.ssm_web.util.CustomRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
<!--凭证匹配器-->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--散列算法名称-->
<property name="hashAlgorithmName" value="md5"/>
<!--散列算法散列次数-->
<property name="hashIterations" value="1"/>
</bean>
<!--配置ehcahe缓存-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<!--配置session管理器-->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="60000"/>
<!--是否删除失效的session-->
<property name="deleteInvalidSessions" value="true"/>
<property name="sessionIdCookie">
<bean class="org.apache.shiro.web.servlet.SimpleCookie">
<property name="name" value="JSESSID" />
</bean>
</property>
</bean>
<!--配置rememberMe管理器-->
<bean id="rememberMeManage" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="rememberMeCookie"/>
</bean>
<!--记住我cookie-->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<property name="maxAge" value="2592000"/>
</bean>
</beans>
登录controller
@RequestMapping("login")
public String login(HttpServletRequest request) throws Exception {
//登录失败shiro会返回一个认证异常信息,注意这是shiro设置到request域中的属性
String exception= (String) request.getAttribute("shiroLoginFailure");
if (exception !=null) {
if (UnknownAccountException.class.getName().equals(exception)) {
throw new CustomException("用户名不存在!");
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
throw new CustomException("用户名或密码不正确!");
} else if ("validateCodeFaile".equals(exception)){
throw new CustomException("验证码错误!");
}else {
throw new Exception();
}
}
//TODO 登录失败跳转login.jsp,认证成功跳转到上一个请求
return "login";
}
3.使用协议
第一openid协议,解决用户的认证,比如所有互联网登陆公司遵守该协议,统一有辨识该用户的唯一标识openid,用户登录其他系统只需要验证openid即可,但是由于各个互联网公司的数据不共享,导致该协议没有遵守下去;
第二:OAuth2.0协议,解决用户授权问题,例如微信小程序获取用户信息,需要有微信服务第三方来接入,基本流程如下:小程序征求用户同意获取授权信息,授权后,小程序再去微信服务器通过token验证是否正确,然后微信服务器再将用户的授权信息发送给小程序;
第三:OIDC1.0协议,该协议完全解决了用户认证与授权的两个过程,详情见https://www.oschina.net/news/73532/oidc-oauth-2
学习博客:
http://www.sojson.com/shiro#so231722903
https://www.cnblogs.com/1315925303zxz/p/6874219.html
拦截器:http://jinnianshilongnian.iteye.com/blog/2025656