Spring Boot Shiro


Shiro 核心 API

Subject:用戶主體(每次請求都會創建Subject)。

	principal:代表身份。可以是用戶名、郵件、手機號碼等等,用來標識一個登錄主體的身份。

	credential:代表憑證。常見的有密碼,數字證書等。

SecurityManager:安全管理器(關聯 Realm),用於安全校驗。

Realm:Shiro 連接數據的橋梁。


Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲。

Caching:緩存,比如用戶登錄后,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率。

CacheManager:緩存控制器,來管理如用戶、角色、權限等緩存的控制器。

SessionManager:會話管理,即用戶登錄后就是一次會話,在沒有退出之前,它的所有信息都在會話中。

SessionDAO:會話儲存。

Shiro 認證與授權

身份認證:

	Step1:應用程序代碼調用 Subject.login(token) 方法后,傳入代表最終用戶身份的 AuthenticationToken 實例 Token。

	Step2:將 Subject 實例委托給應用程序的 SecurityManager(Shiro 的安全管理)並開始實際的認證工作。

	Step3、4、5:SecurityManager 根據具體的 Realm 進行安全認證。

權限認證(涉及到三張表:用戶表、角色表和權限表):

	權限(Permission):即操作資源的權利(添加、修改、刪除、查看操作的權利)。
	
	角色(Role):指的是用戶擔任的角色,一個角色可以有多個權限。
	
	用戶(User):在 Shiro 中,代表訪問系統的用戶,即上面提到的 Subject 認證主體。


請求步驟:

	瀏覽器發出第一次請求的時候,去redis里找不到對應的session,會進入到登錄頁。
	
	當在登錄頁輸入完正確的賬號密碼后,才能登錄成功。

	根據登錄成功后的session生成sessionId,並傳到前端瀏覽器中,瀏覽器以cookie存儲,同時將session存儲到redis中。
	
	每次瀏覽器訪問后台,都會刷新session的過期時間expireTime。
	
	當瀏覽器再次請求時,將當前瀏覽器中的所有的cookie設置到request headers請求頭中。
	根據傳入的sessionId串到共享的redis存儲中匹配。
	如果匹配不到,則會跳轉到登錄頁,如果匹配成功,則會訪問通過。

Spring Boot Shiro 依賴

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

自定義 Realm

自定義 Realm 需要繼承 AuthorizingRealm 類,該類封裝了很多方法,且繼承自 Realm 類。

重寫以下兩個方法:
	doGetAuthenticationInfo() 方法:用來驗證當前登錄的用戶,獲取認證信息。
	doGetAuthorizationInfo() 方法:為當前登錄成功的用戶授予權限和分配角色。

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 登錄成功的用戶授予權限和分配角色
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //獲取用戶名
        String account = (String) principals.getPrimaryPrincipal();

        //從數據庫查詢用戶角色信息
        User user = userService.getUserByAccount(account);

        //設置角色
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        Set<String> roles = new HashSet();
        if (user.getAdmin()) {
            roles.add(Base.ROLE_ADMIN);
        }
        authorizationInfo.setRoles(roles);

        return authorizationInfo;
    }

    /**
     * 執行認證邏輯
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //獲取用戶名
        String account = (String) token.getPrincipal();

        //從數據庫查詢該用戶
        User user = userService.getUserByAccount(account);

        if (null == user) {
            throw new UnknownAccountException();    //沒找到該帳號
        }
        if (UserStatus.blocked.equals(user.getStatus())) {
            throw new LockedAccountException();	//帳號鎖定
        }

        //傳入用戶名和密碼進行身份認證,並返回認證信息
        return new SimpleAuthenticationInfo(
                user.getAccount(),
                user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()),  //鹽(密碼加鹽加密處理)
                getName()
        );
    }
}

自定義 SessionDAO

Cachemanager緩存里可以包含權限認證的緩存、用戶及權限信息的緩存等,也可以做Session緩存。

SessionDAO是做Session持久化的,可以使用Redis來存儲。

默認 SessionDAO:MemorySessionDAO:

	將Session保存在內存中,存儲結構是ConcurrentHashMap。

	public class MemorySessionDAO extends AbstractSessionDAO {

	    private ConcurrentMap<Serializable, Session> sessions = new ConcurrentHashMap();

	    protected Serializable doCreate(Session session) {
	        Serializable sessionId = this.generateSessionId(session);
	        this.assignSessionId(session, sessionId);
	        this.storeSession(sessionId, session);
	        return sessionId;
	    }
	}


/**
 * 將Session保存到Redis
 */
public class CustomSessionDAO extends CachingSessionDAO {

    @Autowired
    private RedisTemplate redisTemplate;

    //默認緩存過期時間:30分鍾
    public final static long DEFAULT_EXPIRE = 60 * 30;

