【Shiro】Spring Boot下多Realm认证和授权
本文最后更新于 1381 天前,其中的信息可能已经有所发展或是发生改变。

问题

现在有用户(yiban)和管理员(admin)两个角色需要实现分开登陆,他们的认证信息存储在不同的表里面;这个时候需要我们创建两个Realm来实现分别的登陆和授权。
这里的案例以我编写的易班请假与考勤系统作为案例。管理员登陆需要对账号密码进行验证登陆;易班账号登陆无需验证密码,直接授权(就是免登陆)。

解决方案

1.shiro的Maven

pom.xml

        <!--shiro 组件 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.2.2</version>
        </dependency>

2.创建一个枚举

通过这个枚举来区分登陆的类型。
VirtualType.java

package com.yiban.yblaas.shiro;

public enum VirtualType {
    ADMIN, //管理员
    YIBAN //易班用户
}

3.自定义Shiro的Token

通过自定义的token来保存需要认证的信息和登陆类型。自定义的token需要继承org.apache.shiro.authc.UsernamePasswordToken。
authc/UserToken.java

package com.yiban.yblaas.shiro.authc;

import com.yiban.yblaas.shiro.VirtualType;
import org.apache.shiro.authc.UsernamePasswordToken;

/**
 * @program: yblaas
 * @description: 自定义Shiro的Token
 * @author: xiaozhu
 * @create: 2020-03-13 16:32
 **/
public class UserToken extends UsernamePasswordToken {
    private VirtualType virtualType;

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

    public VirtualType getVirtualType() {
        return virtualType;
    }

    public void setVirtualType(VirtualType virtualType) {
        this.virtualType = virtualType;
    }
}

4.编写自定义的Realm。

自定义的Realm的命名需要带有枚举中的值,不然不能匹配上对应的Realm。自定义的Realm需要继承org.apache.shiro.realm.AuthorizingRealm,重写doGetAuthorizationInfo授权方法和doGetAuthenticationInfo认证方法。
realm/RealmADMIN.java

package com.yiban.yblaas.shiro.realm;

import com.yiban.yblaas.mapper.DbConfigMapper;
import com.yiban.yblaas.mapper.PermissionsMapper;
import com.yiban.yblaas.mapper.RolesMapper;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @program: yblaas
 * @description: 管理员的Realm
 * @author: xiaozhu
 * @create: 2020-03-13 17:14
 **/
@Component
public class RealmADMIN extends AuthorizingRealm {

    private static final Logger logger = LoggerFactory.getLogger(RealmADMIN.class);

    @Autowired
    private RolesMapper rolesMapper;
    @Autowired
    private PermissionsMapper permissionsMapper;
    @Autowired
    private DbConfigMapper dbConfigMapper;

    /**
     * 功能描述:
     * (重写授权方法)
     *
     * @param principalCollection 1
     * @return : org.apache.shiro.authz.AuthorizationInfo
     * @author : xiaozhu
     * @date : 2020/3/12 22:16
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //管理员角色的id为1
        try {
            String adminUser = (String) principalCollection.getPrimaryPrincipal();
            if(!adminUser.equals(dbConfigMapper.selectValue("admin_user"))){
                return null;
            }
            //从缓存中拿到角色数据(没有设置缓存只能再查一次数据库)
            Set<String> roles = getRolesByUserId("1");
            //从缓存中拿到权限数据(没有设置缓存只能再查一次数据库)
            Set<String> permissions = getPermissionUserId(roles);
            //返回对象
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            authorizationInfo.setRoles(roles);
            authorizationInfo.setStringPermissions(permissions);
            return authorizationInfo;
        } catch (Exception e) {
            logger.error("管理员权限授权错误,错误信息:"+e.toString());
            return null;
        }
    }

    /**
     * 功能描述:
     * (重写认证方法)
     *
     * @param authenticationToken 1
     * @return : org.apache.shiro.authc.AuthenticationInfo
     * @author : xiaozhu
     * @date : 2020/3/12 22:17
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        try {
            //1.从主体传过来的认证信息中获取用户名
            String username = (String) authenticationToken.getPrincipal();
            String userId = dbConfigMapper.selectValue("admin_user");
            String password = dbConfigMapper.selectValue("admin_password");
            if(userId == null || password == null){
                return null;
            }
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userId,password,"RealmADMIN");
            authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
            return authenticationInfo;
        } catch (Exception e) {
            logger.error("管理员认证错误,错误信息"+e.toString());
            return null;
        }
    }

    /**
     * 功能描述:
     * (调用Mapper接口获取用户角色信息)
     *
     * @param adminUser 1
     * @return : java.util.Set<java.lang.String>
     * @author : xiaozhu
     * @date : 2020/3/13 10:35
     */
    private Set<String> getRolesByUserId(String adminUser) {
        try {
            List<String> roles = rolesMapper.selectRoleName(adminUser);
            Set<String> sets = new HashSet<>(roles);
            return sets;
        } catch (Exception e) {
            logger.error("管理员数据库获取角色信息错误,错误信息:"+e.toString());
            return new HashSet<>();
        }
    }

