1. 程式人生 > >SpringBoot+Shiro實現登陸攔截功能

SpringBoot+Shiro實現登陸攔截功能

      上一章講到使用自定義的方式來實現使用者登入的功能,這章採用shiro來實現使用者登陸攔截的功能。

      首先介紹下Shiro:Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼學和會話管理,以下是shiro的整體的框架:

Subject: 即"使用者",外部應用都是和Subject進行互動的subject記錄了當前操作使用者,將使用者的概念理解為當前操作的主體,可能是一個通過瀏覽器請求的使用者,也可能是一個執行的程式。 Subjectshiro中是一個介面,介面中定義了很多認證授相關的方法,外部程式通過subject進行認證授,而subject

是通過SecurityManager安全管理器進行認證授權(Subject相當於SecurityManager的門面)。

SecurityManager: 安全管理器它是shiro的核心,負責對所有的subject進行安全管理。通過SecurityManager可以完成subject的認證、授權等,實質上SecurityManager是通過Authenticator進行認證,通過Authorizer進行授權,通過SessionManager進行會話管理等。此外SecurityManager是一個介面,繼承了Authenticator, Authorizer, SessionManager這三個介面

Authenticator:是一個執行對使用者的身份驗證(登入)的元件。通過與一個或多個Realm 協調來儲存相關的使用者/帳戶資訊。Realm中找到對應的資料,明確是哪一個登陸人。如果存在多個realm,則介面AuthenticationStrategy(策略)會確定什麼樣算是登入成功(例如,如果一個Realm成功,而其他的均失敗,是否登入成功?)。它是一個介面,shiro提供ModularRealmAuthenticator實現類,通過ModularRealmAuthenticator基本上可以滿足大多數需求,也可以自定義認證器。

Authorizer:即授權器,使用者通過認證器認證通過,在訪問功能時

需要通過授權器判斷使用者是否有此功能的操作許可權。就是用來判斷是否有許可權,授權,本質就是訪問控制,控制哪些URL可以訪問.

Realm:即領域,相當於datasource資料來源securityManager進行安全認證需要通過Realm獲取使用者許可權資料,通常一個數據源配置一個realm.s比如:如果使用者身份資料在資料庫那麼realm就需要從資料庫獲取使用者身份資訊。

注意:不要把realm理解成只是從資料來源取資料,在realm中還有認證授權校驗的相關的程式碼

SessionDAO:即會話dao是對session會話操作的一套介面SessionDao代替sessionManager來代替對session進行增刪改查,允許使用者使用任何型別的資料來源來儲存session資料,也可以將資料引入到session框架來。比如要將session儲存到資料庫,可以通過jdbc將會話儲存到資料庫。

CacheManager:快取管理,用於管理其他shiro元件中維護和建立的cache例項,維護這些cache例項的生命週期,快取那些從後臺獲取的用於使用者許可權,驗證的資料,將它們儲存在快取,這樣可以提高效能順序:先從快取中查詢,再從後臺其他介面從其它資料來源中進行查詢,可以用其他現代的企業級資料來源來代替預設的資料來源來提高效能

Cryptography:密碼管理shiro提供了一套加密/解密的元件,方便開發。比如提供常用的雜湊、加/解密等功能。

我們可以把和shiro的互動用下圖來表示:


這個是Shiro身份認證的流程圖:

(注:這個圖片是從其他部落格拷貝過來的,)

這是Shiro的認證流程:

流程如下

1、首先呼叫Subject.isPermitted*/hasRole*介面,其會委託給SecurityManager,而SecurityManager接著會委託給Authorizer

2Authorizer是真正的授權者,如果我們呼叫如isPermitted(user:view),其首先會通過PermissionResolver把字串轉換成相應的Permission例項

3、在進行授權之前,其會呼叫相應Realm獲取Subject相應的角色/許可權用於匹配傳入的角色/許可權

4Authorizer會判斷Realm的角色/許可權是否和傳入的匹配,如果有多個Realm,會委託給ModularRealmAuthorizer進行迴圈判斷,如果匹配如isPermitted*/hasRole*會返回true,否則返回false表示授權失敗。

ModularRealmAuthorizer進行Realm匹配流程

1、首先檢查相應的Realm是否實現了實現了Authorizer

2、如果實現了Authorizer那麼接著呼叫其相應的isPermitted*/hasRole*介面進行匹配

3、如果有一個Realm匹配那麼將返回true,否則返回false

如果Realm進行授權的話,應該繼承AuthorizingRealm,其流程是:

