Shiro学习笔记

一、主流框架简介

1.1 Shiro与SpringSecurity对比

shiro:

  • Shrio学习比较简单,可以用简单的操作实现复杂的权限管理
  • 与Spring Security相比,Shiro更加主流、也更加简单易用,它不但是适用于javaSE环境,也适用于javaEE环境
  • 易于理解的 Java Security API;
  • 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
  • 对角色的简单的签权(访问控制),支持细粒度的签权;
  • 支持一级缓存,以提升应用程序的性能;
  • 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
  • 异构客户端会话访问;
  • 非常简单的加密 API;
  • 不跟任何的框架或者容器捆绑,可以独立运行。

SpringSecurity

  • 充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

区别

  • hiro比Spring更容易使用,实现和最重要的理解
  • Spring Security更加知名的唯一原因是因为品牌名称
  • “Spring”以简单而闻名,但讽刺的是很多人发现安装Spring Security很难,然而,Spring Security却有更好的社区支持
  • Apache Shiro在Spring Security处理密码学方面有一个额外的模块
  • Spring-security 对spring 结合较好,如果项目用的springmvc ,使用起来很方便。但是如果项目中没有用到spring,那就不要考虑它了。
  • Shiro 功能强大、且 简单、灵活。是Apache 下的项目比较可靠,且不跟任何的框架或者容器绑定,可以独立运行

二、Shiro简介

Shiro是apache下的一个开源框架,将软件系统的安全认证相关的功能抽取出来,实现用户身份认证、权限授权、加密、会话管理等功能。

2.1核心架构

img

2.2 认证

身份认证:判断一个用户是否为合法用户的处理过程。

2.2.1认证的关键对象

  • Subject 主体:访问系统的用户。进行认证的都是主体。
  • Principal 身份信息:主体进行身份认证的标识,必须具有唯一性。一个主体可以有多个身份,但是只能有一个主身份
  • credential 凭证信息:只有主体自己知道的安全信息,如密码,证书等。

2.2.2 认证的开发

  • 引入依赖
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
  • 引入配置

配置文件为 .ini文件,可以写一些复杂数据格式

2.2.3 quickstart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {

//创建安全管理器
DefaultSecurityManager securityManager=new DefaultSecurityManager();
//给安全管理器设置realm
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//securityUtils给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//关键对象subject主体
Subject subject = SecurityUtils.getSubject();
//创建token
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123");
try {
System.out.println(subject.isAuthenticated());
subject.login(token);
System.out.println(subject.isAuthenticated());
System.out.println("token:"+token);
}catch (Exception e){
e.printStackTrace();
}
}

2.2.4 认证流程源码查看

  • 用户名校验:SimpleAccountRealm中的doGetAuthenticationInfo方法根据token完成校验。封装得非常深

  • 密码校验:AuthenticatingRealm中的assertCredentialMatch。由Shiro自动校验。

  • AuthenticatingRealm 用来认证Realm->doGetAuthenticationInfo

  • AuthorizingRealm 用来授权Realm->doGetAuthorizationInfo

SimpleAccountRealm有doGetAuthenticationInfo和doGetAuthorizationInfo两个方法,继承了AuthorizingRealm。

如果我们需要自己设计认证和授权的方式,自定义Realm,只需要继承AuthorizingRealm然后重写两个方法就行了。

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
public class SimpleAccountRealm extends AuthorizingRealm {

//TODO - complete JavaDoc
protected final Map<String, SimpleAccount> users; //username-to-SimpleAccount
protected final Map<String, SimpleRole> roles; //roleName-to-SimpleRole
protected final ReadWriteLock USERS_LOCK;
protected final ReadWriteLock ROLES_LOCK;

public SimpleAccountRealm() {
this.users = new LinkedHashMap<String, SimpleAccount>();
this.roles = new LinkedHashMap<String, SimpleRole>();
USERS_LOCK = new ReentrantReadWriteLock();
ROLES_LOCK = new ReentrantReadWriteLock();
//SimpleAccountRealms are memory-only realms - no need for an additional cache mechanism since we're
//already as memory-efficient as we can be:
setCachingEnabled(false);
}

public SimpleAccountRealm(String name) {
this();
setName(name);
}

protected SimpleAccount getUser(String username) {
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}

public boolean accountExists(String username) {
return getUser(username) != null;
}

public void addAccount(String username, String password) {
addAccount(username, password, (String[]) null);
}

public void addAccount(String username, String password, String... roles) {
Set<String> roleNames = CollectionUtils.asSet(roles);
SimpleAccount account = new SimpleAccount(username, password, getName(), roleNames, null);
add(account);
}

protected String getUsername(SimpleAccount account) {
return getUsername(account.getPrincipals());
}

protected String getUsername(PrincipalCollection principals) {
return getAvailablePrincipal(principals).toString();
}

protected void add(SimpleAccount account) {
String username = getUsername(account);
USERS_LOCK.writeLock().lock();
try {
this.users.put(username, account);
} finally {
USERS_LOCK.writeLock().unlock();
}
}

protected SimpleRole getRole(String rolename) {
ROLES_LOCK.readLock().lock();
try {
return roles.get(rolename);
} finally {
ROLES_LOCK.readLock().unlock();
}
}

public boolean roleExists(String name) {
return getRole(name) != null;
}

public void addRole(String name) {
add(new SimpleRole(name));
}

protected void add(SimpleRole role) {
ROLES_LOCK.writeLock().lock();
try {
roles.put(role.getName(), role);
} finally {
ROLES_LOCK.writeLock().unlock();
}
}

protected static Set<String> toSet(String delimited, String delimiter) {
if (delimited == null || delimited.trim().equals("")) {
return null;
}

Set<String> values = new HashSet<String>();
String[] rolenamesArray = delimited.split(delimiter);
for (String s : rolenamesArray) {
String trimmed = s.trim();
if (trimmed.length() > 0) {
values.add(trimmed);
}
}

return values;
}

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();
}
}
}

