Springboot+shiro配置筆記+錯誤小結


  軟件152 尹以操

  springboot不像springmvc,它沒有xml配置文件,那該如何配置shiro呢,其實也不難,用java代碼+注解來解決這個問題。僅以此篇記錄我對shiro的學習,如有對過客造成不便,實在抱歉!

  一、加入jar包

  既然要用到shiro,當然要加入它的jar包咯,在pom.xml中jar包依賴中加入:

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

  二、寫實體類

  這需要三個實體類,hibernate自動生成5個表

  User實體(用戶):

package com.cy.coo.bean;

import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

import com.fasterxml.jackson.annotation.JsonBackReference;

@Entity
public class User {

    @Id
    @GeneratedValue
    private Integer user_id;//用戶序號
    
    @Column(unique=true)
    private String name;//賬戶
    
    private String password;//密碼
    
    private String salt;//
        
    private Integer state;//用戶狀態
    
    private String createtime;//創建時間

    @ManyToMany(fetch=FetchType.EAGER)//立即從數據庫中進行加載數據;
    @JoinTable(name="User_Role",joinColumns={@JoinColumn(name="user_id")},
    inverseJoinColumns={@JoinColumn(name="role_id")})
    private List<Role> roleList;
    
    @JsonBackReference
    public List<Role> getRoleList(){
        return roleList;
    }
    
    public void setRoleList(List<Role> roleList){
        this.roleList=roleList;
    }


    注:其它getter和setter省略

    
}

  關於為什么要在getRolelist這個方法上加上@JsonBackReference注解,可以查看這篇文章http://blog.csdn.net/maxu12345/article/details/45538157

  Role實體(角色):

package com.cy.coo.bean;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

import com.fasterxml.jackson.annotation.JsonBackReference;


@Entity
public class Role implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    private Integer role_id;//角色序號

    private String role_name;//角色名稱

    private String role_description;//角色描述

    @ManyToMany
    @JoinTable(name = "User_Role", joinColumns = { @JoinColumn(name = "role_id") }, inverseJoinColumns = {
            @JoinColumn(name = "user_id") })
    private List<User> userList=new ArrayList<>();
    
    @ManyToMany(fetch=FetchType.EAGER)
    @JoinTable(name="Role_Function",joinColumns={@JoinColumn(name="role_id")},inverseJoinColumns={
            @JoinColumn(name="function_id")})
    private List<Function> functionList=new ArrayList<>();
    
    @JsonBackReference
    public List<Function> getFunctionList(){
        return functionList;
    }
    
    public void setFunctionList(List<Function> functionList){
        this.functionList=functionList;
    }

    @JsonBackReference
    public List<User> getUserList() {
        return userList;
    }

    public void setUserList(List<User> userList) {
        this.userList = userList;
    }
    
    public Integer getRole_id() {
        return role_id;
    }

    public void setRole_id(Integer role_id) {
        this.role_id = role_id;
    }


    public String getRole_name() {
        return role_name;
    }

    public void setRole_name(String role_name) {
        this.role_name = role_name;
    }

    public String getRole_description() {
        return role_description;
    }

    public void setRole_description(String role_description) {
        this.role_description = role_description;
    }


}

  Function實體(權限):

package com.cy.coo.bean;

import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

@Entity
public class Function {

    @Id
    @GeneratedValue
    private Integer function_id;//功能序號
    
    private String permission;//權限字符串
    
    
    @ManyToMany
    @JoinTable(name = "Role_Function", joinColumns = { @JoinColumn(name = "function_id") }, inverseJoinColumns = {
            @JoinColumn(name = "role_id") })
    private List<Role> roleList;

    public List<Role> getRoleList() {
        return roleList;
    }

    public void setRoleList(List<Role> roleList) {
        this.roleList = roleList;
    }

    public Integer getFunction_id() {
        return function_id;
    }

    public void setFunction_id(Integer function_id) {
        this.function_id = function_id;
    }


    public String getPermission() {
        return permission;
    }

    public void setPermission(String permission) {
        this.permission = permission;
    }
    
}

  這幾個實體類的具體關系如下圖,也完美的解釋了為什么會生成5張表:

  三、寫一個與前端交互的controller方法,service層的具體邏輯的方法

    @PostMapping(value = "/logon")
    public Object logon(@RequestBody Login user) {

        return userService.login(user);
    }

  這個方法就是將前端傳來的username和password封裝到Login類中,Login類也只有這兩個屬性,然后調用Service層的login方法來處理。下面是service的login方法:

