Springboot 整合 Shiro

Springboot 整合 Shiro

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
下面我们来看看 Springboot 项目中如何整合使用 Shiro.

整合后项目结构

在这里插入图片描述

Springboot 整合 Shiro

1.pom 文件添加 shiro 依赖

<!-- shiro权限依赖 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>
<!-- shiro ehcache -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>

2.自定义身份认证 UserRealm

package com.example.demo.common.shiro;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.demo.common.utils.Cryption;
import com.example.demo.module.user.entity.TblUser;
import com.example.demo.module.user.service.IUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Component
public class UserRealm extends AuthorizingRealm {

    @Lazy
    @Autowired
    private IUserService iUserService;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        TblUser user = (TblUser)principalCollection.getPrimaryPrincipal();

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermission(user.getRole());

        return info;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;

        //查询用户信息
        TblUser user = iUserService.getOne(new LambdaQueryWrapper<TblUser>().eq(TblUser::getUserCode,token.getUsername()));
        //账号不存在
        if(user == null) {
            throw new UnknownAccountException("账号或密码不正确");
        }

        String password = Cryption.genUserPwd(new String((char[]) token.getCredentials()));
        System.out.println(password);
        if (!user.getPassword().equals(password)) {
            // 密码错误
            throw new IncorrectCredentialsException("账号或密码不正确");
        }

        return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
    }

    /**
     * 密码加密算法
     * @param credentialsMatcher
     */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
        //密码散列算法
        shaCredentialsMatcher.setHashAlgorithmName("MD5");
        //散列迭代次数
        shaCredentialsMatcher.setHashIterations(1);
        super.setCredentialsMatcher(shaCredentialsMatcher);
    }
}

3.密码加密工具 Cryption

package com.example.demo.common.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;

public class Cryption {

    /**
     * 生成初始密码
     */
    private static String genUserPwd() {
        return DigestUtil.md5Hex("888888");
    }


    /**
     * 生成并加密密码
     */
    public static String genUserPwd(String pwd) {
        if (StrUtil.isEmpty(pwd)) {
            return genUserPwd();
        }
        return DigestUtil.md5Hex(pwd);
    }

}

4.shiro 配置 ShiroConfig

package com.example.demo.common.shiro;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import cn.hutool.core.codec.Base64;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * shiro 配置
 */
@Configuration
public class ShiroConfig {

    /**
     * 集成缓存
     * @return sessionManager
     */
    @Bean("sessionManager")
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //设置session过期时间(单位:毫秒),默认为15分钟
        sessionManager.setGlobalSessionTimeout(1000 * 60 * 120);
        //会话验证调度器
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdUrlRewritingEnabled(false);

        return sessionManager;
    }

    @Bean("securityManager")
    public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        // 自定义缓存实现
        securityManager.setCacheManager(ehCacheManager());
        securityManager.setSessionManager(sessionManager);

        // 设置cookie管理
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    /**
     * shiro缓存管理器。需要添加到securityManager中
     * @return cacheManager
     */
    @Bean
    public EhCacheManager ehCacheManager(){
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }

    /**
     * Cookie对象
     * @return simpleCookie
     */
    private SimpleCookie rememberMeCookie() {
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        // 记住时间,单位秒,默认15天
        simpleCookie.setMaxAge(60 * 60 * 24 * 15);
        return simpleCookie;
    }

    /**
     * cookie管理对象;
     * @return cookieRememberMeManager
     */
    private CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        // rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
        return cookieRememberMeManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        //自定义登录页面
        shiroFilter.setLoginUrl("/hello");

        //自定义未授权页面
        shiroFilter.setUnauthorizedUrl("/403");

        Map<String, String> filterMap = new LinkedHashMap<>();
        /**
         * anno:无需认证
         * authc: 必须认证才可以访问
         * user: 如果使用rememberMe 可以直接访问
         * perms: 该资源必须得到资源权限才可以访问
         * role: 该资源必须得到角色权限才可以访问
         */

        //登出
        filterMap.put("/admin/logout", "logout");

        //静态资源 开放
        filterMap.put("/static/**", "anon");

        //登录请求
        filterMap.put("/login", "anon");
		// /emp/data 需要 emp:data 权限才可访问
        filterMap.put("/emp/data", "perms[emp:data]");

        //所有请求需认证
        filterMap.put("/**","authc");

        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

}

