spring boot 使用Shiro進行用戶認證教程

1.依賴

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

2.創建測試數據

1.創建數據庫用戶數據
在這裏插入圖片描述
2.創建UserMapper 獲取用戶數據

@Mapper
public interface UserMapper {
     List<Userinfo> findAll();
}

UserMapper.xml

<?xml version="1.0" encoding ="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper3.0//EN "
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo2.mapper.UserMapper">
    <select id="findAll" resultType="com.example.demo2.beans.Userinfo">
         select * from userinfo;
    </select>
</mapper>

3.創建ShiroConfig,添加相關設置

這裏面比如是否需要加密密碼(認證傳入Shiro的是明文,但是數據庫保存的可能是加密過的,所以需要相關的加密配置,Shiro根據這個配置對認證時傳入的密碼進行加密,然後進行匹配)

package com.example.demo2.config;

import com.example.demo2.app.UserRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
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 java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //md5加密1次
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        hashedCredentialsMatcher.setHashIterations(1);
        hashedCredentialsMatcher.setHashSalted(true);
        return hashedCredentialsMatcher;
    }


    //自定義realm
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return userRealm;
    }
    /**
     * 安全管理器
     * 注:使用shiro-spring-boot-starter 1.4時,返回類型是SecurityManager會報錯,直接引用shiro-spring則不報錯
     *
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        return securityManager;
    }


    /**
     * 設置過濾規則
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSecurityManager(manager);
        shiroFilterFactoryBean.setSuccessUrl("/");
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");

        //注意此處使用的是LinkedHashMap,是有順序的,shiro會按從上到下的順序匹配驗證,匹配了就不再繼續驗證
        //所以上面的url要苛刻,寬鬆的url要放在下面,尤其是"/**"要放到最下面,如果放前面的話其後的驗證規則就沒作用了。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

}

4.創建UserRealm,用於身份權限劃分與認證匹配

package com.example.demo2.app;

import com.example.demo2.beans.Userinfo;
import com.example.demo2.mapper.UserMapper;
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;
import org.springframework.util.DigestUtils;

import java.util.ArrayList;
import java.util.List;

public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserMapper userMapper;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //添加授權認證用戶,一般是添加用戶信息
        //一般在這裏進行用戶權限區分,把那些有權限的用戶加入進去
        info.addStringPermissions(getUserNames());
        return info;
    }



    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      //獲取認證信息
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String userName = token.getUsername();
        //一般來說,獲取到要驗證的用戶id,然後我們獲取數據庫的密碼,然後把賬號密碼傳入進去就行了
        //驗證是有Shrio自動幫我們匹配
        Userinfo userinfo = getUserByUserId(userName);
        if(userinfo!=null){
            //獲取MD5加密後的密碼,一般來說這個應該是存在數據庫的,不是明文密碼
            //爲了測試方便,生成MD5密碼傳入,在ShrioConfig裏設置MD5加密一次,看上面ShiroConfig 
            //到時候驗證時傳入明文密碼,Shrio自動幫我們加密後再匹配
            //就是說這個密碼是數據庫保存的密碼,一般來說,保存是啥就傳入啥
            String pswInSecCode =  getMD5(userinfo.getPassword());
            return new SimpleAuthenticationInfo(userinfo.getUserName(), pswInSecCode, getName());

        }
        //找不到,說明用戶不存在,返回空
        return null;
    }

    public  static String getMD5(String str) {
        String base = str+"";
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }


    private List<String> getUserNames(){
        List<Userinfo> userinfos = userMapper.findAll();
        List<String> userNames = new ArrayList<>();
        if (userinfos != null && userinfos.size() > 0) {
            for (Userinfo userinfo : userinfos) {
                userNames.add(userinfo.getUserName());
            }
        }
        return userNames;
    }

    private Userinfo getUserByUserId(String userId){
        List<Userinfo> userinfos = userMapper.findAll();
        if (userinfos != null && userinfos.size() > 0) {
            for (Userinfo userinfo : userinfos) {
               if(userinfo.getUserName().equals(userId)){
                   return userinfo;
               }
            }
        }
        return null;
    }
}

5.添加LoginController,用於登錄請求

package com.example.demo2.controller;

import com.example.demo2.config.ShiroConfig;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
    /**
     * get請求,登錄頁面跳轉
     *
     * @return
     */
    @GetMapping("/login")
    public String login() {
        //如果已經認證通過,直接跳轉到首頁
        if (SecurityUtils.getSubject().isAuthenticated()) {
            return "redirect:/index";
        }
        return "login";
    }

    /**
     * post表單提交,登錄
     *
     * @param username
     * @param password
     * @param model
     * @return
     */
    @PostMapping("/login")
    public Object login(String username, String password, Model model) {
        Subject user = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        System.out.println("username:" + username + ",psw:" + password);
        try {
            //shiro幫我們匹配密碼什麼的,我們只需要把東西傳給它,它會根據我們在UserRealm裏認證方法設置的來驗證
            user.login(token);
            return "redirect:index";
        } catch (UnknownAccountException e) {
            //賬號不存在和下面密碼錯誤一般都合併爲一個賬號或密碼錯誤,這樣可以增加暴力破解難度
            model.addAttribute("message", "賬號不存在!");
        } catch (DisabledAccountException e) {
            model.addAttribute("message", "賬號未啓用!");
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("message", "密碼錯誤!");
        } catch (Throwable e) {
            model.addAttribute("message", "未知錯誤!");
        }
        return "login";
    }



    /**
     * 退出
     *
     * @return
     */
    @RequestMapping("/logout")
    public String logout() {
        SecurityUtils.getSubject().logout();
        return "login";
    }


}