三、Shiro工作原理

  • Anthentication 认证,验证用户是否有相应的身份—登录认证;
  • Authorization 授权,即权限验证;对已经通过认证的用户检查是否具有某个权限或者角色,从而控制是否能够进行某种操作;
  • Session Managment 会话管理,用户在认证成功之后创建会话,在没有退出之前,当前用户的所有信息都会保存在这个会话中;可以是普通的JavaSE应用,也可以是web应用;
  • Cryptography 加密,对敏感信息进行加密处理,shiro就提供这种加密机制;
  • 支持的特性:
    • Web Support — Shiro提供了过滤器,可以通过过滤器拦截web请求来处理web应用的访问控制
    • Caching 缓存支持,shiro可以缓存用户信息以及用户的角色权限信息,可以提高执行效率
    • Concurrency shiro支持多线程应用
    • Testing 提供测试支持
    • Run As 允许一个用户以另一种身份去访问
    • Remeber Me
  • 说明:Shiro是一个安全框架,不提供用户、权限的维护(用户的权限管理需要我们自己去设计)

四、Shiro整合Springboot

  • JavaSE应用中使用
  • web应用中使用
    • SSM整合Shiro(配置多,用的少)
    • SpringBoot应用整合Shiro

4.1 创建SpringBoot应用

  • lombok
  • spring web
  • thymeleaf

