《尚品甄选》:后台系统——结合redis实现用户登录

文章目录

  • 一、统一结果实体类
  • 二、统一异常处理
  • 三、登录功能实现
  • 四、CORS解决跨域
  • 五、图片验证码
  • 六、登录校验功能实现
    • 6.1 拦截器开发
    • 6.2 拦截器注册
  • 七、ThreadLocal


《尚品甄选》:后台系统——结合redis实现用户登录_第1张图片
要求: 用户输入正确的用户名、密码以及验证码,点击登录可以跳转到后台界面。未登录的用户或者登录过期的用户没有访问后台界面的权限。

一、统一结果实体类

尚品甄选项目中所有接口的返回值统一都会定义为Result。

@Data
@Schema(description = "响应结果实体类")
public class Result<T> {
    //返回码
    @Schema(description = "业务状态码")
    private Integer code;
    //返回消息
    @Schema(description = "响应消息")
    private String message;
    //返回数据
    @Schema(description = "业务数据")
    private T data;
    private Result() {}
    // 返回数据
    public static <T> Result<T> build(T body, Integer code, String message) {
        Result<T> result = new Result<>();
        result.setData(body);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
    // 通过枚举构造Result对象
    public static <T> Result build(T body , ResultCodeEnum resultCodeEnum) {
        return build(body , resultCodeEnum.getCode() , resultCodeEnum.getMessage()) ;
    }
}

为了简化Result对象的构造,可以定义一个枚举类,在该枚举类中定义对应的枚举项来封装code、message的信息,如下所示:

@Getter
public enum ResultCodeEnum {
    SUCCESS(200, "操作成功"),
    LOGIN_ERROR(201, "用户名或者密码错误"),
    VALIDATECODE_ERROR(202, "验证码错误"),
    LOGIN_AUTH(208, "用户未登录"),
    USER_NAME_IS_EXISTS(209, "用户名已经存在"),
    SYSTEM_ERROR(9999, "您的网络有问题请稍后重试"),
    NODE_ERROR(217, "该节点下有子节点,不可以删除"),
    DATA_ERROR(204, "数据异常"),
    ACCOUNT_STOP(216, "账号已停用"),
    STOCK_LESS(219, "库存不足"),
    ;
    private Integer code;      // 业务状态码
    private String message;    // 响应消息
    ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

二、统一异常处理

在项目中,我们想让异常结果也显示为统一的返回结果对象,并且统一处理系统的异常信息,就需要统一异常处理。这里需要用到两个常用的注解:@ControllerAdvice@ExceptionHandler

2.1 全局异常处理器

@ControllerAdvice
public class GlobalExceptionHandler {
    //全局异常处理
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result error() {
        return Result.build(null, ResultCodeEnum.SYSTEM_ERROR);
    }
    @ExceptionHandler(GuiguException.class)
    @ResponseBody
    public Result error(GuiguException e) {
        return Result.build(null, e.getResultCodeEnum());
    }
}

2.2 自定义异常

自定义异常类,继承RuntimeException。

@Data
public class GuiguException extends RuntimeException {
    private Integer code ;          // 错误状态码
    private String message ;        // 错误消息
    private ResultCodeEnum resultCodeEnum ;     // 封装错误状态码和错误消息
    public GuiguException(ResultCodeEnum resultCodeEnum) {
        this.resultCodeEnum = resultCodeEnum ;
        this.code = resultCodeEnum.getCode() ;
        this.message = resultCodeEnum.getMessage();
    }
    public GuiguException(Integer code , String message) {
        this.code = code ;
        this.message = message ;
    }
}

三、登录功能实现

思路: 首先对比用户输入的验证码与redis中的验证码值是否相等,不相等则抛出GuiguException(ResultCodeEnum.VALIDATECODE_ERROR);接着获取表单中的用户名,根据用户名查询数据库,若不存在该用户,则抛出RuntimeException("用户名或者密码错误");用户存在则比较密码是否正确,密码不正确同样抛出RuntimeException("用户名或者密码错误");用户名和密码都正确时,生成令牌token,保存用户数据到redis中,最后返回token给前端。

《尚品甄选》:后台系统——结合redis实现用户登录_第2张图片

代码中token是用UUID随机生成的,存放在redis中的用户数据设置的有效期是30分钟。

public LoginVo login(LoginDto loginDto) {
        //校验验证码
        String captcha = loginDto.getCaptcha();
        String key = loginDto.getCodeKey();
        String redisCode = redisTemplate.opsForValue().get("user:validate" + key);
        if (StrUtil.isEmpty(redisCode) || !StrUtil.equalsIgnoreCase(redisCode, captcha)) {
            throw new GuiguException(ResultCodeEnum.VALIDATECODE_ERROR);
        }
        redisTemplate.delete("user:validate" + key);
        //根据用户名查询数据库
        String userName = loginDto.getUserName();
        SysUser sysUser = sysUserMapper.selectUserInfoByUserName(userName);
        if (sysUser == null) {
            throw new GuiguException(ResultCodeEnum.LOGIN_ERROR);
        }
        //校验密码
        String input_password = loginDto.getPassword();
        String database_password = sysUser.getPassword();
        input_password = DigestUtils.md5DigestAsHex(input_password.getBytes());
        if (!input_password.equals(database_password)) {
            throw new GuiguException(ResultCodeEnum.LOGIN_ERROR);
        }
        //登录成功,生成用户唯一标识token,并放入redis中
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        redisTemplate.opsForValue().set(
                "user:login" + token,
                JSON.toJSONString(sysUser),
                30,
                TimeUnit.MINUTES);
        //返回loginVo对象
        LoginVo loginVo = new LoginVo();
        loginVo.setToken(token);
        return loginVo;
    }
<mapper namespace="com.atguigu.spzx.manager.mapper.SysUserMapper">
    <sql id="columns">
        id,username userName ,password,name,
        phone,avatar,description,status,
        create_time,update_time,is_deleted
    sql>
    
