SpringBoot2.x集成Apache Shiro并完成简单的Case开发

2020-09-23 14:24:12 浏览数 (1)

SpringBoot集成Apache Shiro环境快速搭建

在上文 Apache Shiro权限框架理论介绍 中,我们介绍了Apache Shiro的基础理论知识。本文我们将在 SpringBoot 中集成Apache Shiro,完成一些简单的Case开发。

Apache Shiro和Spring Security不同,它没有自带的登录页面和基于内存的权限验证。所以我们将使用jsp去编写简单的登录页面,使用Mybatis连接MySQL数据库进行用户及其权限和角色信息的存取。

首先在IDEA中,创建一个Spring Boot工程:

选择需要的模块:

项目创建完成后,补充相应的依赖,pom.xml文件中配置的完整依赖项如下:

代码语言:javascript复制
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </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>
    </dependency>

    <!-- apache 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>

    <!-- alibaba的druid数据库连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.9</version>
    </dependency>

    <!-- apache 工具包 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.4</version>
    </dependency>

    <!-- spring 工具包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>5.0.7.RELEASE</version>
    </dependency>

    <!-- jsp 依赖 -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>
</dependencies>

注:在本文中,不会赘述SpringBoot集成Mybatis的配置,若对此不熟悉的话,可以参考我另一篇文章:SpringBoot2.x整合MyBatis

以上也提到了我们需要在数据库中进行用户及其权限和角色信息的存取,并且我们将按照RBAC模型完成文中Case的开发,所以首先需要创建数据库表格及向表格插入一些数据。具体的sql语句如下:

代码语言:javascript复制
-- 权限表 --
create table permission (
  pid int (11) not null auto_increment,
  name varchar (255) not null default '',
  url varchar (255) default '',
  primary key (pid)
) engine = InnoDB default charset = utf8;

insert into permission values ('1','add','');
insert into permission values ('2','delete','');
insert into permission values ('3','edit','');
insert into permission values ('4','query','');

-- 用户表 --
create table user (
  uid int (11) not null auto_increment,
  username varchar (255) not null default '',
  password varchar (255) default '',
  primary key (uid)
) engine = InnoDB default charset = utf8;

insert into user values ('1','admin','123');
insert into user values ('2','user','123');

-- 角色表 --
create table role (
  rid int (11) not null auto_increment,
  rname varchar (255) not null default '',
  primary key (rid)
) 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);

创建与表格所对应的pojo类。如下:

代码语言:javascript复制
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Permission {
    private Integer pid;
    private String name;
    private String url;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private Integer rid;
    private String rname;
    private Set<Permission> permissions = new HashSet<>();
    private Set<User> users = new HashSet<>();
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer uid;
    private String username;
    private String password;
    private Set<Role> roles = new HashSet<>();
}

然后创建dao层的mapper接口:

代码语言:javascript复制
public interface UserMapper {

    /**
     * 根据用户名查找用户
     *
     * @param username 用户名
     * @return user
     */
    User findByUserName(@Param("username") String username);
}

以及编写与之对应的xml文件:

代码语言:javascript复制
<?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="org.zero.example.shiro.mapper.UserMapper">
    <resultMap id="userMap" type="org.zero.example.shiro.model.User">
        <id property="uid" column="uid"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <collection property="roles" ofType="org.zero.example.shiro.model.Role">
            <id property="rid" column="rid"/>
            <result property="rname" column="rname"/>
            <collection property="permissions" ofType="org.zero.example.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 pr.pid = p.pid
      where u.username = #{username}
    </select>
</mapper>

接着是service层接口:

代码语言:javascript复制
public interface UserService {

    /**
     * 根据用户名查找用户
     *
     * @param username 用户名
     * @return user
     */
    User findByUserName(String username);
}

编写实现类来实现UserService接口:

代码语言:javascript复制
@Service("userService")
public class UserServiceImpl implements UserService {

    private final UserMapper userMapper;

    @Autowired
    public UserServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public User findByUserName(String username) {
        return userMapper.findByUserName(username);
    }
}

