zxpnet网站 zxpnet网站
首页
前端
后端服务器
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

zxpnet

一个爱学习的java开发攻城狮
首页
前端
后端服务器
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 大后端课程视频归档
  • 南航面试题
  • 并发编程

  • 性能调优

  • java8语法

  • lombok

  • 日志

  • 工具类

  • spring

  • mybatis

  • springboot

  • redis

  • zookeeper

  • springcloud

  • dubbo

  • netty

  • springsecurity

  • mq消息中间件

  • shiro

    • shiro
      • 认证
      • 授权
      • Shiro 身份验证
        • 身份验证
      • 身份认证流程
        • shiro配置文件
        • 相关类:
        • 源码分析:
      • 自定义Realm
      • 使用MD5、盐salt 和散列
      • 授权方式
      • 权限字符串
      • 授权实现方式
      • 整合思路:
      • 整合JSP方式
      • 整合Shiro
        • shiro常见过滤器
        • 退出登陆
        • ShiroConfig
        • 自定义Realm: OAuth2Realm
        • 自定义Realm: LoginRealm
        • Controller上的权限注解
        • 工具类:ShiroUtils
        • SpringContextUtils
      • Shiro整合thymeleaf模板
      • Shiro优化
        • 1、设置ehcache缓存
        • 2、设置redis缓存
        • 3、加入验证码
        • 4、session管理
      • 六、相关项目
  • beetle

  • 模板引擎

  • jpa

  • 数据结构与算法

  • 数据库知识与设计

  • gradle

  • maven

  • bus

  • 定时任务

  • docker

  • centos

  • 加解密

  • biz业务

  • pigx项目

  • 开源项目

  • 品达通用权限项目-黑马

  • 货币交易项目coin-尚学堂

  • php

  • backend
  • shiro
shollin
2021-09-26
目录

shiro

视频地址:https://www.bilibili.com/video/BV1uz4y197Zm

# 一、权限相当的概念

# 认证

Authentication:用户身份识别,通常被称为用户“登录”

# 授权

Authorization:访问控制。比如某个用户是否具有某个操作的使用权限。

# 二、 Shiro及Shiro认证

官方文档: http://shiro.apache.org/documentation.html

源码下载

git clone https://github.com.cnpmjs.org/apache/shiro.git
git checkout shiro-root-1.8.0 -b shiro-root-1.8.0 #切换到最新的1.8分支上
1
2

img

Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;

SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authorizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;

SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);

SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密的。

# Shiro 身份验证

# 身份验证

身份验证,即在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。

在 shiro 中,用户需要提供principals(身份)和credentials(证明)给 shiro,从而应用能验证用户身份:

principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。

credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。

最常见的 principals 和 credentials 组合就是用户名 / 密码了。接下来先进行一个基本的身份认证。

另外两个相关的概念是之前提到的 Subject 及 Realm,分别是主体及验证主体的数据源。

简单理解: Subject为用户, pricipals 为用户名 credentials 为密码

# 身份认证流程

img

流程如下:

  1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
  2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
  3. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
  4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
  5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

# shiro配置文件

.ini (支持复杂数据格式),用来学习shiro书写我们系统中相关权限数据,Springboot中用不到

[users] 
zhang=123 wang=123
1
2

此处使用 ini 配置文件,通过 [users] 指定了两个主体:zhang/123、wang/123。

/**
 * MD5算法
 *
 * 1、不可逆,一般用作 加密 或 签名
 * 2、如果内容相同,无论执行多少次MD5算法生成结果都是一样的
 *
 * 生成结果:始终是一个16进制的长孺为32位的字符串
 *
 * @author passerbyYSQ
 * @create 2020-08-20 20:13
 */
public class Demo {

