[TOC]
本文主要记录Spring Boot与Shiro进行整合使用,实现强大的用户权限管理,其中涉及如何完成用户认证(即用户登录),用户授权,thymeleaf页面整合shiro权限标签等知识点。
Spring Boot与Shiro框架简介
Spring Boot框架简介
Spring的诞生是 Java 企业版(Java Enterprise Edition,JEE,也称 J2EE)的
轻量级代替品。无需开发重量级的 Enterprise JavaBean(EJB),Spring 为企业级
Java 开发提供了一种相对简单的方法,通过依赖注入和面向切面编程,用简单的Java 对象(Plain Old Java Object,POJO)实现了 EJB 的功能。
虽然 Spring 的组件代码是轻量级的,但它的配置却是重量级的。
所有Spring配置都代表了开发时的损耗。 因为在思考 Spring 特性配置和解决业务问题之间需要进行思维切换,所以写配置挤占了写应用程序逻辑的时间。除此之外,项目的依赖管理也是件吃力不讨好的事情。决定项目里要用哪些库就已经够让人头痛的了,你还要知道这些库的哪个版本和其他库不会有冲突,这难题实在太棘手。并且,依赖管理也是一种损耗,添加依赖不是写应用程序代码。一旦选错了依赖的版本,随之而来的不兼容问题毫无疑问会是生产力杀手。
Spring Boot 让这一切成为了过去。
Spring Boot 简化了基于Spring的应用开发,只需要“run”就能创建一个独立的、生产级别的Spring应用。Spring Boot为Spring平台及第三方库提供开箱即用的设置(提供默认设置),这样我们就可以简单的开始。多数Spring Boot应用只需要很少的Spring配置。
我们可以使用SpringBoot创建java应用,并使用java –jar 启动它,或者采用传统的war部署方式。
Spring Boot 主要目标是:
l 为所有 Spring 的开发提供一个从根本上更快的入门体验。
l 开箱即用,但通过自己设置参数,即可快速摆脱这种方式。
l 提供了一些大型项目中常见的非功能性特性,如内嵌服务器、安全、指标,健康检测、外部化配置等。
l 绝对没有代码生成,也无需 XML 配置。
Shiro框架简介
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
1、 Authentication 认证 ---- 用户登录
2、 Authorization 授权 --- 用户具有哪些权限
3、 Cryptography 安全数据加密
4、 Session Management 会话管理
5、 Web Integration web系统集成
6、 Interations 集成其它应用,spring、缓存框架
Spring Boot与Shiro整合实现用户认证
分析Shiro的核心API
Subject: 用户主体(把操作交给SecurityManager)
SecurityManager:安全管理器(关联Realm)
Realm:Shiro连接数据的桥梁
Spring Boot整合Shiro
导入shiro与spring整合依赖
修改pom.xml
<!-- shiro与spring整合依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
自定义Realm类
package cn.sitcat.springbootshiro.configs;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @author Hiseico
* @date 2019/4/13 18:07
* @desc 自定义Realm
*/
public class UserRealm extends AuthorizingRealm {
/**
* 执行授权逻辑
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权:Authorization");
return null;
}
/**
* 执行认证逻辑
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证:Authentiction");
}
}
编写Shiro配置类
package cn.sitcat.springbootshiro.configs;
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;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author hiseico
* @date 2019/4/13 17:58
* @desc Shiro的配置类
*/
//声明该类为配置类
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilterFactoyBean
*/
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("webSecurityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//添加shiro内置过滤器
/**
* shiro内置过滤器,可以实现权限的拦截器
* 常用的过滤器:
* anon:无需认证(登录)可以访问
* authc:必须认证才可以访问
* user:如果使用remeberMe的功能可以直接访问
* perms:该资源必须得到资源权限才可以访问
* role:该资源必须得到角色权限才可以访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
/* filterMap.put("/add","authc");
filterMap.put("/update","authc");
*/
//使用统配的过滤器拦截多个url
filterMap.put("/login", "anon");
filterMap.put("/test", "anon");
filterMap.put("/*", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//修改拦截后跳转的url
shiroFilterFactoryBean.setLoginUrl("/login");
return shiroFilterFactoryBean;
}
/**
* 创建DefaultWebSecurityManager
*
* @Qualifier 从ioc容器中找到这个对象
*/
@Bean(name = "webSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("realm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 创建Realm
* 使用@Bean将该方法的返回值放到IOC容器中
*/
@Bean(name = "realm")
public UserRealm getShiroRealm() {
return new UserRealm();
}
}
实现用户认证(登录)操作
登录页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body style="margin:0 auto">
<h3><font color="red" th:text="${msg}"></font></h3>
<form action="login" method="post">
用户名:<input type="text" name="name"/><br>
密码:<input type="password" name="password"/><br>
<input type="submit" value="登录"></input>
</form>
</body>
</html>
Controller登录逻辑
package cn.sitcat.springbootshiro.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author liusong
* @date 2019/4/13 17:40
* @desc
*/
@Controller
public class HelloController {
@RequestMapping("/test")
public String hello(Model model) {
model.addAttribute("msg", "springboot project created success !");
return "success";
}
@RequestMapping("/add")
public String add() {
return "user/add";
}
@RequestMapping("/update")
public String update() {
return "user/update";
}
/**
* 登录操作
*
* @param name
* @param password
* @param model
* @return
*/
@RequestMapping("/login")
public String login(String name, String password, Model model) {
/**
* 使用shiro编写认证的操作
*/
//1.获取Subject
Subject subject = SecurityUtils.getSubject();
//2.封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
try {
//3.执行登录方法
subject.login(token);
} catch (UnknownAccountException e) {
//登录失败,用户名错误
model.addAttribute("msg", "用户名不存在!");
return "login";
} catch (IncorrectCredentialsException e) {
//登录失败,密码错误
model.addAttribute("msg", "用户名或密码错误!");
return "login";
}
return "redirect:/test";
}
}
实现Realm的判断逻辑
package cn.sitcat.springbootshiro.configs;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @author Hiseico
* @date 2019/4/13 18:07
* @desc 自定义Realm
*/
public class UserRealm extends AuthorizingRealm {
/**
* 执行授权逻辑
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权:Authorization");
return null;
}
/**
* 执行认证逻辑
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证:Authentiction");
//假设数据库的用户名和密码
String name = "admin";
String password = "admin";
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
if (!name.equals(token.getUsername())) {
//用户不存在
return null; // shiro底层会抛出UnknowAccountException
}
//判断密码
return new SimpleAuthenticationInfo("", password, "");
}
}
整合MyBatis实现登录
导入mybatis相关的依赖
<!--打开数据库连接相关依赖 使用阿里的druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- SpringBoot的Mybatis启动器 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
创建用户表
-- 创建用户表
CREATE TABLE `user`(
`id` BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT,
`name` varchar(200),
`password` varchar(200)
);
在Springboot中配置相关的JDBC连接、连接池及Mybatis包扫描
# 配置数据源
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#jdbc url
spring.datasource.url=jdbc:mysql:///shiro
# 连接数据库用户名和密码
spring.datasource.username=root
spring.datasource.password=root
# 设置连接池类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 配置MyBatis包扫描
mybatis.type-aliases-package=cn.sitcat.springbootshiro.dao
编写User实体
package cn.sitcat.springbootshiro.model;
/**
* @author hiseico
* @date 2019/4/17 21:16
* @desc
*/
public class User {
private Long id;
private String name;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
UserMapper
package cn.sitcat.springbootshiro.dao;
import cn.sitcat.springbootshiro.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;
@Component
@Mapper
public interface UserMapper{
@Select("select * from User where name = #{name}")
User selectUserByName(@Param("name") String name);
}
UserService
package cn.sitcat.springbootshiro.service;
import cn.sitcat.springbootshiro.model.User;
/**
* @author hiseico
* @date 2019/4/17 21:40
* @desc
*/
public interface UserService {
/**
* 根据用户名查询用户信息
* @param name
* @return
*/
User getUserByName(String name);
}
这里可以使用@Mapper注解直接注入Mapper,也可以在SpringBoot启动类上加包扫描注解@MapperScan("cn.sitcat.springbootshiro.dao")
UserServiceImpl
package cn.sitcat.springbootshiro.service;
import cn.sitcat.springbootshiro.dao.UserMapper;
import cn.sitcat.springbootshiro.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author hiseico
* @date 2019/4/17 21:40
* @desc
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
/**
* 根据用户名查询用户信息
*
* @param name
* @return
*/
@Override
public User getUserByName(String name) {
return userMapper.selectUserByName(name);
}
}
修改UserRealm的认证方法 从数据库查询用户信息
/**
* 执行认证逻辑
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证:Authentiction");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = userService.getUserByName(token.getUsername());
if (user == null) {
//用户不存在
return null; // shiro底层会抛出UnknowAccountException
}
//判断密码
return new SimpleAuthenticationInfo("", user.getPassword(), "");
}
SpringBoot与Shiro整合实现用户授权
使用Shiro内置过滤器拦截资源
实例需求:给添加和更新页面增加用户权限,当用户有相应页面的权限才能访问。
修改ShiroConfig配置类中的ShiroFilterFactoyBean的内容
/**
* 创建ShiroFilterFactoyBean
*/
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("webSecurityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//添加shiro内置过滤器
/**
* shiro内置过滤器,可以实现权限的拦截器
* 常用的过滤器:
* anon:无需认证(登录)可以访问
* authc:必须认证才可以访问
* user:如果使用remeberMe的功能可以直接访问
* perms:该资源必须得到资源权限才可以访问
* role:该资源必须得到角色权限才可以访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
/* filterMap.put("/add","authc");
filterMap.put("/update","authc");
*/
//使用统配的过滤器拦截多个url
filterMap.put("/login", "anon");
filterMap.put("/test", "anon");
//授权过滤器
//注意:当授权拦截后,shiro会自动跳转到未授权页面
filterMap.put("/add", "perms[user:add]");
// filterMap.put("/update","perms[user:update]");
//认证过滤器
filterMap.put("/*", "authc");
//设置未授权提示页面
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//修改拦截后跳转的url
shiroFilterFactoryBean.setLoginUrl("/login");
return shiroFilterFactoryBean;
}
在Controller中配置/noAuth
页面跳转到无权限访问提示页面
@RequestMapping("noAuth")
public String noAuth(Model model) {
model.addAttribute("msg", "无权限!");
return "noAuth";
}
完成Shiro的资源授权
修改UserRealm类的认证方法的实现
/**
* 执行授权逻辑
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权:Authorization");
//给资源进行授权
SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
//添加资源的授权字符串
simpleAuthenticationInfo.addStringPermission("user:add");
return simpleAuthenticationInfo;
}
将写死的授权字符串改为动态获取
1.在User表中增加perms字段,存储用户权限,在model也增加相应的字段
id | name | password | perms |
---|---|---|---|
1 | root | root | user:add |
1 | admin | admin | user:update |
在Mapper和中增加根据用户id查询用户信息的方法
Mapper
@Select("select * from User where id = #{id}")
User selectUserById(@Param("id") Long id);
Service
/**
* 根据id查询用户信息
*
* @param id
* @return
*/
@Override
public User selectUserById(Long id) {
return userMapper.selectUserById(id);
}
4.修改UserRealm中授权方法
package cn.sitcat.springbootshiro.configs;
import cn.sitcat.springbootshiro.model.User;
import cn.sitcat.springbootshiro.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author hiseico
* @date 2019/4/13 18:07
* @desc 自定义Realm
*/
public class UserRealm extends AuthorizingRealm {
/**
* 执行授权逻辑
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权:Authorization");
//给资源进行授权
SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
//添加资源的授权字符串
//simpleAuthenticationInfo.addStringPermission("user:add");
//从数据库查询当前登录的用户的授权字符串
Subject subject = SecurityUtils.getSubject();
/**
*此处获取的User对象为下面执行认证逻辑中调用
* return new SimpleAuthenticationInfo(user, user.getPassword(), "");时
* 将user对象放到改方法的principal参数中传递了过来,该参数的类型为Object
*/
User user = (User) subject.getPrincipal();
User dbUser = userService.selectUserById(user.getId());
simpleAuthenticationInfo.addStringPermission(dbUser.getPerms());
return simpleAuthenticationInfo;
}
@Autowired
private UserService userService;
/**
* 执行认证逻辑
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证:Authentiction");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = userService.getUserByName(token.getUsername());
if (user == null) {
//用户不存在
return null; // shiro底层会抛出UnknowAccountException
}
//判断密码
return new SimpleAuthenticationInfo(user, user.getPassword(), "");
}
}
thymeleaf和shiro标签整合使用
1.导入thymeleaf模板引擎对shiro的拓展
<!--thymeleaf 对shiro的扩展坐标-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2.配置ShiroDialect
在ShiroConfig配置类里面添加getShiroDialect方法
/**
* 配置shiroDialect,用于thymeleaf和shiro标签配置使用
* @return
*/
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
3.在页面上使用shiro标签
让对应权限的用户只能看到相应的内容
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}"></h1>
<div shiro:hasPermission="user:add">
用户添加功能:<a href="/add">添加</a>
</div>
<br>
<div shiro:hasPermission="user:update">
用户编辑功能:<a href="/update">编辑</a>
</div>
</body>
</html>