Spring Boot整合Shiro框架进行身份验证

本文详细介绍如何在SpringBoot项目中整合Apache Shiro框架进行身份验证和权限控制,包括项目搭建、实体类创建、数据库配置、Shiro配置及具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring Boot整合Shiro框架进行身份验证

一.什么是Shiro

Apache Shiro 是 Java 的一个安全框架,Shiro 可以帮助我们完成:认证、授权、加密、会话管理等。相比较Spring Security 她更加的小巧易用。其基本功能点如下图所示:
这里写图片描述

Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;

Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web Support:Web 支持,可以非常容易的集成到 Web 环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;

Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

Shiro 不会去维护用户、维护权限;这些需要我们自己去设计和提供;然后通过相应的接口注入给 Shiro。下面我们就来看看在Spring Boot中怎么进行身份的验证。

二.创建Spring Boot项目

创建spring boot项目,需要加入的组件有:web,mysql,thymeleaf,shiro,devtools。
下面是创建完项目后pom文件的依赖:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <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.1.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

三.创建实体类及建表

进行身份的验证或者权限的授权,大体思路是把用户赋予角色,将角色赋予相应的权限。所以我们需要创建用户类,角色类,和权限类。
1.用户类

public class UserInfo implements Serializable {
    private Integer uid;
    private String name;
    private String password; 
    //秘钥
    private String salt;
    // 一个用户具有多个角色
    private List<Role> roleList;
   //省略get、set方法 
}

2.角色类

public class Role {

    private Integer id;
    // 角色标识程序中判断使用,如"admin"
    private String role;
    // 一个角色可以有多个权限
    private List<Permission> permissions;
    // 一个角色对应多个用户
    private List<UserInfo> userInfos;

    //省略get、set方法 
}

3.权限类

public class Permission implements Serializable {
    private Integer id;
    private String name;
    // 资源路径.
    private String url;
    // 权限字符串如 role:create,role:update,role:delete,role:view
    private String permission;
    // 一个权限可以包含多个角色
    private List<Role> roles;

    //省略get、set方法 
}

4.用户表

