Springcloud+token+redis

一、关于Token
token是访问资源的凭据,用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是 这样的:
1.客户端使用用户名跟密码请求登录
2.服务端收到请求,去验证用户名与密码
3.验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
4.客户端收到 Token 以后可以把它存储起来,比如放在localStorage中
5.客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
6.服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就放行

二、为什么要用redis

目前前端可能会调用三个项目的服务端,后端也会使用springcloud进行项目间的服务调用;
而这三个项目的服务端登录账户不同,需要通过redis储存token使前端通过token验证。

三、redis配置过程

POM.xml配置为:

org.springframework.boot
spring-boot-starter-data-redis

创建redis配置类:RedisConfig,继承CachingConfigurerSupport
spring-boot 2.0之前的redis配置为:

/**
* 管理缓存
*/
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
return rcm;
}

spring-boot 2.0之后的配置为:

 @Bean public CacheManager cacheManager(RedisConnectionFactory factory)
 {
     RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); //
 生成一个默认配置,通过config对象即可对缓存进行自定义配置
     config = config.entryTtl(Duration.ofHours(10)) // 设置缓存的默认过期时间,也是使用Duration设置
     .disableCachingNullValues()// 不缓存空值
     .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new
 GenericJackson2JsonRedisSerializer()));
     return RedisCacheManager.builder(factory).cacheDefaults(config).build(); }

由于版本不通,配置也不太相同,此处只说2.0之后的。cacheManage入参为Redis连接工厂类,可在此处进行一些缓存连接的设置,然后建立连接;

然后进行redisTemplate的配置

/**
* RedisTemplate配置
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
    StringRedisTemplate template = new StringRedisTemplate(factory);
    // 设置序列化类,否则会多了一些乱码
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);
    template.setValueSerializer(jackson2JsonRedisSerializer);
    template.afterPropertiesSet();
    return template;
}

此处的配置是为了防止redis的key 和value有乱码

添加RedisUtil管理类

@Component
public class RedisUtil {

@Resource
private RedisTemplate redisTemplate;


public void set(String key, String value) {
    ValueOperations valueOperations = redisTemplate.opsForValue();
    valueOperations.set(key, value);
}

public String get(String key) {
    ValueOperations valueOperations = redisTemplate.opsForValue();
    return valueOperations.get(key);
}
}

设置yml

spring:
 redis:
  host: 127.0.0.1
  port: 6379

四、Token配置

pom配置


io.jsonwebtoken
jjwt
0.9.0

tokenUtil类

public class TokenUtil {

/**
* 签名秘钥
*/
public static final String SECRET = "tokenTest";

/**
* 发布者
*/
public static final String issuer = "tokenTest";

/**
* 过期时间(存放在redis中需要跟redis的过期时间一致)
*/
public static long ttlMillis = 3600000*4;
// public static long ttlMillis = 3000;
/**
* 生成token
*
* @param id
* @return
*/
public static String createJwtToken(Integer id) {
return createJwtToken(id, issuer, ttlMillis);
}

/**
* 生成Token
*
* @param id 编号
* @param issuer 该JWT的签发者,是否使用是可选的
* @param ttlMillis 签发时间 (有效时间,过期会报错)
* @return token String
*/
public static String createJwtToken(Integer id, String issuer, long ttlMillis) {

// 签名算法 ,将对token进行签名
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

// 生成签发时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);

// 通过秘钥签名JWT
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

// Let's set the JWT Claims
JwtBuilder builder = Jwts.builder().setId(id.toString())
.setIssuedAt(now)
.setIssuer(issuer)
.signWith(signatureAlgorithm, signingKey);

// if it has been specified, let's add the expiration
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}

// Builds the JWT and serializes it to a compact, URL-safe string
return builder.compact();

}

// Sample method to validate and read the JWT
public static Claims parseJWT(String jwt) {
// This line will throw an exception if it is not a signed JWS (as expected)
Claims claims;
try{
    claims = Jwts.parser()
    .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
    .parseClaimsJws(jwt).getBody();
}catch (Exception e){
    throw new TokenException("token解析错误");
}
    return claims;
}

