基于SSM的整合见文档
SSMS整合步骤.docx

一、Shiro简介

shiro是一个权限管理工具
java中常见的权限管理工具主要有:shiro、spring security、auth2等
Apache Shiro是一个功能强大且灵活的开源安全框架,可以清晰地处理身份验证,授权,企业会话管理和加密。
Apache Shiro的首要目标是易于使用和理解。安全有时可能非常复杂,甚至是痛苦的,但并非必须如此。框架应尽可能掩盖复杂性,并提供简洁直观的API,以简化开发人员确保其应用程序安全的工作。
以下是Apache Shiro可以做的一些事情:

  1. 验证用户以验证其身份
  2. 为用户执行访问控制,例如:
  3. 确定是否为用户分配了某个安全角色
  4. 确定是否允许用户执行某些操作
  5. 在任何环境中使用Session API,即使没有Web容器或EJB容器也是如此。
  6. 在身份验证,访问控制或会话生命周期内对事件做出反应。
  7. 聚合用户安全数据的1个或多个数据源,并将其全部显示为单个复合用户“视图”。
  8. 启用单点登录(SSO)功能
  9. 无需登录即可为用户关联启用“记住我”服务

……
以及更多 - 全部集成到一个易于使用的内聚API中。
Shiro尝试为所有应用程序环境实现这些目标 - 从最简单的命令行应用程序到最大的企业应用程序,而不强制依赖其他第三方框架,容器或应用程序服务器。当然,该项目旨在尽可能地融入这些环境,但它可以在任何环境中开箱即用。

二、Shiro功能

2.1 shiro功能介绍

Apache Shiro是一个具有许多功能的综合应用程序安全框架。下图显示了Shiro关注其能力的位置,此参考手册将以类似方式组织:

56 - Shiro-基于Springboot - 图1
Authentication 身份认证/登录,验证用户是不是拥有相应的身份;
Authorization 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常
见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Management 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography 加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support Web支持,可以非常容易的集成到Web环境;
Caching 缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing 提供测试支持;
Run As 允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me 记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。

2.2 shiro三大核心API

首先,从外部来看Shiro,即从应用程序角度的来观察如何使用Shiro完成工作。如下图:
56 - Shiro-基于Springboot - 图2

三大核心:
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:
Subject **主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是
Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与
Subject 的所有交互都会委托给SecurityManager,可以把Subject认为是一个门面; SecurityManager才是实际的执行者;
SecurityManager 安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;
可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm **域安全数据源,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用
户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm
得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全
数据源。

2.3 shiro内部架构

56 - Shiro-基于Springboot - 图3
Subject 主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager 相当于SpringMVC中的DispatcherServlet;是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两大核心功能实现(认证+授权)

3.1 认证+授权

Authentication 认证
Authorization 授权
认证:
判断用户提交的账号密码是否合法

授权:
判断用户是否有权限进行相关操作

3.2 SpringBoot整合shiro

3.2.1 项目结构

image.png

3.2.2 User实体类

  1. @Data
  2. @Accessors(chain = true)
  3. public class User {
  4. private String account;
  5. private String pwd;
  6. }

3.2.3 pom.xml导入shior

  1. <dependency>
  2. <groupId>org.apache.shiro</groupId>
  3. <artifactId>shiro-spring</artifactId>
  4. <version>1.4.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.shiro</groupId>
  8. <artifactId>shiro-core</artifactId>
  9. <version>1.4.0</version>
  10. </dependency>

3.2.4 创建自己的Realm类

创建CustomRealm继承AuthorizingRealm类,该类中有两个方法,分别是获取授权信息和认证信息的方法

/**
 * AuthorizingRealm 授权realm
 */