    /**
     * 功能描述:
     * (调用Mapper的接口获取角色的权限信息)
     *
     * @param roles 查询角色list
     * @return : java.util.Set<java.lang.String>
     * @author : xiaozhu
     * @date : 2020/3/13 10:41
     */
    private Set<String> getPermissionUserId(Set<String> roles) {
        Set<String> sets = new HashSet<>();
        try {
            for (String role : roles){
                List<String> permission = permissionsMapper.selectPermission(role);
                for (String permis:permission) {
                    sets.add(permis);
                }
            }
            return sets;
        } catch (Exception e) {
            logger.error("管理员权限数据库获取错误,错误信息:"+e.toString());
            return sets;
        }
    }
}

realm/RealmYIBAN.java

package com.yiban.yblaas.shiro.realm;

import com.yiban.yblaas.mapper.PermissionsMapper;
import com.yiban.yblaas.mapper.RolesMapper;
import com.yiban.yblaas.shiro.authc.UserToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @program: yblaas
 * @description: 易班数据源的Realm
 * @author: xiaozhu
 * @create: 2020-03-13 20:15
 **/
@Component
public class RealmYIBAN extends AuthorizingRealm {

    private static final Logger logger = LoggerFactory.getLogger(RealmYIBAN.class);

    @Autowired
    private RolesMapper rolesMapper;
    @Autowired
    private PermissionsMapper permissionsMapper;

    /**
     * 功能描述:
     * (重写授权方法)
     *
     * @param principalCollection 1
     * @return : org.apache.shiro.authz.AuthorizationInfo
     * @author : xiaozhu
     * @date : 2020/3/12 22:16
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        try {
            //易班角色的id
            String yibanId = (String) principalCollection.getPrimaryPrincipal();
            //从缓存中拿到角色数据(没有设置缓存只能再查一次数据库)
            Set<String> roles = getRolesByUserId(yibanId);
            //从缓存中拿到权限数据(没有设置缓存只能再查一次数据库)
            Set<String> permissions = getPermissionUserId(roles);
            //返回对象
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            authorizationInfo.setRoles(roles);
            authorizationInfo.setStringPermissions(permissions);
            return authorizationInfo;
        } catch (Exception e) {
            logger.error("易班用户授权错误,错误信息:"+e.toString());
            return null;
        }
    }

    /**
     * 功能描述:
     * (重写认证方法)
     *
     * @param authenticationToken 1
     * @return : org.apache.shiro.authc.AuthenticationInfo
     * @author : xiaozhu
     * @date : 2020/3/12 22:17
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        try {
            //就是模拟下认证 因为无需认证 只需要授权
            UserToken token = (UserToken) authenticationToken;
            String userId = token.getUsername();
            char[] access_token = token.getPassword();
            if(userId == null || access_token == null){
                return null;
            }
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userId,access_token,"RealmYIBAN");
            return authenticationInfo;
        } catch (Exception e) {
            logger.error("易班用户认证错误,错误信息:"+e.toString());
            return null;
        }
    }

    /**
     * 功能描述:
     * (调用Mapper接口获取用户角色信息)
     *
     * @param yibanId 易班用户ID
     * @return : java.util.Set<java.lang.String>
     * @author : xiaozhu
     * @date : 2020/3/13 10:35
     */
    private Set<String> getRolesByUserId(String yibanId) {
        try {
            List<String> roles = rolesMapper.selectRoleName(yibanId);
            Set<String> sets = new HashSet<>(roles);
            return sets;
        } catch (Exception e) {
            logger.error("易班用户从数据库获取角色错误,错误信息:"+e.toString());
            return new HashSet<>();
        }
    }

    /**
     * 功能描述:
     * (调用Mapper的接口获取角色的权限信息)
     *
     * @param roles 角色信息
     * @return : java.util.Set<java.lang.String>
     * @author : xiaozhu
     * @date : 2020/3/13 10:41
     */
    private Set<String> getPermissionUserId(Set<String> roles) {
        try {
            Set<String> sets = new HashSet<>();
            for (String role : roles){
                List<String> permission = permissionsMapper.selectPermission(role);
                for (String permis:permission) {
                    sets.add(permis);
                }
            }
            return sets;
        } catch (Exception e) {
            logger.error("易班用户从数据库获取权限信息错误。错误信息:"+e.toString());
            return new HashSet<>();
        }
    }
}

5.指定用户对应的Realm