shiro 认证过滤器

名称说明
anno无需认证
authc必须认证才可以访问
user如果使用rememberMe 可以直接访问
perms该资源必须得到资源权限才可以访问
role该资源必须得到角色权限才可以访问

5.登录方法 LoginController

package com.example.demo.module.sys.controller;

import com.example.demo.common.controller.BaseController;
import com.example.demo.module.user.entity.TblUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

@RestController
public class LoginController extends BaseController {

    @PostMapping(value = "login")
    public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password, boolean rememberMe) {

        try {
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
            subject.login(token);

            TblUser user =(TblUser)SecurityUtils.getSubject().getPrincipal();
//            SecurityUtils.getSubject().getSession().setAttribute("usercode", user.getUserCode());

        } catch (Exception e) {
            log.error(e.getMessage(), e);
            ModelAndView mv = new ModelAndView("login");
            mv.addObject("message",e.getMessage());
            return mv;
        }

        return new ModelAndView("index");
    }

}

6.数据准备

在数据库用户表中准备用户数据进行测试
在这里插入图片描述
准备了2条用户数据,其中 user1 用户拥有 emp:add 权限,admin 拥有 emp:data 权限。

7.启动项目测试
项目启动后访问项目,因为是未登录状态,会自动跳转到登录页
在这里插入图片描述

登录用户 user1
在这里插入图片描述
访问 /emp/data, 因为 user1 用户没有 emp:data 权限,所以访问无权限,跳转到403页面。
在这里插入图片描述
访问 /emp/2 ,因为该连接只做了登录权限限制,所以 user1 用户可以正常访问。
在这里插入图片描述

shiro 注解式开发

shiro提供了相应的注解用于权限控制,如果使用这些注解就需要使用aop的功能来进行判断。shiro提供了spring aop集成,用于权限注解的解析和验证。

想要使用 shiro 的注解,需要在 shiro 配置文件 ShiroConfig 添加如下配置

    //管理shiro bean生命周期
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

shiro 注解说明

注解说明
@RequiresAuthenthentication表示当前Subject已经通过login进行身份验证;即 Subject.isAuthenticated()返回 true
@RequiresUser表示当前Subject已经身份验证或者通过记住我登录的
@RequiresGuest表示当前Subject没有身份验证或者通过记住我登录过,即是游客身份
@RequiresRoles(value = {“admin”,“user”},logical = Logical.AND)表示当前Subject需要角色admin和user
@RequiresPermissions(value = {“user:delete”,“user:b”},logical = Logical.OR)表示当前Subject需要权限user:delete或者user:b

EmpController 通过 shiro 注解控制方法权限

package com.example.demo.module.emp.controller;


import com.example.demo.common.dto.R;
import com.example.demo.module.emp.entity.Emp;
import com.example.demo.module.emp.service.IEmpService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author xx
 * @since 2021-05-14
 */
@RestController
@RequestMapping("/emp")
@Api(tags = "用户管理类")
public class EmpController {

    @Autowired
    private IEmpService iEmpService;

    @RequiresPermissions(value = {"emp:data","emp:update"},logical = Logical.OR)
    @ApiOperation(value = "用户信息接口")
    @GetMapping(value = "/data")
    public List<Emp> all(){
        return iEmpService.list();
    }

    @ApiOperation(value = "根据id获取用户信息接口")
    @ApiImplicitParam(value = "用户id",name = "id")
    @GetMapping(value = "/{id}")
    public R getById(@PathVariable(value = "id")Integer id){
        return R.ok(iEmpService.getById(id));
    }

}

@RequiresPermissions(value = {"emp:data","emp:update"},logical = Logical.OR) 表示 all() 方法需要 emp:dataemp:update 权限才可访问

