shiro整合springmvc

时间:2023-03-09 13:32:59
shiro整合springmvc

说明

  代码及部分相关资料根据慕课网Mark老师的视频进行整理

  其他资料:

流程

配置

  1. 配置web.xml整合shiro

    把shiro整合到springMVC实质上是在web.xml配置过滤器(filter),配置DelegatingFilterProxy,让其代理shiro的过滤器,对需要认证或者授权的请求路径进行过滤。
<!--  DelegatingFilterProxy可以代理Spring管理的bean中的Filter,shiro的filter就是由其代理;
"filter-name"要与spring配置文件中ShiroFilterFactoryBean的id一致;
这里相当于把shiro和springmvc整合到一起-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
  1. 配置spring.xml添加shiro组件

    与上一节的程序一样,需要添加SecutiryManager、Realm两个核心组件。
  • (非必需)创建HashedCredentialsMatcher。用于加密,也可不加密,根据自己需求进行配置,建议加密。
<!--  1.配置用于密码解密的HashedCredentialMatcher  -->
<bean id="matcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashIterations" value="3"/>
<property name="hashAlgorithmName" value="MD5"/>
</bean>
  • 创建realm。此处示例使用自定义的可加密MyEncryptedRealm,引用HashedCredentialMatcher
<!--  2.配置Realm,使用自定义的MyEncryptedRealm,引用HashedCredentialMatcher  -->
<bean id="realm" class="com.lifeofcoding.shiro.realm.MyEncryptedRealm">
<property name="credentialsMatcher" ref="matcher"/>
</bean>
  • 创建SecurityManager。示例使用DefaultWebSecurityManager,引用上面的realm。
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="realm"/>
</bean>
  • 创建ShiroFilterFactoryBean。该Bean会根据配置,生成一个被DelegatingFilterProxy代理的,类型为SpringShiroFilter的过滤器,这个过滤器包含FilterChain,用于对请求进行实际上的更详细的过滤。该Bean的id必须与web.xml中配置的DelegatingFilterProxy的“filter-name”一致。