    <select id="selectUserInfoByUserName" resultType="com.atguigu.spzx.model.entity.system.SysUser">
        select
        <include refid="columns"/>
        from sys_user where username = #{userName}
    select>
mapper>

四、CORS解决跨域

跨域请求: 通过一个域的JavaScript脚本和另外一个域的内容进行交互
域的信息: 协议、域名、端口号
《尚品甄选》:后台系统——结合redis实现用户登录_第3张图片
同域: 当两个域的协议、域名、端口号均相同

CORS是跨域的一种解决方案,CORS给了web服务器一种权限:服务器可以选择是否允许跨域请求访问到它们的资源。

我们可以添加一个配置类,让其继承配WebMvcConfigurer 类,来置跨域请求。

@Component
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")      // 添加路径规则
                .allowCredentials(true)               // 是否允许在跨域的情况下传递Cookie
                .allowedOriginPatterns("*")           // 允许请求来源的域规则
                .allowedMethods("*")
                .allowedHeaders("*");                // 允许所有的请求头
    }
}

五、图片验证码

验证码可以防止恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登录尝试。由于验证码技术具有随机性随机性较强、简单的特点,能够在一定程度上阻碍网络上恶意行为的访问,在互联网领域得到了广泛的应用。

我们将生成的4位验证码值作为value值,存入redis中,设置过期时间为5分钟;并给前端返回验证码的key以及图片验证码对应的字符串数据。

    public ValidateCodeVo generateValidateCode() {
        //生成验证码并放入redis中
        CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 2);
        String codeValue = circleCaptcha.getCode();//4位验证码值
        String imageBase64 = circleCaptcha.getImageBase64();//返回图片验证码,base64编码
        String key = UUID.randomUUID().toString().replaceAll("-", "");
        redisTemplate.opsForValue().set(
                "user:validate" + key,
                codeValue,
                5,
                TimeUnit.MINUTES
        );
        //返回ValidateCodeVo对象
        ValidateCodeVo validateCodeVo = new ValidateCodeVo();
        validateCodeVo.setCodeKey(key);
        validateCodeVo.setCodeValue("data:image/png;base64," + imageBase64);
        return validateCodeVo;
    }

