1. 程式人生 > >一起來學SpringBoot(十六)優雅的整合Shiro

一起來學SpringBoot(十六)優雅的整合Shiro

Apache Shiro是一個功能強大且易於使用的Java安全框架,可執行身份驗證,授權,加密和會話管理。藉助Shiro易於理解的API,您可以快速輕鬆地保護任何應用程式 - 從最小的移動應用程式到最大的Web和企業應用程式。網上找到大部分文章都是以前SpringMVC下的整合方式,很多人都不知道shiro提供了官方的starter可以方便地跟SpringBoot整合。

整合準備

這篇文件的介紹也相當簡單。我們只需要按照文件說明,然後在spring容器中注入一個我們自定義的Realm,shiro通過這個realm就可以知道如何獲取使用者資訊來處理鑑權(Authentication),如何獲取使用者角色、許可權資訊來處理授權(Authorization)

。如果是web應用程式的話需要引入shiro-spring-boot-web-starter,單獨的應用程式的話則引入shiro-spring-boot-starter

依賴

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.4.0-RC2</version>
</dependency>

使用者實體

首先建立一個使用者的實體,用來做認證

package com.maoxs.pojo;

import lombok.Data;

import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

@Data
public class User  implements Serializable {
    private Long uid;       // 使用者id
    private String uname;   // 登入名,不可改
    private String nick;    // 使用者暱稱,可改
private String pwd; // 已加密的登入密碼 private String salt; // 加密鹽值 private Date created; // 建立時間 private Date updated; // 修改時間 private Set<String> roles = new HashSet<>(); //使用者所有角色值,用於shiro做角色許可權的判斷 private Set<String> perms = new HashSet<>(); //使用者所有許可權值,用於shiro做資源許可權的判斷 }

這裡了為了方便,就不去資料庫讀取了,方便測試我們把,許可權資訊,角色資訊,認證資訊都靜態模擬下。

Resources

package com.maoxs.service;

import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;

@Service
public class ResourcesService {
    /**
     * 模擬根據使用者id查詢返回使用者的所有許可權
     *
     * @param uid
     * @return
     */
    public Set<String> getResourcesByUserId(Long uid) {
        Set<String> perms = new HashSet<>();
        //三種程式語言代表三種角色:js程式設計師、java程式設計師、c++程式設計師
        //docker的許可權
        perms.add("docker:run");
        perms.add("docker:ps");
        //maven的許可權
        perms.add("mvn:debug");
        perms.add("mvn:test");
        perms.add("mvn:install");
        //node的許可權
        perms.add("npm:clean");
        perms.add("npm:run");
        perms.add("npm:test");
        return perms;
    }

}

Role

package com.maoxs.service;

import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;

@Service
public class RoleService {

    /**
     * 模擬根據使用者id查詢返回使用者的所有角色
     *
     * @param uid
     * @return
     */
    public Set<String> getRolesByUserId(Long uid) {
        Set<String> roles = new HashSet<>();
        //這裡用三個工具代表角色
        roles.add("docker");
        roles.add("maven");
        roles.add("node");
        return roles;
    }

}

User

package com.maoxs.service;

import com.maoxs.pojo.User;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Random;

@Service
public class UserService {

    /**
     * 模擬查詢返回使用者資訊
     *
     * @param uname
     * @return
     */
    public User findUserByName(String uname) {
        User user = new User();
        user.setUname(uname);
        user.setNick(uname + "NICK");
        user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho=");//密碼明文是123456
        user.setSalt("wxKYXuTPST5SG0jMQzVPsg==");//加密密碼的鹽值
        user.setUid(new Random().nextLong());//隨機分配一個id
        user.setCreated(new Date());
        return user;
    }
}

認證

Shiro 從從Realm獲取安全資料(如使用者、角色、許可權),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm獲取相應的使用者進行比較以確定使用者身份是否合法;也需要從Realm得到使用者相應的角色/許可權進行驗證使用者是否能進行操作;可以把Realm看成DataSource , 即安全資料來源。

Realm

package com.maoxs.realm;

import com.maoxs.cache.MySimpleByteSource;
import com.maoxs.pojo.User;
import com.maoxs.service.ResourcesService;
import com.maoxs.service.RoleService;
import com.maoxs.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Set;

/**
 * 這個類是參照JDBCRealm寫的,主要是自定義瞭如何查詢使用者資訊,如何查詢使用者的角色和許可權,如何校驗密碼等邏輯
 */
public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private ResourcesService resourcesService;

    //告訴shiro如何根據獲取到的使用者資訊中的密碼和鹽值來校驗密碼
    {
        //設定用於匹配密碼的CredentialsMatcher
        HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
        hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
        hashMatcher.setStoredCredentialsHexEncoded(false);
        hashMatcher.setHashIterations(1024);
        this.setCredentialsMatcher(hashMatcher);
    }