4.2 整合Druid和MyBatis

  • 依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!-- druid starter -->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
    </dependency>
    <!--mysql -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
    </dependency>
    <!-- mybatis -->
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.0</version>
    </dependency>
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    spring:
    datasource:
    druid:
    url: jdbc:mysql://localhost:33006/tmp
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    initial-size: 1
    min-idle: 1
    max-active: 20
    mybatis:
    mapper-locations: classpath:mappers/*Mapper.xml
    type-aliases-package: top.retain.shirodemo.beans

2.3 整合Shiro

  • 导入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.1</version>
    </dependency>
  • Shiro配置(java配置方式)

    • SpringBoot默认没有提供对Shiro的自动配置
    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
    @Configuration
    public class ShiroConfig {

    @Bean
    public IniRealm getIniRealm(){
    IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
    return iniRealm;
    }

    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(IniRealm iniRealm){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    //securityManager要完成校验,需要realm
    securityManager.setRealm(iniRealm);
    return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
    //过滤器就是shiro就行权限校验的核心,进行认证和授权是需要SecurityManager的
    filter.setSecurityManager(securityManager);

    //设置shiro的拦截规则
    // anon 匿名用户可访问
    // authc 认证用户可访问
    // user 使用RemeberMe的用户可访问
    // perms 对应权限可访问
    // role 对应的角色可访问
    Map<String,String> filterMap = new HashMap<>();
    filterMap.put("/","anon");
    filterMap.put("/login.html","anon");
    filterMap.put("/regist.html","anon");
    filterMap.put("/user/login","anon");
    filterMap.put("/user/regist","anon");
    filterMap.put("/static/**","anon");
    filterMap.put("/**","authc");

    filter.setFilterChainDefinitionMap(filterMap);
    filter.setLoginUrl("/login.html");
    //设置未授权访问的页面路径
    filter.setUnauthorizedUrl("/login.html");
    return filter;
    }


    }
  • 认证测试

    • UserServiceImpl.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Service
    public class UserServiceImpl {

    public void checkLogin(String userName,String userPwd) throws Exception{
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(userName,userPwd);
    subject.login(token);
    }

    }
    • UserController.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Controller
    @RequestMapping("user")
    public class UserController {

    @Resource
    private UserServiceImpl userService;

    @RequestMapping("login")
    public String login(String userName,String userPwd){
    try {
    userService.checkLogin(userName,userPwd);
    System.out.println("------登录成功!");
    return "index";
    } catch (Exception e) {
    System.out.println("------登录失败!");
    return "login";
    }

    }
    }
  • login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
login
<hr/>
<form action="user/login">
<p>帐号:<input type="text" name="userName"/></p>
<p>密码:<input type="text" name="userPwd"/></p>
<p><input type="submit" value="登录"/></p>
</form>
</body>
</html>

4.2 SpringBoot整合JdbcRealm

4.2.1 JdbcRealm规定的表结构

  • 用户信息表: users

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    create table users(
    id int primary key auto_increment,
    username varchar(60) not null unique,
    password varchar(20) not null,
    password_salt varchar(20)
    );

    insert into users(username,password) values('zhangsan','123456');
    insert into users(username,password) values('lisi','123456');
    insert into users(username,password) values('wangwu','123456');
    insert into users(username,password) values('zhaoliu','123456');
    insert into users(username,password) values('chenqi','123456');
  • 角色信息表: user_roles

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    create table user_roles(
    id int primary key auto_increment,
    username varchar(60) not null,
    role_name varchar(100) not null
    );

    -- admin系统管理员
    -- cmanager 库管人员
    -- xmanager 销售人员
    -- kmanager 客服人员
    -- zmanager 行政人员
    insert into user_roles(username,role_name) values('zhangsan','admin');
    insert into user_roles(username,role_name) values('lisi','cmanager');
    insert into user_roles(username,role_name) values('wangwu','xmanager');
    insert into user_roles(username,role_name) values('zhaoliu','kmanager');
    insert into user_roles(username,role_name) values('chenqi','zmanager');

  • 权限信息表:roles_permissions

    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
    create table roles_permissions(
    id int primary key auto_increment,
    role_name varchar(100) not null,
    permission varchar(100) not null
    );

    -- 权限 sys:c:save sys:c:delete...
    -- 管理员具备所有权限
    insert into roles_permissions(role_name,permission) values("admin","*");
    -- 库管人员
    insert into roles_permissions(role_name,permission) values("cmanager","sys:c:save");
    insert into roles_permissions(role_name,permission) values("cmanager","sys:c:delete");
    insert into roles_permissions(role_name,permission) values("cmanager","sys:c:update");
    insert into roles_permissions(role_name,permission) values("cmanager","sys:c:find");
    -- 销售人员
    insert into roles_permissions(role_name,permission) values("xmanager","sys:c:find");
    insert into roles_permissions(role_name,permission) values("xmanager","sys:x:save");
    insert into roles_permissions(role_name,permission) values("xmanager","sys:x:delete");
    insert into roles_permissions(role_name,permission) values("xmanager","sys:x:update");
    insert into roles_permissions(role_name,permission) values("xmanager","sys:x:find");

    insert into roles_permissions(role_name,permission) values("xmanager","sys:k:save");
    insert into roles_permissions(role_name,permission) values("xmanager","sys:k:delete");
    insert into roles_permissions(role_name,permission) values("xmanager","sys:k:update");
    insert into roles_permissions(role_name,permission) values("xmanager","sys:k:find");
    -- 客服人员
    insert into roles_permissions(role_name,permission) values("kmanager","sys:k:find");
    insert into roles_permissions(role_name,permission) values("kmanager","sys:k:update");
    -- 新增人员
    insert into roles_permissions(role_name,permission) values("zmanager","sys:*:find");

4.2.2 SpringBoot整合Shiro

  • 创建SpringBoot应用

  • 整合Druid和MyBatis

  • 整合shiro

    • 添加依赖

      1
      2
      3
      4
      5
      <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>1.4.1</version>
      </dependency>
    • 配置Shiro

      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
      @Configuration
      public class ShiroConfig {

      @Bean
      public JdbcRealm getJdbcRealm(DataSource dataSource){
      JdbcRealm jdbcRealm = new JdbcRealm();
      //JdbcRealm会自行从数据库查询用户及权限数据(数据库的表结构要符合JdbcRealm的规范)
      jdbcRealm.setDataSource(dataSource);
      //JdbcRealm默认开启认证功能,需要手动开启授权功能
      jdbcRealm.setPermissionsLookupEnabled(true);
      return jdbcRealm;
      }

      @Bean
      public DefaultWebSecurityManager getDefaultWebSecurityManager(JdbcRealm jdbcRealm){
      DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
      securityManager.setRealm(jdbcRealm);
      return securityManager;
      }

      @Bean
      public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){
      ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
      //过滤器就是shiro就行权限校验的核心,进行认证和授权是需要SecurityManager的
      filter.setSecurityManager(securityManager);

      Map<String,String> filterMap = new HashMap<>();
      filterMap.put("/","anon");
      filterMap.put("/login","anon");
      filterMap.put("/register","anon");
      filterMap.put("/user/login","anon");
      filterMap.put("/user/register","anon");
      filterMap.put("/static/**","anon");
      filterMap.put("/**","authc");

      filter.setFilterChainDefinitionMap(filterMap);
      filter.setLoginUrl("/login.html");
      //设置未授权访问的页面路径
      filter.setUnauthorizedUrl("/login.html");
      return filter;
      }

      }

4.3 自定义Realm

使用JdbcRealm可以完成用户权限管理,但是我们必须提供JdbcRealm规定的数据表结构;如果在我们的项目开发中 ,这个JdbcRealm规定的数据表结构不能满足开发需求,该如何处理呢?

  • 自定义数据库表结构
  • 自定义Realm实现认证和授权

4.3.1 数据库设计

  • RBAC基于角色的访问控制

    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
    -- 用户信息表
    create table tb_users(
    user_id int primary key auto_increment,
    username varchar(60) not null unique,
    password varchar(20) not null,
    password_salt varchar(60)
    );

    insert into tb_users(username,password) values('zhangsan','123456');
    insert into tb_users(username,password) values('lisi','123456');
    insert into tb_users(username,password) values('wangwu','123456');
    insert into tb_users(username,password) values('zhaoliu','123456');
    insert into tb_users(username,password) values('chenqi','123456');

    -- 角色信息表
    create table tb_roles(
    role_id int primary key auto_increment,
    role_name varchar(60) not null
    );

    insert into tb_roles(role_name) values('admin');
    insert into tb_roles(role_name) values('cmanager'); -- 仓管
    insert into tb_roles(role_name) values('xmanager'); -- 销售
    insert into tb_roles(role_name) values('kmanager'); -- 客服
    insert into tb_roles(role_name) values('zmanager'); -- 行政

    -- 权限信息表
    create table tb_permissions(
    permission_id int primary key auto_increment, -- 1
    permission_code varchar(60) not null, -- sys:c:find
    permission_name varchar(60) -- 仓库查询
    );
    insert into tb_permissions(permission_code,permission_name) values('sys:c:save','入库');
    insert into tb_permissions(permission_code,permission_name) values('sys:c:delete','出库');
    insert into tb_permissions(permission_code,permission_name) values('sys:c:update','修改');
    insert into tb_permissions(permission_code,permission_name) values('sys:c:find','查询');

    insert into tb_permissions(permission_code,permission_name) values('sys:x:save','新增订单');
    insert into tb_permissions(permission_code,permission_name) values('sys:x:delete','删除订单');
    insert into tb_permissions(permission_code,permission_name) values('sys:x:update','修改订单');
    insert into tb_permissions(permission_code,permission_name) values('sys:x:find','查询订单');


    insert into tb_permissions(permission_code,permission_name) values('sys:k:save','新增客户');
    insert into tb_permissions(permission_code,permission_name) values('sys:k:delete','删除客户');
    insert into tb_permissions(permission_code,permission_name) values('sys:k:update','修改客户');
    insert into tb_permissions(permission_code,permission_name) values('sys:k:find','查询客户');

    -- 用户角色表
    create table tb_urs(
    uid int not null,
    rid int not null
    -- primary key(uid,rid),
    -- constraint FK_user foreign key(uid) references tb_users(user_id),
    -- constraint FK_role foreign key(rid) references tb_roles(role_id)
    );
    insert into tb_urs(uid,rid) values(1,1);
    insert into tb_urs(uid,rid) values(1,2);
    insert into tb_urs(uid,rid) values(1,3);
    insert into tb_urs(uid,rid) values(1,4);
    insert into tb_urs(uid,rid) values(1,5);

    insert into tb_urs(uid,rid) values(2,2);
    insert into tb_urs(uid,rid) values(3,3);
    insert into tb_urs(uid,rid) values(4,4);
    insert into tb_urs(uid,rid) values(5,5);

    -- 角色权限表
    create table tb_rps(
    rid int not null,
    pid int not null
    );
    -- 给仓管角色分配权限
    insert into tb_rps(rid,pid) values(2,1);
    insert into tb_rps(rid,pid) values(2,2);
    insert into tb_rps(rid,pid) values(2,3);
    insert into tb_rps(rid,pid) values(2,4);
    -- 给销售角色分配权限
    insert into tb_rps(rid,pid) values(3,4);
    insert into tb_rps(rid,pid) values(3,5);
    insert into tb_rps(rid,pid) values(3,6);
    insert into tb_rps(rid,pid) values(3,7);
    insert into tb_rps(rid,pid) values(3,8);
    insert into tb_rps(rid,pid) values(3,9);
    insert into tb_rps(rid,pid) values(3,10);
    insert into tb_rps(rid,pid) values(3,11);
    insert into tb_rps(rid,pid) values(3,12);
    -- 给客服角色分配权限
    insert into tb_rps(rid,pid) values(4,11);
    insert into tb_rps(rid,pid) values(4,12);
    -- 给行政角色分配权限
    insert into tb_rps(rid,pid) values(5,4);
    insert into tb_rps(rid,pid) values(5,8);
    insert into tb_rps(rid,pid) values(5,12);

4.3.2 DAO实现

  • Shiro进行认证需要用户信息:

    • 根据用户名查询用户信息
  • Shiro进行授权管理需要当前用户的角色和权限

    • 根据用户名查询当前用户的角色列表(3张表连接查询)

    • 根据用户名查询当前用户的权限列表(5张表连接查询)

4.3.2.1 创建SpringBoot项目,整合MyBatis
4.3.2.2 根据用户名查询用户信息
  • 创建BeanBean
1
2
3
4
5
6
7
@Data
public class User {
private Integer userId;
private String userName;
private String userPwd;
private String pwdSalt;
}
  • 创建DAO
1
2
3
public interface UserDAO {
public User queryUserByUsername(String username) throws Exception;
}
  • 映射配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qfedu.shiro4.dao.UserDAO">

<resultMap id="userMap" type="User">
<id column="user_id" property="userId"></id>
<result column="username" property="userName"/>
<result column="password" property="userPwd"/>
<result column="password_salt" property="pwdSalt"/>
</resultMap>

<select id="queryUserByUsername" resultMap="userMap">
select * from tb_users
where username=#{username}
</select>

</mapper>
4.3.2.3 根据用户名查询角色名列表
  • 创建DAO
1
2
3
public interface RoleDAO {
public Set<String> queryRoleNamesByUsername(String username) throws Exception;
}
  • 映射配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qfedu.shiro4.dao.RoleDAO">

<select id="queryRoleNamesByUsername" resultSets="java.util.Set" resultType="string">
select role_name
from tb_users inner join tb_urs
on tb_users.user_id = tb_urs.uid
inner join tb_roles
on tb_urs.rid = tb_roles.role_id
where tb_users.username=#{username}
</select>

</mapper>
4.3.2.4 根据用户名查询权限列表
  • 创建DAO
1
2
3
public interface PermissionDAO {
public Set<String> queryPermissionsByUsername(String username) throws Exception;
}
  • 映射配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qfedu.shiro4.dao.PermissionDAO">

<select id="queryPermissionsByUsername" resultSets="java.util.Set" resultType="string">
select tb_permissions.permission_code from tb_users
inner join tb_urs on tb_users.user_id=tb_urs.uid
inner join tb_roles on tb_urs.rid=tb_roles.role_id
inner join tb_rps on tb_roles.role_id=tb_rps.rid
inner join tb_permissions on tb_rps.pid=tb_permissions.permission_id
where tb_users.username=#{username}
</select>

</mapper>

4.3.3 整合Shiro

  • 导入依赖
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
  • 配置Shiro-基于Java配置方式
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
@Configuration
public class ShiroConfig {

@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}

//自定义Realm
@Bean
public MyRealm getMyRealm(){
MyRealm myRealm = new MyRealm();
return myRealm;
}

@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm myRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
return securityManager;
}

@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
//过滤器就是shiro就行权限校验的核心,进行认证和授权是需要SecurityManager的
filter.setSecurityManager(securityManager);

Map<String,String> filterMap = new HashMap<>();
filterMap.put("/","anon");
filterMap.put("/index.html","anon");
filterMap.put("/login.html","anon");
filterMap.put("/regist.html","anon");
filterMap.put("/user/login","anon");
filterMap.put("/user/regist","anon");
filterMap.put("/layui/**","anon");
filterMap.put("/**","authc");

filter.setFilterChainDefinitionMap(filterMap);
filter.setLoginUrl("/login.html");
//设置未授权访问的页面路径()
filter.setUnauthorizedUrl("/login.html");
return filter;
}

}

  • 自定义Realm
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
/**
* 1.创建一个类继承AuthorizingRealm类(实现了Realm接口的类)
* 2.重写doGetAuthorizationInfo和doGetAuthenticationInfo方法
* 3.重写getName方法返回当前realm的一个自定义名称
*/
public class MyRealm extends AuthorizingRealm {

@Resource
private UserDAO userDAO;
@Resource
private RoleDAO roleDAO;
@Resource
private PermissionDAO permissionDAO;

public String getName() {
return "myRealm";
}

/**
* 获取授权数据(将当前用户的角色及权限信息查询出来)
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取用户的用户名
String username = (String) principalCollection.iterator().next();
//根据用户名查询当前用户的角色列表
Set<String> roleNames = roleDAO.queryRoleNamesByUsername(username);
//根据用户名查询当前用户的权限列表
Set<String> ps = permissionDAO.queryPermissionsByUsername(username);

SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roleNames);
info.setStringPermissions(ps);
return info;
}

/**
* 获取认证的安全数据(从数据库查询的用户的正确数据)
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//参数authenticationToken就是传递的 subject.login(token)
// 从token中获取用户名
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
//根据用户名,从数据库查询当前用户的安全数据
User user = userDAO.queryUserByUsername(username);

AuthenticationInfo info = new SimpleAuthenticationInfo(
username, //当前用户用户名
user.getUserPwd(), //从数据库查询出来的安全密码
getName());

return info;
}
}

五、Shiro标签的使用

当用户认证进入到主页面之后,需要显示用户信息及当前用户的权限信息;Shiro就提供了一套标签用于在页面来进行权限数据的呈现

  • Shiro提供了可供JSP使用的标签以及Thymeleaf中标签

    • JSP页面中引用:

      1
      <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
    • Thymeleaf模版中引用:

      • 在pom.xml文件中导入thymeleaf模版对shiro标签支持的依赖
      1
      2
      3
      4
      5
      <dependency>
      <groupId>com.github.theborakompanioni</groupId>
      <artifactId>thymeleaf-extras-shiro</artifactId>
      <version>2.0.0</version>
      </dependency>
      • 在ShiroConfig中配置Shiro的
      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Configuration
      public class ShiroConfig {

      @Bean
      public ShiroDialect getShiroDialect(){
      return new ShiroDialect();
      }
      //...
      }
      • Thymeleaf模版中引入shiro的命名空间
      1
      2
      3
      4
      <html xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
      ...
      </html>

5.1 常用标签

  • guest,判断用户是否是游客身份,如果是游客身份则显示此标签内容

    1
    2
    3
    <shiro:guest>
    欢迎游客访问,<a href="login.html">登录</a>
    </shiro:guest>
  • user,判断用户是否是认证身份,如果是认证身份则显示此标签内容

  • principal,获取当前登录用户名

    1
    2
    3
    <shiro:user>
    用户[<shiro:principal/>]欢迎您!
    </shiro:user>
  • notAuthenticated/authenticated

  • hasRole

  • hasPermission

    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
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org"
    xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    index
    <hr/>
    <shiro:guest>
    欢迎游客访问,<a href="login.html">登录</a>
    </shiro:guest>
    <shiro:user>
    用户[<shiro:principal/>]欢迎您!
    当前用户为<shiro:hasRole name="admin">超级管理员</shiro:hasRole>
    <shiro:hasRole name="cmanager">仓管人员</shiro:hasRole>
    <shiro:hasRole name="xmanager">销售人员</shiro:hasRole>
    <shiro:hasRole name="kmanager">客服人员</shiro:hasRole>
    <shiro:hasRole name="zmanager">行政人员</shiro:hasRole>
    </shiro:user>

    <hr/>
    仓库管理
    <ul>
    <shiro:hasPermission name="sys:c:save"><li><a href="#">入库</a></li></shiro:hasPermission>
    <shiro:hasPermission name="sys:c:delete"><li><a href="#">出库</a></li></shiro:hasPermission>
    <shiro:hasPermission name="sys:c:update"><li><a href="#">修改</a></li></shiro:hasPermission>
    <shiro:hasPermission name="sys:c:find"><li><a href="#">查询</a></li></shiro:hasPermission>
    </ul>

    订单管理
    <ul>
    <shiro:hasPermission name="sys:x:save"><li><a href="#">添加订单</a></li></shiro:hasPermission>
    <shiro:hasPermission name="sys:x:delete"><li><a href="#">删除订单</a></li></shiro:hasPermission>
    <shiro:hasPermission name="sys:x:update"><li><a href="#">修改订单</a></li></shiro:hasPermission>
    <shiro:hasPermission name="sys:x:find"><li><a href="#">查询订单</a></li></shiro:hasPermission>
    </ul>

    客户管理
    <ul>
    <shiro:hasPermission name="sys:k:save"><li><a href="#">添加客户</a></li></shiro:hasPermission>
    <shiro:hasPermission name="sys:k:delete"><li><a href="#">删除客户</a></li></shiro:hasPermission>
    <shiro:hasPermission name="sys:k:update"><li><a href="#">修改客户</a></li></shiro:hasPermission>
    <shiro:hasPermission name="sys:k:find"><li><a href="#">查询客户</a></li></shiro:hasPermission>
    </ul>


    </body>
    </html>

六、密码加密加盐

  • 明文-----(加密规则)-----密文
  • 加密规则可以自定义,在项目开发中我们通常使用BASE64和MD5编码方式
    • BASE64:可反编码的编码方式(对称)
      • 明文----密文
      • 密文----明文
    • MD5: 不可逆的编码方式(非对称)
      • 明文----密文
  • 如果数据库用户的密码存储的密文,Shiro该如何完成验证呢?
  • 使用Shiro提供的加密功能,对输入的密码进行加密之后再进行认证。

6.1 Shiro使用加密认证

  • 配置matcher

    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
    @Configuration
    public class ShiroConfig {

    //...
    @Bean
    public HashedCredentialsMatcher getHashedCredentialsMatcher(){
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    //matcher就是用来指定加密规则
    //加密方式
    matcher.setHashAlgorithmName("md5");
    //hash次数
    matcher.setHashIterations(1); //此处的循环次数要与用户注册是密码加密次数一致
    return matcher;
    }

    //自定义Realm
    @Bean
    public MyRealm getMyRealm( HashedCredentialsMatcher matcher ){
    MyRealm myRealm = new MyRealm();
    myRealm.setCredentialsMatcher(matcher);
    return myRealm;
    }

    //...
    }

6.2 用户注册密码加密处理

  • registh.html

    1
    2
    3
    4
    5
    <form action="/user/regist" method="post">
    <p>帐号:<input type="text" name="userName"/></p>
    <p>密码:<input type="text" name="userPwd"/></p>
    <p><input type="submit" value="提交注册"/></p>
    </form>
  • UserController

    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
    @Controller
    @RequestMapping("user")
    public class UserController {

    @Resource
    private UserServiceImpl userService;



    @RequestMapping("/regist")
    public String regist(String userName,String userPwd) {
    System.out.println("------注册");

    //注册的时候要对密码进行加密存储
    Md5Hash md5Hash = new Md5Hash(userPwd);
    System.out.println("--->>>"+ md5Hash.toHex());

    //加盐加密
    int num = new Random().nextInt(90000)+10000; //10000—99999
    String salt = num+"";
    Md5Hash md5Hash2 = new Md5Hash(userPwd,salt);
    System.out.println("--->>>"+md5Hash2);

    //加盐加密+多次hash
    Md5Hash md5Hash3 = new Md5Hash(userPwd,salt,3);
    System.out.println("--->>>"+md5Hash3);

    //SimpleHash hash = new SimpleHash("md5",userPwd,num,3);

    //将用户信息保存到数据库时,保存加密后的密码,如果生成的随机盐,盐也要保存

    return "login";
    }

    }

6.3 如果密码进行了加盐处理,则Realm在返回认证数据时需要返回盐

  • 在自定义Realm中:

    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
    public class MyRealm extends AuthorizingRealm {

    @Resource
    private UserDAO userDAO;
    @Resource
    private RoleDAO roleDAO;
    @Resource
    private PermissionDAO permissionDAO;

    public String getName() {
    return "myRealm";
    }

    /**
    * 获取认证的安全数据(从数据库查询的用户的正确数据)
    */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //参数authenticationToken就是传递的 subject.login(token)
    // 从token中获取用户名
    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    String username = token.getUsername();
    //根据用户名,从数据库查询当前用户的安全数据
    User user = userDAO.queryUserByUsername(username);

