回顾

上一章我们写了一个简单的注册demo,但是我们发现,它不能对参数进行校验,以及对校验结果去统一返回结果。如果按照传统的方法,写if...else来校验数据,那也太繁琐了。好在spring-boot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。

参数校验

  • 我们在参数上面加上对应注解,实现对参数的校验。
    参数校验.png
@Data
public class UserInfoRequest {
    /**
     * 主键
     */
    private Long userId;

    /**
     * 用户姓名
     */
    @NotNull(groups = RegisterGroup .class,message = "用户名不能为空")
    private String userName;

    /**
     * 登录用户名
     */
    @NotNull(groups = {RegisterGroup .class,LoginGroup.class},message = "用户登录名不能为空")
    private String loginName;

    /**
     * 登陆密码
     */
    @Size(groups = {RegisterGroup .class,LoginGroup.class},min = 6, max = 11, message = "密码长度必须是6-16个字符")
    private String userPassword;

    /**
     * 用户头像地址
     */
    @NotNull(groups = {RegisterGroup .class},message = "用户头像不能为空")
    private String userAvatar;

    /**
     * 用户工号
     */
    @NotNull(groups = RegisterGroup .class,message = "用户工号不能为空")
    private Long jobNumber;

    /**
     * 职位id
     */
    @NotNull(groups = RegisterGroup .class,message = "职位id不能为空")
    private Long positionId;

    /**
     * 部门id
     */
    @NotNull(groups = RegisterGroup .class,message = "部门id不能为空")
    private Long departmentId;

    /**
     * 手机号码
     */
    @NotBlank(groups = RegisterGroup .class,message = "手机号码不能为空")
    @Size(groups = {RegisterGroup .class,LoginGroup.class},min = 11, max = 11, message = "手机号码长度不正确")
    @Pattern(groups = {RegisterGroup .class,LoginGroup.class},regexp = "^(((13[0-9])|(14[579])|(15([0-3]|[5-9]))|(16[6])|(17[0135678])|(18[0-9])|(19[89]))\\d{8})$", message = "手机号格式错误")
    private String userPhone;

    /**
     * 邮箱
     */
    @NotBlank(groups = RegisterGroup .class,message = "邮箱不能为空")
    @Email(groups = {RegisterGroup .class,LoginGroup.class},message = "邮箱格式不正确")
    private String userEmail;

    /**
     * 是否为管理者 0==管理者 1==员工
     */
    @NotNull(groups = RegisterGroup .class,message = "用户属性不能为空")
    private Integer isManager;

    /**
     * 创建人
     */
    @NotNull(groups = RegisterGroup .class,message = "创建者不能为空")
    private String creater;

    /**
     * 登陆方式
     */
    @NotNull(groups = LoginGroup .class,message = "出错啦,登陆方式不能为空")
    private Integer loginType;

    //定义接口,用于指明在什么情况下,使用对应的验证规则
    public interface RegisterGroup {
    }

    public interface LoginGroup {
    }
}

这样,当参数不满足条件时,就会抛出MethodArgumentNotValidException异常。我们接下来需要写一个全局异常处理来捕获异常

全局异常处理

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 方法参数错误异常
     * @param e
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResultObjectModel MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        log.error("方法参数错误异常");
        List<String> list=new ArrayList<>();        // 从异常对象中拿到ObjectError对象
        if (!e.getBindingResult().getAllErrors().isEmpty()){
            for(ObjectError error:e.getBindingResult().getAllErrors()){
                list.add(Objects.requireNonNull(error.getDefaultMessage()));
            }
        }
        // 然后提取错误提示信息进行返回
        return ResultObjectModel.fail(ResultCodeEnums.VALIDATE_FAILED.getMsg(),ResultCodeEnums.VALIDATE_FAILED.getCode(),list);
    }
}

我们用postman请求下接口看下效果,可以看到我们捕获到了MethodArgumentNotValidException异常,并将它的message放到了返回数据的data里。
注册参数校验失败.png

登陆服务

登陆服务的实现比较简单,我这边直接贴上service部分的实现给大家看下

public UserLoginResponse userLogin(UserInfoRequest userInfoRequest) {
        String loginName = userInfoRequest.getLoginName();
        String userPassword = userInfoRequest.getUserPassword();
        UserLoginResponse userLoginResponse = new UserLoginResponse();
        if (userInfoRequest.getLoginType().equals(LoginTypeEnums.USER_PASSWORD_LOGIN.getCode())) {
            if (!queryUserIsExist(loginName)) {
                throw new CustomParameterException(ResultCodeEnums.USER_NOT_EXIST);
            } else {
                //查询是否有重名的登录名
                MorseUserPoExample morseUserPoExample = new MorseUserPoExample();
                morseUserPoExample.or().andLoginNameEqualTo(loginName).andStatusEqualTo(0);
                List<MorseUserPo> selectUserInfo = morseUserDao.selectByExample(morseUserPoExample);

                if (userPassword.equals(selectUserInfo.get(0).getUserPassword())) {
                    long userId = selectUserInfo.get(0).getId();
                    String userName = selectUserInfo.get(0).getUserName();
                    String token = JWTUtil.sign(userName, userId);
                    userLoginResponse.setStatus("登陆成功");
                    userLoginResponse.setToken(token);
                    return userLoginResponse;
                } else {
                    throw new CustomParameterException(ResultCodeEnums.USER_LOGIN_FAIL);
                }
            }
        }
        return userLoginResponse;
    }