启动项目测试
登录 user1 用户访问
在这里插入图片描述
登录 admin 用户访问 /emp/data ,因为 admin 用户拥有 emp:data 权限,所以正常访问
在这里插入图片描述

Thymeleaf 扩展 shiro 权限标签

1.pom 文件添加 shiro 标签依赖

<!-- thymleaf 扩展 shiro -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

2.shiro 配置文件 ShiroConfig 添加如下配置开启标签

/**
* 开启shiro标签
 */
@Bean
public ShiroDialect shiroDialect() {
    return new ShiroDialect();
}

3.html 添加标签支持

<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

4.修改主页 index.html 并让主页可以无登录访问

<!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>
</head>
<body>
<p>主页</p>
<div>
    <shiro:guest>
        游客,
        <a th:href="@{/hello}">登录</a>
    </shiro:guest>
    <shiro:user>
        <p>
            <span shiro:principal property="userCode"></span>
            ,欢迎您</p>
        <a th:href="@{/admin/logout}">退出</a>
    </shiro:user>
</div>

<br/>
<br/>

<shiro:user>
    <a shiro:hasPermission="emp:data" th:href="@{/emp/data}">/emp/data</a>
    <br/>
    <a th:href="@{/emp/2}">/emp/2</a>
</shiro:user>
</body>
</html>

5.启动项目测试

未登录状态访问
在这里插入图片描述
登录 user1 用户
在这里插入图片描述
可以发现,shiro 标签已生效。

贴上 shiro 标签使用说明

<!--验证当前用户是否拥有指定权限。  -->
<a shiro:hasPermission="user:add" href="#" >add用户</a>

<!--与hasPermission标签逻辑相反,当前用户没有制定权限时,验证通过。-->
<p shiro:lacksPermission="user:del"> 没有权限 </p>

<!--验证当前用户是否拥有以下所有权限。-->
<p shiro:hasAllPermissions="user:view, user:add"> 权限与判断 </p>

<!--验证当前用户是否拥有以下任意一个权限。-->
<p shiro:hasAnyPermissions="user:view, user:del"> 权限或判断 </p>

<!--验证当前用户是否属于该角色。-->
<a shiro:hasRole="admin" href="#">拥有该角色</a>

<!--与hasRole标签逻辑相反,当用户不属于该角色时验证通过。-->
<p shiro:lacksRole="developer"> 没有该角色 </p>

<!--验证当前用户是否属于以下所有角色。-->
<p shiro:hasAllRoles="developer, admin"> 角色与判断 </p>

<!--验证当前用户是否属于以下任意一个角色。-->
<p shiro:hasAnyRoles="admin, vip, developer"> 角色或判断 </p>

<!--验证当前用户是否为“访客”,即未认证(包含未记住)的用户。-->
<p shiro:guest="">访客 未认证</a></p>

<!--认证通过或已记住的用户-->
<p shiro:user=""> 认证通过或已记住的用户 </p>

<!--已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。-->
<p shiro:authenticated=""> <span shiro:principal=""></span> </p>

<!--输出当前用户信息,通常为登录帐号信息-->
<p> <shiro:principal/> </p>

<!--未认证通过用户,与authenticated标签相对应。-->
<!--与guest标签的区别是,该标签包含已记住用户。-->
<p shiro:notAuthenticated=""> 未认证通过用户 </p>

最后贴上整合后的关键代码

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-test</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- shiro权限依赖 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- thymleaf  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- thymleaf 扩展 shiro -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>
        <!-- 数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- 引入 Druid 数据源依赖:https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.9</version>
        </dependency>
        <!-- mybatis-plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
        <!-- mybatis-plus 代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.2</version>
        </dependency>
        <!-- 代码生成模板-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <!-- swagger2 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--工具-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
            <scope>provided</scope>
        </dependency>
        <!-- shiro ehcache -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

UserRealm

