1. 程式人生 > 實用技巧 >Shiro認證詳解

Shiro認證詳解

Shiro

shiro是一個java的安全框架
官網地址 http://shiro.apache.org/

目錄

Shiro綜述

graph LR A1("CacheManager")-->B A2("Realms")-->B A3("UserDao")-->C A4("CredentialsMatcher")-->C A1-->C subgraph Shiro A("Subject(使用者)")-->B("SecurityManager(安全管理器)") B-->C("Realm域") end
  • Subject
    :主體,代表了當前 “使用者”
  • SecurityManager:安全管理器;即所有與安全有關的操作都會與 SecurityManager 互動;且它管理著所有 Subject;是 Shiro 的核心
  • Realm:域,Shiro 從從 Realm 獲取安全資料(如使用者、角色、許可權),就是說 SecurityManager 要驗證使用者身份,那麼它需要從 Realm 獲取相應的使用者進行比較以確定使用者身份是否合法;也需要從 Realm 得到使用者相應的角色 / 許可權進行驗證使用者是否能進行操作;可以把 Realm 看成 DataSource,即安全資料來源。

Shiro 不提供維護使用者 / 許可權,而是通過 Realm 讓開發人員自己注入。

參考Shiro提供的JdbcRealm中原始碼的實現

//獲取使用者,其會自動繫結到當前執行緒
Subject subject = SecurityUtils.getSubject();
//構建待認證token
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
//登入,即身份驗證
subject.login(token);
//判斷是否已經認證
subject.isAuthenticated()
//登出
subject.logout(token);

graph TB
A(Realm)-->B(CachingRealm)
B-->C(AuthenticatingRealm認證)
C-->D(AuthorizingRealm授權)
D-->E(自己實現的Realm)
D-->E1(Shiro提供的JdbcRealm)
E1-->F1(參考內部實現)
E-->F("doGetAuthorizationInfo()")
E-->G("doGetAuthenticationInfo()")
style E fill:#f96

過濾器

認證攔截器

  • anon 匿名攔截器,不需要認證即可訪問,如 /static/**=anon,/login=anon
  • authc 需要認證才可以訪問,如/**=authc
  • user 使用者已經身份驗證 / 記住我登入的都可;示例 /**=user
  • logout 退出攔截器,如 /logout=logout

注意authc和user的區別

授權攔截器

  • roles 角色授權攔截器,驗證使用者是否擁有角色;如:/admin/**=roles[admin]
  • perms 許可權授權攔截器,驗證使用者是否擁有所有許可權;/user/**=perms["user:create"]

註解

  • @RequiresPermissions 驗證許可權
  • @RequiresRoles 驗證角色
  • @RequiresUser 驗證使用者是否登入(包含通過記住我登入的)
  • @RequiresAuthentication 驗證是否已認證(不含通過記住我登入的)
  • @RequiresGuest 不需要認證即可訪問
//擁有ADMIN角色同時還要有sys:role:info許可權
@RequiresRoles(value={"ADMIN")
@RequiresPermissions("sys:role:info")

整合Shiro

1. 配置SecurityManager

注入Realm和CacheManager(選)

 @Bean("securityManager")
    public org.apache.shiro.mgt.SecurityManager securityManager(ShrioRealm shrioRealm, PhoneRealm phoneRealm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//      //注入自定義myRealm
//      defaultWebSecurityManager.setRealm(shrioRealm);
        //設定多個realm,使用者名稱密碼登入realm,手機號簡訊驗證碼登入realm
        List<Realm> realms = new ArrayList<>();
        realms.add(shrioRealm);
        realms.add(phoneRealm);
        defaultWebSecurityManager.setRealms(realms);

        return defaultWebSecurityManager;
    }

2.實現Realm

注入密碼驗證器,設定是否啟用快取

/**
 *
 * 自定義realm
 * @author yuxf
 * @version 1.0
 * @date 2020/12/21 16:10
 */
public class ShrioRealm extends AuthorizingRealm {

    @Autowired
    TestShiroUserService userService;

    /**
     * 獲取授權資訊
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //從資料庫取角色
        Set<String> roles = userService.getRoles();
        SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();//許可權資訊
        simpleAuthorizationInfo.addRoles(roles);
        simpleAuthorizationInfo.addStringPermission("user:create");
        return  simpleAuthorizationInfo;
    }

    /**
     * 獲取認證資訊
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        if(token.getPrincipal()==null)return null;
        String userName=token.getPrincipal().toString();
        //從資料庫查詢使用者名稱
        String dbUser = userService.loadUserByUserName(userName);
        if(dbUser==null||"".equals(dbUser)) throw  new UnknownAccountException();
        //密碼鹽
        ByteSource salt = ByteSource.Util.bytes(userName);
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, "123456", salt,getName());
        return simpleAuthenticationInfo;
    }
}


 @Bean
    public ShrioRealm shrioRealm() {
        ShrioRealm shrioRealm = new ShrioRealm();
        //設定密碼加密規則
        shrioRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shrioRealm;
    }
    
     /**
     * 憑證匹配器
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
         HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
         hashedCredentialsMatcher.setHashAlgorithmName("md5");//雜湊演算法:這裡使用MD5演算法;
         hashedCredentialsMatcher.setHashIterations(2);//雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
         return hashedCredentialsMatcher;
    }
/**
 * 註冊時需要生成密碼和密碼鹽存入資料庫
 * @author yuxf
 * @version 1.0
 * @date 2020/12/22 17:01
 */
public class PasswordHelper {
    private static String algorithmName = "md5";
    private static final int hashIterations = 2;
    /**
     * 獲取隨機密碼鹽
     * @return
     */
    public  static String getSalt()
    {
        RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
        String salt = randomNumberGenerator.nextBytes().toHex();
        return  salt;
    }