1.1、如果呼叫hasRole*,則直接獲取AuthorizationInfo.getRoles()與傳入的角色比較即可

1.2、如果呼叫如isPermitted(user:view),首先通過PermissionResolver將許可權字串轉換成相應的Permission例項,預設使用WildcardPermissionResolver,即轉換為萬用字元的WildcardPermission

2通過AuthorizationInfo.getObjectPermissions()得到Permission例項集合;通過AuthorizationInfo. getStringPermissions()得到字串集合並通過PermissionResolver解析為Permission例項;然後獲取使用者的角色並通過RolePermissionResolver解析角色對應的許可權集合(預設沒有實現,可以自己提供);

3接著呼叫Permission. implies(Permission p)逐個與傳入的許可權比較,如果有匹配的則返回true,否則false

現在開始上程式碼:

Pom.xml

<!-- 支援JSP,必須匯入這兩個依賴  -->
	<dependency>
           <groupId>org.apache.tomcat.embed</groupId>
           <artifactId>tomcat-embed-jasper</artifactId>
           <scope>provided</scope>
        </dependency>
        
        <dependency>  
            <groupId>javax.servlet.jsp.jstl</groupId>  
            <artifactId>jstl-api</artifactId>  
            <version>1.2</version>  
        </dependency>  
	
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
		
	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        
        <dependency>
           <groupId>postgresql</groupId>
           <artifactId>postgresql</artifactId>
           <version>8.4-702.jdbc4</version>
        </dependency>
        
        <dependency>  
            <groupId>org.postgresql</groupId>  
            <artifactId>postgresql</artifactId>  
            <scope>runtime</scope>  
        </dependency>   
        
        <dependency>
		   <groupId>org.springframework.boot</groupId>
		   <artifactId>spring-boot-devtools</artifactId>
		   <optional>true</optional>
	</dependency>
   
        <dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.0</version>
	</dependency>
        
        <dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.0.20</version>
	</dependency>
        
        <dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.4</version>
	</dependency>

這邊還用了Mybatis的內容,需要讀者自行去學習相關的知識,這裡不詳細介紹了。

專案的整體預覽:

login.jsp:這邊是一個簡單的form表單

<form action="/loginUser" method="post">
    <input type="text" name="username"> <br>
    <input type="password" name="password"> <br>
    <input type="submit" value="提交">
</form>

index.jsp:簡單的展示介面

<h1> 歡迎登入, ${user.username} </h1>

Unauthorized.jsp:自定義跳轉的無許可權介面

<body>
Unauthorized!
</body>

appliaction.yml:

server:  
  port: 8081
  session-timeout: 30
  tomcat.max-threads: 0
  tomcat.uri-encoding: UTF-8

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://伺服器地址:5432/庫名
    username: XXXXX
    password: XXXXX
  mvc:
    view:
      prefix: /pages/
      suffix: .jsp
mybatis:
  mapper-locations: mappers/*.xml
  type-aliases-pacakage: com.Pojo  #對映的型別在Pojo下面

這是存放的相對位置

TestController:控制器類

@Controller
public class TestController {

    @RequestMapping("/login")
    public String login() {
        return "login";
    }

    @RequestMapping("/index")
    public String index() {
        return "index";
    }

    @RequestMapping("/logout")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();//取出當前驗證主體
        if (subject != null) {
            subject.logout();//不為空,執行一次logout的操作,將session全部清空
        }
        return "login";
    }

    @RequestMapping("unauthorized")
    public String unauthorized() {
        return "unauthorized";
    }

    @RequestMapping("/admin")
    @ResponseBody//註解之後只是返回json資料,不返回介面
    public String admin() {
        return "admin success";
    }

    @RequestMapping("/edit")
    @ResponseBody
    public String edit() {
        return "edit success";
    }
    
    /*
     * 整個form表單的驗證流程:
     * 
     * 將登陸的使用者/密碼傳入UsernamePasswordToken,當呼叫subject.login(token)開始,呼叫Relam的doGetAuthenticationInfo方法,開始密碼驗證
     * 此時這個時候執行我們自己編寫的CredentialMatcher(密碼匹配器),執行doCredentialsMatch方法,具體的密碼比較實現在這實現
     * 
     * */
    @RequestMapping("/loginUser")
    public String loginUser(@RequestParam("username") String username,
                            @RequestParam("password") String password,
                            HttpSession session) {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        try {
        	System.out.println("獲取到資訊,開始驗證!!");
            subject.login(token);//登陸成功的話,放到session中
            User user = (User) subject.getPrincipal();
            session.setAttribute("user", user);
            return "index";
        } catch (Exception e) {
            return "login";
        }
    }
}