CREATE TABLE `user_info` (
  `uid` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`uid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `user_info` VALUES ('1', 'ADMIN', '5b48c185e886ff93be5244f979b2864b', 'asdfghjkl');

5.角色表

CREATE TABLE `role` (
  `id` int(11) NOT NULL,
  `role` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `role` VALUES ('1', 'admin');
INSERT INTO `role` VALUES ('2', 'vip');

6.权限表

CREATE TABLE `permission` (
  `id` int(11) NOT NULL,
  `name` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
  `permission` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
  `url` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `permission` VALUES ('1', 'UserManger', 'userInfo:view', 'userInfo/userList');
INSERT INTO `permission` VALUES ('2', 'UserAdd', 'userInfo:add', 'userInfo/userAdd');
INSERT INTO `permission` VALUES ('3', 'UserDelete', 'userInfo:del', 'userInfo/userDel');

7.用户角色关联表

CREATE TABLE `user_role` (
  `role_id` int(11) NOT NULL,
  `uid` int(11) NOT NULL,
  KEY `FKgkmyslkrfeyn9ukmolvek8b8f` (`uid`),
  KEY `FKhh52n8vd4ny9ff4x9fb8v65qx` (`role_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `user_role` VALUES ('1', '1');
INSERT INTO `user_role` VALUES ('1', '2');

8.角色权限关联表

CREATE TABLE `role_permission` (
  `role_id` int(11) NOT NULL,
  `permission_id` int(11) NOT NULL,
  KEY `FKomxrs8a388bknvhjokh440waq` (`permission_id`),
  KEY `FK9q28ewrhntqeipl1t04kh1be7` (`role_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `role_permission` VALUES ('1', '1');
INSERT INTO `role_permission` VALUES ('1', '2');

四.配置application.properties文件

##mapper文件位置
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.example.demo.entity

##配置数据源
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = 000000

spring.thymeleaf.cache = false
spring.thymeleaf.mode = LEGACYHTML5

五.创建mapper类

UserMapper:

package com.example.demo.mapper;

import com.example.demo.entity.UserInfo;

public interface UserMapper {

    public UserInfo getUserByName(String name);

    public List<Role> getRoles(int uid);

    public List<Permission> getPermissions(int roleId);

}

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.example.demo.mapper.UserMapper">


    <select id="getUserByName" parameterType="com.example.demo.entity.UserInfo" resultType="com.example.demo.entity.UserInfo">
        select * from user_info where name = #{name}
    </select>

    <select id="getRoles" parameterType="java.lang.Integer" resultType="com.example.demo.entity.Role">
        select * from role  where id in (select role_id from user_role where uid = #{uid})
    </select>

    <select id="getPermissions" parameterType="java.lang.Integer" resultType="com.example.demo.entity.Permission">
        select * from permission where id in(select permission_id from role_permission where role_id = #{roleId})
    </select>
</mapper>

这个可以参考上一篇Spring Boot整合Mybatis

六.控制器

package com.example.demo.controller;

import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class UserController {

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

    @RequestMapping("/login")
    public String login(HttpServletRequest request, Map<String, Object> map) throws Exception {
        System.out.println("HomeController.login()");
        // 登录失败从request中获取shiro处理的异常信息。
        // shiroLoginFailure:就是shiro异常类的全类名.
        String exception = (String) request.getAttribute("shiroLoginFailure");
        System.out.println("exception=" + exception);
        String msg = "";
        if (exception != null) {
            if (UnknownAccountException.class.getName().equals(exception)) {
                System.out.println("UnknownAccountException -- > 账号不存在:");
                msg = "UnknownAccountException -- > 账号不存在:";
            } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
                System.out.println("IncorrectCredentialsException -- > 密码不正确:");
                msg = "IncorrectCredentialsException -- > 密码不正确:";
            } else if ("kaptchaValidateFailed".equals(exception)) {
                System.out.println("kaptchaValidateFailed -- > 验证码错误");
                msg = "kaptchaValidateFailed -- > 验证码错误";
            } else {
                msg = "else >> " + exception;
                System.out.println("else -- >" + exception);
            }
        }
        map.put("msg", msg);
        // 此方法不处理登录成功,由shiro进行处理
        return "/login";
    }

    @RequestMapping("/test")
    public String test() {
        System.out.println("------test-------");
        return "test";
    }

}

在src/main/resources的templates目录下创建一个index.html和login.html文件
login.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
错误信息:<h4 th:text="${msg}"></h4>
<form action="" method="post">
    <p>账号:<input type="text" name="username" value="admin"/></p>
    <p>密码:<input type="text" name="password" value="123456"/></p>
    <p><input type="submit" value="登录"/></p>
</form>
</body>
</html>

七.引入Shiro

到目前为止并没有Shiro的参与,启动项目我们现在可以随便的访问index.html页面,现在我们要求访问index的时候必须要先登录,如果没有登录则跳转到login.html页面。下面就该Shiro登场了。

这里写图片描述

Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;

Realm:域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个 Shiro 应用:
应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

理论看完了看看具体项目中应该怎么做吧:
1.Shiro 配置
Apache Shiro 核心通过 Filter 来实现,就好像SpringMVC 通过DispachServlet 来主控制一样。 所以我们需要定义一系列关于URL的规则和访问权限。

package com.example.demo.config;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 拦截器.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/static/**", "anon");
        // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");
        // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        filterChainDefinitionMap.put("/**", "authc");
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面,设置的话没经过验证会发送test请求到控制器,由控制器决定转到对应视图
        //shiroFilterFactoryBean.setLoginUrl("/test");
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");

        // 未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 )
     * 
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(1);// 散列的次数,比如散列两次,相当于md5(md5(""));

        return hashedCredentialsMatcher;
    }

    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }


}

这些配置套路都是一样的,主要的校验逻辑是在realm域里面,所以我们主要的工作就是写这个域。
看下Shiro 默认提供的 Realm
这里写图片描述

以后一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(身份验证),而且也间接继承了 CachingRealm(带有缓存实现)。继承AuthorizingRealm类重写doGetAuthenticationInfo(验证)和doGetAuthorizationInfo(授权)两个方法。

package com.example.demo.config;

import javax.annotation.Resource;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

import com.example.demo.entity.UserInfo;
import com.example.demo.service.UserService;


public class MyShiroRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    /**
     * 认证信息.(身份验证) : Authentication 是用来验证用户身份
     * 
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");

        // 获取用户的输入的账号.
        String name = (String) token.getPrincipal();
        System.out.println(token.getCredentials());

        // 通过name和password从数据库中查找 User对象,
        UserInfo userInfo = userService.getUserByName(name);
        if (userInfo == null) {
            return null;
        }

        int uid = userInfo.getUid();
        List<Role> roles = userService.getRoles(uid);
        int roleId = roles.get(0).getId();
        List<Permission> permissions = userService.getPermissions(roleId);
        roles.get(0).setPermissions(permissions);
        userInfo.setRoleList(roles);
        // 加密方式;
        // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
        String password = userInfo.getPassword();
        // 秘钥
        ByteSource salt = ByteSource.Util.bytes(userInfo.getSalt());
        // 当前域的名称(MyShiroRealm)
        String realmName = getName();
        // 认证信息
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, password, salt, realmName);

        return authenticationInfo;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // TODO Auto-generated method stub
        return null;
    }

}

到此工作已经全部结束了,启动项目看看效果吧。浏览器输入http://localhost:8080/index,试图访问index.html页面,但是没有登录自动跳转到了登录页面。
这里写图片描述

输入正确的账号密码后就可以访问到index.html页面了

附:用户表中的密码是进行加过密,Shiro提供一整套的加密方法,这个后面再说。现在提供一个密码加密的工具类供测试使用:

package com.example.demo.util;

import org.apache.shiro.crypto.hash.Md5Hash;

/**
 * 加解密工具类
 *
 */
public class CryptographyUtil {


    public static String md5(String str, String salt) {
        return new Md5Hash(str, salt).toString();
    }

    public static void main(String[] args) {
        String password = "123456";
        System.out.println(CryptographyUtil.md5(password, "asdfghjkl"));
    }

}

项目的整体结构:
这里写图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值