Shiro安全框架简单入门
源地址:https://blog.csdn.net/qq_41112238/article/details/104410281
基础概念
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
三个核心组件:Subject、 SecurityManager 、 Realms.
- Subject:
即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。 - SecurityManager:
它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。 - Realm:
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
环境搭建
项目配置
创建一个springboot项目,完整的pom文件:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.12.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.sjh</groupId> <artifactId>shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <name>shiro</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.3</version> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.9</version> </dependency> <!-- 工具包 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.5</version> </dependency> <!-- spring工具包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.0.2.RELEASE</version> </dependency> <!-- jsp --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <!-- servlet --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </dependency> <!-- jstl --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
然后还需要在application.prpperties中配置数据库的信息
# 数据库的配置 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql:///test?serverTimezone=UTC spring.datasource.username=root spring.datasource.password=sjh2019
测试启动成功
创建数据库
权限表:
CREATE TABLE permission( pid INT(11) PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(255) NOT NULL DEFAULT '', url VARCHAR(255) DEFAULT '' )ENGINE=INNODB DEFAULT CHARSET= utf8; INSERT INTO permission VALUES (1,'add',''); INSERT INTO permission VALUES (2,'del',''); INSERT INTO permission VALUES (3,'update',''); INSERT INTO permission VALUES (4,'query','');
用户表:
CREATE TABLE USER( uid INT(11) PRIMARY KEY AUTO_INCREMENT, username VARCHAR(255) NOT NULL DEFAULT '', PASSWORD VARCHAR(255) NOT NULL DEFAULT '' )ENGINE=INNODB DEFAULT CHARSET= utf8; INSERT INTO USER VALUES (1,'kobe','123'); INSERT INTO USER VALUES (2,'james','123');
角色表:
CREATE TABLE role(r rid INT(11) PRIMARY KEY AUTO_INCREMENT, rname VARCHAR(255) NOT NULL DEFAULT '' )ENGINE=INNODB DEFAULT CHARSET= utf8; INSERT INTO role VALUES (1,'admin'); INSERT INTO role VALUES (2,'customer');
权限-角色表:
CREATE TABLE permission_role( rid INT(11) NOT NULL, pid INT(11) NOT NULL, KEY idx_rid(rid), KEY idx_pid(pid) )ENGINE=INNODB DEFAULT CHARSET= utf8; INSERT INTO permission_role VALUES (1,1); INSERT INTO permission_role VALUES (1,2); INSERT INTO permission_role VALUES (1,3); INSERT INTO permission_role VALUES (1,4); INSERT INTO permission_role VALUES (2,1); INSERT INTO permission_role VALUES (2,4);
用户-角色表:
CREATE TABLE user_role( uid INT(11) NOT NULL, rid INT(11) NOT NULL, KEY idx_uid(uid), KEY idx_rid(rid) )ENGINE=INNODB DEFAULT CHARSET= utf8; INSERT INTO user_role VALUES (1,1); INSERT INTO user_role VALUES (2,2);
创建实体类
权限实体类
public class Permission { private Integer pid; private String name; private String url; public Integer getPid() { return pid; } public void setPid(Integer pid) { this.pid = pid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } @Override public String toString() { return "Permission{" + "pid=" + pid + ", name='" + name + '\'' + ", url='" + url + '\'' + '}'; } }
角色实体类
public class Role { private Integer rid; private String rname; private Set<Permission> permissionSet=new HashSet<>(); private Set<User> userSet=new HashSet<>(); public Integer getRid() { return rid; } public void setRid(Integer rid) { this.rid = rid; } public String getRname() { return rname; } public void setRname(String name) { this.rname = rname; } public Set<Permission> getPermissionSet() { return permissionSet; } public void setPermissionSet(Set<Permission> permissionSet) { this.permissionSet = permissionSet; } public Set<User> getUserSet() { return userSet; } public void setUserSet(Set<User> userSet) { this.userSet = userSet; } @Override public String toString() { return "Role{" + "rid=" + rid + ", rname='" + rname + '\'' + ", permissionSet=" + permissionSet + ", userSet=" + userSet + '}'; } }
用户实体类
public class User { private Integer uid; private String username; private String password; private Set<Role> roleSet=new HashSet<>(); public Integer getUid() { return uid; } public void setUid(Integer uid) { this.uid = uid; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Set<Role> getRoleSet() { return roleSet; } public void setRoleSet(Set<Role> roleSet) { this.roleSet = roleSet; } @Override public String toString() { return "User{" + "uid=" + uid + ", username='" + username + '\'' + ", password='" + password + '\'' + ", roleSet=" + roleSet + '}'; } }
创建操作数据库的接口
@Mapper//标明是mapper类,会被spring自动扫描 public interface UserMapper { //根据用户名查询用户 User findByUsername(@Param("username") String username); }
配置接口和mybatis
在resources目录下创建mapper目录,起名为UserMapper.xml(目录和文件名要和之前的对应)
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.sjh.shiro.mapper.UserMapper"> <select id="findByUsername" parameterType="string" resultType="user"> select * from user where username=#{username}; </select> </mapper>
同时在application.properties中加入mybatis配置
# mybatis mybatis.type-aliases-package=com.sjh.shiro.model mybatis.mapper-locations=classpath:mapper/*.xml
创建业务层接口和实现类
接口:
public interface UserService { User findByUsername(String username); }
实现类:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper;//波浪线报错不要紧,运行时注入 @Override public User findByUsername(String username) { return userMapper.findByUsername(username); } }
测试
找到src/test包下的测试类
@RunWith(SpringRunner.class) @SpringBootTest public class ShiroApplicationTests { @Autowired private UserService userService; @Test public void contextLoads() { User user = userService.findByUsername("kobe"); System.out.println(user); } }
运行测试方法,测试成功
基本案例:根据正确的用户名和密码登陆
自定义认证和授权策略
需要继承AuthorizingRealm
类
//自定义认证授权器 public class AuthRealm extends AuthorizingRealm { @Autowired private UserService userService; //认证登陆 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //AuthenticationToken用于存储前端传来的登录信息,强转为UsernamePasswordToken以获取登陆信息的用户名和密码 UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken)authenticationToken; //获取用户名 String username = usernamePasswordToken.getUsername(); //根据用户名从数据库查询 User user = userService.findByUsername(username); //创建一个认证信息类 //第一个参数 从数据库查询得到的用户对象 //第二个参数 数据库中的密码,会与token中登陆信息的密码比较,匹配后通过 //第三个参数 当前realm的名字 return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName()); } //授权(认证成功后) @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //1 根据realm的名字获取对应realm(获取当前realm) Collection collection = principalCollection.fromRealm(this.getClass().getName()); //2 获取认证后存储的User对象 User user = (User) collection.iterator().next(); //3 获取当前所有角色 Set<Role> roleSet = user.getRoleSet(); //4 遍历角色集合获取权限,并存储到权限集合 List<String> permissionList=new ArrayList<>(); if(!roleSet.isEmpty()){//4.1 遍历角色集合 for(Role role:roleSet){ Set<Permission> permissionSet = role.getPermissionSet();//4.2 获取每个角色中的权限集合 if(!permissionSet.isEmpty()){//4.3 遍历权限集合 for(Permission permission:permissionSet){ permissionList.add(permission.getName());//将权限名存入集合 } } } } //5 创建一个授权信息类,将权限名集合添加到其中 SimpleAuthorizationInfo info=new SimpleAuthorizationInfo(); info.addStringPermissions(permissionList); return info; } }
自定义密码匹配器
需要继承SimpleCredentialsMatcher
public class CredentialMatcher extends SimpleCredentialsMatcher { //重写密码匹配方法 @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //获取token登陆信息中的密码 UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken)token; String password = new String(usernamePasswordToken.getPassword()); //获取数据库中的密码,即传来的AuthenticationInfo认证信息中的数据库密码 String dbPassword= (String) info.getCredentials(); //返回两者是否相同的结果 return this.equals(password,dbPassword); } }
配置shiro
//shiro配置类 @Configuration//注明是配置类 public class ShiroConfig{ @Bean("credentialMatcher")//自定义密码匹配器 public CredentialMatcher getCredentialMatcher(){ return new CredentialMatcher(); } @Bean("authRealm")//自定义认证验证器 public AuthRealm getAuthRealm(@Qualifier("credentialMatcher") CredentialMatcher credentialMatcher){ //自定义认证授权器 AuthRealm authRealm=new AuthRealm(); //将自定义的密码匹配器传入 authRealm.setCredentialsMatcher(credentialMatcher); return authRealm; } @Bean("securityManager")//自定义web安全管理器 public SecurityManager getSecurityManager(@Qualifier("authRealm") AuthRealm authRealm){ //将自定义认证授权器传入默认web安全管理器 DefaultWebSecurityManager manager=new DefaultWebSecurityManager(); manager.setRealm(authRealm); return manager; } @Bean("shiroFilter")//自定义shiro过滤器 public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager){ //将自定义web安全管理器传入shiro过滤器 ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); //设置登陆url shiroFilter.setLoginUrl("/login"); //设置登陆成功url shiroFilter.setSuccessUrl("/index"); //设置登陆失败url shiroFilter.setUnauthorizedUrl("/unAuthorized"); //配置拦截方式,第一个参数是请求路径,第二个参数是使用什么拦截器 LinkedHashMap<String,String> filterChainDefinitionMap=new LinkedHashMap<>(); filterChainDefinitionMap.put("/index","authc");//访问首页需要认证 filterChainDefinitionMap.put("/login","anon");//登陆不需要认证,可匿名 shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilter; } @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(@Qualifier("securityManager")SecurityManager securityManager){ //配置spring使用自定义web安全管理器 AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator proxyCreator=new DefaultAdvisorAutoProxyCreator(); proxyCreator.setProxyTargetClass(true);//为true代表以cglib动态代理方式生成代理类,默认false表示以JDK方式 return proxyCreator; } }
在自定义的shiro过滤器中,配置拦截方式的anno和authc,都是DefaultFilter枚举中的属性,还有很多其他属性,anno表示可以匿名访问不需要认证,authc表示需要认证。
web层控制器
@Controller public class TestController { @RequestMapping("/login") public String login(){ return "login"; } @RequestMapping("/idex") public String index(){ return "index"; } @RequestMapping("/loginUser") public String loginUser(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession httpSession){ try{ //通常我们会将Subject对象理解为一个用户,同样的它也有可能是一个三方程序,它是一个抽象的概念,可以理解为任何与系统交互的“东西”都是Subject。 Subject subject= SecurityUtils.getSubject(); //将前端用户名和密码存入UsernamePasswordToken UsernamePasswordToken token=new UsernamePasswordToken(username,password); //使用该登陆信息尝试登陆 subject.login(token); //获取当前登陆的用户 User user=(User)subject.getPrincipal(); //将登陆用户存到session中 httpSession.setAttribute("user",user); return "index";//登陆成功,跳转主页 }catch (Exception e){ //登陆失败,返回登录页 return "login"; } } }
页面配置与编写
在application.properties中加入以下配置
# jsp配置 前缀/后缀 spring.mvc.view.prefix=/pages/ spring.mvc.view.suffix=.jsp
在src下新建webapp/pages目录(默认在webapp目录下寻找,所以配置时从pages开始)
设置webapp为web模块
login页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Login</title> </head> <body> <h2>欢迎登陆</h2> <form action="/loginUser" method="post"> 用户名:<input type="text" name="username"><br> 密码:<input type="text" name="password"><br> <input type="submit" name="login"><br> </form> </body> </html>
index页面
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %> <html> <head> <title>首页</title> </head> <body> <h2>欢迎登陆,${user.username}</h2> </body> </html>
编写sql,配置
在UserMapper.xml中,修改为以下配置
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.sjh.shiro.mapper.UserMapper"> <resultMap id="userMap" type="com.sjh.shiro.model.User"> <id property="uid" column="uid"/> <result property="username" column="username"/> <result property="password" column="password"/> <collection property="roleSet" ofType="com.sjh.shiro.model.Role"> <id property="rid" column="rid"/> <result property="rname" column="rname"/> <collection property="permissionSet" ofType="com.sjh.shiro.model.Permission"> <id property="pid" column="pid"/> <result property="name" column="name"/> <result property="url" column="url"/> </collection> </collection> </resultMap> <select id="findByUsername" parameterType="string" resultMap="userMap"> SELECT u.*,r.*,p.* FROM USER u INNER JOIN user_role ur ON ur.`uid`=u.`uid` INNER JOIN role r ON r.`rid`=ur.`rid` INNER JOIN permission_role pr ON pr.`rid`=r.`rid` INNER JOIN permission p ON p.`pid`=pr.`pid` WHERE u.`username`= #{username}; </select> </mapper>
测试
运行启动类,访问localhost:8080/index,由于需要权限,自动跳转至login
输入错误的用户名和密码,需要重新登陆,可以发现路径改变为处理登陆的loginUser
控制台输出
输入正确的用户名和密码
成功登陆
案例: 只要登陆即可访问所以页面
在shiro配置shiroFilter类的拦截方式里加入
filterChainDefinitionMap.put("/loginUser","anon");//登陆处理不需要认证 filterChainDefinitionMap.put("/**","user");//其他任何页面只要登陆即可访问
控制器加入方法
@RequestMapping("/admin") @ResponseBody public String admin(){ return "success"; } @RequestMapping("/logout") public String logout(){ //取出验证主体 Subject subject= SecurityUtils.getSubject(); //不为空则注销 if(subject!=null) subject.logout(); return "login"; }
运行启动类,直接访问admin,会失败并跳到登陆页
成功登陆后即可访问
再访问logout测试一下注销,也是成功的
案例: 指定角色可访问页面
先在shiroFiter的拦截方式里加入
filterChainDefinitionMap.put("/admin","roles[admin]");//只允许admin的角色访问admin路径
表示只允许角色是admin的角色访问/admin路径。
因此在登陆时我们还需要把角色信息存入认证信息集中,在AuthRealm类中添加以下代码:
运行启动类,进行测试,之前创建数据库数据时,id为1用户名为kobe的用户角色是admin,id为2用户名为james的用户角色是customer
访问localhost:8080/admin,要求登陆,我们先输入角色不是admin的用户james
会发现没有权限访问,出现404错误
注销后使用角色为admin的用户登陆
此时是成功的
补充一个没有权限的页面,在控制器中增加方法
路径和之前配置拦截方式指定的路径名一样
@RequestMapping("/unAuthorized") public String unAuthorized(){ return "unAuthorized"; }
在pages目录下新建一个unAuthorized.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h2>抱歉,您无权访问</h2> </body> </html>
此时再次用james访问admin,将会显示:
案例: 指定权限可访问页面
先在shiroFiter里配置权限拦截方式
filterChainDefinitionMap.put("/edit","perms[update]");//只允许update权限访问edit路径
在控制器类增加对应方法
@RequestMapping("/edit") @ResponseBody public String edit(){ return "success"; }
由于角色为admin的角色具有所有权限,而角色为customer的角色只有add权限和query权限,所以当我们用kobe访问edit时是成功的,用james则会失败。
配置拦截方式时
- 限制用户使用authc,anon,user等枚举
- 限制角色使用roles[角色名]
- 限制权限使用perms[权限名]
例:
Shiro优点:
- 提供了一套可用且简单的安全框架
- 灵活,应对需求能力强,web能力强
- 可于很多框架和应用进行集成
缺点:
- 学习资料少
- 除了RBAC外,操作的界面也需要自己实现