Springboot整合shiro

1.概述

Apache Shiro 是一款 Java 安全框架,不依赖任何容器,可以运行在 Java SE 和 Java EE 项目中,它的主要作用是用来做身份认证、授权、会话管理、缓存和加密等操作。

和SpringSecurity的作用大概是一致的,但SpringSecurity由于其功能丰富而复杂则多用在大型项目中,而Shiro则适用于中小型项目中,上手快。

2.shiro基本原理与演示

2.1核心组件

1)Subject(用户):当前的操作用户,通过Subject currentUser = SecurityUtils.getSubject()获取。

2)SecurityManager(安全管理器):Shiro 的核心部分,负责安全认证与授权。

3)Realms(数据源):充当与安全管理间的桥梁,查找数据源进行验证和授权操作。

4)Authenticator(认证器):用于认证,从 Realm 数据源取得数据之后进行执行认证流程处理。AuthenticationInfo存储用户的角色信息集合,核心方法是doGetAuthenticationInfo

5)Authorizer(授权器):用户访问控制授权,决定用户是否拥有执行指定操作的权限。AuthorizationInfo存储角色的权限信息集合,核心方法是doGetAuthorizationInfo

6)SessionManager(会话管理器):支持会话管理。

7)CacheManager(缓存管理器):用于缓存认证授权信息。

8)Cryptography(加密组件):提供了加密解密的工具包,用于密码的加密。

2.2Shiro认证 - 基于ini认证

这里先创建SpringBoot项目,也可以直接使用普通的maven项目,由于后续需要进行整合SpringBoot,故这里直接以SpringBoot为基础。[版本说明:SpringBoot 2.7.2,shiro 1.5.2]

1)先导入shiro的核心依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.2</version>
</dependency>

2)在资源目录下创建配置文件shiro-au.ini

#用户的身份、凭据
[users]
zhangsan=555
xiaoluo=666

其中#用来注释,而中括号用来标记类型,比如这里是标记用户信息,用户名和密码使用等号的方式连接。

3)新建测试方法, 使用用户名和密码验证登录

    public void testLogin() {
        //创建Shiro的安全管理器,是shiro的核心
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        //加载shiro.ini配置,得到配置中的用户信息(账号+密码)
        IniRealm iniRealm = new IniRealm("classpath:shiro-au.ini");
        securityManager.setRealm(iniRealm);
        //把安全管理器注入到当前的环境中
        SecurityUtils.setSecurityManager(securityManager);
        //获取subject主体对象,无论有无登录都可以获取到,但是需要属性来判断登录状态
        Subject subject = SecurityUtils.getSubject();
        System.out.println("未登录时认证状态:" + subject.isAuthenticated());
        //创建令牌(携带登录用户的账号和密码)
        UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo", "666");
        //执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
        subject.login(token);
        System.out.println("登录成功认证状态:" + subject.isAuthenticated());
        //登出
        subject.logout();
        System.out.println("退出后认证状态:"+subject.isAuthenticated());
    }

打印结果如下

上述用户名和密码都是正确的,实际上当用户名或密码有一个不正确时就不会打印所有的信息,而是会在执行登录操作时抛出异常

  • 当用户名错误,抛异常UnknownAccountException
  • 当密码错误,抛异常IncorrectCredentialsException

下面看源码,先进入login的方法:

这里调用了安全管理器的login方法,进入

这里调用了认证的方法,进入

继续深入

这里就选择抽象的类进入

进入后,发现其出现了Realm,开始进行数据源的验证

最终找到了其调用的认证的方法

2.3Shiro认证 - 基于Realm认证

在实际的应用场景中,当然不会把用户名和密码放在配置文件中,而是需要结合数据库进行验证。上述只是说明验证登录的流程,需要自定义数据源进行用户名和密码的验证

1)创建用户对象

package com.zxh.test.entity;

import lombok.Data;

@Data
public class User {
    private String username;
    private String password;
    private String addr;
}

2)自定义Realm

package com.zxh.test.controller;

import com.zxh.test.entity.User;
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;

public class MyRealm extends AuthorizingRealm {
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.获取登录用户名
        String username = (String) token.getPrincipal();
        //2.以用户名为条件查询数据库,得到用户信息
        //这里模拟数据
        User user = new User();
        user.setUsername("xiaoluo");
        user.setPassword("123");

        //封装成一个认证info对象
        //判断用户是否为空
        if (user != null) {
            return new SimpleAuthenticationInfo(
                    user,
                    user.getPassword(),
                    super.getName()
            );
        }
        return null;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
}

这里为了演示的简单及说明原理,并没有真正的去连接数据库进行数据的验证

