springboot2.x+redis+token实现鉴权
流程分析:
1.客户端登录,输入用户名和密码,后台进行验证,如果验证失败则返回登录失败的提示。如果验证成功,则生成token然后将username和token双向绑定(可以根据username取出token也可以根据token取出username)存入redis,同时使用token+username作为key把当前时间戳也存入redis。并且给它们都设置过期时间。
2.每次请求都会走拦截器,如果该接口标注了@AuthToken注解,则要检查http header中的Authorization字段,获取token,然后由于token与username双向绑定,我们可以通过获取的token来尝试从redis中获取username,如果可以获取则说明token正确,反之,说明错误,返回鉴权失败。
3.token可以根据用户使用的情况来动态的调整自己过期时间。我们在生成token的同时也往redis里面存入了创建token时的时间戳,每次请求被拦截器拦截token 验证成功之后,将当前时间与存在redis里面的token生成时刻的时间戳进行比较,当当前时间的距离创建时间快要到达设置的redis过期时间的话,就重新设置token过期时间,将过期时间延长。如果用户在设置的redis过期时间的时间长度内没有进行任何操作(没有发请求),则token会在redis中过期。
这里不用jwt生成token,token由MD5算法加密username+password+时间戳得到
一、依赖(因为没有用JWT所以没有token的依赖)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--redis缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
二、目录结构
三、自定义注解@AuthToken
package com.jinv.studentinfo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义@AuthToken
* @author jinv
*/
@Target({ElementType.METHOD , ElementType.TYPE})//Target说明了Annotation所修饰的对象范围:方法、接口
@Retention(RetentionPolicy.RUNTIME)//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
public @interface AuthToken {
}
四、token构造
1.TokenGenerator接口(可以用别的实现类实现多种算法生成token)
package com.jinv.studentinfo.token;
import org.springframework.stereotype.Component;
/**
* token生成器接口
*/
@Component
public interface TokenGenerator {
public String generate(String ... strings);
}
2.Md5TokenGenerator(使用MD5算法根据username+password+时间戳生成token令牌)
package com.jinv.studentinfo.token;
import com.jinv.studentinfo.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import javax.servlet.http.HttpServletRequest;
/**
* token生成器
* @author jinv
*/
@Component
public class Md5TokenGenerator implements TokenGenerator {
@Autowired
RedisUtils redisUtils;
/**
* token生成器
* @param strings 参数可以多个
* @return token token->(s1+s2...+时间戳)MD5加密
*/
@Override
public String generate(String... strings) {
long timeStamp = System.currentTimeMillis();//获取当前时间戳
StringBuilder tokenMeta = new StringBuilder();
for (String s: strings){//整合所有字符串参数为一个
tokenMeta.append(s);
}
tokenMeta.append(timeStamp);//加入时间戳
String token = DigestUtils.md5DigestAsHex(tokenMeta.toString().getBytes());//MD5加密
return token;
}
/**
* token存入redis缓存
* @param username 用户名
* @param password 密码
* @return token(username+password+时间戳)MD5加密
*/
public String tokenUtils(String username, String password){
//设置token
String token = generate(username,password);
redisUtils.set(username,token, ConstantKit.TOKEN_EXPIRE_TIME);//五分钟失效缓存
redisUtils.set(token,username,ConstantKit.TOKEN_EXPIRE_TIME);
Long currentTime = System.currentTimeMillis();//获取当前时间戳
redisUtils.set(username+token,currentTime);//设置第一次登陆的时间为token的初始时间
return token;
}
}
3.ConstantKit(token有关的全局变量)
package com.jinv.studentinfo.token;
/**
* token有关全局变量
*/
public final class ConstantKit {
/**
* 设置删除标志为真
*/
public static final Integer DEL_FLAG_TRUE=1;
/**
* 设置删除标志为假
*/
public static final Integer DEL_FLAG_FALSE=0;
/**
* redis存储token设置的过期时间
* 单位:秒(1h)
*/
public static final Integer TOKEN_EXPIRE_TIME=60*60;
/**
* 设置可以重置token过期时间的时间界限
* 单位:毫秒(30min)
*/
public static final Integer TOKEN_RESET_TIME=1000*60*30;
}
4、redisUtils工具类
package com.jinv.studentinfo.utils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
/**
* Redis工具类
* 操作redis数据库
* 底层封装了RedisTemplate<String,Object>
*/
@Component
public class RedisUtils {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key,long time){
try {
if(time>0){
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key){
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key){
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String ... key){
if(key!=null&&key.length>0){
if(key.length==1){
redisTemplate.delete(key[0]);
}else{
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key){
return key==null?null:redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key,Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key,Object value,long time){
try {
if(time>0){
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}else{
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param by 要增加几(大于0)
* @return
*/
public long incr(String key, long delta){
if(delta<0){
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param by 要减少几(小于0)
* @return
*/
public long decr(String key, long delta){
if(delta<0){
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key,String item){
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object,Object> hmget(String key){
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String,Object> map){
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String,Object> map, long time){
try {
redisTemplate.opsForHash().putAll(key, map);
if(time>0){
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key,String item,Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key,String item,Object value,long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if(time>0){
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item){
redisTemplate.opsForHash().delete(key,item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item){
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item,double by){
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item,double by){
return redisTemplate.opsForHash().increment(key, item,-by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key){
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key,Object value){
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object...values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key,long time,Object...values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if(time>0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key){
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object ...values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key,long start, long end){
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key){
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key,long index){
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index,Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key,long count,Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
六、(最重要的)AuthorizantionInterceptor拦截器
拦截器定义每个有@AuthToken注解的方法在被调用前,都要根据验证token
如何验证token:获取请求头header中的Authorization中的token,去redis缓存中查询,有则允许,无则返回到登录页面
并且根据redis缓存里查询token的失效时间,判断是否需要重新定义失效时间(加时)
package com.jinv.studentinfo.interception;
import com.alibaba.fastjson.JSONObject;
import com.jinv.studentinfo.annotation.AuthToken;
import com.jinv.studentinfo.token.ConstantKit;
import com.jinv.studentinfo.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;
/**
* 鉴权拦截器
* 使用token鉴权
*/
@Slf4j
public class AuthorizationInterceptor implements HandlerInterceptor {
@Autowired
private RedisUtils redisUtils;
//存放鉴权信息的Header名称,默认是Authorization
private String httpHeaderName = "Authorization";
//鉴权失败后返回的错误信息,默认是401 unauthorized
private String unauthorizedErrorMessage = "401 unathorized";
//鉴权失败后返回的HTTP错误码,默认为401
private int unauthorizedErrorCode = HttpServletResponse.SC_UNAUTHORIZED;
//存放登录用户模型Key的Request key
public static final String REQUEST_CURRENT_KEY = "REUQEST_CURRENT_KEY";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//如果有@AuthoToken注解则需要验证token:
if (method.getAnnotation(AuthToken.class)!=null || handlerMethod.getBeanType().getAnnotation(AuthToken.class)!=null){
//1.获取请求头的token
String token = request.getHeader(httpHeaderName);
log.info("token is ()",token);
String username = "";
//根据token获取redis缓存的username
if (token != null && token.length()!=0){//用token校验
username=redisUtils.get(token).toString();//根据token获取redis缓存中的username
log.info("username is: {}",username);
//2.根据token+username获取token的初始时间
if (username !=null && !username.trim().equals("")){//校验成功
Long tokenBirthTime = Long.valueOf((String) redisUtils.get(token+username));//获取token初始时间
log.info("token Birth time is: {}",tokenBirthTime);
Long diff = System.currentTimeMillis()-tokenBirthTime;//时间差——token存在了多久
log.info("token is exist: {}",diff);
//3.重置缓存失效时间
if (diff> ConstantKit.TOKEN_RESET_TIME){
redisUtils.expire(username, ConstantKit.TOKEN_EXPIRE_TIME);
redisUtils.expire(token, ConstantKit.TOKEN_EXPIRE_TIME);
log.info("Reset expire time success!");
Long newBirthTime = System.currentTimeMillis();
redisUtils.set(token+username,newBirthTime.toString());//重置token初始时间
}
//4/设置请求头
request.setAttribute(REQUEST_CURRENT_KEY, username);
return true;
}
} else {//验证失败要重新登录
response.sendRedirect("/studentinfo");
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
七、配置类
1.RedisConfig(redis配置类)
**
package com.jinv.studentinfo.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
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.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* Redis配置类
*/
@Configuration
@EnableCaching//启用缓存,这个注解很重要
//继承CachingConfigurerSupport,为了自定义key的策略,可以不继承
public class RedisConfig extends CachingConfigurerSupport {
@Value("${spring.cache.redis.time-to-live}")
private Duration timeToLive = Duration.ZERO;
@Bean(name = "redisTemplate")
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(stringRedisSerializer);
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
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);
// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(timeToLive)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
2、WebApplicationConfig(添加token拦截器)
package com.jinv.studentinfo.config;
import com.jinv.studentinfo.interception.AuthorizationInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器
*/
@Configuration
public class WebAppConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthorizationInterceptor()).addPathPatterns("/**");//拦截所有
}
}
八、封装返回类
ResultVo
package com.jinv.studentinfo.vo;
import lombok.Data;
import java.io.Serializable;
/**
* 封装返回类
* @Author: jinv
* @CreateTime: 2020-05-15 18:08
* @Description:
*/
@Data
public class ResultVo<T> implements Serializable {
private static final long serialVersionUID=1L;
//响应码
private int code;
//返回消息
private String msg;
//数据
private T data;
//成功
public ResultVo(){
this.code=0;
this.msg="成功";
}
//成功返回数据
public ResultVo(T data){
this();
this.data=data;
}
//成功,返回消息
public ResultVo(String msg){
this();
this.msg=msg;
}
//失败,返回错误码和失败消息
public ResultVo(int code,String msg){
this.code=code;
this.msg=msg;
}
//失败,返回固定500错误码和异常消息
public ResultVo(Throwable e){
this.code=500;
this.msg=e.getMessage();
}
//失败,返回自定义的错误码和异常消息(使用场景,ExceptionHandler隐藏异常细节,直接返回500和未知异常,请联系管理员)
public ResultVo(int code,Throwable e){
this.code=code;
this.msg=e.getMessage();
}
}
九、Web层
LoginController——第一次登陆成功要把username+password传给token的相关构造类,生成token并且存入redis缓存中,设置失效时间等
package com.jinv.studentinfo.web;
import com.alibaba.fastjson.JSON;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.jinv.studentinfo.annotation.AuthToken;
import com.jinv.studentinfo.domain.Student;
import com.jinv.studentinfo.domain.Teacher;
import com.jinv.studentinfo.service.StudentService;
import com.jinv.studentinfo.service.TeacherService;
import com.jinv.studentinfo.token.Md5TokenGenerator;
import com.jinv.studentinfo.utils.KaptchaUtils;
import com.jinv.studentinfo.utils.RedisUtils;
import com.jinv.studentinfo.vo.ResultVo;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Controller
public class LoginController {
@Resource
private DefaultKaptcha kaptchaProducer;//验证码生成器
@Autowired
private Md5TokenGenerator tokenGenerator;//token生成器
@Autowired
private RedisUtils redisUtils;//redis工具类
@Autowired
private StudentService studentService;
@Autowired
private TeacherService teacherService;
private ResultVo resultVo;
@GetMapping("")//去登陆页面
public String index(){
return "login";
}
/**
* 检查登录状态
* @param request
* @return resultVo:已登录:code:0,msg:成功 未登录:返回登录页面
*/
@GetMapping("/checkLogin")
@AuthToken
@ResponseBody
public String checkLogin(HttpServletRequest request){
Long studentId = Long.valueOf(redisUtils.get(request.getHeader("Authorization")).toString());
Student student = studentService.findById(studentId);
Map<String,Student> map = new HashMap<>();
map.put("student",student);
resultVo = new ResultVo(0,map);
return JSON.toJSONString(resultVo);
}
/**
* 登录校验表单以及验证码
* @param @RequestBody 请求体Map<String,String>
* @param request 获取客户端ip
* @return resultVo code=0管理员验证成功,code=1学生验证成功,
* code=2教师验证成功,code=401账号密码错误,
* code=402验证码错误, data——>token令牌
*/
@PostMapping("/login")
@ResponseBody
public String login(
@RequestBody Map map,
HttpServletRequest request){
Long username = Long.valueOf(map.get("username").toString());
String password = (String) map.get("password");
String validateCode = (String) map.get("validateCode");
String judgement = (String) map.get("judgement");
//获取客户端IP
String IP = request.getRemoteAddr();
//获取redis中的验证码
String rightCode = redisUtils.hget("validateCode",IP).toString();
//验证验证码
if (validateCode.trim().equals(rightCode)){//验证成功
if (judgement.equals("0")){//0学生登录
Student student = studentService.findByIdAndPsd(username, password);
if (student!=null){//验证成功
//设置值token缓存
String token = tokenGenerator.tokenUtils(String.valueOf(username), password);
Map<String,String> data = new HashMap<>();
data.put("token",token);
resultVo = new ResultVo(1,data);//学生验证成功
}else {
resultVo = new ResultVo(401,"账号或密码错误");
}
}else{//judgement=1老师登录
Teacher teacher = teacherService.findByIdAndPsd(username,password);
if (teacher!=null){//验证成功
//设置值token缓存
String token = tokenGenerator.tokenUtils(String.valueOf(username), password);
Map<String,String> data = new HashMap<>();
data.put("token",token);
resultVo = new ResultVo(2,data);//教师验证成功
}else {
resultVo = new ResultVo(401,"账号或密码错误");
}
}
}else {
resultVo = new ResultVo(402,"验证码错误");
}
return JSON.toJSONString(resultVo);
}
/**
* 登录验证码图片
*/
@GetMapping("/loginValidateCode")
public void loginValidateCode(HttpServletRequest request,HttpServletResponse response) throws Exception {
KaptchaUtils.validateCode(request,response,kaptchaProducer,redisUtils);
}
}
十、前端
1、login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<title>登录页面</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><!--自适应-->
<link rel="stylesheet" href="../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" />
<link rel="stylesheet" href="../static/css/bootstrap-responsive.min.css" th:href="@{/css/bootstrap-responsive.min.css}" />
<link rel="stylesheet" href="../static/css/matrix-login.css" th:href="@{/css/matrix-login.css}" />
<link rel="stylesheet" href="../static/font-awesome/css/font-awesome.css" th:href="@{/font-awesome/css/font-awesome.css}" />
<link rel="stylesheet" href="../static/css/me.css" th:href="@{/css/me.css}" />
<style>
.error{
color: #b96027;
}
</style>
</head>
<body>
<div id="loginbox">
<div id="loginform" class="form-vertical">
<input type="hidden" name="judgement" id="judgement">
<div class="control-group normal_text"> <h3><img src="../static/img/logo.png" th:src="@{/img/logo.png}" alt="Logo" /></h3></div>
<div class="control-group">
<div class="text-center">
<h3 id="errorMsg" class="error" hidden>用户名或密码错误</h3>
</div>
<div class="controls">
<div class="text-center">
<h3 id="usernameError" class="error" hidden>用户名不能为空</h3>
</div>
<div class="main_input_box">
<span class="add-on bg_lg"><i class="icon-user"></i></span>
<input type="text" id="username" name="username" class="form-control" required placeholder="用户名。。。" />
</div>
</div>
</div>
<div class="control-group">
<div class="controls">
<div class="text-center">
<h3 id="passwordError" class="error" hidden>密码不能为空</h3>
</div>
<div class="main_input_box">
<span class="add-on bg_ly"><i class="icon-lock"></i></span>
<input type="password" id="password" name="password" class="form-control" required placeholder="密码。。。" />
</div>
</div>
</div>
<div class="control-group">
<div class="controls" style="text-align: center">
<img src="/loginValidateCode" th:src="@{/loginValidateCode}" id="loginValidateCode" name="loginValidateCode"
onclick="uploadLoginValidateCode()" class="m-margin-bottom" alt="">
<input type="text" id="validateCode" name="validateCode" class="form-control" required placeholder="验证码。。。">
<div class="text-center">
<h3 id="validateCodeError" class="error" hidden>验证码错误</h3>
</div>
</div>
</div>
<div class="form-actions" style="text-align: center">
<button type="submit" id="tea-log-btn" name="submit" class="btn btn-success" >教师登录</button>
<button type="submit" id="stu-log-btn" name="submit" class="btn btn-warning" >学生登录</button>
</div>
</div>
</div>
<script src="../static/js/jquery.min.js" th:src="@{/js/jquery.min.js}"></script>
<script src="../static/js/jquery.validate.js" th:src="@{/js/jquery.validate.js}"></script>
<script src="../static/js/messages_zh.js" th:src="@{/js/messages_zh.js}"></script><!--验证表单-->
<script src="../static/js/matrix.login.js" th:src="@{/js/matrix.login.js}"></script>
<script src="../static/js/me/login.js" th:src="@{/js/me/login.js}"></script>
</body>
</html>
2、login.js
重点:使用localStorage存储接口返回的token,后面的请求都要设置ajax的header的key:Authorization, value:[token]
//跟新验证码图片
function uploadLoginValidateCode() {
/*路径一定要加上项目的跟路径*/
$("#loginValidateCode").attr("src","/studentinfo/loginValidateCode?random="+new Date().getMilliseconds());//加一段随机数更换图片
}
$("#tea-log-btn").click(function () {
var valid = yanzheng();
if (valid==true){
$("#judgement").val("1");
login();
}
});
$("#stu-log-btn").click(function () {
var valid = yanzheng();
if (valid==true){
$("#judgement").val("0");
login();
}
});
/*验证表单*/
function yanzheng() {
var username = $("#username").val();
var password = $("#password").val();
var validateCode = $("#validateCode").val();
if (username.trim()!=null&&username.trim()!=""){
if (password.trim()!=null&&password.trim()!=""){
if (validateCode.trim().length==4){
return true;
}else {
$("#validateCodeError").show();
return false;
}
}else {
$("#passwordError").show();
return false;
}
}else {
$("#usernameError").show();
return false;
}
}
//登录
function login() {
var validateCode = $("#validateCode").val();
var adata = {
"username":$("#username").val(),
"password":$("#password").val(),
"validateCode":validateCode,
"judgement":$("#judgement").val()
}; //定义JSON字符串格式的数据
if (validateCode.trim()!=null && validateCode.trim()!="" && validateCode.trim().length==4){//校验验证码不为空且为4位数字
$.ajax({
type: "post",
async: false, //默认为true异步,false表示同步,异步不需要等待,同步需要等待回调方法做完
contentType : "application/json;charset=UTF-8",//上传内容格式为json结构
dataType: "json",
data : JSON.stringify(adata),
url: "/studentinfo/login",
success: function (data) {
console.log(data);
// var result = eval("("+data1+")");
var code = data["code"];
var token = data["data"]["token"];
var msg = data["msg"];
console.log(code);
console.log(token);
console.log(msg);
if (code==1){
console.log("学生验证成功");
if (saveToken(token)==true){
location.href="/studentinfo/student/index";//跳转到学生端首页
}
}
if (code==2){
console.log("教师验证成功");
// location.href="/studentinfo/student/index?token="+token;//将token拼接进去
}
else if (code==402) {
$("#validateCodeError").show();
uploadLoginValidateCode();
}
},
error: function(XMLHttpRequest,textStatus){
alert("服务器错误!状态码:"+XMLHttpRequest.status);
// 状态
console.log(XMLHttpRequest.readyState);
// 错误信息
console.log(textStatus);
}
});
}else{
$("#validateCodeError").show();
}
}
//存储token到localStorage中,并且发送ajax请求
function saveToken(token) {
if (! window.localStorage){//浏览器不支持localstorage,localstorage是html5的新属性,并且在IE8以上的IE版本
alert("浏览器不支持localstorage,localstorage是html5的新属性");
return false;
}else {//浏览器支持localStorage
var storage=window.localStorage;
//写入token
localStorage.setItem("token",token);
console.log("已经存入localStorage");
console.log(localStorage.getItem("token"));//也可以这样子取值:localStorage.getItem("token")
return true;
}
}
重点:3.客户端还需要一个用于检测登录状态的ajax,并且该方法是window.onload的即页面加载完就要自动发送ajax请求检测登录状态,并不是每次调用接口都往ajax里面加入token
fragment.js
//加载页面并且检验登录状态
window.onload=function(){//页面加载完马上执行
var token = localStorage.getItem("token");
console.log("加载出来token");
console.log(token);
$.ajax({
type: "get",
url: "/studentinfo/checkLogin",
// data: mydata,
contentType: "application/json",
dataType: "json",
beforeSend: function(request) {
request.setRequestHeader("Authorization", token);//将token加入请求头
},
success(data){
console.log(data);
// var result = eval("("+data+")");
var code = data["code"];
var msg = data["msg"];
var studentName = data["data"]["student"]["studentName"];
if (code==0){//已登录
$("#studentname").html(studentName);
$("#myname").html(studentName);
}else {//未登录
location.href="/studentinfo";//跳转到登录页面
}
},
error(e){
console.log(e);
}
});
};
这里还是有很多未完善的地方,后期会把项目源码贴出来github地址填坑。。。。还在开发中。。。。