shiro整合springmvc
说明
其他资料:
流程
配置
- 配置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>
- 配置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>
工程结构
配置文件
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
com.lifeofcoding.shiro.realm.MyEncryptedRealm.java
com.lifeofcoding.shiro.controller.UserController.java
前端代码
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>
项目结构
配置文件
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
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>
SessionManager优化
使用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()+"\""+"登陆成功"; }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· 手把手教你更优雅的享受 DeepSeek
· AI工具推荐:领先的开源 AI 代码助手——Continue
· 探秘Transformer系列之(2)---总体架构
· V-Control:一个基于 .NET MAUI 的开箱即用的UI组件库
· 乌龟冬眠箱湿度监控系统和AI辅助建议功能的实现