SpringBoot-Shiro用戶認證 (1)

SpringBoot-Shiro用戶認證 (2019.12.12)

在Spring Boot中集成Shiro進行用戶的認證過程主要可以歸納爲以下三點:

1、定義一個ShiroConfig,然後配置SecurityManager Bean,SecurityManager爲Shiro的安全管理器,管理着所有Subject;

2、在ShiroConfig中配置ShiroFilterFactoryBean,其爲Shiro過濾器工廠類,依賴於SecurityManager;

3、自定義Realm實現,Realm包含doGetAuthorizationInfo()doGetAuthenticationInfo()方法,因爲本文只涉及用戶認證,所以只實現doGetAuthenticationInfo()方法。

1. 搭建一個SpringBoot-Shiro程序,然後引入Shiro、MyBatis、數據庫和thymeleaf 依賴

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--mybatis依賴-->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>${mybatis.version}</version>
    </dependency>
    <!--使用阿里巴巴的德魯伊作爲數據源-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.10</version>
    </dependency>
  <!--mysql數據庫-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
	<!-- thymeleaf -->
	<dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-thymeleaf</artifactId>
	</dependency>
    <!--Boot整合的Shiro 依賴 或者引入單獨的  shiro-spring -->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring-boot-starter</artifactId>
      <version>1.4.0-RC2</version>
    </dependency>
    <!--lombok-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>

2. 定義一個Shiro配置類,名稱爲ShiroConfig

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 設置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 登錄的url
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登錄成功後跳轉的url
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 未授權url
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        
        // 定義filterChain,靜態資源不攔截
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        // druid數據源監控頁面不攔截(可選)
        filterChainDefinitionMap.put("/druid/**", "anon");
        // 配置退出過濾器,其中具體的退出代碼Shiro已經替我們實現了 
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/", "anon");
        // 除上以外所有url都必須認證通過纔可以訪問,未通過認證自動訪問LoginUrl
        filterChainDefinitionMap.put("/**", "authc");
        
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
	
    @Bean  //這裏有個坑使用的依賴是shiro-spring-boot-xx 返回值得是DefaultWebSecurityManager
    public DefaultWebSecurityManager securityManager(){  
        // 配置SecurityManager,並注入shiroRealm
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        return securityManager;  
    } 
	
    @Bean  
    public ShiroRealm shiroRealm(){  
        // 配置Realm,需自己實現
        ShiroRealm shiroRealm = new ShiroRealm();  
        return shiroRealm;  
    }  
}

需要注意的是filterChain基於短路機制,即最先匹配原則,如:

