# Shiro 安全模块
[TOC]
1. 准备 SpringMVC 开发环境;
2. 引入 Shiro 依赖;
3. 配置 ShiroConfig 类;
4. 配置自定义 Realm;
5. 登陆验证;
6. 注册验证;
## 准备 SpringMVC 开发环境
准备数据库用户表,存放用户信息。
```sql
-- ----------------------------
-- Table structure for user_t
-- ----------------------------
DROP TABLE IF EXISTS `user_t`;
CREATE TABLE `user_t` (
`id` varchar(32) NOT NULL,
`username` varchar(64) NOT NULL,
`password` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS=1;
```
编写 DAO 接口,用于根据 Username 查询信息以及插入数据。
userMapper.java
```java
@Repository
public interface UserMapper {
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return 将数据封装到Map类型中
*/
public Map<String, Object> queryInfoByUsername(String username);
/**
* 插入一条数据
* @param data Map中包含id,username,password
*/
public void insertData(Map<String, String> data);
}
```
userMapper.xml
```xml
<?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="com.myz.shirodemo.dao.UserMapper">
<select id="queryInfoByUsername" parameterType="java.lang.String" resultType="java.util.Map">
SELECT id, username, password FROM user_t WHERE username = #{username,jdbcType=VARCHAR}
</select>
<insert id="insertData" parameterType="java.util.Map">
INSERT INTO user_t ( id, username,password )
VALUES ( #{id, jdbcType=VARCHAR}, #{username, jdbcType=VARCHAR},#{password, jdbcType=VARCHAR});
</insert>
</mapper>
```
## 引入 Shiro 依赖
```xml
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
<!-- shiro END-->
```
## 配置 ShiroConfig 类
```java
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器。匹配原则是最上面的最优先匹配
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
// 配置不会被拦截的链接
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/doLogin", "anon");
filterChainDefinitionMap.put("/doRegister", "anon");
filterChainDefinitionMap.put("/register", "anon");
// 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/doLogout", "logout");
// 剩余请求需要身份认证
filterChainDefinitionMap.put("/**", "authc");
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 未授权界面;
// shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean(name = "myShiroRealm")
public ShiroRealm myShiroRealm(HashedCredentialsMatcher matcher){
ShiroRealm myShiroRealm = new ShiroRealm();
myShiroRealm.setCredentialsMatcher(matcher);
return myShiroRealm;
}
@Bean
public SecurityManager securityManager(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm(matcher));
return securityManager;
}
/**
* 密码匹配凭证管理器
*
* @return
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 采用MD5方式加密
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 设置加密次数
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}
}
```
1. `shirFilter`( `SecurityManagersecurityManager` ) 方法,是设置 `shiro` 的过滤规则。用于控制哪些请求需要身份认证后才能继续执行,哪些不需要认证等。
身份验证相关:
- authc:基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址;failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure);
- authcBasic:Basic HTTP 身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application);
- logout:退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/); 示例“/logout=logout”
- user:用户拦截器,用户已经身份验证 / 记住我登录的都可;示例“/**=user”
- anon:匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon”
授权相关的:
- roles:角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]”
- perms:权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例“/user/**=perms[“user:create”]”
- port:端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径 / 参数等都一样
- rest:rest 风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll);
- ssl:SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样;
- noSessionCreation:不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常;
2. `myShiroRealm`(`HashedCredentialsMatchermatcher`) 用于配置自定义的 `Realm` 。在 `Shiro` 中,所有有关身份认证及授权管理数据源的获取与管理,都在 `Realm` 中进行。
3. `hashedCredentialsMatcher()` 用于生成加密规则。这里采用 `MD5` 加密 `1024` 次的方式对密码进行加密处理。
4. `securityManager( `HashedCredentialsMatchermatcher` )` 将加密规则属性设置到自定义的 `ShiroRealm` 中,并将这个 `Realm` 加载到 `SecurityManager` 中。
## 配置自定义 Realm
```java
public class ShiroRealm extends AuthenticatingRealm {
@Autowired
private BaseService baseService;
private SimpleAuthenticationInfo info = null;
/**
* 1.doGetAuthenticationInfo,获取认证消息,如果数据库中没有数,返回null,如果得到了正确的用户名和密码,
* 返回指定类型的对象
*
* 2.AuthenticationInfo 可以使用SimpleAuthenticationInfo实现类,封装正确的用户名和密码。
*
* 3.token参数 就是我们需要认证的token
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 将token装换成UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
// 获取用户名即可
String username = upToken.getUsername();
// 查询数据库,是否查询到用户名和密码的用户
Map<String, Object> userInfo = baseService.queryInfoByUsername(username);
if(userInfo != null) {
// 如果查询到了,封装查询结果,返回给我们的调用
Object principal = userInfo.get("username");
Object credentials = userInfo.get("password");
// 获取盐值,即用户名
ByteSource salt = ByteSource.Util.bytes(username);
String realmName = this.getName();
// 将账户名,密码,盐值,realmName实例化到SimpleAuthenticationInfo中交给Shiro来管理
info = new SimpleAuthenticationInfo(principal, credentials, salt,realmName);
}else {
// 如果没有查询到,抛出一个异常
throw new AuthenticationException();
}
return info;
}
}
```
1. 这里我只做了身份认证。新建一个 `ShiroRealm` 类继承 `AuthenticatingRealm` 类,实现 `doGetAuthenticationInfo( AuthenticationTokenauthenticationToken` ) 方法。
2. 这个方法主要就是用于获取数据库中的账户信息,以便用于和用户登录时从前台传过来的账户密码进行对比。
3. 根据用户名到用户表中查询账户名密码,并设置好盐值。这里的盐值要和 `ShiroConfig` 中的盐值规则一样。将账户名,密码,盐值, `realmName` 实例化到 `SimpleAuthenticationInfo` 中交给 `Shiro` 来管理。
4. 如果账户不存在,则抛出 `AuthenticationException` 异常。
5. 这样,每次用户进行 `login` 操作时,就会调用 `doGetAuthenticationInfo` 方法。 `Shiro` 就自动帮我们校验了账户密码是否匹配。
## 登陆验证
```java
@Controller
public class MyController {
@Autowired
private BaseService baseService;
private final Logger logger = LoggerFactory.getLogger(MyController.class);
@RequestMapping("/doLogin")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password) {
// 创建Subject实例
Subject currentUser = SecurityUtils.getSubject();
// 将用户名及密码封装到UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
currentUser.login(token);
// 判断当前用户是否登录
if (currentUser.isAuthenticated() == true) {
return "/index.html";
}
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("登录失败");
}
return "/loginPage.html";
}
@RequestMapping("/doRegister")
public String doRegister(@RequestParam("username") String username,
@RequestParam("password") String password) {
boolean result = baseService.registerData(username,password);
if(result){
return "/login";
}
return "/register";
}
@RequestMapping(value = "/login")
public String login() {
logger.info("login() 方法被调用");
return "loginPage.html";
}
@RequestMapping(value = "/register")
public String register() {
logger.info("register() 方法被调用");
return "registerPage.html";
}
@RequestMapping(value = "/hello")
public String hello() {
logger.info("hello() 方法被调用");
return "helloPage.html";
}
}
```
1. 在 `doLogin` 方法中,实现登录认证过程。
2. 首先获取当前 `Subject` 实例
3. 将用户名和密码封装到 `UsernamePasswordToken` 中
4. 用当前 `Subject` 实例执行 `login` 方法,传入参数为刚刚封装的 `token` 。执行 `login` 方法后, `shiro` 框架最终就会调用刚刚自定义 `ShiroRealm` 中的 `doGetAuthenticationInfo` 方法。
5. 用 `isAuthenticated()` 方法判断用户是否已经登录,如果是则跳转到登录后的页面(这里我跳转到的是 `index.html`)。如果登录失败,则走报异常,最后还是跳转到登录界面。
6. 这里我只 `catch` 了 `AuthenticationException` 异常。然而在 `AuthenticationException` 下有多个子异常,用于各种登录失败的场景,比如账户名不存在,密码不对,登录次数过多等等。大家针对不同的情况做不同的处理。但有一点建议,就是对于前台用户来说,不要暴露过多的错误信息,只是报一个登录失败即可,提高安全性。
## 注册验证
在 `service` 中对 `DAO` 进行封装,实现信息查询以及信息注册。
```
public interface BaseService {
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return 将数据封装到Map类型中
*/
public Map<String, Object> queryInfoByUsername(String username);
/**
* 注册功能
* @param username 用户名
* @param password 密码
* @return
*/
public boolean registerData(String username, String password);
}
```
## MD5 加密
```java
@Service
public class BaseServiceImpl implements BaseService {
@Autowired
private UserMapper userMapper;
@Override
public Map<String, Object> queryInfoByUsername(String username) {
return userMapper.queryInfoByUsername(username);
}
@Override
public boolean registerData(String username, String password) {
// 生成uuid
String id = UUIDUtil.getOneUUID();
// 将用户名作为盐值
ByteSource salt = ByteSource.Util.bytes(username);
/*
* MD5加密:
* 使用SimpleHash类对原始密码进行加密。
* 第一个参数代表使用MD5方式加密
* 第二个参数为原始密码
* 第三个参数为盐值,即用户名
* 第四个参数为加密次数
* 最后用toHex()方法将加密后的密码转成String
* */
String newPs = new SimpleHash("MD5", password, salt, 1024).toHex();
Map<String, String> dataMap = new HashMap<>();
dataMap.put("id", id);
dataMap.put("username", username);
dataMap.put("password", newPs);
// 看数据库中是否存在该账户
Map<String, Object> userInfo = queryInfoByUsername(username);
if(userInfo == null) {
userMapper.insertData(dataMap);
return true;
}
return false;
}
}
```
1. 注册时注意,由于之前配置了盐值规则及加密规则,所以这里要对用户输入的密码也做相同的处理之后再存入数据库中。
2. 使用 `SimpleHash` 类完成密码的加密。最后用 `toHex()` 将加密后的密码转成 `String` 。
## Shiro 缓存配置
```xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shirocache">
<diskStore path="java.io.tmpdir"/>
<!-- 登录记录缓存 锁定10分钟 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authorizationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authenticationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro-activeSessionCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro_cache"
maxElementsInMemory="2000"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
maxElementsOnDisk="0"
overflowToDisk="true"
memoryStoreEvictionPolicy="FIFO"
statistics="true">
</cache>
</ehcache>
```