框架整合

创建模块

创建一个Maven工程

添加依赖

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.9.0</version>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
</project>

添加配置文件

添加application.yml配置文件,注意修改数据库的连接参数。

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml
spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo?characterEncoding=utf8&useSSL=false
    username: root
    password: root
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
shiro:
  loginUrl: /login/login

添加启动类

package com.shiro.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.shiro.demo.mapper")
public class ShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class, args);
    }
}

实现登录认证

后端接口服务实现

创建用户表,这个在Shiro笔记02-基本使用里有。
创建实体类User.java,使用Lombok插件简化代码。

package com.shiro.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String username;
    private String password;
    private String passwordSalt;
}

创建mapper、service、serviceImpl。

package com.shiro.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shiro.demo.entity.User;

public interface UserMapper extends BaseMapper<User> {
}
package com.shiro.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shiro.demo.entity.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper extends BaseMapper<User> {
}
package com.shiro.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.shiro.demo.entity.User;
import com.shiro.demo.mapper.UserMapper;
import com.shiro.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;

public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public User getUserByUsername(String username) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        return userMapper.selectOne(queryWrapper);
    }
}

自定义Realm

package com.shiro.demo.realm;

import com.shiro.demo.entity.User;
import com.shiro.demo.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    /**
     * 自定义授权方法
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 自定义登录认证方法
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = authenticationToken.getPrincipal().toString();
        User user = userService.getUserByUsername(username);
        if (user != null) {
            return new SimpleAuthenticationInfo(username, user.getPassword(), ByteSource.Util.bytes("salt"), username);
        }
        return null;
    }
}

编写配置类

package com.shiro.demo.config;

import com.shiro.demo.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {
    @Autowired
    private MyRealm myRealm;

    /**
     * 配置DefaultWebSecurityManager
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        hashedCredentialsMatcher.setHashIterations(3);
        myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        defaultWebSecurityManager.setRealm(myRealm);
        return defaultWebSecurityManager;
    }

    /**
     * 配置DefaultShiroFilterChainDefinition
     */
    @Bean
    public DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
        defaultShiroFilterChainDefinition.addPathDefinition("/login", "anon");
        defaultShiroFilterChainDefinition.addPathDefinition("/**", "authc");
        return defaultShiroFilterChainDefinition;
    }
}

这里扩展一下:Shiro支持的Filter有哪些,通过官方文档可以看到。
编写Controller

package com.shiro.demo.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/login")
public class LoginController {
    @GetMapping("/login")
    @ResponseBody
    public String login(String username, String password) {
        Subject subject = SecurityUtils.getSubject();
        AuthenticationToken authenticationToken = new UsernamePasswordToken(username, password);
        try {
            subject.login(authenticationToken);
            return "登录成功";
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return "登录失败";
        }
    }
}

启动服务,测试代码,首先把数据库里的密码改成md5加盐加密3次后的结果,浏览器访问:http://localhost:8080/login/login?username=username1&password=password查看结果,可以看到“登录成功”,表示验证通过了。

实现前端页面

下面我们再来添加前端页面,这里使用Thymeleaf,所以要有Thymeleaf的相关依赖。
在resources目录下添加templates文件夹,添加login.html页面和main.html页面。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>Shiro 登录认证</h1>
<br>
<form action="/login/login">
    <div>用户名:<input type="text" name="username" value=""></div>
    <div>密码:<input type="password" name="password" value=""></div>
    <div><input type="submit" value="登录"></div>
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Shiro 登录认证后主页面</h1>
<br>
登录用户为:<span th:text="${session.user}"></span>
</body>
</html>

修改Controller方法。

package com.shiro.demo.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpSession;

@Controller
@RequestMapping("/login")
public class LoginController {
    @GetMapping("/toLogin")
    public String login() {
        return "login";
    }

