验证身份证号注解

注解

​ IDCardValid.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.sc.springboot.domain;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

// 注解可以放在xx上
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
// java文档
@Documented
//什么范围生效
@Retention(RetentionPolicy.RUNTIME)
//约束类
@Constraint(
validatedBy = IDCardValidator.class
)
public @interface IDCardValid {
String message() default "身份证号码不正确";

Class<?>[] groups() default {

};

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

};
}

约束类

IDCardValidator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package com.sc.springboot.domain;


import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class IDCardValidator implements ConstraintValidator<IDCardValid, String> {

/**
* 位权值数组
*/
private static int[] weightCode = new int[17];

/**
* 身份证校验码(末尾)
* 用来比对计算的余数
*/
private static final String[] CHECK_CODE = {"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"};

/**
* 除数11(身份证算法求余关键值)
*/
private static final int DIVIDER = 11;

/**
* 身份证前6位占的字符数(地区6位)
*/
private static final int AREA_NUMBER = 6;

/**
* 新身份证年份标志(比如:1989年取19)
* 旧身份证号与新身份证号的区别是:新的去掉年份和最后一位就是旧的证件号
*/
private static final String YEAR_PREFIX = "19";

private static final int OLD_ID_CARD_LENGTH = 15;

private static final int NEW_ID_CARD_LENGTH = 18;

/**
* description :获取位权值数组
*
* @param
* @return void
* @author susu
*/
private static void setWiBuffer() {
for (int i = 0; i < weightCode.length; i++) {
int k = (int) Math.pow(2, (weightCode.length - i));
weightCode[i] = (k % DIVIDER);
}
}


/**
* description :2.校验身份证长度 和 出生日期 和正则校验
*
* @param idCard
* @return boolean
* @author susu
*/
private static boolean checkLengthAndBirthday(final String idCard) {
if ((idCard.length() == OLD_ID_CARD_LENGTH) || (idCard.length() == NEW_ID_CARD_LENGTH)) {
return regexCheckAndCheckBirthday(idCard);
}
return false;
}

/**
* description :3.校验身份证中的日期是否合法(含正则校验)
*
* @param idCard
* @return boolean
* @author susu
*/
private static boolean regexCheckAndCheckBirthday(final String idCard) {
String birthday = "";
/**
* 1. 加一层正则校验
* 2. 获取证件号的出生日期的字符串:格式如:20221121
*/
if (idCard.length() == OLD_ID_CARD_LENGTH) {
//15位的身份证号没有校验码,所以最好用正则校验一下
if (idCard15RegexCheck(idCard)) {
birthday = YEAR_PREFIX + idCard.substring(AREA_NUMBER, AREA_NUMBER + 6);
}
} else {
//18位的不用正则校验也行
if (idCard18RegexCheck(idCard)) {
birthday = idCard.substring(AREA_NUMBER, AREA_NUMBER + 8);
}
}
return checkStrDate(birthday);
}

/**
* description :最后,校验身份证最后一位检验码是否正确
*
* @param idCard
* @return boolean
* @author susu
*/
public static boolean checkIdCard18(final String idCard) {
//1.获取余数
int dividedResult = getDividedResult(idCard);
//2.根据余数获取对应的身份证校验码
String code = CHECK_CODE[dividedResult];
//3.获取身份证的最后一位(第18位),然后校验
String lastStr = idCard.substring(idCard.length() - 1);
if (code.equals(lastStr)) {
return true;
}
return false;
}

/**
* description :根据前17位加权求和 获取余数
*
* @param idCard
* @return int
* @author susu
*/
public static int getDividedResult(String idCard) {
//先获取前17位数
String[] idCardNum = idCard.substring(0, 17).split("");
int sum = 0;
for (int i = 0; i < idCardNum.length; i++) {
sum += Integer.parseInt(idCardNum[i]) * weightCode[i];
}
return sum % DIVIDER;
}

/*
* "\\d{8}" 1~6位分别代表省市县,只校验是否数字。
* 7~8位代表年份后两位数字
* "(0[1-9]|1[012])" 9~10位代表月份,01~12月
* "(0[1-9]|[12]\\d|3[01])" 11~12位代表日期,1~31日
* "\\d{3}" 13~15位为三位顺序号
*/
private static boolean idCard15RegexCheck(String idCard) {
String reg = "^(\\d{8}(0[1-9]|1[012])(0[1-9]|[12]\\d|3[01])\\d{3})$";
Pattern pattern = Pattern.compile(reg);
Matcher m = pattern.matcher(idCard);
return (m.matches()) ? true : false;
}

/*
* "\\d{6}" 1~6位分别代表省市县,只校验是否数字。
* "(18|19|20)\\d{2}" 7~10位代表年份,先管18,19,20,下个世纪的让下个世纪的人去校验
* "(0[1-9]|1[012])" 11~12位代表月份,01~12月
* "(0[1-9]|[12]\\d|3[01])" 13~14位代表日期,1~31日
* "\\d{3}" 15~17位为三位顺序号
* "(\\d|X|x)" 18位为校验位数字,允许字母x和X
*/
private static boolean idCard18RegexCheck(String idCard) {
String reg = "^(\\d{6}(18|19|20)\\d{2}(0[1-9]|1[012])(0[1-9]|[12]\\d|3[01])\\d{3}(\\d|X|x))$";
Pattern pattern = Pattern.compile(reg);
Matcher m = pattern.matcher(idCard);
return (m.matches()) ? true : false;
}

/**
* description :校验8位日期是否合法(比如:20021122)
*
* @param strDate
* @return boolean
* @author susu
*/
private static boolean checkStrDate(final String strDate) {
try {
LocalDate.parse(strDate, DateTimeFormatter.ofPattern("yyyyMMdd"));
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

@Override
public void initialize(IDCardValid constraintAnnotation) {
//1. 初始化位权数值
setWiBuffer();
}


@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (checkLengthAndBirthday(value)) {
return value.length() == OLD_ID_CARD_LENGTH || checkIdCard18(value);
}
return false;
}
}

ConstraintValidatorContext 类 介绍:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* ConstraintValidatorContext 提供了以下功能:
* 错误消息:通过 ConstraintValidatorContext 可以添加自定义的错误消息,将验证失败的详细信息报告给调用方。
* 例如,可以使用 context.buildConstraintViolationWithTemplate("自定义错误消息").addConstraintViolation() 方法,
* 在验证失败时添加一个自定义的错误消息。
* 禁用默认错误消息:可以使用 context.disableDefaultConstraintViolation() 方法禁用默认的错误消息,
* 从而完全控制错误消息的生成和显示。
* 属性节点路径:ConstraintValidatorContext 可以跟踪验证过程中的属性节点路径,以便在错误消息中可以包含属性的层级关系。
* 例如,可以使用 context.buildConstraintViolationWithTemplate("属性A的值无效")
* .addPropertyNode("A").addConstraintViolation() 方法,在错误消息中指定属性 A 的路径和错误信息。
* 违反的约束:通过 context.buildConstraintViolationWithTemplate() 方法,可以指定正在验证的约束(注解)及其对应的错误消息。
*/

验证是否有效

​ IDCardValidatorTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.sc.springboot;

import javax.validation.*;

import com.sc.springboot.domain.IDCardValid;
import lombok.extern.slf4j.Slf4j;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.Set;

import static org.junit.Assert.*;

@Slf4j
public class IDCardValidatorTest {
private static Validator validator;

@BeforeClass
public static void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}

private static class TestObject {
@IDCardValid
private String idCardNumber;

// Getter and setter methods
public String getIdCardNumber() {
return idCardNumber;
}

public void setIdCardNumber(String idCardNumber) {
this.idCardNumber = idCardNumber;
}
}

@Test
public void testValidIDCardNumber() {
TestObject testObject = new TestObject();
testObject.setIdCardNumber("520201197209083216"); // 有效身份证号码

//这个vilations集合中包含的是错误的bean的信息
Set<ConstraintViolation<TestObject>> violations = validator.validate(testObject);
assertEquals(0, violations.size());

}

@Test
public void testInvalidIDCardNumber() {
TestObject testObject = new TestObject();
testObject.setIdCardNumber("36102120001107201"); // 无效身份证号码
// testObject.setIdCardNumber("520201197209083216"); // 有效身份证号码

Set<ConstraintViolation<TestObject>> violations = validator.validate(testObject);
assertEquals(1, violations.size());
ConstraintViolation<TestObject> violation = violations.iterator().next();
log.error(violation.getMessage());
log.error(violation.getPropertyPath().toString());
}
}