<!--  4.配置shiro的ShiroFilterFactoryBean,引用SecurityManager;
该Bean会创建一个shiro的内部类SpringShiroFilter的对象,并交由DelegatingFilterProxy代理-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="login.html"/>
<property name="unauthorizedUrl" value="403.html"/>
<!-- ShiroFilterFactoryBean会根据以下配置创建shiro的过滤器链 -->
<property name="filterChainDefinitions">
<value>
/login.html = anon
/subLogin = anon
/register = anon
/addPermissions = anon
/* = authc
</value>
</property>
</bean>

filterChain从上到下匹配,当匹配到合适的规则时进行处理,不管后面的规则如何,所以一定要注意顺序。 value值的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值。

anon:它对应的过滤器里面是空的,什么都没做;

authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter

shiro包含11个过滤器,具体信息可查看shiro官网

实战1

maven依赖

    <!-- springmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>
<!-- shiro相关 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<!-- 日志相关 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

工程结构

shiro整合springmvc

配置文件

web.xml:

   <!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="true"> <!-- 声明应用范围(整个WEB项目)内的上下文初始化参数。 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<!--扫描所有spring配置文件,不用在配置文件里import-->
<param-value>classpath*:spring/spring*</param-value>
</context-param> <!-- 配置监听器,用于springIOC -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>--> <!-- DelegatingFilterProxy可以代理Spring管理的bean中的Filter,shiro的filter就是由其代理;
"filter-name"要与spring配置文件中ShiroFilterFactoryBean的id一致;
这里相当于把shiro和springmvc整合到一起-->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping> <!-- 将请求路由到相应的handler -->
<servlet>
<servlet-name>spring-mvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring-mvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

spring.xml:

    <?xml version="1.0" encoding="UTF-8"?>
<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.xsd"> <!-- 1.配置用于密码解密的HashedCredentialMatcher -->
<bean id="matcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashIterations" value="3"/>
<property name="hashAlgorithmName" value="MD5"/>
</bean> <!-- 2.配置Realm,使用自定义的MyEncryptedRealm,引用HashedCredentialMatcher -->
<bean id="realm" class="com.lifeofcoding.shiro.realm.MyEncryptedRealm">
<property name="credentialsMatcher" ref="matcher"/>
</bean> <!-- 3.配置SecurityManager,引用Realm -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="realm"/>
</bean> <!-- 4.配置shiro的ShiroFilterFactoryBean,引用SecurityManager;
该Bean会创建一个shiro的内部类SpringShiroFilter的对象,并交由DelegatingFilterProxy代理-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="login.html"/>
<property name="unauthorizedUrl" value="403.html"/>
<!-- ShiroFilterFactoryBean会根据以下配置创建shiro的过滤器链 -->
<property name="filterChainDefinitions">
<value>
/login.html = anon
/subLogin = anon
/register = anon
/addPermissions = anon
/* = authc
</value>
</property>
</bean>
</beans>

springmvc.xml:

    <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 扫描shiro包下所有组件(包括@Controller、@Component等) -->
<context:component-scan base-package="com.lifeofcoding.shiro"></context:component-scan> <!-- 1.开启注解;
2.注册HandlerMapping和HandlerAdapter的实现类。
配置该参数,spring可以通过context:component-scan/标签的配置,自动将扫描到的@Component,@Controller,@Service,@Repository等注解标记的组件注册到工厂中,来处理请求。
该参数还支持以下功能:
a:默认提供的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormat
b:xml,json的默认读写支持-->
<mvc:annotation-driven/> <!-- 处理静态资源 -->
<mvc:resources mapping="/*" location="/"/>
</beans>

log4j.properties:

# Global logging configuration #\u5728\u5f00\u53d1\u73af\u5883\u4e0b\u65e5\u5fd7\u7ea7\u522b\u8981\u8bbe\u7f6e\u6210DEBUG\uff0c\u751f\u4ea7\u73af\u5883\u8bbe\u7f6e\u6210info\u6216error
log4j.rootLogger=DEBUG, stdout
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

后端代码

com.lifeofcoding.shiro.pojo.User.java

package com.lifeofcoding.shiro.pojo; import java.util.Set; public class User {
private String username;
private String password;
private Set<String> roles; public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPassword() {
return password;
} public void setPassword(String password) {
this.password = password;
} public Set<String> getRoles() {
return roles;
} public void setRoles(Set<String> roles) {
this.roles = roles;
}

}

com.lifeofcoding.shiro.realm.MyEncryptedRealm.java

package com.lifeofcoding.shiro.realm; import com.lifeofcoding.shiro.pojo.User;

import org.apache.shiro.authc.AuthenticationException;

import org.apache.shiro.authc.AuthenticationInfo;

import org.apache.shiro.authc.AuthenticationToken;

import org.apache.shiro.authc.SimpleAuthenticationInfo;

import org.apache.shiro.authz.AuthorizationInfo;

import org.apache.shiro.authz.SimpleAuthorizationInfo;

import org.apache.shiro.crypto.hash.SimpleHash;

import org.apache.shiro.realm.AuthorizingRealm;

import org.apache.shiro.subject.PrincipalCollection;

import org.apache.shiro.util.ByteSource;

import org.apache.shiro.util.CollectionUtils;

import java.util.HashSet;

import java.util.Map;

import java.util.Set;

import java.util.concurrent.ConcurrentHashMap; public class MyEncryptedRealm extends AuthorizingRealm {
/** 加密次数 */
private int iterations;
/** 算法名 */
private String algorithmName; /** 存储用户名和密码 */
private final Map<String,String> userMap;
/** 存储用户及其对应的角色 */
private final Map<String, Set<String>> roleMap;
/** 存储所有角色以及角色对应的权限 */
private final Map<String,Set<String>> permissionMap;
/** 存储用户盐值 */
private Map<String,String> saltMap; {
//设置Realm名,可用于获取该realm
super.setName("MyRealm");
} public MyEncryptedRealm(){
this.iterations = 0;
this.algorithmName = "MD5";
this.userMap = new ConcurrentHashMap<>(16);
this.roleMap = new ConcurrentHashMap<>(16);
this.permissionMap = new ConcurrentHashMap<>(16);
this.saltMap = new ConcurrentHashMap<>(16);
} /**
* 身份认证必须实现的方法
* @param authenticationToken token to do authenticate
* @return org.apache.shiro.authc.AuthenticationInfo
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取主体中的用户名
String userName = (String) authenticationToken.getPrincipal();
//2.通过用户名获取密码,getPasswordByName自定义实现
String password = getPasswordByUserName(userName);
if(null == password){
return null;
}
//3.构建authenticationInfo认证信息
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
//添加盐值
String salt = getSaltByUsername(userName);
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
return authenticationInfo;
} /**
* 用于授权,必须实现
* @param principalCollection principals
* @return org.apache.shiro.authz.AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1.获取用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
String userName = (String) principalCollection.getPrimaryPrincipal();
//2.获取角色信息,getRoleByUserName自定义
Set<String> roles = getRolesByUserName(userName);
//3.获取权限信息,getPermissionsByRole方法同样自定义,也可以通过用户名查找权限信息
Set<String> permissions = getPermissionsByUserName(userName);
//4.构建认证信息并返回。
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(permissions);
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
} /**
* 往realm添加账号信息
* @param user user
*/
public void addAccount(User user) throws UserExistException {
String userName = user.getUsername();
String password = user.getPassword();
Set<String> roles = user.getRoles();
if(null != userMap.get(userName)){
throw new UserExistException("user \""+ userName +"\" already exist");
}
//如果设置的加密次数大于0,则对密码进行加密
if(iterations > 0){
//此处用随机数作为盐值,可改为UUID或其它
String salt = String.valueOf(Math.random()*10);
saltMap.put(userName,salt);
password = doHash(password, salt);
}
userMap.put(userName, password);
//如果roles不为空,存入roleMap
if (!CollectionUtils.isEmpty(roles)){
roleMap.put(userName, roles);
}
} /**
* 自定义部分,通过用户名获取权限信息
* @param userName username
* @return java.util.Set<java.lang.String>
*/
private Set<String> getPermissionsByUserName(String userName) {
//1.先通过用户名获取角色信息
Set<String> roles = getRolesByUserName(userName);
if (CollectionUtils.isEmpty(roles)){
return null;
}
//2.通过角色信息获取对应的权限
Set<String> permissions = new HashSet<>();
roles.forEach(role -> {
if (null != permissionMap.get(role)) {
permissions.addAll(permissionMap.get(role));
}
});
return permissions;
} /**
* 自定义部分,通过用户名获取密码,可改为数据库操作
* @param userName username
* @return java.lang.String
*/
private String getPasswordByUserName(String userName){
return userMap.get(userName);
} /**
* 自定义部分,通过用户名获取角色信息,可改为数据库操作
* @param userName username
* @return java.util.Set<java.lang.String>
*/
private Set<String> getRolesByUserName(String userName){
return roleMap.get(userName);
} /**
* 自定义部分,通过用户名获取角色信息,可改为数据库操作
* @param userName username
* @return java.util.Set<java.lang.String>
*/
private String getSaltByUsername(String userName) {
return saltMap.get(userName);
} /**
* 往realm删除账号信息
* @param userName username
*/
public void deleteAccount(String userName){
userMap.remove(userName);
roleMap.remove(userName);
} /**
* 添加角色权限,变参不传值会接收到长度为0的数组。
* @param roleName name of the role
* @param permissions permissions which this role preserve
*/
public void addPermissions(String roleName,Set<String> permissions){
permissionMap.put(roleName, permissions);
} /**
* 设置加密次数
* @param iterations iterations to doHash
*/
public void setHashIterations(int iterations){
this.iterations = iterations;
} /**
* 设置算法名
* @param algorithmName name of the algorithm to use
*/
public void setAlgorithmName(String algorithmName){
this.algorithmName = algorithmName;
} /**
* 计算哈希值
* @param str str to doHash
* @param salt user's salt
*/
private String doHash(String str,String salt){
salt = null==salt ? "" : salt;
return new SimpleHash(this.algorithmName,str,salt,this.iterations).toString();
} /**
* 注册时,用户已存在的异常
*/
public class UserExistException extends Exception{
private UserExistException(String message) {super(message);}
}

}

