1. 程式人生 > >springboot整合shiro實現多realm不同資料表登陸

springboot整合shiro實現多realm不同資料表登陸

shrio是一個很好的登陸以及許可權管理框架,但是mo預設是單realm單資料表,如果業務中使用者分佈在不同的資料表,單realm就很難實現登陸以及許可權管理的功能,這篇部落格就簡單的介紹一個家長 學生 老師的shiro的多realm登陸驗證,使用springboot,mybatis mysql等相關技術,部落格底部附上原始碼,有興趣的可以去下載

 

1.專案pom依賴

<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.0.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.4.0</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

2.使用者資料表

CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_shiro_mulit_relam` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `springboot_shiro_mulit_relam`;

/*Table structure for table `parent` */

DROP TABLE IF EXISTS `parent`;

CREATE TABLE `parent` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `account` varchar(20) DEFAULT NULL,
  `password` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

/*Data for the table `parent` */

insert  into `parent`(`id`,`name`,`account`,`password`) values (1,'張家長','123','123');

/*Table structure for table `student` */

DROP TABLE IF EXISTS `student`;

CREATE TABLE `student` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `account` varchar(20) DEFAULT NULL,
  `password` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

/*Data for the table `student` */

insert  into `student`(`id`,`name`,`account`,`password`) values (1,'王同學','456','456');

/*Table structure for table `teacher` */

DROP TABLE IF EXISTS `teacher`;

CREATE TABLE `teacher` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `account` varchar(20) DEFAULT NULL,
  `password` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

/*Data for the table `teacher` */

insert  into `teacher`(`id`,`name`,`account`,`password`) values (1,'劉老師','789','789');

3.shiro 配置

實現多realm的關鍵在於繼承shiro預設的UsernamePasswordToken,新增一個欄位用於標識不同的realm,使用者登入的shi時候帶上標識的欄位,shiro則根據欄位去不同的realm去驗證登陸,授權等

3.1 繼承UsernamePasswordToken新增loginType屬性

public class UserToken extends UsernamePasswordToken {
	private String loginType;

    public UserToken() {}

    public UserToken(final String username, final String password, 
            final String loginType) {
        super(username, password);
        this.loginType = loginType;
    }

    public String getLoginType() {
        return loginType;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }
}

 3.2 登陸時使用繼承後的UserToken,新增loginType屬性

@RequestMapping("/studentLogin")
	public Student studentLogin(@RequestParam(value = "account") String account,
								@RequestParam(value = "password") String password, 
								HttpServletRequest request,
								HttpServletResponse response) {

		System.out.println("\n學生登陸");
		Student student = null;
		//設定永不過期
		SecurityUtils.getSubject().getSession().setTimeout(-1000L);
		Subject subject = SecurityUtils.getSubject();
		try {
			// 呼叫安全認證框架的登入方法
			subject.login(new UserToken(account, password, "Student"));
			student = studentService.getStudentByAccount(account);
		} catch (AuthenticationException ex) {
			System.out.println("登陸失敗: " + ex.getMessage());
		}
		return student;

	}

3.3 獲得AuthenticationToken,判斷是單realm還是多realm,分別去不同的方法驗證

public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
	
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException {
        System.out.println("UserModularRealmAuthenticator:method doAuthenticate() execute ");
        // 判斷getRealms()是否返回為空
        assertRealmsConfigured();
        // 強制轉換回自定義的CustomizedToken
        UserToken userToken = (UserToken) authenticationToken;
        // 登入型別
        String loginType = userToken.getLoginType();
        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 登入型別對應的所有Realm
        List<Realm> typeRealms = new ArrayList<>();
        for (Realm realm : realms) {
            if (realm.getName().contains(loginType)) {
                typeRealms.add(realm);
            }
        }

        // 判斷是單Realm還是多Realm
        if (typeRealms.size() == 1){
            System.out.println("doSingleRealmAuthentication() execute ");
            return doSingleRealmAuthentication(typeRealms.get(0), userToken);
        }
        else{
            System.out.println("doMultiRealmAuthentication() execute ");
            return doMultiRealmAuthentication(typeRealms, userToken);
        }
    }
}

3.4 配置明文密碼登陸,方便測試

public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
	
	 
	 @Override  
     public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {  
         UsernamePasswordToken token = (UsernamePasswordToken) authcToken;  

//       Object tokenCredentials = encrypt(String.valueOf(token.getPassword()));  
         //明文密碼
         Object tokenCredentials = String.valueOf(token.getPassword());  
         Object accountCredentials = getCredentials(info);  
         //將密碼加密與系統加密後的密碼校驗,內容一致就返回true,不一致就返回false  
         return equals(tokenCredentials, accountCredentials);  
     }  

     /**
      * @author Adam
      * @description 將傳進來密碼加密方法
      * @date 11:26 2018/9/2
      * @param [data]
      * @return java.lang.String
      */
     private String encrypt(String data) {
         //這裡可以選擇自己的密碼驗證方式 比如 md5或者sha256等
         String sha384Hex = new Sha384Hash(data).toBase64();
         return sha384Hex;  
     }  
}

3.5 shiroconfig 配置 (將三個使用者realm 注入)

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //攔截器.
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
        // 配置不會被攔截的連結 順序判斷
        filterChainDefinitionMap.put("/image/**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/html/**", "anon");

        //測試開放的介面
        filterChainDefinitionMap.put("/login/**", "anon");

        filterChainDefinitionMap.put("/favicon.ico", "anon");
        //配置退出 過濾器,其中的具體的退出程式碼Shiro已經替我們實現了
        filterChainDefinitionMap.put("/logout", "logout");
        //<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心程式碼就不好使了;
        //<!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問-->
//        filterChainDefinitionMap.put("/**", "authc");
        filterChainDefinitionMap.put("/**", "anon");
        // 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
        shiroFilterFactoryBean.setLoginUrl("/login/unauth");
        //未授權介面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/login/unauth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 憑證匹配器
     * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了)
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //雜湊演算法:這裡使用MD5演算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    @Bean(name = "parentRealm")
    public ParentRealm parentRealm(){
        ParentRealm parentRealm = new ParentRealm();
        parentRealm.setCredentialsMatcher(new CustomCredentialsMatcher());
        return parentRealm;
    }
    
    @Bean(name = "teacherRealm")
    public TeacherRealm teacherRealm(){
    	TeacherRealm teacherRealm = new TeacherRealm();
    	teacherRealm.setCredentialsMatcher(new CustomCredentialsMatcher());
        return teacherRealm;
    }
    
    @Bean(name = "studentRealm")
    public StudentRealm studentRealm(){
    	StudentRealm studentRealm = new StudentRealm();
    	studentRealm.setCredentialsMatcher(new CustomCredentialsMatcher());
        return studentRealm;
    }
    


    @Bean(name = "SecurityManager")
    public SecurityManager securityManager(){
    	
    	DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    	//設定realm.
        securityManager.setAuthenticator(modularRealmAuthenticator());
        List<Realm> realms = new ArrayList<>();
        //新增多個Realm
        realms.add(parentRealm());
        realms.add(teacherRealm());
        realms.add(studentRealm());
        securityManager.setRealms(realms);

        return securityManager;
    }

    /**
     *  開啟shiro aop註解支援.
     *  使用代理方式;所以需要開啟程式碼支援;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean(name="simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver
    createSimpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        //資料庫異常處理
        mappings.setProperty("DatabaseException", "databaseError");
        mappings.setProperty("UnauthorizedException","/403");
        r.setExceptionMappings(mappings);
        r.setDefaultErrorView("error");
        r.setExceptionAttribute("ex");
        return r;
    }
    
    /**
     * 系統自帶的Realm管理,主要針對多realm
     * */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator(){
        //自己重寫的ModularRealmAuthenticator
        UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }

3.6 realm 注入service層,去資料庫中查詢資料,驗證密碼

public class ParentRealm extends AuthorizingRealm {
	
	@Autowired
	private ParentService parentService;
	
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("許可權配置-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        return authorizationInfo;
    }

    /**
     * @Author Adam
     * @Description  主要是用來進行身份認證的,也就是說驗證使用者輸入的賬號和密碼是否正確
     * @Date 14:06 2018/9/28
     * @Param [token]
     * @return org.apache.shiro.authc.AuthenticationInfo
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {

        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
        //獲取使用者的輸入的賬號.
        String account = (String)token.getPrincipal();
        Object credentials = token.getCredentials();
		System.out.println("credentials="+credentials);
        //通過username從資料庫中查詢物件
        //實際專案中,這裡可以根據實際情況做快取,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
		Parent parent = parentService.getParentByAccount(account);
        System.out.println("----->>parent="+parent);
        if(parent == null){
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
        		parent,
        		parent.getPassword(),
                getName()
        );
        return authenticationInfo;
    }

}
public class StudentRealm extends AuthorizingRealm {
	
	@Autowired
    private StudentService studentService;

    /**
     * @Author Adam
     * @Description  許可權配置
     * @Date 14:29 2018/9/28
     * @Param [principals]
     * @return org.apache.shiro.authz.AuthorizationInfo
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        System.out.println("許可權配置-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        return authorizationInfo;
    }

    /**
     * @Author Adam
     * @Description  主要是用來進行身份認證的,也就是說驗證使用者輸入的賬號和密碼是否正確。
     * @Date 14:24 2018/9/28
     * @Param [token]
     * @return org.apache.shiro.authc.AuthenticationInfo
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {

        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
        //獲取使用者的輸入的賬號.
        String account = (String)token.getPrincipal();
        Object credentials = token.getCredentials();
		System.out.println("credentials="+credentials);
        //通過username從資料庫中查詢物件
        Student student = studentService.getStudentByAccount(account);
        System.out.println("----->>student="+student);
        if(student == null){
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
        		student,
        		student.getPassword(),
                getName()
        );
        return authenticationInfo;
    }

}
public class TeacherRealm extends AuthorizingRealm {
	
	@Autowired
    private TeacherService teacherService;

    /**
     * @Author Adam
     * @Description  許可權配置
     * @Date 14:09 2018/9/28
     * @Param [principals]
     * @return org.apache.shiro.authz.AuthorizationInfo
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("許可權配置-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        return authorizationInfo;
    }

    /**
     * @Author Adam
     * @Description  主要是用來進行身份認證的,也就是說驗證使用者輸入的賬號和密碼是否正確
     * @Date 14:08 2018/9/28
     * @Param [token]
     * @return org.apache.shiro.authc.AuthenticationInfo
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {

    	System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
        //獲取使用者的輸入的賬號.
        String account = (String)token.getPrincipal();
        Object credentials = token.getCredentials();
        System.out.println("credentials="+credentials);
        //通過username從資料庫中查詢物件
        //實際專案中,這裡可以根據實際情況做快取,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
        Teacher teacher = teacherService.getTeacherByAccount(account);
        System.out.println("----->>teacher="+teacher);
        if(teacher == null){
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
        		teacher,
        		teacher.getPassword(),
                getName()
        );
        return authenticationInfo;
    }

}

4.實現效果

5.原始碼

https://github.com/Yanyf765/springboot-shiro-mulit-realm