文 | 平哥 日期 | 20200927
用于记录自己Spring Boot整合Shiro过程。
官方有篇教程可以参考:Integrating Apache Shiro into Spring-Boot Applications
基本环境和工具
IDE: IntelliJ IDEA
Maven: 3.6.0
JDK: 1.8
Step 1 搭建基础SSM环境
1.1 创建Maven工程,添加SSM+Thymeleaf+Shiro依赖
Step1 创建Maven工程:
省略用IDEA添加Maven工厂项目步骤,这个默认大家都懂……
提示:新建项目后记得配置IDEA的Maven参数,改为本地自己安装的Maven
Step2 添加Spring Boot父依赖、shiro依赖:
<!--添加Spring Boot父依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<!--shiro 的Spring Boot 启动器-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.6.0</version>
</dependency>
<!--thymeleaf的启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--mybatis的启动器-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--数据库的驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
</dependencies>
1.2 配置application.yml中数据库信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
1.3 编写MVC各层代码
编写基本MVC各层包和目录以及相关代码:
MVC目录结构
1.4 编写Thymeleaf页面
Spring Boot 整合Thymeleaf,无需进行任何配置,只需在resources文件夹下创建templates文件夹,在其中创建html文件,SpringMVC即可自动进行跳转:
JS、css、图片等静态资源须放在static文件夹下
对于公共页面的跳转可以利用路径变量创建一个公用的单元方法:
// 公共页面跳转共用单元方法
@RequestMapping("/{path}")
public String getPage(@PathVariable String path){
return path;
}
Step 2 整合Shiro,实现登录认证
注意:相关依赖已在第一步导入,不在赘述。
2.1 配置application.yml
在Spring Boot配置文件application.yml中配置Shiro的默认登录链接:
shiro:
loginUrl: /login
2.2 编写自定义Realm类和Shiro配置类
Step1 创建com.gcp.shiro包
Step2 在其中创建MyRealm类,继承AuthorizingRealm类,重写认证方法:
/*重写认证方法*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("认证用户中……");
// 从token获取用户登录输入的用户名
String unameFromWeb = token.getPrincipal().toString();
// 利用用户名去数据库查询是否有数据
User user = userService.selectUserByUname(unameFromWeb);
if (user!=null) {
AuthenticationInfo info = new SimpleAuthenticationInfo(token.getPrincipal(), user.getPwd(),
ByteSource.Util.bytes("gcp"), token.getPrincipal().toString());
return info;
}
return null;
}
Step3 创建ShiroConfig类,配置SecurityManager bean和Shiro内置过滤器bean:
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm myRealm;
// 配置Security Manager
@Bean
public DefaultWebSecurityManager getSecurityManager(){
// 实例化SecurityManager
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 实例化Shiro默认的密码匹配器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 设置密码加密算法为md5
matcher.setHashAlgorithmName("md5");
// 设置迭代次数
matcher.setHashIterations(2);
// 将密码匹配器加入自定义realm中:
myRealm.setCredentialsMatcher(matcher);
// 将自定义的realm集成到DefaultWebSecurityManager对象中
securityManager.setRealm(myRealm);
return securityManager;
}
// 配置Shiro默认过滤器
@Bean
public ShiroFilterChainDefinition getFilter(){
DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();
// 放行公共的页面和静态资源的访问
filterChainDefinition.addPathDefinition("/login","anon");
filterChainDefinition.addPathDefinition("/css/**","anon");
filterChainDefinition.addPathDefinition("/js/**","anon");
filterChainDefinition.addPathDefinition("/images/**","anon");
filterChainDefinition.addPathDefinition("/themes/**","anon");
filterChainDefinition.addPathDefinition("/userLogin","anon");
// 其余必须登录才能访问
filterChainDefinition.addPathDefinition("/**","user");
return filterChainDefinition;
}
}
2.3 编写用户登录验证单元方法
在登录的PulicController类中:
/**
* 用户登录方法
* @param uname
* @param pwd
* @return
*/
@RequestMapping("userLogin")
@ResponseBody
public Result userLogin(String uname, String pwd){
// 利用用户名密码实例化Shiro token
UsernamePasswordToken tonken = new UsernamePasswordToken(uname, pwd);
try {
// 获取Subject并进行登录
SecurityUtils.getSubject().login(tonken);
return new Result();
}catch (AuthenticationException e) {
e.printStackTrace();
return new Result("用户名或密码不匹配");
}
}
此处login方法会调用MyRealm的认证方法进行匹配。此时项目即可实现用户登录功能了。
2.4 用Shiro实现remember me
Shiro实现记住我功能十分简单:在Shiro的配置文件进行如下修改:
Step1 在设置SecurityManager方法中修改:(添加设置Shiro的remember me 功能)
// 配置Security Manager
@Bean
public DefaultWebSecurityManager getSecurityManager(){
// 实例化SecurityManager
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 实例化Shiro默认的密码匹配器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 设置密码加密算法为md5
matcher.setHashAlgorithmName("md5");
// 设置迭代次数
matcher.setHashIterations(2);
// 将密码匹配器加入自定义realm中:
myRealm.setCredentialsMatcher(matcher);
// 将自定义的realm集成到DefaultWebSecurityManager对象中
securityManager.setRealm(myRealm);
//设置Shiro的remember me功能
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
rememberMeManager()方法代码:
// 设置shiro的remembermeManager
private RememberMeManager rememberMeManager() {
// 实例化shiro的remembermeManager
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
// 设置cookie的有效期
cookieRememberMeManager.setCookie(rememberMeCookie());
// 添加Cookie加密设置
cookieRememberMeManager.setCipherKey("123123123".getBytes());
return cookieRememberMeManager;
}
rememberMeCookie()方法代码:
// 设置Cookie参数
private SimpleCookie rememberMeCookie() {
SimpleCookie cookie = new SimpleCookie();
cookie.setPath("/");
cookie.setHttpOnly(true);
// 单位是秒
cookie.setMaxAge(3*24*60*60);
return cookie;
}
Step2 设置好后,再次修改用户登录的单元方法:添加一个boolean类型的rememberme参数,并且设置默认值为false:
/**
* 用户登录方法
*/
@RequestMapping("userLogin")
@ResponseBody
public Result userLogin(String uname, String pwd,@RequestParam(defaultValue = "false") Boolean rememberme){
// 利用用户名密码实例化Shiro token
UsernamePasswordToken tonken = new UsernamePasswordToken(uname, pwd,rememberme);
// 省略余下代码,余下代码没变化,相见上部
}
Step 3 Shiro实现后台功能及页面显示授权
鉴权就是判断用户是否有权限执行相应方法或看到页面具体内容
授权就是授予认证用户指定的角色或指定的权限。
3.1 后台功能方法鉴权
Shiro在后台可以用在控制器方法,也可以用在业务方法。通常都在控制器方法上添加注解进行鉴权。
本项目中具体用户的增删改查四个方法,利用注解@RequirePermissions("要求的权限")
进行分别鉴权,具体代码如下:
新建UserController类:
@Controller
public class UserController {
//声明单元方法:用户新增
@RequiresPermissions("user:add")
@RequestMapping("userAdd")
@ResponseBody
public String userAdd(){
System.out.println("新增用户单元方法执行。");
return "恭喜,新增用户成功!";
}
//声明单元方法:用户删除
@RequiresPermissions("user:del")
@RequestMapping("userDel")
@ResponseBody
public String userDel(){
System.out.println("用户信息删除单元方法执行");
return "恭喜,用户删除成功!";
}
//声明单元方法:用户修改
@RequiresPermissions("user:edit")
@RequestMapping("userEdit")
@ResponseBody
public String userEdit(){
System.out.println("用户信息修改单元方法执行");
return "恭喜,用户修改成功!";
}
}
3.2 Thymeleaf页面中鉴权
在需要鉴权的页面中,在<html>
标签中添加属性:
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
在具体的需要进行鉴权的页面元素中使用shiro标识,例如:
<ul style="list-sytle:none;">
<li><a href="/userAdd" shiro:hasPermission="user:add">添加用户</a></li>
<li><a href="/userDel" shiro:hasPermission="user:del">删除用户</a></li>
<li><a href="/userEdit" shiro:hasPermission="user:edit">修改用户</a></li>
<li><a href="/userSel" shiro:hasPermission="user:sel">查询用户</a></li>
</ul>
还需要再添加Thymeleaf整合Shiro的依赖:
<!--配置Thymeleaf整合shiro-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
在shiro配置类中添加shiro标识解析bean:
/**
* 配置页面的shiro标识的解析bean
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
3.3 Shiro授权
首先,重写MyRealm中的授权方法:
/*重写授权方法*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取当前认证用户的用户名
String uname = (String)principalCollection.getPrimaryPrincipal();
// 从数据库查询当前用户的权限信息
List<String> permissionList = userService.getPermissions(uname);
// 从数据库查询当前用户的角色信息
List<String> roleList = userService.getRoles(uname);
// 将查询到的权限、角色信息给Shiro
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissionList);
info.addRoles(roleList);
return info;
}
然后,添加相应的两个查询方法,此处代码省略
最后添加鉴权失败后,进行异常拦截的通知类和方法:
新建ExpController类:
@ControllerAdvice
public class ExpController {
@ResponseBody
@ExceptionHandler(UnauthorizedException.class)
public String handleShiroException(Exception ex) {
return "无权限";
}
@ResponseBody
@ExceptionHandler(AuthorizationException.class)
public String AuthorizationException(Exception ex) {
return "权限认证失败";
}
}
Step 4 Shiro整合EhCache
在授权过程中,我们会发现,Shiro每次都会去访问数据库,较为耗费资源,引入缓存即可解决问题,Shiro 支持很多第三方缓存工具。官方提供了 shiro-ehcache,实现了把 EHCache 当 做 Shiro 的缓存工具的解决方案。其中最好用的一个功能是就是缓存认证执行的 Realm 方 法,减少对数据库的访问。
4.1 添加依赖
<!--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>
4.2 编写ehcache缓存配置
在resources下新建ehcache/ehcache-shiro.xml:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="ehcache" updateCheck="false">
<!-- 磁盘缓存位置 -->
<diskStore path="java.io.tmpdir"/>
<!-- 默认缓存策略 -->
<!--timeToIdleSeconds:缓存钝化时间-->
<!--timeToLiveSeconds:缓存有效时间-->
<defaultCache
maxEntriesLocalHeap="1000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="false">
</defaultCache>
</ehcache>
4.3 修改配置文件shiroconfig
在ShiroConfig类的setSecurityManager方法中加入如下代码:
image
getCacheManager()方法具体代码:
/**
* 设置shiro的CacheManager
*/
private CacheManager getCacheManager() {
// 1.实例化 Shiro 自身的 CacheManager,EhCache 的实现类
EhCacheManager shiroCacheManager = new EhCacheManager();
// 2.获取 EhCache 的配置类文件并转成输入流
InputStream is = null;
try {
is = ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml");
} catch (IOException e) {
e.printStackTrace();
}
// 3.实例化 EhCacheManager 自身对象
net.sf.ehcache.CacheManager ehCacheManager =new net.sf.ehcache.CacheManager(is);
// 4.将 EhCacheManager 自身对象赋值给 Shiro 的 CacheManager
shiroCacheManager.setCacheManager(ehCacheManager);
// 5.返回
return shiroCacheManager;
}
Step 5 实现多次输错密码锁定账号
5.1 在ehcache-shiro.xml中配置缓存策略
<!-- 登录记录缓存策略: 锁定10分钟 -->
<cache name="loginRecordCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
5.2 创建自定义凭证匹配器
创建凭证匹配器RetryLimitHashedCredentialsMatcher继承HashedCredentialsMatcher:
@Component
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
// 声明缓存对象
private Ehcache passwordRetryCache;
// 获取EhCache缓存管理器并获取缓存策略
public RetryLimitHashedCredentialsMatcher(EhCacheManager ehCacheManager) {
this.passwordRetryCache = ehCacheManager.getCacheManager().getCache("loginRecordCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
// 声明变量记录错误次数
int i = 0;
// 1. 获取用户登录次数的缓存信息
// 获取用户的身份信息(身份信息为缓存数据的键名)
String username = token.getPrincipal().toString();
// 获取缓存对象
Element element = passwordRetryCache.get(username);
// 判断是否有缓存数据
if (element==null) {
// 没有缓存数据,新建
Element ele = new Element(username,new AtomicInteger(0));
passwordRetryCache.put(ele);
} else {
// 有缓存,提取自增
AtomicInteger atomicInteger = (AtomicInteger) element.getObjectValue();
i = atomicInteger.incrementAndGet();
}
System.out.println("验证次数:"+i);
// 3. 判断i次数
if (i >= 4) {
throw new ExcessiveAttemptsException();
}
// 4. 进行本次登录判断
boolean match = super.doCredentialsMatch(token,info);
// 5. 如果登录成功,则移除登录记录
if (match) {
passwordRetryCache.remove(username);
}
return match;
}
}
5.3 修改配置类ShiroConfig
-
将EhCacheManager的实例化交给Spring容器管理
在获取EhCacheManager的getCacheManager方法前添加@Bean注解:
image
把此对象的实例化交给Spring容器托管,以便RetryLimitHashedCredentialsMatcher构造器使用
-
修改密码匹配器:
使用在ShiroConfig的设置SecurityManager方法中把原来默认的密码匹配器替换为新自定义的类:
image
5.4 修改登录单元方法
image至此,即完成功能开发。
详细代码请详见个人gitee仓库:https://gitee.com/chenpingcloud/git-repository
网友评论