ShiroConfiguration.java:自定義了Shiro的配置器

package com.Auth;

import org.apache.shiro.cache.MemoryConstrainedCacheManager;
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.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;

@Configuration
public class ShiroConfiguration {
    
   //@Qualifier代表spring裡面的
	
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(manager);

        bean.setLoginUrl("/login");//提供登入到url
        bean.setSuccessUrl("/index");//提供登陸成功的url
        bean.setUnauthorizedUrl("/unauthorized");
       
        /*
         * 可以看DefaultFilter,這是一個列舉類,定義了很多的攔截器authc,anon等分別有對應的攔截器
         * */
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/index", "authc");//代表著前面的url路徑,用後面指定的攔截器進行攔截
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginUser", "anon");
        filterChainDefinitionMap.put("/admin", "roles[admin]");//admin的url,要用角色是admin的才可以登入,對應的攔截器是RolesAuthorizationFilter
        filterChainDefinitionMap.put("/edit", "perms[edit]");//擁有edit許可權的使用者才有資格去訪問
        filterChainDefinitionMap.put("/druid/**", "anon");//所有的druid請求,不需要攔截,anon對應的攔截器不會進行攔截
        filterChainDefinitionMap.put("/**", "user");//所有的路徑都攔截,被UserFilter攔截,這裡會判斷使用者有沒有登陸
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);//設定一個攔截器鏈

        return bean;
    }
    
    
    /*
     * 注入一個securityManager
     * 原本以前我們是可以通過ini配置檔案完成的,程式碼如下:
     *  1、獲取SecurityManager工廠,此處使用Ini配置檔案初始化SecurityManager
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        2、得到SecurityManager例項 並繫結給SecurityUtils
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
     * */
    @Bean("securityManager")
    public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
    	//這個DefaultWebSecurityManager建構函式,會對Subject,realm等進行基本的引數注入
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(authRealm);//往SecurityManager中注入Realm,代替原本的預設配置
        return manager;
    }
    
    //自定義的Realm
    @Bean("authRealm")
    public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
        AuthRealm authRealm = new AuthRealm();
        //這邊可以選擇是否將認證的快取到記憶體中,現在有了這句程式碼就將認證資訊快取的記憶體中了
        authRealm.setCacheManager(new MemoryConstrainedCacheManager());
        //最簡單的情況就是明文直接匹配,然後就是加密匹配,這裡的匹配工作則就是交給CredentialsMatcher來完成
        authRealm.setCredentialsMatcher(matcher);
        return authRealm;
    }
    
    /* 
     * Realm在驗證使用者身份的時候,要進行密碼匹配
     * 最簡單的情況就是明文直接匹配,然後就是加密匹配,這裡的匹配工作則就是交給CredentialsMatcher來完成
     * 支援任意數量的方案,包括純文字比較、雜湊比較和其他方法。除非該方法重寫,否則預設值為
     * */
    @Bean("credentialMatcher")
    public CredentialMatcher credentialMatcher() {
        return new CredentialMatcher();
    }
    
    
    /*
     * 以下AuthorizationAttributeSourceAdvisor,DefaultAdvisorAutoProxyCreator兩個類是為了支援shiro註解
     * */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
}

這裡自定義了AuthRealm,CredentialsMatcher,來看看它們具體的程式碼:

public class AuthRealm extends AuthorizingRealm{ //AuthenticatingRealm是抽象類,用於認證
    
	@Autowired
	private UserService userService;
	
