基本项目配置
IDEA搭建Spring Boot + Vue 前后端分离项目
# 基本结构
myWebSite:
backend:
frontend:
并结合 Spring Security 实现基本用户鉴权
本项目包地址使用默认配置 com.example
为根目录
*Spring Security 基本配置
项目除Spring Boot
的yaml
配置,配置类均放于根目录的config下
,Security
配置类为SecurityConfiguration
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.authorizeHttpRequests(conf -> conf
.requestMatchers("/api/auth/**").permitAll() //放行该url下的api请求
.anyRequest().authenticated() //除放行请求外均要通过验证
)
.formLogin(conf -> conf
.loginProcessingUrl("/api/auth/login") //配置登录接口url
.successHandler(this::onAuthenticationSuccess) //handler处理登录结果的响应
.failureHandler(this::onAuthenticationFailure)
)
.logout(conf -> conf
.logoutUrl("/api/auth/logout") //配置注销接口url同理
.logoutSuccessHandler(this::onLogoutSuccess)
)
.csrf(AbstractHttpConfigurer::disable) //取消csrf保护() 因为这里是自定义JWT令牌授权
.sessionManagement(conf -> conf
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
) //JWT使用无状态,security不对session处理
.build();
}
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(RestBean.success().toJsonString());
}
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(RestBean.failure(401, exception.getMessage()).toJsonString());
}
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.getWriter().write("Logout Success");
}
}
以json类型
封装了接口对前端request
响应返回数据,封装类为实体类RestBean
,所有实体类位于根目录entity
下
//这里封装为模板可以将任何数据直接当参数传入函数就可以以Json的格式返回给前端
public record RestBean<T>(int code, T data, String message) {
public static <T> RestBean<T> success(T data) {
return new RestBean<>(200, data, "请求成功");
}
public static <T> RestBean<T> success() {
return success(null);
}
public static <T> RestBean<T> failure(int code, String message) {
return new RestBean<>(code, null, message);
}
//封装一个方法 转化为json 使用alibaba的fastJson2
public String toJsonString() {
return JSONObject.toJSONString(this, JSONWriter.Feature.WriteNulls); //writeNulls指对空数据也会返回json 避免出问题
}
}
其中使用的字符串转化为Json
的转换工具为fastJson2
,Maven
依赖坐标如下
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
接口测试如下:
成功登录
登录失败
Jwt 权限校验
JWT 为 Java Web Token,用于作为Json对象在网络上安全地传递信息。
对于Web开发,可以很好地用在Authorization(授权)并实现单点登录。
JWT 结构为 Header.Payload.Signature
Header:
典型地由两部分组成:Token类型 + 算法名称
{
'alg': "HS256",
'typ': "JWT"
}
Payload:
这部分包含声明,里面可以存放传输所需的一些信息
{
"sub": '123123123',
"name": 'codezijun',
"remember": true
}
Signature:
这部分是由私钥生成的签名,保证JWT唯一且未被更改
Token
是无状态的,只由服务端赋予客户端,然后之后都由客户端在Header
中携带JWT
返回由服务端校验
发布Jwt令牌
创建Jwt令牌
Java Jwt
框架依赖坐标
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.3.0</version>
</dependency>
既然要发布Jwt令牌
,就要先有Jwt令牌
,逻辑的第一步也就时创建Jwt令牌
。
为了实现单点登录的需求,且为了方便前端校验数据,我们需要将必要的信息写入Token
中,并且设定过期时间等,最后再进行签名完成Jwt令牌
的创建。
//这里在根目录创建工具模块utils
//实现Jwt工具类 JwtUtils
@Component
public class JwtUtils {
@Value("${spring.security.jwt.key}")
String key;
@Value("${spring.security.jwt.expire}")
int expire;
public String createJwt(UserDetails details, int id, String username) {
Algorithm algorithm = Algorithm.HMAC256(key);
Date expire = this.expireTime();
return JWT.create()
.withClaim("id", id)
.withClaim("name", username)
//将用户的权限列表作为声明添加到JWT中。这里使用`details.getAuthorities()`获取用户的权限信息,然后将权限名称提取出来并转换为列表。
.withClaim("authorities", details.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList())
//过期时间
.withExpiresAt(expire)
//token颁发时间
.withIssuedAt(new Date())
//最后用算法签名
.sign(algorithm);
}
//计算过期的目标时间
public Date expireTime() {
Calendar calendar = Calendar.getInstance(); //获取当前时间
calendar.add(Calendar.HOUR, expire * 24); //加上expire * 24 小时后为过期时间
return calendar.getTime();
}
}
其中expire
和key
的配置均在application.yml
中
# main/resource/application.yml
spring:
security:
jwt:
key: coderzijunNB
expire: 3
颁发Jwt令牌
之前说过为了方便我们会向前端返回一些比较有用的信息(包括token
)在此我们创建如下文件结构:
#其中,数据库这一层的对象封装在dto,与前端进行数据交互的封装在vo,并进一步分为request和response两种
com.example:
entity:
dto:
vo:
request:
response:
我们要实现的是后端对前端的response
,也就是对用户信息登录的响应,封装为AuthorizeVO
如下:
// entity/vo/response/AuthorizeVO
// 其中包含token和过期时间expire
@Data
public class AuthorizeVO {
String username;
String role;
String token;
Date expire;
}
在之前在登录接口中对成功登录的响应中,我们只返回了成功信息,现在我们在其中加入Jwt令牌
的颁发。
//进一步完善SecurityConfiguration类下onAuthoriztionSuccess方法
//这里没有调用数据库先把部分信息写死
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
response.setContentType("application/json;charset=utf-8");
//获取用户详细信息 Security的User,这个User是security的User
User user = (User) authentication.getPrincipal();
//创建令牌
String token = jwtUtils.createJwt(user, 1, "小明");
AuthorizeVO vo = new AuthorizeVO();
//颁发令牌
vo.setExpire(jwtUtils.expireTime());
vo.setRole("");
vo.setToken(token);
vo.setUsername("小明");
response.getWriter().write(RestBean.success(vo).toJsonString());
}
Jwt请求头校验
要实现Jwt
请求头校验,需要在Security
实现的过滤器链中加入自定义的Jwt校验过滤器
,来实现Jwt
的检验解析
创建filter目录
存放所有的过滤器,创建JwtAuthorizeFilter
类,继承OncePerRequestFilter
,实现如下:
//这里创建的过滤器是为了在Security实现的过滤器链中加入我们自定义的过滤器 实现JWT的校验解析
@Component
public class JwtAuthorizeFilter extends OncePerRequestFilter {
@Resource
JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
//读取请求头中的校验参数 里面携带token(前缀为"Bearer "),并校验合法性
String authorization = request.getHeader("Authorization");
DecodedJWT jwt = jwtUtils.resolveJwt(authorization);
if(jwt != null) {
UserDetails user = jwtUtils.toUser(jwt);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
request.setAttribute("id", jwtUtils.toId(jwt));
}
//进行过滤器链的下一个节点
filterChain.doFilter(request, response);
}
}
//然后在之前实现的filterChain中注册该过滤器
@Resource
JwtAuthorizeFilter jwtAuthorizeFilter
.addFilterBefore(jwtAuthorizeFilter, UsernamePasswordAuthenticationFilter.class)
//处理无权限的响应
.exceptionHandling(conf -> conf //无权限
.authenticationEntryPoint(this::onUnauthorized)
.accessDeniedHandler(this::onAccessDeny)
)
//登录的但是没权限 403
public void onAccessDeny(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException exception) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(RestBean.forbidden(exception.getMessage()).toJsonString());
}
//未验证的情况处理
public void onUnauthorized(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(RestBean.unauthorized(exception.getMessage()).toJsonString());
}
并且在RestBean
中封装对应的响应返回
//401
public static <T> RestBean<T> unauthorized(String message) {
return failure(401, message);
}
//403
public static <T> RestBean<T> forbidden(String message) {
return failure(403, message);
}
完成请求头的校验
Jwt注销登录
在用户注销登录后,相应的应将token
丢弃,为了安全,应该将token
失效后再丢弃。这里配合Redis
实现黑名单注销token
\
Redis依赖坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
拉黑的逻辑比较简单,只需要向token
内写入一个id
,之后根据这个id
存入Redis
即完成拉黑
在工具包内创建Const
类存放常量
package com.example.utils;
//存储常量
public class Const {
public static final String JWT_BLACK_LIST = "jwt:blacklist:";
}
在JwtUtils
内写入以下逻辑
@Resource
StringRedisTemplate template;
//返回是否失效
public boolean invalidJwt(String headerToken) {
//无效token
String token = this.convertToken(headerToken);
if (token == null) return false;
Algorithm algorithm = Algorithm.HMAC256(key);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
try {
DecodedJWT jwt = jwtVerifier.verify(token);
String id = jwt.getId();
return deleteToken(id, jwt.getExpiresAt());
} catch (JWTVerificationException e) {
return false;
}
}
//拉黑token
private boolean deleteToken(String uuid, Date time) {
if(this.isInvalidToken(uuid))
return false;
Date now = new Date();
//计算过期剩余时间
long expire = Math.max(time.getTime() - now.getTime(), 0);
//存入Redis做到拉黑,后面参数指毫秒
template.opsForValue().set(Const.JWT_BLACK_LIST + uuid, "", expire, TimeUnit.MILLISECONDS);
return true;
}
//令牌是否失效
private boolean isInvalidToken(String uuid) {
return Boolean.TRUE.equals(template.hasKey(Const.JWT_BLACK_LIST + uuid));
}
//在之前的创建令牌方法中要加入JWTid作为存放Redis的依据
.withJWTId(UUID.randomUUID().toString())
最后在登出api
中实现逻辑
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
String authorization = request.getHeader("Authorization");
if(jwtUtils.invalidJwt(authorization)) {
writer.write(RestBean.success().toJsonString());
} else {
writer.write(RestBean.failure(400, "退出失败").toJsonString());
}
}
测试接口如下:
完成Jwt登录退出
数据库校验
取代Security
验证自带的用户,使用自己的数据库完成用户校验
mybatis-plus依赖坐标
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
*踩坑 Spring Boot version3.2.0使用 @Mapper 创建 mapper 和 实体类的映射报错导致项目启动失败,降低版本至3.1.2报错解决
创建DTO
实体类Account MyBatis-Plus
要注明联系表及主键
@Data
@TableName("db_account")
@AllArgsConstructor
public class Account {
@TableId(type = IdType.AUTO)
Integer id;
String username;
String password;
String email;
String role;
Date registerTime;
}
创建Mapper
映射,Mapper
映射均存放根目录Mapper
包内
//mapper/AccountMapper
//mybatisplus映射要继承基映射并指明类型
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
}
创建service
做数据库校验,service
服务均存放根目录service
包内,同时其实现类均存放service/impl
下,创建AccountService
//mybatisplus的基本配置实现接口需继承IService使用CRUD接口,继承UserDetailsService覆盖Security自带的检验规则
public interface AccountService extends IService<Account>, UserDetailsService {
Account findByNameOrEmail(String text);
}
实现接口实现类AccountServiceImpl
//实现最基本的查询操作
//注:这里可以根据邮箱或用户名进行登录,其中username仅代表Security的User属性,不一定是用户名,不可当用户名传输
@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = findByNameOrEmail(username);
if(account == null)
throw new UsernameNotFoundException("用户名或密码错误");
return User
.withUsername(username)
.password(account.getPassword())
.roles(account.getRole())
.build();
}
public Account findByNameOrEmail(String text) {
return this.query()
.eq("username", text).or()
.eq("email", text)
.one();
}
}
最后重写SecurityConfiguration
内的onAuthenticationSuccess
中写死部分
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
response.setContentType("application/json;charset=utf-8");
//获取用户详细信息 Security的User
User user = (User) authentication.getPrincipal();
Account account = service.findByNameOrEmail(user.getUsername());
String token = jwtUtils.createJwt(user, account.getId(), account.getUsername());
AuthorizeVO vo = new AuthorizeVO();
vo.setExpire(jwtUtils.expireTime());
vo.setRole(account.getRole());
vo.setToken(token);
vo.setUsername(account.getUsername());
response.getWriter().write(RestBean.success(vo).toJsonString());
}
完成数据库校验
解决跨域问题
跨域问题是因为一些安全机制为了抵抗部分网络攻击而指定的保护措施产生的,就是只允许访问本IP和端口
下的资源
报错如下:
只需在后端添加一个解决跨域的拦截器,让他对我们前端的请求中写入一些跨域的信息
filter/CorsFilter
@Component
@Order(Const.ORDER_CORS)
public class CorsFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
this.addCorsHeader(request, response);
//所有请求直接放行就行,只需要在response加跨域信息
chain.doFilter(request, response);
}
public void addCorsHeader(HttpServletRequest request,
HttpServletResponse response) {
response.addHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.addHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
}
}
//在Const常量中添加
public static final int ORDER_CORS = -102 ;
后端发送验证邮件
<!-- 依赖 邮件和消息队列-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
# 邮件和消息队列配置
spring:
mail:
host: smtp.163.com
username:
password:
rabbitmq:
addresses: localhost
username: guest
passward: guest
配置消息队列
创建配置类config/RabbitConfiguration
@Configuration
public class RabbitConfiguration {
@Bean("emailQueue")
public Queue emailQueue() {
return QueueBuilder.durable("mail").build();
}
}
在AccountService
接口类中添加方法registerEmailVerifyCode
String registerEmailVerifyCode(String type, String email, String ip);
在其对应实现类中实现impl/AccountServiceImpl
@Resource
FlowUtils flowUtils;
@Resource
AmqpTemplate amqpTemplate;
@Resource
StringRedisTemplate stringRedisTemplate;
@Override
public String registerEmailVerifyCode(String type, String email, String ip) {
//加锁防止同一时间过多请求
synchronized (ip.intern()) {
if (!this.verifyLimit(ip)) {
return "请求频繁,请稍后再试";
}
//生成六位验证码
Random random = new Random();
int code = random.nextInt(899999) + 100000;
Map<String, Object> data = Map.of("type", type, "email", email, "code", code);
amqpTemplate.convertAndSend("mail", data);
stringRedisTemplate.opsForValue()
.set(Const.VERIFY_EMAIL_DATA + email, String.valueOf(code), 3, TimeUnit.MINUTES);
return null;
}
}
private boolean verifyLimit(String ip) {
String key = Const.VERIFY_EMAIL_LIMIT + ip;
return flowUtils.limitOnceCheck(key, 60);
}
Const
内加入相关的常量
public static final String VERIFY_EMAIL_LIMIT = "verify:email:limit:";
public static final String VERIFY_EMAIL_DATA = "verify:email:data:";
FlowUtils
工具类实现工具方法
//基本逻辑就是检查某ip是否在请求发送邮件的冷却期内,如果存在就返回0,不存在就使其CD并返回1```````````
@Component
public class FlowUtils {
@Resource
StringRedisTemplate template;
public boolean limitOnceCheck(String key, int blockTime) {
if (Boolean.TRUE.equals(template.hasKey(key))) {
return false;
} else {
template.opsForValue().set(key, "", blockTime, TimeUnit.SECONDS);
return true;
}
}
}
添加消息队列监听器作为消费者来发送邮件
listener/MailQueueListener
@Component
@RabbitListener(queues = "mail")
public class MailQueueListener {
@Resource
JavaMailSender sender;
@Value("${spring.mail.username}")
String username;
@RabbitHandler
public void sendMailMessage(Map<String, Object> data) {
String email = (String) data.get("email");
Integer code = (Integer) data.get("code");
String type = (String) data.get("type");
SimpleMailMessage message = switch (type) {
case "register" ->
createMessage("欢迎注册MyWebSite",
"您的邮件注册码为:" + code + "有效时间3分钟, 为了你的账号安全,请勿向他人泄露验证码信息!", email);
case "reset" ->
createMessage("MyWebSite密码重置",
"您正在进行密码重置操作,验证码:" + code + ",有效时间3分钟,如非本人操作请无视!",email);
default -> null;
};
if (message == null)
return;
sender.send(message);
}
private SimpleMailMessage createMessage(String title, String content, String email) {
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject(title);
message.setText(content);
message.setTo(email);
message.setFrom(username);
return message;
}
}
在Controller层创造接口
controller/AuthorizeController
@RestController
@RequestMapping("/api/auth")
public class AuthorizeController {
@Resource
AccountService service;
@GetMapping("/ask-code")
public RestBean<Void> askVerifyCode(@RequestParam String email,
@RequestParam String type,
HttpServletRequest request) {
String message = service.registerEmailVerifyCode(type, email, request.getRemoteAddr());
return message == null ? RestBean.success() : RestBean.failure(400, message);
}
}
注册接口
请求参数校验
我们在向接口发送请求的时候,为了安全起见,请求所携带的参数需要被校验限制,不然用户可以提交非法请求参数导致后端邮箱发送失败,引起消息队列消费者持续报错,对服务端有安全威胁!导入依赖后只需要通过相应注解即可完成校验。
<!-- 导入依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在Controller
层处理对请求参数的检验返回的exception
,处理类为controller/exception/ValidationController
//使用SpringBoot同样的Slf4j日志打印,并保持和SpringBoot日志输出格式一致
@Slf4j
@RestControllerAdvice
public class ValidationController {
@ExceptionHandler(ValidationException.class)
public RestBean<Void> validateException(ValidationException exception) {
log.warn("Resolve [{}: {}]", exception.getClass().getName(), exception.getMessage());
return RestBean.failure(400, "请求参数有误");
}
}
对/ask-code
接口检验
@GetMapping("/ask-code")
public RestBean<Void> askVerifyCode(@RequestParam @Email String email,
@RequestParam @Pattern(regexp = "(register | reset)") String type,
HttpServletRequest request) {
return this.messageHandle(() -> service.registerEmailVerifyCode(type, email, request.getRemoteAddr()));
}
封装请求数据的实体类EmailRegisterVO
,并对数据进行校验,前端数据均可封装为此实体类进行请求
@Data
public class EmailRegisterVO {
@Email
String email;
@Length(max = 6, min = 6)
String code;
//用户名不可包含非法字符
@Pattern(regexp = "^[a-zA-Z0-9\\u4e00-\\u9fa5]+$")
@Length(min = 1, max = 15)
String username;
@Length(min = 6, max = 20)
String password;
}
在Service
层实现邮箱注册方法
接口设计方法AccountService
String registerEmailAccount(EmailRegisterVO vo);
具体实现AcountServiceImpl
中的邮箱注册方法
@Override
public String registerEmailAccount(EmailRegisterVO vo) {
String email = vo.getEmail();
String username = vo.getUsername();
String key = Const.VERIFY_EMAIL_DATA + email;
String code = stringRedisTemplate.opsForValue().get(key);
if (code == null) return "请先获取验证码";
if (!code.equals(vo.getCode())) return "验证码输入错误,请重新输入";
if (this.existsAccountByEmail(email)) return "此电子邮箱已被注册!";
if (this.existsAccountByUsername(username)) return "此用户名已被注册!";
String password = encoder.encode(vo.getPassword());
Account account = new Account(null, username, password, email, "user", new Date());
if (this.save(account)) {
stringRedisTemplate.delete(key);
return null;
} else {
return "内部错误,请联系管理员";
}
}
private boolean existsAccountByEmail(String email) {
return this.baseMapper.exists(Wrappers.<Account>query().eq("email", email));
}
private boolean existsAccountByUsername(String username) {
return this.baseMapper.exists(Wrappers.<Account>query().eq("username", username));
}
在处理用户认证的Controller
层controller/AuthorizeController
中实现注册接口
//Post接收JSON格式
@PostMapping("/register")
public RestBean<Void> register(@RequestBody @Valid EmailRegisterVO vo) {
return this.messageHandle(() -> service.registerEmailAccount(vo));
}
private RestBean<Void> messageHandle(Supplier<String> action) {
String message = action.get();
return message == null ? RestBean.success() : RestBean.failure(400, message);
}
对于数据处理格式,因为Axios
返回的为JSON
,所以我习惯使用POST
传输JSON
格式数据,习惯对GET
以参数传输交互数据
接口测试
获取验证码
使用验证码注册
成功注册!
成功登录!
其他错误测试均生效
忘记密码接口
结合前端页面的设计,忘记密码的接口设计的实现逻辑也分为两步。
第一步,前端向后端请求验证码后填写表单向后端发送请求,后端对验证码进行检验。
第二步,前端向后端发送用户填写好的表单并携带验证码请求后端,后端校验后执行重置密码的操作。
首先封装两个前端向后端发起请求的实体类,均放在entity/vo/request
邮箱校验确认重置密码的请求实体类ConfirmResetVO
@Data
@AllArgsConstructor
public class ConfirmResetVO {
@Email
String email;
@Length(max = 6, min = 6)
String code;
}
执行重置密码操作的请求实体类EmailResetVO
@Data
@AllArgsConstructor
public class EmailResetVO {
@Email
String email;
@Length(max = 6, min = 6)
String code;
@Length(min = 5, max = 20)
String password;
}
分别实现这两个步骤的Service
方法,AccountService
接口声明方法
String resetConfirm(ConfirmResetVO vo);
String resetEmailAccountPassword(EmailResetVO vo);
AccountServiceImpl
实现类实现方法
@Override
public String resetConfirm(ConfirmResetVO vo) {
String email = vo.getEmail();
String code = stringRedisTemplate.opsForValue().get(Const.VERIFY_EMAIL_DATA + email);
if(code == null) return "请先获取验证码";
if(!code.equals(vo.getCode())) return "验证码错误,请重新输入";
return null;
}
@Override
public String resetEmailAccountPassword(EmailResetVO vo) {
String email = vo.getEmail();
String verify = this.resetConfirm(new ConfirmResetVO(vo.getEmail(), vo.getCode()));
if(verify != null) return verify;
String password = encoder.encode(vo.getPassword());
boolean update = this.update().eq("email", email).set("password", password).update();
if(update) {
stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email);
}
return null;
}
Controller
层实现接口
@PostMapping("/rest-confirm")
public RestBean<Void> resetConfirm(@RequestBody @Valid ConfirmResetVO vo) {
return this.messageHandle(() -> service.resetConfirm(vo));
}
@PostMapping("/reset-password")
public RestBean<Void> resetPassword(@RequestBody @Valid EmailResetVO vo) {
return this.messageHandle(() -> service.resetEmailAccountPassword(vo));
}
接口测试通过
实现限流
基本思路是在跨域拦截器之后添加一个限流拦截器,每次请求通过都会对ip
进行请求计数,如果请求次数过多,会被记录至Redis
进行封禁达到限流目的
Const
类中添加两个常量
public static final String FLOW_LIMIT_COUNTER = "flow:counter:";
public static final String FLOW_LIMIT_BLOCK = "flow:block:";
实现限流拦截器FlowLimitFilter
@Component
@Order(Const.ORDER_LIMIT)
public class FlowLimitFilter extends HttpFilter {
@Resource
StringRedisTemplate template;
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String address = request.getRemoteAddr();
if (this.tryCount(address)) {
chain.doFilter(request, response);
} else {
this.writeBlocakMessage(response);
}
}
private void writeBlocakMessage(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(RestBean.forbidden("操作频繁,请稍后再试").toJsonString());
}
private boolean tryCount(String ip) {
synchronized (ip.intern()) {
if (Boolean.TRUE.equals(template.hasKey(Const.FLOW_LIMIT_BLOCK + ip))) {
//已被BAN
return false;
}
return this.limitPeriodCheck(ip);
}
}
private boolean limitPeriodCheck(String ip) {
if (Boolean.TRUE.equals(template.hasKey(Const.FLOW_LIMIT_COUNTER + ip))) {
long increment = Optional.ofNullable(template.opsForValue().increment(Const.FLOW_LIMIT_COUNTER + ip)).orElse(0L);
if(increment > 10) {
template.opsForValue().set(Const.FLOW_LIMIT_BLOCK + ip, "", 30, TimeUnit.SECONDS);
return false;
}
} else {
template.opsForValue().set(Const.FLOW_LIMIT_COUNTER + ip, "1", 3, TimeUnit.SECONDS);
}
return true;
}
}
对测试接口进行压测,限流成功