    public static void main(String[] args) {

        // ctrl + d 复制当前行
        // ctrl + y 删除当前行
        // ctrl + alt + l 格式化代码

        // 1. 全局的安全管理器
        DefaultSecurityManager securityManager = new DefaultSecurityManager();

        CustomizeMd5Realm realm = new CustomizeMd5Realm();
        //2. 设置 Realm 使用MD5算法的哈希凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("md5");
        credentialsMatcher.setHashIterations(1024);
        realm.setCredentialsMatcher(credentialsMatcher);

        // 设置 Realm(类似数据源)
//        securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//        securityManager.setRealm(new CustomizeRealm());
        securityManager.setRealm(realm);

        // 3.全局的安全工具类,设置安全管理器
        SecurityUtils.setSecurityManager(securityManager);

        // 4.拿到主体
        Subject subject = SecurityUtils.getSubject();

        //5. 创建令牌,模拟主体携带令牌访问
        UsernamePasswordToken token = new UsernamePasswordToken("ysq", "123");

        // 调用主体的方法,进行 认证
        // 认证不通过抛出对应异常
        // try - catch 快捷键 ctrl + alt + t
        try {
            // 是否已经认证
            System.out.println(subject.isAuthenticated());

            subject.login(token);

            System.out.println("登录成功: " + subject.isAuthenticated());
        } catch (UnknownAccountException e1) {
            // username 错误,会抛出 UnknownAccountException 异常
            e1.printStackTrace();
            System.out.println("用户名错误");

        } catch (IncorrectCredentialsException e2) {
            // password 错误,会抛出 IncorrectCredentialsException 异常
            // Credential: 凭证
            e2.printStackTrace();
            System.out.println("密码错误");
        }

//        catch (AuthenticationException e) {
//            e.printStackTrace();
//        }

        // 如果主体已经认证
        if (subject.isAuthenticated()) {
            // 基于单角色的权限控制
//            System.out.println(subject.hasRole("admin"));

            // 基于多角色的权限控制
            // 是否【同时】具有多种角色
//            System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));

            // 判断是否具有对应角色,返回一个boolean数组
//            boolean[] hasRole = subject.hasRoles(Arrays.asList("admin", "user", "super"));
//            for (boolean b : hasRole) {
//                System.out.println(b);
//            }

            // 基于权限字符串的访问控制
            // 资源标识符:操作:资源类型
            subject.isPermitted("user:update:01");

            // 判断是否具有对应权限

            // 判断是否【同时】具有多个权限
//            subject.isPermittedAll()
        }



    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

# 相关类:

DefaultSecurityManager IniRealm SecurityUtils Subject UsernamePasswordToken

异常:UnknownAccountException IncorrectCredentialsException AuthenticationException

DisabledAccountException(帐号被禁用)

LockedAccountException(帐号被锁定)

ExcessiveAttemptsException(登录失败次数过多)

ExpiredCredentialsException(凭证过期)

# 源码分析:

入口 方法 subject.login(token); DelegatingSubject --> securityManager.login(this, token); -->

AuthenticatingSecurityManager AbstractAuthenticator ModularRealmAuthenticator

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }
1
2
3
4
5
6
7
8
9

--> AuthenticationInfo info = realm.getAuthenticationInfo(token); AuthenticatingRealm --> SimpleAccountRealm SimpleAccount

// SimpleAccountRealm   认证的源码
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        SimpleAccount account = getUser(upToken.getUsername());

        if (account != null) {

            if (account.isLocked()) {
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }
            if (account.isCredentialsExpired()) {
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }

        }

        return account;
    }
 
 // 授权的源码
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = getUsername(principals);
        USERS_LOCK.readLock().lock();
        try {
            return this.users.get(username);
        } finally {
            USERS_LOCK.readLock().unlock();
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

认证:

最终执行用户名比较 ,在SimpleAccountRealm 的 doGetAuthenticationInfo()方法里面;

执行密码校验:AuthenticatingRealm assertCredentialsMatch()方法 ( 调用CredentialsMatcher doCredentialsMatch() ),由系统自动进行处理

自定义认证最核心的就是要自定义Realm, 参考 SimpleAccountRealm

AuthenticatingRealm 处理认证 AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)

AuthorizingRealm处理授权 AuthenticationInfo doGetAuthorizationInfo(PrincipalCollection principals)

# 自定义Realm

参考 SimpleAccountRealm ,将认证、授权数据的来源转为数据库的实现

/**
 * 自定义 Realm *
 * 将 认证 或 授权 的数据来源从 .ini 转到 数据库
 */
public class CustomizeRealm extends AuthorizingRealm {