com.lifeofcoding.shiro.controller.UserController.java

package com.lifeofcoding.shiro.controller; import com.lifeofcoding.shiro.pojo.User;

import com.lifeofcoding.shiro.realm.MyEncryptedRealm;

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.authc.UsernamePasswordToken;

import org.apache.shiro.subject.Subject;

import org.apache.shiro.util.CollectionUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.ResponseBody; @Controller

public class UserController {
@Autowired
private MyEncryptedRealm realm; /**
* 用户登录
* @param user 用户信息,包括用户名(username)和密码(password)
* api示例: POST /subLogin?username=java&password=123
* */
@ResponseBody
@RequestMapping(value = "/subLogin",method = RequestMethod.POST,produces= {"application/json;charset=UTF-8"})
public String subLogin(User user){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
try {
subject.login(token);
}catch (Exception e){
return e.getMessage();
}
return "\""+subject.getPrincipal().toString()+"\""+"登陆成功";
} /**
* 用户注册
* @param user 用户信息,包括:用户名(username)、密码(password)、角色(roles)(可选)
* @return 返回注册信息
* api示例: POST /register?username=java&password=123&roles=admin&roles=user 可通过指定多个roles传入roles数组
* */
@ResponseBody
@RequestMapping(value = "/register",method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"})
public String register(User user){
//配置realm设置加密方式
realm.setAlgorithmName("MD5");
//加密次数
realm.setHashIterations(3);
//添加账号
try {
realm.addAccount(user);
}catch (Exception e){
return e.getMessage();
}
return "Add account \"" + user.getUsername() + "\" succeeded";
} /**
* 测试已登录的用户是否拥有某角色
* @param role 角色名
* @return 返回信息
* api示例: GET /testRole?role=admin
* */
@ResponseBody
@RequestMapping(value = "testRole",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
public String testRole(String role){
if (null == role){
return "no input";
}
Subject subject = SecurityUtils.getSubject();
if (subject.hasRole(role)){
return "user \"" + subject.getPrincipal()+"\" has role \"" + role +"\"";
}
return "user \"" + subject.getPrincipal()+"\" do not have role \"" + role + "\"";
} /**
* 测试已登录的用户是否拥有某权限
* @param permission 权限
* @return 返回信息
* api示例: GET /testPermission?permission=user:delete
* */
@ResponseBody
@RequestMapping(value = "testPermission",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
public String testPermission(String permission){
if (null == permission){
return "no input";
}
Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted(permission)){
return "user \"" + subject.getPrincipal()+"\" has permission \"" + permission +"\"";
}
return "user \"" + subject.getPrincipal()+"\" do not have permission \"" + permission + "\"";
} /**
* 添加权限
* api示例: GET /addPermissions?role=admin&permissions=user:delete&permissions=user:modify
* */
@ResponseBody
@RequestMapping(value = "addPermissions",method = RequestMethod.GET,produces = {"application/json;charset=UTF-8"})
public String addPermissions(String role, String...permissions){
if (role==null || CollectionUtils.isEmpty(CollectionUtils.asSet(permissions))){
return "rolename or permissions can not be empty";
}
realm.addPermissions(role, CollectionUtils.asSet(permissions));
return null;
}

}

前端代码

login.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body> <form action="subLogin" method="post">
用户名: <input type="text" name="username"/>\</br>
密码: <input type="password" name="password"/>\</br>
<input type="submit" value="登录">
</form> </body>
</html>

实战2——自定义jdbcRealm

代码与实战1基本一致,仅仅是修改Realm,改为从数据库中获取信息,再修改相关配置。

maven依赖

    <dependencies>
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>
<!-- 日志相关 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.26</version>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>
<!-- AOP相关 aspectjweaver(用于切入点表达式)包含aspectjrt(用于aop相关注解),因此只引入前者-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
</dependencies>

项目结构

shiro整合springmvc

配置文件

spring.xml:

    <?xml version="1.0" encoding="UTF-8"?>
<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.xsd"> <!-- 1.配置用于密码解密的CredentialMatcher -->
<bean id="matcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashIterations" value="3"/>
<property name="hashAlgorithmName" value="MD5"/>
</bean> <!-- 2.配置Realm,使用自定义的MyEncryptedJdbcRealm,引用Matcher -->
<bean id="realm" class="com.lifeofcoding.shiro.realm.MyEncryptedJdbcRealm">
<property name="credentialsMatcher" ref="matcher"/>
</bean> <!-- 3.配置SecurityManager,引用Realm -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="realm"/>
</bean> <!-- 4.配置shiro的ShiroFilterFactoryBean,引用SecurityManager;
该Bean会创建一个shiro的内部类SpringShiroFilter的对象,并交由DelegatingFilterProxy代理-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="login.html"/>
<property name="unauthorizedUrl" value="403.html"/>
<!-- ShiroFilterFactoryBean会根据以下配置创建shiro的过滤器链 -->
<property name="filterChainDefinitions">
<value>
/login.html = anon
/subLogin = anon
/register = anon
/addPermissions = anon
/testPermission = anon
/testRole = anon
/* = authc
</value>
</property>
</bean>
</beans>

spring-dao.xml:

    <?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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置数据源dataSource -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"/>
<property name="password" value="0113"/>
<property name="url" value="jdbc:mysql://localhost:3306/shiro"/>
</bean> <!-- 配置JdbcTemplate,引用dataSource -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean> <!-- 配置事务管理器transactionManager,引用dataSource -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean> <!-- spring-tx模块以AOP方式管理spring中的事务 -->
<!-- 配置AOP全局事务,设置通知(Advice)的属性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" rollback-for="Exception"/>
</tx:attributes>
</tx:advice> <!-- 配置AOP全局事务,设置切面(Aspect),引入txAdvice -->
<aop:config proxy-target-class="true">
<!-- 对realm包下,以"add"和"delete"开头的方法开启事务 -->
<aop:advisor advice-ref="txAdvice"
pointcut="execution(* com.lifeofcoding.shiro.realm..*.add*(..))
or execution(* com.lifeofcoding.shiro.realm..*.delete*(..))"/>
</aop:config>
</beans>

spring-mvc.xml:

    <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!-- 扫描shiro包下所有组件(包括@Controller、@Component等) -->
<context:component-scan base-package="com.lifeofcoding.shiro"/> <!-- 1.开启注解;
2.注册HandlerMapping和HandlerAdapter的实现类。
配置该参数,spring可以通过context:component-scan/标签的配置,自动将扫描到的@Component,@Controller,@Service,@Repository等注解标记的组件注册到工厂中,来处理请求。
该参数还支持以下功能:
a:默认提供的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormat
b:xml,json的默认读写支持-->
<mvc:annotation-driven/> <!-- 处理静态资源 -->
<mvc:resources mapping="/*" location="/"/>
</beans>

log4j.properties:

\# Global logging configuration \#\u5728\u5f00\u53d1\u73af\u5883\u4e0b\u65e5\u5fd7\u7ea7\u522b\u8981\u8bbe\u7f6e\u6210DEBUG\uff0c\u751f\u4ea7\u73af\u5883\u8bbe\u7f6e\u6210info\u6216error
log4j.rootLogger=DEBUG, stdout
\# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

后台代码

UserDaoImpl.java


package com.lifeofcoding.shiro.dao.impl;

import com.lifeofcoding.shiro.dao.UserDao;

import com.lifeofcoding.shiro.pojo.User;

import org.apache.shiro.util.CollectionUtils;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.jdbc.core.RowMapper;

import org.springframework.stereotype.Component;

import javax.annotation.Resource;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.HashSet;

import java.util.List;

import java.util.Set; @Component

public class UserDaoImpl implements UserDao {
@Resource
private JdbcTemplate jdbcTemplate; @Override
public String getPasswordByUserName(String userName) {
String queryPasswordSql = "SELECT password FROM shiro_web_users WHERE username = ?";
List<String> passwords = jdbcTemplate.query(queryPasswordSql, new String[]{userName}, new RowMapper<String>() {
@Override
public String mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString("password");
}
});
if(CollectionUtils.isEmpty(passwords)){
return null;
}
return passwords.get(0);
} @Override
public Set<String> getRolesByUserName(String userName) {
String queryRoleSql = "SELECT role FROM shiro_web_user_roles WHERE username = ?";
List<String> roles = jdbcTemplate.query(queryRoleSql, new String[]{userName}, new RowMapper<String>() {
@Override
public String mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString("role");
}
});
if (CollectionUtils.isEmpty(roles)){
return null;
}
return new HashSet<>(roles);
} @Override
public String getSaltByUserName(String userName) {
String querySaltSql = "SELECT salt FROM shiro_web_users WHERE username = ?";
List<String> salts = jdbcTemplate.query(querySaltSql,new String[]{userName},new RowMapper<String>() {
@Override
public String mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString("salt");
}
});
if (CollectionUtils.isEmpty(salts)){
return null;
}
return salts.get(0);
} @Override
public void addUser(User user) throws Exception{
if (user == null){
return;
}
String addUserSql = "INSERT INTO shiro_web_users (username,password,salt) VALUES (?,?,?)";
jdbcTemplate.update(addUserSql,new Object[]{user.getUsername(),user.getPassword(),user.getSalt()});
} @Override
public void deleteUser(String userName) {
String deleteUserSql = "DELETE FROM shiro_web_users WHERE username = ?";
jdbcTemplate.update(deleteUserSql,userName);
}
}

PermissionDaoImpl.java

package com.lifeofcoding.shiro.dao.impl;

import com.lifeofcoding.shiro.dao.PermissionDao;

import org.apache.shiro.util.CollectionUtils;

import org.springframework.jdbc.core.BatchPreparedStatementSetter;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.jdbc.core.RowMapper;

import org.springframework.stereotype.Component;

import javax.annotation.Resource;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.ArrayList;

import java.util.HashSet;

import java.util.List;

import java.util.Set; @Component

public class PermissionDaoImpl implements PermissionDao {

@Resource

private JdbcTemplate jdbcTemplate;
@Override
public void addPermissions(String roleName, Set<String> permissions) {
String addPermissionSql = "INSERT IGNORE INTO shiro_web_roles_permissions (role,permission) VALUES (?,?)";
//去掉空数据
permissions.remove("");
//后面StatementSetter需要用index遍历集合,所以转为List
ArrayList<String> tempPermissions = new ArrayList<>(permissions);
//批量添加数据
jdbcTemplate.batchUpdate(addPermissionSql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setString(1, roleName);
ps.setString(2, tempPermissions.get(i));
} @Override
public int getBatchSize() {
return tempPermissions.size();
}
});
} @Override
public Set<String> getPermissionsByRole(String role) {
String queryPermissionSql = "SELECT permission FROM shiro_web_roles_permissions WHERE role = ?";
List<String> permissions = jdbcTemplate.query(queryPermissionSql, new String[]{role}, new RowMapper<String>() {
@Override
public String mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString("permission");
}
});
if (CollectionUtils.isEmpty(permissions)){
return null;
}
return new HashSet<>(permissions);
} @Override
public void deletePermissionsByRole(String role) {
String deletePermissionsByRoleSql = "DELETE FROM shiro_web_roles_permissions WHERE role = ?";
jdbcTemplate.update(deletePermissionsByRoleSql,role);
} @Override
public void deletePermission(String permission) {
String deletePermissionSql = "DELETE FROM shiro_web_roles_permissions WHERE permission = ?";
jdbcTemplate.update(deletePermissionSql,permission);
} @Override
public void deleteRolePermission(String role, String permission) {
String deleteRolePermissionSql = "DELETE FROM shiro_web_roles_permissions WHERE role = ? AND permission = ?";
jdbcTemplate.update(deleteRolePermissionSql,new Object[]{role,permission});
}

}

RoleDaoImpl.java

package com.lifeofcoding.shiro.dao.impl;

import com.lifeofcoding.shiro.dao.RoleDao;

import org.springframework.jdbc.core.BatchPreparedStatementSetter;

import org.springframework.jdbc.core.JdbcTemplate;

import org.springframework.stereotype.Component;

import javax.annotation.Resource;

import java.sql.PreparedStatement;

import java.sql.SQLException;

import java.util.ArrayList;

import java.util.Set; @Component

public class RoleDaoImpl implements RoleDao {
@Resource
private JdbcTemplate jdbcTemplate; @Override
public void addRole(String username, Set<String> roles) {
//去掉空数据
roles.remove("");
String addRoleSql = "INSERT IGNORE INTO shiro_web_user_roles (username,role) VALUES (?,?)";
//StatementSetter用index遍历集合,转为List
ArrayList<String> tempRoles = new ArrayList<>(roles);
//批量添加数据
jdbcTemplate.batchUpdate(addRoleSql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setString(1, username);
ps.setString(2, tempRoles.get(i));
} @Override
public int getBatchSize() {
return tempRoles.size();
}
});
} @Override
public void deleteRolesByUsername(String userName) {
String deleteRoleByUsernameSql = "DELETE FROM shiro_web_user_roles WHERE username = ?";
jdbcTemplate.update(deleteRoleByUsernameSql,userName);
} @Override
public void deleteRole(String role) {
String deleteRoleSql = "DELETE FROM shiro_web_user_roles WHERE role = ?";
jdbcTemplate.update(deleteRoleSql,role);
} @Override
public void deleteUserRole(String userName, String role) {
String deleteUserRoleSql = "DELETE FROM shiro_web_user_roles WHERE username = ? AND role = ?";
jdbcTemplate.update(deleteUserRoleSql,new Object[]{userName,role});
}

}

MyEncryptedJdbcRealm.java

package com.lifeofcoding.shiro.realm;

import com.lifeofcoding.shiro.dao.PermissionDao;

import com.lifeofcoding.shiro.dao.RoleDao;

import com.lifeofcoding.shiro.dao.UserDao;

import com.lifeofcoding.shiro.pojo.User;

import org.apache.shiro.authc.AuthenticationException;

import org.apache.shiro.authc.AuthenticationInfo;

import org.apache.shiro.authc.AuthenticationToken;

import org.apache.shiro.authc.SimpleAuthenticationInfo;

import org.apache.shiro.authz.AuthorizationInfo;

import org.apache.shiro.authz.SimpleAuthorizationInfo;

import org.apache.shiro.crypto.hash.SimpleHash;

import org.apache.shiro.realm.AuthorizingRealm;

import org.apache.shiro.subject.PrincipalCollection;

import org.apache.shiro.util.ByteSource;

import org.apache.shiro.util.CollectionUtils;

import javax.annotation.Resource;

import java.util.HashSet;

import java.util.Set; public class MyEncryptedJdbcRealm extends AuthorizingRealm {

@Resource

private UserDao userDao;

@Resource

private PermissionDao permissionDao;

@Resource

private RoleDao roleDao;
/**加密次数*/
private int iterations;
/**加密算法名*/
private String algorithmName; /*---------------------------------实现自定义Realm需要重写的两个方法------------------------------------*/ /**
* 身份认证必须实现的方法
* @param authenticationToken token
* @return org.apache.shiro.authc.AuthenticationInfo
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取主体中的用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
String userName = (String) authenticationToken.getPrincipal();
//2.通过用户名获取密码,getPasswordByName自定义实现
String password = getPasswordByUserName(userName);
if(null == password){
return null;
}
//3.如果密码不为空,则构建authenticationInfo认证信息
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
String salt = getSaltByUserName(userName);
//4.认证信息添加盐值
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
return authenticationInfo;
} /**
* 用于授权,必须实现
* @param principalCollection principal的集合
* @return org.apache.shiro.authz.AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1.获取用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
String userName = (String) principalCollection.getPrimaryPrincipal();
//2.获取角色信息,getRoleByUserName自定义
Set<String> roles = getRolesByUserName(userName);
//3.获取权限信息,getPermissionsByRole方法同样自定义,也可以通过用户名查找权限信息
Set<String> permissions = getPermissionsByUserName(userName);
//4.构建认证信息并返回。
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//添加权限信息
simpleAuthorizationInfo.setStringPermissions(permissions);
//添加角色信息
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
} //类加载时初始化
{
//设置Realm名,可用于获取该realm
super.setName("MyJdbcRealm");
} /**构造方法,初始化哈希次数及算法名称*/
MyEncryptedJdbcRealm(){
iterations = 0;
algorithmName = "MD5";
} /*--------------------------------------自定义部分--------------------------*/ /**
* 自定义部分,通过用户名获取权限信息
* @param userName username
* @return 该用户拥有的所有权限
*/
public Set<String> getPermissionsByUserName(String userName) {
//1.先通过用户名获取所有角色信息
Set<String> roles = userDao.getRolesByUserName(userName);
//2.通过角色信息获取对应的权限
Set<String> permissions = new HashSet<>();
roles.forEach(role -> {
Set<String> tempPermissions = permissionDao.getPermissionsByRole(role);
if (null != tempPermissions) {
permissions.addAll(tempPermissions);
}
});
return permissions;
} /**
* 自定义部分,通过用户名获取密码
* @param userName username
* @return java.lang.String
*/
public String getPasswordByUserName(String userName){
return userDao.getPasswordByUserName(userName);
} /**
* 自定义部分,通过用户名获取盐
* @param userName username
* @return java.lang.String
*/
public String getSaltByUserName(String userName){
return userDao.getSaltByUserName(userName);
} /**
* 自定义部分,通过用户名获取角色信息
* @param userName username
* @return java.util.Set<java.lang.String>
*/
public Set<String> getRolesByUserName(String userName){
return userDao.getRolesByUserName(userName);
} /**
* 往realm添加账号信息
* @param user user
*/
public void addAccount(User user) throws Exception {
String salt = "";
String password = user.getPassword();
String userName = user.getUsername();
//用户信息为空抛出异常
if (user.getUsername()==null || user.getPassword()==null){
throw new InfoEmptyException("username or password can not be empty");
}
//如果用户已经注册,抛出异常
if(null != userDao.getPasswordByUserName(userName)){
throw new UserExistException("user \""+ userName +"\" already exist");
}
//如果设置的加密次数大于0,则进行加密
if(iterations > 0){
salt = randomSalt();
password = doHash(password, salt);
}
user.setPassword(password);
user.setSalt(salt);
userDao.addUser(user);
if (CollectionUtils.isEmpty(user.getRoles())){
return;
}
roleDao.addRole(userName,user.getRoles());
} /**
* 添加角色权限
* @param roleName 角色名
* @param permissions 该角色拥有的权限
*/
public void addPermissions(String roleName, Set<String> permissions) throws Exception{
permissionDao.addPermissions(roleName,permissions);
} /**
* 用随机数作为盐值,可改为UUID或其他
* */
public String randomSalt(){
return String.valueOf(Math.random()*10);
} /**
* 删除账号信息
* @param userName 用户名
*/
public void deleteAccount(String userName) throws Exception{
userDao.deleteUser(userName);
roleDao.deleteRolesByUsername(userName);
} /**
* 设置加密次数
* @param iterations 哈希操作的次数
*/
public void setHashIterations(int iterations){
this.iterations = iterations;
} /**
* 设置算法名
* @param algorithmName 哈希算法名
*/
public void setAlgorithmName(String algorithmName){
this.algorithmName = algorithmName;
} /**
* 进行哈希运算
* @param source 原来的字符
* @param salt 盐值
* @return 运算结果
* */
private String doHash(String source, String salt){
return new SimpleHash(this.algorithmName,source,salt,this.iterations).toString();
} /**
* 注册时,用户已存在的异常类
*/
public class UserExistException extends Exception{
public UserExistException(String message) {super(message);}
} /**
* 用户信息为空的异常
* */
public class InfoEmptyException extends Exception{
public InfoEmptyException(String message) {super(message);}
}

}

实战3——通过注解授权

配置

在springmvc配置文件中添加如下配置,务必在springmvc配置文件中添加,即上面的springmvc.xml文件。

<!-- 开启AOP -->
<aop:config proxy-target-class="true"/> <!-- 用于管理shiro的生命周期 -->
<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- 用于注解方式验证权限的通知 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>

后端代码

直接在controller上添加注解"@RequiresRoles"或者"@RequiresPermissions",如:

@RequiresPermissions("user:delete")
@RequiresRoles("admin")
@ResponseBody
@RequestMapping(value = "testRole",method = RequestMethod.GET)
public String testRole(){
return "has role: admin";
}

使用拥有指定角色或者权限的用户登录,即可访问到该"testRole()"方法,否则会抛异常。

也可以用数组传多个参数进行授权,如:

@RequiresPermissions({"user:delete","user:login"})
@RequiresRoles({"user","admin"})

当当前用户同时拥有所有指定的角色或者权限时,才能访问方法。

实战4——redis实现session管理

实现session管理,主要是给SecurityManager配置SessionManager,而SessionManager,需要配置用于Session增删查改的SessionDao。SessionDao继承AbstractSessionDAO抽象类,需要实现的方法有:

