前面我们使用了 jwt 的 token 来进行登录,但是只说明了它的好处,那么我们来讲一讲他不好的地方:消息体可以被 base64 解密为明文、不适合存放大量信息、无法作废未过期的 token。显然我们准备要存储的东西非常多,用户信息 + 权限信息。所以我们考虑换 redis 来进行存储,抛弃 jwt。

集成

<!-- redis连接 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

reidsDao

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;
import org.springframework.util.CollectionUtils;
 
import java.util.concurrent.TimeUnit;
 
/**
 * redis数据库工具类
 */
@Repository
@Slf4j
@RequiredArgsConstructor
public class RedisDao<K,V> {
 
    /**
     * 过期时间是3600秒,既是1个小时
     */
    private static final long EXPIRATION = 3600L;
 
 
    private final RedisTemplate<K, V> redisTemplate;
 
    /**
     * 设置key值
     * @param key
     * @param value
     */
    public void setKey(K key,V value){
        ValueOperations<K, V> ops = redisTemplate.opsForValue();
        ops.set(key,value);
    }
 
    /**
     * 设置key值
     * @param key
     * @param value
     * @param expiration 过期时间(秒)
     */
    public void setKey(K key,V value,Long expiration){
        if(expiration == null){
            expiration = EXPIRATION;
        }
        ValueOperations<K, V> ops = redisTemplate.opsForValue();
        ops.set(key,value,expiration,TimeUnit.SECONDS);
    }
 
    /**
     * 获得Key值
     * @param key
     * @return
     */
    public V getValue(K key){
        ValueOperations<K, V> ops = this.redisTemplate.opsForValue();
        return ops.get(key);
    }
 
    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public void expire(K key,Long time){
        if(time == null){
            time = EXPIRATION;
        }
        redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }
 
    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(K key){
        return redisTemplate.getExpire(key,TimeUnit.SECONDS);
    }
 
    /**
     * 根据key 和时间单位获取过期时间,单位{@link TimeUnit}
     * @param key 键 不能为null
     * @param timeUnit 时间单位 {@link TimeUnit}
     * @return 返回0代表为永久有效
     */
    public long getExpire(K key,TimeUnit timeUnit){
        return redisTemplate.getExpire(key,timeUnit);
    }
 
    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    public void del(K ... key){
        if(key!=null&&key.length>0){
            if(key.length==1){
                redisTemplate.delete(key[0]);
            }else{
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }
}

redisConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
/**
 * redis配置
 */
@Configuration
public class RedisConfig {
 
    /**
     * 设置key跟value的序列化方式
     * @param factory
     * @return
     */
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String,Object> template = new RedisTemplate<String,Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        // template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        // template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

登录调整

private final RedisDao<String,CustomUserDetails> userRedisDao;
 
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 随机字符串(用于区分同一账号多次登录时,缓存的key不重复)
    String uuidKey = UUID.randomUUID().toString();
    String userTokenKey = BaseConstant.USER_KEY+username + ":" + uuidKey;
    CustomUserDetails details = userService.getUserByUsername(username);
    if(details == null){
        String errorMsg = "账号 " + username + "不存在";
        log.error(errorMsg);
        throw new UsernameNotFoundException(errorMsg);
    }
    details.setUuidKey(uuidKey);
    // 设置权限
    Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
    List<SysRoleModel> roleList = roleService.getRoleCodeByUserId(details.getUserId());
    if(roleList == null || roleList.size() <= 0){
        details.setAuthorities(grantedAuthorities);
        userRedisDao.setKey(userTokenKey,details,null);
        return details;
    }
 
    // 角色
    List<String> roleStrs = Lists.newArrayList();
    for(SysRoleModel role : roleList){
        grantedAuthorities.add(new SimpleGrantedAuthority(role.getRoleCode()));
        roleStrs.add(role.getRoleId());
    }
 
    // 菜单
    List<String> codeArr = menuService.getMenuByRoles(roleStrs);
    if(codeArr != null && codeArr.size() > 0){
        for (String code: codeArr) {
            grantedAuthorities.add(new SimpleGrantedAuthority(code));
        }
    }
 
    details.setAuthorities(grantedAuthorities);
    userRedisDao.setKey(userTokenKey,details,null);
    return details;
}

过滤器调整

/**
 * description: 登录验证成功后调用,验证成功后将生成Token,并重定向到用户主页home
 * 与AuthenticationSuccessHandler作用相同
 *
 * @param request
 * @param response
 * @param chain
 * @param authResult
 * @return void
 */
@Override
protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response,
                                        FilterChain chain,
                                        Authentication authResult) throws IOException, ServletException {
 
    // 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象,这里是CustomUserDetails
    CustomUserDetails user = (CustomUserDetails) authResult.getPrincipal();
    String userKey = BaseConstant.USER_KEY + user.getUsername() + ":" + user.getUuidKey();
    // 多生成一个KEY,用于返回给前端,确保用户信息不暴露出去
    String tokenKey = UUID.randomUUID().toString() + ":" + UUID.randomUUID().toString();
    // 存储后,可以通过tokenKey,拿到userKey,在通过userKey拿到用户的信息
    tokenRedis.setKey(BaseConstant.TOKEN_KEY + tokenKey,userKey,null);
 
    // 登录成功
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=utf-8");
    ActionResult<String> result = new ActionResult<>(ResultCodeEnum.SUCCESS);
    result.setMessage("登录成功");
    result.setData(tokenKey);
    response.getWriter().write(JSON.toJSONString(result));
}
import com.hzw.code.common.constant.BaseConstant;
import com.hzw.code.common.utils.ActionException;
import com.hzw.code.redis.dao.RedisDao;
import com.hzw.code.security.model.CustomUserDetails;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
 
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
/**
 * @author: 胡汉三
 * @date: 2020/5/26 10:20
 * @description: 对所有请求进行过滤
 * BasicAuthenticationFilter继承于OncePerRequestFilter==》确保在一次请求只通过一次filter,而不需要重复执行。
 */
public class PreAuthFilter extends BasicAuthenticationFilter {
    private RedisDao<String, CustomUserDetails> userRedis;
    private RedisDao<String,String> tokenRedis;
    public PreAuthFilter(AuthenticationManager authenticationManager,
                         RedisDao<String, CustomUserDetails> userRedis,
                         RedisDao<String,String> tokenRedis) {
        super(authenticationManager);
        this.userRedis = userRedis;
        this.tokenRedis = tokenRedis;
    }
 
