Shrio与SpringBoot整合(一)

Shiro 简介:

Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。

Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。

更具体的介绍、学习可以看看这个链接:Shiro 简介_w3cschool

这个里面有Shrio的全部学习资料,看完这个之后,在来看Spring Boot 整合Shrio会更加明白,或者Spring Boot 整合Shrio后,再去看看这个文档,也会明白为什么会这么做了。

Spring Boot 的版本:

<version>2.4.10</version>

目录:

1、Spring Boot 与Shrio的简单整合,适合做单体项目

2、Shrio 的会话管理,可以用在前后端分离的项目

3、Shrio 使用redis做缓存

4、Shrio密码加密验证

一、Spring Boot 与Shrio的简单整合,适合做单体项目

1、pom.xml

<!--shiro与spring整合需要的包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!--shiro与thymeleaf整合需要的包-->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

2、Shrio的配置类型:ShiroConfig

package com.example.springbootshrio.config;

import com.example.springbootshrio.shiro.CustomRealm;
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(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/login.html");//登录页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");//未经授权就可以访问的页面
        shiroFilterFactoryBean.setSuccessUrl("/successUrl");//成功页面
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        // anon:所有url都都可以匿名访问,一般写静态资源
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/front/**", "anon");
        filterChainDefinitionMap.put("/api/**", "anon");

        //authc:所有url都必须认证通过才可以访问
        filterChainDefinitionMap.put("/admin/**", "authc");
        filterChainDefinitionMap.put("/user/**", "authc");
        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;

    }

    /**
     * 创建 SecurityManager 并且绑定 Realm
     * @return
     */
    @Bean
    public org.apache.shiro.mgt.SecurityManager securityManager() {
        DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
        defaultSecurityManager.setRealm(customRealm());
        return defaultSecurityManager;
    }


   /**
    *自定义身份认证realm
    */
    @Bean
    public CustomRealm customRealm() {
        CustomRealm customRealm = new CustomRealm();
        return customRealm;
    }

/**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 开启jsp/html页面的注解
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }
}

3、用户名/密码验证,,以及用户授权

package com.example.springbootshrio.shiro;

import org.apache.shiro.SecurityUtils;
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.apache.shiro.subject.SimplePrincipalCollection;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
 * 负责认证用户身份和对用户进行授权
 */
public class CustomRealm extends AuthorizingRealm {

    //这里可以注入其他的服务,去查询用户的密码、查询用户的权限等信息

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //用户名
        String username = (String) SecurityUtils.getSubject().getPrincipal();
        System.out.println("-------根据用户名,获取权限--------");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //权限code,要唯一的,之后的权限验证就是通过匹配它来做的
        Set<String> stringPermissions = new HashSet<>();
        stringPermissions.add("user:info");
        stringPermissions.add("user:update");
        info.setStringPermissions(stringPermissions);
        //还可以通过角色来做权限验证
        Set<String> roles = new HashSet<>();
        roles.add("财务经理");
        roles.add("普通员工");
        info.setRoles(roles);
        return info;
    }

    /**
     * 这里可以注入userService,为了方便演示,我就写死了帐号了密码
     * private UserService userService;
     * <p>
     * 用户名和密码的验证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("-------身份认证方法--------");
        //用户名
        String userName = (String) authenticationToken.getPrincipal();
        if (userName == null) {
            throw new AccountException("用户名或密码不正确");
        }

        //根据用户名从数据库获取密码
        String password = "123";

        //如果身份认证验证成功,返回一个AuthenticationInfo实现;
        return new SimpleAuthenticationInfo(userName, //用户名
                password, //密码
                getName() //当前 realm 的名字
        );
    }
}

4、controller的使用

这里面涉及到的shrio注解,可以参考文档:shiro注解@RequiresPermissions多权限任选一参数用法_qi923701的博客-CSDN博客_requirespermissions的使用

package com.example.springbootshrio.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
public class HomeIndexController {

    //跳转到登录页面
    @GetMapping(value = "/login.html")
    public String defaultLogin() {
        return "login";
    }

    @GetMapping(value = "/login")
    public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
        // 从 SecurityUtils 里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 在认证提交前准备 token(令牌)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 执行认证登陆
        try {
            subject.login(token);
        } catch (UnknownAccountException uae) {
            return "未知账户";
        } catch (IncorrectCredentialsException ice) {
            return "密码不正确";
        } catch (LockedAccountException lae) {
            return "账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
            return "用户名或密码错误次数过多";
        } catch (AuthenticationException ae) {
            return "用户名或密码不正确!";
        }
        //登录成功,跳转到index页面
        if (subject.isAuthenticated()) {
            return "index";
        } else {
            token.clear();
            return "登录失败";
        }
    }

    //退出
    @RequestMapping(value = "/logout", method = RequestMethod.POST)
    @ResponseBody
    public void logout() {
        // 从 SecurityUtils 里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        //退出
        subject.logout();
    }

    /**
     * 通过 代码的if/else 来判断权限,不建议这么使用
     * 角色、资源来判断登录用户是否可以访问当前的方法
     * @return
     */
    @RequestMapping(value = "/getUserInfo", method = RequestMethod.POST)
    @ResponseBody
    public String getUserInfo(){
        Subject subject = SecurityUtils.getSubject();
        //根据角色来判断
        if (subject.hasRole("财务经理")){
            System.out.println("==========登录用户有财务经理的权限,可以进入到这个方法来==================");
            return "你有财务经理的权限";
        }
        //根据资源来判断
        try{
            subject.checkPermission("user:show");
        }catch (AuthorizationException au){
            System.out.println("==========登录用户没有当前方法的权限==================");
            return "你没有当前方法的权限";
        }
        System.out.println("==========登录用户有财务经理的权限,可以进入到这个方法来==================");
        return "你有财务经理的权限";
    }

    /**
     * 通过 注解来判断权限  建议使用
     * @return
     */
    @RequiresRoles("财务经理")//角色的注解
    @RequiresPermissions(value = {"user:info","user:info"})//资源权限的注解
    @RequestMapping(value = "/getRoleInfo", method = RequestMethod.POST)
    @ResponseBody
    public String getRoleInfo(){
        return "你有访问该方法的权限";
    }
}