package com.example.demo.common.shiro;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.demo.common.utils.Cryption;
import com.example.demo.module.user.entity.TblUser;
import com.example.demo.module.user.service.IUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Component
public class UserRealm extends AuthorizingRealm {

    @Lazy
    @Autowired
    private IUserService iUserService;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        TblUser user = (TblUser)principalCollection.getPrimaryPrincipal();

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermission(user.getRole());

        return info;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;

        //查询用户信息
        TblUser user = iUserService.getOne(new LambdaQueryWrapper<TblUser>().eq(TblUser::getUserCode,token.getUsername()));
        //账号不存在
        if(user == null) {
            throw new UnknownAccountException("账号或密码不正确");
        }

        String password = Cryption.genUserPwd(new String((char[]) token.getCredentials()));
        System.out.println(password);
        if (!user.getPassword().equals(password)) {
            // 密码错误
            throw new IncorrectCredentialsException("账号或密码不正确");
        }

        return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
    }

    /**
     * 密码加密算法
     * @param credentialsMatcher
     */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
        //密码散列算法
        shaCredentialsMatcher.setHashAlgorithmName("MD5");
        //散列迭代次数
        shaCredentialsMatcher.setHashIterations(1);
        super.setCredentialsMatcher(shaCredentialsMatcher);
    }
}

ShiroConfig

package com.example.demo.common.shiro;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import cn.hutool.core.codec.Base64;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * shiro 配置
 */
@Configuration
public class ShiroConfig {

    /**
     * 集成缓存
     * @return sessionManager
     */
    @Bean("sessionManager")
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //设置session过期时间(单位:毫秒),默认为15分钟
        sessionManager.setGlobalSessionTimeout(1000 * 60 * 120);
        //会话验证调度器
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdUrlRewritingEnabled(false);

        return sessionManager;
    }

    @Bean("securityManager")
    public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        // 自定义缓存实现
        securityManager.setCacheManager(ehCacheManager());
        securityManager.setSessionManager(sessionManager);

        // 设置cookie管理
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    /**
     * shiro缓存管理器。需要添加到securityManager中
     * @return cacheManager
     */
    @Bean
    public EhCacheManager ehCacheManager(){
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }

    /**
     * Cookie对象
     * @return simpleCookie
     */
    private SimpleCookie rememberMeCookie() {
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        // 记住时间,单位秒,默认15天
        simpleCookie.setMaxAge(60 * 60 * 24 * 15);
        return simpleCookie;
    }

    /**
     * cookie管理对象;
     * @return cookieRememberMeManager
     */
    private CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        // rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
        return cookieRememberMeManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        /*重要,设置自定义拦截器,当访问某些自定义url时,使用这个filter进行验证*/
//        Map<String, Filter> filters = new LinkedHashMap<>();
        // 如果map里面key值为authc,表示所有名为authc的过滤条件使用这个自定义的filter
        // map里面key值为LoginFilter,表示所有名为LoginFilter的过滤条件使用这个自定义的filter,具体见下方
//        filters.put("myFilter", new LoginFilter());
//        shiroFilter.setFilters(filters);

        //自定义登录页面
        shiroFilter.setLoginUrl("/hello");

        //自定义未授权页面
        shiroFilter.setUnauthorizedUrl("/403");

        Map<String, String> filterMap = new LinkedHashMap<>();
        /**
         * anno:无需认证
         * authc: 必须认证才可以访问
         * user: 如果使用rememberMe 可以直接访问
         * perms: 该资源必须得到资源权限才可以访问
         * role: 该资源必须得到角色权限才可以访问
         */

        //登出
        filterMap.put("/admin/logout", "logout");

        //静态资源 开放
        filterMap.put("/static/**", "anon");

        //登录请求
        filterMap.put("/login", "anon");

        filterMap.put("/index", "anon");

//        filterMap.put("/emp/data", "perms[emp:data]");

        //所有请求需认证
        filterMap.put("/**","authc");

//        filterMap.put("/**", "myFilter,authc");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

    //管理shiro bean生命周期
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * 开启shiro标签
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

}

LoginController