/**
     * 用戶登錄 create by yyc 2017年5月12日下午4:31:26
     */
    @Override
    public Object login(Login user) {
        String username = user.getUsername().trim();
        String password = user.getPassword().trim();

        // 檢查空值
        if (!CheckObjectField.CheckField(user)) {
            throw new ResultException(CheckObjectField.FieldName + "為空!");
        }

        // 檢查用戶狀態
        Integer userState = userRepository.findUserState(username);
        if (new Integer("1").equals(userState)) {
            throw new ResultException("該用戶已鎖定");
        }

        // 1、獲取Subject實例對象
        Subject currentUser = SecurityUtils.getSubject();

        // 2、判斷當前用戶是否登錄
        if (currentUser.isAuthenticated() == false) {
            // 3、將用戶名和密碼封裝到UsernamePasswordToken
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);

            // 4、認證
            try {
                currentUser.login(token);// 傳到MyAuthorizingRealm類中的方法進行認證
                Session session = currentUser.getSession();
                session.setAttribute("username", username);
            } catch (AuthenticationException e) {
                throw new ResultException("密碼或用戶名錯誤");
            }
        }
        // 根據用戶名查詢角色信息
        List<String> RoleNames = roleService.findRoleName(username);

        return new LoginReturn(username, RoleNames);

    }

  service中主要是將用戶名和密碼封裝到shiro的UsernamePasswordToken中,然后將token對象放到SecurityUtils.getSubject()的login方法中,以便shiro認證登錄使用。認證失敗就會拋出AuthenticationException這個異常,就對異常進行相應的操作,這里的處理是拋出一個自定義異常ResultException。

  四、寫我認為的shiro的核心類

package com.cy.coo.shiro;


import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.cy.coo.bean.Function;
import com.cy.coo.bean.Role;
import com.cy.coo.bean.User;
import com.cy.coo.service.UserService;

/**
 * 
*
* @author  E-mail:34782655@qq.com
* @version 創建時間:2017年5月8日 上午10:50:50
* 類說明:
*    --
 */
public class  MyAuthorizingRealm extends AuthorizingRealm {

    private final static Logger logger=LoggerFactory.getLogger(MyAuthorizingRealm.class);
    
    @Autowired
    private UserService userService;
    //shiro的權限配置方法
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        
        logger.info("權限配置-->doGetAuthorizationInfo");
        
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        logger.info("----------------------------->"+principals.getPrimaryPrincipal());
        User user=(User) principals.getPrimaryPrincipal();
        for(Role role:user.getRoleList()){
            authorizationInfo.addRole(role.getRole_name());
            for(Function function:role.getFunctionList()){
                authorizationInfo.addStringPermission(function.getPermission());
            }
        }
        
        logger.info("用戶"+user.getName()+"具有的角色:"+authorizationInfo.getRoles());
        logger.info("用戶"+user.getName()+"具有的權限:"+authorizationInfo.getStringPermissions());
        
        return authorizationInfo;
    }

        //shiro的身份驗證方法
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        
        logger.info("正在驗證身份...");
        SimpleAuthenticationInfo info=null;
        
        //將token轉換成UsernamePasswordToken
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        //從轉換后的token中獲取用戶名
        String username= upToken.getUsername();  
        logger.info("----->"+username);
        
        //查詢數據庫,得到用戶
        User user=userService.findByName(username);
        if(user==null){
            return null;
        }
        
        //得到加密密碼的鹽值
        ByteSource salt = ByteSource.Util.bytes(user.getSalt());
//        logger.info("加密密碼的鹽:"+salt);
//        //得到鹽值加密后的密碼:只用於方便數據庫測試,后期不會用到。
//        Object md = new SimpleHash("MD5",upToken.getPassword(),salt,1024);
//        logger.info("鹽值加密后的密碼:"+md);

        info = new SimpleAuthenticationInfo(
                user, //用戶名
                user.getPassword(), //密碼
                salt, //加密的鹽值
                getName()  //realm name
        );
        return info;
    }

}

  這個類繼承shiro的AuthorizingRealm ,主要有兩個方法,一個是權限配置,一個是身份認證,權限配置:當我們要用到權限時shiro會回調doGetAuthorizationInfo這個方法,對當前的用戶分配權限,這個方法中的嵌套for循環是怎么回事呢,其實就是將數據庫中的對應角色、權限放進shiro中,讓他來管理,這需要實體類User中有getRoleList()、getRole_name()和getFunctionList()、getPermission這幾個方法,這幾個個方法就是設計數據庫和實體類時的東西了,關於shiro權限相關的實體類在前面已經給出了。身份認證:在用戶登錄認證的時候回調,認證失敗就拋出AuthenticationException。

  五、shiro配置類

package com.cy.coo.shiro;

import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

@Configuration // 等價於beans
public class ShiroConfig {

    private static final Logger log = LoggerFactory.getLogger(ShiroFilterFactoryBean.class);

    @Bean(name = "securityManager")
    public SecurityManager securityManager(@Qualifier("authRealm") MyAuthorizingRealm authRealm,
            @Qualifier("cookieRememberMeManager") CookieRememberMeManager cookieRememberMeManager) {
        log.info("securityManager()");
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 設置realm.
        securityManager.setRealm(authRealm);

        // 設置rememberMe管理器
        securityManager.setRememberMeManager(cookieRememberMeManager);

        return securityManager;
    }

