1、搭建
- 对于搭建Springboot+mybatis+shiro+thymeleaf这里就不多说,首先看引用的pom文件吧
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- shiro依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- thymeleaf整合shiro标签 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- FastJson依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.46</version>
</dependency>
2、核心配置
- 其次对于使用shiro,要进行核心配置,代码如下
@Configuration
public class ShiroConfig {
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
* Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*
*
* anno:无需认证(登陆)可以访问
* authc:必须认证才能访问
* user:如果使用rememberMe的功能可以直接访问
* perms:该资源必须得到资源权限可以访问
* role:该资源必须得到角色权限才能访问
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
//访问的是后端url地址为 /login的接口
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/login");
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/bootstrap3/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/layui/**", "anon");
filterChainDefinitionMap.put("/vue/**", "anon");
filterChainDefinitionMap.put("/dl", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/register", "anon");
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
//filterChainDefinitionMap.put("/logout", "logout");
//配置某个url需要某个权限码
//filterChainDefinitionMap.put("/hello", "perms[how_are_you]");
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问;user:remember me的可以访问-->
filterChainDefinitionMap.put("/**", "user"); // 其他路径均需要身份认证,一般位于最下面,优先级最低
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
System.out.println("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(myShiroRealm());
//将缓存注入安全管理器,就不会反复执行 realm的授权方法了;只要实现了shiro的cache接口、CacheManager接口就可以用来注入安全管理器
//shiro自带的一个内存缓存,本质是hashmap,MemoryConstrainedCacheManager(),试验没问题,非常轻,简单的登录用这个
//securityManager.setCacheManager(new MemoryConstrainedCacheManager());
return securityManager;
}
/**
* 身份认证realm; (这个需要自己写,账号密码校验;权限等)
* @return
*/
@Bean
public LoginShiroReaml myShiroRealm() {
LoginShiroReaml loginShiroReaml=new LoginShiroReaml();
loginShiroReaml.setCredentialsMatcher(hashedCredentialsMatcher());
return loginShiroReaml;
}
//设置默认加密方式
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 使用md5 算法进行加密
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 设置散列次数: 意为加密几次
hashedCredentialsMatcher.setHashIterations(3);
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/**
* cookie管理对象;
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager(){
System.out.println("ShiroConfiguration.rememberMeManager()");
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
// cookieRememberMeManager.setCipherKey(org.apache.shiro.codec.Base64.decode("6ZmI6I2j5Y+R5aSn5ZOlAA=="));
cookieRememberMeManager.setCookie(rememberMeCookie());
return cookieRememberMeManager;
}
@Bean
public SimpleCookie rememberMeCookie(){
System.out.println("ShiroConfiguration.rememberMeCookie()");
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//<!-- 记住我cookie生效时间30天 ,单位秒;-->
simpleCookie.setMaxAge(259200);
simpleCookie.setHttpOnly(true);
return simpleCookie;
}
/**
* Shiro生命周期处理器
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
//配置ShiroDialect:用于thymeleaf和shiro标签配合使用
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
}
3、登录页面
- 登录页面的html文件就不展示了,以下是登录的js
function loginAjax() {
$.post("/dl", {
loginname : $('#loginname').val(),
loginpass : $('#loginpass').val()
}, function(data) {
if (data == "0") {
layer.msg('登录失败,账号已被注销!', {
icon : 2,
time : 2000
});
} else if (data == "1") {
layer.msg('登录失败,账号已被锁定,请联系管理员!', {
icon : 2,
time : 2000
});
} else if (data == "2") {
window.location.replace(learn + "/index");
} else {
layer.msg('登录失败,请重新登录!', {
icon : 2,
time : 2000
});
}
});
}
4、登录的控制层
- 登录的控制层(LoginController.java),@SystemLog为系统日志的注解,此注解为自定义,如果没有日志可省略,此注解在最后会说明
// 用户登录
@SystemLog(log= "用户登录")
@RequestMapping("/dl")
@ResponseBody
public String dluser(@RequestParam("loginname") String loginname, @RequestParam("loginpass") String loginpass,
HttpServletRequest request) {
String flag="10";
//LearnLogin loginuser=loginService.findLogin(loginname,loginpass);//登录人传入账号和密码,并返回登录人信息
//主体,当前状态为没有认证的状态“未认证”
Subject subject = SecurityUtils.getSubject();
// 登录后存放进shiro token
UsernamePasswordToken token=new UsernamePasswordToken(loginname,loginpass);
try {
subject.login(token);
LearnLogin loginuser = (LearnLogin)subject.getPrincipal();
//subject.hasRole(loginname);//用于触发realm中的授权doGetAuthorizationInfo()方法
if(loginuser!=null&&!"".equals(loginuser)&&loginuser.getLoginid()!=null&&!"".equals(loginuser.getLoginid())){
if("2".equals(loginuser.getLogintype())){
List<LearnRole> listrole = systemService.findLoginRole(loginuser.getLoginid());//查找该登录人的权限
List<LearnMenu> listmenu = systemService.findLoginAllMenu(loginuser.getLoginid());//查找该登录人的以及菜单目录 顶级菜单系统默认id为0
loginuser.setListRole(listrole);
loginuser.setListMenu(listmenu);
request.getSession().setAttribute(Common.LOGIN_USER, loginuser);
flag="2";//登录成功 并将登录人信息存入session
}else if("1".equals(loginuser.getLogintype())){
flag="1";//登录失败 用户被锁定
}else if("0".equals(loginuser.getLogintype())){
flag="0";//登录失败 用户被注销
}else{
flag="3";//登录失败
}
}else{
flag="3";//登录失败
}
}catch (UnknownAccountException ex) {
flag="3";//登录失败
}catch (Exception ex){
flag="3";//登录失败
}
return flag;
}
// 登录成功后跳转主页面
@RequestMapping("/index")
@SystemLog(log= "登录成功")
public String index(Model model, HttpServletRequest request) {
// 取身份信息
LearnLogin loginuser = (LearnLogin) request.getSession().getAttribute(Common.LOGIN_USER);
// 根据用户id取出菜单
//List<SysMenu> menus = menuService.selectMenusByUser(user);
request.getSession().setAttribute("menus", loginuser.getListMenu());
//model.addAttribute("menus", loginuser.getListMenu());
model.addAttribute("user", loginuser);
return "index";
}
5、这里是重点
- 如果运用shiro的话,需要继承AuthorizingRealm,重写里面的方法,代码如下
@Service("loginShiroReaml")
public class LoginShiroReaml extends AuthorizingRealm {
@Resource(name = "loginService")
private LoginService loginService;
@Resource(name = "systemService")
private SystemService systemService;
/**
* 用户登录进行认证
*
* 1.和授权方法一样,AuthenticatingRealm的getAuthenticationInfo,先判断缓存是否有认证信息,没有就调用
* 但试验,登录之后,再次登录,发现还是调用了认证方法,说明第一次认证登录时,没有将认证信息存到缓存中。不像授权信息,
* 将缓存注入安全管理器,就自动保存了授权信息。 难道无法 防止故意多次登录 ,按理说不应该啊?
* 2 可以在登录controller简单用session是否有key 判断是否登录?
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("进来验证了");
//验证账号密码
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//实际上这里在查询数据库是直接根据token.getUsername()来进行查询,从而获取user里面的password
LearnLogin loginuser = loginService.findLogin(token.getUsername());
if(loginuser==null){
return null;
}
//盐值
ByteSource credentialsSalt = ByteSource.Util.bytes("Storm");
//封装用户信息,构建AuthenticationInfo对象并返回
AuthenticationInfo info = new SimpleAuthenticationInfo(loginuser, loginuser.getLoginpass(),
credentialsSalt, this.getClass().getSimpleName());
return info;
}
/**
* 1.根据用户user->2.获取角色id->3.根据角色id获取权限permission
*
* 1.授权方法,在请求需要操作码的接口时会执行此方法。不需要操作码的接口不会执行
* 2.实际上是 先执行 AuthorizingRealm,自定义realm的父类中的 getAuthorizationInfo方法,
* 逻辑是先判断缓存中是否有用户的授权信息(用户拥有的操作码),如果有 就直返回不调用自定义 realm的授权方法了,
* 如果没缓存,再调用自定义realm,去数据库查询。
* 用库查询一次过后,如果 在安全管理器中注入了 缓存,授权信息就会自动保存在缓存中,下一次调用需要操作码的接口时,
* 就肯定不会再调用自定义realm授权方法了。 网上有分析AuthorizingRealm,shiro使用缓存的过程
* 3.AuthorizingRealm 有多个实现类realm,推测可能是把 自定义realm注入了安全管理器,所以才调用自定义的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
SimpleAuthorizationInfo simpleAuthorInfo = new SimpleAuthorizationInfo();
LearnLogin user = (LearnLogin)principals.getPrimaryPrincipal();
//这应该把查出来的无序菜单手动放到缓存
List<LearnMenu> resourceList=new ArrayList<LearnMenu>();
// 功能列表
Set<String> menus = new HashSet<String>();
if (user.isAdmin()){
simpleAuthorInfo.addRole("admin");
simpleAuthorInfo.addStringPermission("*:*:*");
}else{
resourceList = systemService.getResourceByUserId(user.getLoginid());
List<String> resourceIds=new ArrayList<>();
if(resourceList!=null &&!resourceList.isEmpty()){
for(LearnMenu reousrce:resourceList){
resourceIds.add(reousrce.getMid());
}
}
menus = systemService.getPermsByUserId(user.getLoginid());
// 角色加入AuthorizationInfo认证对象
simpleAuthorInfo.addRoles(resourceIds);
// 权限加入AuthorizationInfo认证对象
simpleAuthorInfo.setStringPermissions(menus);
}
return simpleAuthorInfo;
}
}
6、权限分配
- 对于权限分配页面的展示,这里使用shiro+thymeleaf结合,代码如下
这里只贴出相关的权限按钮,其余就不展示
<div class="layui-btn-group">
<button type="button" class="layui-btn layui-btn-sm" onclick="adduser()" shiro:hasPermission="system:user:add" >增加</button>
<button type="button" id="bjyh" class="layui-btn layui-btn-sm layui-btn-normal" onclick="edituser()" shiro:hasPermission="system:user:edit">编辑</button>
<button type="button" class="layui-btn layui-btn-sm layui-btn-danger" onclick="removeuser()" shiro:hasPermission="system:user:remove">删除</button>
</div>
<div class="layui-btn-group">
<button type="button" class="layui-btn layui-btn-sm layui-btn-warm" onclick="importuser()" shiro:hasPermission="system:user:import">导入</button>
<button type="button" class="layui-btn layui-btn-sm layui-btn-primary" onclick="exportuser()" shiro:hasPermission="system:user:export">导出</button>
</div>
这里在html中使用shiro:hasPermission,需要在html头部引入包
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
而对于shiro:hasPermission,如果该用户有这个权限或者角色则页面就展示这个按钮,若没有响应的权限和角色,页面就不展示该对于的按钮,
此处页面只是数据,需要在pom文件中引入响应的依赖,在1处已贴出注释,并且需要在ShiroConfig 的核心配置中配置响应的方法,在2处最后也以贴出注释
7、最后
对于shiro的整个权限分配大致就是这样,仅供参考
8.截图
![image.png](https://img-blog.csdnimg.cn/img_convert/22e34b36a440c2a64e4efc90db8963ce.png)
![image.png](https://img-blog.csdnimg.cn/img_convert/d183695c4125c21d13eab87b34efab80.png)
![image.png](https://img-blog.csdnimg.cn/img_convert/1b25ef931dddf78fafdbb2d252559a56.png)