    /**
     * 生成密碼
     * @param plainPassword 明文密碼
     * @param salt 密碼鹽
     * @return
     */
    public  static String getPassowrd(String plainPassword,String salt)
    {
        String newPassword = new SimpleHash(algorithmName, plainPassword, salt, hashIterations).toHex();
        return  newPassword;
    }
}

3.配置LifecycleBeanPostProcessor

 /**
     * 配置LifecycleBeanPostProcessor 可以自動呼叫配置在Spring IOC容器中 Shiro Bean的生命週期方法
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

4.啟動註解

/**
     * 配置註解生效
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 配置註解生效
     *
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        sourceAdvisor.setSecurityManager(securityManager);
        return sourceAdvisor;
    }

5.配置ShiroFilter

ssm專案中坑

@Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") org.apache.shiro.mgt.SecurityManager securityManager) {
        //shiro物件
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        bean.setLoginUrl("/shiro/login");
        bean.setSuccessUrl("/shrio/index");
        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<String, String>();
        //認證順序是從上往下執行。
        linkedHashMap.put("/logout", "logout");//在這兒配置登出地址,不需要專門去寫控制器。
        linkedHashMap.put("/shiro/phoneLogin", "anon");
        linkedHashMap.put("/demo/**", "anon");
        linkedHashMap.put("/static/**", "anon");
        linkedHashMap.put("/shiro/anon", "anon");
        linkedHashMap.put("/**", "user");//需要進行許可權驗證
        bean.setFilterChainDefinitionMap(linkedHashMap);
        return bean;
    }
    
    

SSM專案中web.xml中配置shiroFilter

<!-- shiro過慮器,DelegatingFilterProxy通過代理模式將spring容器中的bean和filter關聯起來 -->
	<filter>
		<filter-name>shiroFilter</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
		 <!-- 設定true由servlet容器控制filter的生命週期 -->
		<init-param>
			<param-name>targetFilterLifecycle</param-name>
			<param-value>true</param-value>
		</init-param> 
		<!-- 設定spring容器filter的bean id,如果不設定則找與filter-name一致的bean -->
		<init-param>
			<param-name>targetBeanName</param-name>
			<param-value>shiro</param-value>
		</init-param>
	</filter>

快取

https://www.cnblogs.com/nuccch/p/8044226.html

思考:為什麼Shiro要設計成既可以在Realm,也可以在SecurityManager中設定快取管理器呢?

加密

https://www.cnblogs.com/cac2020/p/13850318.html

1. 注入HashedCredentialsMatcher實現(推薦)

需要自己編寫加密幫助類生成密碼和鹽值,比較靈活

@Bean
    public ShrioRealm shrioRealm() {
        ShrioRealm shrioRealm = new ShrioRealm();
        //設定密碼加密規則
        shrioRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shrioRealm;
    }
      /**
     * 憑證匹配器
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//雜湊演算法:這裡使用MD5演算法;
        hashedCredentialsMatcher.setHashIterations(2);//雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
        return hashedCredentialsMatcher;
    }

加密幫助類

/**
 * 註冊時需要生成密碼和密碼鹽存入資料庫
 *
 * @author yuxf
 * @version 1.0
 * @date 2020/12/22 17:01
 */
public class PasswordHelper {
    private static String algorithmName = "md5";
    private static final int hashIterations = 2;

    /**
     * 獲取隨機密碼鹽
     *
     * @return
     */
    public static String getSalt() {
        RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
        String salt = randomNumberGenerator.nextBytes().toHex();
        return salt;
    }

    /**
     * 生成密碼
     *
     * @param plainPassword 明文密碼
     * @param salt          密碼鹽
     * @return
     */
    public static String getPassowrd(String plainPassword, String salt) {
        String newPassword = new SimpleHash(algorithmName, plainPassword, salt, hashIterations).toHex();
        return newPassword;
    }

}

2. 注入PasswordMatcher實現