    // AuthenticationInfo info = new SimpleAuthenticationInfo(
    // username, //当前用户用户名
    // user.getUserPwd(), //从数据库查询出来的安全密码
    // getName());

    //如果数据库中用户的密码是加了盐的
    AuthenticationInfo info = new SimpleAuthenticationInfo(
    username, //当前用户用户名
    user.getUserPwd(), //从数据库查询出来的安全密码
    ByteSource.Util.bytes(user.getPwdSalt()),
    getName());

    return info;
    }
    }

七、退出登录

  • 在Shiro过滤器中进行配置,配置logout对应的路径

    1
    filterMap.put("/exit","logout");
  • 在页面的“退出”按钮上,跳转到logout对应的url

    1
    <a href="exit">退出</a>

八、授权

用户登录成功之后,要进行响应的操作就需要有对应的权限;在进行操作之前对权限进行检查—授权

权限控制通常有两类做法:

  • 不同身份的用户登录,我们现在不同的操作菜单(没有权限的菜单不现实)
  • 对所有用户显示所有菜单,当用户点击菜单以后再验证当前用户是否有此权限,如果没有则提示权限不足

8.1 HTML授权

  • 在菜单页面只显示当前用户拥有权限操作的菜单

  • shiro标签

    1
    2
    3
    <shiro:hasPermission name="sys:c:save">
    <dd><a href="javascript:;">入库</a></dd>
    </shiro:hasPermission>

    但是如果用户直接访问路径,还是可以访问到页面,权限控制不够完整,因此需要添加过滤器授权

