Shiro与SpringBoot的集成
前言:
当下最流行的java框架就是SpringCloud与SpringBoot了,这篇文章总结了下Shiro与SpringBoot的集成使用,做了一个简单的登录功能。但是登录功能依然未使用数据库,数据是模拟的,下篇文章里会总结Shiro+JSP+SpringBoot+MyBatis+mysql来实现认证授权的真实场景。
一.整合过程
首先需要明确使用Shiro的目的,就是为了让系统中的部分资源不可以被随便访问,或者是接口,或者是静态资源,这类资源的访问我们要对请求进行拦截,怎么拦截呢,Shiro中提供了ShiroFilter用来拦截这些访问受限资源的请求,我们只需要将ShiroFilter注入到Spring的容器中即可,然后配置下ShiroFilter需要过滤哪些请求就可以正常使用了。既然很多请求需要被Shiro过滤后才能访问,那么也有部分资源时不需要被拦截的,比如我们的登录页,注册页,亦或者商城的商品浏览页,商品详情页等等这些都是不能拦截的,这部分就是公共资源了。对于公共资源我们应该进行放行。下面我们就来看下怎么一步步实现Shiro与SpringBoot的整合吧。
1.使用Spring initializr创建SpringBoot工程
如下图所示,我们FIle–>New–>Project下选择该功能,然后可以快速创建一个SpringBoot的工程,若是默认的仓库中没有所需jar,就会使用图片中选中的地址进行下载模板。
2.选择jdk版本,选择启动器
创建项目时,注意这两块的选择就行,项目名随便起,看心情吧,JDK选择自己开发环境已经安装了的就行,不过使用Sping initializr功能,JDK7可能已经不支持了,最好是JDK8或者JDK11,这俩都是目前仍在更新的JDK的大版本,其余的JDK16以下的版本都已经停止更新了。
然后就是选择我们需要的启动器了,启动器这里只需要选择支持JSP的启动器,顺便选择lombok,这是支持类实体注解的依赖,此外还需要选择web开发的启动器就行,如下图。
3.创建jsp页面启动工程
到这里其实一个web工程我们就创建完成了,如果网络不好,我们需要静静等待一会,让项目将jar包下载完成。依赖下载完成后,我们需要创建几个jsp页面用来与后台接口进行交互。我们在main的下面创建一个webapp文件夹用来存放jsp页面文件。
我们新建一个login.jsp的页面,页面内容如下,这也是jsp的标准模板
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>标题</title>
<style type="text/css">
*{
margin: 0;padding: 0;}
form{
margin: 0 auto;padding:15px; width: 300px;height:300px;text-align: center;}
#submit{
padding: 10px}
#submit input{
width: 50px;height: 24px;}
</style>
</head>
<body>
<h1>登录页</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
用户名:<input type="text" name ="username"/><br/>
密 码 :<input type="text" name ="password"/><br/>
<input type="submit" value="登录"><br/>
</form>
</body>
</html>
再创建一个index.jsp页面,供登录成功后进行展示。如下图:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>标题</title>
<style type="text/css">
*{
margin: 0;padding: 0;}
form{
margin: 0 auto;padding:15px; width: 300px;height:300px;text-align: center;}
#submit{
padding: 10px}
#submit input{
width: 50px;height: 24px;}
</style>
</head>
<body>
<h1>系统主页</h1>
<ul>
<a href="${pageContext.request.contextPath}/user/logout">退出登录</a>
<li><a href="">用户管理</a></li>
<li><a href=""></a>商品管理</li>
<li><a href=""></a>商户管理</li>
<li><a href=""></a>内容管理</li>
</ul>
</body>
</html>
jsp页面创建完成后,我们来配置下application.yml配置文件,配置下服务端口、服务名、配置前端文件支持jsp等,如下所示:
# 服务端口
server.port=8888
# 项目访问路径
server.servlet.context-path=/shiro
# 项目名
spring.application.name=shiro
# 配置mvc的视图解析器支持jsp,默认不支持
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
4.启动工程查看登录页面,登录成功配置依赖
从图中看出,我们的项目已经正常启动了,如果没有正常启动,可以编辑下该工程的配置信息更改下Working directory.将该值改成如下图所示,然后重启就会正常了。
这是我们项目已经启动正常,然后加入Shiro与JSP的依赖。如下所示:
<!--引入支持jsp的依赖-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!--引入支持jstl的依赖-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!--引入支持shiro的启动器-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
5.创建controller,自定义Realm,并将Realm等其他对象注入到Spring容器中
我们需要提供一个简单的控制器,里面有登录和退出登录的方法。供jsp的调用。代码如下:
@Controller
@RequestMapping("/user")
public class LoginController {
@PostMapping("/login")
public String login(String username,String password){
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
return "redirect:/login.jsp";
} catch(IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
return "redirect:/login.jsp";
}
return "redirect:/index.jsp";
}
@RequestMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login.jsp";
}
}
控制器写好了,我们还需要写一个自己的Realm,我们都知道,Shiro的数据来源都是Realm,所以我们实现自己的登录必须自己实现一个Realm用来获取认证和授权的信息,这里先不实现授权的部分,先只是进行认证的流程操作,不过在认证时依然使用MD5+盐+hash散列的方式对密码进行加密(前面的文章已经说过怎么实现这里就直接写了)。
public class FirstRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("进入认证方法");
String username = (String)authenticationToken.getPrincipal();
Md5Hash md5Hash = new Md5Hash("123","234@#$",1024);
if(username.equals("luban")){
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("luban",md5Hash.toString(), ByteSource.Util.bytes("234@#$"),this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
Realm定义完成以后,我们需要将自定义的Realm注入到Spring容器中,将对象注入到容器中我们可以在xml文件中实现,也可以使用注解实现,最常用的还是通过配置类来实现,即我们自己定义一个配置类通过@Bean注解将对象注入到Spring容器中,如下所示:
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
Map<String,String> map = new HashMap<>();
map.put("/user/login","anon");//表示该资源无需认证授权,无需授权的应该写在上面
map.put("/user/logout","anon");//表示该资源无需认证授权
map.put("/login.jsp","anon");//表示该资源无需认证授权
map.put("/**","authc");//表示所有资源都需要经过认证授权
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//设置授权失败返回的页面
shiroFilterFactoryBean.setLoginUrl("login.jsp");//这也是默认值
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(FirstRealm firstReaml){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(firstReaml);
return defaultWebSecurityManager;
}
@Bean
public FirstRealm getRealm(){
FirstRealm firstRealm = new FirstRealm();
Md5CredentialsMatcher md5CredentialsMatcher = new Md5CredentialsMatcher();
md5CredentialsMatcher.setHashIterations(1024);
firstRealm.setCredentialsMatcher(md5CredentialsMatcher);
return firstRealm;
}
}
如上所示,ShiroFilter是通过ShiroFilterFactoryBean来获取,这个ShiroFilter的工厂,然后为他注入SecurityManager,这里的SecurityManager是DefaultWebSecurityManager,这个是适用于web版本的安全管理器,web开发就使用它就对了,此外在FirstRealm这个自定义的Realm中我们需要自定义一个密码匹配器,用来匹配MD5+盐+hash散列来加密的密码,并告诉该匹配器我们的密码经过了多少次散列,然后就可以了。
下面列出了,ShiroFilter中支持的配置参数,以及对应的含义,通常都是使用最上面的两种。
6.启动工程,测试登录功能。
到这里我们的采用MD5+盐+hash散列的Shiro+SpringBoot实现的登录功能就完成了,接下来我们来测试下,我们使用的是账号是luban:123,我们先使用luban:1234来登录测试下
然后发现登录失败又跳回了登录页,这是因为我们在配置ShiroFilter时,配置了认证失败的返回页就是login.jsp,所以又跳回了这里。
后端报错如下,很明显这是密码错误。
那我们在使用账号luban:123登录测试下,结果如下:
我们可以发现已经正常进入到系统内部了,这样就验证了我们使用Shiro+SpringBoot实现的登录功能就完全正常了。
二.整合中的问题与思考
在整合中我们可能会碰到各种问题,这里整理了下,我在整合过程中碰到的一些问题以及思考。
1.ShiroFilter过滤路径配置问题
在配置我们需要过滤的路径与需要排除的路径时,有的人说需要将公共资源的路径配置在上面,受限资源配置在下面,就像这种
Map<String,String> map = new HashMap<>();
map.put("/user/login","anon");//表示该资源无需认证授权,无需授权的应该写在上面
map.put("/user/logout","anon");//表示该资源无需认证授权
map.put("/login.jsp","anon");//表示该资源无需认证授权
map.put("/**","authc");//表示所有资源都需要经过认证授权
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
但是经测试将过滤资源的代码,放在公共资源的上方是可以正常使用的,当然不排除版本变更修复了这个问题,但是从目前测试使用的Shiro1.5.3以后配置公共资源与受限资源肯定不需要考虑顺序了。
2.配置了login.jsp为受限资源未登录的展示页,还需要配login.jsp为公共资源吗
有人说配置了受限资源访问不成功就重定向到登录页,然后就不需要为登录页设置公共资源的标识了,在ShiroFilter中我们可以看到,当前我写的demo是配置了登录页为公共资源的,在我测试该场景时,如果仅仅指定受限资源未登录的展示页,然后我们访问受限资源index.jsp就会报这个问题,如下所示:
这个错误图中已经说了,就是因为系统重定向次数过多所导致,访问index.jsp我们没有登录系统就将我们重定向到login.jsp然后,发现login.jsp也不是公共资源继续重定向login.jsp就陷入死循环了,所以这里我们得配置login.jsp为公共资源。然后就正常了。如下:
map.put("/login.jsp","anon");//表示该资源无需认证授权
3.登录成功后非正常情况退出,重新登录系统,用户名和密码错误也会进入系统
当然这里展示的代码并没有这个问题,在我发现这个问题后已经修改了代码,主要是这块代码
@PostMapping("/login")
public String login(String username,String password){
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
return "redirect:/login.jsp";
} catch(IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
return "redirect:/login.jsp";
}
return "redirect:/index.jsp";
}
说下这个错误的原因,等用户登录的用户名或者密码错误时,都会被程序catch住,然后抛出异常,程序继续执行下去就会返回inde.jsp页面,又因为刚刚没有正常退出,第一次登录后,Shiro会缓存登录信息,所以我们可以正常进入到index.jsp页面,我们只需要在catch住错误时,直接返回到login.jsp就可以正常了。
三.总结
这篇文章里总结了Shiro与SpringBoot的整合,思路也很明确,先引入Shiro的依赖,然后自定义Realm,然后通过配置类注入Realm与Filter、SecurityManager等的对象,再提高登录的接口,登录的页面,与登录的首页等,最后分析了可能会碰到的几个问题,希望对路过的朋友有所帮助。