assertEquals方法介绍:

assertEquals 是一种断言方法,通常用于单元测试中。它用于验证实际值和期望值是否相等。

在测试中,您可以使用 assertEquals 来比较两个值是否相等。如果实际值和期望值不相等,该断言将会失败,并会生成一个错误报告。

assertEquals 方法通常具有以下形式:

1
assertEquals(expected, actual);

其中:

  • expected 是期望的值,即我们希望得到的结果。
  • actual 是实际的值,即我们需要验证的实际结果。

如果 expectedactual 相等,那么断言成功,测试继续执行;如果它们不相等,断言失败,测试将停止并抛出错误。

Violations介绍

1
Set<ConstraintViolation<TestObject>> violations = validator.validate(testObject);

这段代码是使用JSR 303/349标准定义的Bean验证(Bean Validation)框架来验证一个名为testObject的对象。具体来说,它包含以下几个步骤:

  1. validator.validate(testObject):通过调用validate方法来执行验证操作。validator是已经初始化好的验证器对象,testObject是要验证的目标对象。
  2. Set<ConstraintViolation<TestObject>> violationsvalidate方法返回一个Set集合,其中包含了所有验证失败的结果。ConstraintViolation是验证失败的详细信息对象,它包含了验证失败的字段、对应的值、以及验证失败的原因等信息。

通过对violations进行处理,你可以获取验证失败的详细信息,并根据需要进行相应的处理,例如输出错误消息、记录日志等。

需要注意的是,在使用这段代码之前,你需要确保已经初始化了validator对象,并且已经将相关的验证约束(如注解)添加到了TestObject类的相应字段或方法上,以便进行验证。