Shiro实现登录认证以及整合到Springboot3

一、概念

介绍

Apache Shiro是一个强大灵活的开源安全框架,提供授权、会话管理以及密码加密等功能。

与Spring Security对比

劣势

  1. Spring Security基于Spring开发,项目若使用Spring作为基础,配合Spring Security做权限更加方便,而Shiro需要和Spring进行整合开发

  2. SpringSecurity功能比Shiro更加丰富些,例如安全维护方面

  3. Spring Security社区资源相对比Shiro更加丰富

优势

  1. Shiro的配置和使用比较简单,Spring Security上手复杂

  2. Shiro依赖性低,不需要任何框架和容器,可以独立运行。Spring Security需要依赖Spring容器

  3. Shiro不仅仅是可以使用在web中,它可以工作在任何应用环境中。在集群会话时Shiro最重要的一个好处或许就是它的会话可以独立于容器。

基本功能

Shiro实现登录认证以及整合到Springboot3_第1张图片

基础功能

  • 认证(Authentication): 或“登录”,用以验证用户身份。

  • 授权(Authorization): 访问控制, 比如决定谁可以访问某些资源。

  • 会话管理(Session Management): 管理用户相关的session,即使是在非web或EJB应用中。

  • 加密(Cryptography):可以非常方便地使用(各种)加密算法保证数据的安全。

其他功能

  • 对Web的支持(Web Support): Shiro自带的支持Web的API可以很容易地保证web应用的安全。

  • 缓存(Caching):缓存在Apache Shiro的API中是“一等公民”,可以保证操作的快速高效。

  • 并发(Concurrency): Apache Shiro的并发功能支持开发多线程的应用。

  • 测试(Testing):对测试的支持可以帮助你编写单元测试与集成测试。

  • “以...(身份)运行”(Run As):允许一个用户使用另外某个用户的身份(执行操作),这个功能常用于管理场景中(比如“以管理员身份运行”)。

  • “自动登陆”(Remember Me):可以跨会话记住用户身份,只在某些特殊情况下才需要强制登录。

二、基本使用

在maven中使用


    org.apache.shiro
    shiro-core
    1.9.1

验证登录

@Test
void testShiro(){
    //1.初始化SecurityManager
    IniSecurityManagerFactory factory = new   IniSecurityManagerFactory("classpath:shiro.ini");
    //1.1获取认证管理器
    SecurityManager securityManager = factory.getInstance();
    //1.2将认证管理器添加到SecurityUtils中
    SecurityUtils.setSecurityManager(securityManager);
    //2.获取Subject对象
    Subject subject = SecurityUtils.getSubject();
    //3.创建token对象,web应用用户名和密码
    UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","z3");
    //4.完成登录
    try {
        subject.login(token);
        System.out.println("登录成功");
    }catch (UnknownAccountException e){
        e.printStackTrace();
        System.out.println("用户不存在");
    }catch (IncorrectCredentialsException e){
        e.printStackTrace();
        System.out.println("密码错误");
    }catch (AuthenticationException e) {
        e.printStackTrace();
    }
}

判断用户角色

//5.判断角色
boolean role = subject.hasRole("role1");
System.out.println("是否含有'role1'角色 = " + role);

判断角色权限

//6.判断角色权限
//6.1有返回值
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否拥有'user:insert'权限 = " + permitted);
//6.2 无返回值,没有权限时会抛异常
subject.checkPermission("user:insert");

信息加密

一般数据库中都不会将密码明文存储,而是存储加密后的密码。在认证认证过程中,会将用户输入的密码进行相同的加密过程,如果用户输入的密码加密后和数据库中的一致,则认证成功,否则认证失败。