/user/**=anon
/user/aa=authc 永遠不會執行

配置完ShiroConfig後,接下來對Realm進行實現,然後注入到SecurityManager中。

3. 配置ShiroRealm.java

自定義Realm實現只需繼承AuthorizingRealm類,然後實現doGetAuthorizationInfo()和doGetAuthenticationInfo()方法即可。這兩個方法名乍看有點像,authorization發音[ˌɔ:θəraɪˈzeɪʃn],爲授權,批准的意思,即獲取用戶的角色和權限等信息;authentication發音[ɔ:ˌθentɪ’keɪʃn],認證,身份驗證的意思,即登錄時驗證用戶的合法性,比如驗證用戶名和密碼。

public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserMapper userMapper;
    
    /**
    * 獲取用戶角色和權限
    */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        return null;
    }

    /**
     * 登錄認證
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    	// 獲取用戶輸入的用戶名和密碼
        String userName = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());
        
        //System.out.println("用戶" + userName + "認證-----ShiroRealm.doGetAuthenticationInfo");

        // 通過用戶名到數據庫查詢用戶信息
        User user = userMapper.findByUserName(userName);
        
        if (user == null) {
            throw new UnknownAccountException("用戶名或密碼錯誤!");
        }
        if (!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException("用戶名或密碼錯誤!");
        }
        if (user.getStatus().equals("0")) {
            throw new LockedAccountException("賬號已被鎖定,請聯繫管理員!");
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
    }
}

因爲本節只講述用戶認證,所以doGetAuthorizationInfo()方法先不進行實現。

其中UnknownAccountException等異常爲Shiro自帶異常,Shiro具有豐富的運行時AuthenticationException層次結構,可以準確指出嘗試失敗的原因。你可以包裝在一個try/catch塊,並捕捉任何你希望的異常,並作出相應的反應。例如:

try {
    currentUser.login(token);
} catch ( UnknownAccountException uae ) { ...
} catch ( IncorrectCredentialsException ice ) { ...
} catch ( LockedAccountException lae ) { ...
} catch ( ExcessiveAttemptsException eae ) { ...
} ... catch your own ...
} catch ( AuthenticationException ae ) {
    //unexpected error?
}

雖然我們可以準確的獲取異常信息,並根據這些信息給用戶提示具體錯誤,但最安全的做法是在登錄失敗時僅向用戶顯示通用錯誤提示信息,例如“用戶名或密碼錯誤”。這樣可以防止數據庫被惡意掃描。

在Realm中UserMapper爲Dao層,標準的做法應該還有Service層。接下來編寫和數據庫打交道的Dao層。

4.配置文件 application.yml

server:
  port: 8080
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo?serverTimezone=UTC
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
    #數據源其他配置
    druid:
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
mybatis:
  mapper-locations: mapper/*.xml
  type-aliases-package: com.zhihao.entity

5. 定義實體類(User省略),接口UserMapper外加上UserMapper.xml配置實現

@Mapper
public interface UserMapper {

    User findUserByName(String name);
}
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhihao.dao.UserMapper">
  <resultMap type="com.zhihao.entity.User" id="baseUser">
    <id column="id" property="id" javaType="java.lang.String" jdbcType="VARCHAR"/>
    <id column="username" property="username" javaType="java.lang.String" jdbcType="VARCHAR"/>
    <id column="password" property="password" javaType="java.lang.String" jdbcType="VARCHAR"/>
    <id column="create_time" property="createTime" javaType="java.util.Date" jdbcType="DATE"/>
    <id column="status" property="status" javaType="java.lang.String" jdbcType="VARCHAR"/>
  </resultMap>
  <sql id="baseAll">
    id,username,password,status,create_time
  </sql>
  <select id="findUserByName" resultMap="baseUser" parameterType="String">
    select 
    <include refid="baseAll"/>
    from user where username = #{name}
  </select>
</mapper>

6. UserController.java

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
/**
 * @Author: zhihao
 * @Date: 2019/12/12 13:58
 * @Description: 用戶登錄
 * @Versions 1.0
 **/
@RestController
public class UserController { 
    private Map<String,Object> resultMap = new ConcurrentHashMap();
    /**
     * 登錄認證
     *
     * @param username 用戶名
     * @param password 密碼
     * @return java.util.Map 簡陋的結果包裝
     * @author: zhihao
     * @date: 2019/12/12
     * {@link #}
     */
    @PostMapping("/login")
    public Map login(@NotNull String name, @NotNull String password){
        // 密碼md5加密 (省略)

        UsernamePasswordToken token = new UsernamePasswordToken(name, password);
        //獲取Subject對象
        Subject subject = SecurityUtils.getSubject();
        try {
            //沒有拋出異常,說明登錄成功
            subject.login(token);
            resultMap.put("code", "success");
        } catch (UnknownAccountException e){
            //返回自定義認證失敗的異常信息返回
            resultMap.put("msg", e.getMessage());
        }catch (IncorrectCredentialsException e) {
            resultMap.put("msg", e.getMessage());
        }catch (LockedAccountException e) {
            resultMap.put("msg", e.getMessage());
        }catch (AuthenticationException e) {
            resultMap.put("msg", "認證失敗");
        }
        return resultMap;
    }

     /** 
     * 登錄的url 解析視圖到登錄頁面
     * @return org.springframework.web.servlet.ModelAndView 
     * @author: zhihao
     * @date: 2019/12/12 
     * {@link #}
     */
    @GetMapping("/login")
    public ModelAndView login() {
        ModelAndView view = new ModelAndView();
        view.setViewName("login");
        return view;
    }

    /** 
     * 退出(註銷)訪問根目錄解析視圖到登錄頁面
     * @return org.springframework.web.servlet.ModelAndView 
     * @author: zhihao
     * @date: 2019/12/12 
     * {@link #}
     */
    @GetMapping("/")
    public ModelAndView logins() {
        ModelAndView view = new ModelAndView();
        view.setViewName("login");
        return view;
    }
}