    //定義如何獲取使用者的角色和許可權的邏輯,給shiro做許可權判斷
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
        User user = (User) getAvailablePrincipal(principals);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        System.out.println("獲取角色資訊:" + user.getRoles());
        System.out.println("獲取許可權資訊:" + user.getPerms());
        info.setRoles(user.getRoles());
        info.setStringPermissions(user.getPerms());
        return info;
    }

    //定義如何獲取使用者資訊的業務邏輯,給shiro做登入
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        // Null username is invalid
        if (username == null) {
            throw new AccountException("請輸入使用者名稱");
        }
        User userDB = userService.findUserByName(username);
        if (userDB == null) {
            throw new UnknownAccountException("使用者不存在");
        }
        //查詢使用者的角色和許可權存到SimpleAuthenticationInfo中,這樣在其它地方
        //SecurityUtils.getSubject().getPrincipal()就能拿出使用者的所有資訊,包括角色和許可權
        Set<String> roles = roleService.getRolesByUserId(userDB.getUid());
        Set<String> perms = resourcesService.getResourcesByUserId(userDB.getUid());
        userDB.getRoles().addAll(roles);
        userDB.getPerms().addAll(perms);
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName());
        if (userDB.getSalt() != null) {
            info.setCredentialsSalt(new MySimpleByteSource(userDB.getSalt()));
        }
        return info;
    }

}

相關配置

然後呢在只需要吧這個Realm註冊到Spring容器中就可以啦

@Bean
public CustomRealm customRealm() {
   CustomRealm realm = new CustomRealm();
   return realm;  
}

為了保證實現了Shiro內部lifecycle函式的bean執行 也是shiro的生命週期,注入LifecycleBeanPostProcessor

@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
	return new LifecycleBeanPostProcessor();
}

緊接著配置安全管理器,SecurityManager是Shiro框架的核心,典型的Facade模式,Shiro通過SecurityManager來管理內部元件例項,並通過它來提供安全管理的各種服務。

@Bean
public DefaultWebSecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(customRealm());
    return securityManager;
}

除此之外Shiro是一堆一堆的過濾鏈,所以要對shiro 的過濾進行設定,

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    chainDefinition.addPathDefinition("favicon.ico", "anon");
    chainDefinition.addPathDefinition("/login", "anon");
    chainDefinition.addPathDefinition("/**", "user");
    return chainDefinition;
}

yml

這裡要說明下由於我們引入的是shiro-spring-boot-web-starter,官方對配置進行了一系列的簡化,並加入了一些自動配置項,所以我們要在yml中加入

shiro:
  web:
    enabled: true
  loginUrl: /login

除此之外呢還有這些屬性

預設值 描述
shiro.enabled true 啟用Shiro的Spring模組
shiro.web.enabled true 啟用Shiro的Spring Web模組
shiro.annotations.enabled true 為Shiro的註釋啟用Spring支援
shiro.sessionManager.deleteInvalidSessions true 從會話儲存中刪除無效會話
shiro.sessionManager.sessionIdCookieEnabled true 啟用會話ID到cookie,用於會話跟蹤
shiro.sessionManager.sessionIdUrlRewritingEnabled true 啟用會話URL重寫支援
shiro.userNativeSessionManager false 如果啟用,Shiro將管理HTTP會話而不是容器
shiro.sessionManager.cookie.maxAge -1 會話cookie最大年齡
shiro.sessionManager.cookie.domain 空值 會話cookie域
shiro.sessionManager.cookie.path 空值 會話cookie路徑
shiro.sessionManager.cookie.secure false 會話cookie安全標誌
shiro.rememberMeManager.cookie.maxAge 一年 RememberMe cookie最大年齡
shiro.rememberMeManager.cookie.domain 空值 RememberMe cookie域名
shiro.rememberMeManager.cookie.path 空值 RememberMe cookie路徑
shiro.rememberMeManager.cookie.secure false RememberMe cookie安全標誌
shiro.loginUrl /login.jsp 未經身份驗證的使用者重定向到登入頁面時使用的登入URL
shiro.successUrl / 使用者登入後的預設登入頁面(如果在當前會話中找不到替代)
shiro.unauthorizedUrl 空值 頁面將使用者重定向到未授權的位置(403頁)

在Controller中新增登入方法

