SpringBoot 集成Shiro

Apache Shiro是一個功能強大且靈活的開源安全框架雳刺,可以清晰地處理身份驗證营密,授權(quán),企業(yè)會話管理和加密蛀骇。

  • 驗證用戶以驗證其身份

  • 為用戶執(zhí)行訪問控制,例如:

    • 確定是否為用戶分配了某個安全角色
    • 確定是否允許用戶執(zhí)行某些操作
  • 在任何環(huán)境中使用Session API读拆,即使沒有Web容器或EJB容器也是如此擅憔。

  • 在身份驗證,訪問控制或會話生命周期內(nèi)對事件做出反應(yīng)檐晕。

  • 聚合用戶安全數(shù)據(jù)的1個或多個數(shù)據(jù)源暑诸,并將其全部顯示為單個復(fù)合用戶“視圖”蚌讼。

  • 啟用單點登錄(SSO)功能

  • 無需登錄即可為用戶關(guān)聯(lián)啟用“記住我”服務(wù)
    Shiro針對Shiro開發(fā)團隊所稱的“應(yīng)用程序安全的四大基石” - 身份驗證,授權(quán)个榕,會話管理和加密:

  • 身份驗證:有時稱為“登錄”篡石,這是證明用戶是他們所說的人的行為。

  • 授權(quán):訪問控制的過程西采,即確定“誰”可以訪問“什么”凰萨。

  • 會話管理:即使在非Web或EJB應(yīng)用程序中,也可以管理特定于用戶的會話械馆。

  • 密碼學(xué):使用加密算法保持數(shù)據(jù)安全胖眷,同時仍然易于使用。
    具體參考: http://shiro.apache.org/reference.html

技術(shù)背景

  • 開發(fā)工具:STS(eclipse)
  • 技術(shù)選擇: SpringBoot, SpringDataJpa, shiro
  • 數(shù)據(jù)庫選擇: MySQL, Redis

創(chuàng)建項目

創(chuàng)建maven子項目 study-springboot-backstage 引入一下依賴pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>study-springboot</groupId>
        <artifactId>study-springboot</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>study-springboot-backstage</artifactId>
    <description>后臺管理</description>
    <dependencies>
        <!-- 實體項目 -->
        <dependency>
            <groupId>study-springboot</groupId>
            <artifactId>study-springboot-domain</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- shiro+redis緩存插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>2.4.2.1-RELEASE</version>
        </dependency>
        <!-- 開啟注解 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
       </dependency>
    </dependencies>

    <!-- 打包 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

配置AuthorizingRealm,創(chuàng)建SysUserRealm.java

/**
 * @describe shiro認證
 * @author Bertram.Wang
 */
@Component
public class SysUserRealm extends AuthorizingRealm {

    @Autowired
    private SysUserRepository sysUserRepository;
    @Autowired
    private SysUserService sysUserService;

    
    /**
     * 授權(quán)(驗證權(quán)限時調(diào)用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SysUser user = (SysUser) principals.getPrimaryPrincipal();
        Integer userId = user.getId();

        // 用戶權(quán)限列表
        Set<String> permsSet = sysUserService.getAuthorityByUserId(userId);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permsSet);
        return info;
    }

    /**
     * 認證(登錄時調(diào)用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());
        // 查詢用戶信息
        SysUser user = sysUserRepository.findOneByNameAndPassword(username, password);

        // 賬號不存在
        if (user == null) {
            throw new UnknownAccountException("賬號或密碼不正確");
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userInfo, password, getName());
        return info;
    }

}

shiro配置類霹崎,主要是設(shè)置shiroFilter珊搀,securityManager, sessionManage等信息
自定義SessionManager

/**
 * @Date 2019年4月10日
 * @Sgin MySessionManager
 * @Author Bertram.Wang
 */
public class MySessionManager extends DefaultWebSessionManager {
 
    private static final String AUTHORIZATION = "Authorization";
 
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
 