到此为止,我们就完成了项目基本结构的搭建,接下来我们就可以开始Case的开发了。


自定义权限管理

我们来基于Apache Shiro实现一个自定义的认证、授权及密码匹配规则。首先是创建我们自定义的Realm,在Realm实现授权及认证登录,代码如下:

代码语言:javascript复制
package org.zero.example.shiro.realm;

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.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.zero.example.shiro.model.Permission;
import org.zero.example.shiro.model.Role;
import org.zero.example.shiro.model.User;
import org.zero.example.shiro.service.UserService;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * @program: shiro
 * @description: 自定义Realm
 * @author: 01
 * @create: 2018-09-08 16:13
 **/
public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 从session中拿出用户对象
        User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();
        List<String> permissionList = new ArrayList<>();
        Set<String> roleNameSet = new HashSet<>();

        // 获取用户的角色集
        Set<Role> roleSet = user.getRoles();
        if (!CollectionUtils.isEmpty(roleSet)) {
            for (Role role : roleSet) {
                // 添加角色名称
                roleNameSet.add(role.getRname());

                // 获取角色的权限集
                Set<Permission> permissionSet = role.getPermissions();
                if (!CollectionUtils.isEmpty(permissionSet)) {
                    for (Permission permission : permissionSet) {
                        // 添加权限名称
                        permissionList.add(permission.getName());
                    }
                }
            }
        }

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermissions(permissionList);
        info.setRoles(roleNameSet);

        return info;
    }

    // 认证登录
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        // 获取登录的用户名
        String userName = usernamePasswordToken.getUsername();
        // 从数据库中查询用户
        User user = userService.findByUserName(userName);

        return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
    }
}

因为登录时用户输入的密码需要与数据库里的密码进行对比,所以我们还可以自定义一个密码校验规则。代码如下:

代码语言:javascript复制
package org.zero.example.shiro.matcher;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;

/**
 * @program: shiro
 * @description: 自定义密码校验规则
 * @author: 01
 * @create: 2018-09-08 16:30
 **/
public class CredentialMatcher extends SimpleCredentialsMatcher {

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String password = new String(usernamePasswordToken.getPassword());
        String dbPassword = (String) info.getCredentials();

        return this.equals(password, dbPassword);
    }
}

最后是新建一个配置类来注入shiro相关的配置,代码如下:

代码语言:javascript复制
package org.zero.example.shiro.config;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zero.example.shiro.matcher.CredentialMatcher;
import org.zero.example.shiro.realm.AuthRealm;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @program: shiro
 * @description: shiro配置类
 * @author: 01
 * @create: 2018-09-08 16:34
 **/
@Configuration
public class ShiroConfiguration {

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        // 登录的url
        bean.setLoginUrl("/login");
        // 登录成功后跳转的url
        bean.setSuccessUrl("/index");
        // 权限拒绝时跳转的url
        bean.setUnauthorizedUrl("/unauthorize");

        // 定义请求拦截规则,key是正则表达式用于匹配访问的路径,value则用于指定使用什么拦截器进行拦截
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 拦截index接口,authc表示需要认证才能访问
        filterChainDefinitionMap.put("/index", "authc");
        // anon表示不拦截
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginUser", "anon");
        // 指定admin接口只允许admin角色的用户访问
        filterChainDefinitionMap.put("/admin", "roles[admin]");
        // 用户在登录后可以访问所有的接口
        filterChainDefinitionMap.put("/**", "user");        
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return bean;
    }

    @Bean("securityManager")
    public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
        // 设置自定义的SecurityManager
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(authRealm);

        return manager;
    }

    @Bean("authRealm")
    public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
        // 设置自定义的Realm
        AuthRealm authRealm = new AuthRealm();
        authRealm.setCredentialsMatcher(matcher);

        return authRealm;
    }

    @Bean("credentialMatcher")
    public CredentialMatcher credentialMatcher() {
        // 设置自定义密码校验规则
        return new CredentialMatcher();
    }

    // =========== spring 与 shiro 关联的相关配置 ============

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
        // 设置spring在对shiro进行处理的时候,使用的SecurityManager为我们自定义的SecurityManager
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);

        return advisor;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        // 设置代理类
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);

        return creator;
    }
}