  • Serializable doCreate(Session session)

    存储session
  • Session doReadSession(Serializable sessionId)

    读取session
  • void update(Session session) throws UnknownSessionException

    更新session
  • void delete(Session session)

    删除session
  • Collection getActiveSessions()

    获取活跃的session

maven

添加redis依赖

<!-- redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.0</version>
</dependency>

后台代码

封装jedis的增删查改操作:

JedisUtil.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import java.util.HashSet;
import java.util.Set; @Component

public class JedisUtil {

/**jedis连接池*/

@Autowired

private JedisPool jedisPool;
/**获取资源*/
private Jedis getResource(){
return jedisPool.getResource();
} /**
* set
* */
public byte[] set(byte[] key, byte[] value) {
Jedis jedis = getResource();
try{
jedis.set(key, value);
return value;
}finally {
jedis.close();
}
} /**
* 设置过期时间
* */
public void expire(byte[] key, int seconds) {
Jedis jedis = getResource();
try {
jedis.expire(key,seconds);
} finally {
jedis.close();
}
} /**
* 获取值
* */
public byte[] get(byte[] key) {
Jedis jedis = getResource();
try {
return jedis.get(key);
} finally {
jedis.close();
}
} /**
* 删除
* */
public void del(byte[] key) {
Jedis jedis = getResource();
try {
jedis.del(key);
} finally {
jedis.close();
}
} /**
* "keys"操作
* */
public Set<byte[]> keys(String pattern) {
Jedis jedis = getResource();
try {
return jedis.keys((pattern).getBytes());
} finally {
jedis.close();
}
} /**
* 使用scan获取所有匹配的keys,redis2.8+开始,加入了"scan"操作,
* 允许每次只获取一部分数据,避免数据量大时"keys"造成阻塞
* */
public Set<byte[]> scan(String pattern){
Jedis jedis = getResource();
//初始化游标
byte[] START_CURSOR = "0".getBytes();
//每次要求返回的数据量
int NUM_PER_SCAN = 50;
try{
//设置初始化游标
byte[] cursor = START_CURSOR;
//查询参数对象
ScanParams params = new ScanParams();
//设置匹配模式
params.match(pattern.getBytes());
//设置理想的每次返回的数据数量(不一定会返回这么多)
params.count(NUM_PER_SCAN);
//用一个HashSet来存储查找到的keys,因为结果可能会重复,所以用set去重
Set<byte[]> keys = new HashSet<>();
while(true) {
/*redis的scan与单循环链表相似,每次scan操作,返回部分数据result以及下次scan操作需要的游标cursor*/
ScanResult result = jedis.scan(cursor,params);
//获取下次scan的游标,byte[]类型,如果是String类型,返回结果也会是String类型,需要注意。
cursor = result.getCursorAsBytes();
keys.addAll(result.getResult());
//如果已经遍历完所有数据,则退出
if(result.isCompleteIteration()) {break;}
}
return keys;
}finally {
jedis.close();
}
}

}

AbstractSessionDAO的子类:

RedisSessionDao.java

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.util.CollectionUtils;
import org.springframework.util.SerializationUtils;
import com.lifeofcoding.utils.JedisUtil;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set; public class RedisSessionDao extends AbstractSessionDAO {
/**封装的redis工具类*/
@Resource
private JedisUtil jedisUtil; /**在redis中存储的session的前缀*/
private final String SHIRO_SESSION_PREFIX="shiro-session:"; /**
* 把传入的key(sessionId)转化为在redis中存储的统一格式的key
* */
private byte[] getKey(String key){
return (SHIRO_SESSION_PREFIX+key).getBytes();
} /**
* 保存session到redis中
* */
private void saveSession(Session session){
if (null != session && null != session.getId()) {
//获取session的id并将其传化为指定格式
byte[] key = getKey(session.getId().toString());
//对session进行序列化
byte[] value = SerializationUtils.serialize(session);
jedisUtil.set(key, value);
jedisUtil.expire(key, 600);
}
} /**
* 把session保存到redis
* */
@Override
protected Serializable doCreate(Session session) {
//创建sessionId
Serializable sessionId = generateSessionId(session);
//给session绑定sessionId
assignSessionId(session,sessionId);
//保存session到redis中
saveSession(session);
return sessionId;
} /**
* 读取session
* */
@Override
protected Session doReadSession(Serializable sessionId) {
if (null == sessionId) {
return null;
}
//把sessionId转化为redis中的key的格式
byte[] key = getKey(sessionId.toString());
byte[] value = jedisUtil.get(key);
//返回反序列化后的session
return (Session) SerializationUtils.deserialize(value);
} /**
* 更新session
* */
@Override
public void update(Session session) throws UnknownSessionException {
saveSession(session);
} /**
* 删除session
* */
@Override
public void delete(Session session) {
if (null == session && null == session.getId()){
return;
}
byte[] key = getKey(session.getId().toString());
jedisUtil.del(key);
} /**
* 获取活跃的session
* */
@Override
public Collection<Session> getActiveSessions() {
//获取redis中存储session的所有key
//Set<byte[]> keys = jedisUtil.keys(SHIRO_SESSION_PREFIX+"*");
//可以自己改写、优化scan,用"scan"操作替代"keys",避免数据量大时阻塞。
Set<byte[]> keys = jedisUtil.scan(SHIRO_SESSION_PREFIX+"*");
Set<Session> sessions = new HashSet<Session>();
if (CollectionUtils.isEmpty(keys)){
return sessions;
}
for (byte[] key : keys){
Session session = (Session) SerializationUtils.deserialize(jedisUtil.get(key));
sessions.add(session);
}
return sessions;
}

}

配置文件

redis的配置文件:

spring-redis.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.xsd"> <!-- 创建连接池配置对象 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"/> <!-- 创建连接池 -->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
<constructor-arg name="host" value="127.0.0.1"/>
<constructor-arg name="port" value="6379"/>
<!--<constructor-arg name="timeout" value="60000"/>-->
<!--<constructor-arg name="password" value="123"/>-->
</bean>

实现了AbstractSessionDao抽象类后,在spring配置文件中配置该实现类,然后配置SessionManager。
```xml

```
然后在securityManager中配置sessionManager
```xml

```

SessionManager优化

shiro整合springmvc

