美文网首页java高级开发
Spring Boot之Shiro安全框架

Spring Boot之Shiro安全框架

作者: 老鼠AI大米_Java全栈 | 来源:发表于2019-01-13 16:26 被阅读6次

    Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
    参考https://segmentfault.com/a/1190000011918957

    Shiro权限控制之登录认证

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

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

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

    1. user(用户表)


      image.png
    2. role(角色表)


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

    那么什么是登录认证,我想很多初学者会曲解它的意思,它并不是帮助你去登录用户名账号的。
    要真正理解它,我们就需要知道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类:
    (因为本篇只学习登录认证,所以我们先不用缓存管理,密码编码等功能)

    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","authc");
            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存入的时候绑定了登录传入的用户名和密码(token又很多实现类,推荐使用UsernamePasswordToken);
    3.shiro自带的框架会将token与SimpleAuthenticationInfo类对象进行比较,失败抛出指定异常(需要自己捕获)

    第四步:Controller的编写

    完成上面shiroFactory和realm的配置之后;
    我们就要真正的去调用shiro的认证功能了
    要明白,在shiro的登录认证中:
    Controller帮你获取post参数后,
    进行参数绑定,再调用subject.login()方法;
    如果用户名密码正确,会跳转SuccessUrl,
    所以说Controller获取参数后注入给Shiro,信息错误则在Controller中报错

    @PostMapping(value = "testLogin")
        public Map<String,Object> testLogin(@RequestParam("name")String name,@RequestParam("password")String password){
            Map<String,Object> map = new HashMap<String,Object>();
            //创建subject实例
            Subject subject = SecurityUtils.getSubject();
            //判断当前的subject是否登录
            if (subject.isAuthenticated()==false){
                //将用户名和密码存入UsernamePasswordToken中
                UsernamePasswordToken token = new UsernamePasswordToken(name,password);
                try {
                    //将存有用户名和密码的token存进subject中
                    subject.login(token);
                }catch (UnknownAccountException uae){
                    System.out.println("没有用户名为"+token.getPrincipal()+"的用户");
                } catch (IncorrectCredentialsException ice){
                    System.out.println("用户名为:"+token.getPrincipal()+"的用户密码不正确");
                } catch (LockedAccountException lae){
                    System.out.println("用户名为:"+token.getPrincipal()+"的用户已被冻结");
                } catch (AuthenticationException e){
                    System.out.println("未知错误!");
                }
            }
            return "success";
        }
    

    第五步:在Restful风格下的实现

    以上只是在springmvc中的shiro实现,
    但是实际开发中,前后端分离越来越流行,
    分离之后的RestFulApi我们要怎么实现shiro呢?
    在这里我的想法是自己创建token

    RestFul下的思路:
    1.当我们进行账号密码登录的时候,会创建一个token(UUID随机生成)
    2.token存入的时候要记得它是随机生成的,生成之后会与用户登录的id进行绑定;
    3.我们登录完成之后,返回给浏览器的JSON对象要包含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 UserService userService;
    
        @Autowired
        private TokenService tokenService;
    
        @ApiOperation(value = "登录验证",notes = "成功返回200,失败返回500,返回一个TokenJSON对象")
        @ApiImplicitParams({
                @ApiImplicitParam(name = "name",value = "账号名",required = true,dataType = "String"),
                @ApiImplicitParam(name = "password",value = "密码",required = true,dataType = "String")
        })
        @RequestMapping(value = "/ajaxLogin",method = RequestMethod.POST)
        public Map<String, Object> ajaxLogin(@RequestParam("name")String name, @RequestParam("password")String password){
            tokenService.checkExpire();
            Map<String, Object> map = new HashMap<String,Object>();
            User user = new User(name,password);
            int status = userService.queryUser(user);
            if (status==200){
                map = tokenService.createToken(user);
            }
            map.put("status",status);
            return map;
        }
    
    }
    

    在controller中返回一个User和Token给前端;

    1. 在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 static final int Expire = 3600*25;
    
        @Autowired
        private UserMapper userMapper;
    
        @Autowired
        private TokenMapper tokenMapper;
    
        @Override
        public Map<String, Object> createToken(User user) {
            User user1 = userMapper.selectByNameAndPassword(user);
            //创建TokenEntity参数
            String newtoken = UUID.randomUUID().toString();
            Date updateTime = new Date();
            Date expireTime = new Date(updateTime.getTime()+Expire*1000);
    
            Token token = new Token(newtoken,user1.getId(),updateTime,expireTime);
            //判断token是否已经存在,不存在就存入,存在就更新
            if (tokenMapper.findByUserId(user1.getId())==null){
                tokenMapper.insert(token);
                System.out.println("存入成功");
            }else {
                tokenMapper.updateByToken(token);
                System.out.println("更新成功");
            }
            Map<String,Object> map = new HashMap<String,Object>();
            map.put("token",token);
            return map;
        }
    
        @Override
        public void checkExpire() {
            Date now = new Date();
            List<Token> list = tokenMapper.selectByExample(new TokenExample());
            for (Token token:list){
                if (token.getExpiretime().getTime()<now.getTime()){
                    tokenMapper.deleteByExpireTime(token);
                    System.out.println(token.getTokenid()+"已删除");
                }
            }
        }
    }
    

    上面创建token的时候因为时间原因没有判断用户Id的token是否已在数据库存在,你们可以自己试下;

    1. 我们token已经创建了,并且把它以JSON的格式穿了过去,现在要做的就是把token存到浏览器中:
      在登录界面的登录按钮上,我们设置一个js方法:
    function login() {
    
         var name = document.getElementById('name').value;
         console.log(name);
         var password=document.getElementById('password').value;;
         var url='http://localhost:8088/ajaxLogin'
         $.ajax({
             url:url,
             type:'post',
             data:{name:name,password:password},
             datatype:'json',
             success:function (result) {
                 if(result.status==200){
                     localStorage.setItem("token",result.token)
                     console.log(result)
                 }else if(result.status=500){
                     alert('登录失败!')
                 }
             }
    
         })
     }
    

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

    但是,细心的同学会发现,虽然存进了localStorage中,但是从请求头传给后端是最优解决方案,也就是需要将token附加在Header里,而且我们要做到访问任意url,都能把token从localStorage转存到Header中,这个问题就交给机智的你们了-_-

    相关文章

      网友评论

        本文标题:Spring Boot之Shiro安全框架

        本文链接:https://www.haomeiwen.com/subject/egunrqtx.html