相关接口及登录页面的开发

新建一个 DemoController,用于提供外部访问的接口。代码如下:

代码语言:javascript复制
package org.zero.example.shiro.controller;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.zero.example.shiro.model.User;

import javax.servlet.http.HttpSession;

/**
 * @program: shiro
 * @description: shiro demo
 * @author: 01
 * @create: 2018-09-08 18:01
 **/
@Slf4j
@Controller
public class DemoController {

    @RequestMapping("/login")
    public String login() {
        return "login";
    }

    @RequestMapping("/index")
    public String index() {
        return "index";
    }

    @RequestMapping("/logout")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();
        if (subject != null) {
            subject.logout();
        }

        return "login";
    }

    @RequestMapping("/admin")
    @ResponseBody
    public String admin() {
        return "success admin";
    }

    @RequestMapping("/unauthorize")
    public String unauthorize() {
        return "unauthorize";
    }

    @RequestMapping("/loginUser")
    public String loginUser(@RequestParam("username") String username,
                            @RequestParam("password") String password,
                            HttpSession session) {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();

        try {
            subject.login(token);
            User user = (User) subject.getPrincipal();
            session.setAttribute("user", user);

            return "index";
        } catch (Exception e) {
            log.error("验证不通过: {}", e.getMessage());
            return "login";
        }
    }
}

在配置文件中,配置jsp文件所在的路径:

代码语言:javascript复制
spring:
    mvc:
      view:
        prefix: /pages/
        suffix: .jsp

由于需要跳转jsp,所以还需配置项目的web resource路径:

配置好后会生成一个webapp目录,在该目录下创建pages目录,并新建jsp文件。其中login.jsp文件内容如下:

代码语言:javascript复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Login</title>
</head>
<body>

<h1>欢迎登录</h1>
<form action="/loginUser" method="post">
    <input type="text" name="username"/><br/>
    <input type="text" name="password"/><br/>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

index.jsp文件内容如下:

代码语言:javascript复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Home</title>
</head>
<body>
<h1>欢迎登录, ${user.username}</h1>
</body>
</html>

unauthorize.jsp文件内容如下:

代码语言:javascript复制
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Unauthorize</title>
</head>
<body>
<h2>无权限访问!</h2>
</body>
</html>

启动项目,在没有登录的情况下访问index接口,会跳转到登录页面上:

用户成功后,就会跳转到index页面上:

若使用user用户访问admin接口,则会跳转到权限拒绝页面上,这符合我们定义的规则:

只有admin用户才可以访问所有接口:


如果我们要实现某个接口需要某个权限才能访问的话,可以在ShiroConfiguration类的shiroFilter方法中,关于定义请求拦截规则那一块去配置。例如我希望edit只能由拥有edit权限的用户才能访问,则添加如下代码即可:

代码语言:javascript复制
// 设置用户需要拥有edit权限才可以访问edit接口
filterChainDefinitionMap.put("/edit", "perms[edit]");

如果需要开启权限缓存的话,可以在配置 AuthRealm 的时候进行定义。例如我这里使用Shiro自带的权限缓存,如下:

代码语言:javascript复制
@Bean("authRealm")
public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
    // 设置自定义的Realm
    AuthRealm authRealm = new AuthRealm();
    authRealm.setCredentialsMatcher(matcher);
    // 设置缓存
    authRealm.setCacheManager(new MemoryConstrainedCacheManager());

    return authRealm;
}

总结

优点:

  • 提供了一套框架,而且这个框架可用,且易于使用
  • 灵活,应对需求能力强,Web能力强
  • 可以与很多框架和应用进行集成

缺点:

  • 学习资料比较少
  • 除了需要自己实现RBAC外,操作的界面也需要自己实现

0 人点赞