第九章:Spring Security 使用redis存儲用戶權限信息

前面我們使用了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

----------------------------------------------------------

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章