一、关于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
新增注解,使用这个注解的接口将会判断登录权限
@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;
}
}