8.2 过滤器授权

  • 在shiro过滤器中对请求的url进行权限设置

    1
    2
    3
    4
    filterMap.put("/c_add.html","perms[sys:c:save]");

    //设置未授权访问的页面路径—当权限不足时显示此页面
    filter.setUnauthorizedUrl("/lesspermission.html");

8.3 注解授权

  • 配置Spring对Shiro注解的支持:ShiroConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * 配置shiro的注解能够得到加载和执行
    * @return
    */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
    DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    autoProxyCreator.setProxyTargetClass(true);
    return autoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager){
    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(securityManager);
    // 注解的加载器,让shiro解析@RequirePermissions等注解,Spring无法解析
    return advisor;
    }
  • 在请求的控制器添加权限注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Controller
    @RequestMapping("customer")
    public class CustomerController {

    @RequestMapping("list")
    //如果没有 sys:k:find 权限,则不允许执行此方法
    @RequiresPermissions("sys:k:find")
    // @RequiresRoles("")
    public String list(){
    System.out.println("----------->查询客户信息");
    return "customer_list";
    }

    }
  • 通过全局异常处理,指定权限不足时的页面跳转

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @ControllerAdvice
    public class GlobalExceptionHandler {

    @ExceptionHandler
    public String doException(Exception e){
    if(e instanceof AuthorizationException){
    return "lesspermission";
    }
    return null;
    }

    }