public class CustomRealm extends AuthorizingRealm {
    private static final Logger logger = LoggerFactory.getLogger(CustomRealm.class);
    /*
     * 获取权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
    /*
     * 获取认证信息
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String account = (String) token.getPrincipal();
        String pwd = new String((char[])token.getCredentials());
        User user = new User().setAccount("zhangsan").setPwd("123456");
        if (user==null){
            return null; //报账号不存在异常
        }
        SimpleAuthenticationInfo info =
                new SimpleAuthenticationInfo(user.getAccount(),user.getPwd(),this.getName());
        return info;
    }
}

3.2.5 创建shiro配置类

创建shiro的配置类,配置realm、securitymanager和shiro过滤器

import com.woniuxy.shiro.realm.CustomRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration  //配置类标志
public class ShiroConfiguration {
    private static  final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);
    //1.配置realm:域对象,调用service获取用户的账号密码、权限信息  自定义
    @Bean   //<bean>  默认以方法名作为id:此处就是realm
    public CustomRealm realm(){
        logger.info("创建realm");
        return new CustomRealm();
    }
    //2.安全管理器(核心)
    @Bean   //参数将相当于  <property ref = "realm">
    public SecurityManager securityManager(CustomRealm realm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        //设置realm
        manager.setRealm(realm);
        //注入session的管理
        manager.setSessionManager(sessionManager());
        //
        return manager;
    }

    //2.5步:shiro session的管理
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 去掉shiro登录时url里的JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

    /**
     * 3.shiro过滤器
     * 主要来指定哪些请求需要认证、哪些不需要、哪些需要什么样的权限
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        //配置安全管理器
        factoryBean.setSecurityManager(securityManager);
        //配置没有权限时要请求的url(页面、后台url)
        factoryBean.setUnauthorizedUrl("/html/error.html");//如果在后台跳 表明前后端不分离
        //需要登录但是又没有登录时,自动跳转到指定的页面获取请求后台的url
        factoryBean.setLoginUrl("/login.html");
        //设置过滤器链
        //HashMap 无序   LinkedHashMap 有序
        Map<String,String> map = new LinkedHashMap<>();
        //key:url    value:标签shiro的过滤器
        //anon是shiro的内置过滤器的名字,是anonymous单词前四个字母  表示能够匿名访问
        //main.html
        map.put("/login.html","anon");
        map.put("/user/login","anon");
        map.put("/user/regist","anon");
        //静态资源放行
        map.put("/html/*","anon");
        map.put("/css/*","anon");
        map.put("/js/*","anon");
        map.put("/images/*","anon");
        //注销操作
        map.put("/user/logout","logout");
        //除开以上情况以外的
        //authc也是shiro内置过滤器的名字,是Authentication单词的缩写  表示需要认证(登录)之后才能访问
        map.put("/**","authc");
        //
        factoryBean.setFilterChainDefinitionMap(map);
        //
        return factoryBean;
    }
}

注:如果url上出现默认**JSESSIONID,则需要添加上述代码第2.5步:shiro session的管理,并在第二步中注入session的管理:manager.setSessionManager(sessionManager());

3.2.6 创建UserController处理请求

import com.woniuxy.shiro2.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public String login(User user){
        //1.获取到“当前用户”对象
        Subject currentUser = SecurityUtils.getSubject();

        //2.判断用户是否认证过
        if (!currentUser.isAuthenticated()){
            //没有认证过   token  令牌
            String pwd = user.getPwd();
            UsernamePasswordToken token =
                    new UsernamePasswordToken(user.getAccount(),pwd);
            try {
                //当前用户携带令牌去认证,去找securitymanager进行认证
                //认证时可能会报异常,只要出现异常说明令牌有问题,账号密码有问题
                //不报任何异常表明账号密码没问题
                currentUser.login(token);
                //
                return "认证成功";
            }catch (UnknownAccountException e){
                //账号不存在
                e.printStackTrace();
                //认证失败
                return "账号不存在";
            } catch (IncorrectCredentialsException e){
                e.printStackTrace();
                //密码错误
                return "密码错误";
            }
        }
        return "登录成功";
    }
}

3.2.7 静态资源的编写与导入

  1. 在resources/static目录下创建js文件夹并导入jQuery
  2. 在resources/static目录下创建login.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
     <meta charset="UTF-8">
     <title>Title</title>
     <script src="js/jquery.min.js"></script>
     <script>
         $(function () {
             $("#login").click(function () {
                 let account = $("#account").val();
                 let pwd = $("#pwd").val();
                 $.ajax({
                     url:"user/login",
                     type:"POST",
                     data:{
                         account:account,
                         pwd:pwd
                     },
                     success:function (res) {
                         console.log(res)
                     }
                 });
             });
         });
     </script>
    </head>
    <body>
     <div>
         <input type="text" id="account"><br>
         <input type="password" id="pwd"><br>
         <button id="login">登录</button>
     </div>
    </body>
    </html>
    
    运行程序访问login.html进行登录操作,发现在realm的获取认证信息方法中打印到账号密码信息,表明shiro正常执行,该方法返回null表示没有找到任何账号信息,所以会报UnknownAccountException。

四、Shiro认证流程

4.1 大致认证流程

56 - Shiro-基于Springboot - 图5

4.2 详细认证流程

56 - Shiro-基于Springboot - 图6

五、Shiro过滤器

shiro常见内置过滤器

  • anon 匿名访问
  • authc 需要认证
  • logout 登出(注销)

浏览器发送请求时,所有的请求都会被shiro拦截,拦截之后shiro根据请求的URL与Map中的key进行从前往后的匹配,匹配到对应的key则获取到value中指定的过滤器名字,然后再根据过滤器的名字判断是否需要认证
56 - Shiro-基于Springboot - 图7
解决没有认证情况下前端发送ajax请求后端将页面以JSON方式返回的问题:**
原因在于:ajax发完请求之后要求后台返回JSON数据,而shiro判断到当前用户没有认证,则以请求转发的方式返回login.html给浏览器,浏览器接收到数据,认为后台返回了数据,因此把页面当做是JSO那数据看待。
解决方案:
自定义一个controller,在controller中返回JSON数据,在过滤器链配置登录页面URL时填写controller相关方法的url

//配置没有权限时要请求的url(页面、后台url)
factoryBean.setUnauthorizedUrl("/jump/unauthorized");//如果在后台跳 表明前后端不分离
//需要登录但是又没有登录时,自动跳转到指定的页面获取请求后台的url
factoryBean.setLoginUrl("/jump/toLogin");

自定义controller

@RestController
@RequestMapping("/jump")
public class JumpController {
    //去登录
    @RequestMapping("/toLogin")
    public ResponseResult<String> toLogin(){
        ResponseResult<String> result = new ResponseResult<>();
        result.setCode(500);
        result.setData("TO_LOGIN");
        result.setMessage("请登录");
        return result;
    }
    //没权限
    @RequestMapping("/unauthorized")
    public ResponseResult<String> unauthorized(){
        ResponseResult<String> result = new ResponseResult<>();
        result.setCode(500);
        result.setData("NO_UNAUTHORIZED");
        result.setMessage("你没有权限");
        return result;
    }
}

六、Shiro加密模块

shiro加密模块是shiro提供的一个非常有用且重要的模块,其支持多种加密方式,可以将指定的明文加密成密文。
加密方式分类

  • 对称加密:可以通过加密算法将明文加密成密文,也可以通过同样的加密算法将密文解密成明文
  • 非对称加密:只能将明文加密成密文,无法将密文进行解密,常用用来对密码加密

shiro提供了SimpleHash类来加密明文

import org.apache.shiro.crypto.hash.SimpleHash;
public class CryTest {
    public static void main(String[] args) {
        //1.明文
        String pwd = "123";
        //2.加密得到密文  通过MD5加密方式将pwd加密得到32位的字符串
        pwd = new SimpleHash("MD5",pwd).toString();
        System.out.println(pwd);// 202cb962ac59075b964b07152d234b70
        pwd = "123";
        //参数4:加密次数
        pwd = new SimpleHash("MD5",pwd,null,100).toString();
        System.out.println(pwd); //156650ad747cfb25fcb9a7a2f8a02bdc
        pwd = "123";
        //参数3:盐值   盐值不能是随机
        //盐一般用数据库中不会被修改的字段值作为盐  例如:id、账号、身份证
        //假设数据库中有5个字段是不可能被修改的  盐值就可以用这5个字段排列组合 120
        pwd = new SimpleHash("MD5",pwd,"zhangsan",100).toString();
        System.out.println(pwd);  //f391b473a0240b477164108ee71e68f8
    }
}

注意:盐一般用数据库中不会被修改的字段值作为盐 例如:id、账号、身份证,不能是随机的

认证中使用密文**
在realm的获取认证信息方法中将User对象的pwd改为密文

User user = new User().setAccount("zhangsan").setPwd("f391b473a0240b477164108ee71e68f8");

由于MD5加密方式是非对称加密,因此在登录时应该将用户提交的明文通过同样的加密方式进行加密,然后再封装到token中

//没有认证过   token  令牌
String pwd = new SimpleHash("MD5",user.getPwd(),user.getAccount(),100).toString();
UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(),pwd);

运行项目测试

七、整合Mybatis,从数据库中获取账号密码

创建数据库

CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro;
CREATE TABLE `user`(
    id INT PRIMARY KEY AUTO_INCREMENT,
    account VARCHAR(20),
    pwd VARCHAR(32)
);

插入数据:此处的密码应该为密文(具体加密方式自定义,但是每次加密需要相同的方式)
56 - Shiro-基于Springboot - 图8
创建UserMapper接口编写SQL语句

@Mapper
public interface UserMapper {
    @Select("select * from user where account = #{account}")
    public User findUserByAccount(String account);
}

在主启动类上扫描mapper

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

创建UserService

public interface UserService {
    public User findUserByAccount(String account);
}

创建UserServiceImpl实现类

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public User findUserByAccount(String account) {
        return userMapper.findUserByAccount(account);
    }
}

在realm中注入UserService并修改获取认证信息方法

@Autowired
private UserService userService; //注入service对象
@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //token中包含了账号密码信息
        logger.info("调用realm的获取认证信息方法,得到对应用户的账号信息");
        //获取账号、密码信息
        String account = (String) token.getPrincipal();
        String pwd = new String((char[])token.getCredentials());
        logger.info(account+","+pwd);
        //通过账号作为查询条件去数据库中查出对应的账号密码信息  调用service相关方法
        //模拟假设获得了账号密码
        //User user = new User().setAccount("zhangsan").setPwd("f391b473a0240b477164108ee71e68f8");
        User user = userService.findUserByAccount(account);
        if (user==null){
            return null; //报账号不存在异常
        }
        //将查询到的结果封装到AuthenticationInfo中返回给securitymanager,securitymanager会根据
        //AuthenticationInfo中的信息与token中的信息做比对  判断账号密码是否正确
        //参数1:查询到的账号
        //参数2:查询到的密码
        //参数3:当前realm的名字
        SimpleAuthenticationInfo info =
                new SimpleAuthenticationInfo(user.getAccount(),user.getPwd(),this.getName());
        return info;
    }

重启项目进行测试
注意:在测试过程中如果出现不正常的情况,注销一下试试