    @GetMapping("/login")
    public String login(String username, String password, HttpSession httpSession) {
        Subject subject = SecurityUtils.getSubject();
        AuthenticationToken authenticationToken = new UsernamePasswordToken(username, password);
        try {
            subject.login(authenticationToken);
            httpSession.setAttribute("user", authenticationToken.getPrincipal().toString());
            return "main";
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return "login";
        }
    }
}

同时,还要修改defaultShiroFilterChainDefinition里的策略。

@Bean
public DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
    defaultShiroFilterChainDefinition.addPathDefinition("/login/**", "anon");
    defaultShiroFilterChainDefinition.addPathDefinition("/**", "authc");
    return defaultShiroFilterChainDefinition;

启动项目,浏览器访问http://localhost:8080/login/toLogin就来到了登录页面,输入正确的用户名密码,会跳转到main页面,并且在main页面上,可以看到当前登录的用户名信息。

多个Realm的认证策略设置

多个Realm实现原理

当应用程序配置多个Realm时,例如:用户名密码校验、手机号验证码校验等等。
Shiro的ModularRealmAuthenticator会使用内部的AuthenticationStrategy组件判断认证是成功还是失败。
AuthenticationStrategy是一个无状态的组件,它在身份验证尝试中被询问4次(这4次交互所需的任何必要的状态将被作为方法参数):
(1)在所有Realm被调用之前
(2)在调用Realm的getAuthenticationInfo方法之前
(3)在调用Realm的getAuthenticationInfo方法之后
(4)在所有Realm被调用之后
认证策略的另外一项工作就是聚合所有Realm的结果信息封装至一个AuthenticationInfo实例中,并将此信息返回,以此作为Subject的身份信息。
Shiro中定义了 3 种认证策略的实现:

AuthenticationStrategy

描述

AtLeastOneSuccessfulStrategy

只要有一个(或更多)的Realm验证成功,那么认证将视为成功

FirstSuccessfulStrategy

第一个Realm验证成功,整体认证将视为成功,且后续Realm将被忽略

AllSuccessfulStrategy

所有Realm成功,认证才视为成功

ModularRealmAuthenticator内置的认证策略默认实现是AtLeastOneSuccessfulStrategy方式。可以通过配置修改策略。

多个Realm代码实现

首先需要定义多个Realm,然后创建DefaultWebSecurityManager的实例,指定AuthenticationStrategy,并设置多个Realm。

@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
    DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
    ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
    modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
    defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator);
    List<Realm> realmList = new ArrayList<>();
    realmList.add(myRealm1);
    realmList.add(myRealm2);
    defaultWebSecurityManager.setRealms(realmList);
    return defaultWebSecurityManager;
}

Remember Me功能

Shiro提供了记住我(RememberMe)的功能,比如访问一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问。

基本流程

  1. 首先在登录页面选中 RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来
  2. 关闭浏览器再重新打开,会发现浏览器还是记住你的
  3. 访问一般的网页服务器端,仍然知道你是谁,且能正常访问
  4. 但是,如果我们访问电商平台时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你

代码实现

修改配置类,增加RememberMe的设置。

package com.shiro.demo.config;

import com.shiro.demo.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {
    @Autowired
    private MyRealm myRealm;

    /**
     * 配置DefaultWebSecurityManager
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        hashedCredentialsMatcher.setHashIterations(3);
        myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        defaultWebSecurityManager.setRealm(myRealm);
        defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
        return defaultWebSecurityManager;
    }

    public RememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(simpleCookie());
        cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
        return cookieRememberMeManager;
    }

    public SimpleCookie simpleCookie() {
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        // cookie.setDomain(domain);
        simpleCookie.setPath("/");
        simpleCookie.setHttpOnly(true);
        simpleCookie.setMaxAge(30 * 24 * 60 * 60);
        return simpleCookie;
    }

    /**
     * 配置DefaultShiroFilterChainDefinition
     */
    @Bean
    public DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
        defaultShiroFilterChainDefinition.addPathDefinition("/login/toLogin", "anon");
        defaultShiroFilterChainDefinition.addPathDefinition("/login/login", "anon");
        defaultShiroFilterChainDefinition.addPathDefinition("/**", "authc");
        defaultShiroFilterChainDefinition.addPathDefinition("/**", "user");
        return defaultShiroFilterChainDefinition;
    }

