SpringBoot&Shiro实现权限管理
引言
相信大家前来看这篇文章的时候,是有SpringBoot和Shiro基础的,所以本文只介绍整合的步骤,如果哪里写的不好,恳请大家能指出错误,谢谢!依赖以及一些配置文件请在源码里参考,请参见 https://github.com/Slags/springboot-learn/tree/master/1.springboot-shiro-authentication ,
个人博客:www.fqcoder.cn
一、数据库模板设计
在本文中,我们使用RBAC(Role-Based Access Control,基于角色的访问控制)模型设计用户,角色和权限间的关系。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。如下图所示:
然后我们在来根据这个模型图,设计数据库表,记得自己添加一点测试数据哦
CREATE TABLE `tb_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
CREATE TABLE `tb_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色名称',
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
CREATE TABLE `tb_role_permission` (
`role_id` int(11) NOT NULL COMMENT '角色id',
`permission_id` int(11) NOT NULL COMMENT '权限id'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`create_time` datetime(0) DEFAULT NULL,
`status` int(10) DEFAULT NULL COMMENT '状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
CREATE TABLE `tb_user_role` (
`role_id` int(11) NOT NULL COMMENT '角色id',
`user_id` int(11) NOT NULL COMMENT '用户id'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
二、Pojo设计
我们创建对应的类,笔者这里用了lombok插件,记得先安装插件
@Data
public class User implements Serializable {
private Integer id;
private String username;
private String password;
private Date createTime;
private Integer status;
}
@Data
public class Role implements Serializable {
private Integer id;
private String name;
private String description;
}
@Data
public class Permission implements Serializable {
private Integer id;
private String url;
private String name;
}
三、Dao层设计
因为我们只是做一个演示,只涉及到用户登录,用户角色、权限查找,并未实现过多方法
创建UserMapper
、RolePermissionMapper
、UserRoleMapper
三个接口
注意:记得在Mapper接口上面加一个扫描注解@Mapper或者在boot启动类上加一个@MapperScan(value = "mapper包路径")注解
public interface UserMapper {
@Select("select * from tb_user where username=#{username}")
User selectByName(String username);
}
--------------------------
public interface UserRoleMapper {
/**
*
* 查询用户角色(可能一个用户有多个角色)
* @param username
* @return
*/
@Select("select r.id,r.name,r.description from tb_role r " +
"left join tb_user_role ur on(r.id = ur.role_id)" +
"left join tb_user u on(u.id=ur.user_id)" +
"where u.username =#{username}")
List<Role> findByUserName(String username);
}
------------------------------------------------
public interface RolePermissionMapper {
/**
* 通过角色id查询权限
* @param roleId
* @return
*/
@Select("select p.id,p.url,p.name from tb_permission p " +
"left join tb_role_permission rp on(p.id=rp.permission_id)" +
"left join tb_role r on(r.id=rp.role_id)" +
"where r.id=#{roleId}")
List<Permission> findByRoleId(Integer roleId);
}
四、Shiro整合实现思路
好了,前面的一些东西,都是可以算是准备工作,现在才是真正开始整合Shiro了,我们先来屡一下思路,实现认证权限功能主要可以归纳为3点:
1.定义一个ShiroConfig配置类,配置 SecurityManager Bean , SecurityManager为Shiro的安全管理器,管理着所有Subject;
2.在ShiroConfig中配置 ShiroFilterFactoryBean ,它是Shiro过滤器工厂类,依赖SecurityManager ;
3.自定义Realm实现类,包含 doGetAuthorizationInfo()
和doGetAuthenticationInfo()
方法 ,
五、定义ShiroConfig配置类
/**
* @ClassName ShiroConfig
* @Description TODO
* @Author fqCoder
* @Date 2020/2/29 3:08
* @Version 1.0
*/
@Configuration
public class ShiroConfig {
/**
* 这是shiro的大管家,相当于mybatis里的SqlSessionFactoryBean
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//页面权限控制
shiroFilterFactoryBean.setFilterChainDefinitionMap(ShiroFilterMapFactory.shiroFilterMap());
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
/**
* web应用管理配置
* @param shiroRealm
* @param cacheManager
* @param manager
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager(Realm shiroRealm, CacheManager cacheManager, RememberMeManager manager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setCacheManager(cacheManager);
securityManager.setRememberMeManager(manager);//记住Cookie
securityManager.setRealm(shiroRealm);
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* session过期控制
* @return
* @author fuce
* @Date 2019年11月2日 下午12:49:49
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager defaultWebSessionManager=new DefaultWebSessionManager();
// 设置session过期时间3600s
Long timeout=60L*1000*60;//毫秒级别
defaultWebSessionManager.setGlobalSessionTimeout(timeout);
return defaultWebSessionManager;
}
/**
* 加密算法
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");//采用MD5 进行加密
hashedCredentialsMatcher.setHashIterations(1);//加密次数
return hashedCredentialsMatcher;
}
/**
* 记住我的配置
* @return
*/
@Bean
public RememberMeManager rememberMeManager() {
Cookie cookie = new SimpleCookie("rememberMe");
cookie.setHttpOnly(true);//通过js脚本将无法读取到cookie信息
cookie.setMaxAge(60 * 60 * 24);//cookie保存一天
CookieRememberMeManager manager=new CookieRememberMeManager();
manager.setCookie(cookie);
return manager;
}
/**
* 缓存配置
* @return
*/
@Bean
public CacheManager cacheManager() {
MemoryConstrainedCacheManager cacheManager=new MemoryConstrainedCacheManager();//使用内存缓存
return cacheManager;
}
/**
* 配置realm,用于认证和授权
* @param hashedCredentialsMatcher
* @return
*/
@Bean
public AuthorizingRealm shiroRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
MyShiroRealm shiroRealm = new MyShiroRealm();
//校验密码用到的算法
// shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return shiroRealm;
}
/**
* 启用shiro方言,这样能在页面上使用shiro标签
* @return
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
/**
* 启用shiro注解
*加入注解的使用,不加入这个注解不生效
*/
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
}
注意:(当时笔者遇到的一个小问题,贴出来给大家涨姿势)
注解无效,登录时不会执行验证角色和权限的方法,只会执行登录验证方法,遂查询资料,得知shiro在subject.login(token)方法时不会执行doGetAuthorizationInfo方法,只有在访问到有权限验证的接口时会调用查看权限,于是猜想注解无效,发现shiro的权限注解需要开启才能有用,添加在配置文件中加入
advisorAutoProxyCreator
和getAuthorizationAttributeSourceAdvisor
两个bean开启shiro注解,解决问题。
六.创建ShiroFilterMapFactory类
注意:
1.这里要用LinkedHashMap 保证有序
2.filterChain基于短路机制,即最先匹配原则,
3.像anon、authc等都是Shiro为我们实现的过滤器,我给出了一张表,在文章尾附录,自行查看
/**
* @ClassName ShiroFilterMapFactory
* @Description TODO
* @Author fqCoder
* @Date 2020/2/29 3:09
* @Version 1.0
*/
public class ShiroFilterMapFactory {
public static Map<String, String> shiroFilterMap() {
// 设置路径映射,注意这里要用LinkedHashMap 保证有序
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//对所有用户认证
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
//对所有页面进行认证
filterChainDefinitionMap.put("/**", "authc");
return filterChainDefinitionMap;
}
}
配置完了ShiroConfig后,实现自己的Realm,然后注入到SecurityManager里
七、实现自定义Realm类
自定义Realm类需要继承 AuthorizingRealm 类,实现 doGetAuthorizationInfo()和doGetAuthenticationInfo()方法即可 ,
doGetAuthorizationInfo() 方法是进行授权的方法,获取角色的权限信息
doGetAuthenticationInfo()方法是进行用户认证的方法,验证用户名和密码
/**
* @ClassName MyShiroRealm
* @Description TODO
* @Author fqCoder
* @Date 2020/2/29 3:08
* @Version 1.0
*/
@Service
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private RolePermissionMapper rolePermissionMapper;
/**
* 获取用户角色和权限
* @param principal
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
if(principal == null){
throw new AuthorizationException("principals should not be null");
}
User userInfo= (User) SecurityUtils.getSubject().getPrincipal();
System.out.println("用户-->"+userInfo.getUsername()+"获取权限中");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//用户获取角色集
List<Role> roleList=userRoleMapper.findByUserName(userInfo.getUsername());
Set<String> roleSet=new HashSet<>();
for (Role r:roleList){
Integer roleId=r.getId();//获取角色id
simpleAuthorizationInfo.addRole(r.getName());//添加角色名字
List<Permission> permissionList=rolePermissionMapper.findByRoleId(roleId);
for (Permission p:permissionList){
//添加权限
simpleAuthorizationInfo.addStringPermission(p.getName());
}
}
System.out.println("角色为-> " + simpleAuthorizationInfo.getRoles());
System.out.println("权限为-> " + simpleAuthorizationInfo.getStringPermissions());
return simpleAuthorizationInfo;
}
/**
* 登录认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户输入的用户名密码
String username= (String) token.getPrincipal();
String password=new String((char[])token.getCredentials());
System.out.println("用户输入--->username:"+username+"-->password:"+password);
//在数据库中查询
User userInfo=userMapper.selectByName(username);
if (userInfo == null) {
throw new UnknownAccountException("用户名或密码错误!");
}
if (!password.equals(userInfo.getPassword())) {
throw new IncorrectCredentialsException("用户名或密码错误!");
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, // 用户名
userInfo.getPassword(), // 密码
getName() // realm name
);
return authenticationInfo;
}
}
其中UnknownAccountException
等异常为Shiro自带异常,Shiro具有丰富的运行时AuthenticationException
层次结构,可以准确指出尝试失败的原因。
八、控制层设计
1.创建一个LoginController.class类
用来处理登录访问请求
/**
* @ClassName LoginController
* @Description TODO
* @Author fqCoder
* @Date 2020/2/29 6:06
* @Version 1.0
*/
@Controller
public class LoginController {
@GetMapping("/login")
public String login(){
return "login";
}
@GetMapping("/")
public String home(){
return "redirect:/index";
}
@GetMapping("/index")
public String index(Model model){
User user= (User) SecurityUtils.getSubject().getPrincipal();
model.addAttribute("user",user);
return "index";
}
@PostMapping("login")
@ResponseBody
public AjaxResult login(User user,Boolean rememberMe){
System.out.println("user = " + user);
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
//获取Subject 对象
Subject subject= SecurityUtils.getSubject();
try {
if (rememberMe){
token.setRememberMe(true);
}
subject.login(token);
return AjaxResult.success("/index");
} catch (UnknownAccountException e) {
return AjaxResult.error(e.getMessage());
} catch (IncorrectCredentialsException e) {
return AjaxResult.error(e.getMessage());
}
}
@GetMapping("/403")
public String forbid(){
return "403";
}
}
2.创建一个UserController.class类
用于处理User类的访问请求,并使用Shiro权限注解控制权限:
/**
* @ClassName UserController
* @Description TODO
* @Author fqCoder
* @Date 2020/3/3 15:14
* @Version 1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequiresPermissions("user:queryAll")
@GetMapping("/queryAll")
public String queryAll(){
//只演示框架...功能不实现
return "查询列表";
}
@RequiresPermissions("user:add")
@GetMapping("/add")
public String userAdd(){
return "添加用户";
}
@RequiresPermissions("user:delete")
@GetMapping("/delete")
public String userDelete(){
return "删除用户";
}
}
九、前端页面设计
1.编写login.html页面
这里我只贴重要代码,具体的代码,到这里找哦!
<form id="loginForm">
<input type="text" id="username" name="username" class="text" />
<input type="password" id="password" name="password" />
</form>
<div class="signin">
<input id="loginBut" type="button" value="Login" >
</div>
-------js代码----
<script type="text/javascript">
$.fn.serializeObject = function () {
var o = {};
var a = this.serializeArray();
$.each(a, function () {
if (o[this.name]) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
o[this.name].push(this.value);
} else {
o[this.name] = this.value || '';
}
});
return o;
};
$(function () {
$("#loginBut").click(function () {
var arr=$('#loginForm').serializeObject();
$.ajax({
url: '/login',
type: 'post',
data: arr,
dataType: "json",
success: function (data) {
if (data.code==200){
location.href=data.msg;
} else {
alert(data.msg);
}
},
error: function (data) {
alert(data.msg);
}
})
});
});
</script>
当用户登录进来的时候调到index.html
2.编写index.html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>番茄欢迎您!</h1>
登录用户:【[[${user.username}]]】
<a th:href="@{/logout}">注销</a>
<h2>权限测试</h2>
<a th:href="@{/user/queryAll}">获取用户全部信息</a>
<a th:href="@{/user/add}">添加用户</a>
<a th:href="@{/user/delete}">删除用户</a>
</body>
</html>
3.编写403页面
比较简单,此处能用就行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403</title>
</head>
<body>
<h1>403权限不够</h1>
<a href="/index">首页</a>
</body>
</html>
十、测试&问题
启动项目:访问http://localhost:8080/,它会自动拦截,页面重定向到 http://localhost:8080/login ,登录成功跳转到http://localhost:8080/index
问题:
登录测试用户的时候,访问没有权限的链接请求时,后台抛出org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method
异常
当时以为在ShiroConfig配置类中配置了shiroFilterFactoryBean.setUnauthorizedUrl("/403");
没有权限的请求会自动从定向到/403,然后却是抛出了异常,后来在一篇文章中看到了,说这个设置只对filterChain起作用 ,针对这个问题,我们可以定义一个全局异常捕获类:
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
@ExceptionHandler(value = AuthorizationException.class)
public String handleAuthorizationException() {
return "403";
}
}
然后再启动项目,登录测试账号,访问没有权限的请求,页面成功定向到/403
源码链接: https://github.com/Slags/springboot-learn/tree/master/1.springboot-shiro-authentication
至此,笔者刚开始写,不是写的很好,欢迎各位网友踊跃指出不足,谢谢!
附录:
1.Shiro拦截机制表
Filter Name | Class | Description |
---|---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例/static/**=anon
|
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 基于表单的拦截器;如/**=authc ,如果没有登录会跳到相应的登录页面登录 |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | Basic HTTP身份验证拦截器 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter | 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/),示例/logout=logout
|
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | 不创建会话拦截器,调用subject.getSession(false) 不会有什么问题,但是如果subject.getSession(true) 将抛出DisabledSessionException 异常 |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例/user/**=perms["user:create"]
|
port | org.apache.shiro.web.filter.authz.PortFilter | 端口拦截器,主要属性port(80) :可以通过的端口;示例/test= port[80] ,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样 |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | rest风格拦截器,自动根据请求方法构建权限字符串;示例/users=rest[user] ,会自动拼出user:read,user:create,user:update,user:delete权限字符串进行权限匹配(所有都得匹配,isPermittedAll) |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有所有角色;示例/admin/**=roles[admin]
|
ssl | org.apache.shiro.web.filter.authz.SslFilter | SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口443;其他和port拦截器一样; |
user | org.apache.shiro.web.filter.authc.UserFilter | 用户拦截器,用户已经身份验证/记住我登录的都可;示例/**=user
|