一 、设计流程图
二、引入所需要的依赖
这里的注入依赖可能不全
redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
hutool
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>
json解析
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
mybaits-plus
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
三、配置数据库
向数据库添加属性
四、配置Redis连接
在application.yml中添加Redis的连接信息
spring:
redis:
host: 192.168.150.101
port: 6379
password: 123
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 100
五、搭好三层框架
Controller层
@RestController
@Slf4j
public class PhoneEnrollController {
}
Service层
public interface SendStudentPhoneService extends IService<Student> {
}
@Service
public class SendStudentPhoneServiceImpl extends ServiceImpl<SendStudentPhoneMapper, Student> implements SendStudentPhoneService {
}
Mapper层
@Mapper
public interface SendStudentPhoneMapper extends BaseMapper<Student> {
}
六、编写短信验证码发送信息代码
@RestController
@Slf4j
@RequiredArgsConstructor
public class PhoneEnrollController {
private final SendStudentPhoneServiceImpl sendStudentPhoneService;
@PostMapping("/student/phone")
public Result SendCode(String phone){
return sendStudentPhoneService.sendCode(phone);
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class SendStudentPhoneServiceImpl extends ServiceImpl<SendStudentPhoneMapper, Student> implements SendStudentPhoneService {
private final StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone) {
//判断手机是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.error("手机格式不合法");
}
//生成随机验证码
String code = RandomUtil.randomNumbers(6);
//保存到Redis中
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_SSL, TimeUnit.MINUTES);
//发送验证码
log.info("发送验证码成功,{}" ,code);
return Result.success();
}
}
这里先采用log来代替发送信息
其中采用的手机格式是否合法是采用的RedexPatterns和RedexUtils
import cn.hutool.core.util.StrUtil;
/**
* @author 虎哥
*/
public class RegexUtils {
/**
* 是否是无效手机格式
* @param phone 要校验的手机号
* @return true:符合,false:不符合
*/
public static boolean isPhoneInvalid(String phone){
return mismatch(phone, RegexPatterns.PHONE_REGEX);
}
/**
* 是否是无效邮箱格式
* @param email 要校验的邮箱
* @return true:符合,false:不符合
*/
public static boolean isEmailInvalid(String email){
return mismatch(email, RegexPatterns.EMAIL_REGEX);
}
/**
* 是否是无效验证码格式
* @param code 要校验的验证码
* @return true:符合,false:不符合
*/
public static boolean isCodeInvalid(String code){
return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
}
// 校验是否不符合正则格式
private static boolean mismatch(String str, String regex){
if (StrUtil.isBlank(str)) {
return true;
}
return !str.matches(regex);
}
}
/**
* @author 虎哥
*/
public abstract class RegexPatterns {
/**
* 手机号正则
*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
/**
* 邮箱正则
*/
public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
/**
* 密码正则。4~32位的字母、数字、下划线
*/
public static final String PASSWORD_REGEX = "^\\w{4,32}$";
/**
* 验证码正则, 6位数字或字母
*/
public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";
}
在Redis中可以查看到code信息
七、编写验证码登录注册的代码
public Result login(String phone, String code) {
//判断手机号是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.error("手机格式不合法");
}
//从Redis中获取code对比
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if (cacheCode == null || !cacheCode.equals(code)) {
return Result.error("验证码错误");
}
//从数据库中搜索用户,判断用户是否注册过
Student student = query().eq("phone", phone).one();
if(student == null){
//如果不存在就保存到数据中
Student stu = new Student();
stu.setUsername(RandomUtil.randomString(10));
stu.setName("***");
stu.setIsVaild((short) 1);
stu.setPhone(phone);
stu.setCreateTime(LocalDateTime.now());
stu.setUpdateTime(LocalDateTime.now());
save(stu);
//生成Jwt令牌作token
try {
jwt = makeJwt(stu);
}catch (Exception e)
{
System.out.println(e);
}
}
try {
jwt = makeJwt(student);
}catch (Exception e)
{
System.out.println(e);
}
return Result.success(jwt);
}
private String makeJwt(Student student) throws JsonProcessingException {
Map<String,Object> claims = new HashMap<>();
claims.put("id",student.getId());
claims.put("username",student.getUsername());
claims.put("name",student.getName());
String jwt = JwtUtils.generateJwt(claims);
//json数据 不能传递时间信息所以要拷贝到一个 新的对象
//将对象转为json
String json = mapper.writeValueAsString(BeanUtil.copyProperties(student,People.class));
//存储到Redis中
stringRedisTemplate.opsForValue().set(LOGIN_TOKEN_KEY+jwt,json,LOGIN_TOKEN_SSL,TimeUnit.MINUTES);
return jwt;
}
这里使用了JwtUtils工具类来生成JWT令牌,这里不建议使用JWT令牌了,因为redis的token会改变导致多次登录有多个key 的情况,这里建议使用一个简单的唯一的随机生成
public class JwtUtils {
private static String signKey = "mmznb";
private static Long expire = 43200000L;//12h
/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
可以看到成功的信息
八、编写拦截器的代码
这里采用的是两层拦截器
一层是刷新Redis的TTL
一层是对没有token的进行拦截
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (GetTokenId.getUserId() == null) {
response.setStatus(401);
return false;
}
return true;
}
@Slf4j
@Component
@RequiredArgsConstructor
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor (StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURL().toString();
log.info("请求的url:{}", url);
//获取Token中jwt
String jwt = request.getHeader("token");
//从redis中找到相应的键值对
String Key = LOGIN_TOKEN_KEY+jwt;
String jsonUser = stringRedisTemplate.opsForValue().get(Key);
//如果为空就放行
if (jsonUser == null) {
return true;
}
People people = mapper.readValue(jsonUser,People.class);
GetTokenId.setUserId(people.getId());
//不为空就刷新有效期
stringRedisTemplate.expire(Key,LOGIN_CODE_SSL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
GetTokenId.shutdown();
}
}
在WebConfig中注册
@Configuration//配置类
public class WebConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/*/login","/*/activation"
,"/*/create","/*/sendEmail","/*/updatePassword"
,"/*/password","/*/findNewId","/*/getCarousel"
,"/*/newDetail","/*/phone","/*/loginPhone").order(1);
registry.addInterceptor( new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
九、编写发送短信的功能
这里采用的是阿里云的短信功能
搜索短信服务,在发送测试中绑定测试手机号
注册一个RAM访问控制
在国内消息中的资质管理审核一下
然后是注入依赖
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>3.0.0</version>
</dependency>
这里我不想再系统环境中配置所以就在yml中配置了
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.phone")
public class AliPhoneProperties {
private String endpoint;
private String signName;
private String accessKeyId;
private String accessKeySecret;
private String templateCode;
}
需要在yml文件中配置上面几个东东
@Slf4j
@Component
public class PhoneUtils {
@Autowired
private AliPhoneProperties aliPhoneProperties;
public com.aliyun.dysmsapi20170525.Client createClient(String AccessKeyId,String AccessKeySecret,String endpoint) throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
.setAccessKeyId(AccessKeyId)
.setAccessKeySecret(AccessKeySecret);
config.endpoint = endpoint;
return new com.aliyun.dysmsapi20170525.Client(config);
}
public void sendPhone(String phone,String code) throws Exception {
com.aliyun.dysmsapi20170525.Client client = new PhoneUtils().createClient(aliPhoneProperties.getAccessKeyId(), aliPhoneProperties.getAccessKeySecret(), aliPhoneProperties.getEndpoint());
com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest()
.setSignName(aliPhoneProperties.getSignName())
.setTemplateCode(aliPhoneProperties.getTemplateCode())
.setPhoneNumbers(phone)
.setTemplateParam(String.format("{\"code\":\"%s\"}", code));
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
try {
// 复制代码运行请自行打印 API 的返回值
client.sendSmsWithOptions(sendSmsRequest, runtime);
} catch (TeaException error) {
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
System.out.println(error.getMessage());
// 诊断地址
System.out.println(error.getData().get("Recommend"));
com.aliyun.teautil.Common.assertAsString(error.message);
} catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error);
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
System.out.println(error.getMessage());
// 诊断地址
System.out.println(error.getData().get("Recommend"));
com.aliyun.teautil.Common.assertAsString(error.message);
}
}
}
这里是从阿里云帮助文档中修改后的工具类
最后修改log.info发送验证码 为
phoneUtils.sendPhone(phone,code);
别忘了还要注入phoneUtils
private final PhoneUtils phoneUtils;