目录
一、什么是MD5
全称为 消息摘要算法版本5 (Message Digest Algorithm 5)
它是一种Hash算法,作用是为了信息安全
简单来说,你可以理解MD5值就是一串128bit的数据
特点
不可逆性 | 根据 MD5 值计算不出原始数据 |
唯一性 | 不同的原始数据计算出不同的MD5值 |
一个MD5理论上的确是可能对应无数多个原文的,因为MD5是有限多个的而原文可以是无数多个。
性质
压缩性 | 任意长度的数据,算出的MD5值长度都是固定的(相当于超损压缩) |
容易计算 | 从原数据计算出MD5值很容易。 |
抗修改性 | 对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别 |
抗碰撞 | 已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。 |
强抗碰撞 | 想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的 |
注:虽说MD5有不可逆的特点
但是由于某些MD5破解网站,专门用来查询MD5码,其通过把常用的密码先MD5处理,并将数据存储起来,然后跟需要查询的MD5结果匹配,这时就有可能通过匹配的MD5得到明文,所以有些简单的MD5码是反查到加密前原文的。
为了提高安全性,可以进行加盐处理,这样每次生成的值都不一样
可以直接添加使用源码,也可以引入依赖使用别人的方法
public class Md5Util {
/**
* 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
*/
protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
protected static MessageDigest messagedigest = null;
static {
try {
messagedigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsaex) {
System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
nsaex.printStackTrace();
}
}
/**
* 生成字符串的md5校验值
*
* @param s
* @return
*/
public static String getMD5String(String s) {
return getMD5String(s.getBytes());
}
/**
* 判断字符串的md5校验码是否与一个已知的md5码相匹配
*
* @param password 要校验的字符串
* @param md5PwdStr 已知的md5校验码
* @return
*/
public static boolean checkPassword(String password, String md5PwdStr) {
String s = getMD5String(password);
return s.equals(md5PwdStr);
}
public static String getMD5String(byte[] bytes) {
messagedigest.update(bytes);
return bufferToHex(messagedigest.digest());
}
private static String bufferToHex(byte bytes[]) {
return bufferToHex(bytes, 0, bytes.length);
}
private static String bufferToHex(byte bytes[], int m, int n) {
StringBuffer stringbuffer = new StringBuffer(2 * n);
int k = m + n;
for (int l = m; l < k; l++) {
appendHexPair(bytes[l], stringbuffer);
}
return stringbuffer.toString();
}
private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
// 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
stringbuffer.append(c0);
stringbuffer.append(c1);
}
}
springboot中使用
引入依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
在启动类中配置一个bean
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
我们简单设计一个新增用户的接口,并对密码进行MD5加密处理
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping
//前端传来的user对象是一个json格式
public Result<?> addUser(@RequestBody User user){
user.setPassword(passwordEncoder.encode(user.getPassword()));
//这个方法已经进行了加盐处理,相同的初始数据生成的值不一样
userService.save(user);
return Result.success("新增用户成功");
}
}
现在我们在前端或是postman中创建两个密码相同的用户,然后到数据库中查看
密码同样是123456,但是生成的密码却不一样
我们再来设计对应的登录接口
@Override
public Map<String, Object> login(User user) {
//根据用户名与密码查询结果
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername,user.getUsername());
// wrapper.eq(User::getPassword,user.getPassword()); 引入了MD5加密,就不能直接比对密码了
User loginUser = this.baseMapper.selectOne(wrapper);
//若结果不为空,并且密码和传入密码匹配则生成token,并将用户信息存入redis
if (loginUser!=null && passwordEncoder.matches(user.getPassword(),loginUser.getPassword())){
//暂时用UUID,终极方案应为jwt
String key = "user:" + UUID.randomUUID();
//存入redis
loginUser.setPassword(null); //密码不要存进去
//默认这是永久有效的,要设置timeout时间
redisTemplate.opsForValue().set(key,loginUser,30, TimeUnit.MINUTES);
//返回数据
Map<String,Object> data = new HashMap<>();
data.put("token",key);
return data;
}
return null;
}
成功登录
二、Spring Validation
Spring提供了一个参数校验框架,使用预定义的注解完成参数校验,叫做Spring Validation。
使用步骤:
1、引入Spring Validation起步依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、在需要被校验的参数前面添加@Pattern
@Patten(regexp = "^正则表达式$")
@Validated
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register") //校验
public Result register(@Pattern(regexp = "^\\S{5,16}$") String username,
@Pattern(regexp = "^\\S{5,16}$") String password) {
//查询用户
User user = userService.findByUserName(username);
if (user == null){
//注册
userService.register(username,password);
return Result.success();
}else {
//占用
return Result.error("用户名以及被占用");
}
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e){
e.printStackTrace();
//有的异常可能没有message,所以要先用三元运算符判断一下
return Result.error(StringUtils
.hasLength(e.getMessage()) ? e.getMessage() : "操作失败");
}
}
3、在Controller类上添加@Validated
验证
分组校验
把校验项进行归类分组,在完成不同的功能时,校验指定组中的验项
比如这个实体类需要被分组校验
@Data
public class Category {
private Integer id;//主键ID
@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
@JsonFormat(pattern = "yyyy-MM-DD HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-DD HH:mm:ss")
private LocalDateTime updateTime;//更新时间
}
步骤
1、定义分组
//在实体类底下添加分组
public interface Add{
}
public interface Update{
}
2、定义校验项时指定归属的分组
@Data
public class Category {
@NotNull(groups = Update.class)
private Integer id;//主键ID
@NotNull(groups ={ Add.class,Update.class })
@NotEmpty
private String categoryName;//分类名称
@NotNull(groups ={ Add.class,Update.class })
@NotEmpty
private String categoryAlias;//分类别名
public interface Add{
}
public interface Update{
}
}
id被分到update组,name和alias被分到add组和update组,这样只有在update的时候才会校验id,而在add的时候就不校验id
3、校验时要指定的分组
在@Validated(分组.class)
@PostMapping
public Result category(@RequestBody @Validated(Category.Add.class) Category category){
String categoryName = category.getCategoryName();
if (StringUtils.hasLength(categoryService.selectByName(categoryName))){
return Result.error("分类重复");
}
categoryService.category(category);
return Result.success();
}
@PutMapping()
public Result updateCategory(@RequestBody @Validated(Category.Update.class) Category category){
categoryService.updateCategory(category);
return Result.success();
}
默认分组
注:
如果某个校验项中没有指定分组,默认属于Default分组
分组之间可以继承,A extent B,那么A组就有B组的所有校验项
Add extend 默认,那么add就是default分组了;可以有多个分组继承default,那么它们都是default了
因此刚才实体类的代码也可以这么写
@Data
public class Category {
@NotNull(groups = Update.class)
private Integer id;//主键ID
@NotNull()
@NotEmpty
private String categoryName;//分类名称
@NotNull()
@NotEmpty
private String categoryAlias;//分类别名
public interface Add extends Default{
}
public interface Update extends Default{
}
}
@Notnull中什么也不写那就默认是default,但是此时add与update都继承了default,所以其实也相当于上面的一样
自定义校验
参数校验的注解很强大,但是已经自带的注解有时候不能满足我们的需求
比如:要求某个属性的值必须是 1 或者 0
这种需求就没有现成的注解可以满足我们的需求,因此我们就需要自己手动创建一个校验注解
打开@NotNull注解查看源码,我们直接模仿它来完成我们的注解
注解也直接拿过来,其中
@Documented:元注解,标识被作用到注解是可以被抽取到帮助文档中
@Target:元注解,标识被作用的注解可以被用在哪里,方法、属性等
@Retention:元注解,标识被作用的注解在那个阶段会被保留,如编译阶段、运行阶段等
@Constraint:标识谁来给这个注解来提供校验的规则
其中属性已经定好了,我们也直接拿过来,其中
String message default:提供校验失败后的提示信息
Class<?>[ ] groups() default { }:指定分组
Class<? extends Payload>[] payload() default { }:负载,获取到state注解的附加信息
步骤1、创建注解类
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = { StateValidation.class }
)
public @interface State {
String message() default "{jakarta.validation.constraints.NotNull.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
步骤二、创建实现类
创建一个实现类,要继承ConstraintValidator(<哪一个注解,作用的字段的类型>)
public class StateValidation implements ConstraintValidator<State,String> {
/**
*
* @param s 将来要校验的数据
* @param constraintValidatorContext
* @return false则不通过,true则通过
*/
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
//提供校验规则
if (s == null){
return false;
}
if (s.equals("已发布") || s.equals("草稿")){
return true;
}
return false;
}
}
3、使用
这样我们自定义的注解就可以生效了