手机验证码+Redis登录验证+token+登录拦截
文章目录
- 手机验证码+Redis登录验证+token+登录拦截
- 解决方案
- 思想
- 以阿里云为例
- 1.阿里云官网开通短信服务
- 2.创建签名
- 3.创建短信模板
- 4.依赖短信服务的包
- 5.编写发短信的工具类
- 6.前端发送请求携带手机号到后端
- 7.开启Redis
- 8.后端接收数据
- 9.token的使用
- 10.使用工具类做到对象与字符串之间的转换
- 11.前端用户收到验证码后发送登录请求
- 12.每次请求都携带token
- 13.定义后端拦截器
- 14.定义异常处理类
- 15.前端验证状态码
- 16.用户数据
解决方案
- 各种云平台
- 各种数据商
- 各种第三方的延伸产品
思想
- 用户使用手机验证码登录
- 前端点击按钮通过第三方云平台发送短信验证码
- 在发送验证码前先判断用户是否存在
- 如果用户存在则发送验证码,同时把验证码存入redis中,key为手机号value为随机验证码
- 用户收到验证码后点击登录进行登录验证
- 通过前端传的手机号在redis中查询数据,判断验证码是否匹配
- 如果匹配则登录成功,同时清除redis中的验证码数据
- 此时生成一串uuid
- 首先将uuid作为key,用户信息作为value存入redis中,以免前端保存用户信息不安全
- 再把uuid作为参数生成token传到前端,然后再把token保存至浏览器中
- 定义一个拦截器
- 前端每次请求时都在请求头中携带token信息
- 后端定义过滤器,判断请求头中携带token是否正确
- 如果判断错误则抛出异常,定义异常处理类返回状态码
- 状态码如果未登录则在前端跳转至登录页面,如果登录状态则正常访问
以阿里云为例
1.阿里云官网开通短信服务
2.创建签名
3.创建短信模板
4.依赖短信服务的包
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.1</version>
</dependency>
5.编写发短信的工具类
public class SMSUtils {
//参数一表示接收短信的手机号
//参数二表示发送的验证码
public static void sendSms(String phone, String code) {
try {
//加载外部配置文件
InputStream resourceAsStream = SMSUtils.class.getClassLoader().getResourceAsStream("sms.properties");
Properties properties = new Properties();
properties.load(resourceAsStream);
//初始化发送人
com.aliyun.dysmsapi20170525.Client client = createClient(properties.getProperty("accessKeyId"), properties.getProperty("accessKeySecret"));
SendSmsRequest sendSmsRequest = new SendSmsRequest()
//要发送的手机号
.setPhoneNumbers(phone)
//签名
.setSignName("xxxx")
//模板编号
.setTemplateCode("SMS_173425292")
//发送的验证码
.setTemplateParam("{\"code\":" + code + "}");
//发送短信验证码
client.sendSms(sendSmsRequest);
} catch (Exception e) {
e.printStackTrace();
}
}
public static com.aliyun.dysmsapi20170525.Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
Config config = new Config()
// 您的AccessKey ID
.setAccessKeyId(accessKeyId)
// 您的AccessKey Secret
.setAccessKeySecret(accessKeySecret);
// 访问的域名
config.endpoint = "dysmsapi.aliyuncs.com";
return new com.aliyun.dysmsapi20170525.Client(config);
}
}
6.前端发送请求携带手机号到后端
7.开启Redis
- 导入jedis的依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
- 编写redis工具类
public class RedisUtils {
private static JedisPool jedisPool;
//初始化连接池
static {
InputStream resourceAsStream = RedisUtils.class.getClassLoader().getResourceAsStream("redis.properties");
Properties properties = new Properties();
try {
properties.load(resourceAsStream);
} catch (IOException e) {
e.printStackTrace();
}
//初始化连接池的各种属性
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(Integer.parseInt(properties.getProperty("maxTotal")));
jedisPoolConfig.setMaxIdle(Integer.parseInt(properties.getProperty("maxIdle")));
jedisPool = new JedisPool(jedisPoolConfig, properties.getProperty("host"), Integer.parseInt(properties.getProperty("port")));
}
//从连接池中取出一个jedis
public static Jedis getJedis(){
return jedisPool.getResource();
}
//存值得方法
public static void setCache(String key,String value,String redisKey){
Jedis jedis = getJedis();
//存入redis中值
jedis.set(redisKey+key,value);
//把连接放回连接池
jedis.close();
}
//取值的方法
public static String getCache(String key,String redisKey){
Jedis jedis = getJedis();
//以key取redis中的值
String s = jedis.get(redisKey+key);
jedis.close();
//返回取出的值
return s;
}
}
8.后端接收数据
@GetMapping("getCode")
public AxiosResult<Void> getCode(String phone){
//通过手机号查询数据库验证用户是否存在
User userByPhone = userService.findUserByPhone(phone);
if (userByPhone!=null){
//如果数据存在则生成6位随机数字验证码
int code =(int) (Math.random()*(999999-100000+1)+100000);
//先把验证码存入redis中,key为前端传入的手机号,value为随机数字验证码
RedisUtils.setCache(phone,code+"",RedisKey.PREFIX_KEY);
//调用工具类发送短信验证码
SMSUtils.sendSms(phone,code+"");
return AxiosResult.success();
}
return AxiosResult.error();
}
9.token的使用
- 引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
- 编写token工具类
@Component
public class TokenUtils {
//自定义秘钥
private static final String SECRET = "0x6c717a";
//生成token
public String getToken(String uuid) {
try {
//参数为自定义秘钥
Algorithm algorithm = Algorithm.HMAC256(SECRET);
String token = JWT.create()
//签名
.withIssuer("阿飞")
//token中携带的值
.withClaim("uuid", uuid)
.sign(algorithm);
return token;
} catch (JWTCreationException exception) {
//Invalid Signing configuration / Couldn't convert Claims.
}
return "";
}
//解析token
public String verifyToken(String token){
try { //秘钥
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
//签名
.withIssuer("阿飞")
.build(); //Reusable verifier instance
//解析token
DecodedJWT jwt = verifier.verify(token);
//通过生成token是存入参数的key取出
Claim uuid = jwt.getClaim("uuid");
//返回从token中解析出的值
return uuid.asString();
} catch (JWTVerificationException exception){
System.out.println(exception.toString());
}
return "";
}
}
10.使用工具类做到对象与字符串之间的转换
- 依赖jackson的包
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope>
</dependency>
- 创建工具类
public class JsonUtils {
private static ObjectMapper objectMapper = new ObjectMapper();
//对象转字符串方法
public static String Obj2Str(Object o) {
try {
String s = objectMapper.writeValueAsString(o);
return s;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return "";
}
//字符串转对象方法
public static <T>T Str2Obj( String str, Class<T> clazz){
try {
T t = objectMapper.readValue(str, clazz);
return t;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
11.前端用户收到验证码后发送登录请求
@PostMapping("doLogin")
public AxiosResult<String> doLogin(String phone,String code){
//通过前端传来的手机号从redis中取值
String cache = RedisUtils.getCache(phone, RedisKey.PREFIX_KEY);
//判断redis中的值与前端传来的验证码是否一致
if(code.equals(cache)){
//通过手机号查询数据库查出用户信息
User user = userService.findUserByPhone(phone);
//生成一个uuid
String uuid = UUID.randomUUID().toString();
//把用户信息转成字符串存入redis中
RedisUtils.setCache(uuid, JSONUtils.obj2str(user),RedisKey.LOGIN_USER);
//传入uuid生成token返回前端
String token = TokenUtils.getToken(uuid);
return AxiosResult.success(token);
}
return AxiosResult.error();
}
12.每次请求都携带token
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
config.baseURL = 'http://localhost:8080/';
//添加请求头Authorization: Bearer <token>格式
config.headers.Authorization="Bearer "+localStorage.getItem("token");
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
13.定义后端拦截器
- 编写java代码
//创建一个类实现HandlerInterceptor接口
public class LoginInterceptor implements HandlerInterceptor {
@Override
//重写本方法
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求头中的数据
String authorization = request.getHeader("Authorization");
//判断token是否合法,如果不合法则抛出自定义异常,合法则返回true继续执行代码
if(StringUtils.isEmpty(authorization)){
throw new MyException(EnumStatus.NO_LOGIN);
}
if(!authorization.startsWith("Bearer ")){
throw new MyException(EnumStatus.NO_LOGIN);
}
String[] s = authorization.split(" ");
if (s[1].equals("null")){
throw new MyException(EnumStatus.NO_LOGIN);
}
String s1 = s[1];
DecodedJWT decodedJWT = TokenUtils.verifierToken(s1);
if (decodedJWT==null){
throw new MyException(EnumStatus.NO_LOGIN);
}
return true;
}
}
- 配置拦截器
配置文件的方式
<!-- 在spring的xml配置文件中配置拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<!--要拦截的请求路径-->
<mvc:mapping path="/**"/>
<!--放行的请求,一般登录请求和获取验证码的请求要放行-->
<mvc:exclude-mapping path="/doLogin"/>
<mvc:exclude-mapping path="/getCode"/>
<!--配置拦截器的路径-->
<bean class="com.qy28.sm.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
注解的方式
@Configuration
@ComponentScan(basePackages = {"com.qy28.cn"}, excludeFilters = {
@ComponentScan.Filter(Service.class)
})
@EnableWebMvc
public class ControllerConfig implements WebMvcConfigurer {
//使用@Autowired要先把拦截器放入容器中,如果不在容器中可以直接new
@Autowired
private LoginInterceptor loginInterceptor;
@Override
//重写本方法配置拦截器
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/**")
.excludePathPatterns("/common/doLogin","/common/verifyCode","/common/getActivateEmail","/employee/doActivate");
}
}
14.定义异常处理类
//类上添加@RestControllerAdvice注解
@RestControllerAdvice
public class MyExceptionHandler {
//方法上添加@ExceptionHandler注解参数为 自定义异常.class
@ExceptionHandler(HttpException.class)
public AxiosResult<Void> myHandler(MyException e) {
//捕捉到异常后返回前端状态码
return AxiosResult.error(e.getEnumStatus());
}
}
15.前端验证状态码
axios.interceptors.response.use(function (response) {
// 对响应数据做点什
let {status, data, massage} = response.data;
if (status == 200) {
return data;
} else if(status==305){
//如果未登录则跳转至登录页面
location.replace(`../login.html`)
}else {
if(status==303){
$("#activate").css("display","block");
}
$(document).Toasts('create', {
body: massage,
title: '登录失败',
icon: 'fas fa-envelope fa-lg',
})
}
return Promise.reject(false);
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
16.用户数据
如果之后的请求逻辑需要用到用户信息的话,直接从请求头中获取token,然后通过工具类解析token,从reids中取用户信息,可以减少与数据库的交互,也保证了用户信息的安全
//抽取成一个方法
public static User getUser(){
//通过请求从请求头中获取token
String token = ServletUtils.getRequest().getHeader("Authorization").split(" ")[1];
//解析token获取redis中的key
String uuid = verifierToken(token).getClaim("UUID").asString();
//通过key获取数据
String cache = RedisUtils.getCache(uuid, RedisKey.LOGIN_USER);
//字符串转对象
User user = JSONUtils.str2Obj(cache, User.class);
return user;
}