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