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外,操作的界面也需要自己实现
#Java##Spring##笔记#
全部评论
老哥现在是研二吗?在开始投简历了吗?
点赞 回复
分享
发布于 2020-02-21 09:56

相关推荐

2 8 评论
分享
牛客网
牛客企业服务