  使用DefaultSessionManager管理session时,session通过retrieveSession(SessionKey sessionKey)方法获取,该方法又调用retrieveSessionFromDataSource(sessionId),利用SessionDao从数据源中获取session,此处sessionDao就是之前的自己实现的RedisSessionDao,而“数据源”,就是redis。

  通过debug可以发现有时候在处理一次请求时,retrieveSession方法调用了很多次,这样就意味着访问了很多次redis,这给redis带来了不必要的压力。此时,可以重写该方法,把session存储到request中,需要获取session时,直接从request中获取,避免redis服务器不必要的开销。

  自定义SessionManager,需要继承 DefaultSessionManager的子类DefaultWebSessionManager,而不是直接继承DefaultSessionManager,否则获取到的sessionId和request为null;

代码如下:

CustomSessionManager.java

package com.lifeofcoding.session;

import org.apache.shiro.session.Session;

import org.apache.shiro.session.UnknownSessionException;

import org.apache.shiro.session.mgt.SessionKey;

import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;

import org.apache.shiro.web.session.mgt.WebSessionKey;

import javax.servlet.ServletRequest;

import java.io.Serializable; public class CustomSessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException{
//通过SessionKey获取SessionId
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
//通过SessionKey获取ServletRequest
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
//尝试从request中根据sessionId获取session
if (null!=request && null!=sessionId){
Session session = (Session) request.getAttribute(sessionId.toString());
if (null!=session) {
return session;
}
}
/*如果request中没有session,则使用父类获取session,并保存到request中,
父类DefaultWebSession是通过SessionDao获取session,在这里是从redis获取*/
Session session = super.retrieveSession(sessionKey);
if (null != request && null != sessionId){
request.setAttribute(sessionId.toString(),session);
}
return session;
}

}

