需求:
在公司新的系統里面博主我使用的是ApachShiro 作為安全框架、作為后端的鑒權以及登錄、分配權限等操作 管理員的信息都是存儲在管理員表
前台App 用戶也需要校驗用戶名和密碼進行登錄、但是用戶的信息卻是存在另一張表里面、如何給這兩個不同的數據表進行登錄?鑒權呢?
當然 按照Shiro的強大,我們完全可以用一個接口作為登錄的驗證、不同的Realm 來執行不同的邏輯即可
相關知識儲備 Realm
用最簡單的話來說 一個Realm就是一個檢驗用戶身份的組件,但這里這個組件需要我們繼承去重寫,因為每個系統有各自不同的業務邏輯,這些事情是Shiro所不能了解的,我們得通過這個
Realm 告訴Shiro 我們的密碼是怎么加密得到的,還有用戶名是哪個,以及加密的方式是啥
————————————————————————————————————————————
了解這些需要了解的東西之后,我們模仿現有的Realm,照貓畫虎的來一個
@Component public class WeChatRealm extends AuthorizingRealm { @Autowired private VehicleOwnerService vehicleOwnerService; @Autowired private SysUserService sysUserService; /** * 授權 微信接口沒有權限 * * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //沒有權限機制返回Null即可 return null; } /* * @Author MRC * @Description 認證 * @Date 11:36 2019/9/11 * @Param [token] * @return org.apache.shiro.authc.AuthenticationInfo **/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("微信登錄認證"); //登錄用戶名 String username = (String) token.getPrincipal(); Wrapper<VehicleOwner> vehicleOwnerWrapper = new EntityWrapper<>(); vehicleOwnerWrapper.eq("phone",username); VehicleOwner vehicleOwner = vehicleOwnerService.selectOne(vehicleOwnerWrapper); if (vehicleOwner == null) { //找不到這個用戶直接返回null return null; } //構造一個簡單的認證信息 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( vehicleOwner, //用戶名 vehicleOwner.getPassword(), //密碼 ByteSource.Util.bytes(username + vehicleOwner.getSalt()),//salt=username+salt getName() //realm name ); return authenticationInfo; } }
- 繼承 AuthorizingRealm 重寫 doGetAuthorizationInfo()鑒權方法 以及 doGetAuthenticationInfo()認證方法
- 按照傳入的用戶名查找這個用戶是否存在,不存在就返回null即可
- 這里不校驗密碼,直接把用戶名和密碼以及鹽值(如果存在加鹽機制)就一起傳遞過去 交給Shiro去校驗
加入到Shiro SecurityManager當中
/** * 前端驗證Realm * @return */ public WeChatRealm getWeChatRealm() { //使用MD5憑證管理器 weChatRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return weChatRealm; }
通過@Bean 的方式注入一個SecurityManager 對象 並且加入多個Realm
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // securityManager.setRealm(myShiroRealm()); List<Realm> list = new ArrayList<>(); list.add(myShiroRealm()); list.add(getWeChatRealm()); //設置多個Realm securityManager.setRealms(list); // 自定義session管理 使用redis securityManager.setSessionManager(sessionManager()); // 自定義緩存實現 使用redis securityManager.setCacheManager(redisCacheManager()); return securityManager; }
配置是配置好了 那他們兩個是如何工作的呢?
debug 走你~
從前台拿到的用戶名和密碼封裝成 token 傳遞到login方法內
Subject subject = SecurityUtils.getSubject();
//封裝toKen UsernamePasswordToken token = new UsernamePasswordToken(sysUser.getUsername(), sysUser.getPassword()); //這里會拋出異常 subject.login(token);
繼續跟進 ,進入用戶名校驗的過程。
token 里面封裝了我們傳遞過來的admin用戶名和密碼
繼續跟進,進入到login方法。 跳轉到authenticate(token)方法 這里才是真正意義上驗證方法
跟進到 authenticate(AuthenticationToken token) 方法
doAuthenticate(token) 才是要進行驗證的方法,繼續跟進,進入到
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
//初始化Realms assertRealmsConfigured();
//取出我們多個Realm Collection<Realm> realms = getRealms();
//一個或者多個執行不同的方法 if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
這里取出我們配置的兩個Realm
我們配置里兩個控制器,需要去做兩個不同的校驗,我們繼續跟進。
這里我把這個方法的源代碼貼出來,分析一下
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
//獲取驗證策略 AuthenticationStrategy strategy = getAuthenticationStrategy();
//獲取到一個簡單的驗證信息(圖1) AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); if (log.isTraceEnabled()) { log.trace("Iterating through {} realms for PAM authentication", realms.size()); }
//開始循環拿出所有的Realm for (Realm realm : realms) {
//這里返回的是傳入的 aggregate ,不知道這個是干嘛的(圖2) aggregate = strategy.beforeAttempt(realm, token, aggregate);
//判斷是否支持toKen if (realm.supports(token)) { log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm); AuthenticationInfo info = null; Throwable t = null; try {
//關鍵 開始獲取驗證信息 開始用戶名和密碼的比對 info = realm.getAuthenticationInfo(token); } catch (Throwable throwable) { t = throwable; if (log.isWarnEnabled()) { String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:"; log.warn(msg, t); } } //驗證完成后 aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); } else { log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); } } //將最終的驗證信息返回出去 圖7
//如果aggregate(我們驗證的用戶信息)為空則拋出一個異常 aggregate = strategy.afterAllAttempts(token, aggregate); return aggregate; }
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//獲取緩存里面的驗證信息 AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) {
//緩存里面沒有,開始驗證,跳轉到我們自己寫的邏輯 (圖3) //otherwise not cached, perform the lookup: info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) {
//這里把我們前台傳遞過來的token 以及從數據庫查詢出來的對象要進行一個對比 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); } //密碼正確 返回info return info; }
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
### 獲取密碼憑證管理器 CredentialsMatcher cm = getCredentialsMatcher(); if (cm != null) {
//檢驗密碼正確性(圖6) 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."); } }
圖一
在所有的嘗試之前,它先New 出一個簡單的驗證對象
圖二
返回的依舊是一個傳入的一個Aggregate對象
圖3 跳轉到我們自己寫的邏輯層 返回一個用戶名和密碼的包裝體
圖4 這里沒有開啟緩存,直接跳出,不走下面的緩存相關的方法
圖5 拿到我們配置的憑證管理器,配置的MD5以及加密次數
圖6 檢驗密碼的正確性 使用equals方法進行比對兩個密碼的方法
第二遍循環,因為在第一個循環(第一個Realm)里面已經匹配到,第二個肯定匹配不到,我們繼續跟進
返回了一個NULL
圖7
如果兩個都匹配不到,就會拋出一個異常,賬號不存在的異常,我們捕獲即可