7. 接下來編寫login.html和index.html頁面

編寫登錄頁面login.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>登錄</title>
</head>
<script type="text/javascript" src="/js/jquery-3.3.1.min.js"></script>
<body>
<div>
  <div>
    <input type="text" placeholder="用戶名" name="username" id="username" required="required"/>
    <input type="password" placeholder="密碼" name="password" id="password" required="required"/>
    <button onclick="login()">登錄</button>
  </div>
</div>
</body>
<script type="text/javascript">
  function login() {
    var username = $("#username").val();
    var password = $("#password").val();
    $.ajax({
      type: "post",
      url: "/login",
      data: {"username": username, "password": password},
      dataType: "json",
      success: function (result) {
        if (result.code == "success") {
          location.href ='/index';
        } else {
          alert(result.msg);
        }
      }
    });
  }
</script>
</html>

首頁:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>首頁</title>
</head>
<body> 
<p>你好![[${user.userName}]]</p>
<a th:href="@{/logout}">註銷</a>
</body>
</html>

8. 測試

http://localhost:8080/

http://localhost:8080/index

http://localhost:8080/aaaaaaa

http://localhost:8080/web

可發現頁面都被重定向到http://localhost:8080/login:

登錄成功後,點擊註銷連接,根據ShiroConfig的配置filterChainDefinitionMap.put("/logout", "logout"),Shiro會自動幫我們註銷用戶信息,並重定向到/路徑。

擴展資料:

攔截規則anonauthc等爲Shiro爲我們實現的過濾器,具體如下表所示:

Filter Name Class Description
anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名攔截器,即不需要登錄即可訪問;一般用於靜態資源過濾;示例/static/**=anon
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基於表單的攔截器;如/**=authc,如果沒有登錄會跳到相應的登錄頁面登錄
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP身份驗證攔截器
logout org.apache.shiro.web.filter.authc.LogoutFilter 退出攔截器,主要屬性:redirectUrl:退出成功後重定向的地址(/),示例/logout=logout
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter 不創建會話攔截器,調用subject.getSession(false)不會有什麼問題,但是如果subject.getSession(true)將拋出DisabledSessionException異常
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 權限授權攔截器,驗證用戶是否擁有所有權限;屬性和roles一樣;示例/user/**=perms["user:create"]
port org.apache.shiro.web.filter.authz.PortFilter 端口攔截器,主要屬性port(80):可以通過的端口;示例/test= port[80],如果用戶訪問該頁面是非80,將自動將請求端口改爲80並重定向到該80端口,其他路徑/參數等都一樣
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter rest風格攔截器,自動根據請求方法構建權限字符串;示例/users=rest[user],會自動拼出user:read,user:create,user:update,user:delete權限字符串進行權限匹配(所有都得匹配,isPermittedAll)
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 角色授權攔截器,驗證用戶是否擁有所有角色;示例/admin/**=roles[admin]
ssl org.apache.shiro.web.filter.authz.SslFilter SSL攔截器,只有請求協議是https才能通過;否則自動跳轉會https端口443;其他和port攔截器一樣;
user org.apache.shiro.web.filter.authc.UserFilter 用戶攔截器,用戶已經身份驗證/記住我登錄的都可;示例/**=user

用戶表:

/*
 Navicat Premium Data Transfer

 Source Server         : 本地數據庫
 Source Server Type    : MySQL
 Source Server Version : 50540
 Source Host           : localhost:3306
 Source Schema         : shiro

 Target Server Type    : MySQL
 Target Server Version : 50540
 File Encoding         : 65001

 Date: 12/12/2019 17:20:50
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用戶id',
  `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用戶名',
  `password` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用戶密碼',
  `status` char(1) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '1' COMMENT '用戶狀態',
  `create_time` datetime NOT NULL COMMENT '創建時間',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '123', '321', '1', '2019-12-12 15:53:28');

SET FOREIGN_KEY_CHECKS = 1;

項目代碼(點擊打開)

發佈了29 篇原創文章 · 獲贊 3 · 訪問量 1618
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章