Spring Boot上的Shiro安全框架

6,938 阅读10分钟
原文链接: segmentfault.com

Shiro权限控制之登录认证


前言:相信点进来的同学大部分是刚接触shiro框架
所以我们从最基础开始,当然我会抛开那些shiro的官方图(真的有人会认真看那玩意儿?),一步步向大家讲解shiro的配置过程登录认证的简单实现

Shiro是用来帮助我们做权限管理的,本篇文章的shiro使用在Web项目上,所以我用了最新的spring boot为框架。(当然使用xml来进行配置也可以,原理是一样的,只是写法不同)

在开始学习之前理解什么是shiro的权限管理?
我们知道shiro的主要功能有认证,授权,加密,会话管理,缓存等
一大堆功能会让你觉得学起来毫无胃口,这里我们主要知道什么是认证授权就行

(这样理解肯定不准确,但是更易懂)
认证就是登录认证:你登录了这个网页,shiro会通过一个口令(这里我们用token)来认证你,当然你也会用这个口令去得到服务器的认可,进行后续的权限操作;
授权就是权限受理:shiro会根据你提供的信息进行认证之后,给予你相应的权力(如删除,添加等);

要记住Shiro不会给你创建和维护关系表,需要我们自己在数据库创建出对应的关系表:用户——角色——权限
让我们看下这几张表:
1.user(用户表)

2.role(角色表)

3.permission(权限表)

用户和角色是一对多的关系,一个用户可以拥有多个角色(比如管理员,普通用户)
角色和权限是多对多的关系,一个角色可以用个多个权限,一个权限也能对应多个用户
当然还有关联表,这里不多说,因为我们只做登录验证,所以目前只需要一张用户表即可

那么什么是登录认证,我想很多初学者会曲解它的意思,它并不是帮助你去登录用户名账号的。
要真正理解它,我们就需要知道shiro是用来干什么的?登录认证在shiro中起什么作用?

前面说了shiro是用来做权限管理的,而登录之后怎样才能让shiro一直记得你,这就是登录认证的作用
那么有同学就会问,为什么要用shiro的认证,而不去使用数据库的用户表来认证?
这个问题我也问过,继续理解便会知道:
因为你之后的每次操作都要用服务端返回给你的数据来校验,如果使用User表数据是极不安全和不可靠的,既然加入了shiro框架,就要考虑到安全性,所以我们会使用token来进行校验,这也是本篇文章的重点

废话不多说,我们开始吧:

第一步:引入相关包

这里我使用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>cn.lxt</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>Demo project for Spring Boot</description>

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

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

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

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>1.5.8.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>1.5.8.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>1.5.8.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
            <version>1.5.8.RELEASE</version>
        </dependency>

        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <version>1.5.8.RELEASE</version>
            <optional>true</optional>
            <scope>true</scope>
        </dependency>

        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.5</version>
        </dependency>

        <!--aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>1.5.8.RELEASE</version>
        </dependency>

        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.5</version>
        </dependency>

        <!--shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>

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

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-cas</artifactId>
            <version>1.3.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.5</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>5.1.30</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>

第二步:配置Shiro

pom配置好之后,我们就要用java编写shiro的全局配置类。
在配置shiro之前我们需要明白它的三大要素:
Subject:单个对象,与如何应用交互的用户对象;
SecurityManager:安全管理器,管理Subject;
Realm:域,SecurityManager与Realm交互获得数据(用户-角色-权限)

知道这些后我们开始新建一个ShiroConfig类:(因为本篇只学习登录认证,所以我们先不用缓存管理,密码编码等功能)

package cn.lxt.shiro;


