Shiro + redis 前后端管理用户权限

这几天一直在开发前后端权限管理,就想到刚好用用shiro吧,就在网上找了找,然后根据业务需求做了一下合并,如下。

1、ShiroRealm.java  主要用来验证用户和角色权限。


package shiro;


import utils.ApplicationContextRegister;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

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

/**
 * @Author:Jerry
 * @Created: 2018/7/23
 * @Modifier:Jerry
 * @Updated:2018/7/23
 * @Description: 自定义Realm,主要是用来验证用户权限和验证用户登录信息
 * @Version:BUILD1001
 */
public class MyShiroRealm extends AuthorizingRealm {
    /**
     * 验证用户权限的位置
     * 进入方法的场景:
     *  1、@RequiresRoles("admin") ,@RequiresPermissions("admin") 在方法上加注解的时候;
     *  2、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”) 自己去调用这个方法
     *  3、[@shiro.hasPermission name = "admin"][/@shiro.hasPermission]: 在页面加这个标签的时候,这个项目是前后端分离,所以这个方法无效
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        ManageInfoVo manageInfoVo = (ManageInfoVo)principals.getPrimaryPrincipal();
        //给予admin超级权限
        if(manageInfoVo.getUserName().equals("admin")){
            SysMenuDoMapperExt sysMenuDoMapperExt =  ApplicationContextRegister.getBean(SysMenuDoMapperExt.class);
            List<SysMenuVo> listSysMenuVo = sysMenuDoMapperExt.getList(new SysMenuDto());
            Set<String> permsSet = new HashSet<>();
            for(SysMenuVo sysMenuVo : listSysMenuVo){
                if(!StringUtil.isEmpty(sysMenuVo.getPerms())){
                    permsSet.add(sysMenuVo.getPerms());
                }
            }
            authorizationInfo.addStringPermissions(permsSet);
        }else{
            SysMenuService sysMenuService = ApplicationContextRegister.getBean(SysMenuService.class);
            List<SysMenuDo > listSysMenuDo = sysMenuService.getListByUserName(manageInfoVo.getUserName());
            Set<String> permsSet = new HashSet<>();
            for(SysMenuDo sysMenuDo : listSysMenuDo){
                if(!StringUtil.isEmpty(sysMenuDo.getPerms())){
                    permsSet.add(sysMenuDo.getPerms());
                }
            }
            authorizationInfo.addStringPermissions(permsSet);
        }
        return authorizationInfo;
    }

    /**
     * 验证用户名和密码是否正确
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        //获取用户的输入的账号.
        String username = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());
        try{
            ManageInfoDoMapperExt manageInfoDoMapperExt = ApplicationContextRegister.getBean(ManageInfoDoMapperExt.class);
            ManageInfoDto manageInfoDto = new ManageInfoDto();
            manageInfoDto.setUserName(username);
            manageInfoDto.setPassword(password);
            ManageInfoVo manageInfoVo = manageInfoDoMapperExt.login(manageInfoDto);
            if(null == manageInfoVo || null == manageInfoVo.getId()){
                throw new ValidateException("用户名或密码错误");
            }
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    manageInfoVo, //用户信息
                    password, //密码
                    ByteSource.Util.bytes(manageInfoVo.getUserName()),//salt 加密,未用到,这里就不做介绍了。
                    getName() //realm name
            );
            return authenticationInfo;
        }catch (Exception e){
            throw new ValidateException("用户名或密码错误");
        }
    }

}
​

说下上面用到的工具类:

ApplicationCOntextRegister.java


package utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
 * @Author:jerry
 * @Created: 2018/7/30
 * @Modifier:jerry
 * @Updated:2018/7/30
 * @Description:  定义便捷类用于shiro中获取service
 * @Version:BUILD1001
 */
@Component
public class ApplicationContextRegister implements ApplicationContextAware {
    private static Logger logger = LoggerFactory.getLogger(ApplicationContextRegister.class);
    private static ApplicationContext APPLICATION_CONTEXT;