public static void main(String[] args) {
    System.out.println(TokenUtil.createJwtToken(1234));
}
}

token错误异常

public class TokenException extends RuntimeException {

public TokenException(String msg) {
super(msg);
}

}

在异常管理类中添加token错误异常管理,返回401即为token错误,前端将会返回到登录页面

@ResponseStatus(value = HttpStatus.OK)
@ExceptionHandler(TokenException.class)
@ResponseBody
public Result handleTokenException(TokenException e) {
String errorMsg="TokenException:";
if (e!=null){
errorMsg=e.getLocalizedMessage();
log.warn(e.getLocalizedMessage());
}
return new ResultUtil<>().setErrorMsg(401,errorMsg);
}
 
  

新增注解,使用这个注解的接口将会判断登录权限

@Target({ElementType.METHOD})// 可用在方法名上
@Retention(RetentionPolicy.RUNTIME)// 运行时有效
public @interface LoginRequired {

}

五、添加拦截器进行整合

继承HandlerInterceptor类后可进行自定义拦截器,通过注解进行拦截

public class AuthenticationInterceptor implements HandlerInterceptor {

    @Resource
    private RedisUtil redisUtil;
    
    // 在业务处理器处理请求之前被调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    System.out.println("进入拦截器");
    // 如果不是映射到方法直接通过
    if (!(handler instanceof HandlerMethod)) {
    return true;
    }
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    Method method = handlerMethod.getMethod();
    // 判断接口是否需要登录
    LoginRequired methodAnnotation = method.getAnnotation(LoginRequired.class);
    // 有 @LoginRequired 注解,需要认证
    if (methodAnnotation != null) {
    // 判断是否存在令牌信息,如果存在,则允许登录
    String accessToken = request.getHeader("Authorization");
    System.out.println(accessToken);
    
    if (null == accessToken) {
    throw new TokenException( "无token,请重新登录");
    } else {
    
    // 从Redis 中查看 token 是否过期
    Claims claims;
    
    try{
    claims = TokenUtil.parseJWT(accessToken);
    }catch (ExpiredJwtException e){
    throw new TokenException("token失效,请重新登录");
    }catch (SignatureException se){
    throw new TokenException("token令牌错误");
    }
    
    String userId = claims.getId();
    System.out.println(userId);
    if(!userId.equals(redisUtil.get(accessToken))){
    throw new TokenException("用户不存在,请重新登录");
    }
    return true;
    }
    } else {//不需要登录可请求
    return true;
    }
    }

向spring中注入拦截器

@Configuration
public class WebMvcConfigurer extends WebMvcConfigurationSupport {

@Override
public void addInterceptors(InterceptorRegistry registry) {
System.out.println("注入拦截器");
// addPathPatterns 用于添加拦截规则
// excludePathPatterns 用户排除拦截
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
super.addInterceptors(registry);
}

@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}

六、登录时通过tokenUtil.createJwtToken方法来获取token并放入redis

String token=TokenUtil.createJwtToken(user.getUserId());
redisUtil.set(token,user.getUserId().toString());
agentUser.setToken(token);

后来每次请求就能判断token了

七、Eureka微服务之间传递token

由于微服务之间的调用没有header,所以需要使用一个新的拦截器来传递header

首先在yml中加入配置:
注意事项!:使用feign时千万不能把该配置放在feign的hystrix下面!否则获取header的时候会报错!别问我怎么知道的!
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE #加上这个就可以获取到HttpServletRequest
thread:
timeoutInMilliseconds: 10000

然后新建一个拦截器,实现RequestInterceptor,以进行header的传递

@Configuration
public class FeginInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate requestTemplate) {
try {
Map headers = getHeaders();
for(String headerName : headers.keySet()){
requestTemplate.header(headerName, headers.get(headerName));
}
}catch (Exception e){
e.printStackTrace();
}
}

private Map getHeaders(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Map map = new LinkedHashMap<>();
Enumeration enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return map;
}


}

你可能感兴趣的:(Springcloud+token+redis)