1、概述
AdminEAP为本人基于AdminLTE改造的后台管理框架,包含了基本的系统管理功能和各种交互demo,项目已经开源到Github,并部署到阿里云。
Github : https://github.com/bill1012/AdminEAP
AdminEAP DEMO: http://www.admineap.com
本文介绍在AdminEAP框架下集成Shiro的配置和核心代码,集成shiro之后,把系统的认证和鉴权给shiro管理。下图为集成之后的登录界面。
2、Shiro简介
Apache Shiro是Java的一个安全框架。目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛,而且Shiro的API也是非常简单;其基本功能点如下图所示:
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
- Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
- Web Support:Web支持,可以非常容易的集成到Web环境;
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
- Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
更详细的说明在这里不展开,可参考博客 第一章 Shiro简介——《跟我学Shiro》
3、具体实现
AdminEAP使用了Spring框架,所以是Spring集成Shiro
3.1 web.xml配置
放在web.xml前面,让框架扫描到
<filter><filter-name>shiroFilter</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class><init-param><param-name>targetFilterLifecycle</param-name><param-value>true</param-value></init-param></filter><filter-mapping><filter-name>shiroFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping>
3.2 spring-shiro.xml的配置文件(重点)
这个配置文件需要web.xml引用到,在AdminEAP中,通过以下xml片段引用了所有的spring的配置。
<context-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring*.xml</param-value></context-param>
spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/data/jpa"><description>Shiro Configuration</description><!-- 缓存管理--><bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean><!-- 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的ShiroDbRealm.java --><bean id="adminRealm" class="com.cnpc.framework.filter.SystemAuthorizingRealm" /><!-- Shiro默认会使用Servlet容器的Session,可通过sessionMode属性来指定使用Shiro原生Session --><!-- 即<property name="sessionMode" value="native"/>,详细说明见官方文档 --><!-- 这里主要是设置自定义的单Realm应用,若有多个Realm,可使用'realms'属性代替 --><bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"><property name="realm" ref="adminRealm" /><property name="cacheManager" ref="cacheManager"/></bean><!-- Shiro主过滤器本身功能十分强大,其强大之处就在于它支持任何基于URL路径表达式的、自定义的过滤器的执行 --><!-- Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截,Shiro对基于Spring的Web应用提供了完美的支持 --><bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"><!-- Shiro的核心安全接口,这个属性是必须的 --><property name="securityManager" ref="securityManager" /><!-- 要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.html"页面 --><property name="loginUrl" value="/login" /><!-- 登录成功后要跳转的连接(本例中此属性用不到,因为登录成功后的处理逻辑在LoginController里硬编码为main.jsp了) --><!-- <property name="successUrl" value="/system/main"/> --><!-- 用户访问未对其授权的资源时,所显示的连接 --><!-- 若想更明显的测试此属性可以修改它的值,如unauthor.jsp --><property name="unauthorizedUrl" value="/login" /><!-- Shiro连接约束配置,即过滤链的定义 --><!-- 此处可配合我的这篇文章来理解各个过滤连的作用http://blog.csdn.net/jadyer/article/details/12172839 --><!-- 下面value值的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的 --><!-- anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种 --><!-- authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter --><property name="filterChainDefinitions"><value><!--登录界面可匿名-->/login=anon<!--注销可匿名-->/logout=anon<!--静态资源可匿名-->/resources/**=anon<!--其他要认证-->/**=authc </value></property></bean><!-- 保证实现了Shiro内部lifecycle函数的bean执行 --><bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /><!-- 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 --><!-- 配置以下两个bean即可实现此功能 --><!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run --><!-- 由于本例中并未使用Shiro注解,故注释掉这两个bean(个人觉得将权限通过注解的方式硬编码在程序中,查看起来不是很方便,没必要使用) --><!-- <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/></bean> -->
</beans>
3.3 自定义Realm的SystemAuthorizingRealm.java的实现
package com.cnpc.framework.filter;import com.cnpc.framework.base.entity.User;
import com.cnpc.framework.base.service.FunctionService;
import com.cnpc.framework.base.service.RoleService;
import com.cnpc.framework.base.service.UserService;
import com.cnpc.framework.utils.PropertiesUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
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.stereotype.Service;import javax.annotation.Resource;
import java.util.Set;/*** @author billjiang qq:475572229* 系统安全认证实现类*/
@Service
public class SystemAuthorizingRealm extends AuthorizingRealm {/*** 认证回调函数, 登录时调用*/@Resourceprivate UserService userService;@Resourceprivate RoleService roleService;@Resourceprivate FunctionService functionService;/*** 用户认证** @param authcToken 含登录名密码的信息* @return 认证信息*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {if (authcToken == null)throw new AuthenticationException("parameter token is null");UsernamePasswordToken token = (UsernamePasswordToken) authcToken;// 校验用户名密码String password=String.copyValueOf(token.getPassword());User user= userService.getUserByLoginName(token.getUsername());if (user!=null) {if(!password.equals(user.getPassword())&& isNeedPassword()){throw new IncorrectCredentialsException();}// 注意此处的返回值没有使用加盐方式,如需要加盐,可以在密码参数上加return new SimpleAuthenticationInfo(user, token.getPassword(), token.getUsername());}throw new UnknownAccountException();}/*** 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用 shiro 权限控制有三种* 1、通过xml配置资源的权限* 2、通过shiro标签控制权限* 3、通过shiro注解控制权限* 当调用subject.hasRole subject.isPermitted 或者shiro注解/标签的时候会调用本方法,获取的数据如果配置了缓存会存在缓存中*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {if (principals == null) {throw new AuthorizationException("parameters principals is null");}//获取已认证的用户名(登录名)String username=(String)super.getAvailablePrincipal(principals);Set<String> roleCodes=roleService.getRoleCodeSet(username);Set<String> functionCodes=functionService.getFunctionCodeSet(roleCodes);SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();authorizationInfo.setRoles(roleCodes);authorizationInfo.setStringPermissions(functionCodes);return authorizationInfo;}//是否需要校验密码登录,用于开发环境 0默认为开发环境,其他为正式环境(1,或者不配)public boolean isNeedPassword(){String version=PropertiesUtil.getValue("system.version");if("0".equals(version))return false;elsereturn true;}
}
3.4 登录与注销代码
package com.cnpc.framework.base.controller;import com.cnpc.framework.base.pojo.ResultCode;
import com.cnpc.framework.base.service.RoleService;
import com.cnpc.framework.base.service.UserService;
import com.cnpc.framework.utils.EncryptUtil;
import com.cnpc.framework.utils.PropertiesUtil;
import com.cnpc.framework.utils.StrUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.Md5CredentialsMatcher;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Set;@Controller
public class LoginController {private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);@Resourceprivate RoleService roleService;private final static String MAIN_PAGE = PropertiesUtil.getValue("page.main");private final static String LOGIN_PAGE = PropertiesUtil.getValue("page.login");@RequestMapping(value = "/login")private String doLogin(HttpServletRequest request, Model model) {//已经登录过,直接进入主页Subject subject = SecurityUtils.getSubject();if (subject != null && subject.isAuthenticated()) {boolean isAuthorized = Boolean.valueOf(subject.getSession().getAttribute("isAuthorized").toString());if (isAuthorized)return MAIN_PAGE;}String userName = request.getParameter("userName");//默认首页,第一次进来if (StrUtil.isEmpty(userName)) {return LOGIN_PAGE;}String password = request.getParameter("password");//密码加密+加盐password = EncryptUtil.getPassword(password, userName);UsernamePasswordToken token = new UsernamePasswordToken(userName, password);token.setRememberMe(true);subject = SecurityUtils.getSubject();String msg;try {subject.login(token);//通过认证if (subject.isAuthenticated()) {Set<String> roles = roleService.getRoleCodeSet(userName);if (!roles.isEmpty()) {subject.getSession().setAttribute("isAuthorized", true);return MAIN_PAGE;} else {//没有授权msg = "您没有得到相应的授权!";model.addAttribute("message", new ResultCode("1", msg));subject.getSession().setAttribute("isAuthorized", false);LOGGER.error(msg);return LOGIN_PAGE;}} else {return LOGIN_PAGE;}//0 未授权 1 账号问题 2 密码错误 3 账号密码错误} catch (IncorrectCredentialsException e) {msg = "登录密码错误. Password for account " + token.getPrincipal() + " was incorrect";model.addAttribute("message", new ResultCode("2", msg));LOGGER.error(msg);} catch (ExcessiveAttemptsException e) {msg = "登录失败次数过多";model.addAttribute("message", new ResultCode("3", msg));LOGGER.error(msg);} catch (LockedAccountException e) {msg = "帐号已被锁定. The account for username " + token.getPrincipal() + " was locked.";model.addAttribute("message", new ResultCode("1", msg));LOGGER.error(msg);} catch (DisabledAccountException e) {msg = "帐号已被禁用. The account for username " + token.getPrincipal() + " was disabled.";model.addAttribute("message", new ResultCode("1", msg));LOGGER.error(msg);} catch (ExpiredCredentialsException e) {msg = "帐号已过期. the account for username " + token.getPrincipal() + " was expired.";model.addAttribute("message", new ResultCode("1", msg));LOGGER.error(msg);} catch (UnknownAccountException e) {msg = "帐号不存在. There is no user with username of " + token.getPrincipal();model.addAttribute("message", new ResultCode("1", msg));LOGGER.error(msg);} catch (UnauthorizedException e) {msg = "您没有得到相应的授权!" + e.getMessage();model.addAttribute("message", new ResultCode("1", msg));LOGGER.error(msg);}return LOGIN_PAGE;}@RequestMapping(value = "/logout")private String doLogout() {Subject subject = SecurityUtils.getSubject();subject.logout();return LOGIN_PAGE;}}
3.5 前台登录界面代码 login.html核心脚本
$(function () {$('input').iCheck({checkboxClass: 'icheckbox_square-blue',radioClass: 'iradio_square-blue',increaseArea: '20%' // optional});fillbackLoginForm();$("#login-form").bootstrapValidator({message:'请输入用户名/密码',submitHandler:function (valiadtor,loginForm,submitButton) {rememberMe($("input[name='rememberMe']").is(":checked"));valiadtor.defaultSubmit();},fields:{userName:{validators:{notEmpty:{message:'登录邮箱名或用户名不能为空'}}},password:{validators:{notEmpty:{message:'密码不能为空'}}}}});<#if message??>new LoginValidator({code:"${message.code?default('-1')}",message:"${message.message?default('')}",userName:'userName',password:'password'})</#if>});//使用本地缓存记住用户名密码function rememberMe(rm_flag){//remember meif(rm_flag){localStorage.userName=$("input[name='userName']").val();localStorage.password=$("input[name='password']").val();localStorage.rememberMe=1;}//delete remember msgelse{localStorage.userName=null;localStorage.password=null;localStorage.rememberMe=0;}}//记住回填function fillbackLoginForm(){if(localStorage.rememberMe&&localStorage.rememberMe=="1"){$("input[name='userName']").val(localStorage.userName);$("input[name='password']").val(localStorage.password);$("input[name='rememberMe']").iCheck('check');$("input[name='rememberMe']").iCheck('update');}}
以上的代码是集成shiro认证和鉴权的功能,可能还有些相关的代码,比如密码加密(加盐)没有放上来,不过所有的代码已经放到Github上,大家可以上我的文章头部的Github上来获取所有的代码,让我们开始用Shiro来管理我们的系统安全吧。