JWT

可以看到,我在登陆服务的实现里还用到了jwt,这又是什么呢?
我们看下官方解释:

  • JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于在各方之间作为JSON对象安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。JWTS可以使用秘密(使用HMAC算法)或公钥/私钥对使用RSA或ECDSA来签名。
  • 虽然JWTS可以加密,但也提供保密各方之间,我们将重点放在签名令牌。签名的令牌可以验证包含在其中的声明的完整性,而加密的令牌隐藏这些声明以防其他各方。当令牌使用公钥/私钥对签名时,签名也证明只有持有私钥的方才是签名的方。

我们引入jwt依赖后,编写工具类:

public class JWTUtil {
    /**
     * 过期时间为60分钟
     */
    private static final long EXPIRE_TIME = 60*60*1000;

    /**
     * token私钥
     */
    private static final String TOKEN_SECRET = "morse-api";

    /**
     * 生成签名,1分钟后过期
     * @param loginName
     * @param userId
     * @return
     */
    public static String sign(String loginName,Long userId){
        //过期时间
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        //私钥及加密算法
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        //设置头信息
        HashMap<String, Object> header = new HashMap<>(2);
        header.put("typ", "JWT");
        header.put("alg", "HS256");
        //附带username和userID生成签名
        return JWT.create()
                .withHeader(header)    //头信息
                .withClaim("userId",userId)   //自定义信息通过 withClaim 方法进行添加
                .withIssuer("morse.codecareer.cn")
                .withIssuedAt(new Date())  // 生成签名的时间
                .withAudience(loginName)
                .withExpiresAt(date)    //过期时间
                .sign(algorithm);       //签名私钥
    }


    public static boolean verity(String token){
        try {
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            JWTVerifier verifier = JWT.require(algorithm).withIssuer("morse.codecareer.cn").build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (IllegalArgumentException e) {
            throw new CustomParameterException(ResultCodeEnums.TOKEN_VERIFIER_FAIL);
        } catch (JWTVerificationException e) {
            throw new CustomParameterException(e.getMessage()+"请重新登陆",ResultCodeEnums.TOKEN_VERIFIER_FAIL.getCode());
        }

    }

    public static long tokenGetUserId(String token){
        try {
            long userId;
            userId = JWT.decode(token).getClaim("userId").asLong();
            return userId;
        }catch (Exception e){
            throw new CustomParameterException(ResultCodeEnums.TOKEN_VERIFIER_FAIL);
        }
    }

    public static String tokenGetLoginName(String token){
        try {
            String loginName;
            loginName = JWT.decode(token).getAudience().get(0);
            return loginName;
        }catch (Exception e){
            throw new CustomParameterException(ResultCodeEnums.TOKEN_VERIFIER_FAIL);
        }
    }

}

用户登陆后,我们返回token给用户。用户将这个token放入到header里请求之后的每个接口。
那么接下来我们需要写一个拦截器校验用户请求头里的token是否正确,是否未过期。

  • JWT拦截器
public class AuthenticationInterceptor implements HandlerInterceptor {
    @Autowired
    private MorseUserDao morseUserDao;

    @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();
        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        } else {
            // 从请求头中获取tocken
            String token = request.getHeader("Access-Token");
//        System.out.println(tocken);
            // 当没有获取到tocken时的处理
            if (token == null) {
                throw new CustomParameterException(ResultCodeEnums.USER_NOT_LOGIN);
            } else {
                try {
                    // tocken还未过期时的处理
                    // 获取声明部分
                    String loginName = JWTUtil.tokenGetLoginName(token);
                    Long userId=JWTUtil.tokenGetUserId(token);
                    //查询用户
                    MorseUserPoExample morseUserPoExample = new MorseUserPoExample();
                    morseUserPoExample.or().andLoginNameEqualTo(loginName).andStatusEqualTo(0);
                    List<MorseUserPo> selectUserInfo = morseUserDao.selectByExample(morseUserPoExample);

                    if (selectUserInfo.size() == 1) {
                        // 验证 token
                        boolean jwtVerifier = JWTUtil.verity(token);
                        if (jwtVerifier){
                            request.setAttribute("userId",userId);
                            return true;
                        }
                    } else if (selectUserInfo.size() == 0) {
                        throw new CustomParameterException(ResultCodeEnums.USER_NOT_EXIST);
                    } else {
                        throw new CustomParameterException(ResultCodeEnums.ERROR);
                    }
                } catch (JWTVerificationException e) {
                    // tocken过期时的处理
                    throw new CustomParameterException(e.getMessage()+"请重新登陆", ResultCodeEnums.TOKEN_VERIFIER_FAIL.getCode());
                }
            }
        }
        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 {
        System.out.println("处理器完成后的方法,此时说明控制器已经执行完毕。");
    }

}

验证完token没有问题后,我们会向request中的attribute中塞入解析后的user_id。我们后端服务通过这个id去操作对应用户的数据。

前端联调

直接放图吧,哈哈哈,太困了,敷衍一下。后端返回成功后弹出“登陆成功”的toast。
登陆截图.png

Q.E.D.





莫道君行早,更有早行人。