    /**
     * 设置spring上下文
     *
     * @param applicationContext spring上下文
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        logger.debug("ApplicationContext registed-->{}", applicationContext);
        APPLICATION_CONTEXT = applicationContext;
    }

    /**
     * 获取容器
     *
     * @return
     */
    public static ApplicationContext getApplicationContext() {
        return APPLICATION_CONTEXT;
    }

    /**
     * 获取容器对象
     *
     * @param type
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> type) {
        return APPLICATION_CONTEXT.getBean(type);
    }
}

可以参考链接:https://www.cnblogs.com/mrx520/p/7802831.html  

2、ShiroConfig.java 主要是关于shiro的配置,如:cacheManager,sessionManager,sessionListerner等。核心配置都在这里。


package shiro;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Author:jieli.xu
 * @Created: 2018/7/23
 * @Modifier:jieli.xu
 * @Updated:2018/7/23
 * @Description: shiro 配置
 * @Version:BUILD1001
 */
@Configuration
public class ShiroConfig {
    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int    port;

    @Value("${spring.redis.timeout}")
    private int    timeout;

    @Value("${spring.redis.pool.max-idle}")
    private int    minIdle;

    @Value("${spring.redis.pool.max-idle}")
    private int    maxIdle;

    @Value("${spring.redis.pool.max-wait}")
    private long   maxWaitMillis;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.database}")
    private int    database;

    @Bean
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


    @Bean
    ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        //注意过滤器配置顺序 不能颠倒
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
        filterChainDefinitionMap.put("/loginout", "logout");
        //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
        shiroFilterFactoryBean.setLoginUrl("/user/login");
        // 登录成功后要跳转的链接
//        shiroFilterFactoryBean.setSuccessUrl("/user/login");
        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;

    }


    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置realm.
        securityManager.setRealm(myShiroRealm());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    @Bean
    MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        return myShiroRealm;
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 配置shiro redisManager
     *
     * @return
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setExpire(1800);// 配置缓存过期时间
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        redisManager.setDatabase(database);
        redisManager.setTimeout(timeout);
        return redisManager;
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisCacheManagerOverride cacheManager() {
        RedisCacheManager redisCacheManager1 = new RedisCacheManager();
        redisCacheManager1.setRedisManager(redisManager());
        RedisCacheManagerOverride redisCacheManager = new RedisCacheManagerOverride();
        return redisCacheManager;
    }


    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    @Bean
    public SessionDAO sessionDAO() {
        return redisSessionDAO();
    }

   /**
     * shiro session的管理
     *//**
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(1800 * 1000);
        sessionManager.setSessionDAO(sessionDAO());
        Collection<SessionListener> listeners = new ArrayList<SessionListener>();
        listeners.add(new BDSessionListener());
        sessionManager.setSessionListeners(listeners);
        return sessionManager;
    }
     */
    //自定义sessionManager

    @Bean
    public SessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setGlobalSessionTimeout(1800 * 1000);//过期时间:
        Collection<SessionListener> listeners = new ArrayList<SessionListener>();
        listeners.add(new BDSessionListener());
        mySessionManager.setSessionListeners(listeners);
        mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    }

}

BDSession.java 监听Session 

package shiro;

import java.util.concurrent.atomic.AtomicInteger;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;

/**
 * 用来监听Session,过期或停止后可以进行一些操作。
 * 这里只做参考,具体操作根据业务需求
 */
public class BDSessionListener implements SessionListener {

	private final AtomicInteger sessionCount = new AtomicInteger(0);

	@Override
	public void onStart(Session session) {
		sessionCount.incrementAndGet();
	}

	@Override
	public void onStop(Session session) {
		sessionCount.decrementAndGet();
	}

	@Override
	public void onExpiration(Session session) {
		sessionCount.decrementAndGet();

	}

	public int getSessionCount() {
		return sessionCount.get();
	}

}

MySessionManage.java 因为是前后端分离,所以需要他们穿token过来。头部请求把token带上


package shiro;


import com.alibaba.druid.util.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * @Author:jieli.xu
 * @Created: 2018/7/23
 * @Modifier:jieli.xu
 * @Updated:2018/7/23
 * @Description:  重新获取session的方法
 * @Version:BUILD1001
 */