    /**
     * description: 从request的header部分读取Token
     *
     * @param request
     * @param response
     * @param chain
     * @return void
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        String tokenHeader = request.getHeader(BaseConstant.TOKEN_HEADER);
        // 如果请求头中没有Authorization信息则直接放行了
        if (tokenHeader == null || !tokenHeader.startsWith(BaseConstant.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }
 
    /**
     * description: 读取Token信息,创建UsernamePasswordAuthenticationToken对象
     *
     * @param tokenHeader
     * @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        //解析Token时将“Bearer ”前缀去掉
        String token = tokenHeader.replace(BaseConstant.TOKEN_PREFIX, "");
        String userKey = tokenRedis.getValue(BaseConstant.TOKEN_KEY + token);
        if(StringUtils.isBlank(userKey)){
            throw new ActionException("token无效");
        }
        CustomUserDetails userDetails = userRedis.getValue(userKey);
        if (userDetails != null){
            // 刷新token
            tokenRedis.expire(BaseConstant.TOKEN_KEY + token,null);
            tokenRedis.expire(userKey,null);
            return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), null, userDetails.getAuthorities());
        }
        return null;
    }
}

测试

登录

接口验证

这里我们在登录的时候,就把用户的信息存储到了 redis,并且使用用户名 + UUID(userKey)的形式来做 key 保证同一个用户登录多个客户端的时候不会影响到。然后在登录成功后的过滤器中套上一个外层的 key(tokenKey),用来确保我们不会把用户的关键信息暴露出来。这个 key 存储了用户的信息的 key。这样我们就可以在用户请求的时候在过滤器中取出 token 来判断他是否登录过,以及 token 是否还有效。

在判断 token 有效的同时,在刷新一下 token 的存活时间。

这样,我们的 token 跟用户信息都存储到 redis 里面去了!

在选择序列化类型的时候,我们只是针对 key 做的处理,没有对 value 进行处理。所以我们看到 key 没有 \ xac\xed\x00 这样的字符串,而 value 都是这样的字符串。

下一步我们要开始做前端的内容了。因为后端到这里,基本的框架已经弄得差不多了!我们接下来开始使用 vue + element ui 来做我们的前端架构。


项目的源码地址:https://gitee.com/gzsjd/fast