Shiro是什么
Apache Shiro是Java的一个安全框架。
Shiro可以帮助我们完成:认证、授权、加密、会话管理、Web集成、缓存等。对比另一个安全框架Spring Sercurity,它更简单和灵活。
- Authentication:身份认证/ 登录 ,验证用户是不是拥有相应的身份;
- Authorization:授权,即 验证权限 ,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。
- Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的JavaSE环境的,也可以是Web环境的。 (shiro自己实现的session区别于Web容器中的session)
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
- Web Support:Web支持,可以非常容易的集成到Web环境中
- Caching:缓存,比如用户登录后,其用户信息,拥有的角色/权限不必每次去查,提高效率。
- Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持
- Run As:允许一个用户切换为另一个用户(如果他们允许)的身份进行访问
- Rember Me:记住我,这是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro不会去维护用户,维护权限;这些需要自己去设计/提供;然后通过响应的接口注入给Shiro即可。
shiro的入门
----一般来讲,我们使用shiro都是和spring集成使用,在真实项目开发中,几乎不会单独使用,所以,这里我就直接使用SSM整合的情况下去集成使用shiro来做为入门例子,主要注意点放在使用shiro的步骤上,先把架子搭建起来:
- 搭建SSM+shiro安全整合的架子所需要导入的包。
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.apache.taglibs</groupId>
<artifactId>taglibs-standard-spec</artifactId>
<version>1.2.5</version>
<type>bundle</type>
</dependency>
<dependency>
<groupId>org.apache.taglibs</groupId>
<artifactId>taglibs-standard-impl</artifactId>
<version>1.2.5</version>
<type>bundle</type>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version> <!--5.1.6 8.0.11 -->
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</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>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.6</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
- web.xml
1.配置web.xml中的shiro拦截器
----这个可以进入shiro官网 阿帕奇网https://shiro.apache.org/
,找到配文档,复制过来即可。
-------当前shiro最新版本为1.8x,但是我现在讲的是1.3x版。
下载源码文件:
解压后,进入C:\Users\Administrator\Desktop\shiro-root-1.3.2\samples\spring\src\main\webapp\WEB-INF打开web.xml文件
复制它到我们项目的web.xml文件中。
2.配置springmvc.xml的调度器
3.配置spring.xml的对web的监听器,即初始化 IOC容器。
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!-- alt /可选ContextLoaderListener和 DispatcherServlet-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
- spring.xml 与springmvc.xml
spring.xml里配置shiro的核心组件SecurityManager:
-----在2解压shir源码文件中,找到C:\Users\Administrator\Desktop\shiro-root-1.3.2\samples\spring\src\main\webapp\WEB-INF下的‘applicationContext.xml'文件
制制如下内容到spring.xml中。以后的<list>中文件是配子多个核心Realm。
配置ehcache缓存bean,导入ehcache并新建配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache>
<diskStore path="c:/ehcache/"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
配置自己的域realm
---新建自己的realm类:public class ShiroRealm implements Realm {...
---<bean id="shiroRealm" class="cn.ybzy.shirodemo.security.ShiroRealm"></bean>
开启shiro的注解,能够在sprnig的组件中使用shiro的自身注解,但一般注解又在controller类(链接到视图时)里用,如果这三个bean配在spring.xml中,则controller不起作用
-----springmvc是spring的子类。
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
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-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<context:component-scan base-package="cn.ybzy.shirodemo">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
</context:component-scan>
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 配置C3P0数据连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="acquireIncrement" value="${jdbc.acquireIncrement}"></property>
<property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property>
<property name="minPoolSize" value="${jdbc.minPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
<property name="maxStatements" value="${jdbc.maxStatements}"></property>
<property name="maxStatementsPerConnection" value="${jdbc.maxStatementsPerConnection}"></property>
</bean>
<!-- 事务配置 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务属性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="load*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="select*" read-only="true"/>
<tx:method name="*" read-only="false"/>
</tx:attributes>
</tx:advice>
<!-- 配置事务的切入点 -->
<aop:config>
<aop:pointcut expression="execution(* cn.ybzy.shirodemo.service.*.*(..))"
id="txPointcut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
<!-- 整合Mybatis -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configLocation" value="classpath:mybatis.xml"></property>
<property name="mapperLocations" value="classpath:cn/ybzy/shirodemo/dao/*.xml"></property>
</bean>
<mybatis-spring:scan base-package="cn.ybzy.shirodemo.dao"/>
<!-- shiro的核心组件配置bean -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/> <!-- 这里的缓存是realm缓存 -->
<property name="authenticator" ref="authenticator"></property> <!--多个realm时的认证策略配置 -->
<!-- <property name="realm" ref="shiroRealm"></property> 单个Realm是用到-->
<property name="realms"> <!--多个Realm是用到可以进入(ctrl+点击)进入realm所在的类查看有realm和realms属性 -->
<list>
<ref bean="weixinRealm"/> <!-- 三个Realm都做认证,只要一个通过即可,认证按上下先后顺序来做的 -->
<ref bean="qQRealm"/>
<ref bean="shiroRealm"/>
</list>
</property>
</bean>
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property>
</bean>
<!-- ShiroRealm继承了AuthenticatingRealm.它的 credentialsMatcher属性我们要改我们自定的验证方式,而credentialsMatcher的一个实现类为HashedCredentialsMatcher-->
<bean id="shiroRealm" class="cn.ybzy.shirodemo.service.shiro.ShiroRealm">
<property name="credentialsMatcher">
<bean class="cn.ybzy.shirodemo.service.MyCredentialsMatcher">
</bean>
</property>
</bean>
<bean id="weixinRealm" class="cn.ybzy.shirodemo.service.shiro.WeixinRealm">
<property name="credentialsMatcher">
<bean class="cn.ybzy.shirodemo.service.MyCredentialsMatcher2">
</bean>
</property>
</bean>
<bean id="qQRealm" class="cn.ybzy.shirodemo.service.shiro.QQRealm">
<property name="credentialsMatcher">
<bean class="cn.ybzy.shirodemo.service.MyCredentialsMatcher1">
</bean>
</property>
</bean>
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean><!--全部通过认证策略 -->
</property>
<!-- <property name="realms"> 多个Realm是用到可以进入(ctrl+点击)进入realm所在的类查看有realm和realms属性
<list>
<ref bean="weixinRealm"/> 三个Realm都做认证,只要一个通过即可,认证按上下先后顺序来做的
<ref bean="qQRealm"/>
<ref bean="shiroRealm"/>
</list>
</property> -->
</bean>
<!-- shiro的拦截规则 -->
<!-- 需要我们注意的知识点:这个bean的id值,必须是web.xml配置文件中的<filter-name>shiroFilter</filter-name> 一致-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.html"/> <!-- 登录的页面,没有登录的情况访问需要登录认证后才能访问的页面 -->
<property name="successUrl" value="/admin/main.html"/> <!-- 登录成功以后,跳转到的页面 -->
<property name="unauthorizedUrl" value="/unauthorized.html"/> <!-- 登录成功以后,访问了要求有权限控制的页面,跳转的页面 -->
<!-- <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property> -->
<property name="filterChainDefinitions"> <!-- 是设置拦截规则的 -->
<value>
<!-- url地址 = 拦截规则 : anon:匿名-即所有网页都可该问,/**由小到大过滤后的所有的 -->
/login.html = anon
/logout.html = logout <!--默认退出到web根目录'/',要处理之 -->
/admin/userlist.html = roles[user] <!-- 设置音通用户角色 -->
/admin/adduser.html = roles[admin] <!-- 设置超级管理员角色 -->
/admin/userlist.html = perms[userlist] <!--设置权限标记,userlist为权限名 -->
/admin/adduser.html = perms[adduser] <!--设置权限标记 ,adduser为权限名-->
/admin/** = authc
/** = anon
<!-- authc:需要登录成功以后才能够方法的规则
注意的知识点:规则是有顺序的,从上到下,拦截的范围,必须是从小到大 -->
</value>
</property>
</bean>
</beans>
-----------------------------------------------------------------------------springmvc.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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
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-4.3.xsd">
<context:component-scan base-package="cn.ybzy.shirodemo" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
</context:component-scan>
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:default-servlet-handler/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!-- 开启shiro的注解,能够在sprnig的组件中使用shiro的自身注解,一般注解在controller类里,如果这三个bean配在spring.xml中,则controller不起作用 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
- jdbc.properties --mysql8.x AND log4.properties
GMT:有时差8小时, url的时区使用中国标准时间。也是就serverTimezone=Asia/ShanghaiuseSSL=false&useUnicode=true
jdbc.user=root
jdbc.password=root
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql://localhost:3306/shirodemouseSSL=false&useUnicode=true& characterEncoding=UTF8
&serverTimezone=Asia/Shanghai
jdbc.acquireIncrement=10
jdbc.initialPoolSize=30
jdbc.minPoolSize=5
jdbc.maxPoolSize=40
jdbc.maxStatements=1000
jdbc.maxStatementsPerConnection=100
----------------------------------------------------------log4.properties---------------------------------------
log4j.rootLogger = debug,stdout, D
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.Threshold = INFO
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p %m%n
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = C:/log4j.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = INFO
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern=%d %p %m%n
- mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 驼峰命名规则-->
<setting name="mapUnderscoreToCamelCase" value="true"></setting>
</settings>
</configuration>
- 模型类文件,DAO,SERVICE文件。
模型类:User.java,Role.java,Permission.java
public class User {
private Integer id;
private String username;
private String password;
private Integer state; //0表示禁用,1表示启用
private Set<String> roles; //角色有多个,自动绑成一个集合
-----
}
public class Role {
private Integer id;
private String rname;
private String rcode; //主要用于判断超级管理员中用到,当code=“admin"时为超级管理员
private Set<Permission> permissions;
------
}
public class Permission {
private Integer id;
private String code;
private String url;
-----
}
- 测试shiro架子有没有搭好。
------这里只是测试一下,我们不配ShiroRealm.
spring.mxl:
<!-- shiro的核心组件配置bean -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/> <!-- 这里的缓存是realm缓存 -->
</bean>
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property>
</bean>
<!-- shiro的拦截规则 -->
<!-- 需要我们注意的知识点:这个bean的id值,必须是web.xml配置文件中的<filter-name>shiroFilter</filter-name> -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.html"/> <!-- 登录的页面,没有登录的情况访问需要登录认证后才能访问的页面 -->
<property name="successUrl" value="/admin/main.html"/> <!-- 登录成功以后,跳转到的页面 -->
<property name="unauthorizedUrl" value="/unauthorized.html"/> <!-- 登录成功以后,访问了要求有权限控制的页面,跳转的页面 -->
<!-- <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property> -->
<property name="filterChainDefinitions"> <!-- 是设置拦截规则的 -->
<value>
<!-- url地址 = 拦截规则 : anon:匿名-即所有网页都可该问,/**由小到大过滤后的所有的 -->
/login.html = anon
/logout.html = logout <!--默认退出到web根目录'/',要处理之 -->
/admin/** = authc
/** = anon
<!-- authc:需要登录成功以后才能够方法的规则
注意的知识点:规则是有顺序的,从上到下,拦截的范围,必须是从小到大 -->
</value>
</property>
</bean>
login.jsp,main.jsp如下:
ShiroController.java:
@Controller
public class ShiroController {
@GetMapping(value={"/login.html","/"})
public String login() {
return "login";
}
@GetMapping("/admin/main.html")
public String main() {
return "/admin/main";
}
}
运行测试:
http://localhost/shirodemo1/login.html
或http://localhost/shirodemo1/
或http://localhost/shirodemo1/main.html
都只进入到如下网页,说明架子搭建成功。
---表示我们不能进入让主页面,因为它要权限。
使用shiro的Realm实现登录认证
Realm的讲解
上面的例子只是初步的用上了shiro,但是这样的使用例子,实际的项目应用上,意义还不是很大,
下面,我们就在前面的入门例子的基础上更进一步的使用shiro来做一个登录认证的实
例:
使用shiro,首先要了解几个基本的概念:
1、Subject:主题,主体,使用shiro都是从获取到subjent对象开始的
2、Resource:资源,代表可以访问呢的url地址
3、Permission:权限,是当前登录的用户,能不能放问某个资源URL的权限
4、Role:角色,可以理解为权限的集合,一个角色里可以包含很多个权限,只需要给一个用户分配一个角色,就相当于给它分配了很多个权限
Realm简单应用
准备:数据库表t_user,User.java,UserDao.java,UserDao.xml(mybatis实现接口),UserService.java,UserServiceImpl.java
《1》t_user,因为没有做添加,删除数据功能,所以我在表中自加了一条记录。admin1,123456,1状态码。
《2》UserDao.java,UserDao.xml
public interface UserDao{
public User getUserByUsername(@Param("username") String username);
}
-------------------------------------------------------------------------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.ybzy.shirodemo.dao.UserDao">
<select id="getUserByUsername" resultType="cn.ybzy.shirodemo.model.User">
select * from t_user where username=#{username}
</select>
</mapper>
《3》UserService.java,UserServiceImpl.java
public interface UserService {
public User getUserByUsername(String username);
}
-----------------------------------------------------------------------------------------------------------------------------------------
@Service("userService") //放进ioc中
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public User getUserByUsername(String username) {
return userDao.getUserByUsername(username);
}
}
- login.jsp视图中建立登录表单,spring.xml配一个shiroRealm
<form action="${pageContext.request.contextPath}/login.html" method="post" >
用户名称:<input type="text" name="username"><br />
<br /> 密码:<input type="password" name="password" id="pwd"><br />
<br /> <input type="submit" value="登录">
</form>
spring.xml配一个shiroRealm
<!-- shiro的核心组件配置bean -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/> <!-- 这里的缓存是realm缓存 -->
<!-- <property name="authenticator" ref="authenticator"></property> --> <!--多个realm时的认证策略配置 -->
<property name="realm" ref="shiroRealm"></property> <!-- ..Realm会自动从数据库表中获取用户信息 -->
</bean>
<bean id="shiroRealm" class="cn.ybzy.shirodemo.service.shiro.ShiroRealm">
</bean>
ShiroRealm.java
public class ShiroRealm implements Realm{
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken arg0) throws AuthenticationException {
return null;
}
@Override
public String getName() {
return null;
}
@Override
public boolean supports(AuthenticationToken token) {
return false;
}
}
-
在Controller里建立方法,响应这个请求,接收username和password的值
@PostMapping("/login.html")
public String login(User user) {
//System.out.println("表单传来的用户信息: "+user); //如果在login.jsp页中输入:xiongshaowen,passd.表单传来的用户信息: User [id=null, username=xiongshaowen, password=passd, state=null, roles=null]
//1认证的核心组件,subject,获取Subject对象
Subject subject = SecurityUtils.getSubject();
//2.登录验证的第二步,将表单提交过来的用户名和密码封装到token对象
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
//3.调用subject对象里的login方法,进行登陆验证。
try {
subject.login(token); //让shiro框架来帮我们验证登陆,token会把用户信息传到ShiroRealm的supports方法,后来用了AuthenticationInfo doGetAuthenticationInfo(... token)中
}catch(Exception e){
e.printStackTrace();
return "loginError";
}
return "redirect:/admin/main.html";
}
----subject.login(token); //让shiro框架来帮我们验证登陆,token会把用户信息传到ShiroRealm的supports方法
----在login方法里获取到shiro的认证核心组件Subject接口的对象(这个对象中封装这登录用户loginUser对象),并调用Subjent接口的实现对象里的login方法判断用户名和密码是不是对的:
我们来测一下是不是传到supports()方法中了。
一,首先ShiroRealm.java的supports重写中写上如下代码。
@Override
public boolean supports(AuthenticationToken token) {
UsernamePasswordToken token2 = (UsernamePasswordToken) token;//AuthenticationToken一个实现类UsernamePasswordToken
System.out.println(token2.getUsername());
return false;
}
二,
localhost:8081/shirodemo0/login.html
回车,可以到控制台中看到‘admin',admin是我输入的用户名。实开中用Realm的实现类AutherticatingRealm.
真正项目的时候,登录的验证工作用到Realm的实现类AutherticatingRealm.现在我们把ShiroRealm改为:
public class ShiroRealm extends AuthenticatingRealm {
@Autowired
private UserDao userDao;
/**
* 真正项目的时候,登录的验证工作
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken token2 = (UsernamePasswordToken) token;
String username = token2.getUsername();
User user = userDao.getUserByUsername(username);
if(user==null)
throw new UnknownAccountException("用户名或密码正确!");
if(user.getState() == 0)
throw new LockedAccountException("用户被管理员禁 用!");
//返回对象info是会比对用户密码,如果正解放行登陆 principlas可以是用户名,也可以是登录用户user对象,第二个参数是从数据库获取的密码,第三个参数是盐值,第四个参数是Realm的实现类名
AuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),null,this.getName());//SimpleAuthenticationInfo(principals, hashedCredentials, credentialsSalt,realmName)
return info;
}
}
----返回 AuthenticationInfo info中info自动作密码比对式作,而 AuthenticationInfo 是一个接口,所以要强转为一个实现类对象.
现在又测试一下:
http://localhost/shirodemo1
回车,退出登录---代码如下:main.jsp
-----shiro的session是shiro自己实现的,登录后有缓存登录状态的,所以我们位测试方便做一个登出的功能.
-----登出后自动返回web的根目录,所以控制类中要链接视图到'/'--根目录下。<a>默认为get方式请求。
@GetMapping(value={"/login.html","/"})
<body>
登陆成功后的主页面main.jsp
<a href="${pageContext.request.contextPath}/logout.html">退出登陆</a>
</body>
Realm实现复杂登录认证-加密密码。
----前面的例子中,密码都是明文的,这在实际项目中是及其不推荐的做法,我们保存的密码是一定要加密的,shiro是给我们提供的加密的工具,不用我们自己实现加密的方法,它的用法:
- 首先要把存入数据库的密码从明文变成密文,可以通过SimpleHash这个类来实现
---这里我们不做添加用户信息等功能,所以运行一下java代码,产生密码,再复制到数据库表中。
SimpleHashRequest()对应上面验证比对方法SimpleAuthenticationInfo(user,user.getPassword(),null,this.getName())。
public static void main(String[] args) {
Object rs = new SimpleHash("MD5","123456",null,1024); //1024次加密
System.out.println(rs);
}
//fc1709d0a95a6be30bc5926fdb7f22f4
- 在比对密码的时候,要将form表单发过来的密码进行加密后再与数据库里的密码比对
在前面的代码跟踪里,我们已经了解到了,密码对比的时候,默认的是使用的SimpleCredentialsMatcher类(通过调试打断点查的到),但是前面也说了,这个类里的密码没有进行加密操作,所以我们要将自定义的ShrioRealm类关联的这个类换掉(通过ShiroRealm的父类中的属性:credentialsMatcher来关联的),具体做法在配置文件里给这个属性手动赋值,不让它取默认值:
<bean id="shiroRealm" class="cn.ybzy.shirodemo.service.shiro.ShiroRealm">
<property name="credentialsMatcher" >
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property> <!-- 加密算法MD5-->
<property name="hashIterations" value="1024"></property> <!--加密次数 -->
<property name="storedCredentialsHexEncoded" value="true"></property><!--十六进制加密结果 -->
</bean>
</property>
</bean>
如何知道比对类呢?:按ctrl+鼠标点击‘AuthenticationgRealm'--open Implementsation进入它的实现类。
——————》
注意: HashedCredentialsMatcher extends SimpleCredentialsMatcher。
经过上面的两个步骤后,一方面数据库中的密码是加密后的密文,另一方面获取到表单中用户输入的密码后会用同样的加密规则加密后,才进行比对,初步实现了我们的加密要求!
- 给加密的密文加盐值。
----简单的说,就是一组安全随机数。它会在特定的时候,加入到密码中(一般来说是加密后的密码)。从而使密码变得更有味道(从单一简单化到复杂化),更安全。
----众所周知,用户名和密码是被保存在数据库中。可是一旦数据库发生了泄露,用户名和密码就都遭到了泄露。攻击者可以轻松的获取用户名和密码,进行操作。更大的危害是, 由于现在需要注册的网站、app越来越多。 用户名和密码很多时候都是相同的。一旦某处发生了泄露,则后果会慢慢的扩散。 这些危害大家可以查询下近些年发生的一些安全事故,如Sony数据库泄露、网易数据库泄露、CSDN数据库泄露等。
解决这个问题的通用方法是:
1、对密码进行加密存储
这样的好处是,即使数据库发生了泄露,攻击者也不会拿到明文密码,依然无法直接使用这些密码。
但是这样的存储方式也存在缺点:很多用户在注册时都是使用的弱密码。攻击者拿到数据库密文后,根据猜测的常见的弱密码加密成密文。然后依次匹配数据库中的密文密码。这样就可以得到其中使用弱密码的用户的密码值。同时app、web等软件由于用户体验等原因,也不可能让用户设置安全系数过高的密码。这也给攻击者破解密码提供了条件。
---举个例子:如ATM、微信支付密码使用的是纯6位数字,这样就有了10^6种可能,攻击者只要拿到了全套的密码对应的密文,就可以获取到所有用户的密码。那么该怎么解决呢? 这就涉及到固定盐值加密
2、对密码进行加密
---如前文所说,这个盐是一个随机数。当用户注册一个简单密码时,系统会同时生成这样一个salt,与该用户对应,保存到数据库中。
这样当用户的密码是888888时,后台真实存储的密码时888888盐化以后的值。
操作步骤如下:
(1)注册、修改密码时,前台将 888888加密后的pwd1,传入后台
(2)后台拿到pwd1以后,生成一个相应的随机数 salt。将pwd1与salt拼接并再次加密,生成pwd2
(3)后台将pwd2和salt 一并存储到数据库中。
(1-)当用户每次输入用户名密码后,将密码加密生成pwd1'后,传入后台。
(2-)后台拿到pwd1'后,根据用户名id拿到对应的盐值。与盐值拼接加密后,生成pwd2‘。
(3-)然后判断pwd2'与数据库中的pwd2是否一致即可。
这里有两点需要注意:
1、密码在前后台的加密方式可以采用不同的形式
2、盐值的拼接不一定非要拼接到最后,也可以放在前边、插在中间、甚至拆开或者倒序拼接。
3、在项目开发中,常常用用户名来做上面的随机数salt.
这样即使是简单密码也没关系。因为相同的密码在数 据库中存储的值并不一样。攻击者无法简单的密码的密文进行破解。
加盐--具体在shiro里的操作举例:
1、导入js的MD5加密文件(别人已做好工的具,加1次密码,后台比对密码时,在spring.xml中配了获取客户端密码后自动再加1024次,再到ShiroRealm中验证),将前端的提交表单的时候实现用户密码的加密,要不然的话后台永远对比不成功。
注:javascript
<title>login</title>
<script type="text/javascript" src="${pageContext.request.contextPath}/static/lib/md5/md5a.js"></script>
<script type="text/javascript">
function checkForm() {
var pwdipt = document.getElementById("pwd")
var pwdvalue = pwdipt.value;
//第一次加密密码
pwdvalue = md5(pwdvalue);
pwdipt.value=pwdvalue;
if (pwdvalue.value.length == 32) { //md5加密后的密文为32位
return true;
} else {
return false; //表单提交不起作用
}
alert(pwdvalue+"-"+pwdvalue.length);
if (pwdipt.value.length == 32) { //md5加密后的密文为32位
return true;
} else {
return false; //表单提交不起作用
}
}
</script>
</head>
<body>
登陆页面 login.jsp
<form action="${pageContext.request.contextPath}/login.html" method="post" onsubmit="return checkForm()">
用户名称:<input type="text" name="username"><br />
<br /> 密码:<input type="password" name="password" id="pwd"><br />
<br /> <input type="submit" value="登录">
</form>
</body>
</html>
2、添加用户加入盐值,将ShiroRealm中的doGetAuthenticationInfo方法返回的AuthenticationInfo对象加入盐值。
注意,注意:由于本人没有做添加用户等功能,所以在此我手工建一个main方法产生相应的加密密码再加盐值产生新的密码,再复制到数据库中。
public static void main(String[] args) {
Object rs = new SimpleHash("MD5","123456",null,1); //
System.out.println(rs);
}
//e10adc3949ba59abbe56e057f20f883e
再加盐(用户名--admin)加1024次:把此产生的密码放进数据表中。
public static void main(String[] args) {
Object rs = new SimpleHash("MD5","e10adc3949ba59abbe56e057f20f883e","admin",1024); //1024次加密
System.out.println(rs);
}
//c2b632dc87637a5fd03fdf9b61693d17
3、ShiroRealm中返回的AuthentionInfo对象也要加盐值,这样密码比对的时候才能是用户输入密码和数据库里的密码匹对上
public class ShiroRealm extends AuthenticatingRealm {
@Autowired
private UserDao userDao;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken token2 = (UsernamePasswordToken) token;
String username = token2.getUsername();
User user = userDao.getUserByUsername(username);
if(user==null)
throw new UnknownAccountException("用户名或密码正确!");
if(user.getState() == 0)
throw new LockedAccountException("用户被管理员禁 用!");
//返回对象info是会比对用户密码,如果正解放行登陆 principlas可以是用户名,也可以是登录用户user对象,第二个参数是从数据库获取的密码,第三个参数是盐值,第四个参数是Realm的实现类名
ByteSource slat = ByteSource.Util.bytes(username);
AuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),slat,getName());//SimpleAuthenticationInfo(principals, hashedCredentials, credentialsSalt,realmName)
return info;
}
}
4、测试: --->localhost:8081/shirodemo1 回车,用户名:amdin 密码:123456
如登录成功说明加盐值成功。
- 重放攻击与处理
-----重放攻击(ReplayAttacks)又叫重播攻击、回放攻击或新鲜性攻击(Freshness Attacks)。这里举个简单的例子:当用户A进行登录后,前台会将加密后的密码,以数据包的形式发送到服务端。服务端会进行盐化等加密手段后,再进行安全校验。 可是如果这个数据包被攻击者截获。并且分析出数据包的结构(如哪些字段代表用户名、哪些代表IP/会话ID),然后进行适当的修改,再次发送给服务端后,服务端依然会进行常规的校验,依然会验证通过。也就是说无论客户端,服务端的加密手段多么的复杂, 一旦攻击者有能力截获和修改前后台通信的数据包,那么前面讲的防御措施都将不起作用。 。依然会验证通过的原因? 用户每次发送的数据包中用户名、密码等部分不变的机理。所以应对的办法:我们可以让他改变,怎么改变呢?
这里提供一个解决思路,我们也来缉简单实现一下:
(1-)每次登陆时(login.jsp--login.html),我们可以生成一个随机数(一个动态生成的salt),这个salt在前后台各自保存一份。
@Controller
public class ShiroController {
@GetMapping(value={"/login.html","/"})
public String login(Model model,HttpSession session) {
//生成一组16位随机数,用于重放攻击处理中。
int hashcodeV = UUID.randomUUID().hashCode(); //每次执行都会产生不同的值,绝对不相同
if(hashcodeV < 0) hashcodeV = -hashcodeV; //让随机数取正值
String uuidsalt = String.format("%016d", hashcodeV); //让盐值取uuid随机数所16位十进制表示
//把盐值,同时保存到前后端
model.addAttribute("uuidsalt",uuidsalt); //前端 ,每次刷新页面都会产生一个随机数
session.setAttribute("uuidsalt", uuidsalt); //后端
// System.out.println(uuidsalt);
return "login";
}
(2-)当用户名输入完密码pwd后。前台会进行 f1(pwd)加密,然后用动态生成的salt加密的密钥,然后再次加密。也就是 pwd1=f2(f1(pwd)+salt)。之后前台就把这个pwd1发送到后台。(注意由于动态salt每次都会改变,所以pwd1每次也会改变)
login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>login</title>
<script type="text/javascript"
src="${pageContext.request.contextPath}/static/lib/md5/md5a.js"></script>
<script type="text/javascript"
src="${pageContext.request.contextPath}/static/lib/aes/aes.js"></script>
<script type="text/javascript"
src="${pageContext.request.contextPath}/static/lib/aes/pad-zeropadding-min.js"></script>
<script type="text/javascript">
function checkForm() {
var pwdipt = document.getElementById("pwd")
var pwdvalue = pwdipt.value;
//第一次加密密码
pwdvalue = md5(pwdvalue);
//加密后的密码进行第二次加密(可以java解密的加密)
var uuidsalt = document.getElementById("uuidsalt").value;
pwdvalue = encrypt(pwdvalue, uuidsalt, uuidsalt);
pwdipt.value = pwdvalue;
//alert(pwdvalue);
if (pwdipt.value.length == 44) { //md5加密后的密文为32位,第二加密后变为44位了
return true;
} else {
return false; //表单提交不起作用
}
}
//aes加密的一个加密方法
function encrypt(data, key, iv) { //key,iv偏移量-一般与key相同:16位的字符串
var key1 = CryptoJS.enc.Latin1.parse(key);
var iv1 = CryptoJS.enc.Latin1.parse(iv);
return CryptoJS.AES.encrypt(data, key1, {
iv : iv1,
mode : CryptoJS.mode.CBC,
padding : CryptoJS.pad.ZeroPadding
}).toString();
}
</script>
</head>
<body>
<input id="uuidsalt" type="hidden" value="${uuidsalt}"> 登陆页面
login.jsp
<br />
<br />
<form action="${pageContext.request.contextPath}/login.html"
method="post" onsubmit="return checkForm()">
用户名称:<input type="text" name="username"><br />
<br /> 密码:<input type="password" name="password" id="pwd"><br />
<br /> <input type="submit" value="登录">
</form>
<shiro:guest>
游客访问<a href="login.jsp">登陆</a>
</shiro:guest>
</body>
</html>
-----导入的两个文件在网盘上也有,放在项目中的位置如下图:
aes.js,pa-zeropadding-min.js两个文件的作用是加密,该加密的东西可逆解密,并且长度一定是44位大小,不大也不小。
在行运时,查看login.jsp源代码时可以看到每次刷新后,value="${uuidsalt}"的值都不一样。
----通过一个隐藏表单表,获取随机盐值,再传递 。
(3-)后台拿到数据后,在用动态salt(即f2()使用可逆的机密算法)解密,然后再拼接固化salt接着再次加密,最后与数据库对比即可
前台:login.jsp初始化(触发后台的@GetMapping(value={"/login.html","/"})方法)get请求获取后台产生uuidsalt,经过javascript的ase加密(可解)通过form的post请求传到后台的@PostMapping("/login.html")方法处理---》验证。
---写一个解密工具类,解密前端传来的密码为明文.(里面有‘加密’,‘解密’两个方法,我们在这只用解密方法)。
public class AesEncryptUtil {
/**
* 加密方法
* @param data 要加密的数据
* @param key 加密key
* @param iv 加密iv
* @return 加密的结果
* @throws Exception
*/
public static String encrypt(String data, String key, String iv) throws Exception {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");//"算法/模式/补码方式"
int blockSize = cipher.getBlockSize();
byte[] dataBytes = data.getBytes();
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(plaintext);
return new Base64().encodeToString(encrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 解密方法
* @param data 要解密的数据
* @param key 解密key
* @param iv 解密iv
* @return 解密的结果
* @throws Exception
*/
public static String desEncrypt(String data, String key, String iv) throws Exception {
try {
byte[] encrypted1 = new Base64().decode(data);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original);
return originalString;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 测试
*/
public static void main(String args[]) throws Exception {
String test = "18729990110";
String data = null;
String key = "dufy20170329java";
String iv = "dufy20170329java";
data = encrypt(test, key, iv);
System.out.println(data);
System.out.println(desEncrypt(data, key, iv));
}
}
ShiroController.java 解密前端发送的ase密码,再传上递 ShiroRealm.java验证。
@Controller
public class ShiroController {
@GetMapping(value={"/login.html","/"})
public String login(Model model,HttpSession session) {
//生成一组16位随机数,用于重放攻击处理中。
int hashcodeV = UUID.randomUUID().hashCode(); //每次执行都会产生不同的值,绝对不相同
if(hashcodeV < 0) hashcodeV = -hashcodeV; //让随机数取正值
String uuidsalt = String.format("%016d", hashcodeV); //让盐值取uuid随机数所16位十进制表示
//把盐值,同时保存到前后端
model.addAttribute("uuidsalt",uuidsalt); //前端 ,每次刷新页面都会产生一个随机数
session.setAttribute("uuidsalt", uuidsalt); //后端
// System.out.println(uuidsalt);
return "login";
}
@GetMapping("/admin/main.html")
public String main() {
return "/admin/main";
}
@PostMapping("/login.html")
public String login(String username,String password,HttpSession session) throws Exception {
//System.out.println("表单传来的用户信息: "+user); //如果在login.jsp页中输入:xiongshaowen,passd.表单传来的用户信息: User [id=null, username=xiongshaowen, password=passd, state=null, roles=null]
//1认证的核心组件,subject,获取Subject对象
Subject subject = SecurityUtils.getSubject();
//从服务器端取出盐值(一进入localhost/shirodemotest/login.html就保存的值,见上一个方法)
String key = (String) session.getAttribute("uuidsalt");
//封装入token之前,将密进行一次解密成明文
password = AesEncryptUtil.desEncrypt(password, key, key);
session.removeAttribute("uuidsalt"); //解密成功,立马把后端的随机盐值删除
//2.登录验证的第二步,将表单提交过来的用户名和密码封装到token对象
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
//3.调用subject对象里的login方法,进行登陆验证。
try {
subject.login(token); //让shiro框架来帮我们验证登陆,token会把用户信息传到ShiroRealm的supports方法,后来的AuthenticationInfo doGetAuthenticationInfo(... token)中
}catch(Exception e){
e.printStackTrace();
return "loginError";
}
return "redirect:/admin/main.html";
}
}
测试: http://localhost:8081/shirodemo/
自定义密码验证类MyCredentialsMatcher
----前面已经在spring.xml中配了密码验证类为:HashedCredentialsMatcher extends SimpleCredentialsMatcher.
<bean id="shiroRealm" class="cn.ybzy.shirodemo.service.shiro.ShiroRealm">
<property name="credentialsMatcher" >
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property> 加密算法MD5
<property name="hashIterations" value="1024"></property> 加密次数
<property name="storedCredentialsHexEncoded" value="true"></property> 十六进制加密结果
</bean>
</property>
</bean>
----=-从上面可以看出,这SimpleCredentialsMatcher类是系统提供,东西已经写死了,如果我们想按照我们的方式来进行比对验证的话,这时我们可以自定义它的子类,来重写它的方法或属性。
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token1, AuthenticationInfo info) {
//完全的由我自书已定义用户输入的密码,和数据库中的密码的比对规则
UsernamePasswordToken token = (UsernamePasswordToken) token1;
String pwd=new String(token.getPassword());
System.out.println(pwd);
Session session = SecurityUtils.getSubject().getSession();
String key = (String) session.getAttribute("uuidsalt");
//System.out.println(Arrays.toString(token2.getPassword()));
try {
pwd = AesEncryptUtil.desEncrypt(pwd, key, key);//解密
session.removeAttribute("uuidsalt");
}catch(Exception e1){
throw new IncorrectCredentialsException("受到重放攻击!");
}
String formPassword = (new SimpleHash("MD5",pwd,token.getUsername(),1024)).toString();
String accountCredentials = (String)getCredentials(info);
return accountCredentials.equals(formPassword);
}
}
String formPassword = (new SimpleHash("MD5",pwd,token.getUsername(),1024)).toString();
String accountCredentials = (String)getCredentials(info);
return accountCredentials.equals(formPassword);
----上一段代码参考SimpleCredentialsMatcher类中的equals(Object tokenCredentials, Object accountCredentials)代码。
protected boolean equals(Object tokenCredentials, Object accountCredentials) {
if (log.isDebugEnabled()) {
log.debug("Performing credentials equality check for tokenCredentials of type [" +
tokenCredentials.getClass().getName() + " and accountCredentials of type [" +
accountCredentials.getClass().getName() + "]");
}
if (isByteSource(tokenCredentials) && isByteSource(accountCredentials)) {
if (log.isDebugEnabled()) {
log.debug("Both credentials arguments can be easily converted to byte arrays. Performing " +
"array equals comparison");
}
byte[] tokenBytes = toBytes(tokenCredentials);
byte[] accountBytes = toBytes(accountCredentials);
return MessageDigest.isEqual(tokenBytes, accountBytes);
} else {
return accountCredentials.equals(tokenCredentials);
}
}
spring.xml改:
<bean id="shiroRealm" class="cn.ybzy.shirodemo.service.shiro.ShiroRealm">
<property name="credentialsMatcher" >
<bean class="cn.ybzy.shirodemo.service.shiro.MyCredentialsMatcher">
</bean>
</bean>
ShiroController.java--@PostMapping("/login.html"):
去掉以下代码:
String key = (String) session.getAttribute("uuidsalt");
//封装入token之前,将密进行一次解密成明文
password = AesEncryptUtil.desEncrypt(password, key, key);
session.removeAttribute("uuidsalt");
多个域Realm的登录认证
----在复杂的真实项目中,很可能会出现,登录认证的用户数据,不在一张数据表里,甚至不在一个数据库里,这种情况下进行登录认证,一个Realm就不够了,如:网站提供了多个第三方登陆(qq,weixin,weibo登陆)
----Realm做什么,Realm就是从数据库里获取数据进行登录用户和密码的比对工作,多个数据源,就必须对应多个Realm,从前面跟踪login方法的源码,其实我们已经发现shiro是支持多个Realm的:
- spring.xml配置多个Realm.
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/> <!-- 这里的缓存是realm缓存 -->
<property name="authenticator" ref="authenticator"></property> <!--多个realm时的认证策略配置 -->
<!-- <property name="realm" ref="shiroRealm"></property> 单个Realm是用到-->
<property name="realms"> <!--多个Realm是用到可以进入(ctrl+点击)进入realm所在的类查看有realm和realms属性 -->
<list>
<ref bean="weixinRealm"/> <!-- 三个Realm都做认证,只要一个通过即可,认证按上下先后顺序来做的 -->
<ref bean="qQRealm"/>
<ref bean="shiroRealm"/>
</list>
</property>
</bean>
<bean id="shiroRealm" class="cn.ybzy.shirodemo.service.shiro.ShiroRealm">
<property name="credentialsMatcher">
<bean class="cn.ybzy.shirodemo.service.MyCredentialsMatcher">
</bean>
</property>
</bean>
<bean id="weixinRealm" class="cn.ybzy.shirodemo.service.shiro.WeixinRealm">
<property name="credentialsMatcher">
<bean class="cn.ybzy.shirodemo.service.MyCredentialsMatcher2">
</bean>
</property>
</bean>
<bean id="qQRealm" class="cn.ybzy.shirodemo.service.shiro.QQRealm">
<property name="credentialsMatcher">
<bean class="cn.ybzy.shirodemo.service.MyCredentialsMatcher1">
</bean>
</property>
</bean>
注意: <property name="authenticator" ref="authenticator"></property>一定要放到realms的property标签之前!
- 密码比对验证类,MyCredentialsMatcher1与MyCredentialsMatcher2这里我们定义成一样的,只要有return true就说明验证成功,当然实际情况可不能这样,这里是为了测试多Realm验证而做的事。
public class MyCredentialsMatcher1 extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token1, AuthenticationInfo info) {
//完全的由我自书已定义用户输入的密码,和数据库中的密码的比对规则
return true;
}
}
- WeixinRealm.java,QQRealm.java
-------------------------------------------------------------------------WeixinRealm.java--------------------------------------------------
public class WeixinRealm extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("这个WeixinRealm类里的doGetAuthenticationInfo,就是专门用来实现微信第三方登录!");
AuthenticationInfo info = new SimpleAuthenticationInfo("adminWeiXIN","1234567",null,getName());
return info;
}
}
------------------------------------------------------------------QQRealm.java----------------------------------------------------------
public class QQRealm extends AuthenticatingRealm{
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("这个QQRealm类里的doGetAuthenticationInfo,就是专门用来实现QQ第三方登录!");
AuthenticationInfo info = new SimpleAuthenticationInfo("adminQQ","123456",null,getName());
return info;
}
}
- <property name="authenticator" ref="authenticator"></property>多个realm时的认证策略配置,不配的情况下,默认为只要一个认证通过就可以算认证通过。
----下面我们来配一下,要全通过验证才算通过验证的策略。
这里边的验证策略其实有三种:
--FirstSuccessfulStrategy:第一个Realm验证成功了,就登录成功
--AtLeastOneSuccessfulStrategy:至少有一个Realm验证成功就行,默认的策略
--AllSuccessfulStrategy,全Realm验证成功才行
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean><!--全部通过认证策略 -->
</property>
</bean>
通过我们的实验对比:
1、当由多个Realm类的时候,每一个Realm都会去进行一次登录的认证,它们各自是不会去管其他的Realm认证结果
2、在默认的情况下,多个Realm进行登录认证的时候,只要有一个Realm认证通过,整个的认证就会通过