@Test
void testShiroMD5() {
    //1.密码明文
    String password = "z3";
    //2.使用MD5加密
    Md5Hash passwordMD5 = new Md5Hash(password);
    System.out.println("加密后的密码 =" + passwordMD5.toHex());
    //3.带盐的MD5加密
    Md5Hash passMD5Salt = new Md5Hash(password,"salt");
    System.out.println("带盐的MD5加密 = " + passMD5Salt.toHex());
    //4.为了保证数据安全,可以对密码进行多次加密
    Md5Hash passMD5SaltPlus = new Md5Hash(password,"salt",3);
    System.out.println("迭代三次带盐的MD5加密 = " + passMD5SaltPlus.toHex());
    //5.使用Md5Hash的父类SimpleHash进行加密
    SimpleHash simpleHash = new SimpleHash("MD5", password, "salt", 3);
    System.out.println("使用SimpleHash进行加密 = " + simpleHash);
}

自定义登录认证

Shiro默认的登录认证是不带加密的,要实现加密认证需要自定义Realm来实现登录认证

自定义登录认证

.ini配置

[main]
# 配置MD5散列凭证匹配器
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
# 设置散列迭代次数,用于增强密码安全性
md5CredentialsMatcher.hashIterations=3
​
# 配置自定义Realm,com.womeng.mp.shiro.realm.MyRealm是自定义Realm的完整类名
myRealm=com.womeng.mp.shiro.realm.MyRealm
# 将配置的MD5散列凭证匹配器设置到自定义Realm中
myRealm.credentialsMatcher=$md5CredentialsMatcher
# 将自定义Realm添加到SecurityManager的realms列表中
securityManager.realms=$myRealm

三、SpringBoot3整合Shiro

定义Service接口

PS:需要事先准备好数据库和实体类

  1. 定义Service接口

    public interface IUserService extends IService {
        User getUserInfoByName(String name);
    }
  2. 实现接口

    @Service
    @RequiredArgsConstructor//lombok注解:对一开始需要初始化的变量进行初始化,这里对应userMapper
    public class UserServiceImpl extends ServiceImpl implements IUserService {
    ​
        //注入mapper
        private final UserMapper userMapper;
    ​
        @Override
        public User getUserInfoByName(String name) {
            //1.创建查询条件
            QueryWrapper wrapper = new QueryWrapper().eq("name",name);
            //2.查询并返回
            return userMapper.selectOne(wrapper);
        }
    ​
    }

添加依赖

在pom.xml项目中添加依赖


    org.apache.shiro
    shiro-spring
    jakarta
    1.11.0
    
    
        
            org.apache.shiro
            shiro-core
        
        
            org.apache.shiro
            shiro-web
        
    



    org.apache.shiro
    shiro-core
    jakarta
    1.11.0


    org.apache.shiro
    shiro-web
    jakarta
    1.11.0
    
        
            org.apache.shiro
            shiro-core
        
    

这里我没有直接导入shiro-spring-boot-web-starter,即下面这个maven坐标



    org.apache.shiro
    shiro-spring-boot-web-starter
    2.0.1

是因为在启动过程中发生报错

Type javax.servlet.Filter not present

大概原因是Spring Boot 3.0 使用了Servlet 5.0,而javax.servlet此时已经迁移到了jakarta.servlet中。Shiro已经提供了适配Servlet 5.0 的依赖包,使用标签即可选取适配版本,不过部分Shiro包中仍嵌套依赖了一些没有适配jakarta的依赖包,所以我们需要使用将其排除,再引入同版本的jakarta适配包

参考:Java17和springboot3.0使用shiro报ClassNotFoundException_shiro java17-CSDN博客

自定义Realm

import com.womeng.mp.entity.User;
import com.womeng.mp.service.IUserService;
import lombok.RequiredArgsConstructor;
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.stereotype.Component;
​
@Component
@RequiredArgsConstructor//lombok注解:对一开始需要初始化的变量进行初始化,这里对应userService
public class MyRealm extends AuthorizingRealm {
    
    private final IUserService userService;
​
    //1.自定义授权方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;//因为暂时用不到,所以直接返回null
    }