public class MySessionManager extends DefaultWebSessionManager {
    private static final String AUTHORIZATION = "token";

    public MySessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        if(StringUtils.isEmpty(sessionId)){
            sessionId = super.getSessionId(request, response)+"";
        }
        return sessionId;
    }

}

RedisCache  redis缓存,自定义缓存存储数据信息,也是因为redis-cache会把我的信息覆盖掉。

里面redisClient 是redis管理工具,具体操作你们可以根据自己的redisClient 进行修改。我这里不做详细描述


package com.bxm.depthtaskcms.shiro;

import com.bxm.depthtaskcms.utils.ApplicationContextRegister;
import com.mchange.v2.ser.SerializableUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import java.io.NotSerializableException;
import java.util.*;
/**
 * @Author:jerry
 * @Created: 
 * @Modifier:jerry
 * @Updated:
 * @Description: 重写redis缓存,方便管理
 * @Version:BUILD1001
 */
public class RedisCache<K,V>  implements Cache<K,V> {
    @Autowired
    private RedisClient redisClient;
    private Logger logger = LoggerFactory.getLogger(RedisCache.class);
    private String keyPrefix = "shiro_redis_cache:";
    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }


    /**
     * 获得组装后的byte[] key
     * @param key
     * @return
     */
    private byte[] getByteKey(K key)  throws  NotSerializableException {
        if(key instanceof String){
            String preKey = this.keyPrefix + key;
            return preKey.getBytes();
        }else{
            if(key instanceof SimplePrincipalCollection){
                SimplePrincipalCollection spc = (SimplePrincipalCollection)key;
                Object object = spc.getPrimaryPrincipal();
                if(object instanceof  ManageInfoVo){
                    ManageInfoVo manageInfoVo = (ManageInfoVo)object;
                    String preKey = this.keyPrefix+(manageInfoVo.getId());
                    return preKey.getBytes();
                }
            }
            if(key instanceof  ManageInfoVo){
                String preKey = this.keyPrefix+((ManageInfoVo) key).getId();
                return preKey.getBytes();
            }
            return SerializableUtils.toByteArray(key);
        }
    }

    @Override
    public V get(K key) throws CacheException {
       try{
           redisClient = ApplicationContextRegister.getBean(RedisClient.class);
           byte[] groupKey = getByteKey(key);
           byte[] value = redisClient.get(Constant.REDIS_DEFAULT_DB,groupKey);
           if(value == null){
               return null;
           }
           return (V)SerializableUtils.fromByteArray(value);
       }catch (Exception e){
           logger.error(e.getMessage());
       }
       return null;
    }

    @Override
    public V put(K key, V value) throws CacheException {
        try{
            redisClient = ApplicationContextRegister.getBean(RedisClient.class);
            redisClient.setex(Constant.REDIS_DEFAULT_DB,getByteKey(key),Constant.SHIRO_REDIS_CACHE_TIME ,SerializableUtils.toByteArray(value));
            byte[] bytes = redisClient.get(Constant.REDIS_DEFAULT_DB,getByteKey(key));
            return (V)SerializableUtils.fromByteArray(bytes);
        }catch (Exception e){
            logger.error("反序列化失败:"+e.getMessage());
        }
        return null;
    }

    @Override
    public V remove(K key) throws CacheException {
        try{
            redisClient = ApplicationContextRegister.getBean(RedisClient.class);
            byte[] bytes = redisClient.get(Constant.REDIS_DEFAULT_DB,getByteKey(key));
            Set<K> hashSet = keys();
            for(K keyEntity : hashSet){
                redisClient.del(Constant.REDIS_DEFAULT_DB,((byte[])keyEntity));
            }
          //  redisClient.del(Constant.REDIS_DEFAULT_DB,getByteKey(key));
            return null;
        }catch (Exception e){
            logger.error("删除异常"+e.getMessage());
        }
        return null;
    }

    @Override
    public void clear() throws CacheException {

    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Set<K> keys() {
       try{
           redisClient = ApplicationContextRegister.getBean(RedisClient.class);
           K key = (K)"*";
           Set<K> setKey = new HashSet<>();
           setKey = (Set<K>)redisClient.hkeys(Constant.REDIS_DEFAULT_DB,getByteKey(key));
           return  setKey;
       }catch (Exception e){
           logger.error("获取key失败");
       }
       return null;
    }

    @Override
    public Collection<V> values() {
        return null;
    }
}