package com.example.demo.module.sys.controller;

import com.example.demo.common.controller.BaseController;
import com.example.demo.module.user.entity.TblUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

@RestController
public class LoginController extends BaseController {

    @PostMapping(value = "login")
    public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password, boolean rememberMe) {

        try {
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
            subject.login(token);

            TblUser user =(TblUser)SecurityUtils.getSubject().getPrincipal();
//            SecurityUtils.getSubject().getSession().setAttribute("usercode", user.getUserCode());

        } catch (Exception e) {
            log.error(e.getMessage(), e);
            ModelAndView mv = new ModelAndView("login");
            mv.addObject("message",e.getMessage());
            return mv;
        }

        return new ModelAndView("index");
    }

}

ShiroUtil

package com.example.demo.common.utils;

import com.example.demo.common.shiro.UserRealm;
import com.example.demo.module.user.entity.TblUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.mgt.RealmSecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;

/**
 * Shiro工具类
 */
public class ShiroUtil {

    /**
     * 密码散列算法为md5
     */
    public final static String HASH_ALGORITHM_NAME = "MD5";
    /**
     * 散列迭代次数
     */
    public final static int HASH_ITERATIONS = 1;

    public static String md5(String password) {
        return new SimpleHash(HASH_ALGORITHM_NAME, password, HASH_ITERATIONS).toString();
    }

    public static Session getSession() {
        return SecurityUtils.getSubject().getSession();
    }

    public static Subject getSubject() {
        return SecurityUtils.getSubject();
    }

    public static TblUser getUser() {
        return (TblUser)SecurityUtils.getSubject().getPrincipal();
    }

    public static Integer getUserId() {
        return getUser().getId();
    }

    public static void setSessionAttribute(Object key, Object value) {
        getSession().setAttribute(key, value);
    }

    public static Object getSessionAttribute(Object key) {
        return getSession().getAttribute(key);
    }

    public static boolean isLogin() {
        return SecurityUtils.getSubject().getPrincipal() != null;
    }

    public static void logout() {
        SecurityUtils.getSubject().logout();
    }

    /**
     * 刷新权限
     */
    public static void flushPrivileges() {
        RealmSecurityManager rsm = (RealmSecurityManager) SecurityUtils.getSecurityManager();
        UserRealm realm = (UserRealm) rsm.getRealms().iterator().next();
        // 只刷新当前用户权限
//        realm.clearCachedAuthorization();
        // 刷新所有在线用户权限
        Cache<Object, AuthorizationInfo> cache = realm.getAuthorizationCache();
        if (cache != null) {
            for (Object key : cache.keys()) {
                cache.remove(key);
            }
        }
    }

}

ExceptHandler

package com.example.demo.common.exception;

import com.example.demo.common.constant.Constants;
import com.example.demo.common.dto.R;
import org.apache.shiro.authz.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.ModelAndView;

@RestControllerAdvice
public class ExceptHandler {

    private final static Logger LOGGER = LoggerFactory.getLogger(ExceptHandler.class);

    /**
     * 无权限
     * @param e
     * @return
     */
    @ExceptionHandler(UnauthorizedException.class)
    public ModelAndView handleUnauthorizedExceptionException(UnauthorizedException e) {
        LOGGER.error("error:{}", e.getMessage());
        return new ModelAndView("403");
    }

    @ExceptionHandler(Exception.class)
    public R handleException(Exception e) {
        LOGGER.error("error:", e);
        return R.fail(Constants.OTHER_FAIL_CODE, e.getMessage());
    }
}

index.html

<!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>
</head>
<body>
<p>主页</p>
<div>
    <shiro:guest>
        游客,
        <a th:href="@{/hello}">登录</a>
    </shiro:guest>
    <shiro:user>
        <p>
            <span shiro:principal property="userCode"></span>
            ,欢迎您</p>
        <a th:href="@{/admin/logout}">退出</a>
    </shiro:user>
</div>

<br/>
<br/>