3)验证登录

    @Test
    public void testLogin2() {
        //创建Shiro的安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        //使用自定义的数据源
        MyRealm myRealm = new MyRealm();
        securityManager.setRealm(myRealm);
        //把安全管理器注入到当前的环境中
        SecurityUtils.setSecurityManager(securityManager);
        //获取到subject主体对象
        Subject subject = SecurityUtils.getSubject();
        System.out.println("认证状态:" + subject.isAuthenticated());
        //创建令牌
        UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo", "666");
        //执行登录操作
        subject.login(token);
        System.out.println("认证状态:" + subject.isAuthenticated());
    }

打印的结果是上一小节是一样的。

2.4Shiro授权 - 基于ini授权

在shiro-au.ini文件中密码后面添加角色即可,以逗号分隔

[users]
zhangsan=555,role1,role2,role3
xiaoluo=666,role2

测试方法

    @Test
    public void testRole() {
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        IniRealm iniRealm = new IniRealm("classpath:shiro-au.ini");
        securityManager.setRealm(iniRealm);
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo", "666");
        subject.login(token);
        System.out.println(subject.hasRole("role1"));
        System.out.println(subject.hasRole("role2"));
    }

用户xiaoluo只有role2角色,故打印结果是false,true 。

除了判断角色外,还可以判断权限,在ini文件添加角色的权限表达式

[roles]
# 权限表达式   资源:操作
role1=*:*
role2=user:add,user:select

分别用两个账号进行测试

    @Test
    public void testPermitted() {
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        IniRealm iniRealm = new IniRealm("classpath:shiro-au.ini");
        securityManager.setRealm(iniRealm);
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo", "666");
        subject.login(token);
        System.out.println(subject.isPermitted("user:edit"));//false
        System.out.println(subject.isPermitted("user:add"));//true
        System.out.println(subject.isPermitted("user:select"));//true
    }

    @Test
    public void testPermitted2() {
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        IniRealm iniRealm = new IniRealm("classpath:shiro-au.ini");
        securityManager.setRealm(iniRealm);
        SecurityUtils.setSecurityManager(securityManager);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "555");
        subject.login(token);
        System.out.println(subject.isPermitted("user:edit"));//true
        System.out.println(subject.isPermitted("user:add"));//true
        System.out.println(subject.isPermitted("user:select"));//true
    }

可以看出,xiaoluo只有部分权限,而zhangsan的权限是*,也就意味着拥有所有的权限。

2.5 Shiro加密

上面的例子中密码都是明文的,而shiro也提供了加密的方法,这里以MD5加密为例。

    @Test
    public void MD5Test() {
        String password = "123";
        //使用其封装好的加密类
        Md5Hash md5Hash = new Md5Hash(password);
        System.out.println(md5Hash);
        //加盐加密
        Md5Hash md5Hash2 = new Md5Hash(password, "000000");
        System.out.println(md5Hash2);
        //加盐加密并多次加密
        Md5Hash md5Hash3 = new Md5Hash(password, "000000", 6);
        System.out.println(md5Hash3);
        //Md5Hash的父类是SimpleHash,也可以使用父类加密 其加密的密文和上面是一样的
        SimpleHash simpleHash = new SimpleHash("MD5", password);
        SimpleHash simpleHash2 = new SimpleHash("MD5", password, "000000");
        SimpleHash simpleHash3 = new SimpleHash("MD5", password, "000000", 6);
    }

shiro默认的登录认证是不加密的,若需要进行加密验证,则需要自定义认证。

3.整合Spingboot-前后端一体

 结合thymeleaf进行说明。

3.1准备工作

1)创建SpringBoot项目,引入依赖

       <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>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
         </dependency>    

2)创建实体类User

package com.zxh.test.entity;

import lombok.Data;

@Data
public class User {
    private String username;
    private String password;
    private String salt;
}

3)配置数据源等信息

spring.datasource..url=jdbc:mysql://localhost:3306/db2023_test?useUnicode=true&characterEncoding=UTF-8
spring.datasource..username=root
spring.datasource..password=123456

mybatis.mapper-locations=classpath:mapper/*Mapper.xml

4)创建数据库和用户表

CREATE TABLE `t_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `password` varchar(200) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `salt` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;

INSERT INTO `db2023_test`.`t_user`(`id`, `username`, `password`, `salt`) VALUES (2, 'zs', '6cf1387af615e766d100860f3546813f', '000000');

这里的密码已通过上述的shiro的6次迭代加盐加密。

3.2引入shiro

1)导入依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.2</version>
        </dependency>

2)创建UserService服务

package com.zxh.test.service;

import com.zxh.test.dao.UserDao;
import com.zxh.test.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class UserService {

    @Autowired
    private UserDao userDao;

    public User selectByName(String name){
        return userDao.selectByName(name);
    }
}

其调用dao的代码在此省略,只展示xml

    <select id="selectByName" resultType="com.zxh.test.entity.User">
        select * from t_user where username = #{name}
    </select>

3)自定义realm

package com.zxh.test.config;

import com.zxh.test.entity.User;
import com.zxh.test.service.UserService;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * shiro自定义配置realm
 */