RedisCacheManager.java 管理redisCache


package shiro;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.context.annotation.Bean;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * @Author:
 * @Modifier:jerry
 * @Description:
 * @Version:BUILD1001
 */

public class RedisCacheManagerOverride  implements CacheManager {

    private final ConcurrentMap<String, RedisCache> caches = new ConcurrentHashMap();
    //
    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        RedisCache cache = (RedisCache)this.caches.get(name);
        if (cache == null) {
            cache = new RedisCache<K,V>();
            this.caches.put(name, cache);
        }

        return (Cache)cache;
    }
}

在定义一个ShiroUtil,用来便捷获取登录人员的信息

package shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.mgt.RealmSecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;

import org.springframework.beans.factory.annotation.Autowired;
import sun.security.util.Cache;

import java.security.Principal;
import java.util.Collection;
import java.util.List;

public class ShiroUtils {

    public static Subject getSubjct() {
        return SecurityUtils.getSubject();
    }

    /**
     * 获取当前用户信息
     * @return
     */
    public static ManageInfoVo getUser() {
        try{
            Object object = getSubjct().getPrincipal();
            if(null == object)
                throw new UnauthenticatedException();
            return (ManageInfoVo)object;
        }catch (Exception e){
            throw new UnauthenticatedException("");
        }
    }

    /**
     * 获取用户id
     * @return
     */
    public static Long getUserId() {
        return getUser().getId();
    }

    /**
     * 退出登录
     */
    public static void logout() {
        getSubjct().logout();
    }

    /**
     * 刷新用户角色权限
     */
    public static void cleanCash(){
        RealmSecurityManager rsm = (RealmSecurityManager) SecurityUtils.getSecurityManager();
        //MyShiroRealm为在项目中定义的realm类
        MyShiroRealm shiroRealm = (MyShiroRealm)rsm.getRealms().iterator().next();
        Subject subject = SecurityUtils.getSubject();
//        String realmName = subject.getPrincipals().getRealmNames().iterator().next();
//        SimplePrincipalCollection principals = new SimplePrincipalCollection(subject.getPrincipals(),realmName);
//        subject.runAs(principals);
        //用realm删除principle
        shiroRealm.getAuthorizationCache().remove(subject.getPrincipals());
    }

}

定义全局异常,当用户没有权限或者登录失败的时候,自动给前段返回信息。

参考:spring boot 全局异常处理 

    @ExceptionHandler(value=UnauthorizedException.class)
    @ResponseBody
    public ResultModel<String> unauthorizedHandler( UnauthorizedException exception) throws Exception{
        ResultModel<String> result = new ResultModel<>();
        result.setSuccessed(false);
        result.setErrorCode(ErrorCode.SERVER_ERROR.getErrorCode());
        result.setErrorDesc("用户没有权限");
        return result;
    }
    @ExceptionHandler(value=UnauthenticatedException.class)
    @ResponseBody
    public ResultModel<String> unauthenticatedHandler( UnauthenticatedException exception) throws Exception{
        ResultModel<String> result = new ResultModel<>();
        result.setSuccessed(false);
        result.setErrorCode(ErrorCode.ILLEGAL_USER.getErrorCode());
        result.setErrorDesc(ErrorCode.ILLEGAL_USER.getErrorMessage());
        return result;
    }

最后附上登录:

 ///登录开始

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try{
            subject.login(token);
        }catch (Exception e){
            throw new ValidateException("用户名或密码错误");
        }
        if(subject.isAuthenticated()){
            ManageInfoVo manageInfoVo = (ManageInfoVo) subject.getPrincipal();
            Subject subjectToken = SecurityUtils.getSubject();
            manageInfoVo.setToken((String)subjectToken.getSession().getId());
            return manageInfoVo;
        }else {
            throw new ValidateException("用户名或密码错误,登录失败。");
        }

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值