//    @Bean
//    public DefaultWebSecurityManager defaultWebSecurityManager() {
//        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//        ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
//        modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
//        defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator);
//        List<Realm> realmList = new ArrayList<>();
//        realmList.add(myRealm1);
//        realmList.add(myRealm2);
//        defaultWebSecurityManager.setRealms(realmList);
//        return defaultWebSecurityManager;
//    }
}

修改登录方法,传递rememberMe参数。

package com.shiro.demo.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpSession;

@Controller
@RequestMapping("/login")
public class LoginController {
    @GetMapping("/toLogin")
    public String login() {
        return "login";
    }

    @GetMapping("/login")
    public String login(String username, String password, @RequestParam(defaultValue = "false") boolean rememberMe, HttpSession httpSession) {
        Subject subject = SecurityUtils.getSubject();
        AuthenticationToken authenticationToken = new UsernamePasswordToken(username, password, rememberMe);
        try {
            subject.login(authenticationToken);
            httpSession.setAttribute("user", authenticationToken.getPrincipal().toString());
            return "main";
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return "login";
        }
    }

    @GetMapping("/loginRememberMe")
    public String loginRememberMe(HttpSession httpSession) {
        httpSession.setAttribute("user", "rememberMe");
        return "main";
    }
}

修改login.html页面,添加rememberMe的checkbox。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>Shiro 登录认证</h1>
<br>
<form action="/login/login">
    <div>用户名:<input type="text" name="username"></div>
    <div>密码:<input type="password" name="password"></div>
    <div>记住用户:<input type="checkbox" name="rememberMe"></div>
    <div><input type="submit" value="登录"></div>
</form>
</body>
</html>

修改application.yml里的shiro.loginUrl为:/login/toLogin。
启动项目进行测试,访问:http://localhost:8080/login/loginRemeberMe,页面会自动跳转到登录页:/login/toLogin。
然后,我们输入账户密码,并勾选“记住用户”的checkbox选项框,点击登录,然后在新标签页打开/login/loginRememberMe请求,可以看到页面也跳到了main.html里。
关掉浏览器,再次访问http://localhost:8080/login/loginRememberMe,可以发现,并没有进到登录页,而是直接进到了main.html,说明rememberMe效果实现了。
观察两次登录(勾选和不勾选RememberMe)请求里cookie的值,可以发现,不勾选的时候,rememberMe的cookie值是deleteMe,勾选的时候,rememberMe的cookie值是一个加密字符串,这就是区别,Shiro通过这个加密字符串可以解析出来用户信息,就没有跳转到登录页面。

用户登录认证后登出

用户登录后,配套的有登出操作。直接通过Shiro过滤器即可实现登出。
修改mian.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Shiro 登录认证后主页面</h1>
<br>
登录用户为:<span th:text="${session.user}"></span>
<br>
<a href="/logout">登出</a>
</body>
</html>

修改配置类,添加logout过滤器。

defaultShiroFilterChainDefinition.addPathDefinition("/logout", "logout");

登录后,点击”登出“超链接,就回到了登录页面,观察/logout请求里cookie的rememberMe值,可以发现是deleteMe,也就是把rememberMe的信息给清除掉。

授权、角色认证

授权

用户登录后,需要验证是否具有指定角色指定权限。Shiro也提供了方便的工具进行判断。
这个工具就是Realm的doGetAuthorizationInfo方法进行判断。触发权限判断的有两种方式:

  1. 在页面中通过shiro:****属性判断
  2. 在接口服务中通过注解@Requires****进行判断

后端接口服务注解