	/*
	 * 真實授權抽象方法,供子類呼叫
	 * 
	 * 這個是當登陸成功之後會被呼叫,看當前的登陸角色是有有許可權來進行操作
	 * */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		    System.out.println("doGetAuthorizationInfo方法");
		    User user = (User) principals.fromRealm(this.getClass().getName()).iterator().next();
	        List<String> permissionList = new ArrayList<>();
	        List<String> roleNameList = new ArrayList<>();
	        Set<Role> roleSet = user.getRoles();//拿到角色
	        if (CollectionUtils.isNotEmpty(roleSet)) {
	            for(Role role : roleSet) {
	                roleNameList.add(role.getRname());//拿到角色
	                Set<Permission> permissionSet = role.getPermissions();
	                if (CollectionUtils.isNotEmpty(permissionSet)) {
	                    for (Permission permission : permissionSet) {
	                        permissionList.add(permission.getName());
	                    }
	                }
	            }
	        }
	        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
	        info.addStringPermissions(permissionList);//拿到許可權
	        info.addRoles(roleNameList);//拿到角色
	        return info;
	}
    
	/*
	 * 用於認證登入,認證介面實現方法,該方法的回撥一般是通過subject.login(token)方法來實現的
	 * AuthenticationToken 用於收集使用者提交的身份(如使用者名稱)及憑據(如密碼):
	 * AuthenticationInfo是包含了使用者根據username返回的資料資訊,用於在匹馬比較的時候進行相互比較
	 * 
	 * shiro的核心是java servlet規範中的filter,通過配置攔截器,使用攔截器鏈來攔截請求,如果允許訪問,則通過。
	 * 通常情況下,系統的登入、退出會配置攔截器。登入的時候,呼叫subject.login(token),token是使用者驗證資訊,
	 * 這個時候會在Realm中doGetAuthenticationInfo方法中進行認證。這個時候會把使用者提交的驗證資訊與資料庫中儲存的認證資訊,將所有的資料拿到,在匹配器中進行比較
	 * 這邊是我們自己實現的CredentialMatcher類的doCredentialsMatch方法,返回true則一致,false則登陸失敗
	 * 退出的時候,呼叫subject.logout(),會清除回話資訊
	 * 
	 * */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("將使用者,密碼填充完UsernamePasswordToken之後,進行subject.login(token)之後");
		UsernamePasswordToken  userpasswordToken = (UsernamePasswordToken) token;//這邊是介面的登陸資料,將資料封裝成token
		String username = userpasswordToken.getUsername();
		User user = userService.findByUsername(username);
		return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName());
	}

}
/*
 * 密碼校驗方法繼承SimpleCredentialsMatcher或HashedCredentialsMatcher類,自定義實現doCredentialsMatch方法
 * */
public class CredentialMatcher extends SimpleCredentialsMatcher {
    
	/*
	 * 這裡是進行密碼匹配的方法,自己定義
	 * 通過使用者的唯一標識得到 AuthenticationInfo 然後和 AuthenticationToken (使用者名稱 密碼),進行比較
	 * */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    	System.out.println("這邊是密碼校對");
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String password = new String(usernamePasswordToken.getPassword());
        String dbPassword = (String) info.getCredentials();//資料庫裡的密碼
        return this.equals(password, dbPassword);
    }
}

UserMapper.java:

public interface UserMapper {
    User findByUsername(@Param("username") String username);
}

UserMapper對應的UserMapper.xml如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 

<mapper namespace="com.Mapper.UserMapper">
    <resultMap id="userMap" type="com.Pojo.User">
        <id property="uid" column="uid" />
        <id property="username" column="username" />
        <id property="password" column="password" />
        <collection property = "roles" ofType="com.Pojo.Role">
           <id property="rid" column="rid" />
           <id property="rname" column="rname" />
           <collection property="permissions" ofType="com.Pojo.Permission">
               <id property="pid" column="pid" />
               <id property="name" column="name" />
               <id property="url" column="url" />
           </collection>
        </collection>
    </resultMap>
    
    <select id="findByUsername" parameterType="string" resultMap="userMap">
      SELECT u.*, r.*, p.*
        FROM "user" u
        INNER JOIN user_role ur on ur.uid = u.uid
        INNER JOIN role r on r.rid = ur.rid
        INNER JOIN permission_role pr on pr.rid = r.rid
        INNER JOIN permission p on pr.pid = p.pid
        WHERE u.username = #{username}
    </select>
</mapper> 

這邊注意,在我springBoot的啟動類中,已經把包掃描了@MapperScan("com.Mapper")

@SpringBootApplication
@MapperScan("com.Mapper")
public class SpringBootShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootShiroApplication.class, args);
    }
}

這邊還有service類,和service的實現類:

public interface UserService {
   User findByUsername(String username);
}

@Service
public class UserServiceImpl implements UserService{
    @Resource
    private UserMapper userMapper;
    @Override
    public User findByUsername(String username) {
        return userMapper.findByUsername(username);
    }

}

另外這邊也定義了幾個Pojo:

User.java:使用者類

public class User {
    private Integer uid;
    private String username;
    private String password;
    private Set<Role> roles = new HashSet<Role>();
    public Integer getUid() {
		return uid;
	}
	public void setUid(Integer uid) {
		this.uid = uid;
	}
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public Set<Role> getRoles() {
		return roles;
	}
	public void setRoles(Set<Role> roles) {
		this.roles = roles;
	}	
}

