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

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

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

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

-- 权限表 --
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 类。如下:

@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 接口:

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

以及编写与之对应的 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="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 层接口:

public interface UserService {

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

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

@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 实现授权及认证登录,代码如下:

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
 **/
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());
    }
}

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

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: 自定义密码校验规则
 **/
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 相关的配置,代码如下:

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配置类
 **/
@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,用于提供外部访问的接口。代码如下:

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 文件所在的路径:

spring:
    mvc:
      view:
        prefix: /pages/
        suffix: .jsp

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

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

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Login</title>
</head>
<body>
 
欢迎登录
<form action="/loginUser" method="post">
    <input type="text" /><br/>
    <input type="text" /><br/>
    <input type="submit" value="登录"/>
</form>
</body>
</html>

index.jsp 文件内容如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Home</title>
</head>
<body>
欢迎登录, ${user.username}
</body>
</html>

unauthorize.jsp 文件内容如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Unauthorize</title>
</head>
<body>
<h2 id="h0">无权限访问!</h2>
</body>
</html>

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


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

// 设置用户需要拥有edit权限才可以访问edit接口
filterChainDefinitionMap.put("/edit", "perms[edit]");

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

@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 外,操作的界面也需要自己实现