自定义SessionManager后,修改配置文件,把DefaultSessionManager改为自己的SessionManager。

 <!--  使用自定义的sessionManager,减少对redis的压力  -->
<bean id="sessionManager" class="com.lifeofcoding.session.CustomSessionManager">
<property name="sessionDAO" ref="redisSessionDao"/>
</bean>

实战5——使用redis实现缓存管理

  在程序中,对用户权限数据的访问量是比较大的,如果每次授权,都去数据库中取数据,这是十分不理想的,可以用redis来充当缓存,缓存用户的授权数据,减轻数据库压力。

后端代码

1.继承Cache类,编写RedisCache,用于对redis中的缓存数据进行增删查改。Cache类实质上相当于DAO,仅仅是对缓存进行增删查改。

RedisCache.java

package com.lifeofcoding.shiro.cache;

import com.lifeofcoding.shiro.utils.JedisUtil;

import org.apache.shiro.cache.Cache;

import org.apache.shiro.cache.CacheException;

import org.springframework.stereotype.Component;

import org.springframework.util.SerializationUtils;

import javax.annotation.Resource;

import java.util.Collection;

import java.util.Set; @Component

public class RedisCache<K,V> implements Cache<K,V> {
@Resource
private JedisUtil jedisUtil; /**
* cache的前缀
* */
private final String CACHE_PREFIX = "shiro-cache:"; private byte[] getKey(K k){
if (k instanceof String){
return (CACHE_PREFIX + k).getBytes();
}
return SerializationUtils.serialize(k);
} @Override
public V get(K k) throws CacheException {
System.out.println("read cache from redis for user: "+k.toString());
byte[] value = jedisUtil.get(getKey(k));
if (null != value){
return (V) SerializationUtils.deserialize(value);
}
return null;
} @Override
public V put(K k, V v) throws CacheException {
byte[] key = getKey(k);
byte[] value = SerializationUtils.serialize(v);
jedisUtil.set(key,value);
jedisUtil.expire(key,600);
return v;
} @Override
public V remove(K k) throws CacheException {
byte[] key = getKey(k);
byte[] value = jedisUtil.get(key);
jedisUtil.del(key);
if (null != value){
return (V) SerializationUtils.deserialize(value);
}
return null;
} @Override
public void clear() throws CacheException { } @Override
public int size() {
return 0;
} @Override
public Set<K> keys() {
return null;
} @Override
public Collection<V> values() {
return null;
}

}

