Spring Boot上的Shiro安全框架

Shiro權限控制之登陸認證


前言:相信點進來的同窗大部分是剛接觸shiro框架
因此咱們從最基礎開始,固然我會拋開那些shiro的官方圖(真的有人會認真看那玩意兒?),一步步向你們講解shiro的配置過程登陸認證的簡單實現前端

Shiro是用來幫助咱們作權限管理的,本篇文章的shiro使用在Web項目上,因此我用了最新的spring boot爲框架。(固然使用xml來進行配置也能夠,原理是同樣的,只是寫法不一樣)java

在開始學習以前理解什麼是shiro的權限管理?
咱們知道shiro的主要功能有認證,受權,加密,會話管理,緩存等
一大堆功能會讓你以爲學起來毫無胃口,這裏咱們主要知道什麼是認證受權就行mysql

(這樣理解確定不許確,可是更易懂)
認證就是登陸認證:你登陸了這個網頁,shiro會經過一個口令(這裏咱們用token)來認證你,固然你也會用這個口令去獲得服務器的承認,進行後續的權限操做;
受權就是權限受理:shiro會根據你提供的信息進行認證以後,給予你相應的權力(如刪除,添加等);web

要記住Shiro不會給你建立和維護關係表,須要咱們本身在數據庫建立出對應的關係表:用戶——角色——權限
讓咱們看下這幾張表:
1.user(用戶表)ajax

clipboard.png

2.role(角色表)spring

clipboard.png

3.permission(權限表)sql

clipboard.png

用戶和角色是一對多的關係,一個用戶能夠擁有多個角色(好比管理員,普通用戶)
角色和權限是多對多的關係,一個角色能夠用個多個權限,一個權限也能對應多個用戶
固然還有關聯表,這裏很少說,由於咱們只作登陸驗證,因此目前只須要一張用戶表便可數據庫

那麼什麼是登陸認證,我想不少初學者會曲解它的意思,它並非幫助你去登陸用戶名帳號的。
要真正理解它,咱們就須要知道shiro是用來幹什麼的?登陸認證在shiro中起什麼做用?apache

前面說了shiro是用來作權限管理的,而登陸以後怎樣才能讓shiro一直記得你,這就是登陸認證的做用
那麼有同窗就會問,爲何要用shiro的認證,而不去使用數據庫的用戶表來認證?
這個問題我也問過,繼續理解便會知道:
由於你以後的每次操做都要用服務端返回給你的數據來校驗,若是使用User表數據是極不安全和不可靠的,既然加入了shiro框架,就要考慮到安全性,因此咱們會使用token來進行校驗,這也是本篇文章的重點json

廢話很少說,咱們開始吧:

第一步:引入相關包

這裏我使用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","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給前端;
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 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是否已在數據庫存在,大家能夠本身試下;
3.OK,咱們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中了。

clipboard.png

可是,細心的同窗會發現,雖然存進了localStorage中,可是從請求頭傳給後端是最優解決方案,也就是須要將token附加在Header裏,並且咱們要作到訪問任意url,都能把token從localStorage轉存到Header中,這個問題就交給機智的大家了,若是實在作不出來能夠私信我

以上即是Spring Boot上Shiro安全框架的登陸驗證簡單實現;
以爲還能夠請點個贊,贊不了也能夠收藏;
總之,謝謝閱讀~

相關文章
相關標籤/搜索