六、登录校验功能实现

6.1 拦截器开发

后台管理系统中除了登录接口、获取验证码的接口在访问的时候不需要验证用户的登录状态,其余的接口在访问的时候都必须要求用户登录成功以后才可以进行访问。

《尚品甄选》:后台系统——结合redis实现用户登录_第4张图片

自定义一个类,实现HandlerInterceptor接口,实现接口中的两个方法:preHandle()、afterCompletion()。首先从请求头中获取token,如果token不存在,则不放行;接着用token从redis中获取用户数据,若redis中没有用户数据,也不放行;若存在用户数据,则把用户数据息放到ThreadLocal中,并重新更新过期时间为30分钟。

@Component
public class LoginAuthInterceptor implements HandlerInterceptor {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求方式,如果请求方式是options(预检请求),直接放行
        String method = request.getMethod();
        if ("OPTIONS".equals(method)) {
            return true;
        }
        //判断用户是否登录
        String token = request.getHeader("token");
        if (StrUtil.isEmpty(token)) {
            responseNoLoginInfo(response);
            return false;
        }
        String userInfoJson = redisTemplate.opsForValue().get("user:login" + token);
        if (StrUtil.isEmpty(userInfoJson)) {
            responseNoLoginInfo(response);
            return false;
        }
        //把用户信息放到ThreadLocal中,并更新过期时间
        AuthContextUtil.set(JSON.parseObject(userInfoJson, SysUser.class));
        redisTemplate.expire("user:login" + token, 30, TimeUnit.MINUTES);
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        AuthContextUtil.remove();
    }
    //响应208状态码给前端
    private void responseNoLoginInfo(HttpServletResponse response) {
        Result<Object> result = Result.build(null, ResultCodeEnum.LOGIN_AUTH);
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(JSON.toJSONString(result));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (writer != null) writer.close();
        }
    }
}

注意:

1、更新Redis中数据的存活时间的主要目的就是为了保证用户在使用该系统的时候,Redis中会一直保证用户的登录状态,如果用户在30分钟之内没有使用该系统,那么此时登录超时。此时用户就需要重新进行登录。
2、将从Redis中获取到的用户存储到ThreadLocal中,这样在一次请求的中就可以在controller、service、mapper中获取用户数据

6.2 拦截器注册

为了方便路径管理,我们把需要放行的路径写在了配置文件中:

# 配置放行路径
spzx:
  auth:
    noAuthUrls:
      - /admin/system/index/login
      - /admin/system/index/generateValidateCode

实体类定义:别忘记在启动类上加入@EnableConfigurationProperties(value = {UserProperties.class})

@ConfigurationProperties(prefix = "spzx.auth")
@Data
public class UserProperties {
    private List<String> noAuthUrls;
}
@Component
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Autowired
    private LoginAuthInterceptor loginAuthInterceptor;
    @Autowired
    private UserProperties userProperties;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginAuthInterceptor)
                .excludePathPatterns(userProperties.getNoAuthUrls())
                .addPathPatterns("/**");
    }
}

七、ThreadLocal

ThreadLocal是jdk所提供的一个线程工具类,叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量,使用该工具类可以实现在同一个线程进行数据的共享。

public class AuthContextUtil {
    // 创建一个ThreadLocal对象
    private static final ThreadLocal<SysUser> threadLocal = new ThreadLocal<>() ;
    // 定义存储数据的静态方法
    public static void set(SysUser sysUser) {
        threadLocal.set(sysUser);
    }
    // 定义获取数据的方法
    public static SysUser get() {
        return threadLocal.get() ;
    }
    // 删除数据的方法
    public static void remove() {
        threadLocal.remove();
    }
}

你可能感兴趣的:(#,微服务:尚品甄选,redis,java,springcloud)