    @Override
    protected Serializable doCreate(Session session) {
        //創造SessionId
        Serializable sessionId = generateSessionId(session);
        //注冊SessionId
        assignSessionId(session, sessionId);
        //緩存Session
        redisTemplate.opsForValue().set(sessionId.toString(), session, DEFAULT_EXPIRE, TimeUnit.SECONDS);
        return sessionId;
    }

    @Override
    protected void doUpdate(Session session) {
        if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
            //會話過期/停止
            return;
        }
        redisTemplate.opsForValue().set(session.getId().toString(), session, DEFAULT_EXPIRE, TimeUnit.SECONDS);
    }

    @Override
    protected void doDelete(Session session) {
        redisTemplate.delete(session.getId().toString());
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        return (Session) redisTemplate.opsForValue().get(sessionId.toString());
    }
}

自定義 SessionManager(待完善)

public class CustomSessionManager extends DefaultWebSessionManager {

    public static final String TOKEN = "token";

    /**
     * 調用登陸接口的時候,是沒有token的。
     * 登陸成功后,產生了token,我們把它放到request中。
     * 返回結果給客戶端的時候,把它從request中取出來,並且傳遞給客戶端。
     * 客戶端每次帶着這個token過來,就相當於是瀏覽器的cookie的作用,也就能維護會話了。
     */
    @Override
    public Serializable getSessionId(SessionKey key) {
        Serializable sessionId = key.getSessionId();
        if(sessionId == null && WebUtils.isWeb(key)){
            HttpServletRequest request = WebUtils.getHttpRequest(key);
            HttpServletResponse response = WebUtils.getHttpResponse(key);
            sessionId = this.getSessionId(request,response);
        }
        HttpServletRequest request = WebUtils.getHttpRequest(key);
        request.setAttribute(TOKEN,sessionId.toString());
        return sessionId;
    }

    /**
     * DefaultWebSessionManager默認實現中,是通過Cookie確定SessionId。
     * 重寫時,只需要把獲取SessionId的方式變更為在request header中獲取即可。
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String id = httpRequest.getHeader(TOKEN);

        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        }
        return super.getSessionId(request, response);
    }
}

配置類 ShiroConfig

@Configuration
public class ShiroConfig {

    /**
     * 創建 ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        
        //設置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //LinkedHashMap 是有序的,進行順序攔截器配置
        Map<String, String> filterMap = new LinkedHashMap();
        filterMap.put("/static/**", "anon");	//無需認證可以訪問
        filterMap.put("/login", "anon");
        filterMap.put("/register", "anon");

        //配置退出過濾器,其中具體的退出代碼Shiro已經替我們實現了,登出后跳轉配置的LoginUrl
        filterMap.put("/logout", "logout");

        filterMap.put("/**/create", "authc");	//必須認證才可以訪問
        filterMap.put("/**/update", "authc");
        filterMap.put("/**/delete", "authc");
        filterMap.put("/upload", "authc");

        filterMap.put("/admin", "perms[admin]");	//資源必須得到資源權限才能訪問,多個參數寫法:perms["admin,user"]
        filterMap.put("/admin", "role[admin]");	//資源必須得到角色權限才能訪問

        filterMap.put("/**", "anon");


