SpringBoot整合Shiro
- SpringBoot
Shiro 简介
Shiro
是一个功能强大和易于使用的Java
安全框架,为开发人员提供一个直观而全面的解决方案的认证,授权,加密,会话管理
Shiro 四个主要的功能:
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份
- Authorization:授权,即权限验证,判断某个已经认证过的用户是否拥有某些权限访问某些资源,一般授权会有角色授权和权限授权
- Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的,web 环境中作用是和 HttpSession 是一样的
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
Shiro 的其它几个特点:
- Web Support:Web支持,可以非常容易的集成到Web环境;
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
- Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro 的架构有 3 个主要概念:Subject
, SecurityManager
和Realms
Subject
:主体,相当于是请求过来的“用户”SecurityManager
: 管理着所有Subject
,负责进行认证和授权、及会话、缓存的管理,是Shiro
的心脏,所有具体的交互都通过SecurityManager
进行拦截并控制Realm
:一般我们都需要去实现自己的Realm
,可以有1
个或多个Realm
,即当我们进行登录认证时所获取的安全数据来源(帐号/密码)
学习参考:https://www.cnblogs.com/progor/p/10970971.html
SpringBoot整合Shiro环境搭建
步骤如下:
创建
SpringBoot
项目springboot-shiro-first
,导入Shiro
依赖包<!--SpringBoot 整合 Shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency>
编写简单的前端代码
index.html
:<!DOCTYPE html > <html lang="en" xmlns:th="http://www.thymeleaf.org" > <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>首页</h1> <p th:text="${msg}"></p> <a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a> </body> </html>
add.html
:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>add</h1> </body> </html>
update.html
:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>update</h1> </body> </html>
编写
controller
代码MyController
:@Controller public class MyController { @RequestMapping({"/","/index"}) public String toIndex(Model model) { model.addAttribute("msg","hello, Shiro!"); return "index"; } @RequestMapping("/user/add") public String add() { return "user/add"; } @RequestMapping("/user/update") public String update() { return "user/update"; } }
编写
Shiro
配置类ShiroConfig
package com.wyj.config; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ShiroConfig { // ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置安全管理器 bean.setSecurityManager(defaultWebSecurityManager); return bean; } // DefaultWebSecurityManager @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联 UserRealm securityManager.setRealm(userRealm); return securityManager; } // 创建 realm 对象, 需要自定义类 // @Bean(name = "userRealm") @Bean public UserRealm userRealm() { return new UserRealm(); } }
注意:
以上代码为
Shiro
配置类的固定写法,需要创建ShiroFilterFactoryBean
过滤器对象、DefaultWebSecurityManager
对象、自定义Realm
对象通过
@Qualifier
标签拿到userRealm()
方法创建的Bean
对象,绑定到getDefaultWebSecurityManager()
方法中的参数userRealm
上
编写自定义的
Realm
类,需要继承AuthorizingRealm
抽象类/** * 自定义 UserRealm, 需要继承 AuthorizingRealm */ public class UserRealm extends AuthorizingRealm { // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权doGetAuthorizationInfo"); return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthenticationInfo"); return null; } }
测试搭建环境
测试首页和
add
和update
按钮正常跳转
登录拦截
如果点击add
、update
时,要设置拦截功能:在进行页面跳转过程中首先会跳转到一个登录界面。Shiro
可以通过拦截器链实现该功能,常见的拦截器有:
anon
:任何人都可以访问authc
:只有认证后才可以访问logout
:只有登录后才可以访问roles
[角色名]:只有拥有特定角色才能访问perms
["行为"]:只有拥有某种行为的才能访问
代码实现步骤如下:
手动创建拦截跳转页面,并编写相应
controller
login.html
:<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>登录</title> <!--semantic-ui--> <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet"> </head> <body> <!--主容器--> <div class="ui container"> <div class="ui segment"> <div style="text-align: center"> <h1 class="header">登录</h1> </div> <div class="ui placeholder segment"> <div class="ui column very relaxed stackable grid"> <div class="column"> <div class="ui form"> <form th:action="@{/toLogin}" method="post"> <div class="field"> <label>Username</label> <div class="ui left icon input"> <input type="text" placeholder="Username" name="uname"> <i class="user icon"></i> </div> </div> <div class="field"> <label>Password</label> <div class="ui left icon input"> <input type="password" name="pwd"> <i class="lock icon"></i> </div> </div> <input type="submit" class="ui blue submit button"/> </form> </div> </div> </div> </div> </div> </div> <script th:src="@{/js/jquery-3.1.1.min.js}"></script> <script th:src="@{/js/semantic.min.js}"></script> </body> </html>
MyController.java
:@RequestMapping("/toLogin") public String toLogin() { return "login"; }
编写登录拦截功能
登录拦截功能代码的实现需要在
ShiroConfig
类中getShiroFilterFactoryBean(...)
添加以下代码:@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager) { // ... // (1)设置拦截器链 : anon authc logout roles perms Map<String, String> filterMap = new LinkedHashMap<>(); //filterMap.put("/user/add","authc"); //filterMap.put("/user/update","authc"); // filterMap.put("/user/**","authc"); bean.setFilterChainDefinitionMap(filterMap); // (2)设置拦截登录的请求(拦截之后跳转的页面) bean.setLoginUrl("/toLogin"); return bean; }
filterMap.put("/user/**","authc");
表示路径user
下的所有请求必须通过认证才会跳转bean.setLoginUrl("/toLogin");
表示路径user
下的所有请求会跳转到URL
为/toLogin
的登录页面进行认证
测试
点击
add
、update
时,会跳转到登录界面(登录拦截):如果删除代码:
// (2)设置拦截登录的请求(拦截之后跳转的页面) bean.setLoginUrl("/toLogin");
点击
add
、update
时,跳转失败:
用户认证
用户在跳转页面时需要经过验证,否则不能跳转到界面里,登录拦截实现了拦截功能,此时需要在登录界面输入相应的用户名和密码进行验证,来判断输入的用户是否可以通过验证并进行界面跳转。步骤如下:
前端登录界面输入用户名和密码跳转到相应的
controller
进行处理。编写一个对应的controller
@RequestMapping("/login") public String login(String username, String password, Model model) { // 获取当前用户 Subject subject = SecurityUtils.getSubject(); // 封装用户的登录数据 UsernamePasswordToken token = new UsernamePasswordToken(username, password); // 执行登录方法 try { subject.login(token); return "index"; } catch(UnknownAccountException e) { //用户名不存在 model.addAttribute("msg","用户名错误"); return "login"; } catch (IncorrectCredentialsException e) { // 密码不存在 model.addAttribute("msg","密码错误"); return "login"; } }
此时进行登录测试会发现自动执行了
UserRealm
类中的doGetAuthenticationInfo(...)
认证方法在
UserRealm
类补全的doGetAuthenticationInfo(...)
认证方法代码// 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthenticationInfo"); // 模拟获取数据库中的用户名和密码 String uname = "root"; String pwd = "123456"; UsernamePasswordToken userToken = (UsernamePasswordToken) token; System.out.println(userToken.getUsername()); // 验证用户名是否输入正确 if (!userToken.getUsername().equals(uname)) { // 用户名输入错误, return null 表示抛出异常 UnknownAccountException return null; } // 验证密码是否输入正确, 由 Shiro 自动完成 return new SimpleAuthenticationInfo("", pwd, ""); }
用户名密码输入错误,需要将错误信息显示在登录界面,在登录代码login.html中写一个错误信息标签
<div class="field" style="align-content: center"> <p th:text="${msg}" style="color: red"></p> </div>
测试
整合Mybatis
整合MyBatis流程如下:
pom
文件导入jdbc
、Druid
、Mybatis
、log4j
依赖包,yaml
文件中配置数据源参数编写
pojo
对象以及对应的mapper
和MapperXML
映射,yaml
文件中整合MyBatis
编写
service
层代码重写
UserRealm
类中的认证方法doGetAuthenticationInfo(...)
// 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthenticationInfo"); // 模拟获取数据库中的用户名和密码 //String uname = "root"; //String pwd = "123456"; UsernamePasswordToken userToken = (UsernamePasswordToken) token; /*// 验证用户名是否输入正确 if (!userToken.getUsername().equals(uname)) { // 用户名输入错误, return null 表示抛出异常 UnknownAccountException return null; } // 验证密码是否输入正确, 由 Shiro 自动完成 return new SimpleAuthenticationInfo("", pwd, "");*/ User user = userService.queryUserByName(userToken.getUsername()); // 验证用户名是否输入正确 if (user == null) { // 用户名输入错误, return null 表示抛出异常 UnknownAccountException return null; } // 验证密码是否输入正确, 由 Shiro 自动完成 // user.getPwd() pwd 需要定义为 String 类型, 定义为 int 类型报错 return new SimpleAuthenticationInfo("", user.getPwd(), ""); }
注意:
SimpleAuthenticationInfo()
中传入的密码需要String
类型,传入int
类型认证失败
用户授权
如果需要实现部分用户可以访问add
页面,部分页面可以访问update
页面,需要用到用户授权功能,实现步骤如下:
在
ShiroConfig
配置类getShiroFilterFactoryBean(...)
方法中添加如下代码:// 授权 // 携带 user:add 字符串的用户才能有权限访问 user 文件夹下的 add 页面 filterMap.put("/user/add","perms[user:add]"); filterMap.put("/user/update","perms[user:update]");
表示如果想访问
/user
文件夹下的add
资源,需要用户具有user:add
权限之后登录认证成功之后输入正确的用户名和密码发现报错未授权,错误
401
此时执行了
UserRealm
类中的授权方法需要在
UserRealm
类中的授权方法编写授权代码用户未授权可以跳转到自定义未授权页面,而不是
401
页面ShiroConfig
类getShiroFilterFactoryBean(...)
方法添加:// 设置未授权跳转页面 bean.setUnauthorizedUrl("/unauth");
类
MyController
添加:@RequestMapping("/unauth") @ResponseBody public String unauth() { return "用户未授权访问权限!"; }
测试如果用户未授权跳转到以下界面:
在
UserRealm
类中的授权方法编写授权代码// 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权doGetAuthorizationInfo"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 为所有用户增加 user:add 权限 //info.addStringPermission("user:add"); // 获取当前登录用户 Subject subject = SecurityUtils.getSubject(); // 获取认证方法中查到的当前登录用户信息 : User 对象 User currentUser = (User) subject.getPrincipal(); // 获取当前用户在数据库中查询到的拥有的权限, 并为当前用户设置该权限 info.addStringPermission(currentUser.getPerms()); return info; }
对象
subject
通过getPrincipal()
方法获得当前User
用户,User
用户的信息是在认证代码中数据库查询到的,修改认证代码如下所示,通过return
返回了一个SimpleAuthenticationInfo
对象,将user
对象作为SimpleAuthenticationInfo
对象的第一个参数传入,之后授权代码便可以通过getPrincipal()
方法获得当前User
用户的信息了。// 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { // ... return new SimpleAuthenticationInfo(user, user.getPwd(), ""); }
通过
addStringPermission()
为当前User
用户设置与数据库对应一致的权限,数据库表usetest
增加权限字段perms
,对应的User实体类也增加perms
属性@Data //Lombok标签 @AllArgsConstructor @NoArgsConstructor public class User { //属性名称要与数据库对应表字段名称一致(不区分大小写) private int id; private String username; private String pwd; private String perms; }
整合Thymeleaf
当某一个用户没有update
权限时,希望将update
隐藏,需要整合Thymeleaf
,流程如下:
导入
Thymeleaf
与Shiro
整合依赖包<!--thymeleaf整合shiro--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
Shiro
配置类ShiroConfig
中进行配置// 整合ShiroDialect : 用来整合 Shiro 和 Thymeleaf @Bean public ShiroDialect getShiroDialect() { return new ShiroDialect(); }
前端首页
index.html
代码导入命名空间<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
修改
index.html
代码<body> <h1>首页</h1> <p th:text="${msg}"></p> <div> <a th:href="@{/toLogin}">登录</a> </div> <div shiro:hasPermission="user:add"> <a th:href="@{/user/add}">add</a> </div> <div shiro:hasPermission="user:update"> <a th:href="@{/user/update}">update</a> </div> </body>
修改之后,测试发现,不具有add
权限的用户登录,只显示update
操作,此时已经登录成功,需要隐藏登录
选项,通过获取用户session
的操作来实现,步骤如下:
认证方法中将已登录的用户信息保存在
session
中// 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // ... // 获取当前 session, 并将当前用户信息存在 session 中 Subject currentSubject = SecurityUtils.getSubject(); Session session = currentSubject.getSession(); session.setAttribute("loginUser", user); return new SimpleAuthenticationInfo(user, user.getPwd(), ""); }
修改首页
index.html
代码,拿到session
信息,判断该用户是否存在,存在表示已经登录,隐藏登录
选项<div th:if="${session.loginUser==null}"> <a th:href="@{/toLogin}">登录</a> </div>
最终测试发现登录成功之后,登录
选项消失