    public MySessionManager() {
        super();
    }
 
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果請求頭中有 Authorization 則其值為sessionId
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否則按默認規(guī)則從cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}

shiroConfig.java

/**
 * @Date 2019年4月10日
 * @Sgin ShiroConfig
 * @Author Bertram.Wang
 */
@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 沒有登陸的用戶只能訪問登陸頁面
        shiroFilterFactoryBean.setLoginUrl("/initlogin");
        // 登錄成功后要跳轉(zhuǎn)的鏈接
        //shiroFilterFactoryBean.setSuccessUrl("/auth/index");
        // 未授權(quán)界面; ----這個配置了沒卵用仿畸,具體原因想深入了解的可以自行百度
        shiroFilterFactoryBean.setUnauthorizedUrl("/initlogin");
        //自定義攔截器
        Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
        //限制同一帳號同時在線的個數(shù)食棕。
        filtersMap.put("kickout", kickoutSessionControlFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);
        // 權(quán)限控制map.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/**/kickout", "anon");
        filterChainDefinitionMap.put("/**", "authc,kickout");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public SecurityManager securityManager(SysUserRealm sysUserRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(sysUserRealm);
        // 自定義緩存實現(xiàn) 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定義session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }


    /**
     * cacheManager 緩存 redis實現(xiàn) 使用的是shiro-redis開源插件
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * shiro redisManager 使用的是shiro-redis開源插件
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("localhost");
        redisManager.setPort(6379);
        redisManager.setExpire(1800);// 配置緩存過期時間
        redisManager.setTimeout(0);
        redisManager.setPassword("Redis1234!");
        return redisManager;
    }

    /**
     * Session Manager 
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    } 

    /**
     * RedisSessionDAO shiro 
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * *限制同一賬號登錄同時登錄人數(shù)控制
     * @return
     */
    @Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter() {
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        kickoutSessionControlFilter.setCacheManager(cacheManager());
        kickoutSessionControlFilter.setSessionManager(sessionManager());
        kickoutSessionControlFilter.setKickoutAfter(false);
        kickoutSessionControlFilter.setMaxSession(1);
        kickoutSessionControlFilter.setKickoutUrl("/auth/kickout");
        return kickoutSessionControlFilter;
    }


    /***
     * *授權(quán)所用配置
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /***
     * *使授權(quán)注解起作用不如不想配置可以在pom文件中加入
     * <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
       </dependency>
     * @param securityManager
     * @return
     */
//    @Bean
//    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
//      AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
//        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
//        return authorizationAttributeSourceAdvisor;
//    }

    /**
     * Shiro生命周期處理器
     */
    @Bean
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
}

限制規(guī)則:


引用圖
  • anon:準(zhǔn)許直接訪問
  • authc:要求對請求用戶進行身份驗證,以便請求繼續(xù)错沽,如果沒有驗證簿晓,則強制用戶通過將其定向到您配置的LoginUurl來登錄。
  • logout:接收到請求后千埃,將立即注銷當(dāng)前正在執(zhí)行的子節(jié)點憔儿,然后將它們重定向到已配置的redirecturl。
  • perms: 如果當(dāng)前用戶具有映射值指定的權(quán)限放可,則允許訪問;如果用戶沒有指定的所有權(quán)限谒臼,則拒絕訪問
    具體參考:http://shiro.apache.org/static/1.4.0/apidocs/org/apache/shiro/web/filter/

測試控制器

@RestController
public class TestController {
    @Autowired
    private SysUserService sysUserService;
    @GetMapping("/hello")
    public Response<?> hello() {
        SysUser sysUser = sysUserRepository.findOneById(2);
        return success(authorityByUserId);
    }
}

測試類

/**
 * <p> 測試<p>
 * @Author Bertram.Wang 
 */
@RunWith(SpringRunner.class)   
@SpringBootTest(classes=Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApplicationTest {
    private static final Logger log = LoggerFactory.getLogger(ApplicationTest.class);
    @LocalServerPort
    private int port;
    private String base;
    @Autowired
    private TestRestTemplate restTemplate;
    @Before
    public void setUp() throws Exception {
        this.base = String.format("http://localhost:%d/%s", port, "/backstage");
    } 
    /**
     * *設(shè)置請求消息頭
     */
    private static HttpHeaders setHttpHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type", "application/json;charset=utf-8;Accept:application/json;");// 設(shè)置編碼 這個一定不能去
        headers.add("Authorization", AUTHORIZATION);
        return headers;
    }
    // sessionID
    private static final String AUTHORIZATION = "sessionId";
    
    private String requestGET(String url){
        HttpEntity<Object> requestEntity = new HttpEntity<>(setHttpHeaders());
        ResponseEntity<String> rest = restTemplate.exchange(this.base + url, HttpMethod.GET, requestEntity, String.class);
        return rest.getBody();
    }
    private String requestPOST(String url, Object data){
        HttpEntity<Object> requestEntity = new HttpEntity<>(data, setHttpHeaders());
        ResponseEntity<String> rest = restTemplate.postForEntity(this.base + url, requestEntity, String.class);
        return rest.getBody();
    }
    // ---------------------LoginController--------------------------
    @Test
    public void logoutTest() throws Exception {
        String requestGET = requestGET("/logout");
        log.info("===================rest:{}", requestGET);
    }
    @Test
    public void loginTest() throws Exception {
        SysUserAO sysUserAO = new SysUserAO();
        sysUserAO.setUsername("admin");
        sysUserAO.setPassword("admin");
        String requestPOST = requestPOST("/login", sysUserAO);
        log.info("===================rest:{}", requestPOST);
    }
    
    @Test
    public void helloTest() throws Exception {
        String requestGET = requestGET("/hello");
        log.info("===================rest:{}", requestGET);
    }
}

執(zhí)行helloTest方法,

rest:{"code":20303,"message":"請先登錄","time":1555320155}耀里;

先執(zhí)行l(wèi)oginTest方法:

rest:{"code":0,"message":"成功","time":1555320426,"data":{"id":"a521dbf5-6a63-45d2-85de-95c121aeb0e9","host":"127.0.0.1","lastAccessTime":"2019-04-15T09:27:05.985+0000","timeout":1800000,"attributeKeys":["org.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEY","org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY"],"startTimestamp":"2019-04-15T09:27:05.985+0000"}}

把rest.data.id替換sessionId蜈缤,再執(zhí)行helloTest方法:

rest:{"code":0,"message":"成功","time":1555328243,"data":{"id":2,"createDate":"2019-04-10T09:31:51.000+0000","modifyDate":"2019-04-10T09:31:53.000+0000","name":"admin","password":"8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918","roles":[{"id":1,"createDate":"2019-04-03T06:53:22.000+0000","modifyDate":"2019-04-03T06:53:25.000+0000","name":"ADMIN","parentId":0,"menus":[{"id":1,"createDate":"2018-08-30T09:27:32.000+0000","modifyDate":"2018-10-10T08:44:03.000+0000","name":"系統(tǒng)管理","parentId":0,"permission":null,"type":0,"url":null,"icon":"fa fa-cog","orderNum":1}]}]}}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市冯挎,隨后出現(xiàn)的幾起案子底哥,更是在濱河造成了極大的恐慌,老刑警劉巖房官,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件趾徽,死亡現(xiàn)場離奇詭異,居然都是意外死亡翰守,警方通過查閱死者的電腦和手機孵奶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜡峰,“玉大人了袁,你說我怎么就攤上這事朗恳。” “怎么了早像?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵僻肖,是天一觀的道長肖爵。 經(jīng)常有香客問我卢鹦,道長,這世上最難降的妖魔是什么劝堪? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任冀自,我火速辦了婚禮,結(jié)果婚禮上秒啦,老公的妹妹穿的比我還像新娘熬粗。我一直安慰自己,他們只是感情好余境,可當(dāng)我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布驻呐。 她就那樣靜靜地躺著,像睡著了一般芳来。 火紅的嫁衣襯著肌膚如雪含末。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天即舌,我揣著相機與錄音佣盒,去河邊找鬼。 笑死顽聂,一個胖子當(dāng)著我的面吹牛肥惭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播紊搪,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼蜜葱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了耀石?” 一聲冷哼從身側(cè)響起牵囤,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎娶牌,沒想到半個月后奔浅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡诗良,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年汹桦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鉴裹。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡舞骆,死狀恐怖钥弯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情督禽,我是刑警寧澤脆霎,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站狈惫,受9級特大地震影響睛蛛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胧谈,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一忆肾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧菱肖,春花似錦客冈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至退疫,卻和暖如春渠缕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蹄咖。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工褐健, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人澜汤。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓蚜迅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親俊抵。 傳聞我的和親對象是個殘疾皇子谁不,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容