springmvc 是现在主流的mvc框架,shiro是一款轻量级安全框架。与spring security不同,shiro的配置简单,更加容易上手。所以这次采用了springmvc + shiro的组合,来实现简单的权限管理。废话不多说,首先上代码。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itsu</groupId>
<artifactId>shiro</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>shiro Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.apache.shiro</groupId>-->
<!-- <artifactId>shiro-all</artifactId>-->
<!-- <version>1.3.2</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.14</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>com.yugabyte</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0-yb-11</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-cache-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-cache</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
</dependencies>
<build>
<finalName>shiro</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
在引入了相关jar包后开始搭建spring mvc + shiro环境
1.修改web.xml 加入shiro 过滤器以及引入spring & springmvc配置文件,并进行基本的springmvc环境。关于springmvc的配置网上应该有很多,这里就不再赘述了,在博文最后我会贴上相关配置。
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<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>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-*.xml</param-value>
</context-param>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>characterFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
2. 将ShiroFilterFactoryBean交给spring进行管理。其中shiro内置了几个过滤器,其中 anon 表示无需认证即可访问。authc 表示需要认证才能访问;user 表示设置了"记住我“后可以访问的url;logout表示推出系统的过滤器。
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/>
<property name="unauthorizedUrl" value="/error/noAuth"/>
<property name="filterChainDefinitions">
<value>
/logout = logout
/static/** = anon
/login.do = anon
/login = anon
/ = user
/** = authc
</value>
</property>
</bean>
也可以根据自己的需要去自定义过滤器。然后可以通过 filters属性进行spirng注入。如:
<property name="filters">
<util:map>
<entry key="sessionFilter" value-ref="sessionfilter"></entry>
</util:map>
</property>
<bean class="com.itsu.app.shrio.filter.Sessionfilter" id="sessionfilter"/>
3.配置自定义realm。 需要继承AuthorizingRealm,并重写它的两个方法。其中doGetAuthenticationInfo方法是用来验证登陆的。而doGetAuthorizationInfo是用来判断授权信息的。在做认证的时候需要从访问数据库得到用户的账号和密码。
public class MyRealm extends AuthorizingRealm {
@Resource
private UserMapper userMapper;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String userName = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set<String> roles = getRolesByUsername(userName);
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
/*模拟获取角色,实际情况应该从数据库获取*/
private Set<String> getRolesByUsername(String userName) {
Set<String> roles = new HashSet<>();
roles.add("admin");
roles.add("user");
return roles;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = (String) token.getPrincipal();
User user = getUserByUserName(userName);
if (user == null) {
return null;
}
int status = user.getStatus();
// int status = 0;
if (status != 0) {
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, user.getPassword(), getName());
/*设置密码加盐*/
authenticationInfo.setCredentialsSalt(ByteSourceUtil.bytes(userName));
return authenticationInfo;
}
public User getUserByUserName(String userName) {
System.out.println("从数据库中获取数据");
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name", userName);
User user = userMapper.selectOne(queryWrapper);
return user;
}
}
4.创建webSecurityManager并交给spring进行管理。并将自定义realm注入到DefaultWebSecurityManager中。这里需要注意,HashedCredentialsMatcher这个类是shiro提供的用于密码加密的类。我们可以自定义加密的方式,和加密算法迭代的次数。hashAlgorithmName这个属性表示是hash散裂算法的名称,我用的是MD5,hashIterations表示迭代次数,我设置为1表述密码只需要进行一次MD5加密。需要注意的是,在用来测试时,在数据库中存放的密码是需要先经过MD5加密后的。也就是说,shiro只会比较加密后的密码,和用户所输入的密码是否匹配。
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm"/>
</bean>
<!--自定义realm实现-->
<bean id="myRealm" class="com.itsu.app.shrio.realm.MyRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"></property>
<property name="name" value="MyRealm"></property>
</bean>
<!--配置密码MD5加密-->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher" id="credentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="1"/>
</bean>
到此为止,一个最基本的spring mvc + shiro环境就已经搭建完成了。我们来写一个登陆功能来看看效果。
<div align="center" style="text-align: center">
<form id="form">
<div>
<label for="userName">用戶名</label>
<input type="text" id="userName" name="userName">
</div>
<div>
<label for="password">密码</label>
<input type="password" id="password" name="password">
</div>
<div>
<input type="submit" value="提交">
<input type="reset" value="重置">
</div>
</form>
</div>
</body>
<script type="text/javascript" src="/static/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript">
$(function () {
$("#form").submit(function () {
var userName = $("#userName").val();
var password = $("#password").val();
var rememberMe = $("#rememberMe").is(":checked");
// alert(rememberMe);
// return false;
var formData = {userName: userName, password: password, rememberMe: rememberMe};
submitData(formData);
return false;
})
function submitData(formData) {
$.ajax({
url: "/login.do",
type: "post",
dataType: "json",
data: formData,
success: function (data) {
if (data.code == '200') {
alert("success " + data.msg);
} else {
alert("error: " + data.msg);
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
console.log(XMLHttpRequest.responseText);
console.log(textStatus);
}
})
}
})
</script>
后台controller代码
@PostMapping("/login.do")
@ResponseBody
public Map login(User user) {
Map map = new HashMap();
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword());
subject.login(token);
if (subject.hasRole("admin")) {
System.out.println("有admin权限");
}
map.put("code", "200");
map.put("msg", "登录成功");
return map;
}
访问http://localhost:8080,会自动跳转到login.html,这是因为配置了<property name="loginUrl" value="/login"/>。当shiro发现你没有进行登陆验证的时候,会主动跳转到你配置的登陆url 也就是 /login。然后我们输入账号和密码看看会发生什么。
输入自己预先设置好的账号和密码,点击登陆。我们看到这里已经登陆成功了。
查看控制台,可以看到控制台打印出来了当前用户拥有admin权限。
5. 通过注解的形式配置(角色、权限)管理
首先需要引入两个类让spring进行管理。 这里有一个坑,这两个bean对象需要在spring-mvc.xml配置文件中进行配置。放在applicationContext xml中无效。 另外shiro底层是通过spring-aop技术实现的注解形式的权限配置。所以,我们在需要在spring-mvc.xml中引入aop的配置。
<!--配置通过注解控制可访问的URL-->
<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor" id="lifecycleBeanPostProcessor"></bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"
id="attributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
<!--开启aop代理-->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
在controller中写两个方法来测试。 @RequriesRoles(value={需要的角色})。对于权限的设置可以使用@RequiresPermissions()注解进行配置,连个注解的使用是一样的,所以省略了@RequiresPermissions()
@RequiresRoles(value = {"admin"})
@GetMapping("/testRole")
@ResponseBody
public String testRole() {
return "admin";
}
@ResponseBody
@RequiresRoles(value = {"admin1"})
@GetMapping(value = "/testRole1")
public String testRole1() {
return "admin1角色";
}
在浏览器中输入http://localhost:8080/testRole ,发现访问成功。
在浏览器中输入http://localhost:8080/testRole1,发现报错了。页面显示Subject does not have role [admin1]。这是因为shiro发现当前登陆的用户没有admin1的角色,于是会抛出AuthenticationException,我在后台配置了全局异常捕获。会自动跳转到500.html错误也,并打印错误信息。
@ControllerAdvice
public class MyExceptionHandler {
private Logger logger = LoggerFactory.getLogger(MyExceptionHandler.class);
@ExceptionHandler(value = RuntimeException.class)
public Object handlerRuntimeException(HttpServletRequest req, HttpServletResponse resp, Exception e) {
logger.error(e.getMessage());
ModelAndView mv = new ModelAndView("/error/500");
mv.addObject("msg", e.getMessage());
return mv;
}
@ExceptionHandler(value = AuthenticationException.class)
@ResponseBody
public Object handlerAuthenticationException(HttpServletRequest req, HttpServletResponse resp, Exception e) {
logger.error(e.getMessage());
Map map = new HashMap();
map.put("code", "500");
map.put("msg", "用户名或密码错误");
return map;
}
}
查看控制台可以很明确的发现这一情况。
6. shiro配置session管理,并使用redis用于存储session
首先需要配置spring-redis相关配置。如下:
需要配置jedisConnectionFactory 并配置上你的redis服务器的地址,用户名和密码,端口,数据库编号等等。。。我们可以自己去阿里云、百度云、腾讯云等等购买一台云服务器并配置好redis环境。也可以在自己本地搭建一个redis server。如果是在本地构建redis server,推荐用VMware创建一台Linux虚拟机(任何发行版都可,如Ubuntu、Centos ...),因为redis官方任务redis是应用在服务器端的 key -value内存数据库,因此官方没有推出windows版本。 如果小伙伴们不会用Linux 或者 Linux不够熟练,还有一个办法,虽然redis官方没有推出redis的windows版本,但是微软爸爸倒是十分热心的提供了相应版本。可以通过这个url 去github上下载。https://github.com/MicrosoftArchive/redis ,但是redis的windows版,目前最新的版本还停留在3.x版本。并且似乎很久没有更新过了...(可能是不再维护了吧.... 你懂的...)
<?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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" id="jedisConnectionFactory"
p:usePool="true" p:poolConfig-ref="poolConfig" p:hostName="xxx.xx.xxx.xx" p:port="6379" p:password="123456"
p:database="0">
<!--<property name="usePool" value="true"/>
<property name="hostName" value="192.168.152.129"/>-->
</bean>
<bean class="redis.clients.jedis.JedisPoolConfig" id="poolConfig">
<property name="maxTotal" value="100"/>
<property name="maxIdle" value="100"/>
<property name="minIdle" value="10"/>
<property name="maxWaitMillis" value="1000"/>
<property name="blockWhenExhausted" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="testOnCreate" value="false"/>
</bean>
<!--key采用Strng字符串,value采用二进制存储。 方便存储shiro 中的session & authen(认证) & author(授权)对象-->
<bean class="org.springframework.data.redis.core.RedisTemplate" id="redisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
<property name="keySerializer" ref="stringRedisSerializer"/>
<property name="valueSerializer" ref="jdkSerializationRedisSerializer"/>
<property name="hashKeySerializer" ref="stringRedisSerializer"/>
<property name="hashValueSerializer" ref="jdkSerializationRedisSerializer"/>
</bean>
<!--默认采用String 字符串存储基本的对象信息-->
<bean class="org.springframework.data.redis.core.StringRedisTemplate" id="stringRedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
</bean>
<!--序列化原文保存-->
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" id="stringRedisSerializer"/>
<!--json 字符串序列化-->
<bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"
id="genericJackson2JsonRedisSerializer"/>
<!--jdk默认序列化,实体类默认需要实现序列化接口-->
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"
id="jdkSerializationRedisSerializer"/>
<!-- <bean class="com.itsu.app.utils.FastJsonRedisSerializer" id="fastJsonRedisSerializer"/>-->
<!-- <bean class="com.itsu.app.utils.GenericJackson2JsonRedisSerializerEx" id="genericJackson2JsonRedisSerializerEx"/>-->
<bean id="jedisUtil" class="com.itsu.app.utils.JedisUtil"/>
</beans>
开始配置shiro 整合redis session管理。配置如下,需要自己定义RedisSessionDao,继承AbstractSessionDAO自己去写实现的方法。需要注意,在重写doCreate方法的时候一定要先调用父类的generateSessionId 方法生成一个sessionId 并且调用父类assignSessionId方法将当前要创建的session与你生成的sessionId进行绑定,不然会报错。剩下的都是一些基本的redis操作,就不做赘述了。
<!--session manager-->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="300000"/>
<property name="deleteInvalidSessions" value="true"/>
<property name="sessionDAO" ref="sessionDao"/>
</bean>
<!--redis 管理Session(shiro.session)-->
<bean id="sessionDao" class="com.itsu.app.session.RedisSessionDao"/>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>
public class RedisSessionDao extends AbstractSessionDAO {
@Resource
private JedisUtil jedisUtil;
private final String session_prefix = "shiro:session";
private String getKey(String key) {
return session_prefix + ":" + key;
}
private void saveSession(Session session) {
if (session != null && session.getId() != null) {
String key = getKey(session.getId().toString());
byte[] sessionValue = SerializationUtils.serialize(session);
jedisUtil.set(key, sessionValue);
// jedisUtil.expire(key,);
}
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionid = super.generateSessionId(session);
super.assignSessionId(session, sessionid);
saveSession(session);
return sessionid;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
return null;
}
String key = getKey(sessionId.toString());
byte[] sessionByts = jedisUtil.get(key);
Session session = (Session) SerializationUtils.deserialize(sessionByts);
return session;
}
@Override
public void update(Session session) throws UnknownSessionException {
saveSession(session);
}
@Override
public void delete(Session session) {
if (session != null && session.getId() != null) {
jedisUtil.del(getKey(session.getId().toString()));
}
}
@Override
public Collection<Session> getActiveSessions() {
Set<String> keys = jedisUtil.keys(session_prefix);
Set<Session> values = new HashSet<>();
for (String key : keys) {
Session session = (Session) SerializationUtils.deserialize(jedisUtil.get(key));
values.add(session);
}
return values;
}
}
@Component
public class JedisUtil {
@Resource
private RedisTemplate redisTemplate;
public void set(String key, byte[] value) {
redisTemplate.opsForValue().set(key, value);
}
public byte[] get(String key) {
return (byte[]) redisTemplate.opsForValue().get(key);
}
public void del(String key) {
redisTemplate.delete(key);
}
public void expire(String key, long time) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
public Set<String> keys(String keyPrefix) {
Set<String> keys = redisTemplate.keys(keyPrefix + "*");
return keys;
}
}
完成之后,我们重写启动一下服务,再次登陆一下。然后我们看看redis中是否有将session存储进来。可以看到,redis的确将session存储进来了。
7. shiro添加缓存处理。
可以看到,在我之前的配置中,认证和授权都会去访问数据库。而我们都知道,访问数据库属于IO 操作,需要读写硬盘。这样的效率一定会很差。理想状态下,用户只需要在第一次登陆的时候访问数据库获取信息进行认证。之后我们将认证信息存储在缓存中。下一次再进行相关操作直接从缓存中读取,而不再去读数据库。这样就能大大的提升性能。而我们的项目中刚好又用到了redis,redis可是时下最主流的缓存的解决方案了。所以我们可以用shiro + reids实现缓存功能。
需要了解的是,shiro默认支持多种缓存方案。redis、Ehache 包括最简单的内存缓存(即定义一个全局的CurrentHashMap用来存储数据)。这里我只针对redis这种缓存方案进行整理。
看配置,首先我定义了一个RedisCacheManager类交给spring进行管理,并传入了一个list,list中存入两个值,分别是缓存的名字。 authenCache表示认证的缓存名,authorCache表示授权的缓存名。创建RedisCache 类实现Cache接口。重写它需要重写的方法。在自定义realm中还需要开启authenticationCachingEnabled =true,和 authorizationCachingEnabled = true表示开启认证和授权的缓存处理。
<bean id="redisCacheManager" class="com.itsu.app.shrio.cache.RedisCacheManager">
<property name="cacheNames">
<list>
<value>authenCache</value>
<value>authorCache</value>
</list>
</property>
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm"/>
<property name="cacheManager" ref="redisCacheManager"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
<!--自定义realm实现-->
<bean id="myRealm" class="com.itsu.app.shrio.realm.MyRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"></property>
<property name="name" value="MyRealm"></property>
<property name="authenticationCachingEnabled" value="true"/>
<property name="authenticationCacheName" value="authenCache"/>
<property name="authorizationCachingEnabled" value="true"/>
<property name="authorizationCacheName" value="authorCache"/>
</bean>
public class RedisCacheManager implements CacheManager {
private List<String> cacheNames;
public void setCacheNames(List<String> cacheNames) {
this.cacheNames = cacheNames;
}
@Resource
private JedisUtil jedisUtil;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
Cache<K, V> cache = null;
if (cacheNames.contains(name)) {
cache = new RedisCache(name, jedisUtil);
}
return cache;
}
}
@Component
public class RedisCache<K, V> implements Cache<K, V> {
private JedisUtil jedisUtil;
private RedisSerializer serializer = new JdkSerializationRedisSerializer();
private final String cache_prefix = "cache";
private String cacheName;
public RedisCache() {
}
public RedisCache(String name, JedisUtil jedisUtil) {
this.cacheName = name;
this.jedisUtil = jedisUtil;
}
private String getKey(K key) {
return cache_prefix + ":" + key + ":" + this.cacheName;
}
@Override
public V get(K key) throws CacheException {
byte[] value = jedisUtil.get(getKey(key));
if (value != null) {
return (V) serializer.deserialize(value);
}
return null;
}
@Override
public V put(K key, V value) throws CacheException {
String k = getKey(key);
byte[] v = serializer.serialize(value);
jedisUtil.set(k, v);
// jedisUtil.expire(k, 600);
return value;
}
@Override
public V remove(K key) throws CacheException {
String k = getKey(key);
byte[] v = jedisUtil.get(k);
jedisUtil.del(k);
if (v != null) {
return (V) SerializationUtils.deserialize(v);
}
return null;
}
@Override
public void clear() throws CacheException {
....
}
@Override
public int size() {
....
}
@Override
public Set<K> keys() {
....
}
@Override
public Collection<V> values() {
....
}
}
完成以后我们再重启服务器来看效果,重写登陆以后查看redis的情况
发现缓存的确被写入了redis。
总结(遇到的坑):
spring + shiro + redis整合其中存在一些影藏的bug。
例1:在自定义realm中我对密码的加盐处理,需要将盐转换成 ByteSource对象,Shiro自己提供了一个Util,在ByteSource.Util.bytes() 方法,但是遗憾的是这个类没有实现序列化接口。因为我定义的redis对value的序列化方式为默认的JdkSerializationRedisSerializer,这个序列化功能可以将对象转成二进制文件存储进redis,好是很好用,但是要求被序列化的对象统统的需要实现Serializable接口。然而,shiro自己提供的SimpleByteSource并没有实现这个接口。。。(是不是很坑爹!当时我也是一点一点去撸源码才发现的这个bug),所以细心的小伙伴应该发现了我在自定义realm中并没有用shiro提供的bytesource util 而是自己定义了一个MySimpleByteSource,并提供了一个ByteSourceUtil类来将自定义realm中的”盐“转为ByteSource对象。
SimpleByteSource源码:
自己实现ByteSource接口
public class MySimpleByteSource implements ByteSource, Serializable {
private static final long serialVersionUID = 1269274896730884458L;
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public MySimpleByteSource() {
}
public MySimpleByteSource(byte[] bytes) {
this.bytes = bytes;
}
public MySimpleByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public MySimpleByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public MySimpleByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public MySimpleByteSource(File file) {
this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file);
}
public MySimpleByteSource(InputStream stream) {
this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public byte[] getBytes() {
return this.bytes;
}
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
public String toString() {
return this.toBase64();
}
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource) o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
自己提供的ByteSourceUtil
public class ByteSourceUtil {
public static ByteSource bytes(byte[] bytes) {
return new MySimpleByteSource(bytes);
}
public static ByteSource bytes(String arg0) {
return new MySimpleByteSource(arg0.getBytes());
}
}
例2:本来并不打算使用JdkSerializationRedisSerializer序列化功能,而是打算使用redis 官方推荐的GenericJackson2JsonRedisSerializer序列化功能,可以将java 对象转化为json字符串进行存储,并且在反序列化的时候不用提供对象的Class属性即可反序列化成java对象。这样redis中保存的value的可读性就能打他提升。(json总比二进制能够看得懂吧。。。) 我之前一直用的这个,但是这一次我发现GenericJackson2JsonRedisSerializer对于对象中的一些新的属性支持的不太好。在集成shiro Session这个功能的时候,GenericJackson2JsonRedisSerializer一直不能将json字符串反序列化成Session对象。至今还没有找到原因,总是报有个别属性无法反序列化。。。 如果哪位大神知道是怎么回事,应该怎么解决的话,还请赐教。
代码的github地址:https://github.com/zjwan461/shiro 有需要的小伙伴可以拿去。第一次写技术博客,不到位的地方还请多多包涵。
如需转载还请标注原文出处,谢谢合作!!!