<shiro:user>
    <a shiro:hasPermission="emp:data" th:href="@{/emp/data}">/emp/data</a>
    <br/>
    <a th:href="@{/emp/2}">/emp/2</a>
</shiro:user>
</body>
</html>

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <title>登录</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="/static/fonts/font-awesome-4.7.0/css/font-awesome.min.css">
    <link rel="stylesheet" type="text/css" href="/static/css/util.css">
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
</head>

<body>
    <div class="limiter">
        <div class="container-login100">
            <div class="wrap-login100">
                <div class="login100-form-title" style="background-image: url(/static/images/bg-01.jpg);">
                    <span class="login100-form-title-1" th:text="#{login.tip}">登 录</span>
                </div>

                <form class="login100-form validate-form" th:action="@{/login}" method="post">
                    <span style="color: red;padding: 10px;" th:text="${message}"></span>
                    <div class="wrap-input100 validate-input m-b-26" th:data-validate="#{login.usernameValidate}">
                        <span class="label-input100" th:text="#{login.username}">用户名</span>
                        <label>
                            <input class="input100" type="text" name="username" th:placeholder="#{login.usernamePlaceholder}"/>
                        </label>
                        <span class="focus-input100"></span>
                    </div>

                    <div class="wrap-input100 validate-input m-b-18" th:data-validate="#{login.passwordValidate}">
                        <span class="label-input100" th:text="#{login.password}">密码</span>
                        <label>
                            <input class="input100" type="password" name="password" th:placeholder="#{login.passwordPlaceholder}"/>
                        </label>
                        <span class="focus-input100"></span>
                    </div>

                    <div class="flex-sb-m w-full p-b-30">
                        <div class="contact100-form-checkbox">
                            <input class="input-checkbox100" id="ckb1" type="checkbox" name="rememberMe">
                            <label class="label-checkbox100" for="ckb1" th:text="#{login.remember}">记住我</label>
                        </div>

                        <div>
                            <a href="javascript:" class="txt1" th:text="#{login.forget}">忘记密码?</a>
                        </div>
                    </div>

                    <div class="container-login100-form-btn m-b-18">
                        <button class="login100-form-btn" th:text="#{login.btn}">登 录</button>
                    </div>

                    <div class="flex-sa-m w-full p-b-30">
                        <div>
                            <a class="txt1" th:href="@{/hello(l='zh_CN')}">中文</a>
                        </div>

                        <div>
                            <a class="txt1" th:href="@{/hello(l='en_US')}">English</a>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>

    <script src="/static/js/jquery-3.2.1.min.js" type="text/javascript"></script>
    <script src="/static/js/main.js" type="text/javascript"></script>

</body>

</html>

ehcache.xml


<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">

    <!--
    diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
       user.home – 用户主目录
       user.dir  – 用户当前工作目录
       java.io.tmpdir – 默认临时文件路径
     -->
    <diskStore path="java.io.tmpdir"/>

    <defaultCache
            maxElementsInMemory="10000"
            maxElementsOnDisk="0"
            eternal="true"
            overflowToDisk="true"
            diskPersistent="false"
            timeToIdleSeconds="0"
            timeToLiveSeconds="0"
            diskSpoolBufferSizeMB="50"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LFU"/>

    <cache name="KACache"
           eternal="false"
           maxElementsInMemory="100"
           overflowToDisk="false"
           diskPersistent="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="10800"
           memoryStoreEvictionPolicy="FIFO"/>

    <!--
      name:缓存名称。
      maxElementsInMemory:缓存最大数目
      maxElementsOnDisk:硬盘最大缓存个数。
      eternal:对象是否永久有效,一但设置了,timeout将不起作用。
      overflowToDisk:是否保存到磁盘,当系统当机时
      timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
      timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
      diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
      diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
      diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
      memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
       clearOnFlush:内存数量最大时是否清除。
        memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
           FIFO,first in first out,这个是大家最熟的,先进先出。
           LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
           LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
   -->
</ehcache>

Springboot 整合 Shiro 完成。
内容如有帮助,记得点赞哦~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值