架構之路之spring+shiro的整合__架構之路

來源:互聯網
上載者:User
1.前言 1.1 shiro介紹


Authentication:身份認證/登入,驗證使用者是不是擁有相應的身份;
Authorization:授權,即許可權驗證,驗證某個已認證的使用者是否擁有某個許可權;即判斷使用者是否能做事情,常見的如:驗證某個使用者是否擁有某個角色。或者細粒度的驗證某個使用者對某個資源是否具有某個許可權;
Session Manager:會話管理,即使用者登入後就是一次會話,在沒有退出之前,它的所有資訊都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;
Cryptography:加密,保護資料的安全性,如密碼加密儲存到資料庫,而不是明文儲存;
Web Support:Web支援,可以非常容易的整合到Web環境;
Caching:緩衝,比如使用者登入後,其使用者資訊、擁有的角色/許可權不必每次去查,這樣可以提高效率;
Concurrency:shiro支援多線程應用的並發驗證,即如在一個線程中開啟另一個線程,能把許可權自動傳播過去;
Testing:提供測試支援;
Run As:允許一個使用者假裝為另一個使用者(如果他們允許)的身份進行訪問;
Remember Me:記住我,這個是非常常見的功能,即一次登入後,下次再來的話不用登入了。

1.2使用api

Subject:主體,代表了當前“使用者”,這個使用者不一定是一個具體的人,與當前應用互動的任何東西都是Subject,如網路爬蟲,機器人等;即一個抽象概念;所有Subject都綁定到SecurityManager,與Subject的所有互動都會委託給SecurityManager;可以把Subject認為是一個門面;SecurityManager才是實際的執行者;

SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager互動;且它管理著所有Subject;可以看出它是Shiro的核心,它負責與後邊介紹的其他組件進行互動,如果學習過SpringMVC,你可以把它看成DispatcherServlet前端控制器;

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

也就是說對於我們而言,最簡單的一個Shiro應用:
1、應用代碼通過Subject來進行認證和授權,而Subject又委託給SecurityManager;
2、我們需要給Shiro的SecurityManager注入Realm,從而讓SecurityManager能得到合法的使用者及其許可權進行判斷。

2.整合項目 2.1 依賴

<!-- shiro -->        <dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-core</artifactId>            <version>1.2.3</version>        </dependency>        <dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-web</artifactId>            <version>1.2.3</version>        </dependency>        <dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-spring</artifactId>            <version>1.2.3</version>        </dependency>

2.2 shiro-context.xml

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">    <!--自訂許可權認證-->    <bean id="authRealm" class="com.tl.skyLine.shiro.AuthRealm">        <!--自訂密碼密碼編譯演算法  -->        <!--<property name="credentialsMatcher" ref="passwordMatcher"/>-->        <!--<property name="roleDao" ref="roleDao"/>-->        <!--<property name="permissionDao" ref="permissionDao"/>-->    </bean>    <!-- 設定密碼加密策略 md5hash -->    <!--<bean id="passwordMatcher" class="com.fyh.www.shiro.CustomCredentialsMatcher"/>-->    <!-- 配置許可權管理器(核心) -->    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">        <!-- 我們自訂的realm -->        <property name="realm" ref="authRealm"/>        <!-- 緩衝管理器 -->        <property name="cacheManager" ref="cacheManager"/>    </bean>    <!-- 此bean要被web.xml引用,和web.shiroFilter -->    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">        <!-- 許可權管理器 -->        <property name="securityManager" ref="securityManager"/>        <!-- 登入地址 -->        <property name="loginUrl" value="/pages/login.jsp"/>        <!-- 登入後跳轉到業務頁面 -->        <property name="successUrl" value="/pages/home.jsp"/>        <!-- 錯誤頁面 -->        <property name="unauthorizedUrl" value="/pages/unauthorized.jsp"/>        <!-- 許可權配置 -->        <property name="filterChainDefinitions">            <value>                <!-- 匿名登入請求 -->                /public/** = anon                <!-- **代表任意子目錄 -->                /static/**=anon                <!-- 需要許可權為edit的使用者才能訪問此請求-->                /user/edit=perms[user:edit]                <!-- 需要管理員角色才能訪問此頁面 -->                <!--/user/edit=roles[admin]-->                <!--攔截非靜態資源的所有請求-->                /** = authc                <!--authc:確保已認證的使用者發送的請求才能通過(若未認證,則跳轉到登入頁面)-->            </value>        </property>    </bean>    <!-- 使用者授權/認證資訊Cache, 採用EhCache  緩衝 -->    <!--<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">-->    <!--<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>-->    <!--</bean>-->    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>    <!-- 保證實現了Shiro內部lifecycle函數的bean執行 -->    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>    <!-- 安全管理器 -->    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">        <property name="securityManager" ref="securityManager"/>    </bean>    <!--啟用Shiro相關的註解-->    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"          depends-on="lifecycleBeanPostProcessor"/>    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">        <property name="securityManager" ref="securityManager"/>    </bean></beans>

