SpringBoot2.x集成Apache Shiro並完成簡單的Case開發

SpringBoot集成Apache Shiro環境快速搭建

在上文 Apache Shiro權限框架理論介紹 中,咱們介紹了Apache Shiro的基礎理論知識。本文咱們將在 SpringBoot 中集成Apache Shiro,完成一些簡單的Case開發。html

Apache Shiro和Spring Security不一樣,它沒有自帶的登陸頁面和基於內存的權限驗證。因此咱們將使用jsp去編寫簡單的登陸頁面,使用Mybatis鏈接MySQL數據庫進行用戶及其權限和角色信息的存取。java

首先在IDEA中,建立一個Spring Boot工程:
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發mysql

選擇須要的模塊:
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發web

項目建立完成後,補充相應的依賴,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>

注:在本文中,不會贅述SpringBoot集成Mybatis的配置,若對此不熟悉的話,能夠參考我另外一篇文章:SpringBoot2.x整合MyBatisspring

以上也提到了咱們須要在數據庫中進行用戶及其權限和角色信息的存取,而且咱們將按照RBAC模型完成文中Case的開發,因此首先須要建立數據庫表格及向表格插入一些數據。具體的sql語句以下: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接口:apache

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

由於登陸時用戶輸入的密碼須要與數據庫裏的密碼進行對比,因此咱們還能夠自定義一個密碼校驗規則。代碼以下:

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相關的配置,代碼以下:

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,用於提供外部訪問的接口。代碼以下:

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路徑:
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發

配置好後會生成一個webapp目錄,在該目錄下建立pages目錄,並新建jsp文件。其中login.jsp文件內容以下:

<%@ 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文件內容以下:

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

unauthorize.jsp文件內容以下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Unauthorize</title>
</head>
<body>
<h2>無權限訪問!</h2>
</body>
</html>

啓動項目,在沒有登陸的狀況下訪問index接口,會跳轉到登陸頁面上:
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發

用戶成功後,就會跳轉到index頁面上:
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發

若使用user用戶訪問admin接口,則會跳轉到權限拒絕頁面上,這符合咱們定義的規則:
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發

只有admin用戶才能夠訪問全部接口:
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發


若是咱們要實現某個接口須要某個權限才能訪問的話,能夠在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外,操做的界面也須要本身實現
相關文章
相關標籤/搜索