@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public Result login(@RequestParam("username") String userName, @RequestParam("password") String Password) throws Exception {
    Subject currentUser = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(userName, Password);
    token.setRememberMe(true);// 預設不記住密碼
    try {
        currentUser.login(token); //登入
        log.info("==========登入成功=======");
        return new Result(true, "登入成功");

    } catch (UnknownAccountException e) {
        log.info("==========使用者名稱不存在=======");
        return new Result(false, "使用者名稱不存在");
    } catch (DisabledAccountException e) {
        log.info("==========您的賬戶已經被凍結=======");
        return new Result(false, "您的賬戶已經被凍結");
    } catch (IncorrectCredentialsException e) {
        log.info("==========密碼錯誤=======");
        return new Result(false, "密碼錯誤");
    } catch (ExcessiveAttemptsException e) {
        log.info("==========您錯誤的次數太多了吧,封你半小時=======");
        return new Result(false, "您錯誤的次數太多了吧,封你半小時");
    } catch (RuntimeException e) {
        log.info("==========執行異常=======");
        return new Result(false, "執行異常");
    }
}
@RequestMapping("/logout")
public String logOut() {
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "index";
}

這樣就實現了整合認證的流程,,如果token資訊與資料庫表總username和password資料一致,則該使用者身份認證成功。

鑑權

只用註解控制鑑權授權

使用註解的優點是控制的粒度細,並且非常適合用來做基於資源的許可權控制。

只用註解的話非常簡單。我們只需要使用url配置配置一下所以請求路徑都可以匿名訪問:

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
	DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
    //這裡配置所有請求路徑都可以匿名訪問
    chain.addPathDefinition("/**", "anon");
    // 這另一種配置方式。但是還是用上面那種吧,容易理解一點。
    // chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");
    return chain;
}

然後在控制器類上使用shiro提供的種註解來做控制:

註解 功能
@RequiresGuest 只有遊客可以訪問
@RequiresAuthentication 需要登入才能訪問
@RequiresUser 已登入的使用者或“記住我”的使用者能訪問
@RequiresRoles 已登入的使用者需具有指定的角色才能訪問
@RequiresPermissions 已登入的使用者需具有指定的許可權才能訪問

示例

/**
 * created by CaiBaoHong at 2018/4/18 15:51<br>
 *     測試shiro提供的註解及功能解釋
 */
@RestController
public class Test1Controller {
    // 由於TestController類上沒有加@RequiresAuthentication註解,
    // 不要求使用者登入才能呼叫介面。所以hello()和a1()介面都是可以匿名訪問的
    @GetMapping("/hello")
    public String hello() {
        return "hello spring boot";
    }

    // 遊客可訪問,這個有點坑,遊客的意思是指:subject.getPrincipal()==null
    // 所以使用者在未登入時subject.getPrincipal()==null,介面可訪問
    // 而使用者登入後subject.getPrincipal()!=null,介面不可訪問
    @RequiresGuest
    @GetMapping("/guest")
    public String guest() {
        return "@RequiresGuest";
    }

    // 已登入使用者才能訪問,這個註解比@RequiresUser更嚴格
    // 如果使用者未登入呼叫該介面,會丟擲UnauthenticatedException
    @RequiresAuthentication
    @GetMapping("/authn")
    public String authn() {
        return "@RequiresAuthentication";
    }

    // 已登入使用者或“記住我”的使用者可以訪問
    // 如果使用者未登入或不是“記住我”的使用者呼叫該介面,UnauthenticatedException
    @RequiresUser
    @GetMapping("/user")
    public String user() {
        return "@RequiresUser";
    }

    // 要求登入的使用者具有mvn:build許可權才能訪問
    // 由於UserService模擬返回的使用者資訊中有該許可權,所以這個介面可以訪問
    // 如果沒有登入,UnauthenticatedException
    @RequiresPermissions("mvn:install")
    @GetMapping("/mvnInstall")
    public String mvnInstall() {
        return "mvn:install";
    }

    // 要求登入的使用者具有mvn:build許可權才能訪問
    // 由於UserService模擬返回的使用者資訊中【沒有】該許可權,所以這個介面【不可以】訪問
    // 如果沒有登入,UnauthenticatedException
    // 如果登入了,但是沒有這個許可權,會報錯UnauthorizedException
    @RequiresPermissions("gradleBuild")
    @GetMapping("/gradleBuild")
    public String gradleBuild() {
        return "gradleBuild";
    }

    // 要求登入的使用者具有js角色才能訪問
    // 由於UserService模擬返回的使用者資訊中有該角色,所以這個介面可訪問
    // 如果沒有登入,UnauthenticatedException
    @RequiresRoles("docker")
    @GetMapping("/docker")
    public String docker() {
        return "docker programmer";
    }

    // 要求登入的使用者具有js角色才能訪問
    // 由於UserService模擬返回的使用者資訊中有該角色,所以這個介面可訪問
    // 如果沒有登入,UnauthenticatedException
    // 如果登入了,但是沒有該角色,會丟擲UnauthorizedException
    @RequiresRoles("python")
    @GetMapping("/python")
    public String python() {
        return "python programmer";
    }

}

注意 解決spring aop和註解配置一起使用的bug。如果您在使用shiro註解配置的同時,引入了spring aop的starter,會有一