8.3.1 手动授权

  • 在代码中进行手动的权限校验

    1
    2
    3
    4
    5
    6
    7
    Subject subject = SecurityUtils.getSubject();
    if(subject.isPermitted("sys:k:find")){
    System.out.println("----------->查询客户信息");
    return "customer_list";
    }else{
    return "lesspermission";
    }

九、缓存使用

使用Shiro进行权限管理过程中,每次授权都会访问realm中的doGetAuthorizationInfo方法查询当前用户的角色及权限信息,如果系统的用户量比较大则会对数据库造成比较大的压力

Shiro支持缓存以降低对数据库的访问压力(缓存的是授权信息)

9.1 导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.7.0</version>
</dependency>

9.2 配置缓存策略

  • 在resources目录下创建一个xml文件(ehcache.xml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" dynamicConfig="false">

<diskStore path="C:\TEMP" />

<cache name="users" timeToLiveSeconds="300" maxEntriesLocalHeap="1000"/>

<defaultCache name="defaultCache"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
maxElementsOnDisk="100000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<!--缓存淘汰策略:当缓存空间比较紧张时,我们要存储新的数据进来,就必然要删除一些老的数据
LRU 最近最少使用
FIFO 先进先出
LFU 最少使用
-->
</ehcache>

9.3 加入缓存管理

  • ShiroConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public EhCacheManager getEhCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return ehCacheManager;
}

@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm myRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
// 设置缓存管理器
securityManager.setCacheManager(getEhCacheManager());
return securityManager;
}

十、session管理

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

  • 如果我们需要对session进行管理

    • 自定义session管理器
    • 将自定义的session管理器设置给SecurityManager
  • 配置自定义SessionManager:ShiroConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Bean
    public DefaultWebSessionManager getDefaultWebSessionManager(){
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    System.out.println("----------"+sessionManager.getGlobalSessionTimeout()); // 1800000
    //配置sessionManager
    sessionManager.setGlobalSessionTimeout(5*60*1000);
    return sessionManager;
    }

    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm myRealm){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(myRealm);
    securityManager.setCacheManager(getEhCacheManager());
    securityManager.setSessionManager(getDefaultWebSessionManager());
    return securityManager;
    }

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!