手机验证码+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;
    }