Shiro和Spring Security应该是我们比较常用的权限框架了,这篇文章教大家怎么通过springboot整合shiro从零开始搭建一个包含权限控制的后台管理系统。
目录
第一步:创建springboot项目
第二步:添加maven的依赖
第三步:修改配置文件
第四步:创建数据库表
第五步:创建表user对应类
第六步:创建UserRealm
第七步:创建Shiro配置类
第八步:实现登录功能
控制器层
业务层
UserRealm
持久层
第九步:测试登录功能
login.html
login.js
home.html
ShiroConfig.java
第十步:实现授权功能
UserRealm.java
UserController.java
/html/home.html
自定义过滤器实现鉴权
ShiroConfig.java
第十一步:测试鉴权功能
通过IntelliJ IDEA创建一个springboot项目,这里的项目就命名为springboot-shiro
修改pom.xml,添加相关的maven依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.5.9
cn.edu.sgu.www
springboot-shiro
0.0.1-SNAPSHOT
springboot-shiro
Spring Boot整合Shiro权限认证框架
1.8
1.3.2
8.0.28
1.1.21
1.18.22
2.0.8
2.2.2
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
${lombok.version}
mysql
mysql-connector-java
${mysql.version}
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis.version}
com.alibaba
druid
${druid.version}
com.alibaba
fastjson
${fastjson.version}
org.apache.shiro
shiro-spring
${shiro.version}
org.springframework.boot
spring-boot-maven-plugin
完成前面两步之后,删除多余的test包,项目结构如图:
修改系统的配置文件application.yml(修改默认的配置文件application.properties的名称为application.yml,只需要改后缀名),修改完成之后,把以下内容复制到application.yml中。
spring:
profiles:
active: dev
# mybatis的mapper.xml文件的位置
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
最后再新建一个application-dev.yml
# 设置启动端口号
server:
port: 8080
spring:
# 配置数据源
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springboot-shiro
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# 只返回不为null的数据
jackson:
default-property-inclusion: non_null
1、通过navicat数据库连接工具新建数据库springboot-shiro,没有用过navicat的童鞋可以参考博主的文章推荐一款非常简单实用的数据库连接工具Navicat Premiumhttps://blog.csdn.net/heyl163_/article/details/132111378
2、在springboot-shiro数据库下创建用户表user
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '12345' COMMENT '密码',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`gender` tinyint(4) UNSIGNED NOT NULL COMMENT '性别',
`is_enable` tinyint(4) UNSIGNED NOT NULL COMMENT '启用状态',
`last_login_time` datetime NULL DEFAULT NULL COMMENT '上一次登录时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('2023', '系统管理员', 'system', '', '18888888888', 2, 1, '2022-11-25 00:15:42');
INSERT INTO `user` VALUES ('mhxy1218', '沐雨橙风ιε', 'mumu', 'mhxy1218', '16666666666', 1, 1, '2023-07-02 00:00:29');
配置mapper包扫描路径
在springboot的启动类或者任意一个配置类上使用注解@MapperScan注解配置mybatis的mapper包扫描路径
@MapperScan("cn.edu.sgu.www.shiro.mapper")
在项目根目录下创建一个config包,在config包下面创建一个MybatisConfig类。
package cn.edu.sgu.www.shiro.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
/**
* mybatis配置类
* @author heyunlin
* @version 1.0
*/
@Configuration
@MapperScan("cn.edu.sgu.www.shiro.mapper")
public class MybatisConfig {
}
项目根目录下创建realm包,在realm包下创建UserRealm.java,并继承AuthorizingRealm,实现AuthorizingRealm的两个认证和授权的抽象方法。
package cn.edu.sgu.www.shiro.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class UserRealm extends AuthorizingRealm {
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
在config包下创建ShiroConfig.java
package cn.edu.sgu.www.shiro.config;
import cn.edu.sgu.www.shiro.realm.UserRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* shiro配置类
*/
@Configuration
public class ShiroConfig {
/**
* 配置安全管理器
* @param userRealm UserRealm
* @return DefaultWebSecurityManager
*/
@Bean
public DefaultWebSecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 配置Shiro过滤器工厂
* @param securityManager 安全管理器
* @return ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 注册安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
/*
* 设置登录页面的地址
* 当用户访问认证资源的时候,如果用户没有登录,那么就会跳转到该属性指定的页面
*/
shiroFilterFactoryBean.setLoginUrl("/login.html");
return shiroFilterFactoryBean;
}
}
在UserController类中添加一个login()方法,新建UserLoginDTO对象来接收前端传来的用户名和密码,同时通过validation验证这两个字段。
package cn.edu.sgu.www.shiro.controller;
import cn.edu.sgu.www.shiro.dto.UserLoginDTO;
import cn.edu.sgu.www.shiro.restful.JsonResult;
import cn.edu.sgu.www.shiro.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @author heyunlin
* @version 1.0
*/
@RestController
@RequestMapping(path = "/user", produces = "application/json;charset=utf-8")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping(path = "/login", method = RequestMethod.POST)
public JsonResult login(UserLoginDTO loginDTO) {
userService.login(loginDTO);
return JsonResult.success();
}
}
package cn.edu.sgu.www.shiro.service;
import cn.edu.sgu.www.shiro.dto.UserLoginDTO;
/**
* @author heyunlin
* @version 1.0
*/
public interface UserService {
/**
* 登录认证
* @param loginDTO 登录信息
*/
void login(UserLoginDTO loginDTO);
}
UserServiceImpl中实现用户登录的业务代码,当我们调用Subject的login()方法时,会执行UserRealm下面的认证方法doGetAuthenticationInfo()
package cn.edu.sgu.www.shiro.service.impl;
import cn.edu.sgu.www.shiro.dto.UserLoginDTO;
import cn.edu.sgu.www.shiro.entity.User;
import cn.edu.sgu.www.shiro.exception.GlobalException;
import cn.edu.sgu.www.shiro.mapper.UserMapper;
import cn.edu.sgu.www.shiro.restful.ResponseCode;
import cn.edu.sgu.www.shiro.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author heyunlin
* @version 1.0
*/
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Autowired
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public void login(UserLoginDTO loginDTO) {
String username = loginDTO.getUsername();
// 根据用户名查询用户信息
User user = userMapper.selectByUsername(username);
if (user != null) {
if (user.getIsEnable()) {
// shiro登录认证
UsernamePasswordToken token = new UsernamePasswordToken(username, loginDTO.getPassword());
Subject subject = SecurityUtils.getSubject();
subject.login(token);
// 设置session失效时间:永不超时
subject.getSession().setTimeout(-1001);
} else {
throw new GlobalException(ResponseCode.FORBIDDEN, "账号已被锁定,禁止登录!");
}
} else {
throw new GlobalException(ResponseCode.NOT_FOUND, "用户名不存在~");
}
}
}
实现认证的方法,当调用Subject.login()方法时,在shiro框架内部会去调用realm的认证方法。
package cn.edu.sgu.www.shiro.realm;
import cn.edu.sgu.www.shiro.entity.User;
import cn.edu.sgu.www.shiro.exception.GlobalException;
import cn.edu.sgu.www.shiro.mapper.UserMapper;
import cn.edu.sgu.www.shiro.restful.ResponseCode;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class UserRealm extends AuthorizingRealm {
private final UserMapper userMapper;
@Autowired
public UserRealm(UserMapper userMapper) {
this.userMapper = userMapper;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 得到用户名
String username = token.getUsername();
// 根据用户名查询用户信息
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new GlobalException(ResponseCode.BAD_REQUEST, "登录失败,用户不存在~");
}
if (user.getIsEnable()) {
String password = new String(token.getPassword());
if (user.getPassword().equals(password)) {
return new SimpleAuthenticationInfo(user, password, username);
} else {
throw new GlobalException(ResponseCode.BAD_REQUEST, "用户名或密码错误,登录失败!");
}
}
return null;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
UserMapper接口新增selectByUsername()方法
package cn.edu.sgu.www.shiro.mapper;
import cn.edu.sgu.www.shiro.entity.User;
import org.springframework.stereotype.Repository;
/**
* @author heyunlin
* @version 1.0
*/
@Repository
public interface UserMapper {
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return User 查询到的用户信息
*/
User selectByUsername(String username);
}
通过xml的方式绑定sql语句
创建几个简单的页面试一下效果
准备工作:
在resources目录下创建static目录,存放静态资源文件,在static目录下创建js和html目录,把jquery.min.js复制到js目录下。
static目录下创建登录页面login.html
登录页面
js目录下创建login.js,点击登录按钮时提交用户的数据到接口/user/login,完成登录操作
$(document).ready(function () {
$("#login").click(function () {
let username = $("#username").val();
let password = $("#password").val();
$.post("/user/login", {
username: username,
password: password
}, function (res) {
if (res.code === 200) {
location.href = "/html/home.html";
}
});
});
});
html目录下创建一个页面home.html
系统首页
欢迎来到系统首页!
添加资源访问规则,配置登录页面和登录接口的地址可以不需要登录认证就能访问,同时/html/home.html需要登录才能访问。
package cn.edu.sgu.www.shiro.config;
import cn.edu.sgu.www.shiro.realm.UserRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* shiro配置类
*/
@Configuration
public class ShiroConfig {
/**
* 配置安全管理器
* @param userRealm UserRealm
* @return DefaultWebSecurityManager
*/
@Bean
public DefaultWebSecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 配置Shiro过滤器工厂
* @param securityManager 安全管理器
* @return ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 注册安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
/*
* 设置登录页面的地址
* 当用户访问认证资源的时候,如果用户没有登录,那么就会跳转到指定的页面
*/
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 定义资源访问规则
Map map = new LinkedHashMap<>();
/*
* 过滤器说明
* anon:不需要认证就可以访问的资源
* authc:需要登录认证才能访问的资源
*/
map.put("/html/home.html", "authc");
// 不需要认证就能访问
map.put("/login.html", "anon");
map.put("/user/login", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}
当我们访问localhost:8080/login.html时,输入mumu/mhxy1218,点击登录时,会跳转到/html/home.html。
然后我们清空浏览器缓存,刷新页面,发现页面重定向回了login.html。这是因为在shiro配置类里配置了/html/home.html要身份认证之后才能访问。
map.put("/html/home.html", "authc");
接下来,讲解如何通过shiro完成授权,在UserRealm里的doGetAuthorizationInfo()方法中实现授权的代码,查询用户的权限保存到shiro中,为了方便演示效果,我们模拟几条数据。
完成doGetAuthorizationInfo()方法的具体实现
package cn.edu.sgu.www.shiro.realm;
import cn.edu.sgu.www.shiro.entity.User;
import cn.edu.sgu.www.shiro.exception.GlobalException;
import cn.edu.sgu.www.shiro.mapper.UserMapper;
import cn.edu.sgu.www.shiro.restful.ResponseCode;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
/**
* @author heyunlin
* @version 1.0
*/
@Component
public class UserRealm extends AuthorizingRealm {
private final UserMapper userMapper;
@Autowired
public UserRealm(UserMapper userMapper) {
this.userMapper = userMapper;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 得到用户名
String username = token.getUsername();
// 根据用户名查询用户信息
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new GlobalException(ResponseCode.BAD_REQUEST, "登录失败,用户不存在~");
}
if (user.getIsEnable()) {
String password = new String(token.getPassword());
if (user.getPassword().equals(password)) {
return new SimpleAuthenticationInfo(user, password, username);
} else {
throw new GlobalException(ResponseCode.BAD_REQUEST, "用户名或密码错误,登录失败!");
}
}
return null;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set permissions = new HashSet<>();
permissions.add("/user/delete");
permissions.add("/user/update");
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
}
在UserController中添加两个方法delete()和update()
package cn.edu.sgu.www.shiro.controller;
import cn.edu.sgu.www.shiro.dto.UserLoginDTO;
import cn.edu.sgu.www.shiro.restful.JsonResult;
import cn.edu.sgu.www.shiro.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @author heyunlin
* @version 1.0
*/
@RestController
@RequestMapping(path = "/user", produces = "application/json;charset=utf-8")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping(path = "/login", method = RequestMethod.POST)
public JsonResult login(UserLoginDTO loginDTO) {
userService.login(loginDTO);
return JsonResult.success();
}
@RequestMapping(path = "/delete", method = RequestMethod.POST)
public JsonResult delete() {
return JsonResult.success("删除成功");
}
@RequestMapping(path = "/update", method = RequestMethod.POST)
public JsonResult update() {
return JsonResult.success("修改成功");
}
}
home.html中新增两个按钮,同时还有引入jquery。
系统首页
欢迎来到系统首页!
|
自定义过滤器AuthorizationFilter实现鉴权功能,注意,这个过滤器只处理接口资源,所以把项目的静态资源/路径作为排除项,静态资源直接放行。
package cn.edu.sgu.www.shiro.filter;
import cn.edu.sgu.www.shiro.restful.JsonResult;
import cn.edu.sgu.www.shiro.restful.ResponseCode;
import com.alibaba.fastjson.JSON;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 鉴权过滤器
* @author heyunlin
* @version 1.0
*/
@WebFilter
public class AuthorizationFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
String requestURI = request.getRequestURI();
Subject subject = SecurityUtils.getSubject();
if (subject != null && !subject.isPermitted(requestURI)) {
HttpServletResponse response = (HttpServletResponse) resp;
response.setContentType("application/json;charset=utf-8");
// 构建返回对象
JsonResult jsonResult= JsonResult.error(ResponseCode.UNAUTHORIZED, "正在访问未授权的资源");
String data = JSON.toJSONString(jsonResult);
response.getWriter().write(data);
return;
}
chain.doFilter(req, resp);
}
}
把自定义过滤器加入到shiro的过滤器链中,处理删除和修改两个接口。
map.put("/user/delete", "authorization");
map.put("/user/update", "authorization");
package cn.edu.sgu.www.shiro.config;
import cn.edu.sgu.www.shiro.filter.AuthorizationFilter;
import cn.edu.sgu.www.shiro.realm.UserRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* shiro配置类
*/
@Configuration
public class ShiroConfig {
/**
* 配置安全管理器
* @param userRealm UserRealm
* @return DefaultWebSecurityManager
*/
@Bean
public DefaultWebSecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 配置Shiro过滤器工厂
* @param securityManager 安全管理器
* @return ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 注册安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
/*
* 设置登录页面的地址
* 当用户访问认证资源的时候,如果用户没有登录,那么就会跳转到指定的页面
*/
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 定义资源访问规则
Map map = new LinkedHashMap<>();
/*
* 过滤器说明
* anon:不需要认证就可以访问的资源
* authc:需要登录认证才能访问的资源
*/
map.put("/html/home.html", "authc");
// 不需要认证就能访问
map.put("/login.html", "anon");
map.put("/user/login", "anon");
// 设置自定义过滤器
map.put("/user/delete", "authorization");
map.put("/user/update", "authorization");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
Map filters = shiroFilterFactoryBean.getFilters();
filters.put("authorization", new AuthorizationFilter());
shiroFilterFactoryBean.setFilters(filters);
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}
为了测试,注释掉修改接口的资源权限,然后重启一下项目。
点击删除按钮会提升删除成功,但是修改的时候提示未授权。
好了,文章就分享到这里了,如果看完这篇文章感觉对你有所帮助,不要忘了点赞+收藏哦~
代码已经上传到git仓库,可按需获取:
springboot整合shiro实现认证和授权功能https://gitee.com/he-yunlin/springboot-shiro.git
更多代码详情,请参考博主的另一篇文章:
springboot整合shiro实现认证和授权功能改进https://blog.csdn.net/heyl163_/article/details/131518274