6.添加IndexController用於登錄成功後跳轉請求

package com.example.demo2.controller;

import org.apache.shiro.SecurityUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class IndexController {
    /**
     * 首頁,並將登錄用戶的全名返回前臺
     * @param model
     * @return
     */
    @RequestMapping(value = {"/", "/index"})
    public String index(Model model) {
        String sysUser = (String) SecurityUtils.getSubject().getPrincipal();
        model.addAttribute("userName", sysUser);
        return "index";
    }
}

7.添加jsp,用於前端測試

依賴:

   <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>

添加jsp:
在這裏插入圖片描述
login.jsp用戶登錄:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
    <title>登錄</title>
</head>
<body>
<h1>${message}</h1>
<form method="post" action="/login">
    用戶名:<input name="username"><br>
    密碼:<input name="password"><br>
    <input type="submit" value="登錄">
</form>
</body>
</html>

index.jsp用於登錄成功:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
    <title>首頁</title>
</head>
<body>
hello ${userName}
</body>
</html>

8.運行測試

輸入密碼錯誤提示:
在這裏插入圖片描述
輸入密碼成功後跳轉:
在這裏插入圖片描述

9.session管理

一般默認都支持session,按照上面的教程,沒有登錄或者session過期後訪問任何接口與任何頁面都會返回登錄界面。
我們可以自定義session配置,在上面的ShiroConfig類裏,添加下面:

 /**
     * 安全管理器
     * 注:使用shiro-spring-boot-starter 1.4時,返回類型是SecurityManager會報錯,直接引用shiro-spring則不報錯
     *
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    
//使用默認的MemorySessionDAO
    @Bean
    public SessionDAO sessionDAO() {
        return new MemorySessionDAO();
    }

    /**
     * shiro session的管理
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {  //配置默認的sesssion管理器
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //設置session過期爲60秒,默認30分鐘
        sessionManager.setGlobalSessionTimeout(60 * 1000);
        sessionManager.setSessionDAO(sessionDAO());
        Collection<SessionListener> listeners = new ArrayList<>();
        listeners.add(new BDSessionListener());
        sessionManager.setSessionListeners(listeners);
        return sessionManager;
    }

    //Session監聽
    private class BDSessionListener implements SessionListener{
        @Override
        public void onStart(Session session) {

        }

        @Override
        public void onStop(Session session) {

        }

        @Override
        public void onExpiration(Session session) {

        }
    }

瀏覽器登錄成功後。60秒內,關閉頁面再進入,不需要重新登錄:
在這裏插入圖片描述
不操作60秒後,需要重新登錄。

取消對接口的限制

Shiro可以編譯過濾攔截規則,具體在ShiroConfig裏面。

   /**
     * 設置過濾規則
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSecurityManager(manager);
        shiroFilterFactoryBean.setSuccessUrl("/");
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");

        //注意此處使用的是LinkedHashMap,是有順序的,shiro會按從上到下的順序匹配驗證,匹配了就不再繼續驗證
        //所以上面的url要苛刻,寬鬆的url要放在下面,尤其是"/**"要放到最下面,如果放前面的話其後的驗證規則就沒作用了。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

按照這個規則,所有請求接口都會攔截,就是任何請求,沒有登錄過都會返回登錄頁面。
可以按照下面的修改,只攔截指定頁面:

/**
     * 設置過濾規則
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSecurityManager(manager);
        shiroFilterFactoryBean.setSuccessUrl("/");
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");

        //注意此處使用的是LinkedHashMap,是有順序的,shiro會按從上到下的順序匹配驗證,匹配了就不再繼續驗證
        //所以上面的url要苛刻,寬鬆的url要放在下面,尤其是"/**"要放到最下面,如果放前面的話其後的驗證規則就沒作用了。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        //authc是需要認證的,anon是不需要攔截認證
       // filterChainDefinitionMap.put("/**", "authc");
        //我們這裏設置index才需要攔截認證,其他接口都不攔截即可
        filterChainDefinitionMap.put("/index", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

具體一些更多相關,可以參下這篇文章https://www.cnblogs.com/qsymg/p/9836122.html

相關參考:
本文參考借鑑於https://blog.csdn.net/gnail_oug/article/details/80662553,感謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章