login.html

  <form method="get" action="/login">
      <table>
          <tr>
              <td>用户名:</td>
              <td>密码:</td>
          </tr>
          <tr>
              <td><input type="text" name="username"></td>
              <td><input type="text" name="password"></td>
          </tr>
          <tr>
              <td><input type="submit" value="登录" /></td>
          </tr>
      </table>
  </form>

index.html

这里涉及到的<shiro></shiro>标签,可以参考:

Shiro JSP 标签_w3cschool

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="/jquery.min.js"></script>
</head>
<body>
    <!--通过角色来控制页面的权限-->
    <shiro:hasRole name="财务经理">
      <p>通过角色来控制页面的权限</p>
    </shiro:hasRole>
    <!--多个角色来控制页面的权限-->
    <shiro:hasAndRole name="财务经理,普通员工">
      <p>多个角色来控制页面的权限</p>
    </shiro:hasAndRole>

    <!--通过资源来控制权限-->
    <shiro:hasPermission name="user:info">
        <p>通过资源来控制权限</p>
    </shiro:hasPermission>

    <!--多个资源来控制权限-->
    <shiro:hasAndPermission name="user:info,user:update">
        <p>多个资源来控制权限</p>
    </shiro:hasAndPermission>

    <button id="_button">点击</button>
    <!--点击2,是可以访问到后台的,但是点击退出后,在点击2,那么就不能访问后台了,因为以及退出了。-->
    <button id="_button2">点击2</button>
    <button id="_button3">退出</button>
</body>
<script>
    $("#_button").click(function () {
        $.ajax({
            type: "post",
            url:"/getUserInfo",
            success: function (res) {
                alert(JSON.stringify(res))
            }
        });
    });
    $("#_button2").click(function () {
        $.ajax({
            type: "post",
            url:"/getRoleInfo",
            success: function (res) {
                alert(JSON.stringify(res))
            }
        });
    });
    $("#_button3").click(function () {
        $.ajax({
            type: "post",
            url:"/logout",
            success: function (res) {
                alert(JSON.stringify(res))
            }
        });
    });
</script>
</html>

index.html的<shrio>标签是用来控制页面权限的,页面必须要引入:

xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"

才可以使用<shrio>标签

二、Shrio 的会话管理,可以用在前后端分离的项目

正常来讲 Shiro 是从 Cookie 中获取 SessionId 的,然后找到相对应的 Session 来保证用户登陆的正确性和权限的正确性, 但是在前后端分离的项目中,由于每次的 SessionId 都是不一样的,所以这里选择的是重写 DefaultWebSessionManager 的部分方法, 然后在用户登陆的时候给前端返回 SessionId 来当用户的凭证信息,前端在请求头中携带信息,来解决 Shiro 的用户 Token 认证问题。

1、重写的类:

package com.example.springbootshrio.config;


import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * session处理器
 */
public class MySessionManager extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "Authorization";

//    getReferencedSessionId()方法中的关联源的意思:
//    检测sessionId关联源(
//    如果可以从cookie中获取sessionId,则在request中设置sessionId的关联源为cookie;
//    如果不可以读取,则从request访问路径中获取,如果不存在,则从request的parameter中获取,如果从request的访问路径中或者parameter中获取到的不为空,则设置关联源为url;
//    并在将sessionId和合法信息存储到request中
//    )

    //sessionId的关联源为 无状态请求
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public MySessionManager() {
        super();
    }

    /**
     * 重写获取sessionId的方法调用当前 Manager 的获取方法
     * @param request
     * @param response
     * @return
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //从请求头获取 sessionId
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果请求头中有 Authorization 则其值为 sessionId
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从 cookie 取 sessionId
            return super.getSessionId(request, response);
        }
    }
}

2、配置类:ShiroConfig

    /**
     * 创建 SecurityManager 并且绑定 Realm
     * @return
     */
    @Bean
    public org.apache.shiro.mgt.SecurityManager securityManager() {
        DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
        //自定义 realm
        defaultSecurityManager.setRealm(customRealm());
        // 自定义session管理
        defaultSecurityManager.setSessionManager(sessionManager());   //这一行是新增的
        return defaultSecurityManager;
    }


    /**
     * shiro session管理
     */
    @Bean
    public MySessionManager sessionManager() {
        MySessionManager sessionManager = new MySessionManager();
        return sessionManager;
    }