        //設置默認登錄的URL,身份認證失敗會訪問該URL
        shiroFilterFactoryBean.setLoginUrl("/login");
        //身份認證設置成功之后要跳轉的URL
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //設置未授權界面,權限認證失敗會訪問該URL
        shiroFilterFactoryBean.setUnauthorizedUrl("/unAuthorized");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 配置安全管理器:DefaultWebSecurityManager
     */
    @Bean
    public SecurityManager securityManager(CustomRealm realm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

    /**
     * 創建自定義 Realm
     */
    @Bean
    public CustomRealm customRealm() {
        CustomRealm shiroRealm = new CustomRealm();
        //配置密碼加密
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("md5");    //加密方式
        matcher.setHashIterations(2);   //加密次數
        shiroRealm.setCredentialsMatcher(matcher);
        return shiroRealm;
    }

    /**
     * 配置 SessionManager
     */
    @Bean
    public SessionManager sessionManager() {
        CustomSessionManager customSessionManager = new CustomSessionManager();
        //session過期時間:1小時(默認半小時)
        customSessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
        customSessionManager.setSessionDAO(new CustomSessionDAO());
        return customSessionManager;
    }

    /**
     * Spring Boot Shiro 開啟注釋
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

Session的查詢、刷新

SimpleSession幾個屬性:

	Serializable id:session id;

	Date startTimestamp:session的創建時間;

	Date stopTimestamp:session的失效時間;

	Date lastAccessTime:session的最近一次訪問時間,初始值是startTimestamp

	long timeout:session的有效時長,默認30分鍾

	boolean expired:session是否到期

	Map<Object, Object> attributes:session的屬性容器


查詢:Session session = SecurityUtils.getSubject().getSession();	//返回的就是綁定在當前subjuct的session。

刷新:SimpleSession的touch()

	public void touch() {
		this.lastAccessTime = new Date();
	}

	Web應用,每次進入ShiroFilter都會自動調用session.touch()來更新最后訪問時間。

	過期時間判斷:當前時間-lastAccessTime=是否超過有效時長。

Shiro支持三種方式的授權

1、編程式,通過寫if/else授權代碼塊

	Subject subject = SecurityUtils.getSubject();
	if(subject.hasRole("admin")) {
	    // 有權限,執行相關業務
	} else {
	    // 無權限,給相關提示
	}

2、注解式,通過在執行的Java方法上放置相應的注解完成

	@RequiresPermissions("admin")
	public List<User> listUser() {
	    // 有權限,獲取數據  
	}

3、JSP/GSP標簽,在JSP/GSP頁面通過相應的標簽完成

	<shiro:hasRole name="admin">
	    <!-- 有權限 -->
	</shiro:hasRole>

Spring Boot 集成 Shiro 和 Ehcache(待完善)

引入配置文件 ehcache.xml:

	application.xml配置文件添加:spring.cache.ehcache.config=classpath:ehcache.xml

	<?xml version="1.0" encoding="UTF-8"?>
	<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
	         updateCheck="false">
	    
	    <diskStore path="java.io.tmpdir/shiro-cache"/>

	    <defaultCache	//默認緩存策略
	            eternal="false"	//對象是否永久有效,一但設置了,timeout將不起作用
	            maxElementsInMemory="1000"	//緩存最大元素數目
	            overflowToDisk="false"	//當內存中對象數量達到maxElementsInMemory時,Ehcache將對象寫到磁盤中。
	            diskPersistent="false"	//是否在磁盤上持久化。指重啟JVM后,數據是否有效。默認為false
	            timeToIdleSeconds="0"	//設置對象在失效前的允許閑置時間(單位:s),默認是0(永久有效)。
	            timeToLiveSeconds="600"	//設置對象在失效前允許存活時間(單位:s),默認是0(永久有效)。
	            //當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存
	            //LRU(最近最少使用,默認策略)、FIFO(先進先出)、LFU(最少訪問次數)
	            memoryStoreEvictionPolicy="LRU" />
	 
	    <cache
	            name="users"	//緩存名稱
	            eternal="false"
	            maxElementsInMemory="500"
	            overflowToDisk="false"
	            diskPersistent="false"
	            timeToIdleSeconds="0"
	            timeToLiveSeconds="300"
	            memoryStoreEvictionPolicy="LRU" />
	</ehcache>

Controller 與 Service 使用

Controller:

    @PostMapping("/login")
    public Result login(@RequestBody User user) {
        Result r = new Result();
        //獲取Subject用戶主體
        Subject subject = SecurityUtils.getSubject();
        //封裝用戶數據
        UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(), user.getPassword());
        try {
            //執行認證操作(調用UserRealm中的方法認證)
            subject.login(token);
            //認證通過
            User currentUser = userService.getUserByAccount(user.getAccount());
            subject.getSession().setAttribute(Base.CURRENT_USER, currentUser);
            r.setResultCode(ResultCode.SUCCESS);
            r.getData().put("token", subject.getSession().getId());
        } catch (UnknownAccountException e) {
            r.setResultCode(ResultCode.USER_NOT_EXIST); //用戶不存在
        } catch (LockedAccountException e) {
            r.setResultCode(ResultCode.USER_ACCOUNT_FORBIDDEN); //賬號被鎖定
        }catch (IncorrectCredentialsException e) {
            r.setResultCode(ResultCode.USER_LOGIN_PASSWORD_ERROR);   //密碼錯誤
        } catch (AuthenticationException e) {
            r.setResultCode(ResultCode.USER_LOGIN_ERROR);   //認證錯誤(包含以上錯誤)
        }
        return r;
    }

    @PostMapping("/register")
    public Result register(@RequestBody User user) {
        Result r = new Result();

        User temp = userService.getUserByAccount(user.getAccount());
        if (null != temp) {
            r.setResultCode(ResultCode.USER_HAS_EXISTED);
            return r;
        }

        userService.saveUser(user);

		r.setResultCode(ResultCode.SUCCESS);
        return r;
    }

Service:

    @Override
    @Transactional
    public void saveUser(User user) {
    	//密碼加密
        String newPassword = new SimpleHash(
                "md5",user.getPassword(),
                ByteSource.Util.bytes("salt"),2).toHex();

        user.setPassword(newPassword);

        return userRepository.save(user);
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM