Web实战课的学习笔记3

实现登录2.2

jsr303参数校验

  1. 引入依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

  1. 在controller层中,给需验证的传入参数加上@Valiad,找到需验证的参数的对象,给此对象中需要进行验证的参数添加相应的注解。如,给LoginController类中的doLogin方法的接收参数loginVo前添加@Valiad注解,因为loginVo是LoginVo类的对象,所以进入到LoginVo类中,对意味电话号码的mobile和密码password进行注解添加,首先两者均不可为空,因此给两个属性前都添加@NotNull,表示禁止此属性值为null,再使用 @Length(min=限制的长度)规定密码的长度,为保证输入手机号码是合法的再添加自定义注解@IsMobie。
  2. 为实现自定义注解,先再启动类的同包下创建一个validator包,用于存放校验模型,在包中创建IsMobie类,将此类的改为注解类,进入到@NotNoll注释对应的类中,将其中对类进行的注解都复制在IsMobile类前,再将@NotNoll注释对应的类中的message、groups和payload属性都复制到IMobile接口类中。将如果校验不通过的信息写在message的默认值中。
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
       validatedBy = {IsMobileValidator.class}
)
public @interface IsMobile {

   boolean required() default true;//运行在特殊时刻为空。如不传参数

   String message() default "手机号码格式错误";  //校验不通过时提示信息

   Class<?>[] groups() default {};

   Class<? extends Payload>[] payload() default {};

}

补充:
[ 1 ].default的多种应用方法
(1)default在方法和类前是表示的是同一包中的类可以访问,声明时没有加修饰符,认为是friendly。
(2)default 是Java8中引入的关键字,它可以使一个具体的类存在与一个接口类中,在接口类的实现类中可以不用重写这个具体方法。

public interface testDefault {

    void test2();

    default void test1(){
        System.out.println("this is a specific class");
    }
}
class testClass implements testDefault{

    @Override
    public void test2() {
        System.out.println("this is a  abstract class by rewrite");
    }
}

(3)default在switch-case中,就是当case里的值与switch里的key没有匹配的时候,执行default里的方法。
(4)在注解属性中default后的值为此对象的默认值。
[ 2 ].自定义注解类所用到的各种注解的含义
(1)@Target:对注解的作用目标的配置注解,其中的属性ElementType按照注释可能出现在Java中的语法位置,提供了一个简单的分类,各取值的意思分别是,
······TYPE:类,接口或是枚举声明之前
······FIELD:字段声明之前
······METHOD:方法声明之前
······PARAMETER:正式的参数声明之前
······CONSTRUCTOR:构造函数声明之前
······LOCAL_VARIABLE:局部变量声明之前
······ANNOTATION_TYPE:注释类型声明之前
······PACKAGE:包声明之前
(2)@Retention:用来说明该注解类的生命周期,有三个参数分别为,
······RetentionPolicy.SOURCE : 注解只保留在源文件中;
······RetentionPolicy.CLASS : 注解保留在class文件中,在加载到JVM虚拟机时丢弃;
······RetentionPolicy.RUNTIME : 注解保留在程序运行期间,此时可以通过反射获得定义在某个类上的所有注解。
(3)@Documented:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化
(4)@Constraint:用于处理注解的逻辑,也就是来限定自定义注解的方法,将校验器进行引入

  1. 为创建用于实现判断手机号是否合规的逻辑,创建IsMobileValidator类,是这个类实现ConstraintValidator<自定义注解名称,注解修饰的类型>接口。将实现的接口类进行重写,initialize方法用于初始化,isValid方法用于实现判断是否合法的逻辑。新建用于判断是否为空的boolean属性required,将其默认值设为false。
    在isValid中根据required的值判断信息是否为必须的,如果不为空就是必须的,调用于之前创建的判断手机号格式的ValidatorUtil类的方法,来进行对手机号码的合法性的判断。
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

    private boolean required = false;

    @Override
    public void initialize(IsMobile isMobile) {
        required = isMobile.required();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (required){
            return ValidatorUtil.isMobile(s);
        }else {
            if (StringUtils.isEmpty(s)){
                return true;
            }else {
                return ValidatorUtil.isMobile(s);
            }
        }
    }
}

补充:
这个IsMobileValidator因为实现了ConstraintValidator,所以它默认被spring管理成bean,可以在这个逻辑处理类里面用@Autowiredu或者@Resources注入别的服务,而且不用在类上面用@Compent注解成spring的bean。

  1. 这样参数校验功能就实现了,可以将controller中参数校验部分的代码进行删除。

异常处理

重新运行程序,输入错误的手机号码,但是并没有出现,手机号码格式错误的提示信息显示出来。打开浏览器的开发者工具可以看到,校验器是正常工作的,为使页面的显示更为友好,加入异常拦截。

自定义异常拦截器

  1. 在起始类同包下创建exception包,并在这个包下创建GlobleExceptionHandler类。为这个类添加@ControllerAdvice注解,实现全局异常处理。为方便输出将java对象转为json格式的数据,添加@ResponseBody注解。编写在拦截到异常时需做的事情的逻辑实现方法,并给它添加@ExceptionHandler,在这个注解的value中规定拦截的异常的类型。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
        e.printStackTrace();
        if (e instanceof GlobalException){
            GlobalException ex = (GlobalException)e;
            return Result.error(ex.getCodeMsg());
        }else if (e instanceof BindException){//判断是否为绑定异常
            BindException ex = (BindException)e;    //强转
            List<ObjectError> errors = ex.getAllErrors();   //获取所有的错误
            ObjectError error = errors.get(0);      //拿到第一个错误
            String msg = error.getDefaultMessage();     //拿到错误信息
            return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));      //拼接完之后的message应该是 参数校验异常:msg的内容
        }else {
            return Result.error(CodeMsg.SERVER_ERROR);
        }
    }
}

  1. 优化:
    (1)定义一个全局异常类。继承RuntimeException,创建CodeMsg类的cm属性,并创建为其赋值的该类的构造方法。给类添加@Data,用于避免手动生成set和get方法。
@Data
public class GlobalException extends RuntimeException {

    private CodeMsg codeMsg;

    public GlobalException(CodeMsg codeMsg) {
        super(codeMsg.getMsg());
        this.codeMsg = codeMsg;
    }


}

(2)修改service类中的代码,将login方法中返回值类型换为boolean类型,若原代码中返回的为异常情况,就将其替换为使用throw的异常抛出,若全程没有错误,就返回true。

 public boolean login(LoginVo loginVo) {
        if(loginVo == null){
            throw new GlobalException(CodeMsg.SERVER_ERROR);        //出现异常直接往外抛
        }
        String mobile = loginVo.getMobile();
        String formPass = loginVo.getPassword();
        //判断手机号是否存在
        MiaoshaUser user = getById(Long.parseLong(mobile));
        if(user == null){
            throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
        }
        //验证密码
        String dbPass = user.getPassword();
        String saltDB = user.getSalt();
        String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
        if (!calcPass.equals(dbPass)){
            throw new GlobalException(CodeMsg.PASSWORD_ERROR);
        }
        return true;
    }

(3)修改Controller层的代码,将doLogi中的方法改为直接调用service的login方法,因为如果有异常会直接抛出,所以返回时直接返回成功。

  @RequestMapping("/do_login")
    @ResponseBody
    public Result<Boolean> doLogin(@Valid LoginVo loginVo){

        log.info(loginVo.toString());
        //登录
        miaoshaUserService.login(loginVo);
        return Result.success(true);

    }

分布式Session(重要)

开发时如果是多台服务器,session有可能落不到同一台服务器上从而造成session数据的丢失。虽然可以用session同步来解决这个问题,但是在实际应用中却很少这么做,因为如果同步的服务器过多是一件很恐怖的事情。
原理:通过uuid获取session
实现:

  1. 生成cookie
    (1)在MiaoshaUserService中,找到login方法。在通过手机号码、密码和服务的校验后,开始写生成cookie步骤。
    (2) 在util包中创建UUIDUtil类。
public class UUIDUtil {
    public static String uuid(){
        
        return UUID.randomUUID().toString().replace("-", "");   //去掉原生自带的"-"
    }
}

(3)想通过UUID生产token,要将这个token写在cookie中,传递给客户端,为区别不同的用户,需对用户进行表示。将用户写在redis当中,将RedisService对象引入这个类。
(4)为了给token生成一个prefix,在redis类中为用户创建一个新的key类,在其中创建一个token,其值为”tk“。设置过期时间TOKEN_EXPIRE。


public class MiaoshaUserKey extends BasePrefix {

 	public static final int TOKEN_EXPIRE = 3600*24*2;
    public MiaoshaUserKey(int expireSeconds, String prefix) {
        super(expireSeconds, prefix);
    }
    public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
}

(5)返回到MiaoshaUserService的login方法中,通过RedisService的set方法向redis中存储数据。
(6) 新建Cookie,新建对象需要name和value两个值,新建Cookie的name。将cookie的name和之前的token分别作为,cookie的name和value添加在Cookie的new对象中。
(7)为Cookie设置时效,如果miaoshaKey中的token过期了,那么这个cookie就过期了。
(8) 设置Cookie的路径为根目录,并给login方法传入的数据中添加HttpServletResponse对象。用response的addCookie方法,将Cookie写入客户端。

 //生成cookie
        String token = UUIDUtil.uuid();
        redisService.set(MiaoshaUserKey.token, token, user); //将user和token绑定并存入Redis中
        Cookie cookie = new Cookie(COOKIE_NAME_TOKEN, token);   //根据token生成cookie
        cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
        cookie.setPath("/");
        response.addCookie(cookie); //将cookie放入response客户端中

(9)到调用了MiaoshaUserService类中的login方法的controller类中,给从前端传入的值中添加HttpServletResponse对象,并将其加入login方法的调用中。

  1. 登录后跳转页面
    (1)在resources包下的templates,创建goods_list.html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>商品列表</title>
</head>
<body>
    <p th:text="'hello:' + ${user.nickname}"></p>
</body>
</html>

(2)新建Controller,用于跳转页面。添加类中添加list方法,返回值为goods_list页面的名称。

@Controller
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    MiaoshaUserService miaoshaUserService;

    @RequestMapping("/to_list")
    public String list(Model model) {
        model.addAttribute("user", new MiaoshaUser());
        return "goods_list";
    }

}

(3)在login.html中,在登录成功后添加跳转goods_list页面的跳转语句。

window.location.href="/goods/to_list";

(4)运行测试:跳转成功,并且cookie生成成功
在这里插入图片描述
(5)因为cookie已经成功的上传到了客户端中,所以可以直接获取客户端中的cookie,对GoodsController类进行修改。用@CookieValue来获取指定value的cookie值,value为token的name。大部的手机为了兼容性,都不将token存在cookie中,所以再使用@RequestPparam获取cookie。为两种获取cookie的方法设定优先级,先取RequestPparam中的cookie如果取不到,再取CookieValue中的。

@Controller
@RequestMapping("/goods")
public class GoodsController {

    private static Logger log = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    MiaoshaUserService miaoshaUserService;

    @Autowired
    RedisService redisService;

    @RequestMapping("/to_list")
    public String list(Model model, @CookieValue(value=MiaoshaUserService.COOKIE_NAME_TOKEN,required = false)String cookieToken,
                       @RequestParam(value=MiaoshaUserService.COOKIE_NAME_TOKEN,required = false)String paramToken) {
        if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){
            return "login";
        }
        String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;//设定优先级
        log.info("this is goodcontroller"+token);
        MiaoshaUser user = miaoshaUserService.getByToken(token);
        model.addAttribute("user",user);
        return "goods_list";
    }

}

注意:如果运行出现500错误,并提示“Required String parameter ‘token’ is not present”
可能是给cookieToken和paramToken漏添加了required = false,将俩个数值都会设为不是必须的。
(5)找到MiaoshaService,创建getByToken方法,在其中创建一个方法用于从缓存中取值。这个类中必不可少的是一定要有参数认证。

    public MiaoshaUser getByToken(String token) {
        if (StringUtils.isEmpty(token)){
            return null;
        }
        return redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
    }

延长token的时效:
(1)重新把缓存重点值获取一下,创建新的cookie,将数据写进去。先将生成cookie的代码分割出来。将其复制在新建的addCookie中。
(2)在原生成cookie的地方,调用addCookie方法,并将这一句复制在getByToken中,这样就实现了新建cookie。这样修改后,在功能上实现了,但是并不优雅。每次调用方法时传入的值都过多。
将代码优雅:
(1)为使方法传入参数比较少,写一个类,将传入参数都获取一遍。创建新的继承WebMvcConfigurer类的Adapter类WebConfig子类,并为其添加@Configuration注解。

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Autowired
    UserArgumentResolver userArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(userArgumentResolver);
    }
}

(2)创建逻辑实现类UserArgumentResolver,实现了HandlerMethodArgumentResolver接口。将controller中对获取到的token参数的操作都移植到resolveArgument方法下。通过捕捉所有的cookie,进行遍历来获取我们所需要的cookie。

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    MiaoshaUserService miaoshaUserService;

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        Class<?> clazz = methodParameter.getParameterType();    //获取参数的类型
        return clazz == MiaoshaUser.class;      //如果为真,才会执行下面的resolveArguement方法
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
                                  NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);       //先拿到request和response
        HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);

        String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);         //再拿到paramToken和cookieToken
        String cookieToken = getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
            return null;
        }
        String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;  //paramToken优先使用
        return miaoshaUserService.getByToken(response, token);
    }

    private String getCookieValue(HttpServletRequest request, String cookieNameToken) {
        Cookie[] cookies = request.getCookies();        //获取所有的cookie
        for (Cookie cookie : cookies){
            if (cookie.getName().equals(cookieNameToken)){
                return cookie.getValue();
            }
        }
        return null;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值