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 {
/**
* 认证回调函数, 登录时调用
*/
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private FunctionService functionService;
/**
* 用户认证
*
* @param authcToken 含登录名密码的信息
* @return 认证信息
*/
@Override
protected 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注解/标签的时候会调用本方法,获取的数据如果配置了缓存会存在缓存中
*/
@Override
protected 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;
else
return 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);
@Resource
private 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 me
if(rm_flag){
localStorage.userName=$("input[name='userName']").val();
localStorage.password=$("input[name='password']").val();
localStorage.rememberMe=1;
}
//delete remember msg
else{
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来管理我们的系统安全吧。