创建一个org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类,并重写doAuthenticate()方法,让特定的Realm完成特定的功能。
authc/pam/UserModularRealmAuthenticator.java

package com.yiban.yblaas.shiro.authc.pam;

import com.yiban.yblaas.shiro.VirtualType;
import com.yiban.yblaas.shiro.authc.UserToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;

/**
 * @program: yblaas
 * @description: 自定义shiro的多Realm时候处理的方法
 * @author: xiaozhu
 * @create: 2020-03-13 16:37
 **/
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
    private static final Logger logger = LoggerFactory.getLogger(UserModularRealmAuthenticator.class);

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken){
        try {
            // 判断getRealms()是否返回为空
            assertRealmsConfigured();
            // 强制转换回自定义的CustomizedToken
            UserToken userToken = (UserToken) authenticationToken;
            // 登录类型
            VirtualType virtualType = userToken.getVirtualType();
            // 所有Realm
            Collection<Realm> realms = getRealms();
            // 登录类型对应的所有Realm
            Collection<Realm> typeRealms = new ArrayList<>();
            for (Realm realm : realms) {
                if (realm.getName().contains(virtualType.toString())) {
                    typeRealms.add(realm);
                    // 注:这里使用类名包含枚举,区分realm
                }
            }
            // 判断是单Realm还是多Realm
            if (typeRealms.size() == 1) {
                return doSingleRealmAuthentication(typeRealms.iterator().next(), userToken);
            } else {
                return doMultiRealmAuthentication(typeRealms, userToken);
            }
        } catch (IllegalStateException e) {
            logger.error("自定义多Realm认证错误,错误信息:"+e.toString());
            return null;
        }
    }
}

6.自定义多授权注入

在角色查询权限和查询某角色是否拥有某个权限的时候指定其对应的Realm。新建org.apache.shiro.authz.ModularRealmAuthorizer子类,并重写isPermitted权限分配Realm和hasRole角色分配Realm的方法。
注意:这个的权限命名是有要求的,根据权限名称中是否拥有admin、teacher、student来区分采用哪一个Realm。
authc/UserModularRealmAuthorizer.java

package com.yiban.yblaas.shiro.authc;

import com.yiban.yblaas.shiro.VirtualType;
import com.yiban.yblaas.shiro.realm.RealmADMIN;
import com.yiban.yblaas.shiro.realm.RealmYIBAN;
import org.apache.shiro.authz.Authorizer;
import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Set;

/**
 * @program: yblaas
 * @description: 自定义多授权注入
 * @author: xiaozhu
 * @create: 2020-03-13 21:30
 **/
public class UserModularRealmAuthorizer extends ModularRealmAuthorizer {

    private static final Logger logger = LoggerFactory.getLogger(UserModularRealmAuthorizer.class);

    /**
     * 功能描述:
     * (自定义权限分配的Realm)
     *
     * @param principals 用户ID
     * @param permission 权限名称
     * @return : boolean
     * @author : xiaozhu
     * @date : 2020/4/17 18:54
     */
    @Override
    public boolean isPermitted(PrincipalCollection principals, String permission) {
        try {
            assertRealmsConfigured();
            for (Realm realm : getRealms()) {
                if (!(realm instanceof Authorizer)){ continue;}

                if (realm.getName().contains(VirtualType.ADMIN.toString())) {//类名判断
                    if (permission.contains("admin")){
                        //权限名称是否带admin
                        return ((RealmADMIN) realm).isPermitted(principals, permission);    // 使用改realm的授权方法
                    }
                }
                if (realm.getName().contains(VirtualType.YIBAN.toString())) {
                    //其他的都为易班的用户
                    if (permission.contains("teacher")||permission.contains("student")) {
                        return ((RealmYIBAN) realm).isPermitted(principals, permission);
                    }
                }
            }
            return false;
        } catch (IllegalStateException e) {
            logger.error("用户权限认证匹配错误,错误信息:"+e.toString());
            return false;
        }
    }


    /**
     * 功能描述:
     * (角色获取权限分配Realm)
     *
     * @param principals 用户ID
     * @param roleIdentifier 角色名称
     * @return : boolean
     * @author : xiaozhu
     * @date : 2020/4/17 18:55
     */
    @Override
    public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
        try {
            assertRealmsConfigured();
            for (Realm realm : getRealms()) {
                if (!(realm instanceof Authorizer)){ continue;}

                if (realm.getName().contains(VirtualType.ADMIN.toString())) {//类名判断
                    if(roleIdentifier.equals("admin")){
                        return ((RealmADMIN) realm).hasRole(principals, roleIdentifier);    // 使用改realm的授权方法
                    }
                }
                if (realm.getName().contains(VirtualType.YIBAN.toString())) {
                    if (roleIdentifier.equals("teacher") || roleIdentifier.equals("student")) {
                        return ((RealmYIBAN) realm).hasRole(principals, roleIdentifier);
                    }
                }
            }
            return false;
        } catch (IllegalStateException e) {
            logger.error("用户权限获取匹配Realm错误,错误信息:"+e.toString());
            return false;
        }
    }
}