import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class shiroConfig {

    /**
     * 负责shiroBean的生命周期
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     *这是个自定义的认证类,继承子AuthorizingRealm,负责用户的认证和权限处理
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public MyShiroRealm shiroRealm(){
        MyShiroRealm realm = new MyShiroRealm();
        //realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }

    /** 安全管理器
     * 将realm加入securityManager
     * @return
     */
    @Bean
    public SecurityManager securityManager(){
        //注意是DefaultWebSecurityManager!!!
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

    /** shiro filter 工厂类
     * 1.定义ShiroFilterFactoryBean
     * 2.设置SecurityManager
     * 3.配置拦截器
     * 4.返回定义ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        //1
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //2
        //注册securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        System.out.println("11");
        //3
        // 拦截器+配置登录和登录成功之后的url
        //LinkHashMap是有序的,shiro会根据添加的顺序进行拦截
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        //配置不会被拦截的连接  这里顺序判断
        //anon,所有的url都可以匿名访问
        //authc:所有url都必须认证通过才可以访问
        //user,配置记住我或者认证通过才能访问
        //logout,退出登录
        filterChainDefinitionMap.put("/JQuery/**","anon");
        filterChainDefinitionMap.put("/js/**","anon");
        //配置退出过滤器
        filterChainDefinitionMap.put("/example1","anon");
        filterChainDefinitionMap.put("/lxt","anon");
        filterChainDefinitionMap.put("/login","anon");
        filterChainDefinitionMap.put("/success","anon");
        filterChainDefinitionMap.put("/index","anon");
        filterChainDefinitionMap.put("/Register","anon");
        filterChainDefinitionMap.put("/logout","logout");
        //过滤连接自定义,从上往下顺序执行,所以用LinkHashMap /**放在最下边
        filterChainDefinitionMap.put("/**","authc");
        //设置登录界面,如果不设置为寻找web根目录下的文件
        shiroFilterFactoryBean.setLoginUrl("/lxt");
        //设置登录成功后要跳转的连接
        shiroFilterFactoryBean.setSuccessUrl("/success");
        //设置登录未成功,也可以说无权限界面
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        System.out.println("shiro拦截工厂注入类成功");

        //4
        //返回
        return shiroFilterFactoryBean;
    }
}

以上需要注意几点:
1.shiroFilter是入口,主要有四步操作,代码中已经注释清楚
2.shiroFilterFactoryBean.setLoginUrl("/lxt");启动类不管你输入怎样的url,他都会跳转到登录启动类;
3.shiroFilterFactoryBean.setSuccessUrl("/success");登录成功后跳转的类,这个方法大家可以不用管,因为我感觉它根本用不到,大神别喷!

第三步:配置Realm

看完了ShiroConfig类之后,许多人会问:噫!我的MyShiroRealm怎么导入不进来!
其实这个方法的调用需要我们自己再写一个Realm类继承AuthorizingRealm。
继承之后我们需要重写两个方法:
1.doGetAuthorizationInfo()方法用于角色和权限的控制,暂不使用;
2.doGetAuthenticationInfo()方法用于登录认证,重点
下面贴出代码:

package cn.lxt.shiro;

import cn.lxt.bean.User;
import cn.lxt.service.UsersService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

public class MyShiroRealm extends AuthorizingRealm {

    @Autowired
    private UsersService usersService;

    /**
     * 用于获取登录成功后的角色、权限等信息
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        return null;
    }

    /**
     * 验证当前登录的Subject
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //拿到账号(username)
        String username = (String) token.getPrincipal();
        System.out.println("username=:"+username);
        //检查token的信息
        System.out.println(token.getCredentials());

        User user = usersService.findByName(username);
        if (user==null){
            return null;
        }

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

通过以上代码你会发现,我们是怎样进行验证的,进行验证的关系点是传入的参数token
现在大家应该明白了token在本篇文章中的作用!

当然有些同学看到这里还是云里雾里,在这我稍微讲解一些思路:
1.当我们进行账号密码登录的时候,会创建一个token(token只是一种概念,具体的实现还是要定义的)到数据库;
2.token存入的时候要记得它是随机生成的,生成之后会与用户登录的id进行绑定;
3.所以我们登录完成之后,返回给浏览器的JSON对象要包含token值,浏览器会把token值存入到浏览器中。

第四步:Token的创建与传输

思路清楚之后我们要进行实现:
1.创建token:

package cn.lxt.controller;

import cn.lxt.bean.User;
import cn.lxt.service.TokenService;
import cn.lxt.service.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;

@Controller
public class LoginController {

    @Autowired
    private UsersService usersService;

    @Autowired
    private TokenService tokenService;

    //登录成功后返回一个map
    @PostMapping(value = "/login")
    @ResponseBody
    public Map<String, Object> login(HttpServletRequest request){
        String username = request.getParameter("name");
        User user = usersService.findByName(username);
        //给这个User创建一个token
        Map<String,Object> map = tokenService.createToken(user.getId());
        map.put("user",user);
        return map;
    }

}

在controller中返回一个User和Token给前端;
2.在Service中创建token,并且存入数据库:

package cn.lxt.service.Impl;

import cn.lxt.bean.Token;
import cn.lxt.dao.TokenMapper;
import cn.lxt.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Service
public class TokenServiceImple implements TokenService{

    private final static int EXPIRE = 3600*12;

    @Autowired
    private TokenMapper tokenMapper;

    @Override
    //未完成功能:判断是否已存在,已存在覆盖掉
    public Map createToken(int userId) {
        Map<String,Object> map = new HashMap<String, Object>();
        //设置一个随机产生的token
        String token = UUID.randomUUID().toString();
        //设置创建时间
        Date createTime = new Date();
        //设置过期时间    创建日期加12小时
        Date expireTime = new Date(createTime.getTime()+EXPIRE*1000);

        //判断token是否已存在
        //这里先省略

        Token tokenEntity = new Token(userId,token,expireTime,createTime);
        //保存到数据库
        tokenMapper.insert(tokenEntity);

        System.out.println("存入成功");
        map.put("token",token);
        return map;
    }
}

上面创建token的时候因为时间原因没有判断用户Id的token是否已在数据库存在,你们可以自己试下;
3.OK,我们token已经创建了,并且把它以JSON的格式穿了过去,现在要做的就是把token存到浏览器中:
在登录界面的登录按钮上,我们设置一个js方法:

function login() {
    $.ajax({
        url:"/login",
        type:"post",
        dataType:"JSON",
        data:$(".login").serialize(),
        success:function (map) {
            if(map.token!=null){
                localStorage.setItem("token",map.token);
                window.location.href="/success";
            }else{
                alert("token为空");
            }
        }
    })
}

上面代码把token传进localStorage中了。

但是,细心的同学会发现,在Realm代码中,传入参数时(AuthenticationToken token)它是从前端的Header中拿到token的,而我们的token存放在localStorage中,所以我们现在要做到访问任意url,都能把token从localStorage转存到Header中,这个问题就交给机智的你们了,如果实在做不出来可以私信我(关注我会回复的快一点,真的)

以上便是Spring Boot上Shiro安全框架的登录验证简单实现;
觉得还可以的请点个赞,赞不了也可以收藏下;
总之,谢谢阅读~