2.继承CacheManager,编写RedisCacheManager,用来返回cache。CacheManager只有一个方法“getCache(String var1)”,通过传入cache的名字,返回对应的cache,仅此而已。

RedisCacheManager.java

package com.lifeofcoding.shiro.cache;

import org.apache.shiro.cache.Cache;

import org.apache.shiro.cache.CacheException;

import org.apache.shiro.cache.CacheManager;

import javax.annotation.Resource; public class RedisCacheManager implements CacheManager {

@Resource

private RedisCache redisCache;
/**
* 该方法用来给shiro获取cache对象。
* 参数s为cache的名称,此处只有一个cache,即RedisCache,直接返回单例的RedisCache实例即可。
* */
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return redisCache;
}

}

<

配置文件

给SecurityManager配置CacheManager

<!--  5.配置CacheManager  -->
<bean id="cacheManager" class="com.lifeofcoding.shiro.cache.RedisCacheManager"/> <!-- 6.配置SecurityManager,引用Realm、SessionManager、CacheManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="realm"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="cacheManager"/>
</bean>

拓展

把授权数据放redis,每次需要授权数据时就访问redis,这对redis的资源也造成一定浪费,可以在RedisCache中用Map等集合类,构造二级缓存,每次需要数据,直接从二级缓存中获取,如果没有数据,再从redis中取。

