Shiro认证详解

Shiro

shiro是一个java的安全框架
官网地址 //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>

缓存

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

思考:为什么Shiro要设计成既可以在Realm,也可以在SecurityManager中设置缓存管理器呢?

加密

//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