框架整合
创建模块
创建一个Maven工程
添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.9.0</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
</project>
添加配置文件
添加application.yml配置文件,注意修改数据库的连接参数。
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?characterEncoding=utf8&useSSL=false
username: root
password: root
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
shiro:
loginUrl: /login/login
添加启动类
package com.shiro.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.shiro.demo.mapper")
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class, args);
}
}
实现登录认证
后端接口服务实现
创建用户表,这个在Shiro笔记02-基本使用里有。
创建实体类User.java
,使用Lombok插件简化代码。
package com.shiro.demo.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String username;
private String password;
private String passwordSalt;
}
创建mapper、service、serviceImpl。
package com.shiro.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shiro.demo.entity.User;
public interface UserMapper extends BaseMapper<User> {
}
package com.shiro.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shiro.demo.entity.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {
}
package com.shiro.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.shiro.demo.entity.User;
import com.shiro.demo.mapper.UserMapper;
import com.shiro.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User getUserByUsername(String username) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
return userMapper.selectOne(queryWrapper);
}
}
自定义Realm
package com.shiro.demo.realm;
import com.shiro.demo.entity.User;
import com.shiro.demo.service.UserService;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 自定义授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 自定义登录认证方法
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = authenticationToken.getPrincipal().toString();
User user = userService.getUserByUsername(username);
if (user != null) {
return new SimpleAuthenticationInfo(username, user.getPassword(), ByteSource.Util.bytes("salt"), username);
}
return null;
}
}
编写配置类
package com.shiro.demo.config;
import com.shiro.demo.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm myRealm;
/**
* 配置DefaultWebSecurityManager
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(3);
myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
defaultWebSecurityManager.setRealm(myRealm);
return defaultWebSecurityManager;
}
/**
* 配置DefaultShiroFilterChainDefinition
*/
@Bean
public DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
defaultShiroFilterChainDefinition.addPathDefinition("/login", "anon");
defaultShiroFilterChainDefinition.addPathDefinition("/**", "authc");
return defaultShiroFilterChainDefinition;
}
}
这里扩展一下:Shiro支持的Filter有哪些,通过官方文档可以看到。
编写Controller
package com.shiro.demo.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/login")
public class LoginController {
@GetMapping("/login")
@ResponseBody
public String login(String username, String password) {
Subject subject = SecurityUtils.getSubject();
AuthenticationToken authenticationToken = new UsernamePasswordToken(username, password);
try {
subject.login(authenticationToken);
return "登录成功";
} catch (AuthenticationException e) {
e.printStackTrace();
return "登录失败";
}
}
}
启动服务,测试代码,首先把数据库里的密码改成md5加盐加密3次后的结果,浏览器访问:http://localhost:8080/login/login?username=username1&password=password查看结果,可以看到“登录成功”,表示验证通过了。
实现前端页面
下面我们再来添加前端页面,这里使用Thymeleaf,所以要有Thymeleaf的相关依赖。
在resources目录下添加templates文件夹,添加login.html页面和main.html页面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>Shiro 登录认证</h1>
<br>
<form action="/login/login">
<div>用户名:<input type="text" name="username" value=""></div>
<div>密码:<input type="password" name="password" value=""></div>
<div><input type="submit" value="登录"></div>
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Shiro 登录认证后主页面</h1>
<br>
登录用户为:<span th:text="${session.user}"></span>
</body>
</html>
修改Controller方法。
package com.shiro.demo.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpSession;
@Controller
@RequestMapping("/login")
public class LoginController {
@GetMapping("/toLogin")
public String login() {
return "login";
}
@GetMapping("/login")
public String login(String username, String password, HttpSession httpSession) {
Subject subject = SecurityUtils.getSubject();
AuthenticationToken authenticationToken = new UsernamePasswordToken(username, password);
try {
subject.login(authenticationToken);
httpSession.setAttribute("user", authenticationToken.getPrincipal().toString());
return "main";
} catch (AuthenticationException e) {
e.printStackTrace();
return "login";
}
}
}
同时,还要修改defaultShiroFilterChainDefinition
里的策略。
@Bean
public DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
defaultShiroFilterChainDefinition.addPathDefinition("/login/**", "anon");
defaultShiroFilterChainDefinition.addPathDefinition("/**", "authc");
return defaultShiroFilterChainDefinition;
启动项目,浏览器访问http://localhost:8080/login/toLogin就来到了登录页面,输入正确的用户名密码,会跳转到main页面,并且在main页面上,可以看到当前登录的用户名信息。
多个Realm的认证策略设置
多个Realm实现原理
当应用程序配置多个Realm时,例如:用户名密码校验、手机号验证码校验等等。
Shiro的ModularRealmAuthenticator会使用内部的AuthenticationStrategy组件判断认证是成功还是失败。
AuthenticationStrategy是一个无状态的组件,它在身份验证尝试中被询问4次(这4次交互所需的任何必要的状态将被作为方法参数):
(1)在所有Realm被调用之前
(2)在调用Realm的getAuthenticationInfo
方法之前
(3)在调用Realm的getAuthenticationInfo
方法之后
(4)在所有Realm被调用之后
认证策略的另外一项工作就是聚合所有Realm的结果信息封装至一个AuthenticationInfo实例中,并将此信息返回,以此作为Subject的身份信息。
Shiro中定义了 3 种认证策略的实现:
AuthenticationStrategy | 描述 |
AtLeastOneSuccessfulStrategy | 只要有一个(或更多)的Realm验证成功,那么认证将视为成功 |
FirstSuccessfulStrategy | 第一个Realm验证成功,整体认证将视为成功,且后续Realm将被忽略 |
AllSuccessfulStrategy | 所有Realm成功,认证才视为成功 |
ModularRealmAuthenticator内置的认证策略默认实现是AtLeastOneSuccessfulStrategy方式。可以通过配置修改策略。
多个Realm代码实现
首先需要定义多个Realm,然后创建DefaultWebSecurityManager的实例,指定AuthenticationStrategy,并设置多个Realm。
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator);
List<Realm> realmList = new ArrayList<>();
realmList.add(myRealm1);
realmList.add(myRealm2);
defaultWebSecurityManager.setRealms(realmList);
return defaultWebSecurityManager;
}
Remember Me功能
Shiro提供了记住我(RememberMe)的功能,比如访问一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问。
基本流程
- 首先在登录页面选中 RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来
- 关闭浏览器再重新打开,会发现浏览器还是记住你的
- 访问一般的网页服务器端,仍然知道你是谁,且能正常访问
- 但是,如果我们访问电商平台时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你
代码实现
修改配置类,增加RememberMe的设置。
package com.shiro.demo.config;
import com.shiro.demo.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm myRealm;
/**
* 配置DefaultWebSecurityManager
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(3);
myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
defaultWebSecurityManager.setRealm(myRealm);
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
return defaultWebSecurityManager;
}
public RememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(simpleCookie());
cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
return cookieRememberMeManager;
}
public SimpleCookie simpleCookie() {
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
// cookie.setDomain(domain);
simpleCookie.setPath("/");
simpleCookie.setHttpOnly(true);
simpleCookie.setMaxAge(30 * 24 * 60 * 60);
return simpleCookie;
}
/**
* 配置DefaultShiroFilterChainDefinition
*/
@Bean
public DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
defaultShiroFilterChainDefinition.addPathDefinition("/login/toLogin", "anon");
defaultShiroFilterChainDefinition.addPathDefinition("/login/login", "anon");
defaultShiroFilterChainDefinition.addPathDefinition("/**", "authc");
defaultShiroFilterChainDefinition.addPathDefinition("/**", "user");
return defaultShiroFilterChainDefinition;
}
// @Bean
// public DefaultWebSecurityManager defaultWebSecurityManager() {
// DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
// modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
// defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator);
// List<Realm> realmList = new ArrayList<>();
// realmList.add(myRealm1);
// realmList.add(myRealm2);
// defaultWebSecurityManager.setRealms(realmList);
// return defaultWebSecurityManager;
// }
}
修改登录方法,传递rememberMe参数。
package com.shiro.demo.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpSession;
@Controller
@RequestMapping("/login")
public class LoginController {
@GetMapping("/toLogin")
public String login() {
return "login";
}
@GetMapping("/login")
public String login(String username, String password, @RequestParam(defaultValue = "false") boolean rememberMe, HttpSession httpSession) {
Subject subject = SecurityUtils.getSubject();
AuthenticationToken authenticationToken = new UsernamePasswordToken(username, password, rememberMe);
try {
subject.login(authenticationToken);
httpSession.setAttribute("user", authenticationToken.getPrincipal().toString());
return "main";
} catch (AuthenticationException e) {
e.printStackTrace();
return "login";
}
}
@GetMapping("/loginRememberMe")
public String loginRememberMe(HttpSession httpSession) {
httpSession.setAttribute("user", "rememberMe");
return "main";
}
}
修改login.html页面,添加rememberMe的checkbox。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>Shiro 登录认证</h1>
<br>
<form action="/login/login">
<div>用户名:<input type="text" name="username"></div>
<div>密码:<input type="password" name="password"></div>
<div>记住用户:<input type="checkbox" name="rememberMe"></div>
<div><input type="submit" value="登录"></div>
</form>
</body>
</html>
修改application.yml里的shiro.loginUrl为:/login/toLogin。
启动项目进行测试,访问:http://localhost:8080/login/loginRemeberMe,页面会自动跳转到登录页:/login/toLogin。
然后,我们输入账户密码,并勾选“记住用户”的checkbox选项框,点击登录,然后在新标签页打开/login/loginRememberMe请求,可以看到页面也跳到了main.html里。
关掉浏览器,再次访问http://localhost:8080/login/loginRememberMe,可以发现,并没有进到登录页,而是直接进到了main.html,说明rememberMe效果实现了。
观察两次登录(勾选和不勾选RememberMe)请求里cookie的值,可以发现,不勾选的时候,rememberMe的cookie值是deleteMe,勾选的时候,rememberMe的cookie值是一个加密字符串,这就是区别,Shiro通过这个加密字符串可以解析出来用户信息,就没有跳转到登录页面。
用户登录认证后登出
用户登录后,配套的有登出操作。直接通过Shiro过滤器即可实现登出。
修改mian.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Shiro 登录认证后主页面</h1>
<br>
登录用户为:<span th:text="${session.user}"></span>
<br>
<a href="/logout">登出</a>
</body>
</html>
修改配置类,添加logout过滤器。
defaultShiroFilterChainDefinition.addPathDefinition("/logout", "logout");
登录后,点击”登出“超链接,就回到了登录页面,观察/logout请求里cookie的rememberMe值,可以发现是deleteMe,也就是把rememberMe的信息给清除掉。
授权、角色认证
授权
用户登录后,需要验证是否具有指定角色指定权限。Shiro也提供了方便的工具进行判断。
这个工具就是Realm的doGetAuthorizationInfo
方法进行判断。触发权限判断的有两种方式:
- 在页面中通过shiro:****属性判断
- 在接口服务中通过注解@Requires****进行判断
后端接口服务注解
通过给接口服务方法添加注解可以实现权限校验,可以加在控制器方法上,也可以加在业务方法上,一般加在控制器方法上。常用注解如下:
- @RequiresAuthentication:验证用户是否登录,等同于
subject.isAuthenticated()
- @RequiresUser:验证用户是否被记忆,等同于
subject.isAuthenticated() && subject.isRemembered()
- @RequiresGuest:验证是否是一个guest请求,是否是游客请求,
subject.getPrinciple()
的值是null - @RequiresRoles:验证subject是否有相应角色,如果没有,抛出
AuthorizationException
异常,如果有,执行方法 - @RequiresPermissions:验证subject是否有相应权限,如果没有,抛出
AuthorizationException
异常,如果有,执行方法
授权验证
没有角色无法访问
添加一个角色验证的方法。
package com.shiro.demo.controller;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/index")
public class IndexController {
@GetMapping("/home")
@ResponseBody
public String home() {
return "hello world";
}
@RequiresRoles("role1")
@GetMapping("/testRoles")
@ResponseBody
public String testRoles() {
return "角色验证通过";
}
}
修改main.html,添加一个超链接,用于触发刚刚的请求。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Shiro 登录认证后主页面</h1>
<br>
登录用户为:<span th:text="${session.user}"></span>
<br>
<a href="/logout">登出</a>
<br>
<a href="/index/testRoles">测试角色</a>
</body>
</html>
修改MyRealm,重写授权方法。
/**
* 自定义授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("进入自定义授权方法");
return null;
}
重启服务,点击超链接进行访问,此时会报错,控制台可以看到,已经走了自定义的授权方法,下面我们就需要再自定义授权方法里写一些代码,实现角色判断和权限判断。
获取角色进行验证
先尝试写死,手动给用户提供一个角色,正常情况下,这个角色是从数据库里获取的,再次通过浏览器访问,查看效果,此时可以看到角色验证通过了。
/**
* 自定义授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("role1");
return simpleAuthorizationInfo;
}
下面把这块改成活的,也就是从数据库获取角色,相关SQL参考这里。
编写mapper方法,service实现,修改MyRealm。
@Select("SELECT role_name FROM user_role WHERE username=#{username}")
List<String> getRoleListByUsername(String username);
@Override
public List<String> getRoleListByUsername(String username) {
return userMapper.getRoleListByUsername(username);
}
/**
* 自定义授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<String> roleList = userService.getRoleListByUsername(principalCollection.getPrimaryPrincipal().toString());
simpleAuthorizationInfo.addRoles(roleList);
return simpleAuthorizationInfo;
}
进行测试,此时数据就是活的啦。
获取权限进行验证
权限验证同理,相关SQL参考这里。
编写mapper方法,service实现,修改MyRealm。
@Select("SELECT permission FROM role_permission WHERE role_name IN (SELECT role_name FROM user_role WHERE username=#{username});")
List<String> getPermissionListByUsername(String username);
@Override
public List<String> getPermissionListByUsername(String username) {
return userMapper.getPermissionListByUsername(username);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<String> roleList = userService.getRoleListByUsername(principalCollection.getPrimaryPrincipal().toString());
simpleAuthorizationInfo.addRoles(roleList);
List<String> permissionList = userService.getPermissionListByUsername(principalCollection.getPrimaryPrincipal().toString());
simpleAuthorizationInfo.addStringPermissions(permissionList);
return simpleAuthorizationInfo;
}
修改controller方法,添加权限验证注解。
@RequiresPermissions("video:get")
@GetMapping("/testPermissions")
@ResponseBody
public String testPermissions() {
return "权限验证通过";
}
修改main.html,添加请求入口。
<a href="/index/testPermissions">测试权限</a>
通过浏览器访问进行测试。
异常处理
对于没有角色,没有权限这样的情况,会报错误页面,这样不够友好,我们可以创建异常处理类,将错误信息提示出来。
创建GlobalException.java。
package com.shiro.demo.exception;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobalException {
@ResponseBody
@ExceptionHandler(UnauthorizedException.class)
public String unauthorizedException(Exception e) {
return "无权限";
}
@ResponseBody
@ExceptionHandler(AuthorizationException.class)
public String authorizationException(Exception e) {
return "权限认证失败";
}
}
前端页面授权验证
要想让Thymeleaf支持Shiro语法,需要添加依赖。
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
添加配置类用于解析shiro属性。
/**
* 配置ShiroDialect解析Shiro相关属性
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
shiro属性,下面写成了标签的样子,实际是shiro属性。
<shiro:guest>用户没有身份验证时显示相应信息,即游客访问信息</shiro:guest>
<shiro:user>用户已经身份验证/记住我登录后显示相应的信息</shiro:user>
<shiro:authenticated>用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的</shiro:authenticated>
<shiro:notAuthenticated>用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证</shiro:notAuthenticated>
<shiro:principal/>相当于((User)Subject.getPrincipals()).getUsername()<shiro:principal property="username"/>
<shiro:lacksPermission name="org:create">如果当前Subject没有权限将显示body体内容</shiro:lacksPermission>
<shiro:hasRole name="admin">如果当前Subject有角色将显示body体内容</shiro:hasRole>
<shiro:hasAnyRoles name="admin,user">如果当前Subject有任意一个角色(或的关系)将显示body体内容</shiro:hasAnyRoles>
<shiro:lacksRole name="abc">如果当前Subject没有角色将显示body体内容</shiro:lacksRole>
<shiro:hasPermission name="user:create">如果当前Subject有权限将显示body体内容</shiro:hasPermission>
修改main.html,可以实现将无权限的标签进行隐藏。
<a shiro:hasRole="role1" href="/index/testRoles">测试角色</a>
<a shiro:hasPermission="video:get" href="/index/testPermissions">测试权限</a>
实现缓存
缓存工具EhCache
EhCache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。可以和大部分Java项目无缝整合,例如:Hibernate中的缓存就是基于EhCache实现的。
EhCache支持内存和磁盘存储,默认存储在内存中,如内存不够时把缓存数据同步到磁盘中。EhCache支持基于Filter的Cache实现,也支持Gzip压缩算法。
EhCache直接在JVM虚拟机中缓存,速度快,效率高,EhCache缺点是缓存共享麻烦,集群分布式应用使用不方便。
EhCache搭建使用
给项目加入Ehcache,添加pom依赖。
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.6.11</version>
<type>pom</type>
</dependency>
在resources下添加ehcache.xml配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!--磁盘的缓存位置-->
<diskStore path="java.io.tmpdir/ehcache"/>
<!--默认缓存-->
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap"/>
</defaultCache>
<!--helloworld 缓存-->
<cache name="HelloWorldCache"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="5"
timeToLiveSeconds="5"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"/>
<!--defaultCache:默认缓存策略,当 ehcache 找不到定义的缓存时,则使用这个缓存策略,只能定义一个-->
<!--
name:缓存名称
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数
eternal:对象是否永久有效,一但设置了,timeout 将不起作用
overflowToDisk:是否保存到磁盘,当系统宕机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false 对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当 eternal=false 对象不是永久有效时使用,默认是0,也就是对象存活时间无穷大
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置 DiskStore(磁盘缓存)的缓存区大小。默认是 30MB。每个 Cache 都应该有自己的一个缓冲区
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒
memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。默认策略是 LRU(最近最少使用)。你可以设置为 FIFO(先进先出)或是 LFU(较少使用)
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)
FIFO,first in first out,这个是大家最熟的,先进先出
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个 hit 属性,hit 值最小的将会被清出缓存
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存
-->
</ehcache>
创建测试类进行测试。
package com.shiro.demo.test;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import java.io.InputStream;
public class Test {
public static void main(String[] args) {
InputStream inputStream = Test.class.getClassLoader().getResourceAsStream("classpath:ehchace.xml");
CacheManager cacheManager = new CacheManager(inputStream);
Cache cache = cacheManager.getCache("HelloWorldCache");
Element element = new Element("name", "value");
cache.put(element);
Element cacheElement = cache.get("name");
System.out.println(cacheElement.getObjectValue());
}
}
Shiro整合EhCache
Shiro官方提供了shiro-ehcache,实现了整合EhCache作为Shiro的缓存工具。可以缓存认证执行的Realm方法,减少对数据库的访问,提高认证效率。
添加pom依赖。
<!--Shiro 整合 EhCache-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
在resources下添加配置文件ehcache/ehcache-shiro.xml。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="ehcache" updateCheck="false">
<!--磁盘的缓存位置-->
<diskStore path="java.io.tmpdir"/>
<!--默认缓存-->
<defaultCache
maxEntriesLocalHeap="1000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="false">
</defaultCache>
<!--登录认证信息缓存:缓存用户角色权限-->
<cache name="loginRolePsCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true"/>
</ehcache>
修改ShiroConfig类。
/**
* 配置DefaultWebSecurityManager
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(3);
myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
defaultWebSecurityManager.setRealm(myRealm);
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
defaultWebSecurityManager.setCacheManager(getEhCacheManager());// 设置缓存管理器
return defaultWebSecurityManager;
}
private EhCacheManager getEhCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
try(InputStream inputStream = ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml")) {
CacheManager cacheManager = new CacheManager(inputStream);
ehCacheManager.setCacheManager(cacheManager);
} catch (IOException e) {
e.printStackTrace();
}
return ehCacheManager;
}
启动项目进行测试,查看日志,执行了3条SQL。
SELECT id,username,password,password_salt FROM user WHERE username=?
SELECT role_name FROM user_role WHERE username=?
SELECT permission FROM role_permission WHERE role_name IN (SELECT role_name FROM user_role WHERE username=?)
再次点击main.html页面的权限验证,角色验证,控制台并没有打印内容,说明此时的信息是从cache里获取的,并没有查询数据库。
会话管理
SessionManager
会话管理器,负责创建和管理用户的会话(Session)生命周期,它能够在任何环境中在本地管理用户会话,即使没有Web/Servlet/EJB容器,也一样可以保存会话。默认情况下,Shiro会检测当前环境中现有的会话机制(比如Servlet容器)进行适配,如果没有(比如独立应用程序或者非Web环境),它将会使用内置的企业会话管理器来提供相应的会话管理服务,其中还涉及一个名为SessionDAO的对象。SessionDAO负责Session的持久化操作(CRUD),允许Session数据写入到后端持久化数据库。
会话管理实现
SessionManager由SecurityManager管理。Shiro提供了三种实现。
顺便说下这个图怎么生成的,先找到SessionManager接口,右键选择Diagrams-Show Diagram…,选择Java class diagram。
右键SessionManager,选择Show Implementations,按住Ctrl+鼠标左键,勾选需要的类,最后按下回车。
- DefaultSessionManager:用于JavaSE环境
- ServletContainerSessionManager:用于web环境,直接使用Servlet容器的会话
- DefaultWebSessionManager:用于web环境,自己维护会话(不使用Servlet容器的会话管理)
获得Session方式
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("key", "value");
Object value = session.getAttribute("key");
Controller中的request,在Shiro过滤器的doFilterInternal()
方法,被包装成ShiroHttpServletRequest
,SecurityManager和 SessionManager会话管理器决定session来源于ServletRequest还是由Shiro管理的会话。无论是通过request.getSession
或 subject.getSession
获取到session,操作session,两者都是等价的。