2.3 匯入spring-context.xml和springmvc-context.xml中

<import resource="shiro-context.xml"/>

這邊對之前的配置要做一些修改,涉及到spring父子容器的查看範圍的問題,參考下一篇文章

點擊開啟連結

修改的spring-context.xml:

 <context:component-scan base-package="com.tl.skyLine">        <!-- 排除不掃描的,controller放在子容器springmvc的設定檔中 -->        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>    </context:component-scan>

修改的springmvc-context.xml:

<!--只在springmvc容器中配置controller的掃描-->    <context:component-scan base-package="com.tl.skyLine" use-default-filters="false">        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>    </context:component-scan>

2.4 web.xml

<filter>        <filter-name>shiroFilter</filter-name>        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>    </filter>    <filter-mapping>        <filter-name>shiroFilter</filter-name>        <url-pattern>/*</url-pattern>    </filter-mapping>

這個shiroFilter與shiro-context.xml中的<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">對應。

2.5 自訂AuthRealm

package com.tl.skyLine.shiro;import com.tl.skyLine.model.User;import com.tl.skyLine.repository.PermissionDao;import com.tl.skyLine.repository.RoleDao;import com.tl.skyLine.repository.UserDao;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.stereotype.Component;import java.util.stream.Collectors;/** * Created by tl on 17/2/20. */@Componentpublic class AuthRealm extends AuthorizingRealm {    @Autowired    private RoleDao roleDao;    @Autowired    private PermissionDao permissionDao;    @Autowired    private UserDao userDao;    /**     * 用來為當前登陸成功的使用者授予許可權和角色(已經登陸成功了)     */    @Override    protected AuthorizationInfo doGetAuthorizationInfo(            PrincipalCollection principals) {        //擷取使用者名稱        //String username = (String) principals.getPrimaryPrincipal();        //擷取目前使用者        User user = (User) principals.fromRealm(getName()).iterator().next();        //得到許可權字串        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();        info.addRoles(roleDao.getRoles(user.getId())                .stream().map(role -> role.getName()).collect(Collectors.toList()));        info.addStringPermissions(permissionDao.getPermissionByUser(user.getId())                .stream().map(permission -> permission.getName()).collect(Collectors.toList()));        return info;    }    /**     * 用來驗證當前登入的使用者,擷取認證資訊     */    @Override    protected AuthenticationInfo doGetAuthenticationInfo(            AuthenticationToken authcToken) throws AuthenticationException {        UsernamePasswordToken upToken = (UsernamePasswordToken) authcToken;        User user = userDao.findOneByUsername(upToken.getUsername());        if (user == null) {            return null;        } else {            AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());            return info;        }    }}

在認證、授權內部實現機制中都有提到,最終處理都將交給Realm進行處理。 因為在Shiro中,最終是通過Realm來擷取應用程式中的使用者、角色及許可權資訊的。通常情況下,在Realm中會直接從我們的資料來源中擷取Shiro需要的驗證資訊。 可以說,Realm是專用於安全架構的DAO.

shiro登陸成功,再次訪問url的時候會請求doGetAuthorizationInfo時候會將登陸使用者對應的角色,許可權全部查到,然後set到SimpleAuthorizationInfo執行個體對象info中,儲存的分別是role和permission對象的name欄位,


這個name子段跟shiro-context.xml中的shiroFilterbean對象的filterChainDefinitions屬性對應:


2.6 角色權限類別的結構


我這邊是用mongodb資料庫,沒有外部索引鍵關聯約束,就是基本的使用者,角色,許可權,然後外加兩個中間表,我就不一一貼出了。 2.7 repository

由於非關聯式資料庫的約束性,這邊做表關聯查詢沒有關係型資料庫那麼簡單,這個對整體架構沒有影響,我把這個代碼貼一下:

RoleDaoImpl:

package com.tl.skyLine.repository.impl;import com.tl.skyLine.model.Role;import com.tl.skyLine.model.UserRole;import com.tl.skyLine.repository.RoleDao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;/** * RoleDaoImpl * Created by tl on 17/2/13. */@Component("roleDao")public class RoleDaoImpl implements RoleDao {    @Autowired    private MongoTemplate mongoTemplate;    @Override    public Role findOne(String roleId) {        return this.mongoTemplate.findOne(new Query().addCriteria(Criteria.where("id").is(roleId)), Role.class);    }    @Override    public List<Role> getRoles(String userId) {        Query query = new Query();        query.addCriteria(Criteria.where("userId").is(userId));        List<UserRole> userRoles = this.mongoTemplate.find(query, UserRole.class);        List<Role> roles = new ArrayList<Role>();        userRoles.stream().forEach(userRole -> {            roles.add(this.findOne(userRole.getRoleId()));        });        return roles;    }    @Override    public void store(Role role) {        this.mongoTemplate.save(role);    }    @Override    public void store(UserRole userRole) {        this.mongoTemplate.save(userRole);    }    @Override    public Role findOneByName(String name) {        return this.mongoTemplate.findOne(                new Query().addCriteria(Criteria.where("name").is(name))                , Role.class);    }}

PermissionImpl:

package com.tl.skyLine.repository.impl;import com.tl.skyLine.model.Permission;import com.tl.skyLine.model.RolePermission;import com.tl.skyLine.repository.PermissionDao;import com.tl.skyLine.repository.RoleDao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;/** * PermissionDaoImpl * Created by tl on 17/2/13. */@Component("permissionDao")public class PermissionDaoImpl implements PermissionDao {    @Autowired    private MongoTemplate mongoTemplate;    @Autowired    private RoleDao roleDao;    @Override    public Permission findOne(String permissionId) {        return this.mongoTemplate.findOne(new Query().addCriteria(Criteria.where("id").is(permissionId)), Permission.class);    }    @Override    public List<Permission> getPermissionByRole(String roleId) {        Query query = new Query();        query.addCriteria(Criteria.where("roleId").is(roleId));        List<RolePermission> rolePermissions = this.mongoTemplate.find(query, RolePermission.class);        List<Permission> permissions = new ArrayList<Permission>();        rolePermissions.stream().forEach(rolePermission -> {            permissions.add(this.findOne(rolePermission.getPermissionId()));        });        return permissions;    }    @Override    public List<Permission> getPermissionByUser(String userId) {        List<Permission> permissions = new ArrayList<Permission>();        roleDao.getRoles(userId).stream().forEach(role -> {            permissions.addAll(this.getPermissionByRole(role.getId()));        });        return permissions;    }    @Override    public void store(Permission permission) {        this.mongoTemplate.save(permission);    }    @Override    public void store(RolePermission rolePermission) {        this.mongoTemplate.save(rolePermission);    }    @Override    public Permission findOneByName(String name) {        return this.mongoTemplate.findOne(                new Query().addCriteria(Criteria.where("name").is(name))                , Permission.class);    }}

2.8 單元測試增加測試資料

    /**     * 關聯使用者角色許可權     */    @Test    public void testShiro() {        Role role = new Role();        role.setName("admin");        role.setDescription("管理員權限");        roleDao.store(role);        UserRole userRole = new UserRole();        userRole.setRoleId(role.getId());        userRole.setUserId(userDao.findOneByUsername("admin").getId());        roleDao.store(userRole);        Permission permission = new Permission();        permission.setName("edit");        permission.setDescription("編輯許可權");        permissionDao.store(permission);        RolePermission rolePermission = new RolePermission();        rolePermission.setRoleId(role.getId());        rolePermission.setPermissionId(permission.getId());        permissionDao.store(rolePermission);    }

2.9 controller類和jsp

PublicController:

/** * PublicController * Created by tl on 17/2/13. */@Controller@RequestMapping("/public")public class PublicController {    @Autowired    private UserDao userDao;    //使用者登入    @RequestMapping("/login")    public String login(User user, HttpServletRequest request) {        Subject subject = SecurityUtils.getSubject();        userDao.findAll();        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());        try {            subject.login(token);//會跳到我們自訂的realm中            request.getSession().setAttribute("user", user);            return "success";        } catch (Exception e) {            e.printStackTrace();            request.getSession().setAttribute("user", user);            request.setAttribute("error", "使用者名稱或密碼錯誤。");            return "login";        }    }    @RequestMapping("/logout")    public String logout(HttpServletRequest request) {        request.getSession().invalidate();        return "index";    }}

login.jsp

<!DOCTYPE html><%@ page language="java" contentType="text/html; charset=GB2312" %><html lang="en"><head>    <title>登入頁面</title></head><body><%    String path = request.getContextPath();// 獲得項目完全路徑(假設你的項目叫skyLine,那麼獲得到的地址就是http://localhost:8080/skyLine/):    String basePath = request.getScheme() + "://"            + request.getServerName() + ":" + request.getServerPort()            + path + "/";%><form action="${basePath}/public/login" method="post">    <span>使用者名稱:</span><input type="text" name="username"/><br>    <span>密碼:</span><input type="password" name="password"/><br>    <input type="submit" value="登陸">${error}</form></body></html>

success.jsp

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %><!DOCTYPE html><%@ page language="java" contentType="text/html; charset=GB2312" %><html lang="en"><head>    <title>success</title></head><body><p>登陸成功!歡迎你${user.username}</p><a href="${pageContext.request.contextPath}/user/list">進入使用者list頁面</a><shiro:hasPermission name="user:edit">    <p>你有許可權看到此處!</p></shiro:hasPermission></body></html>

上面的<shiro:hasPermission name="user:edit">註解,下節再講

點擊開啟連結 3.開始測試shiro許可權

     首先項目啟動之後,不再運行web.xml這行代碼<welcome-file-list>,而是直接被shiroFilter過濾,執行shiro-context.xml設定檔中的<property name="loginUrl" value="/pages/login.jsp"/>,進入登入頁面,然後輸入登入資訊,進入publicController中的login方法,執行到subject.login(token);時會跳轉到我們自訂的realm中進行使用者名稱和密碼校正,成功跳轉success.jsp,失敗重新返回login.jsp頁面,同時顯示報錯資訊。

    下面針對shiro-contex.xml中的url設定,訪問http://localhost:8080/user/edit,提示訪問成功,如果將設定檔的/user/edit=perms[user:edit]中的use:edit改成use:edit,重新啟動,再次訪問則會報錯,提示沒有存取權限


   但是,上面的配置有一個局限性,就是每次在設定檔裡面加url許可權,會很麻煩,每次都要重啟,而且shiro許可權是細粒化的這種加配置的方法會產生很多代碼,是有意下文我們將介紹通過註解在controller和jsp中如何?許可權細粒化。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.