通过给接口服务方法添加注解可以实现权限校验,可以加在控制器方法上,也可以加在业务方法上,一般加在控制器方法上。常用注解如下:

  1. @RequiresAuthentication:验证用户是否登录,等同于subject.isAuthenticated()
  2. @RequiresUser:验证用户是否被记忆,等同于subject.isAuthenticated() && subject.isRemembered()
  3. @RequiresGuest:验证是否是一个guest请求,是否是游客请求,subject.getPrinciple()的值是null
  4. @RequiresRoles:验证subject是否有相应角色,如果没有,抛出AuthorizationException异常,如果有,执行方法
  5. @RequiresPermissions:验证subject是否有相应权限,如果没有,抛出AuthorizationException异常,如果有,执行方法

授权验证

没有角色无法访问

添加一个角色验证的方法。

package com.shiro.demo.controller;

import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/index")
public class IndexController {
    @GetMapping("/home")
    @ResponseBody
    public String home() {
        return "hello world";
    }

    @RequiresRoles("role1")
    @GetMapping("/testRoles")
    @ResponseBody
    public String testRoles() {
        return "角色验证通过";
    }
}

修改main.html,添加一个超链接,用于触发刚刚的请求。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Shiro 登录认证后主页面</h1>
<br>
登录用户为:<span th:text="${session.user}"></span>
<br>
<a href="/logout">登出</a>
<br>
<a href="/index/testRoles">测试角色</a>
</body>
</html>

修改MyRealm,重写授权方法。

/**
 * 自定义授权方法
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("进入自定义授权方法");
    return null;
}

重启服务,点击超链接进行访问,此时会报错,控制台可以看到,已经走了自定义的授权方法,下面我们就需要再自定义授权方法里写一些代码,实现角色判断和权限判断。

获取角色进行验证

先尝试写死,手动给用户提供一个角色,正常情况下,这个角色是从数据库里获取的,再次通过浏览器访问,查看效果,此时可以看到角色验证通过了。

/**
 * 自定义授权方法
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    simpleAuthorizationInfo.addRole("role1");
    return simpleAuthorizationInfo;
}

下面把这块改成活的,也就是从数据库获取角色,相关SQL参考这里。
编写mapper方法,service实现,修改MyRealm。

@Select("SELECT role_name FROM user_role WHERE username=#{username}")
List<String> getRoleListByUsername(String username);

@Override
public List<String> getRoleListByUsername(String username) {
    return userMapper.getRoleListByUsername(username);
}

/**
 * 自定义授权方法
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    List<String> roleList = userService.getRoleListByUsername(principalCollection.getPrimaryPrincipal().toString());
    simpleAuthorizationInfo.addRoles(roleList);
    return simpleAuthorizationInfo;
}

进行测试,此时数据就是活的啦。

获取权限进行验证

权限验证同理,相关SQL参考这里。
编写mapper方法,service实现,修改MyRealm。

@Select("SELECT permission FROM role_permission WHERE role_name IN (SELECT role_name FROM user_role WHERE username=#{username});")
List<String> getPermissionListByUsername(String username);

@Override
public List<String> getPermissionListByUsername(String username) {
    return userMapper.getPermissionListByUsername(username);
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    List<String> roleList = userService.getRoleListByUsername(principalCollection.getPrimaryPrincipal().toString());
    simpleAuthorizationInfo.addRoles(roleList);
    List<String> permissionList = userService.getPermissionListByUsername(principalCollection.getPrimaryPrincipal().toString());
    simpleAuthorizationInfo.addStringPermissions(permissionList);
    return simpleAuthorizationInfo;
}

修改controller方法,添加权限验证注解。

@RequiresPermissions("video:get")
@GetMapping("/testPermissions")
@ResponseBody
public String testPermissions() {
    return "权限验证通过";
}

修改main.html,添加请求入口。

<a href="/index/testPermissions">测试权限</a>

通过浏览器访问进行测试。

异常处理

对于没有角色,没有权限这样的情况,会报错误页面,这样不够友好,我们可以创建异常处理类,将错误信息提示出来。
创建GlobalException.java。

package com.shiro.demo.exception;

import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class GlobalException {
    @ResponseBody
    @ExceptionHandler(UnauthorizedException.class)
    public String unauthorizedException(Exception e) {
        return "无权限";
    }

    @ResponseBody
    @ExceptionHandler(AuthorizationException.class)
    public String authorizationException(Exception e) {
        return "权限认证失败";
    }
}

前端页面授权验证

要想让Thymeleaf支持Shiro语法,需要添加依赖。

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

添加配置类用于解析shiro属性。

/**
 * 配置ShiroDialect解析Shiro相关属性
 */
@Bean
public ShiroDialect shiroDialect() {
    return new ShiroDialect();
}

shiro属性,下面写成了标签的样子,实际是shiro属性。

<shiro:guest>用户没有身份验证时显示相应信息,即游客访问信息</shiro:guest>
<shiro:user>用户已经身份验证/记住我登录后显示相应的信息</shiro:user>
<shiro:authenticated>用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的</shiro:authenticated>
<shiro:notAuthenticated>用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证</shiro:notAuthenticated>
<shiro:principal/>相当于((User)Subject.getPrincipals()).getUsername()<shiro:principal property="username"/>
<shiro:lacksPermission name="org:create">如果当前Subject没有权限将显示body体内容</shiro:lacksPermission>
<shiro:hasRole name="admin">如果当前Subject有角色将显示body体内容</shiro:hasRole>
<shiro:hasAnyRoles name="admin,user">如果当前Subject有任意一个角色(或的关系)将显示body体内容</shiro:hasAnyRoles>
<shiro:lacksRole name="abc">如果当前Subject没有角色将显示body体内容</shiro:lacksRole>
<shiro:hasPermission name="user:create">如果当前Subject有权限将显示body体内容</shiro:hasPermission>

修改main.html,可以实现将无权限的标签进行隐藏。

<a shiro:hasRole="role1" href="/index/testRoles">测试角色</a>
<a shiro:hasPermission="video:get" href="/index/testPermissions">测试权限</a>

实现缓存

缓存工具EhCache

EhCache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。可以和大部分Java项目无缝整合,例如:Hibernate中的缓存就是基于EhCache实现的。
EhCache支持内存和磁盘存储,默认存储在内存中,如内存不够时把缓存数据同步到磁盘中。EhCache支持基于Filter的Cache实现,也支持Gzip压缩算法。
EhCache直接在JVM虚拟机中缓存,速度快,效率高,EhCache缺点是缓存共享麻烦,集群分布式应用使用不方便。

EhCache搭建使用

给项目加入Ehcache,添加pom依赖。

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.6.11</version>
    <type>pom</type>
</dependency>

在resources下添加ehcache.xml配置文件。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
    <!--磁盘的缓存位置-->
    <diskStore path="java.io.tmpdir/ehcache"/>
    <!--默认缓存-->
    <defaultCache
            maxEntriesLocalHeap="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            maxEntriesLocalDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </defaultCache>
    <!--helloworld 缓存-->
    <cache name="HelloWorldCache"
           maxElementsInMemory="1000"
           eternal="false"
           timeToIdleSeconds="5"
           timeToLiveSeconds="5"
           overflowToDisk="false"
           memoryStoreEvictionPolicy="LRU"/>
    <!--defaultCache:默认缓存策略,当 ehcache 找不到定义的缓存时,则使用这个缓存策略,只能定义一个-->
    <!--
    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>

创建测试类进行测试。

package com.shiro.demo.test;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

import java.io.InputStream;

public class Test {
    public static void main(String[] args) {
        InputStream inputStream = Test.class.getClassLoader().getResourceAsStream("classpath:ehchace.xml");
        CacheManager cacheManager = new CacheManager(inputStream);
        Cache cache = cacheManager.getCache("HelloWorldCache");
        Element element = new Element("name", "value");
        cache.put(element);
        Element cacheElement = cache.get("name");
        System.out.println(cacheElement.getObjectValue());
    }
}

Shiro整合EhCache

Shiro官方提供了shiro-ehcache,实现了整合EhCache作为Shiro的缓存工具。可以缓存认证执行的Realm方法,减少对数据库的访问,提高认证效率。
添加pom依赖。

<!--Shiro 整合 EhCache-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.2</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

在resources下添加配置文件ehcache/ehcache-shiro.xml。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="ehcache" updateCheck="false">
    <!--磁盘的缓存位置-->
    <diskStore path="java.io.tmpdir"/>
    <!--默认缓存-->
    <defaultCache
            maxEntriesLocalHeap="1000"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="3600"
            overflowToDisk="false">
    </defaultCache>
    <!--登录认证信息缓存:缓存用户角色权限-->
    <cache name="loginRolePsCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true"/>
</ehcache>

修改ShiroConfig类。

/**
 * 配置DefaultWebSecurityManager
 */
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
    DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    hashedCredentialsMatcher.setHashAlgorithmName("MD5");
    hashedCredentialsMatcher.setHashIterations(3);
    myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
    defaultWebSecurityManager.setRealm(myRealm);
    defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
    defaultWebSecurityManager.setCacheManager(getEhCacheManager());// 设置缓存管理器
    return defaultWebSecurityManager;
}
private EhCacheManager getEhCacheManager() {
    EhCacheManager ehCacheManager = new EhCacheManager();
    try(InputStream inputStream = ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml")) {
        CacheManager cacheManager = new CacheManager(inputStream);
        ehCacheManager.setCacheManager(cacheManager);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return ehCacheManager;
}

启动项目进行测试,查看日志,执行了3条SQL。

SELECT id,username,password,password_salt FROM user WHERE username=?
SELECT role_name FROM user_role WHERE username=?
SELECT permission FROM role_permission WHERE role_name IN (SELECT role_name FROM user_role WHERE username=?)

再次点击main.html页面的权限验证,角色验证,控制台并没有打印内容,说明此时的信息是从cache里获取的,并没有查询数据库。

会话管理

SessionManager

会话管理器,负责创建和管理用户的会话(Session)生命周期,它能够在任何环境中在本地管理用户会话,即使没有Web/Servlet/EJB容器,也一样可以保存会话。默认情况下,Shiro会检测当前环境中现有的会话机制(比如Servlet容器)进行适配,如果没有(比如独立应用程序或者非Web环境),它将会使用内置的企业会话管理器来提供相应的会话管理服务,其中还涉及一个名为SessionDAO的对象。SessionDAO负责Session的持久化操作(CRUD),允许Session数据写入到后端持久化数据库。

会话管理实现

SessionManager由SecurityManager管理。Shiro提供了三种实现。

Shiro笔记03-与Spring Boot整合_spring


顺便说下这个图怎么生成的,先找到SessionManager接口,右键选择Diagrams-Show Diagram…,选择Java class diagram。

右键SessionManager,选择Show Implementations,按住Ctrl+鼠标左键,勾选需要的类,最后按下回车。

  1. DefaultSessionManager:用于JavaSE环境
  2. ServletContainerSessionManager:用于web环境,直接使用Servlet容器的会话
  3. DefaultWebSessionManager:用于web环境,自己维护会话(不使用Servlet容器的会话管理)

获得Session方式

Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("key", "value");
Object value = session.getAttribute("key");

Controller中的request,在Shiro过滤器的doFilterInternal()方法,被包装成ShiroHttpServletRequest,SecurityManager和 SessionManager会话管理器决定session来源于ServletRequest还是由Shiro管理的会话。无论是通过request.getSessionsubject.getSession获取到session,操作session,两者都是等价的。