3、使用,controller:用户登录成功后,给用户返回sessionId就可以了

Session session = subject.getSession();
        session.getId();

4、前端使用:每次请求时,把后台给的sessionid放在请求头里面给后台就可以了。

$("#_button4").click(function () {
        $.ajax({
            type: "post",
            headers:{"Authorization":token},
            url:"/getRoleInfo",
            success: function (res) {
                alert(JSON.stringify(res))
            }
        });
    });

这里需要注意:session管理器中定义的是从前端的请求头里面获取sessionId,那么前端就需要把sessionId放在头里面,也可以不这么放,可以自定义把sessionId放在其他地方的。

三、Shrio 使用redis做缓存

1、pom.xml,要3.1以上版本的,不然从redis中获取不到数据的。

        <!-- shiro+redis缓存插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.3</version>
        </dependency>

2、shrio配置:

/**
     * 创建 SecurityManager
     * @return
     */
    @Bean
    public org.apache.shiro.mgt.SecurityManager securityManager() {
        DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
        //自定义 realm
        defaultSecurityManager.setRealm(customRealm());
        // 自定义session管理
        defaultSecurityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
        defaultSecurityManager.setCacheManager(redisCacheManager());//新增加的行
        return defaultSecurityManager;
    }
 
/**
     * shiro session管理
     */
    @Bean
    public MySessionManager sessionManager() {
        MySessionManager sessionManager = new MySessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());//新增加的行
        return sessionManager;
    }

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     * @return
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("127.0.0.1:6379");
        redisManager.setTimeout(1800);// 配置缓存过期时间
        // redisManager.setPassword(password);//redis密码
        return redisManager;
    }
    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }
    /**
     * 把shrio的缓存换成redis
     * cacheManager缓存 redis实现
     * 使用的是shiro-redis开源插件
     * @return
     */
    @Bean
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

其他的都不行动了,这个时候shiro的缓存,session都是用的redis了。

四、Shrio登录密码加密

Shiro 提供了用于加密密码和验证密码服务的 CredentialsMatcher 接口,而 HashedCredentialsMatcher 正是 CredentialsMatcher 的一个实现类。写项目的话,总归会用到用户密码的非对称加密,目前主流的非对称加密方式是 MD5 ,以及在 MD5 上的加盐处理,而 HashedCredentialsMatcher 也允许我们指定自己的算法和盐。这里将介绍 HashedCredentialsMatcher 的使用。

1、配置类:ShiroConfig

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

/**
     * 自定义身份认证realm
     * @return
     */
    @Bean
    public CustomRealm customRealm() {
        CustomRealm customRealm = new CustomRealm();
        customRealm.setCredentialsMatcher(hashedCredentialsMatcher());//新增的,代表Realm将用这个作为凭证匹配器,如果存在多个Realm,其他realm需要用到这个凭证匹配器,那么也是需要添加这行配置的。
        return customRealm;
    }

2、加密的小工具:Shiro提供的加密解密工具,密码保存到数据的时候需要先用它加密后在保存。

import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;

public class PasswordUtils {
    /**
     * 通过Shiro提供的加密工具进行密码加密
     * @param username
     * @param password
     * @return
     */
    public static String pwd(String username, String password) {
        String hashAlgorithmName = "MD5";//加密算法
        Object salt = ByteSource.Util.bytes(username);//使用用户名作为盐值
        int hashIterations = 2;//md5迭代次数
        Object result = new SimpleHash(hashAlgorithmName, password, salt, hashIterations);
        return  String.valueOf(result);
    }
}

3、在realm中的doGetAuthenticationInfo方法使用:

/**
     * 这里可以注入userService,为了方便演示,我就写死了帐号了密码
     * private UserService userService;
     * <p>
     * 用户名和密码的验证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println(getName()+"-------身份认证方法--------");
        //用户名,前端传给后台的
        String userName = (String) authenticationToken.getPrincipal();
        if (userName == null) {
            throw new AccountException("用户名或密码不正确");
        }
        //从数据库拿到的密码,他是一个加密过的密码,这里为了演示,密码直接写死为123
        String password = PasswordUtils.pwd(userName, "123");
        System.out.println("StudentsRealm pwd:"+password);

        //由shiro来做密码校验。如果身份认证验证成功,返回一个AuthenticationInfo实现;
        return new SimpleAuthenticationInfo(userName, //用户名
                password, //密码
                ByteSource.Util.bytes(userName),//salt=username+salt 盐值
                getName() //当前 realm 的名字
        );
    }

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值