  1. Shiro提供的PasswordService 相當於 密碼幫助類,可用於生成密碼和驗證密碼
  2. 如果使用公鹽(hashService.setGeneratePublicSalt(true)),則必須設定HashFormat為Shiro1CryptFormat或不設定,預設為這個,否則無法儲存鹽值導致驗證失敗,密碼加密結果如:$shiro1$MD5$3$QvLJZY8JiAJMnK9vRjlG6w==$jbNS0N/3fq2KUXufYwGwWA==,裡面包含了加密的方法型別,雜湊次數,鹽值,加密結果,驗證密碼時會取出加密密碼中的鹽值來hash客戶端的密碼來驗證密碼是否正確
  3. 鹽值儲存在密碼中,無需額外儲存
@Bean
    public PhoneRealm phoneRealm() {
        PhoneRealm phoneRealm = new PhoneRealm();
        //PasswordMatcher
        PasswordMatcher passwordMatcher = new PasswordMatcher();
        passwordMatcher.setPasswordService(passwordService());
        phoneRealm.setCredentialsMatcher(passwordMatcher);
        return phoneRealm;
    }
    
     @Bean
    public  PasswordService passwordService()
    {
        DefaultHashService hashService = new DefaultHashService();
        hashService.setHashIterations(3);
        hashService.setHashAlgorithmName("MD5");
        hashService.setGeneratePublicSalt(true);
        //設定HashService
        DefaultPasswordService passwordService = new DefaultPasswordService();
        passwordService.setHashService(hashService);
       // passwordService.setHashFormat(new HexFormat());
        return  passwordService;
    }

多身份Realm認證

  1. (推薦)自定義AuthenticationToken並重寫Realm的supports方法,來明確Real支援的Token

注意不要繼承UsernamePasswordToken

public class PhoneVcodeToken implements AuthenticationToken {
    private String phone;
    private String vcode;
    public  PhoneVcodeToken(String phone,String vcode)
    {
        this.phone=phone;
        this.vcode=vcode;
    }
    @Override
    public Object getPrincipal() {
        return phone;
    }

    @Override
    public Object getCredentials() {
        return vcode;
    }
}

Realm

public class PhoneRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String userName = token.getPrincipal().toString();
        if (userName.equals("admin")) {
            //123456a
            return new SimpleAuthenticationInfo(userName, "$shiro1$MD5$3$j8X4VX1f6T6zGiGEFIW5yA==$ipG89XmDquh++g5xXmV1dQ==", getName());
        } else {
            //123456
            return new SimpleAuthenticationInfo(userName, "$shiro1$MD5$3$QvLJZY8JiAJMnK9vRjlG6w==$jbNS0N/3fq2KUXufYwGwWA==", getName());
        }
    }

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof PhoneVcodeToken;
    }
}
  1. 自定義AuthenticationToken並加入型別引數,重寫ModularRealmAuthenticator 在doAuthenticate()方法中根據型別來選擇Realm
/**
 * @author chenzhi
 * @Description: 自定義當使用多realm時管理器
 * @Date:Created: in 13:41 2018/8/13
 * @Modified by:
 */
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
 
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) {
        //先判斷Realm是否為空
        assertRealmsConfigured();
        //強轉為自定義的Token
        MyUsernamePasswordToken myUsernamePasswordToken = (MyUsernamePasswordToken) authenticationToken;
        //拿到登入型別
        String loginType = myUsernamePasswordToken.getLoginType();
        //拿到所有Realm集合
        Collection<Realm> realms = getRealms();
        List<Realm> myrealms = new ArrayList<>();
        //遍歷每個realm 根據loginType將對應的Reaml加入到myrealms
        for (Realm realm : realms) {
            //拿到Realm的類名 ,所以在定義Realm時,類名要唯一標識並且包含列舉中loginType某一個Type
            //注意:一個Realm的類名不能包含有兩個不同的loginType
            if (realm.getName().contains(loginType)) {
                myrealms.add(realm);
            }
        }
        //判斷是單Reaml還是多Realm
        if (myrealms.size() == 1) {
            return doSingleRealmAuthentication(myrealms.iterator().next(), myUsernamePasswordToken);
        } else {
            return doMultiRealmAuthentication(myrealms, myUsernamePasswordToken);
        }
    }
}

認證流程

token=new UsernamePasswordToken(userName,password)
graph TB subgraph Suject A1("Subject")--"subject = SecurityUtils.getSubject();"-->C1("token") B1(token)-->C1("subject.login(token)") end subgraph SecurityManager A2("securityManager.login(token)") B2("onSuccessfulLogin(token, info, loggedIn)") end subgraph ModularRealmAuthenticator A3("authenticate(token)") end subgraph Realm A4("getAuthenticationInfo(token)")--獲取認證資訊-->B4("doGetAuthenticationInfo(token)") B4--傳入認證資訊並驗證密碼-->C4("assertCredentialsMatch(token,info)") end subgraph CredentialsMatcher A5("doCredentialsMatch(token,info)") end C1-->A2 A2--this.authenticator-->A3 A3-->B2 A3--"this.getRealms()"-->A4 C4--"getCredentialsMatcher()"-->A5