Permission.java:許可權類

public class Permission {
   private Integer pid;
   private String name;
   private String url;
   
    public Integer getPid() {
	    return pid;
	}
	public void setPid(Integer pid) {
		this.pid = pid;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getUrl() {
		return url;
	}
	public void setUrl(String url) {
		this.url = url;
	}
	
}

Role.java:角色類

public class Role {

    private Integer rid;

    private String rname;

    private Set<Permission> permissions = new HashSet<>();//一個角色有多個許可權

    private Set<User> users = new HashSet<>();

    public Integer getRid() {
        return rid;
    }

    public void setRid(Integer rid) {
        this.rid = rid;
    }

    public String getRname() {
        return rname;
    }

    public void setRname(String rname) {
        this.rname = rname;
    }

    public Set<Permission> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<Permission> permissions) {
        this.permissions = permissions;
    }

    public Set<User> getUsers() {
        return users;
    }

    public void setUsers(Set<User> users) {
        this.users = users;
    }
}

具體的sql如下:

-------許可權表------
CREATE TABLE permission
(
  pid serial NOT NULL,
  name character varying(255)  NOT NULL,
  url character varying(255),
  CONSTRAINT permission_pkey PRIMARY KEY (pid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE permission
  OWNER TO logistics;

INSERT INTO permission values('1','add','')
INSERT INTO permission values('2','delete','')
INSERT INTO permission values('3','edit','')
INSERT INTO permission values('4','query','')

-------使用者表------
CREATE TABLE "user"
(
  uid serial NOT NULL,
  username character varying(255)  NOT NULL,
  password character varying(255),
  CONSTRAINT user_pkey PRIMARY KEY (uid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE "user"
  OWNER TO logistics;

INSERT INTO "user" values('1','admin','123456')
INSERT INTO "user" values('2','demo','123456')

-------角色表------
CREATE TABLE role
(
  rid serial NOT NULL,
  rname character varying(255)  NOT NULL,
  CONSTRAINT role_pkey PRIMARY KEY (rid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE role
  OWNER TO logistics;

INSERT INTO role values('1','admin')
INSERT INTO role values('2','customer')

-----許可權角色關係表-----
CREATE TABLE permission_role
(
  rid integer NOT NULL,
  pid integer NOT NULL,
  CONSTRAINT permission_role_pkey PRIMARY KEY (pid,rid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE permission_role
  OWNER TO logistics;


INSERT INTO permission_role values(1,1)
INSERT INTO permission_role values(1,2)
INSERT INTO permission_role values(1,3)
INSERT INTO permission_role values(1,4)
INSERT INTO permission_role values(2,1)
INSERT INTO permission_role values(2,4)

-----使用者角色關係表-----
CREATE TABLE user_role
(
  rid integer NOT NULL,
  uid integer NOT NULL,
  CONSTRAINT user_role_pkey PRIMARY KEY (uid, rid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE user_role
  OWNER TO logistics;


INSERT INTO user_role values(1,1)
INSERT INTO user_role values(2,2)

此時我們開始測試:

輸入localhost:8081/admin,由於我們在ShiroConfiguration中配置了一個攔截器鏈,對應的URL路徑都會被對應的攔截器給攔截來處理。

LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/index", "authc");//代表著前面的url路徑,用後面指定的攔截器進行攔截
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginUser", "anon");
        filterChainDefinitionMap.put("/admin", "roles[admin]");//admin的url,要用角色是admin的才可以登入,對應的攔截器是RolesAuthorizationFilter
        filterChainDefinitionMap.put("/edit", "perms[edit]");//擁有edit許可權的使用者才有資格去訪問
        filterChainDefinitionMap.put("/druid/**", "anon");//所有的druid請求,不需要攔截,anon對應的攔截器不會進行攔截
        filterChainDefinitionMap.put("/**", "user");//所有的路徑都攔截,被UserFilter攔截,這裡會判斷使用者有沒有登陸
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);//設定一個攔截器鏈

這裡我們可以看到,admin路徑是被roles對應的攔截器RolesAuthorizationFilter攔截,在方法isAccessAllowed中進行處理,判斷是不是admin角色的使用者,是這個角色的才可以訪問,否則前往自己定義的無許可權介面,這裡別名對應的攔截器是在DefaultFilter這個列舉類中有定義:

    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);

由於現在沒有登入,所以一開始會前往登入介面,填寫使用者賬號和密碼,點選提交,因為我們form表單的action是loginUser,此時資料提交到Controller中對應的處理方法中:

/*
     * 整個form表單的驗證流程:
     * 
     * 將登陸的使用者/密碼傳入UsernamePasswordToken,當呼叫subject.login(token)開始,呼叫Relam的doGetAuthenticationInfo方法,開始密碼驗證
     * 此時這個時候執行我們自己編寫的CredentialMatcher(密碼匹配器),執行doCredentialsMatch方法,具體的密碼比較實現在這實現
     * 
     * */
    @RequestMapping("/loginUser")
    public String loginUser(@RequestParam("username") String username,
                            @RequestParam("password") String password,
                            HttpSession session) {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        try {
        	System.out.println("獲取到資訊,開始驗證!!");
            subject.login(token);//登陸成功的話,放到session中
            User user = (User) subject.getPrincipal();
            session.setAttribute("user", user);
            return "index";
        } catch (Exception e) {
            return "login";
        }
    }

我們會把使用者名稱,密碼存入到UsernamePasswordToken中,UsernamePasswordToken是一個使用者,密碼認證令牌,裡面有使用者名稱,密碼,是否快取等屬性。然後程式碼就會跳轉到我們自己編寫的Realm--AuthRealm的doGetAuthenticationInfo方法(具體可以看這篇博文https://www.cnblogs.com/ccfdod/p/6436353.html 這理由詳細的介紹,這個程式碼呼叫如下:subject.login(token)-->DelegatingSubject類的login方法-->SecurityManager的login-->DefaultSecurityManager的login方法-->AuthenticatingSecurityManager的authenticate方法-->實現類AuthenticatingRealm中的getAuthenticationInfo方法)。在我們自己的getAuthenticationInfo方法中,我們根據使用者名稱查詢出使用者的資訊,返回AuthenticationInfo物件,如果token與獲取到的AuthenticationInfo都不為空,快取AuthenticationInfo資訊。接著程式碼會跳轉到我們的憑證驗證的方法CredentialMatcher類的doCredentialsMatch方法:

@Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    	System.out.println("這邊是密碼校對");
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String password = new String(usernamePasswordToken.getPassword());
        String dbPassword = (String) info.getCredentials();//資料庫裡的密碼
        return this.equals(password, dbPassword);
    }

其實我們在呼叫AuthenticatingRealm的getAuthenticationInfo方法時:

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
  
        AuthenticationInfo info = getCachedAuthenticationInfo(token);  
        if (info == null) {  
            //otherwise not cached, perform the lookup:  
            info = doGetAuthenticationInfo(token);  
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);  
            if (token != null && info != null) {  
                cacheAuthenticationInfoIfPossible(token, info);  
            }  
        } else {  
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);  
        }  
  
        if (info != null) {  
            assertCredentialsMatch(token, info);  
        } else {  
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);  
        }  
  
        return info;  
    }  

當AuthenticationInfo查出來不為空時,進行憑證密碼匹配,呼叫assertCredentialsMatch(token,info):

protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {  
        CredentialsMatcher cm = getCredentialsMatcher();  
        if (cm != null) {  
            if (!cm.doCredentialsMatch(token, info)) {  
                //not successful - throw an exception to indicate this:  
                String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";  
                throw new IncorrectCredentialsException(msg);  
            }  
        } else {  
            throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +  
                    "credentials during authentication.  If you do not wish for credentials to be examined, you " +  
                    "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");  
        }  
    } 

這邊呼叫cm.doCredentialsMatch(token, info)方法,這邊要闡述下CredentialsMatcher是一個介面,用來憑證密碼匹配的,繼承並實現doCredentialsMatch方法即可,這邊我們自定義的CredentialMatcher類,繼承了SimpleCredentialsMatcher類,而SimpleCredentialsMatcher實現了CredentialsMatcher方法。所以繼續接著上面思路的進入我們的密碼匹配方法,如果匹配正確則返回true,如果驗證失敗則返回false。此時一個完整的登入驗證完成。

那麼當我們繼續訪問其他的URL時,會進入我們授權的方法,AuthRealm類的doGetAuthorizationInfo(),主要是拿到登入使用者的角色和許可權,以此判斷該使用者是否有許可權進入URL,沒有許可權則被跳轉到unauthorized.jsp介面,( bean.setUnauthorizedUrl("/unauthorized");--原先設定的沒有訪問許可權的情況)。

   這篇文章就講到這,如果讀者有補充,或者頁面中有不對的地方請指正。