实战6——RememberMe

很多情况下,网站需要提供“记住我”的功能,可以使用shiro的CookieRememberMeManager实现。在配置方面只需在spring配置文件中添加配置即可。

<!--  6.设置cookie名称和时间,cookie保存加密的用户信息,可在浏览器开发者工具查看   -->
<bean id="simpleCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<property name="name" value="rememberMeCookie"/>
<property name="maxAge" value="600"/>
</bean> <!-- 7.设置RememberMeManager,引用cookie -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="simpleCookie"/>
</bean> <!-- 8.配置SecurityManager,引用Realm、SessionManager、cacheManager和RememberMeManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="realm"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="cacheManager"/>
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>

当然,也要修改User类,添加rememberMe字段,让用户自行决定是否启用该功能,同时修改UserController实现该功能。

private boolean rememberMe;

public boolean getRememberMe() {
return rememberMe;
} public void setRememberMe(boolean rememberMe) {
this.rememberMe = rememberMe;
}
@ResponseBody
@RequestMapping(value = "/subLogin",method = RequestMethod.POST,produces= {"application/json;charset=UTF-8"})
public String subLogin(User user){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
try {
//设置自动登录
token.setRememberMe(user.getRememberMe());
subject.login(token);
}catch (Exception e){
return e.getMessage();
}
return "\""+subject.getPrincipal().toString()+"\""+"登陆成功";
}

文件传送门

github地址