在我们的开发当中 我们一般权限都是个 比较繁琐 但又必不可少的 一部分 【不管我们的 数据库设计 还是我们采用何种技术 我们的权限库表 大多都是大同小异 业务逻辑也是如此】 在我们不使用任何框架的时候 我们也是可以做到 但是细节过于麻烦 在很多时候 都是重复造轮子的过程 所以出现了 很多开源比较休息的额权限框架如:shiro Spring security。。。。。
废话不多说了 今天我们来讲讲shiro
在我们 设计库表时候 : 我们大多情况 是这样的
用户表 :
用户角色关系表:
角色表 :
角色菜单关系表:
菜单表:
shiro单机集成spring下:
<!-- 保证实现了 Shiro 内部 lifecycle 函数的 bean 执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<!-- 用户授权信息Cache(本机内存实现)-->
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
<!-- shiro 的自带 ehcahe 缓存管理器 -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManagerConfigFile" value="classpath:config/shiro/ehcache-shiro.xml"/>
</bean>
<!--自定义Realm -->
<bean id="myRealm" class="com.system.shiro.MyRealm"/>
<!-- 凭证匹配器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm" />
<!-- redis 缓存 -->
<property name="cacheManager" ref="cacheManager" />
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/index.jsp" />
<property name="successUrl" value="/loginSuccess.shtml" />
<property name="filterChainDefinitions">
<value>
<!-- 静态资源放行 -->
/statics/** = anon
/common/** = anon
/error/** = anon
<!-- 登录资源放行 -->
/toLogin/** = anon
/login/** = anon
<!-- shiro 自带登出 -->
/logout = logout
</value>
</property>
</bean>
最简单的shiro集成
我们只是需要自己写个 Realm 实现 AuthorizingRealm 实现它的两个方法 【认证 和 授权】 在授权里 我们要把角色集合 和 权限集合 放进去
在认证里 我们需要注意的是 我们可以自定义 密码加密方法覆写 setCredentialsMatcher方法
@PostConstruct //初始的时候 加载一次 在init 之前 运行
public void initCredentialsMatcher() {
//该句作用是重写shiro的密码验证,让shiro用自己的验证
setCredentialsMatcher(new CustomCredentialsMatcher());
}
/**
* 自定义shiro验证时使用的加密算法
*/
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
Object tokenCredentials = encrypt(token);
Object accountCredentials = getCredentials(info);//数据库密码
//将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false
return equals(tokenCredentials, accountCredentials);
}
private String encrypt(UsernamePasswordToken token) {
String password = PasswordUtil.encrypt(token.getUsername().toLowerCase(), String.valueOf(token.getPassword()),
PasswordUtil.getStaticSalt());
return password;
}
}
密码加密工具类
import org.junit.Test;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.security.Key;
import java.security.SecureRandom;
public class PasswordUtil {
/**
* JAVA6支持以下任意一种算法 PBEWITHMD5ANDDES PBEWITHMD5ANDTRIPLEDES
* PBEWITHSHAANDDESEDE PBEWITHSHA1ANDRC2_40 PBKDF2WITHHMACSHA1
* */
/**
* 定义使用的算法为:PBEWITHMD5andDES算法
*/
public static final String ALGORITHM = "PBEWithMD5AndDES";//加密算法
public static final String Salt = "63293188";//密钥
/**
* 定义迭代次数为1000次
*/
private static final int ITERATIONCOUNT = 1000;
/**
* 获取加密算法中使用的盐值,解密中使用的盐值必须与加密中使用的相同才能完成操作. 盐长度必须为8字节
*
* @return byte[] 盐值
* */
public static byte[] getSalt() throws Exception {
// 实例化安全随机数
SecureRandom random = new SecureRandom();
// 产出盐
return random.generateSeed(8);
}
public static byte[] getStaticSalt() {
// 产出盐
return Salt.getBytes();
}
/**
* 根据PBE密码生成一把密钥
*
* @param password
* 生成密钥时所使用的密码
* @return Key PBE算法密钥
* */
private static Key getPBEKey(String password) {
// 实例化使用的算法
SecretKeyFactory keyFactory;
SecretKey secretKey = null;
try {
keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
// 设置PBE密钥参数
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
// 生成密钥
secretKey = keyFactory.generateSecret(keySpec);
} catch (Exception e) {
e.printStackTrace();
}
return secretKey;
}
/**
* 加密明文字符串
*
* @param plaintext
* 生成密钥时所使用的密码
* @param password
* 待加密的明文字符串
* @param salt
* 盐值
* @return 加密后的密文字符串
* @throws Exception
*/
public static String encrypt(String plaintext, String password, byte[] salt) {
Key key = getPBEKey(password);
byte[] encipheredData = null;
PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATIONCOUNT);
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
encipheredData = cipher.doFinal(plaintext.getBytes());
} catch (Exception e) {
}
return bytesToHexString(encipheredData);
}
/**
* 解密密文字符串
*
* @param ciphertext
* 待解密的密文字符串
* @param password
* 生成密钥时所使用的密码(如需解密,该参数需要与加密时使用的一致)
* @param salt
* 盐值(如需解密,该参数需要与加密时使用的一致)
* @return 解密后的明文字符串
* @throws Exception
*/
public static String decrypt(String ciphertext, String password, byte[] salt) {
Key key = getPBEKey(password);
byte[] passDec = null;
PBEParameterSpec parameterSpec = new PBEParameterSpec(getStaticSalt(), ITERATIONCOUNT);
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
passDec = cipher.doFinal(hexStringToBytes(ciphertext));
}
catch (Exception e) {
e.printStackTrace();
}
return new String(passDec);
}
/**
* 将字节数组转换为十六进制字符串
*
* @param src
* 字节数组
* @return
*/
public static String bytesToHexString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
/**
* 将十六进制字符串转换为字节数组
*
* @param hexString
* 十六进制字符串
* @return
*/
public static byte[] hexStringToBytes(String hexString) {
if (hexString == null || hexString.equals("")) {
return null;
}
hexString = hexString.toUpperCase();
int length = hexString.length() / 2;
char[] hexChars = hexString.toCharArray();
byte[] d = new byte[length];
for (int i = 0; i < length; i++) {
int pos = i * 2;
d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
}
return d;
}
private static byte charToByte(char c) {
return (byte) "0123456789ABCDEF".indexOf(c);
}
@Test
public void test() {
int i=10;
for (int j = 0; j < i; j++) {
if((j)%3==0)
{
System.out.print("<br>");
}
else {
System.out.print(j);
}
}
System.out.print(-1%2==0);
String str = "admin";
String password = "123456";
LogUtil.info("明文:" + str);
LogUtil.info("密码:" + password);
try {
byte[] salt = PasswordUtil.getStaticSalt();
String ciphertext = PasswordUtil.encrypt(str, password, salt);
LogUtil.info("密文:" + ciphertext);
String plaintext = PasswordUtil.decrypt(ciphertext, password, salt);
LogUtil.info("明文:" + plaintext);
} catch (Exception e) {
e.printStackTrace();
}
}
}
而且我们在{ SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, user.getPassword(), getName());} 红色标识的地方 有时候 我们是写对象 有的时候是写 用户名
这块我 强调一下 这里是根据 SecurityUtils.getSubject().getPrincipal() 后面要获取的 也就是 我们在上面 往里放什么 后面取什么
shiro 集群版的 session共享
我只要 把session 管理权力移交出来 交给 缓存管理 达到 一个资源的共享 这里 shiro 给我准备了一个默认的native session manager,DefaultWebSessionManager,所以我们要修改 spring 配置文件,注入 DefaultWebSessionManager。我们继续看DefaultWebSessionManager的源码,发现其父类 DefaultSessionManager 中有sessionDAO 属性,这个属性是真正实现了session储存的类,这个就是我们自己实现的 redis session的储存类。
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.system.utils.RedisManager;
import com.system.utils.SerializerUtil;
public class RedisSessionDao extends AbstractSessionDAO {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private RedisManager redisManager;
/**
* The Redis key prefix for the sessions
*/
private static final String KEY_PREFIX = "shiro_redis_session:";
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return;
}
redisManager.del(KEY_PREFIX + session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<Session>();
Set<byte[]> keys = redisManager.keys(KEY_PREFIX + "*");
if(keys != null && keys.size()>0){
for(byte[] key : keys){
Session s = (Session)SerializerUtil.deserialize(redisManager.get(SerializerUtil.deserialize(key)));
sessions.add(s);
}
}
return sessions;
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
logger.error("session id is null");
return null;
}
Session s = (Session)redisManager.get(KEY_PREFIX + sessionId);
return s;
}
private void saveSession(Session session) throws UnknownSessionException{
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return;
}
//设置过期时间
long expireTime = 1800000l;
session.setTimeout(expireTime);
redisManager.setEx(KEY_PREFIX + session.getId(), session, expireTime);
}
public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
}
public RedisManager getRedisManager() {
return redisManager;
}
}
import java.io.Serializable;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class RedisManager {
@Autowired
private RedisTemplate<Serializable, Serializable> redisTemplate;
/**
* 过期时间
*/
// private Long expire;
/**
* 添加缓存数据(给定key已存在,进行覆盖)
* @param key
* @param obj
* @throws DataAccessException
*/
public <T> void set(String key, T obj) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
redisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.set(bkey, bvalue);
return null;
}
});
}
/**
* 添加缓存数据(给定key已存在,不进行覆盖,直接返回false)
* @param key
* @param obj
* @return 操作成功返回true,否则返回false
* @throws DataAccessException
*/
public <T> boolean setNX(String key, T obj) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.setNX(bkey, bvalue);
}
});
return result;
}
/**
* 添加缓存数据,设定缓存失效时间
* @param key
* @param obj
* @param expireSeconds 过期时间,单位 秒
* @throws DataAccessException
*/
public <T> void setEx(String key, T obj, final long expireSeconds) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
connection.setEx(bkey, expireSeconds, bvalue);
return true;
}
});
}
/**
* 获取key对应value
* @param key
* @return
* @throws DataAccessException
*/
public <T> T get(final String key) throws DataAccessException{
byte[] result = redisTemplate.execute(new RedisCallback<byte[]>() {
@Override
public byte[] doInRedis(RedisConnection connection) throws DataAccessException {
return connection.get(key.getBytes());
}
});
if (result == null) {
return null;
}
return SerializerUtil.deserialize(result);
}
/**
* 删除指定key数据
* @param key
* @return 返回操作影响记录数
*/
public Long del(final String key){
if (StringUtils.isEmpty(key)) {
return 0l;
}
Long delNum = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keys = key.getBytes();
return connection.del(keys);
}
});
return delNum;
}
public Set<byte[]> keys(final String key){
if (StringUtils.isEmpty(key)) {
return null;
}
Set<byte[]> bytesSet = redisTemplate.execute(new RedisCallback<Set<byte[]>>() {
@Override
public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keys = key.getBytes();
return connection.keys(keys);
}
});
return bytesSet;
}
}
序列化工具类
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
/**
* 序列化工具类
* @author HandyZcy
*
*/
public class SerializerUtil {
private static final JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
/**
* 序列化对象
* @param obj
* @return
*/
public static <T> byte[] serialize(T obj){
try {
return jdkSerializationRedisSerializer.serialize(obj);
} catch (Exception e) {
throw new RuntimeException("序列化失败!", e);
}
}
/**
* 反序列化对象
* @param bytes 字节数组
* @param cls cls
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T deserialize(byte[] bytes){
try {
return (T) jdkSerializationRedisSerializer.deserialize(bytes);
} catch (Exception e) {
throw new RuntimeException("反序列化失败!", e);
}
}
}
整体配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
"
>
<description>Shiro安全配置</description>
<!-- 分布式 配置参考:http://blog.csdn.net/lishehe/article/details/45223823 -->
<!-- 保证实现了 Shiro 内部 lifecycle 函数的 bean 执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<!--
用户授权信息Cache(本机内存实现)
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
-->
<!-- shiro 的自带 ehcahe 缓存管理器
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManagerConfigFile" value="classpath:config/shiro/ehcache-shiro.xml"/>
</bean>
-->
<!-- 自定义cacheManager -->
<bean id="redisCache" class="com.system.shiro.RedisCache">
<constructor-arg ref="redisManager"></constructor-arg>
</bean>
<!-- 自定义redisManager-redis -->
<bean id="redisCacheManager" class="com.system.shiro.RedisCacheManager">
<property name="redisManager" ref="redisManager" />
</bean>
<!--自定义Realm -->
<bean id="myRealm" class="com.system.shiro.MyRealm"/>
<!-- 凭证匹配器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm" />
<property name="sessionMode" value="http" />
<property name="sessionManager" ref="defaultWebSessionManager" />
<!-- redis 缓存 -->
<property name="cacheManager" ref="redisCacheManager" />
</bean>
<bean id="defaultWebSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- session存储的实现 -->
<property name="sessionDAO" ref="shiroRedisSessionDAO" />
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<property name="sessionIdCookie" ref="shareSession" />
<!-- 设置全局会话超时时间,默认30分钟(1800000) -->
<property name="globalSessionTimeout" value="1800000" />
<!-- 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true -->
<property name="deleteInvalidSessions" value="true" />
<!-- 会话验证器调度时间 -->
<property name="sessionValidationInterval" value="1800000" />
<!-- 定时检查失效的session -->
<property name="sessionValidationSchedulerEnabled" value="true" />
</bean>
<!--
通过@Component 注解交由 Spring IOC 管理
<bean id="redisManager" class="com.system.utils.RedisManager"></bean>
-->
<!-- session会话存储的实现类 -->
<bean id="shiroRedisSessionDAO" class="com.system.shiro.RedisSessionDao">
<property name="redisManager" ref="redisManager"/>
</bean>
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<bean id="shareSession" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- cookie的name,对应的默认是 JSESSIONID -->
<constructor-arg name="name" value="SHAREJSESSIONID" />
<!-- jsessionId的path为 / 用于多个系统共享jsessionId -->
<property name="path" value="/" />
<property name="httpOnly" value="true"/>
</bean>
<!-- 配置shiro的过滤器工厂类,id- shiroFilter要和我们在web.xml中配置的过滤器一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager" />
<!-- 要求登录时的链接,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
<property name="loginUrl" value="/index.jsp" />
<!-- 登录成功后要跳转的连接 -->
<property name="successUrl" value="/loginSuccess.shtml" />
<!-- 用户访问未对其授权的资源时,所显示的连接 -->
<property name="unauthorizedUrl" value="/error/forbidden.jsp" />
<!-- 自定义权限配置:url 过滤在这里做 -->
<property name="filterChainDefinitions">
<!-- 参考:http://blog.csdn.net/jadyer/article/details/12172839 -->
<!--
Shiro验证URL时,URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时)故filterChainDefinitions的配置顺序为自上而下,以最上面的为准
-->
<!-- Pattern里用到的是两颗星,这样才能实现任意层次的全匹配 -->
<value>
<!-- 静态资源放行 -->
/statics/** = anon
/common/** = anon
/error/** = anon
<!-- 登录资源放行 -->
/toLogin/** = anon
/login/** = anon
<!-- shiro 自带登出 -->
/logout = logout
<!-- 表示用户必需已通过认证,并拥有 superman 角色 && superman:role:list 权限才可以正常发起'/role'请求-->
/role/** = authc,roles[superman],perms[superman:role:list]
/right/** = authc,roles[superman],perms[superman:right:list]
/manager/preEditPwd = authc
/manager/editUserBase = authc
<!-- 表示用户必需已通过认证,并拥有 superman 角色 && superman:manager:list 才可以正常发起'/manager'请求 -->
/manager/** = authc,roles[superman],perms[superman:manager:list]
/** = authc
</value>
</property>
</bean>
<!-- 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 -->
<!-- 配置以下两个bean即可实现此功能 -->
<!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run -->
<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>