    /**
     * realm
     * 
     * @return
     */
    @Bean(name = "authRealm")
    public MyAuthorizingRealm myAuthRealm(
            @Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher,
            @Qualifier("ehCacheManager") EhCacheManager  ehCacheManager) {
        log.info("myShiroRealm()");
        MyAuthorizingRealm myAuthorizingRealm = new MyAuthorizingRealm();
        // 設置密碼憑證匹配器
        myAuthorizingRealm.setCredentialsMatcher(matcher); // myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        // 設置緩存管理器
        myAuthorizingRealm.setCacheManager(ehCacheManager);

        return myAuthorizingRealm;
    }

    /**
         * 緩存管理器
         * @return
         */
        @Bean(value="ehCacheManager")
        public EhCacheManager ehCacheManager(@Qualifier("ehCacheManagerFactoryBean") EhCacheManagerFactoryBean bean) {
                log.info("ehCacheManager()");
                EhCacheManager cacheManager = new EhCacheManager();                
                cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
                return cacheManager;
        }

    /**
     * cookie對象;
     * 
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie() {
        log.info("rememberMeCookie()");
        // 這個參數是cookie的名稱,對應前端的checkbox 的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        // <!-- 記住我cookie生效時間30天(259200) ,單位秒;-->
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
     * 記住我管理器 cookie管理對象;
     * 
     * @return
     */
    @Bean(name = "cookieRememberMeManager")
    public CookieRememberMeManager rememberMeManager() {
        System.out.println("rememberMeManager()");
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

    /**
     * 密碼匹配憑證管理器
     * 
     * @return
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        log.info("hashedCredentialsMatcher()");
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

        hashedCredentialsMatcher.setHashAlgorithmName("MD5");// 散列算法:這里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(1024);// 散列的次數,比如散列兩次,相當於
                                                            // md5(md5(""));

        return hashedCredentialsMatcher;
    }

    /**
     * 開啟shiro aop注解支持. 使用代理方式;所以需要開啟代碼支持; Controller才能使用@RequiresPermissions
     * 
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            @Qualifier("securityManager") SecurityManager securityManager) {
        log.info("authorizationAttributeSourceAdvisor()");
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
        log.info("shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 必須設置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 攔截器.
        Map<String, String> map = new LinkedHashMap<String, String>();

        map.put("/logout", "logout");
        map.put("/login", "anon");
        map.put("/logon", "anon");

        map.put("/**", "authc");

        // 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登錄成功后要跳轉的鏈接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 未授權界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }
}

  這個沒什么好說的,最后一個類是shiro的過濾器配置。可以看到我在每個方法上面加了一個@Bean(name="..."),其實這是spring的注解,將這個類放到spring容器中管理,在方法形參中使用@Qualifier(...)來使用它,以致於我們在方法體中調用某個方法時就方面多了。

在這里,關於shiro在springboot中的基礎配置就完成了。下面是期間遇到的錯誤解決方案:

錯誤一:關於實體類的錯誤,springboot 中hibernate懶加載  報錯....................................No  Session
解決方法:新建類 配置OpenEntityManagerInViewFilter    以及  上面角色類(表)和用戶類(表)(Role、User)Role的紅色字體也是必須的,及@ManyToMany(fetch=FetchType.EAGER)
由於博主基礎的局限還不知道具體的原因是什么,但是解決了就好。
@Configuration
public class HibernateConfig {
           @Bean
           public FilterRegistrationBean registerOpenEntityManagerInViewFilterBean() {
               FilterRegistrationBean registrationBean = new FilterRegistrationBean();
               OpenEntityManagerInViewFilter filter = new OpenEntityManagerInViewFilter();
               registrationBean.setFilter(filter);
               registrationBean.setOrder(5);
               return registrationBean;
           }
}

參考文章:

http://stackoverflow.com/questions/33622222/spring-boot-opensessioninviewfilter-with-actuator-and-custom-security 

http://www.jianshu.com/p/a827ecdda99f

http://www.w_2bc.com/article/201653 /*這個鏈接博客園不讓我發啊,把w_2_b_c中的下划線刪了即可*/

錯誤二:這個在前面也提到過了,返回json數據出現Could not write JSON document: Infinite recursion(無法編寫JSON文檔:無限遞歸 );

在后面的使用中發現這個錯誤也是這樣解決的,java.lang.IllegalStateException: Cannot call sendError() after the response has been committed,錯誤頁面到最后就是這樣,如圖:

 

解決方法:

  在controller返回數據到統一json轉換的時候,出現了json infinite recursion stackoverflowerror的錯誤,即json在將對象轉換為json格式的數據的時候,出現了無限遞歸調用的情況。
具體的情況如下:
    A類中,有個屬性:List<B> b, A與B的關系為 OneToMany;在B類中,有屬性A a,引用到A中的字段id,並作為外鍵。hibernate查詢結果正常,可以看到返回的A對象中,有b參數值,但在json轉換的時候就出現了無限遞歸的情況。個人分析,應該是json在序列化A中的b屬性的時候,找到了B類,然后序列化B類,而B類中有a屬性,因此,為了序列化a屬性,json又得去序列化A類,如此遞歸反復,造成該問題。
解決:
    在B類中a的getter setter方法上加注解@JsonBackReference,其實自己試過只在getter方法上加@JsonBackReference也夠了。
 
 


免責聲明!

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



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