​
    //2.自定义登录认证方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.获取用户输入身份信息
        String name = authenticationToken.getPrincipal().toString();
        //2.获取数据库用户信息,可以自己建一张只有name和password的表,仅用于测试
        User user = userService.getUserInfoByName(name);//这个是自定义的Service接口
        //3.非空判断
        if (user != null){
            return new SimpleAuthenticationInfo(
                    authenticationToken.getPrincipal(),//身份信息
                    user.getPassword(),//获取数据库中加密好的密码
                    ByteSource.Util.bytes("salt"),//盐值,要注意,一般要从数据库中获取
                    authenticationToken.getPrincipal().toString()//身份信息的字符串形式
            );
        }
        return null;
    }
}

创建配置类

要想在Spring项目中使用Shiro,还需要创建配置类,可以新建config包放在项目根目录下(跟application启动类同级)

import com.womeng.mp.shiro.realm.MyRealm;
import lombok.RequiredArgsConstructor;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
@RequiredArgsConstructor
public class ShiroConfig {
​
    private final MyRealm myRealm;
​
    //配置SecurityManager
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(){
        //1.创建SecurityManager
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //2.创建加密对象,设置加密规则,要注意,这里的加密规则要保证跟数据库中密码的加密规则一致
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        //2.1 采用MD5加密
        matcher.setHashAlgorithmName("md5");
        //2.2 设置迭代次数
        matcher.setHashIterations(3);
        //3.将加密规则存储到MyRealm中
        myRealm.setCredentialsMatcher(matcher);
        //4.将MyRealm存入SecurityManager
        securityManager.setRealm(myRealm);
        //5.将securityManager添加到SecurityUtils中进行全局配置
        SecurityUtils.setSecurityManager(securityManager);
        //6.返回,不返回不行,springboot会报错,即使设置成void
        return securityManager;
    }
​
    //配置Shiro内置过滤器拦截范围
    @Bean
    public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
        DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
        //设置不认证可以访问的资源
        definition.addPathDefinition("/user/userLogin","anon");
        definition.addPathDefinition("/login","anon");
        //设置需要进行登录认证才能访问的资源
        definition.addPathDefinition("/**","authc");
        return definition;
    }
}

上面这段代码中的第5点,如果不在这里设置,后面使用SecurityUtils.getSubject()时会找不到securityManager,具体报错信息如下:

No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton.  This is an invalid application configuration.

我能想到的就是在这里将securityManager添加到SecurityUtils中进行全局配置。

测试

  1. 定义Controller接口

    @RestController
    @RequestMapping("/user")
    @RequiredArgsConstructor //lombok注解:对一开始需要初始化的变量进行初始化
    public class UserController {
        //注意final修饰!!!
        private final IUserService userService;
    ​
        @GetMapping("userLogin")
        public String userLogin(String name , String pwd){
            //1.获取subject对象
            Subject subject = SecurityUtils.getSubject();
            //2.封装请求数据到token
            UsernamePasswordToken token = new UsernamePasswordToken(name, pwd);
            //3.调用login方法进行登录认证
            try {
                subject.login(token);
                return "登录成功";
            } catch (AuthenticationException e) {
                e.printStackTrace();
                System.out.println("登录失败");
                return "登录失败";
            }
        }
    }

  2. 运行启动类,并在浏览器或其他测试工具中访问http://localhost:8080/user/userLogin?name=Xxx&pwd=Xxx

    PS:将Xxx换成自己的测试字段,如果不出意外,会显示

    登录成功

后话

在测试过程中还出现过如下报错

No realms have been configured!  One or more realms must be present to execute an authentication attempt.

网上找了好多办法,都没用。后面发现是Service接口的问题。

一开始的接口是这样子的,想着不会硬编码,但是会报错

@Override
public User getUserInfoByName(String name) {
    //1.创建查询条件
    LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
        .eq(name != null, User::getName, name);
    //2.查询并返回
    return userMapper.selectOne(wrapper);
}

改成下面这样就没事了

@Override
public User getUserInfoByName(String name) {
    //1.创建查询条件
    QueryWrapper wrapper = new QueryWrapper().eq("name",name);
    //2.查询并返回
    return userMapper.selectOne(wrapper);
}

实体类的字段和数据库中的字段都是name,不清楚为什么会报错。

你可能感兴趣的:(web安全,springboot)