@Component
public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        User user = userService.selectByName(username);
        if (user != null) {
            ByteSource salt = ByteSource.Util.bytes(user.getSalt());
            return new SimpleAuthenticationInfo(username, user.getPassword(), salt, super.getName());
        }
        return null;
    }
}

这里的密码通过shiro的md5加盐加密

4)配置shiro

package com.zxh.test.config;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * shiro类
 */
@Configuration
public class ShiroConfig {

    @Autowired
    private MyRealm myRealm;

    //创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //创建加密对象,指定加密策略
        HashedCredentialsMatcher matcher=new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("md5");
        matcher.setHashIterations(6);
        myRealm.setCredentialsMatcher(matcher);
        securityManager.setRealm(myRealm);
        return securityManager;
    }

    //配置Shiro过滤器
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //给ShiroFilter配置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //配置系统公共资源
        map.put("/userLogin", "anon");//anon表示这个资源无需认证//配置系统受限资源
        map.put("/**", "authc");//authc表示这个资源需要认证和授权
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

}

6)创建controller

@Controller
public class UserController {

    @PostMapping("/userLogin")
    @ResponseBody
    public String userLogin(String username, String password) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            return "登录成功";
        } catch (Exception e) {
            if (e instanceof UnknownAccountException || e instanceof IncorrectCredentialsException) {
                return "用户名或密码错误";
            }
            return "登录失败";
        }
    }
}

7)启动项目后,使用postman以表单方式提交

 当用户名和密码都正确时,会显示登录成功,反正则显示用户名或密码错误。

3.3引入前端页面

1)在templates目录下新建两个页面

涉及到权限,必须有页面进行支撑。

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
  <form action="/userLogin" method="post">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="password" name="password"></p>
      <p><button type="submit" >登录</button></p>
  </form>
</body>
</html>

main.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>主页面</title>
</head>
<body>
<p>登录成功了,我 是主页,登录用户:<span th:text="${session.user}"></span></p>
</body>
</html>

2)在controller对登录的接口进行修改,并添加两个视图解析

    @PostMapping("/userLogin")
    //@ResponseBody 需要去掉此注解
    public String userLogin(String username, String password, HttpSession session) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            //设置session信息,供前端使用
            session.setAttribute("user", token.getPrincipal());
//            return "登录成功";
            return "main";
        } catch (Exception e) {
            if (e instanceof UnknownAccountException || e instanceof IncorrectCredentialsException) {
                return "用户名或密码错误";
            }
            return "登录失败";
        }
    }

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

    @GetMapping("/main")
    public String main() {
        return "main";
    }

此类并没有使用@RestController注解,原因就是这里包含了视图解析,不能使用此注解。

3)修改shiro的配置类的过滤器

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //给ShiroFilter配置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //配置系统公共资源
        map.put("/userLogin", "anon");//anon表示这个资源无需认证
        map.put("/login", "anon");
        //配置系统受限资源
        map.put("/**", "authc");//authc表示这个资源需要认证和授权

        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

启动后直接访问/main会直接跳转到登录页面

输入正确的用户名和密码即可进入到主页

3.4 多realm认证

上述只是根据用户名和密码校验登录信息,而在实际的场景中,可能会存在手机号等其他多种方式的登录,故可配置多种数据源方式的登录。

shiro会调用内部组件AuthenticationStrategy进行判断,其在身份认证尝试中被调用4次,同时也聚合了所有的Realm的结果信息封装到AuthenticationInfo并返回,依次作为Subject的身份信息。

shiro的3中认证策略如下表:

策略 说明
AtlLeastOneSuccessfulStrateay 默认的策略。只要有一个(或多个)验证成功,则认证视为成功
FirstSuccessfulStrategy 第一个Realm 验证成功,整体认证将视为成功,且后续 Realm 将被忽略
AllSuccessfulStrategy 所有 Realm 成功,认证才视为成功

那么如何更改默认的认证策略呢?只需在安全管理器中设置即可。由于其默认的策略已符合大部分需求,故在此简单说明配置,不具体展开:

DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//创建认证对象 并设置认证策略
ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
securityManager.setAuthenticator(modularRealmAuthenticator);

与此同时,设置realm的方法要由单个变成多个,

securityManager.setRealms(Arrays.asList(myRealm, myRealm2));

 UnauthorizedException:没有权限时的异常

 

posted @ 2023-02-22 16:41  钟小嘿  阅读(2806)  评论(0编辑  收藏  举报