springboot+shiro进行系统权限管理
前言
以前在开发项目过程中都没有用到过权限管理,但是作为一个合格的后台管理系统进行权限管理是非常必要的,目前常用的java安全框架主要有shiro和spring security,但是看网上大部分人说spring security学习起来比shiro困难的多,而且它不能脱离spring独立运行。由于时间有限,考虑再三,决定先从简单的学起,以后有空再去学习spring security。下面我来分享一下自己学习过程中做的一个简单的demo。
准备过程
-
新建springboot项目,maven导入要用到的jar包
我这里导入的是shiro-spring、shiro-web,其实还可以直接导入springboot 的shiro starter包,两种方式在后面的shiro配置类写法上是有区别的,本文就只介绍第一种的案例。<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.4.1</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
-
springboot配置mybatis数据库连接
#mybatis类型别名 mybatis.type-aliases-package=com.hui.entity spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=root
-
创建数据库,包括用户表、权限表、角色表,我这里为了简单就直接贴图片:
(1)user表:
(2) roles表:
(3) permissions表:
-
创建数据库实体类
@Data public class User { private Integer id; private String user; private String password; } @Data public class Roles { private Integer userId; private String role; } @Data public class Permissions { private String role; private String permission; }
-
编写Mapper接口
这里为了方便不使用xml配置文件的方式,直接用注解@Mapper @Repository public interface UserMapper { @Select("select password from user where user='${userName}'") public String findPasswordByName(@Param("userName") String userName); @Select("select b.role from user a inner join roles b on a.id=b.user_id where a.user=#{userName}") public List<String> findRolesByName(String userName); @Select("select permission from permissions where role=#{role}") public List<String> findPermissionByRole(String role); @Insert("insert into user values(#{user},#{password})") public int insertUser(User user); }
-
编写service
@Service public class UserService { @Autowired private UserMapper userMapper; public String findPasswordByName(String userName) { return userMapper.findPasswordByName(userName); } public List<String> findRolesByName(String userName) { return userMapper.findRolesByName(userName); } public List<String> findPermissionByRole(String role) { return userMapper.findPermissionByRole(role); } }
7.编写页面
(1)登录页面(login.html)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>系统登录页面</h3>
<form th:action="@{/checkLogin}" method="post">
用户名:<input type="text" name="user"/><br/>
密 码:<input type="password" name="password">
<br/>
<input type="submit" value="提交">
</form>
</body>
</html>
(2)新增用户界面(addUser.html)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>add user</title>
</head>
<body>
<h3>添加用户页面</h3>
<form th:action="@{/addUserDeal}" method="post">
用户名:<input type="text" name="user"/><br/>
密 码:<input type="text" name="password">
<br/>
<input type="submit" value="提交">
</form>
</body>
</html>
(2)首页(index.html)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>add user</title>
</head>
<body>
<h3>index界面</h3>
<H3>登录成功</H3>
</body>
</html>
整合shiro(重点)
1.编写shiro自定义Realm类
为了验证方便需要运行该类的main函数*(已注释),得到加密后的密码放到数据库中,
import com.hui.service.UserService;
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.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
public class MyRealm extends AuthorizingRealm {
{
super.setName("myRealm");
}
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String userName = (String) principalCollection.getPrimaryPrincipal();
//获得用户的角色,及权限进行绑定
for(String role: userService.findRolesByName(userName)){
authorizationInfo.addRole(role);
for(String permiss:userService.findPermissionByRole(role)){
authorizationInfo.addStringPermission(permiss);
}
}
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String userName = token.getUsername();
String passwod = userService.findPasswordByName(userName);
if (passwod == null) {
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userName, passwod,
ByteSource.Util.bytes("saltstr"), //加盐,可用随机字符
super.getName()
);
// 返回给安全管理器,由 securityManager 比对密码的正确性
return authenticationInfo;
}
/*public static void main(String[] args) {
SimpleHash hash = new Md5Hash("123456","saltstr",2);
System.out.println(hash.toString());
}*/
}
2.spring shiro 配置类
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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;
@Configuration
public class ShiroConfig {
@Bean
public HashedCredentialsMatcher getMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:MD5算法
hashedCredentialsMatcher.setHashIterations(2);//散列的次数
return hashedCredentialsMatcher;
}
@Bean
public MyRealm myRealm() {
MyRealm myRealm = new MyRealm();
//Shiro加密
myRealm.setCredentialsMatcher(getMatcher());
return myRealm;
}
@Bean
public DefaultWebSecurityManager getSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shirFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(getSecurityManager());
//拦截器
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/checkLogin", "anon");
filterChainDefinitionMap.put("/addUser", "roles[admin]");
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
//其余接口一律拦截,主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "authc");
// 如果不设置默认会自动寻找Web工程根目录下的"/login"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
验证权限
1.编写controller
import com.hui.entity.User;
import com.hui.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.DisabledAccountException;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j
@Controller
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public String toLoginPage() {
return "login";
}
@RequestMapping("/checkLogin")
@ResponseBody
public String checkLogin(User user) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUser(), user.getPassword());
try {
subject.login(token);
return "login success";
} catch (UnknownAccountException e) {
return "账号不存在";
} catch (DisabledAccountException e) {
return "账号未启用";
} catch (IncorrectCredentialsException e) {
return "密码错误";
} catch (Throwable e) {
return "未知错误";
}
}
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/addUser")
public String addUser() {
return "addUser";
}
@RequestMapping("/addUserDeal")
@ResponseBody
public String addUserDeal() {
return "addUser";
}
}
2.启动项目,校验3种情况,我们要先了解shiro自带的过滤器含义:
- authc(org.apache.shiro.web.filter.authc.FormAuthenticationFilter)
基于表单的拦截器;如"/**=authc",如果没有登录会跳到相应的登录页面登录.主要属性:usernameParam:表单提交的用户名参数名(username); passwordParam:表单提交的密码参数名(password);rememberMeParam:表单提交的密码参数名(rememberMe) loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储key(shiroLoginFailure)- authcBasic(org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter)
Basic HTTP身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息- logout(org.apache.shiro.web.filter.authc.LogoutFilter)
退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址;示例"/logout=logout"- anon(org.apache.shiro.web.filter.authc.AnonymousFilter)
匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例"/static/**=anon"- roles(org.apache.shiro.web.filter.authz.RolesAuthorizationFilter)
角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]”- 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风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例"/users=rest[user]",会自动拼出"user:read,user:create,user:update,user:delete"权限字符串进行权限匹配(所有都得匹配,isPermittedAll)- ssl(org.apache.shiro.web.filter.authz.SslFilter)
SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样;- user(org.apache.shiro.web.filter.authc.UserFilter)
用户拦截器,用户已经身份验证/记住我登录的都可;示例"/**=user"- noSessionCreation(org.apache.shiro.web.filter.session.NoSessionCreationFilter)
不创建会话拦截器,调用 subject.getSession(false)不会有什么问题,但是如果 subject.getSession(true)将抛出 DisabledSessionException异常
(1)不登录直接进入首页(/index):
由于在配置类中配置loginurl 路径,且设置了filterChainDefinitionMap.put("/**", “authc”) 即/index不能够匿名访问,直接访问会进入登录界面。如果想能够直接访问,需要在前面添加filterChainDefinitionMap.put("/index", “anon”)
(2)校验用户名密码
(3)校验角色权限
我们配置了添加用户界面需要admin权限:filterChainDefinitionMap.put("/addUser", “roles[admin]”)
所有根据数据库zhangsan具有权限,lisi没有权限。
参考文档
https://www.cnblogs.com/koal/p/5235379.html