7.配置核心安全事务管理器

在核心事务管理器中使用我们自定义的认证和权限分配。
filter/ShiroConfiguration.java

package com.yiban.yblaas.shiro.filter;

import com.yiban.yblaas.session.CustomSessionManager;
import com.yiban.yblaas.session.RedisSessionDao;
import com.yiban.yblaas.shiro.authc.UserModularRealmAuthorizer;
import com.yiban.yblaas.shiro.authc.pam.UserModularRealmAuthenticator;
import com.yiban.yblaas.shiro.cache.ShiroCacheManager;
import com.yiban.yblaas.shiro.realm.RealmADMIN;
import com.yiban.yblaas.shiro.realm.RealmYIBAN;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.realm.Realm;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @program: yblaas
 * @description: shiro过滤器
 * @author: xiaozhu
 * @create: 2020-03-13 21:10
 **/
@Configuration
public class ShiroConfiguration {

    @Resource
    private RealmADMIN realmADMIN;
    @Resource
    private RealmYIBAN realmYIBAN;
    @Resource
    private RedisSessionDao redisSessionDao;
    @Resource
    private ShiroCacheManager shiroCacheManager;

    @Bean(name="shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager manager) {
        ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
        bean.setSecurityManager(manager);
        //配置登录的url和登录成功的url以及验证失败的url
        //loginUrl:没有登录的用户请求需要登录的页面时自动跳转到登录页面。
        bean.setLoginUrl("/public/user_login");
        //unauthorizedUrl:没有权限默认跳转的页面,登录的用户访问了没有被授权的资源自动跳转到的页面。
        bean.setUnauthorizedUrl("/error/403.html");
        //配置自定义的Filter
        Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
        filtersMap.put("roles", new RolesOrFilter());
        bean.setFilters(filtersMap);
        //配置访问权限
        LinkedHashMap<String, String> filterChainDefinitionMap=new LinkedHashMap<>();
        filterChainDefinitionMap.put("/", "anon"); //首页
        filterChainDefinitionMap.put("/**","authc"); //需要登陆授权
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return bean;
    }

    //配置核心安全事务管理器
    @Bean(name="securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
        List<Realm> realms = new ArrayList<>();
        //添加多个Realm
        realms.add(realmADMIN);
        //admin需要加密
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); //创建加密对象
        matcher.setHashAlgorithmName("md5"); //加密的算法
        matcher.setHashIterations(1);//加密次数
        realmADMIN.setCredentialsMatcher(matcher); //放入自定义Realm
        realms.add(realmYIBAN);
        manager.setAuthenticator(modularRealmAuthenticator());  // 需要再realm定义之前 放入自定义的多Realm认证
        manager.setAuthorizer(modularRealmAuthorizer()); //放入自定义的多Realm授权
        manager.setRealms(realms);
        //配置自定义sessionManager
        manager.setSessionManager(sessionManager());
        //配置自定义cacheManager
        manager.setCacheManager(shiroCacheManager);
        return manager;
    }

    /**
     * 功能描述:
     * (系统自带的Realm管理,主要针对多realm 认证)
     *
     * @return : ModularRealmAuthenticator
     * @author : xiaozhu
     * @date : 2020/3/13 21:13
     */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator() {
        //自己重写的ModularRealmAuthenticator
        UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }

    /**
     * 功能描述:
     * (系统自带的Realm管理,主要针对多realm 授权)
     *
     * @return : org.apache.shiro.authz.ModularRealmAuthorizer
     * @author : xiaozhu
     * @date : 2020/3/13 22:03
     */
    @Bean
    public ModularRealmAuthorizer modularRealmAuthorizer() {
        //自己重写的ModularRealmAuthorizer
        UserModularRealmAuthorizer modularRealmAuthorizer = new UserModularRealmAuthorizer();
        return modularRealmAuthorizer;
    }

    /**
     * 功能描述:
     * (开启注解支持)
     *
     * @param securityManager 1
     * @return : AuthorizationAttributeSourceAdvisor
     * @author : xiaozhu
     * @date : 2020/3/13 22:06
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 功能描述:
     * (自定义session缓存)
     *
     * @return : com.yiban.yblaas.session.CustomSessionManager
     * @author : xiaozhu
     * @date : 2020/4/17 11:19
     */
    @Bean
    public CustomSessionManager sessionManager(){
        //把sessionManager注入Bean
        CustomSessionManager manager = new CustomSessionManager();
        manager.setSessionDAO(redisSessionDao);
        return manager;
    }
}

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