    /**
     * Authorization 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * Authentication 认证
     * @param authenticationToken
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 在 token 种获取 username(身份信息)
        // principal 主角
        String principal = (String) authenticationToken.getPrincipal();
        System.out.println(principal);

        // 根据身份信息,通过 jdbc 从数据查询出 password
        if ("zs".equalsIgnoreCase(principal)) {

            // credential: 凭据、凭证(正确的,从数据库读取出来的)
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    principal, "123", this.getName()
            );
            return authenticationInfo;
        }

        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

相关类:

SimpleAuthenticationInfo AuthenticationInfo

# 使用MD5、盐salt 和散列

MD5算法

作用:一般用来加密或者签名校验(文件MD5值的比较)

特点:MD5算法不可逆,只能明文生成密文。如果内容相同,无论执行多少次MD5,生成的结果始终一致。 生成结果始终是一个16进制32位长度字符串

相关类: Md5Hash HashedCredentialsMatcher ByteSource.Util.bytes("x0~*Y")

// 定义自定义的Realm, 并使用MD5加盐凭证匹配器
CustomizeMd5Realm realm = new CustomizeMd5Realm();
// 设置 Realm 使用MD5算法的哈希凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5"); // 使用MD5算法
credentialsMatcher.setHashIterations(1024); // 散列次数
realm.setCredentialsMatcher(credentialsMatcher);

// md5 + 盐 + 散列
Md5Hash md5Hash = new Md5Hash("123", "x0~*Y", 1024);
// 转成16进制
String s = md5Hash.toHex();
1
2
3
4
5
6
7
8
9
10
11
12
// 参数1:用户名 参数2:md5+盐后的值 参数3:盐值 参数4:realm的名字
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
	principal, "d6206916641be56dc7eb24e04e3463a5",
	ByteSource.Util.bytes("x0~*Y"), // 注册时(数据库中存储)的随机盐
	this.getName()
);
1
2
3
4
5
6

# 三、Shiro授权

授权可简单理解为who对what(which)进行How操作:

  • Who,即主体(Subject),主体需要访问系统中的资源。

  • What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。

  • How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。

权限分为粗颗粒和细颗粒,粗颗粒权限是指对资源类型的权限,细颗粒权限是对资源实例的权限。

主体、资源、权限关系如下图:

img

# 授权方式

基于角色 RBAC (Role-Based Access Control)

基于权限 RBAC (Resource-Based Access Control)

# 权限字符串

在ini文件中用户、角色、权限的配置规则是:“用户名=密码,角色1,角色2...” “角色=权限1,权限2...”,首先根据用户名找角色,再根据角色找权限,角色是权限集合。

权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,**“:”**是资源/操作/实例的分割符,权限字符串也可以使用 * 通配符。

用户创建权限:user:create,或user:create:* 用户修改实例001的权限:user:update:001 用户实例001的所有权限:user:*:001

一般而已,我们操作只需要关注前面两节: 资源:操作 :
: : 所有资源的所有操作权限--->admin

# 授权实现方式

1、 编程式 subject.hasRole("")

2、注解式 @RequiresPermissions("sys:schedule:list") @RequiresRoles

3、标签式: jsp thymeleaf

/**
     * Authorization 授权
     * 该方法可能会被调用多次,如果每次都去查数据库,性能较低。应该考虑缓存
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 拿到主体的身份信息(username)
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();

        System.out.println("---");

        // 根据身份信息,从数据库查询其角色信息(权限信息)
        // 假设:ysq 是 admin, user
        if ("ysq".equals(primaryPrincipal)) {
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            authorizationInfo.addRole("admin");
            authorizationInfo.addRole("user");

            //authorizationInfo.addStringPermission("user:*:01");

            return authorizationInfo;
        }

        return null;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 四、Shiro与Springboot整合

# 整合思路:

资源分类: 公共资源、受限资源

将所有的请求交给Shiro进行处理,即过滤器 ShiroFilter ,通过调用SecurityManager去进行操作,认证通过就进入系统,认证通过再进行授权

# 整合JSP方式

1、搭建jsp环境

<!-- 引入jsp解析依赖 和C标签 -->
<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
1
2
3
4
5
6
7
8
9
10
11

application.properties

#jsp 支持
spring.mvc.view.suffix=.jsp
spring.mvc.view.prefix=/WEB-INF/jsp/
#spring.mvc.view.prefix=/
1
2
3
4

如果jsp在webapp目录下,需要在在idea中配置工作目录

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="ctx" value="${pageContext.request.contextPath}"/>
1
2
3
4

若打开jsp页面,浏览器变成了下载,则将maven重新刷新一下

标签名称 标签条件(均是显示标签内容)
shiro:authenticated 登录之后
shiro:notAuthenticated 不在登录状态时
shiro:guest 用户在没有RememberMe时
shiro:user 用户在RememberMe时
<shiro:hasAnyRoles name="abc,123" > 在有abc或者123角色时
<shiro:hasRole name="abc"> 拥有角色abc
<shiro:lacksRole name="abc"> 没有角色abc
<shiro:hasPermission name="abc"> 拥有权限资源abc
<shiro:lacksPermission name="abc"> 没有abc权限资源
shiro:principal 显示用户身份名称
<shiro:principal property="username"/> 显示用户身份中的属性值

# 整合Shiro

// 配置认证资源
        Map<String, String> filterMap = new HashMap<>();

        filterMap.put("/index.jsp","authc"); //

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
1
2
3
4
5
6

# shiro常见过滤器

anon:例子/admins/**=anon 没有参数,表示可以匿名使用。

authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,FormAuthenticationFilter是表单认证,没有参数

roles:例子/admins/user/=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。

perms:例子/admins/user/=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/=perms["user:add:,user:modify:"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。

rest:例子/admins/user/=rest[user],根据请求的方法,相当于/admins/user/=perms[user:method] ,其中method为post,get,delete等。

port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString

是你访问的url里的?后面的参数。

authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证

ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https

user:例如/admins/user/**=user没有参数表示必须存在用户, 身份认证通过或通过记住我认证通过的可以访问,当登入操作时不做检查

注:

anon,authcBasic,auchc,user是认证过滤器,

perms,roles,ssl,rest,port是授权过滤器

# 退出登陆

/**
     * 退出登录
     * 销毁主体的认证记录(信息),下次访问需要重新认证
     *
     * @return
     */
    @RequestMapping("/logout")
    public ResponseEntity<String> logout() {
        Subject subject = SecurityUtils.getSubject();

        User user = (User) subject.getPrincipal();
        userService.logout(user.getUsername());
        subject.logout();

        return ResponseEntity.ok().build();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# ShiroConfig

/**
 * Shiro配置
 *
 * @author Mark sunlightcs@gmail.com
 */
@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(oAuth2Realm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }

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

        //oauth过滤
        Map<String, Filter> filters = new HashMap<>();
        filters.put("oauth2", new OAuth2Filter());
        shiroFilter.setFilters(filters);

        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/app/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/aaa.txt", "anon");
        filterMap.put("/**", "oauth2");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

// Shiro生命周期处理器
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 自定义Realm: OAuth2Realm

@Component
public class OAuth2Realm extends AuthorizingRealm {

    @Autowired
    private ShiroService shiroService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof OAuth2Token;
    }

    /**
     * 授权(验证权限时调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
        Long userId = user.getUserId();

        //用户权限列表
        Set<String> permsSet = shiroService.getUserPermissions(userId);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permsSet);
        return info;
    }

    /**
     * 认证(登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String accessToken = (String) token.getPrincipal();

        //根据accessToken,查询用户信息
        SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken);
        //token失效
        if(tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()){
            throw new IncorrectCredentialsException("token失效,请重新登录");
        }

        //查询用户信息
        SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId());
        //账号锁定
        if(user.getStatus() == 0){
            throw new LockedAccountException("账号已被锁定,请联系管理员");
        }

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
        return info;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 自定义Realm: LoginRealm

public class LoginRealm extends AuthorizingRealm {

    /**
     * 或者在ShiroConfig中设置
     */
    public LoginRealm() {
        // 匹配器。需要与密码加密规则一致
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 设置匹配器的加密算法
        hashedCredentialsMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
        // 设置匹配器的哈希散列次数
        hashedCredentialsMatcher.setHashIterations(1024);
        // 将对应的匹配器设置到Realm中
        this.setCredentialsMatcher(hashedCredentialsMatcher);
    }

    /**
     * 可以往Shiro中注册多种Realm。某种Token对象需要对应的Realm来处理。
     * 复写该方法表示该方法支持处理哪一种Token
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取身份信息
        String primaryPrincipal = (String) principals.getPrimaryPrincipal();

        // 从容器中获取UserService组件
        UserService userService = (UserService) SpringContextUtils.getBean("userService");
        List<Role> roles = userService.getRolesByUsername(primaryPrincipal);

        if (!CollectionUtils.isEmpty(roles)) {
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            for (Role role : roles) {
//                System.out.println("---: " + role.getId() + "----" + role.getRoleName());
                authorizationInfo.addRole(role.getRoleName());

                // 查询当前角色的权限信息
                List<Permission> perms = userService.getPermsByRoleId(role.getId());
                if (!CollectionUtils.isEmpty(perms)) {
                    for (Permission perm : perms) {
                        authorizationInfo.addStringPermission(perm.getPermissionName());
                    }
                }

            }
            return authorizationInfo;
        }

        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 从Token中获取身份信息。这里实际上是username,这里从UsernamePasswordToken的源码可以看出
        String principal = (String) token.getPrincipal();
        // 从IOC容器中获取UserService组件
        UserService userService = (UserService) SpringContextUtils.getBean("userService");

        User user = userService.findByUsername(principal);

        if (!ObjectUtils.isEmpty(user)) {
            // 返回正确的信息(数据库存储的),作为比较的基准
            return  new SimpleAuthenticationInfo(
                    user, user.getPassword(),
                    ByteSource.Util.bytes(user.getSalt()), this.getName()
            );
        }

        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

# Controller上的权限注解

@RequiresRoles(value = {"admin","user"}) // 同时具有多个角色
@RequiresPermissions(value = {"user:update:*"}) // 权限字符串验证方式
1
2

# 工具类:ShiroUtils

/**
 * Shiro工具类
 *
 * @author Mark sunlightcs@gmail.com
 */
public class ShiroUtils {

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

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

	public static SysUserEntity getUserEntity() {
		return (SysUserEntity)SecurityUtils.getSubject().getPrincipal();
	}

	public static Long getUserId() {
		return getUserEntity().getUserId();
	}
	
	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 String getKaptcha(String key) {
		Object kaptcha = getSessionAttribute(key);
		if(kaptcha == null){
			throw new RRException("验证码已失效");
		}
		getSession().removeAttribute(key);
		return kaptcha.toString();
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# SpringContextUtils

/**
 * Spring Context 工具类
 *
 * @author Mark sunlightcs@gmail.com
 */
@Component
public class SpringContextUtils implements ApplicationContextAware {
	public static ApplicationContext applicationContext; 

	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		SpringContextUtils.applicationContext = applicationContext;
	}

	public static Object getBean(String name) {
		return applicationContext.getBean(name);
	}

	public static <T> T getBean(String name, Class<T> requiredType) {
		return applicationContext.getBean(name, requiredType);
	}

	public static boolean containsBean(String name) {
		return applicationContext.containsBean(name);
	}

	public static boolean isSingleton(String name) {
		return applicationContext.isSingleton(name);
	}

	public static Class<? extends Object> getType(String name) {
		return applicationContext.getType(name);
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# Shiro整合thymeleaf模板

<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>
1
2
3
4
5

引入命名空间

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="Thymeleaf" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
1
//    配置ShiroDialect:用于thymeleaf和shiro标签配合使用
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }
1
2
3
4
5

# Shiro优化

# 1、设置ehcache缓存

loginRealm.setCacheManager(ehCacheManager);
loginRealm.setCachingEnabled(true); // 开启全局缓存
loginRealm.setAuthenticationCachingEnabled(true); // 认证缓存
loginRealm.setAuthenticationCacheName("authenticationCache"); // 认证缓存名称
loginRealm.setAuthorizationCachingEnabled(true); // 授权缓存
loginRealm.setAuthorizationCacheName("authorizationCache"); // 授权缓存的名称
1
2
3
4
5
6

# 2、设置redis缓存

可以参考开源项目: https://github.com/alexxiyang/shiro-redis

思路: 参考 EhcacheManager 需实现 CacheManager (传入的是缓存名称,返回Cache的实现, 如 EhCache )

存在的问题: 盐的序列化问题(解决办法:自定义类实现ByteSource,参考SimpleByteSource, 实现序列化接口); RestTemplate中 key hashValue 中的Key的序列化问题

/**
 * Redis配置
 *
 * @author Mark sunlightcs@gmail.com
 */
@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory factory;

    @Bean
    @ConditionalOnClass(RedisOperations.class)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用 String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的 key也采用 String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用 jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的 value序列化方式采用 jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }

    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    @Bean
    public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# 3、加入验证码

  • 前端页面加上验证码, 并后台写上对应的action
  • 放行验证码
  • 在login的action中去处理验证码判断逻辑
/**
 * 生成验证码配置
 *
 * @author Mark sunlightcs@gmail.com
 */
@Configuration
public class KaptchaConfig {

    @Bean
    public DefaultKaptcha producer() {
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "black");
        properties.put("kaptcha.textproducer.char.space", "5");
        properties.put("kaptcha.textproducer.font.names", "Arial,Courier,cmr10,宋体,楷体,微软雅黑");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

写service

/**
 * 系统验证码
 *
 * @author Mark sunlightcs@gmail.com
 */
@Data
@TableName("sys_captcha")
public class SysCaptchaEntity {
    @TableId(type = IdType.INPUT)
    private String uuid;
    /**
     * 验证码
     */
    private String code;
    /**
     * 过期时间
     */
    private Date expireTime;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service("sysCaptchaService")
public class SysCaptchaServiceImpl extends ServiceImpl<SysCaptchaDao, SysCaptchaEntity> implements SysCaptchaService {
    @Autowired
    private Producer producer;

    @Override
    public BufferedImage getCaptcha(String uuid) {
        if(StringUtils.isBlank(uuid)){
            throw new RRException("uuid不能为空");
        }
        //生成文字验证码
        String code = producer.createText();

        SysCaptchaEntity captchaEntity = new SysCaptchaEntity();
        captchaEntity.setUuid(uuid);
        captchaEntity.setCode(code);
        //5分钟后过期
        captchaEntity.setExpireTime(DateUtils.addDateMinutes(new Date(), 5));
        this.save(captchaEntity);

        return producer.createImage(code);
    }

    @Override
    public boolean validate(String uuid, String code) {
        SysCaptchaEntity captchaEntity = this.getOne(new QueryWrapper<SysCaptchaEntity>().eq("uuid", uuid));
        if(captchaEntity == null){
            return false;
        }

        //删除验证码
        this.removeById(uuid);

        if(captchaEntity.getCode().equalsIgnoreCase(code) && captchaEntity.getExpireTime().getTime() >= System.currentTimeMillis()){
            return true;
        }

        return false;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

写action

/**
 * 登录相关
 *
 * @author Mark sunlightcs@gmail.com
 */
@RestController
public class SysLoginController extends AbstractController {
	@Autowired
	private SysUserService sysUserService;
	@Autowired
	private SysUserTokenService sysUserTokenService;
	@Autowired
	private SysCaptchaService sysCaptchaService;

	/**
	 * 验证码
	 */
	@GetMapping("captcha.jpg")
	public void captcha(HttpServletResponse response, String uuid)throws IOException {
		response.setHeader("Cache-Control", "no-store, no-cache");
		response.setContentType("image/jpeg");

		//获取图片验证码
		BufferedImage image = sysCaptchaService.getCaptcha(uuid);

		ServletOutputStream out = response.getOutputStream();
		ImageIO.write(image, "jpg", out);
		IOUtils.closeQuietly(out);
	}

	/**
	 * 登录
	 */
	@PostMapping("/sys/login")
	public Map<String, Object> login(@RequestBody SysLoginForm form)throws IOException {
		boolean captcha = sysCaptchaService.validate(form.getUuid(), form.getCaptcha());
		if(!captcha){
			return R.error("验证码不正确");
		}

		//用户信息
		SysUserEntity user = sysUserService.queryByUserName(form.getUsername());

		//账号不存在、密码错误
		if(user == null || !user.getPassword().equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex())) {
			return R.error("账号或密码不正确");
		}

		//账号锁定
		if(user.getStatus() == 0){
			return R.error("账号已被锁定,请联系管理员");
		}

		//生成token,并保存到数据库
		R r = sysUserTokenService.createToken(user.getUserId());
		return r;
	}


	/**
	 * 退出
	 */
	@PostMapping("/sys/logout")
	public R logout() {
		sysUserTokenService.logout(getUserId());
		return R.ok();
	}
	
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

# 4、session管理

Shiro的认证和授权是基于session实现的,Shiro包含了对session的管理

  • 自定义session管理器 DefaultWebSessionManager

  • 将自定义session管理器设置给SecurityManager

    可以在Service等全局地方访问 session里面的数据

5、RememberMe

RememberMeManager rememberMeManager = new CookieRememberMeManager();
1

6、多Realm配置

  • 将多个realm配置进ModularRealmAuthenticator, 再配置 securityManager.setAuthenticator(authenticator);

    Collection<Realm> realms = Lists.newArrayList();
            securityManager.setRealms(realms);
    ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
    authenticator.setRealms(realms);
    securityManager.setAuthenticator(authenticator);
    
    1
    2
    3
    4
    5
  • 配置认证策略

# 五、权限数据库表设计

# 六、相关项目

开涛学Shiro示例: https://github.com/zhangkaitao/shiro-example

activemq介绍
beetle基础与入门

← activemq介绍 beetle基础与入门→

最近更新
01
国际象棋
09-15
02
成语
09-15
03
自然拼读
09-15
更多文章>
Theme by Vdoing | Copyright © 2019-2023 zxpnet | 粤ICP备14079330号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式